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
+
+
+
+
+
+
+
+
+ @forelse($employees as $employee)
+ Nombre
+ Correo Electrónico
+ Fecha de Registro
+ Acciones
+
+
+ @empty
+
+ {{ $employee->name }}
+
+
+ {{ $employee->email }}
+
+
+ {{ $employee->created_at->format('d/m/Y H:i') }}
+
+
+
+
+
+ @endforelse
+
+
+ No hay empleados registrados actualmente.
+
+
+
+ Editar Empleado
+
+
Dueño
+ +Modelo de Bicicleta
+ +Día y Horario Pactado
+ +Cliente
-- {{ $appointment->client->name }} -
+ +Cliente
+{{ $appointment->client->name }}
+Teléfono
-- {{ $appointment->contact_phone ?? '-' }} -
+ +Teléfono de contacto
+{{ $appointment->contact_phone ?? 'Sin teléfono registrado' }}
+Fecha pactada de entrega
-- {{ $appointment->scheduled_at->format('d/m/Y') }} -
+ +Bicicleta
+{{ $appointment->bike_model ?? 'Sin modelo' }}
+Hora
-- {{ $appointment->scheduled_at->format('H:i') }} -
+ +Fecha y Hora Pactada
++ {{ ucfirst($appointment->scheduled_at->locale('es')->translatedFormat('l, d \d\e F \d\e Y')) }} a las {{ $appointment->scheduled_at->format('H:i') }} +
+Modelo de Bicicleta
-- {{ $appointment->bike_model ?? '-' }} -
+ +Presupuesto Estimado
++ {{ $appointment->estimated_cost ? '$' . number_format($appointment->estimated_cost, 0, ',', '.') : 'Sin presupuestar' }} +
+Problema Reportado
-- {{ $appointment->problem_description }} -
-Notas
-- {{ $appointment->notes ?? '-' }} -
-Descripción del problema
+{{ $appointment->problem_description }}
+Repuestos Necesarios
+{{ $appointment->parts_needed }}
+Notas Internas
+{{ $appointment->notes }}
+Registrado el: {{ $client->created_at->format('d/m/Y H:i') }}
+Nombre Completo
+{{ $client->name }}
+Teléfono / WhatsApp
+ @if($client->phone) +{{ $client->phone }}
+ @else +No registrado
+ @endif +Correo Electrónico
+ @if($client->email) +{{ $client->email }}
+ @else +No registrado
+ @endif +Dirección
+ @if($client->address) +{{ $client->address }}
+ @else +No registrada
+ @endif +{{ $job->problem_description }}
- @if($job->estimated_cost) -Registrado el: {{ $supplier->created_at->format('d/m/Y H:i') }}
+Nombre / Razón Social
+{{ $supplier->name }}
+Teléfono / WhatsApp
+ @if($supplier->phone) +{{ $supplier->phone }}
+ @else +No registrado
+ @endif +Correo Electrónico
+ @if($supplier->email) +{{ $supplier->email }}
+ @else +No registrado
+ @endif +Dirección
+ @if($supplier->address) +{{ $supplier->address }}
+ @else +No registrada
+ @endif +