30 changed files with 969 additions and 106 deletions
@ -0,0 +1,44 @@ |
|||
<?php |
|||
|
|||
namespace App\Exports; |
|||
|
|||
use App\Models\Puesto; |
|||
use Maatwebsite\Excel\Concerns\FromCollection; |
|||
use Maatwebsite\Excel\Concerns\WithHeadings; |
|||
|
|||
class PuestosExport implements FromCollection, WithHeadings |
|||
{ |
|||
protected $puestos; |
|||
|
|||
// Permite pasar una colección personalizada si lo deseas |
|||
public function __construct($puestos = null) |
|||
{ |
|||
$this->puestos = $puestos; |
|||
} |
|||
|
|||
public function collection() |
|||
{ |
|||
// Obtén los puestos |
|||
$puestos = $this->puestos ?: Puesto::where('eliminado', 0)->get(['nombre']); |
|||
|
|||
// Construye la colección con número consecutivo |
|||
$data = []; |
|||
$contador = 1; |
|||
foreach ($puestos as $puesto) { |
|||
$data[] = [ |
|||
'ID' => $contador++, |
|||
'Nombre del Puesto' => $puesto->nombre, |
|||
]; |
|||
} |
|||
|
|||
return collect($data); |
|||
} |
|||
|
|||
public function headings(): array |
|||
{ |
|||
return [ |
|||
'ID', |
|||
'Nombre del Puesto', |
|||
]; |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use Illuminate\Http\Request; |
|||
|
|||
class AdminController extends Controller |
|||
{ |
|||
public function dashboard() |
|||
{ |
|||
return view('admin.dashboard'); |
|||
} |
|||
} |
@ -0,0 +1,128 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use App\Models\Puesto; |
|||
use Illuminate\Http\Request; |
|||
use App\Exports\PuestosExport; |
|||
use Maatwebsite\Excel\Facades\Excel; |
|||
use PDF; |
|||
|
|||
class PuestoController extends Controller |
|||
{ |
|||
public function index(Request $request) |
|||
{ |
|||
$busqueda = $request->busqueda; |
|||
|
|||
if ($busqueda) { |
|||
$puestos = Puesto::where('nombre', 'LIKE', "%{$busqueda}%") |
|||
->where('eliminado', 0) |
|||
->get(); |
|||
|
|||
if ($puestos->isEmpty()) { |
|||
return redirect()->route('puestos.index') |
|||
->with('error', 'No existe ningún puesto con el nombre "' . $busqueda . '". Por favor, inténtalo de nuevo.'); |
|||
} |
|||
} else { |
|||
$puestos = Puesto::where('eliminado', 0)->get(); |
|||
} |
|||
|
|||
return view('puestos', ['puestos' => $puestos]); |
|||
} |
|||
|
|||
public function create() |
|||
{ |
|||
return view('puestosCrearEditar', ['puesto' => null]); |
|||
} |
|||
|
|||
public function store(Request $request) |
|||
{ |
|||
$validated = $request->validate([ |
|||
'nombre' => ['required', 'string', 'max:255'], |
|||
], [ |
|||
'nombre.required' => 'El campo nombre es obligatorio.', |
|||
'nombre.string' => 'El campo nombre debe ser una cadena de texto.', |
|||
'nombre.max' => 'El campo nombre no debe exceder 255 caracteres.', |
|||
]); |
|||
$puesto = new Puesto($validated); |
|||
$puesto->eliminado = 0; |
|||
$puesto->save(); |
|||
|
|||
return redirect()->route('puestos.index') |
|||
->with('success', 'Puesto creado exitosamente'); |
|||
} |
|||
|
|||
|
|||
public function edit($id) |
|||
{ |
|||
$puesto = Puesto::findOrFail($id); |
|||
return view('puestosCrearEditar', ['puesto' => $puesto]); |
|||
} |
|||
|
|||
public function update(Request $request, $id) |
|||
{ |
|||
$validated = $request->validate([ |
|||
'nombre' => ['required', 'string', 'max:255'], |
|||
], [ |
|||
'nombre.required' => 'El campo nombre es obligatorio.', |
|||
'nombre.string' => 'El campo nombre debe ser una cadena de texto.', |
|||
'nombre.max' => 'El campo nombre no debe exceder 255 caracteres.', |
|||
]); |
|||
|
|||
$puesto = Puesto::findOrFail($id); |
|||
$puesto->update($validated); |
|||
|
|||
return redirect()->route('puestos.index') |
|||
->with('success', 'Puesto actualizado exitosamente'); |
|||
} |
|||
|
|||
public function destroy($id) |
|||
{ |
|||
$puesto = Puesto::findOrFail($id); |
|||
\App\Models\User::where('puesto_id', $id)->update(['puesto_id' => null]); |
|||
$puesto->eliminado = 1; |
|||
$puesto->save(); |
|||
|
|||
return redirect()->route('puestos.index') |
|||
->with('success', 'Puesto eliminado exitosamente. Los usuarios afectados necesitan ser reasignados.'); |
|||
} |
|||
|
|||
public function exportExcel() |
|||
{ |
|||
return Excel::download(new PuestosExport, 'puestos.xlsx'); |
|||
} |
|||
|
|||
public function exportPDF() |
|||
{ |
|||
$puestos = Puesto::where('eliminado', 0)->get(); |
|||
$pdf = PDF::loadView('exports.puestos', ['puestos' => $puestos]); |
|||
return $pdf->download('puestos.pdf'); |
|||
} |
|||
|
|||
public function export($format) |
|||
{ |
|||
$puestos = Puesto::where('eliminado', 0) |
|||
->orderBy('updated_at', 'desc') |
|||
->get(); |
|||
|
|||
switch($format) { |
|||
case 'excel': |
|||
return Excel::download(new PuestosExport($puestos), 'puestos.xlsx'); |
|||
case 'pdf': |
|||
$pdf = PDF::loadView('exports.puestos', ['puestos' => $puestos]); |
|||
return $pdf->download('puestos.pdf'); |
|||
default: |
|||
return redirect()->back()->with('error', 'Formato no soportado'); |
|||
} |
|||
} |
|||
|
|||
public function toggleStatus($id) |
|||
{ |
|||
$puesto = Puesto::findOrFail($id); |
|||
$puesto->eliminado = !$puesto->eliminado; |
|||
$puesto->save(); |
|||
|
|||
return redirect()->route('puestos.index') |
|||
->with('success', 'Estado del puesto actualizado correctamente'); |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use App\Models\tipo; |
|||
use Illuminate\Http\Request; |
|||
|
|||
class TipoController extends Controller |
|||
{ |
|||
/** |
|||
* Display a listing of the resource. |
|||
*/ |
|||
public function index() |
|||
{ |
|||
// |
|||
} |
|||
|
|||
/** |
|||
* Show the form for creating a new resource. |
|||
*/ |
|||
public function create() |
|||
{ |
|||
// |
|||
} |
|||
|
|||
/** |
|||
* Store a newly created resource in storage. |
|||
*/ |
|||
public function store(Request $request) |
|||
{ |
|||
// |
|||
} |
|||
|
|||
/** |
|||
* Display the specified resource. |
|||
*/ |
|||
public function show(tipo $tipo) |
|||
{ |
|||
// |
|||
} |
|||
|
|||
/** |
|||
* Show the form for editing the specified resource. |
|||
*/ |
|||
public function edit(tipo $tipo) |
|||
{ |
|||
// |
|||
} |
|||
|
|||
/** |
|||
* Update the specified resource in storage. |
|||
*/ |
|||
public function update(Request $request, tipo $tipo) |
|||
{ |
|||
// |
|||
} |
|||
|
|||
/** |
|||
* Remove the specified resource from storage. |
|||
*/ |
|||
public function destroy(tipo $tipo) |
|||
{ |
|||
// |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Middleware; |
|||
|
|||
use Closure; |
|||
use Illuminate\Http\Request; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
class AdminMiddleware |
|||
{ |
|||
/** |
|||
* Handle an incoming request. |
|||
* |
|||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next |
|||
*/ |
|||
public function handle(Request $request, Closure $next): Response |
|||
{ |
|||
if (!auth()->check() || auth()->user()->tipo->nombre !== 'Administrador') { |
|||
return redirect('/')->with('error', 'No tienes permisos para acceder a esta página.'); |
|||
} |
|||
|
|||
return $next($request); |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
<?php |
|||
|
|||
namespace App\Models; |
|||
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory; |
|||
use Illuminate\Database\Eloquent\Model; |
|||
|
|||
class tipo extends Model |
|||
{ |
|||
use HasFactory; |
|||
protected $table = 'tipos'; |
|||
protected $fillable = ['nombre']; |
|||
} |
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
namespace Database\Factories; |
|||
|
|||
use Illuminate\Database\Eloquent\Factories\Factory; |
|||
|
|||
/** |
|||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\tipo> |
|||
*/ |
|||
class TipoFactory extends Factory |
|||
{ |
|||
/** |
|||
* Define the model's default state. |
|||
* |
|||
* @return array<string, mixed> |
|||
*/ |
|||
public function definition(): array |
|||
{ |
|||
return [ |
|||
// |
|||
]; |
|||
} |
|||
} |
@ -0,0 +1,34 @@ |
|||
<?php |
|||
|
|||
use Illuminate\Database\Migrations\Migration; |
|||
use Illuminate\Database\Schema\Blueprint; |
|||
use Illuminate\Support\Facades\Schema; |
|||
|
|||
return new class extends Migration |
|||
{ |
|||
/** |
|||
* Run the migrations. |
|||
*/ |
|||
public function up(): void |
|||
{ |
|||
Schema::create('tipos', function (Blueprint $table) { |
|||
$table->id(); |
|||
$table->string('nombre'); |
|||
$table->timestamps(); |
|||
|
|||
}); |
|||
DB::table('tipos')->insert(['nombre'=> 'Administrador']); |
|||
DB::table('tipos')->insert(['nombre'=> 'Usuario']); |
|||
DB::table('tipos')->insert(['nombre'=> 'Jefe de Departamento']); |
|||
DB::table('tipos')->insert(['nombre'=> 'Servicios Generales']); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* Reverse the migrations. |
|||
*/ |
|||
public function down(): void |
|||
{ |
|||
Schema::dropIfExists('tipos'); |
|||
} |
|||
}; |
@ -0,0 +1,53 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<title>Reporte de Puestos</title> |
|||
<style> |
|||
body { |
|||
font-family: Arial, sans-serif; |
|||
} |
|||
table { |
|||
width: 100%; |
|||
border-collapse: collapse; |
|||
margin-top: 20px; |
|||
} |
|||
th, td { |
|||
border: 1px solid #ddd; |
|||
padding: 8px; |
|||
text-align: left; |
|||
} |
|||
th { |
|||
background-color: #f2f2f2; |
|||
} |
|||
.header { |
|||
text-align: center; |
|||
margin-bottom: 30px; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="header"> |
|||
<h1>Reporte de Puestos</h1> |
|||
<p>Fecha de generación: {{ date('d/m/Y H:i:s') }}</p> |
|||
</div> |
|||
|
|||
<table> |
|||
<thead> |
|||
<tr> |
|||
<th>#</th> |
|||
<th>Nombre del Puesto</th> |
|||
|
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@foreach($puestos as $index => $puesto) |
|||
<tr> |
|||
<td>{{ $index + 1 }}</td> |
|||
<td>{{ $puesto->nombre }}</td> |
|||
|
|||
</tr> |
|||
@endforeach |
|||
</tbody> |
|||
</table> |
|||
</body> |
|||
</html> |
@ -0,0 +1,142 @@ |
|||
@extends('layouts.dashboard') |
|||
|
|||
@section('content') |
|||
<div class="container mx-auto px-4 py-6"> |
|||
<!-- Mensajes de éxito y error --> |
|||
@if(session('success')) |
|||
<div id="success-message" class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4" role="alert"> |
|||
<span class="block sm:inline">{{ session('success') }}</span> |
|||
</div> |
|||
@endif |
|||
|
|||
@if(session('error')) |
|||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert"> |
|||
<span class="block sm:inline">{{ session('error') }}</span> |
|||
</div> |
|||
@endif |
|||
|
|||
<div class="bg-white rounded-lg shadow-lg"> |
|||
<!-- Encabezado con título y botones de acción --> |
|||
<div class="p-4 border-b border-gray-200 flex justify-between items-center"> |
|||
<h2 class="text-2xl font-bold">Gestión de Puestos</h2> |
|||
<div class="flex items-center space-x-6"> |
|||
<!-- Íconos de agregar --> |
|||
<div class="flex space-x-4"> |
|||
<a href="{{ route('puestos.excel') }}" |
|||
class="text-green-600 hover:text-green-700 transition-colors duration-200" |
|||
title="Exportar a Excel"> |
|||
<i class="fas fa-file-excel text-xl"></i> |
|||
</a> |
|||
|
|||
<a href="{{ route('puestos.pdf') }}" |
|||
class="text-red-600 hover:text-red-700 transition-colors duration-200" |
|||
title="Exportar a PDF"> |
|||
<i class="fas fa-file-pdf text-xl"></i> |
|||
</a> |
|||
<!-- Agregar nuevo puesto --> |
|||
<a href="{{ route('puestos.create') }}" |
|||
class="text-blue-500 hover:text-blue-600 transition-colors duration-200" |
|||
title="Agregar nuevo puesto"> |
|||
<i class="fas fa-plus text-xl"></i> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Barra de búsqueda --> |
|||
<div class="p-4 border-b border-gray-200 bg-gray-50"> |
|||
<form action="{{ route('puestos.index') }}" method="GET" class="flex gap-2"> |
|||
<div class="relative w-full sm:w-64"> |
|||
<input type="text" |
|||
name="busqueda" |
|||
placeholder="Buscar puesto..." |
|||
value="{{ request('busqueda') }}" |
|||
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> |
|||
<div class="absolute left-3 top-2.5 text-gray-400"> |
|||
<i class="fas fa-search"></i> |
|||
</div> |
|||
</div> |
|||
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"> |
|||
Buscar |
|||
</button> |
|||
@if(request('busqueda')) |
|||
<a href="{{ route('puestos.index') }}" class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600"> |
|||
Limpiar |
|||
</a> |
|||
@endif |
|||
</form> |
|||
</div> |
|||
|
|||
<!-- Tabla de puestos --> |
|||
<div class="overflow-x-auto"> |
|||
<table class="min-w-full divide-y divide-gray-200"> |
|||
<thead class="bg-gray-50"> |
|||
<tr> |
|||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Número</th> |
|||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Puesto</th> |
|||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody class="bg-white divide-y divide-gray-200"> |
|||
@foreach($puestos as $index => $puesto) |
|||
<tr class="hover:bg-gray-50"> |
|||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $index + 1 }}</td> |
|||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> |
|||
<i class="fas fa-briefcase text-blue-500 mr-2"></i> |
|||
{{ $puesto->nombre }} |
|||
</td> |
|||
<td class="flex space-x-2 px-6 py-4 whitespace-nowrap text-sm"> |
|||
<a href="{{ route('puestos.edit', $puesto->id) }}" |
|||
class="text-yellow-600 hover:text-yellow-700 transition-colors duration-200" |
|||
title="Editar puesto"> |
|||
<i class="fas fa-edit"></i> |
|||
</a> |
|||
<form action="{{ route('puestos.destroy', $puesto->id) }}" method="POST" class="d-inline"> |
|||
@csrf |
|||
@method('DELETE') |
|||
<a href="#" onclick="event.preventDefault(); confirmarEliminacion(this);" |
|||
class="text-red-600 hover:text-red-700 transition-colors duration-200" |
|||
title="Eliminar puesto"> |
|||
<i class="fas fa-trash"></i> |
|||
</a> |
|||
</form> |
|||
</td> |
|||
</tr> |
|||
@endforeach |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<script> |
|||
// Desaparecer el mensaje después de 3 segundos |
|||
setTimeout(function() { |
|||
var message = document.getElementById('success-message'); |
|||
if (message) { |
|||
message.style.transition = 'opacity 0.5s ease'; |
|||
message.style.opacity = '0'; |
|||
setTimeout(function() { |
|||
message.remove(); |
|||
}, 500); |
|||
} |
|||
}, 3000); |
|||
|
|||
function confirmarEliminacion(button) { |
|||
Swal.fire({ |
|||
title: '¿Estás seguro?', |
|||
text: "Esta acción no se puede deshacer", |
|||
icon: 'warning', |
|||
showCancelButton: true, |
|||
confirmButtonColor: '#3085d6', |
|||
cancelButtonColor: '#d33', |
|||
confirmButtonText: 'Sí, eliminar', |
|||
cancelButtonText: 'Cancelar' |
|||
}).then((result) => { |
|||
if (result.isConfirmed) { |
|||
button.closest('form').submit(); |
|||
} |
|||
}); |
|||
} |
|||
</script> |
|||
@endsection |
@ -0,0 +1,84 @@ |
|||
@extends('layouts.dashboard') |
|||
|
|||
@section('content') |
|||
<div class="container mx-auto px-4 py-6"> |
|||
<div class="max-w-lg mx-auto"> |
|||
<div class="bg-white rounded-lg shadow-lg overflow-hidden"> |
|||
<div class="p-6"> |
|||
<!-- Encabezado del formulario --> |
|||
<div class="flex items-center justify-between mb-6"> |
|||
<h2 class="text-2xl font-bold text-gray-800"> |
|||
{{ isset($puesto) ? 'Editar Puesto' : 'Nuevo Puesto' }} |
|||
</h2> |
|||
<div class="h-10 w-10 bg-blue-100 rounded-full flex items-center justify-center"> |
|||
<i class="fas fa-briefcase text-blue-600"></i> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Mensajes de error --> |
|||
@if($errors->any()) |
|||
<div class="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded-r-lg"> |
|||
<div class="flex items-center"> |
|||
<i class="fas fa-exclamation-circle text-red-500 mr-3"></i> |
|||
<div class="text-red-700"> |
|||
<ul> |
|||
@foreach($errors->all() as $error) |
|||
<li>{{ $error }}</li> |
|||
@endforeach |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@endif |
|||
<!-- Formulario --> |
|||
<form id="puestoForm" |
|||
action="{{ isset($puesto) ? route('puestos.update', $puesto->id) : route('puestos.store') }}" |
|||
method="POST"> |
|||
@csrf |
|||
@if(isset($puesto)) |
|||
@method('PUT') |
|||
@endif |
|||
|
|||
<div class="space-y-6"> |
|||
<!-- Campo Nombre --> |
|||
<div> |
|||
<label for="nombre" class="block text-sm font-medium text-gray-700 mb-2"> |
|||
Nombre del Puesto |
|||
</label> |
|||
<div class="relative rounded-md shadow-sm"> |
|||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> |
|||
<i class="fas fa-tag text-gray-400"></i> |
|||
</div> |
|||
<input type="text" |
|||
name="nombre" |
|||
id="nombre" |
|||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|||
required |
|||
placeholder="Ingresa el nombre del puesto" |
|||
value="{{ isset($puesto) ? $puesto->nombre : old('nombre') }}"> |
|||
</div> |
|||
@error('nombre') |
|||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> |
|||
@enderror |
|||
</div> |
|||
|
|||
|
|||
|
|||
<!-- Botones de acción --> |
|||
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200"> |
|||
<a href="{{ route('puestos.index') }}" |
|||
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
|||
Cancelar |
|||
</a> |
|||
<button type="submit" |
|||
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
|||
{{ isset($puesto) ? 'Actualizar' : 'Guardar' }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@endsection |
Loading…
Reference in new issue