diff --git a/app/Http/Controllers/Admin/EmployeeController.php b/app/Http/Controllers/Admin/EmployeeController.php new file mode 100644 index 0000000..6eee130 --- /dev/null +++ b/app/Http/Controllers/Admin/EmployeeController.php @@ -0,0 +1,88 @@ +orderBy('name')->get(); + return view('admin.employees.index', compact('employees')); + } + + /** + * Crear un nuevo empleado + */ + public function store(Request $request) + { + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + + $user = new User(); + $user->name = $request->name; + $user->email = $request->email; + $user->password = Hash::make($request->password); + $user->role = 'employee'; + $user->save(); + + return redirect()->route('admin.employees.index')->with('success', 'Empleado creado correctamente.'); + } + + /** + * Eliminar un empleado + */ + public function destroy(User $user) + { + // Evitar que se elimine a sí mismo o a otro administrador (seguridad adicional) + if ($user->role !== 'employee') { + return redirect()->route('admin.employees.index')->with('error', 'Solo se pueden eliminar empleados.'); + } + + if ($user->id === auth()->id()) { + return redirect()->route('admin.employees.index')->with('error', 'No puedes eliminar tu propia cuenta.'); + } + + $user->delete(); + + return redirect()->route('admin.employees.index')->with('success', 'Empleado eliminado correctamente.'); + } + + /** + * Actualizar datos de un empleado + */ + public function update(Request $request, User $user) + { + if ($user->role !== 'employee') { + return redirect()->route('admin.employees.index')->with('error', 'Solo se pueden editar empleados.'); + } + + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id], + 'password' => ['nullable', 'string', 'min:8', 'confirmed'], + ]); + + $user->name = $request->name; + $user->email = $request->email; + + if ($request->filled('password')) { + $user->password = Hash::make($request->password); + } + + $user->save(); + + return redirect()->route('admin.employees.index')->with('success', 'Empleado actualizado correctamente.'); + } +} diff --git a/app/Http/Controllers/AgendaController.php b/app/Http/Controllers/AgendaController.php index eea2e3c..e8b9b5a 100644 --- a/app/Http/Controllers/AgendaController.php +++ b/app/Http/Controllers/AgendaController.php @@ -19,8 +19,14 @@ class AgendaController extends Controller 'title' => $appointment->scheduled_at->format('H:i') . ' - ' . $appointment->client->name, 'start' => $appointment->scheduled_at->format('Y-m-d\TH:i:s'), 'extendedProps' => [ - 'description' => $appointment->problem_description, - 'status' => $appointment->status, + 'client_name' => $appointment->client->name, + 'client_phone' => $appointment->contact_phone, + 'bike_model' => $appointment->bike_model, + 'description' => $appointment->problem_description, + 'parts_needed' => $appointment->parts_needed, + 'estimated_cost'=> $appointment->estimated_cost, + 'status' => $appointment->status, + 'notes' => $appointment->notes, ], ]; } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index cb8e726..8df88c5 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -7,6 +7,13 @@ use App\Models\Client; class ClientController extends Controller { + private function checkAdmin() + { + if (auth()->check() && auth()->user()->role !== 'admin') { + abort(403, 'Solo los administradores pueden realizar esta acción.'); + } + } + /** * Display a listing of the resource. */ @@ -31,6 +38,7 @@ class ClientController extends Controller */ public function create() { + $this->checkAdmin(); return view('clients.create'); } @@ -39,6 +47,7 @@ class ClientController extends Controller */ public function store(Request $request) { + $this->checkAdmin(); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255', 'not_regex:/[0-9]/'], 'phone' => 'nullable|string|max:50', @@ -76,19 +85,21 @@ class ClientController extends Controller /** * Display the specified resource. */ - public function show(string $id) + public function show(Client $client) { - // + return view('clients.show', compact('client')); } public function edit(Client $client) { + $this->checkAdmin(); // Reutilizamos la vista de create, o creamos una edit.blade.php similar return view('clients.edit', compact('client')); } public function update(Request $request, Client $client) { + $this->checkAdmin(); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255', 'not_regex:/[0-9]/'], 'phone' => 'nullable|string|max:50', @@ -109,6 +120,7 @@ class ClientController extends Controller */ public function destroy(Client $client) { + $this->checkAdmin(); try { $client->delete(); return redirect()->route('clients.index')->with('success', 'Cliente eliminado correctamente.'); diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index c128252..64633de 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -8,6 +8,13 @@ use App\Models\Supplier; class ExpenseController extends Controller { + private function checkAdmin() + { + if (auth()->check() && auth()->user()->role !== 'admin') { + abort(403, 'Solo los administradores pueden realizar esta acción.'); + } + } + public function index(Request $request) { $query = $request->input('search'); @@ -33,12 +40,14 @@ class ExpenseController extends Controller public function create() { + $this->checkAdmin(); $suppliers = Supplier::orderBy('name')->get(); return view('expenses.create', compact('suppliers')); } public function store(Request $request) { + $this->checkAdmin(); $validated = $request->validate([ 'supplier_id' => 'nullable|exists:suppliers,id', 'description' => 'required|string|max:255', @@ -66,12 +75,14 @@ class ExpenseController extends Controller public function edit(Expense $expense) { + $this->checkAdmin(); $suppliers = Supplier::orderBy('name')->get(); return view('expenses.edit', compact('expense', 'suppliers')); } public function update(Request $request, Expense $expense) { + $this->checkAdmin(); $validated = $request->validate([ 'supplier_id' => 'nullable|exists:suppliers,id', 'description' => 'required|string|max:255', @@ -90,6 +101,7 @@ class ExpenseController extends Controller public function destroy(Expense $expense) { + $this->checkAdmin(); $expense->delete(); return redirect()->route('expenses.index')->with('success', 'Gasto eliminado correctamente.'); } diff --git a/app/Http/Controllers/ProductosController.php b/app/Http/Controllers/ProductosController.php index 496f05b..a3df1ac 100644 --- a/app/Http/Controllers/ProductosController.php +++ b/app/Http/Controllers/ProductosController.php @@ -11,6 +11,13 @@ use Illuminate\Support\Facades\Storage; class ProductosController extends Controller { + private function checkAdmin() + { + if (auth()->check() && auth()->user()->role !== 'admin') { + abort(403, 'Solo los administradores pueden realizar esta acción.'); + } + } + /*public function index(Request $request) { $query = $request->input('search'); @@ -55,6 +62,7 @@ class ProductosController extends Controller public function create() { + $this->checkAdmin(); $suppliers = Supplier::orderBy('name')->get(); $tags = Tag::orderBy('name')->get(); return view('productos.create', compact('suppliers', 'tags')); @@ -62,6 +70,7 @@ class ProductosController extends Controller public function store(Request $request) { + $this->checkAdmin(); $validated = $request->validate([ 'name' => 'required|string|max:255', 'price' => 'required|numeric|min:0', @@ -124,6 +133,7 @@ class ProductosController extends Controller public function edit(Product $product) { + $this->checkAdmin(); $suppliers = Supplier::orderBy('name')->get(); $tags = Tag::orderBy('name')->get(); return view('productos.edit', compact('product', 'suppliers', 'tags')); @@ -131,6 +141,7 @@ class ProductosController extends Controller public function update(Request $request, Product $product) { + $this->checkAdmin(); $validated = $request->validate([ 'name' => 'required|string|max:255', 'type' => 'required|in:bike,accessory,clothing,spare,service,children,skate,rollers,other', @@ -194,6 +205,7 @@ class ProductosController extends Controller public function destroy(Product $product) { + $this->checkAdmin(); if ($product->image_path) { Storage::disk('public')->delete($product->image_path); } diff --git a/app/Http/Controllers/SupplierController.php b/app/Http/Controllers/SupplierController.php index 7938f8c..8e0ef12 100644 --- a/app/Http/Controllers/SupplierController.php +++ b/app/Http/Controllers/SupplierController.php @@ -4,6 +4,13 @@ use Illuminate\Http\Request; use App\Models\Supplier; class SupplierController extends Controller { + private function checkAdmin() + { + if (auth()->check() && auth()->user()->role !== 'admin') { + abort(403, 'Solo los administradores pueden realizar esta acción.'); + } + } + /** * grilla de proveedores con buscador */ @@ -27,12 +34,14 @@ class SupplierController extends Controller */ public function create() { + $this->checkAdmin(); return view('suppliers.create'); } public function store(Request $request) { + $this->checkAdmin(); $validated = $request->validate([ 'name' => 'required|string|max:255', 'phone' => 'nullable|string|max:50', @@ -56,11 +65,13 @@ class SupplierController extends Controller public function edit(Supplier $supplier) { + $this->checkAdmin(); return view('suppliers.edit', compact('supplier')); } public function update(Request $request, Supplier $supplier) { + $this->checkAdmin(); $validated = $request->validate([ 'name' => 'required|string|max:255', 'phone' => 'nullable|string|max:50', @@ -79,6 +90,7 @@ class SupplierController extends Controller */ public function destroy(Supplier $supplier) { + $this->checkAdmin(); try { $supplier->delete(); return redirect()->route('suppliers.index') diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 4e9652f..426c8fb 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -21,14 +21,5 @@ class DatabaseSeeder extends Seeder 'password' => bcrypt('password'), // Cambiar en producción 'role' => 'admin', ]); - - //Crear un Empleado de prueba - User::factory()->create([ - 'name' => 'Empleado Test', - 'email' => 'taller@lauck.com', - 'password' => bcrypt('password'), - 'role' => 'employee', - ]); - } } diff --git a/resources/js/app.js b/resources/js/app.js index c207710..6d8e271 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -45,43 +45,53 @@ document.addEventListener('DOMContentLoaded', function () { window.location.href = `/agenda/dia/${fecha}` }, */ - eventClick: function (info) { + const evento = info.event; + const modal = document.getElementById("eventModal"); + if (!modal) return; - const evento = info.event + modal.classList.remove("hidden"); + document.body.style.overflow = "hidden"; - const modal = document.getElementById("eventModal") - if (!modal) return + // Llenar campos dinámicos + document.getElementById("modalClientName").innerText = evento.extendedProps.client_name ?? 'Sin asignar'; + document.getElementById("modalBikeModel").innerText = evento.extendedProps.bike_model ?? 'Sin modelo'; - modal.classList.remove("hidden") - - const statusMap = { - pending: 'Pendiente', - confirmed: 'Confirmado', - in_progress: 'En progreso', - ready: 'Listo', - delivered: 'Entregado' - } - - const estado = statusMap[evento.extendedProps.status] ?? 'desconocido' - - document.getElementById("modalTitle").innerText = - evento.extendedProps.description + " - " + estado - - const fecha = evento.start - - const hora = fecha.toLocaleTimeString("es-AR", { - hour: "2-digit", - minute: "2-digit", + // Formatear Fecha y Hora + const fecha = evento.start; + const dateStr = fecha.toLocaleDateString("es-AR", { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + }); + const timeStr = fecha.toLocaleTimeString("es-AR", { + hour: '2-digit', + minute: '2-digit', hour12: false - }) + }); + const formattedDateTime = dateStr.charAt(0).toUpperCase() + dateStr.slice(1) + " a las " + timeStr; + document.getElementById("modalDateTime").innerText = formattedDateTime; - document.getElementById("modalDate").innerText = - "Hora: " + hora + const url = window.appointmentShowTemplate.replace(':id', evento.id); + document.getElementById("viewEventBtn").href = url; - const url = window.appointmentShowTemplate.replace(':id', evento.id) - - document.getElementById("viewEventBtn").href = url + // Mapear Badge de Estado y clases correspondientes (estilo pastel idéntico a pendiente) + const statusMap = { + pending: { label: 'Pendiente', classes: ['bg-red-100', 'text-red-800', 'border-red-200', 'dark:bg-red-900/30', 'dark:text-red-400', 'dark:border-red-800'] }, + confirmed: { label: 'Confirmado', classes: ['bg-blue-100', 'text-blue-800', 'border-blue-200', 'dark:bg-blue-900/30', 'dark:text-blue-400', 'dark:border-blue-800'] }, + in_progress: { label: 'En progreso', classes: ['bg-orange-100', 'text-orange-800', 'border-orange-200', 'dark:bg-orange-950/30', 'dark:text-orange-400', 'dark:border-orange-800'] }, + ready: { label: 'Listo', classes: ['bg-green-100', 'text-green-800', 'border-green-200', 'dark:bg-green-900/30', 'dark:text-green-450', 'dark:border-green-800'] }, + delivered: { label: 'Entregado', classes: ['bg-neutral-100', 'text-neutral-800', 'border-neutral-200', 'dark:bg-neutral-800', 'dark:text-neutral-400', 'dark:border-neutral-700'] } + }; + const statusObj = statusMap[evento.extendedProps.status] || { label: 'Desconocido', classes: ['bg-neutral-100', 'text-neutral-800', 'border-neutral-200'] }; + + const badge = document.getElementById("modalStatusBadge"); + if (badge) { + badge.innerText = statusObj.label; + badge.className = "px-2.5 py-0.5 text-xs font-black rounded-full uppercase tracking-wider border"; + statusObj.classes.forEach(c => badge.classList.add(c)); + } }, events: window.appointments ?? [], @@ -109,14 +119,22 @@ document.addEventListener('DOMContentLoaded', function () { calendar.render() - const closeBtn = document.getElementById("closeModal") + // Manejo del cierre del modal con múltiples triggers (Cerrar, Botón X y Click afuera) + const closeModalElements = [ + document.getElementById("closeModal"), + document.getElementById("closeModalCross"), + document.getElementById("eventModal") + ] - if (closeBtn) { - closeBtn.addEventListener("click", function () { - document.getElementById("eventModal").classList.add("hidden") - document.body.style.overflow = "auto" - }) - } + closeModalElements.forEach(el => { + if (el) { + el.addEventListener("click", function (e) { + if (el.id === "eventModal" && e.target !== el) return; + document.getElementById("eventModal").classList.add("hidden") + document.body.style.overflow = "auto" + }) + } + }) }) diff --git a/resources/views/admin/employees/index.blade.php b/resources/views/admin/employees/index.blade.php new file mode 100644 index 0000000..41a00b1 --- /dev/null +++ b/resources/views/admin/employees/index.blade.php @@ -0,0 +1,323 @@ + + + +
+ + +
+

Registrar Nuevo Empleado

+
+ @csrf +
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+ +
+ +
+
+
+ + +
+
+ + + + + + + + + + + @forelse($employees as $employee) + + + + + + + @empty + + + + @endforelse + +
NombreCorreo ElectrónicoFecha de RegistroAcciones
+ {{ $employee->name }} + + {{ $employee->email }} + + {{ $employee->created_at->format('d/m/Y H:i') }} + +
+ + + +
+ @csrf + @method('DELETE') + +
+
+
+ No hay empleados registrados actualmente. +
+
+
+
+ + + + + @push('scripts') + + @endpush +
diff --git a/resources/views/agenda.blade.php b/resources/views/agenda.blade.php index c17570a..88be42c 100644 --- a/resources/views/agenda.blade.php +++ b/resources/views/agenda.blade.php @@ -37,29 +37,84 @@