From 91da97685faa8b3ef79d937d80105310e4a52148 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Wed, 8 Apr 2026 00:48:36 -0600 Subject: [PATCH] Add citas module: scheduling, calendar, blocked schedules --- PLAN_AGENDAMIENTO_CITAS.md | 472 +++++++++++++++++ app/Http/Controllers/Admin/CitaController.php | 212 ++++++++ .../Admin/HorarioBloqueadoController.php | 210 ++++++++ app/Http/Requests/CitaRequest.php | 41 ++ app/Http/Requests/HorarioBloqueadoRequest.php | 31 ++ app/Models/Cita.php | 176 +++++++ app/Models/HorarioBloqueado.php | 119 +++++ .../2024_01_01_000008_create_citas_table.php | 38 ++ ...00009_create_horarios_bloqueados_table.php | 26 + public/css/admin.css | 40 +- .../views/admin/citas/calendario.blade.php | 478 ++++++++++++++++++ resources/views/admin/citas/create.blade.php | 293 +++++++++++ resources/views/admin/citas/edit.blade.php | 226 +++++++++ resources/views/admin/citas/index.blade.php | 272 ++++++++++ resources/views/admin/citas/show.blade.php | 199 ++++++++ .../views/admin/horarios/create.blade.php | 209 ++++++++ resources/views/admin/horarios/edit.blade.php | 154 ++++++ .../views/admin/horarios/index.blade.php | 140 +++++ .../views/admin/layouts/master.blade.php | 25 + resources/views/admin/mensajes/show.blade.php | 15 +- routes/admin.php | 34 ++ 21 files changed, 3406 insertions(+), 4 deletions(-) create mode 100644 PLAN_AGENDAMIENTO_CITAS.md create mode 100644 app/Http/Controllers/Admin/CitaController.php create mode 100644 app/Http/Controllers/Admin/HorarioBloqueadoController.php create mode 100644 app/Http/Requests/CitaRequest.php create mode 100644 app/Http/Requests/HorarioBloqueadoRequest.php create mode 100644 app/Models/Cita.php create mode 100644 app/Models/HorarioBloqueado.php create mode 100644 database/migrations/2024_01_01_000008_create_citas_table.php create mode 100644 database/migrations/2024_01_01_000009_create_horarios_bloqueados_table.php create mode 100644 resources/views/admin/citas/calendario.blade.php create mode 100644 resources/views/admin/citas/create.blade.php create mode 100644 resources/views/admin/citas/edit.blade.php create mode 100644 resources/views/admin/citas/index.blade.php create mode 100644 resources/views/admin/citas/show.blade.php create mode 100644 resources/views/admin/horarios/create.blade.php create mode 100644 resources/views/admin/horarios/edit.blade.php create mode 100644 resources/views/admin/horarios/index.blade.php diff --git a/PLAN_AGENDAMIENTO_CITAS.md b/PLAN_AGENDAMIENTO_CITAS.md new file mode 100644 index 0000000..346adb6 --- /dev/null +++ b/PLAN_AGENDAMIENTO_CITAS.md @@ -0,0 +1,472 @@ +# Plan Técnico: Módulo de Agendamiento de Citas - Lash Vanshy + +## 1. Estructura de Base de Datos + +### 1.1 Tabla: citas + +```php +// database/migrations/2024_01_01_000008_create_citas_table.php + +Schema::create('citas', function (Blueprint $table) { + $table->id(); + $table->foreignId('mensaje_id')->nullable()->constrained('mensajes')->onDelete('set null'); + $table->string('cliente_nombre'); + $table->string('cliente_email'); + $table->string('cliente_telefono', 50)->nullable(); + $table->string('servicio')->default('Lash Extensions'); + $table->date('fecha'); + $table->time('hora_inicio'); + $table->time('hora_fin'); + $table->enum('estado', ['pendiente', 'confirmada', 'completada', 'cancelada'])->default('pendiente'); + $table->text('notas')->nullable(); + $table->timestamps(); +}); +``` + +### 1.2 Tabla: horarios_bloqueados + +```php +// database/migrations/2024_01_01_000009_create_horarios_bloqueados_table.php + +Schema::create('horarios_bloqueados', function (Blueprint $table) { + $table->id(); + $table->date('fecha'); + $table->time('hora_inicio'); + $table->time('hora_fin'); + $table->string('motivo')->nullable(); + $table->timestamps(); +}); +``` + +## 2. Modelos + +### 2.1 App/Models/Cita.php + +```php +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +class Cita extends Model +{ + protected $fillable = [ + 'mensaje_id', + 'cliente_nombre', + 'cliente_email', + 'cliente_telefono', + 'servicio', + 'fecha', + 'hora_inicio', + 'hora_fin', + 'estado', + 'notas', + ]; + + protected $casts = [ + 'fecha' => 'date', + 'hora_inicio' => 'datetime:H:i', + 'hora_fin' => 'datetime:H:i', + ]; + + public function mensaje(): BelongsTo + { + return $this->belongsTo(Mensaje::class); + } + + public function scopePendientes($query) { /* ... */ } + public function scopeConfirmadas($query) { /* ... */ } + public function scopePorFecha($query, $fecha) { /* ... */ } +} +``` + +### 2.2 App/Models/HorarioBloqueado.php + +```php +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; + +class HorarioBloqueado extends Model +{ + protected $fillable = ['fecha', 'hora_inicio', 'hora_fin', 'motivo']; + + protected $casts = ['fecha' => 'date']; +} +``` + +## 3. Backend - Controladores y API + +### 3.1 App/Http/Controllers/Admin/CitaController.php + +```php +namespace App\Http\Controllers\Admin; + +use App\Http\Controllers\Controller; +use App\Models\Citas; +use App\Models\HorarioBloqueado; +use Illuminate\Http\Request; +use Carbon\Carbon; + +class CitaController extends Controller +{ + // --- CRUD BÁSICO --- + public function index(Request $request) + { + $citas = Cita::with('mensaje') + ->when($request->fecha, fn($q) => $q->whereDate('fecha', $request->fecha)) + ->when($request->estado, fn($q) => $q->where('estado', $request->estado)) + ->orderBy('fecha')->orderBy('hora_inicio') + ->paginate(20); + + return view('admin.citas.index', compact('citas')); + } + + public function create(Request $request) + { + $mensajeId = $request->mensaje_id; + $mensaje = $mensajeId ? Mensaje::find($mensajeId) : null; + $fechasBloqueadas = HorarioBloqueado::pluck('fecha')->toArray(); + + return view('admin.citas.create', compact('mensaje', 'fechasBloqueadas')); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'cliente_nombre' => 'required', + 'cliente_email' => 'required|email', + 'cliente_telefono' => 'nullable', + 'fecha' => 'required|date|after_or_equal:today', + 'hora_inicio' => 'required', + 'notas' => 'nullable', + ]); + + // Calcular hora fin (60 minutos) + $horaFin = Carbon::parse($validated['hora_inicio'])->addMinutes(60)->format('H:i:s'); + + // Verificar disponibilidad + if (!self::verificarDisponibilidad($validated['fecha'], $validated['hora_inicio'], $horaFin)) { + return back()->with('error', 'El horario no está disponible.'); + } + + Cita::create([ + ...$validated, + 'mensaje_id' => $request->mensaje_id, + 'hora_fin' => $horaFin, + 'servicio' => 'Lash Extensions', + ]); + + return redirect()->route('admin.citas.index')->with('success', 'Cita agendada correctamente.'); + } + + public function edit(Cita $cita) { /* ... */ } + + public function update(Request $request, Cita $cita) { /* ... */ } + + public function destroy(Cita $cita) + { + $cita->delete(); + return back()->with('success', 'Cita eliminada.'); + } + + // --- API DE DISPONIBILIDAD --- + public function getHorariosDisponibles(Request $request) + { + $fecha = $request->fecha; + $citas = Cita::whereDate('fecha', $fecha)->where('estado', '!=', 'cancelada')->get(); + $bloqueados = HorarioBloqueado::whereDate('fecha', $fecha)->get(); + + $horarios = self::generarHorariosDisponibles($citas, $bloqueados); + + return response()->json($horarios); + } + + public function getCitasMes(Request $request) + { + $inicio = Carbon::parse($request->fecha)->startOfMonth(); + $fin = Carbon::parse($request->fecha)->endOfMonth(); + + $citas = Cita::whereBetween('fecha', [$inicio, $fin])->get(); + + return response()->json($citas); + } + + // --- HELPER: Verificar disponibilidad --- + private function verificarDisponibilidad($fecha, $horaInicio, $horaFin) + { + // Verificar citas existentes + $conflicto = Cita::whereDate('fecha', $fecha) + ->where('estado', '!=', 'cancelada') + ->where(function ($q) use ($horaInicio, $horaFin) { + $q->whereBetween('hora_inicio', [$horaInicio, $horaFin]) + ->orWhereBetween('hora_fin', [$horaInicio, $horaFin]) + ->orWhere(function ($q2) use ($horaInicio, $horaFin) { + $q2->where('hora_inicio', '<=', $horaInicio)->where('hora_fin', '>=', $horaFin); + }); + })->exists(); + + if ($conflicto) return false; + + // Verificar horarios bloqueados + $bloqueado = HorarioBloqueado::whereDate('fecha', $fecha) + ->where(function ($q) use ($horaInicio, $horaFin) { + $q->whereBetween('hora_inicio', [$horaInicio, $horaFin]) + ->orWhereBetween('hora_fin', [$horaInicio, $horaFin]); + })->exists(); + + return !$bloqueado; + } + + // --- HELPER: Generar array de horarios disponibles --- + private function generarHorariosDisponibles($citas, $bloqueados) + { + $horarios = []; + $inicio = 9; // 9:00 AM + $fin = 18; // 7:00 PM (último inicio a las 6pm para 60min) + + for ($hora = $inicio; $hora < $fin; $hora++) { + $horaStr = str_pad($hora, 2, '0', STR_PAD_LEFT) . ':00:00'; + $horaFinStr = Carbon::parse($horaStr)->addMinutes(60)->format('H:i:s'); + + // Verificar si está libre + $libre = true; + + foreach ($citas as $cita) { + if ($this->horarioConflicta($horaStr, $horaFinStr, $cita->hora_inicio, $cita->hora_fin)) { + $libre = false; + break; + } + } + + if ($libre) { + foreach ($bloqueados as $bloqueado) { + if ($this->horarioConflicta($horaStr, $horaFinStr, $bloqueado->hora_inicio, $bloqueado->hora_fin)) { + $libre = false; + break; + } + } + } + + if ($libre) { + $horarios[] = ['hora' => $horaStr, 'label' => Carbon::parse($horaStr)->format('h:i A')]; + } + } + + return $horarios; + } + + private function horarioConflicta($inicio, $fin, $existenteInicio, $existenteFin) + { + return $inicio < $existenteFin && $fin > $existenteInicio; + } +} +``` + +### 3.2 App/Http/Controllers/Admin/HorarioBloqueadoController.php + +```php +namespace App\Http\Controllers\Admin; + +class HorarioBloqueadoController extends Controller +{ + public function index() + { + $bloqueados = HorarioBloqueado::orderBy('fecha')->paginate(15); + return view('admin.horarios-bloqueados.index', compact('bloqueados')); + } + + public function store(Request $request) + { + $request->validate([ + 'fecha' => 'required|date', + 'hora_inicio' => 'required', + 'hora_fin' => 'required', + 'motivo' => 'nullable', + ]); + + HorarioBloqueado::create($request->all()); + return back()->with('success', 'Horario bloqueado.'); + } + + public function destroy(HorarioBloqueado $bloqueado) + { + $bloqueado->delete(); + return back()->with('success', 'Bloqueo eliminado.'); + } +} +``` + +### 3.3 Actualizar MensajeController para integrar botón de agendar + +```php +// En show() del MensajeController, pasar flag para mostrar botón +public function show(Mensaje $mensaje): View +{ + if (!$mensaje->leido) { + $mensaje->marcarLeido(); + } + + $yaTieneCita = Cita::where('mensaje_id', $mensaje->id)->exists(); + + return view('admin.mensajes.show', compact('mensaje', 'yaTieneCita')); +} +``` + +## 4. Rutas + +### routes/admin.php + +```php +// Citas +Route::prefix('citas')->name('admin.citas.')->group(function () { + Route::get('/', [CitaController::class, 'index'])->name('index'); + Route::get('/create', [CitaController::class, 'create'])->name('create'); + Route::get('/create/{mensaje_id}', [CitaController::class, 'create'])->name('create-from-mensaje'); + Route::post('/', [CitaController::class, 'store'])->name('store'); + Route::get('/{cita}/edit', [CitaController::class, 'edit'])->name('edit'); + Route::put('/{cita}', [CitaController::class, 'update'])->name('update'); + Route::delete('/{cita}', [CitaController::class, 'destroy'])->name('destroy'); + Route::patch('/{cita}/estado', [CitaController::class, 'cambiarEstado'])->name('estado'); +}); + +// API de disponibilidad +Route::get('/citas/disponibles', [CitaController::class, 'getHorariosDisponibles'])->name('admin.citas.disponibles'); +Route::get('/citas/calendario', [CitaController::class, 'getCitasMes'])->name('admin.citas.calendario'); + +// Horarios bloqueados +Route::prefix('horarios-bloqueados')->name('admin.horarios.')->group(function () { + Route::get('/', [HorarioBloqueadoController::class, 'index'])->name('index'); + Route::post('/', [HorarioBloqueadoController::class, 'store'])->name('store'); + Route::delete('/{bloqueado}', [HorarioBloqueadoController::class, 'destroy'])->name('destroy'); +}); +``` + +## 5. Frontend - Vistas + +### 5.1 admin/citas/index.blade.php + +- Tabla con filtros: por fecha, por estado +- Calendario mensual (vista) +- Buttons: crear, editar, eliminar, cambiar estado + +### 5.2 admin/citas/create.blade.php + +```html +
+ @csrf + + + + + + + + + + + + + + + +
+ +@push('scripts') + +@endpush +``` + +### 5.3 admin/citas/calendario.blade.php (Página completa) + +- Vista de mes con FullCalendar o tabla HTML +- Clic en día muestra horarios disponibles +- Arrastrar para crear cita (opcional) + +### 5.4 admin/mensajes/show.blade.php (Actualizar) + +```html +
+ ← Volver + + @if(!$yaTieneCita) + + 📅 Agendar Cita + + @else + ✓ Ya tiene cita + @endif +
+``` + +### 5.5 admin/horarios-bloqueados/index.blade.php + +- Formulario para bloquear horarios +- Lista de bloqueos activos +- Eliminar bloqueo + +## 6. Configuración del Negocio (Constantes) + +```php +// app/Constants/Negocio.php +namespace App\Constants; + +class Negocio +{ + const DURACION_CITA_MINUTOS = 60; + const HORA_APERTURA = '09:00'; + const HORA_CIERRE = '19:00'; + const DIAS_ATENCION = [1, 2, 3, 4, 5, 6]; // Lunes-Sábado (Carbon: 1=Lunes) +} +``` + +## 7. Flujo de Usuario + +### Desde Mensaje: +1. Admin recibe mensaje en admin/mensajes +2. Visualiza el mensaje en admin/mensajes/{id} +3. Ve botón "Agendar Cita" (si no tiene cita) +4. Clic -> admin/citas/create/{mensaje_id} +5. Formulario prellenado con datos del mensaje +6. Selecciona fecha y horario disponible +7. Confirma -> Cita creada -> Redirección a lista de citas + +### Adminsitración Directa: +1. Va a admin/citas +2. Clic "Nueva Cita" o usa el calendario +3. Completa formulario +4. Confirma + +## 8. Testing Checklist + +- [ ] Crear cita exitosamente +- [ ] Crear cita desde mensaje (datos prellenados) +- [ ] Validar conflicto de horarios (dos citas mismo horario) +- [ ] Validar horario bloqueado no permite cita +- [ ] Cambiar estado de cita (pendiente -> confirmadas -> completada -> cancelada) +- [ ] Eliminar cita +- [ ] Bloquear horario y verificar no disponible +- [ ] Ver calendario con citas del mes +- [ ] Validar días no laborables (Domingo) +- [ ] Validar horarios fuera de rango (antes 9am, después 7pm) + +## 9. Notas Adicionales + +- Usar SQLite existente (compatible) +- Implementar validación de rango de horario en frontend y backend +- Considerar agregar notificaciones por email al agendar (futuro) +- Los horarios bloqueados deben poder ser recurrentes (opcional v2) \ No newline at end of file diff --git a/app/Http/Controllers/Admin/CitaController.php b/app/Http/Controllers/Admin/CitaController.php new file mode 100644 index 0000000..5ef635c --- /dev/null +++ b/app/Http/Controllers/Admin/CitaController.php @@ -0,0 +1,212 @@ +orderBy('fecha', 'desc')->orderBy('hora_inicio', 'desc'); + + // Filtros + if ($request->filled('estado')) { + $query->where('estado', $request->estado); + } + + if ($request->filled('fecha')) { + $query->whereDate('fecha', $request->fecha); + } + + if ($request->filled('fecha_inicio') && $request->filled('fecha_fin')) { + $query->whereBetween('fecha', [$request->fecha_inicio, $request->fecha_fin]); + } + + if ($request->filled('buscar')) { + $buscar = $request->buscar; + $query->where(function ($q) use ($buscar) { + $q->where('nombre_cliente', 'like', "%{$buscar}%") + ->orWhere('email_cliente', 'like', "%{$buscar}%") + ->orWhere('telefono_cliente', 'like', "%{$buscar}%") + ->orWhere('servicio', 'like', "%{$buscar}%"); + }); + } + + $citas = $query->paginate(15)->appends($request->query()); + + return view('admin.citas.index', compact('citas')); + } + + /** + * Mostrar formulario de creación + */ + public function create(): View + { + $mensajes = Mensaje::orderBy('created_at', 'desc')->get(); + + return view('admin.citas.create', compact('mensajes')); + } + + /** + * Guardar nueva cita + */ + public function store(CitaRequest $request): RedirectResponse + { + $data = $request->validated(); + + // Calcular hora_fin basada en hora_inicio y duracion + $horaInicio = Carbon::parse($data['hora_inicio']); + $horaFin = $horaInicio->addMinutes($data['duracion']); + $data['hora_fin'] = $horaFin->format('H:i:s'); + + // Verificar disponibilidad antes de crear + $conflicto = Cita::whereDate('fecha', $data['fecha']) + ->where(function ($query) use ($data) { + $query->where(function ($q) use ($data) { + $q->whereTime('hora_inicio', '<', $data['hora_fin']) + ->whereTime('hora_fin', '>', $data['hora_inicio']); + }); + }) + ->whereIn('estado', ['pendiente', 'confirmada']) + ->exists(); + + if ($conflicto) { + return back()->withErrors(['hora_inicio' => 'Ya existe una cita programada en este horario.'])->withInput(); + } + + // Verificar si el horario está bloqueado + if (HorarioBloqueado::estaBloqueado($data['fecha'], $data['hora_inicio'])) { + return back()->withErrors(['hora_inicio' => 'El horario está bloqueado.'])->withInput(); + } + + Cita::create($data); + + return redirect()->route('admin.citas.index')->with('success', 'Cita creada correctamente.'); + } + + /** + * Mostrar detalles de una cita + */ + public function show(Cita $cita): View + { + $cita->load('mensaje'); + + return view('admin.citas.show', compact('cita')); + } + + /** + * Mostrar formulario de edición + */ + public function edit(Cita $cita): View + { + $mensajes = Mensaje::orderBy('created_at', 'desc')->get(); + + return view('admin.citas.edit', compact('cita', 'mensajes')); + } + + /** + * Actualizar cita + */ + public function update(CitaRequest $request, Cita $cita): RedirectResponse + { + $data = $request->validated(); + + // Calcular hora_fin si cambió hora_inicio o duracion + if ($request->has('hora_inicio') || $request->has('duracion')) { + $horaInicio = Carbon::parse($data['hora_inicio'] ?? $cita->hora_inicio); + $duracion = $data['duracion'] ?? $cita->duracion; + $horaFin = $horaInicio->addMinutes($duracion); + $data['hora_fin'] = $horaFin->format('H:i:s'); + } + + // Verificar conflicto si se cambia fecha u hora + $conflicto = Cita::where('id', '!=', $cita->id) + ->whereDate('fecha', $data['fecha']) + ->where(function ($query) use ($data) { + $query->where(function ($q) use ($data) { + $q->whereTime('hora_inicio', '<', $data['hora_fin']) + ->whereTime('hora_fin', '>', $data['hora_inicio']); + }); + }) + ->whereIn('estado', ['pendiente', 'confirmada']) + ->exists(); + + if ($conflicto) { + return back()->withErrors(['hora_inicio' => 'Ya existe una cita programada en este horario.'])->withInput(); + } + + $cita->update($data); + + return redirect()->route('admin.citas.index')->with('success', 'Cita actualizada correctamente.'); + } + + /** + * Eliminar cita + */ + public function destroy(Cita $cita): RedirectResponse + { + $cita->delete(); + + return redirect()->route('admin.citas.index')->with('success', 'Cita eliminada correctamente.'); + } + + /** + * Cambiar estado de la cita + */ + public function cambiarEstado(Request $request, Cita $cita): RedirectResponse + { + $request->validate([ + 'estado' => ['required', 'in:pendiente,confirmada,completada,cancelada'], + ]); + + $cita->update(['estado' => $request->estado]); + + return back()->with('success', 'Estado de la cita actualizado correctamente.'); + } + + /** + * Mostrar calendario de citas + */ + public function calendario(Request $request): View + { + $fecha = $request->filled('fecha') ? Carbon::parse($request->fecha) : Carbon::now(); + + $citas = Cita::with('mensaje') + ->whereDate('fecha', $fecha) + ->whereIn('estado', ['pendiente', 'confirmada']) + ->orderBy('hora_inicio') + ->get(); + + return view('admin.citas.calendario', compact('citas', 'fecha')); + } + + /** + * Obtener citas para una fecha específica (API) + */ + public function porFecha(Request $request): JsonResponse + { + $request->validate([ + 'fecha' => ['required', 'date'], + ]); + + $citas = Cita::whereDate('fecha', $request->fecha) + ->whereIn('estado', ['pendiente', 'confirmada']) + ->orderBy('hora_inicio') + ->get(); + + return response()->json($citas); + } +} diff --git a/app/Http/Controllers/Admin/HorarioBloqueadoController.php b/app/Http/Controllers/Admin/HorarioBloqueadoController.php new file mode 100644 index 0000000..422dec2 --- /dev/null +++ b/app/Http/Controllers/Admin/HorarioBloqueadoController.php @@ -0,0 +1,210 @@ +orderBy('hora_inicio', 'desc'); + + // Filtros + if ($request->filled('estado')) { + if ($request->estado === 'activos') { + $query->where('activo', true); + } elseif ($request->estado === 'inactivos') { + $query->where('activo', false); + } + } + + if ($request->filled('fecha')) { + $query->whereDate('fecha', $request->fecha); + } + + if ($request->filled('fecha_inicio') && $request->filled('fecha_fin')) { + $query->whereBetween('fecha', [$request->fecha_inicio, $request->fecha_fin]); + } + + $horarios = $query->paginate(15)->appends($request->query()); + + return view('admin.horarios-bloqueados.index', compact('horarios')); + } + + /** + * Mostrar formulario de creación + */ + public function create(): View + { + return view('admin.horarios-bloqueados.create'); + } + + /** + * Guardar nuevo horario bloqueado + */ + public function store(HorarioBloqueadoRequest $request): RedirectResponse + { + $data = $request->validated(); + + // Por defecto activo si no se especifica + if (! isset($data['activo'])) { + $data['activo'] = true; + } + + // Verificar superposición con otros bloques + $superpuesto = HorarioBloqueado::whereDate('fecha', $data['fecha']) + ->where(function ($query) use ($data) { + $query->where(function ($q) use ($data) { + $q->whereTime('hora_inicio', '<', $data['hora_fin']) + ->whereTime('hora_fin', '>', $data['hora_inicio']); + }); + }) + ->exists(); + + if ($superpuesto) { + return back()->withErrors(['hora_inicio' => 'Ya existe un horario bloqueado en este rango.'])->withInput(); + } + + HorarioBloqueado::create($data); + + return redirect()->route('admin.horarios-bloqueados.index')->with('success', 'Horario bloqueado creado correctamente.'); + } + + /** + * Mostrar detalles de un horario bloqueado + */ + public function show(HorarioBloqueado $horario_bloqueado): View + { + return view('admin.horarios-bloqueados.show', compact('horario_bloqueado')); + } + + /** + * Mostrar formulario de edición + */ + public function edit(HorarioBloqueado $horario_bloqueado): View + { + return view('admin.horarios-bloqueados.edit', compact('horario_bloqueado')); + } + + /** + * Actualizar horario bloqueado + */ + public function update(HorarioBloqueadoRequest $request, HorarioBloqueado $horario_bloqueado): RedirectResponse + { + $data = $request->validated(); + + // Verificar superposición con otros bloques (excluyendo el actual) + $superpuesto = HorarioBloqueado::where('id', '!=', $horario_bloqueado->id) + ->whereDate('fecha', $data['fecha']) + ->where(function ($query) use ($data) { + $query->where(function ($q) use ($data) { + $q->whereTime('hora_inicio', '<', $data['hora_fin']) + ->whereTime('hora_fin', '>', $data['hora_inicio']); + }); + }) + ->exists(); + + if ($superpuesto) { + return back()->withErrors(['hora_inicio' => 'Ya existe un horario bloqueado en este rango.'])->withInput(); + } + + $horario_bloqueado->update($data); + + return redirect()->route('admin.horarios-bloqueados.index')->with('success', 'Horario bloqueado actualizado correctamente.'); + } + + /** + * Eliminar horario bloqueado + */ + public function destroy(HorarioBloqueado $horario_bloqueado): RedirectResponse + { + $horario_bloqueado->delete(); + + return redirect()->route('admin.horarios-bloqueados.index')->with('success', 'Horario bloqueado eliminado correctamente.'); + } + + /** + * Activar horario bloqueado + */ + public function activar(HorarioBloqueado $horario_bloqueado): RedirectResponse + { + $horario_bloqueado->activar(); + + return back()->with('success', 'Horario bloqueado activado.'); + } + + /** + * Desactivar horario bloqueado + */ + public function desactivar(HorarioBloqueado $horario_bloqueado): RedirectResponse + { + $horario_bloqueado->desactivar(); + + return back()->with('success', 'Horario bloqueado desactivado.'); + } + + /** + * Verificar si una fecha y hora está bloqueada (API) + */ + public function verificar(Request $request): JsonResponse + { + $request->validate([ + 'fecha' => ['required', 'date'], + 'hora' => ['required', 'date_format:H:i'], + ]); + + $estaBloqueado = HorarioBloqueado::estaBloqueado($request->fecha, $request->hora); + + return response()->json([ + 'bloqueado' => $estaBloqueado, + 'mensaje' => $estaBloqueado ? 'El horario está bloqueado.' : 'El horario está disponible.', + ]); + } + + /** + * Obtener bloques para una fecha específica + */ + public function porFecha(Request $request): JsonResponse + { + $request->validate([ + 'fecha' => ['required', 'date'], + ]); + + $bloques = HorarioBloqueado::getBloquesPorFecha($request->fecha); + + return response()->json($bloques); + } + + /** + * Crear bloqueo rápido desde el calendario + */ + public function bloqueoRapido(Request $request): RedirectResponse + { + $request->validate([ + 'fecha' => ['required', 'date'], + 'hora_inicio' => ['required', 'date_format:H:i'], + 'hora_fin' => ['required', 'date_format:H:i', 'after:hora_inicio'], + 'motivo' => ['nullable', 'string', 'max:255'], + ]); + + HorarioBloqueado::create([ + 'fecha' => $request->fecha, + 'hora_inicio' => $request->hora_inicio, + 'hora_fin' => $request->hora_fin, + 'motivo' => $request->motivo ?? 'Bloqueo rápido', + 'activo' => true, + ]); + + return back()->with('success', 'Horario bloqueado correctamente.'); + } +} diff --git a/app/Http/Requests/CitaRequest.php b/app/Http/Requests/CitaRequest.php new file mode 100644 index 0000000..19c45b9 --- /dev/null +++ b/app/Http/Requests/CitaRequest.php @@ -0,0 +1,41 @@ +route('cita')?->id; + + return [ + 'mensaje_id' => ['nullable', 'exists:mensajes,id'], + 'nombre_cliente' => ['required', 'string', 'max:255'], + 'email_cliente' => ['required', 'email', 'max:255'], + 'telefono_cliente' => ['required', 'string', 'max:50'], + 'servicio' => ['required', 'string', 'max:255'], + 'fecha' => ['required', 'date', 'after_or_equal:today'], + 'hora_inicio' => ['required', 'date_format:H:i'], + 'hora_fin' => ['nullable', 'date_format:H:i'], + 'duracion' => ['required', 'integer', 'min:15', 'max:480'], + 'estado' => ['required', Rule::in(['pendiente', 'confirmada', 'completada', 'cancelada'])], + 'notas' => ['nullable', 'string'], + ]; + } + + public function messages(): array + { + return [ + 'fecha.after_or_equal' => 'La fecha debe ser hoy o posterior.', + 'estado.in' => 'El estado debe ser: pendiente, confirmada, completada o cancelada.', + ]; + } +} diff --git a/app/Http/Requests/HorarioBloqueadoRequest.php b/app/Http/Requests/HorarioBloqueadoRequest.php new file mode 100644 index 0000000..9f437b4 --- /dev/null +++ b/app/Http/Requests/HorarioBloqueadoRequest.php @@ -0,0 +1,31 @@ + ['required', 'date'], + 'hora_inicio' => ['required', 'date_format:H:i'], + 'hora_fin' => ['required', 'date_format:H:i', 'after:hora_inicio'], + 'motivo' => ['nullable', 'string', 'max:255'], + 'activo' => ['nullable', 'boolean'], + ]; + } + + public function messages(): array + { + return [ + 'hora_fin.after' => 'La hora de fin debe ser posterior a la hora de inicio.', + ]; + } +} diff --git a/app/Models/Cita.php b/app/Models/Cita.php new file mode 100644 index 0000000..e8e9bb3 --- /dev/null +++ b/app/Models/Cita.php @@ -0,0 +1,176 @@ + 'date', + 'hora_inicio' => 'datetime:H:i', + 'hora_fin' => 'datetime:H:i', + 'duracion' => 'integer', + ]; + + /** + * Relación con Mensaje + */ + public function mensaje(): BelongsTo + { + return $this->belongsTo(Mensaje::class); + } + + /** + * Scope para filtrar citas pendientes + */ + public function scopePendiente(Builder $query): Builder + { + return $query->where('estado', 'pendiente'); + } + + /** + * Scope para filtrar citas confirmadas + */ + public function scopeConfirmada(Builder $query): Builder + { + return $query->where('estado', 'confirmada'); + } + + /** + * Scope para filtrar citas completadas + */ + public function scopeCompletada(Builder $query): Builder + { + return $query->where('estado', 'completada'); + } + + /** + * Scope para filtrar citas canceladas + */ + public function scopeCancelada(Builder $query): Builder + { + return $query->where('estado', 'cancelada'); + } + + /** + * Scope para filtrar citas por fecha específica + */ + public function scopePorFecha(Builder $query, $fecha): Builder + { + return $query->whereDate('fecha', $fecha); + } + + /** + * Scope para filtrar citas entre dos fechas + */ + public function scopeEntreFechas(Builder $query, $inicio, $fin): Builder + { + return $query->whereBetween('fecha', [$inicio, $fin]); + } + + /** + * Calcular hora_fin basado en hora_inicio + duracion + */ + public function calcularHoraFin(): string + { + $horaInicio = Carbon::parse($this->hora_inicio); + $horaFin = $horaInicio->addMinutes($this->duracion); + + $this->hora_fin = $horaFin->format('H:i:s'); + + return $this->hora_fin; + } + + /** + * Obtener estado formateado para mostrar + */ + public function getEstadoFormateadoAttribute(): string + { + $estados = [ + 'pendiente' => 'Pendiente', + 'confirmada' => 'Confirmada', + 'completada' => 'Completada', + 'cancelada' => 'Cancelada', + ]; + + return $estados[$this->estado] ?? $this->estado; + } + + /** + * Obtener hora de inicio formateada + */ + public function getHoraInicioFormatAttribute(): string + { + return Carbon::parse($this->hora_inicio)->format('H:i'); + } + + /** + * Obtener hora de fin formateada + */ + public function getHoraFinFormatAttribute(): string + { + return Carbon::parse($this->hora_fin)->format('H:i'); + } + + /** + * Obtener duración formateada (horas y minutos) + */ + public function getDuracionFormatAttribute(): string + { + $horas = floor($this->duracion / 60); + $minutos = $this->duracion % 60; + + if ($horas > 0 && $minutos > 0) { + return "{$horas}h {$minutos}min"; + } elseif ($horas > 0) { + return "{$horas}h"; + } else { + return "{$minutos}min"; + } + } + + /** + * Verificar si la cita está activa (no cancelada ni completada) + */ + public function isActiva(): bool + { + return in_array($this->estado, ['pendiente', 'confirmada']); + } + + /** + * Cambiar estado de la cita + */ + public function cambiarEstado(string $nuevoEstado): bool + { + $estadosValidos = ['pendiente', 'confirmada', 'completada', 'cancelada']; + + if (! in_array($nuevoEstado, $estadosValidos)) { + return false; + } + + return $this->update(['estado' => $nuevoEstado]); + } +} diff --git a/app/Models/HorarioBloqueado.php b/app/Models/HorarioBloqueado.php new file mode 100644 index 0000000..dcc6a62 --- /dev/null +++ b/app/Models/HorarioBloqueado.php @@ -0,0 +1,119 @@ + 'date', + 'activo' => 'boolean', + ]; + + /** + * Scope para filtrar horarios activos + */ + public function scopeActivo(Builder $query): Builder + { + return $query->where('activo', true); + } + + /** + * Scope para filtrar horarios por fecha + */ + public function scopePorFecha(Builder $query, $fecha): Builder + { + return $query->whereDate('fecha', $fecha); + } + + /** + * Verificar si una fecha y hora está bloqueada + */ + public static function estaBloqueado($fecha, $hora): bool + { + $horaCarbon = Carbon::parse($hora)->format('H:i:s'); + + return self::whereDate('fecha', $fecha) + ->where('activo', true) + ->whereTime('hora_inicio', '<=', $horaCarbon) + ->whereTime('hora_fin', '>', $horaCarbon) + ->exists(); + } + + /** + * Verificar si una fecha tiene bloques activos + */ + public static function tieneBloques($fecha): bool + { + return self::whereDate('fecha', $fecha) + ->where('activo', true) + ->exists(); + } + + /** + * Obtener los bloques activos para una fecha + */ + public static function getBloquesPorFecha($fecha) + { + return self::whereDate('fecha', $fecha) + ->where('activo', true) + ->orderBy('hora_inicio') + ->get(); + } + + /** + * Activar el horario bloqueado + */ + public function activar(): bool + { + return $this->update(['activo' => true]); + } + + /** + * Desactivar el horario bloqueado + */ + public function desactivar(): bool + { + return $this->update(['activo' => false]); + } + + /** + * Obtener hora de inicio formateada + */ + public function getHoraInicioFormatAttribute(): string + { + return Carbon::parse($this->hora_inicio)->format('H:i'); + } + + /** + * Obtener hora de fin formateada + */ + public function getHoraFinFormatAttribute(): string + { + return Carbon::parse($this->hora_fin)->format('H:i'); + } + + /** + * Obtener rango de horario formateado + */ + public function getRangoHorarioAttribute(): string + { + return "{$this->hora_inicio_format} - {$this->hora_fin_format}"; + } +} diff --git a/database/migrations/2024_01_01_000008_create_citas_table.php b/database/migrations/2024_01_01_000008_create_citas_table.php new file mode 100644 index 0000000..d7f6f53 --- /dev/null +++ b/database/migrations/2024_01_01_000008_create_citas_table.php @@ -0,0 +1,38 @@ +id(); + $table->unsignedBigInteger('mensaje_id')->nullable(); + $table->string('nombre_cliente', 255); + $table->string('email_cliente', 255); + $table->string('telefono_cliente', 50); + $table->string('servicio', 255); + $table->date('fecha'); + $table->time('hora_inicio'); + $table->time('hora_fin'); + $table->integer('duracion')->default(60); + $table->enum('estado', ['pendiente', 'confirmada', 'completada', 'cancelada'])->default('pendiente'); + $table->text('notas')->nullable(); + $table->timestamps(); + + // Foreign key to mensajes table + $table->foreign('mensaje_id') + ->references('id') + ->on('mensajes') + ->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::dropIfExists('citas'); + } +}; diff --git a/database/migrations/2024_01_01_000009_create_horarios_bloqueados_table.php b/database/migrations/2024_01_01_000009_create_horarios_bloqueados_table.php new file mode 100644 index 0000000..c29020f --- /dev/null +++ b/database/migrations/2024_01_01_000009_create_horarios_bloqueados_table.php @@ -0,0 +1,26 @@ +id(); + $table->date('fecha'); + $table->time('hora_inicio'); + $table->time('hora_fin'); + $table->string('motivo', 255)->nullable(); + $table->boolean('activo')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('horarios_bloqueados'); + } +}; diff --git a/public/css/admin.css b/public/css/admin.css index b75af3e..8133fe1 100755 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -604,8 +604,8 @@ body { } /* ============================================ - SEARCH & FILTERS - ============================================ */ + SEARCH & FILTERS + ============================================ */ .search-box { position: relative; max-width: 300px; @@ -632,6 +632,42 @@ body { align-items: center; } +/* ============================================ + DROPDOWN MENU IN SIDEBAR + ============================================ */ +.sidebar-nav .dropdown-menu { + position: static; + background: white; + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.5rem; + margin-left: 1rem; + box-shadow: 0 5px 20px var(--shadow); +} + +.sidebar-nav .dropdown-item { + padding: 0.5rem 1rem; + border-radius: 8px; + color: var(--text); + font-weight: 500; + transition: all 0.3s ease; +} + +.sidebar-nav .dropdown-item:hover { + background: rgba(248, 180, 196, 0.2); + color: var(--primary-dark); +} + +.sidebar-nav .dropdown-item.active { + background: linear-gradient(135deg, var(--primary), var(--primary-dark)); + color: white; +} + +.sidebar-nav .dropdown-divider { + margin: 0.5rem 0; + border-color: var(--border); +} + /* ============================================ RESPONSIVE ============================================ */ diff --git a/resources/views/admin/citas/calendario.blade.php b/resources/views/admin/citas/calendario.blade.php new file mode 100644 index 0000000..bf19281 --- /dev/null +++ b/resources/views/admin/citas/calendario.blade.php @@ -0,0 +1,478 @@ +@extends('admin.layouts.master') + +@section('title', 'Calendario de Citas - Lash Vanshy') + +@section('page-title', 'Calendario de Citas') + +@section('styles') + + + +@endsection + +@section('content') + + + + +
+ +
+ + +
+
+
+ Leyenda: +
+ + Pendiente +
+
+ + Confirmada +
+
+ + Completada +
+
+ + Cancelada +
+
+ + Bloqueado +
+
+
+
+ + + + +
+
+
+
+
+ + + +@endsection + +@push('scripts') + + + + + +@endpush \ No newline at end of file diff --git a/resources/views/admin/citas/create.blade.php b/resources/views/admin/citas/create.blade.php new file mode 100644 index 0000000..70d7b02 --- /dev/null +++ b/resources/views/admin/citas/create.blade.php @@ -0,0 +1,293 @@ +@extends('admin.layouts.master') + +@section('title', 'Crear Cita - Lash Vanshy') + +@section('page-title', 'Nueva Cita') + +@section('content') + + + +
+
+
+
+ Agendar Nueva Cita +
+
+
+ @csrf + + @if($mensaje) + +
+ + Nota: Esta cita se creará a partir del mensaje de + {{ $mensaje->nombre }} + + (Ver mensaje) + +
+ @endif + +
+ +
+ + + @error('cliente_nombre') +
{{ $message }}
+ @enderror +
+ + +
+ + + @error('cliente_email') +
{{ $message }}
+ @enderror +
+ + +
+ + + @error('cliente_telefono') +
{{ $message }}
+ @enderror +
+ + +
+ + +
+
+ +
+ +
+ +
+ + + @error('fecha') +
{{ $message }}
+ @else + + + Horario: 9:00 AM - 7:00 PM (Lun-Sáb) + + @enderror +
+ + +
+ + + @error('hora_inicio') +
{{ $message }}
+ @else + + Duración: 60 minutos + + @enderror +
+
+
+
+
+ + +
+ + + @error('notas') +
{{ $message }}
+ @enderror +
+ +
+ + Volver + + +
+
+
+
+
+ + +
+
+
+ Información +
+
+
    +
  • Duración de cada cita: 60 minutos
  • +
  • Horario de atención: 9:00 AM - 7:00 PM
  • +
  • Días laborables: Lunes a Sábado
  • +
  • Último turno: 6:00 PM
  • +
+
+
+ + +
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/admin/citas/edit.blade.php b/resources/views/admin/citas/edit.blade.php new file mode 100644 index 0000000..ef77cbb --- /dev/null +++ b/resources/views/admin/citas/edit.blade.php @@ -0,0 +1,226 @@ +@extends('admin.layouts.master') + +@section('title', 'Editar Cita - Lash Vanshy') + +@section('page-title', 'Editar Cita') + +@section('content') + + + +
+
+
+
+ Editar Cita +
+
+
+ @csrf + @method('PUT') + +
+ +
+ + + @error('cliente_nombre') +
{{ $message }}
+ @enderror +
+ + +
+ + + @error('cliente_email') +
{{ $message }}
+ @enderror +
+ + +
+ + + @error('cliente_telefono') +
{{ $message }}
+ @enderror +
+ + +
+ + +
+
+ +
+ +
+ +
+ + + @error('fecha') +
{{ $message }}
+ @else + + + Horario: 9:00 AM - 7:00 PM + + @enderror +
+ + +
+ + + @error('hora_inicio') +
{{ $message }}
+ @else + + Duración: 60 minutos + + @enderror +
+
+
+
+
+ + +
+ + +
+ + +
+ + + @error('notas') +
{{ $message }}
+ @enderror +
+ +
+ + Volver + + +
+
+
+
+
+ + +
+
+
+ Información de la Cita +
+
+
+
Fecha Creación:
+
{{ $cita->created_at->format('d/m/Y H:i') }}
+ +
Última Actualización:
+
{{ $cita->updated_at->format('d/m/Y H:i') }}
+ + @if($cita->mensaje) +
Desde Mensaje:
+
+ Ver mensaje +
+ @endif +
+
+
+ +
+
+ ¿Cancelar Cita? +
+
+

+ ¿Necesita cancelar esta cita? Puede cambiar el estado a "Cancelada" desde el formulario o eliminarla permanentemente. +

+
+ @csrf + @method('DELETE') + +
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/citas/index.blade.php b/resources/views/admin/citas/index.blade.php new file mode 100644 index 0000000..0cc5291 --- /dev/null +++ b/resources/views/admin/citas/index.blade.php @@ -0,0 +1,272 @@ +@extends('admin.layouts.master') + +@section('title', 'Citas - Lash Vanshy') + +@section('page-title', 'Citas') + +@section('content') + + + + +
+
+
+
+ +
+
+

{{ $stats['pendientes'] ?? 0 }}

+

Pendientes

+
+
+
+
+
+
+ +
+
+

{{ $stats['confirmadas'] ?? 0 }}

+

Confirmadas

+
+
+
+
+
+
+ +
+
+

{{ $stats['completadas'] ?? 0 }}

+

Completadas

+
+
+
+
+
+
+ +
+
+

{{ $stats['canceladas'] ?? 0 }}

+

Canceladas

+
+
+
+
+ + +
+
+
+
+ + + + + + + + + @if(request()->hasAny(['buscar', 'fecha', 'estado'])) + + Limpiar + + @endif +
+ + +
+
+
+ + +
+
+ + Listado de Citas + + + Total: {{ $citas->total() }} citas + +
+
+ @if($citas->isEmpty()) +
+ +

No hay citas

+

No se encontraron citas con los filtros seleccionados.

+ + Crear Primera Cita + +
+ @else +
+ + + + + + + + + + + + @foreach($citas as $cita) + + + + + + + + @endforeach + +
Fecha/HoraClienteServicioEstadoAcciones
+
+ + + {{ $cita->fecha->format('d/m/Y') }} + + + {{ $cita->hora_inicio }} - {{ $cita->hora_fin }} + +
+
+
+ {{ $cita->cliente_nombre }} + {{ $cita->cliente_email }} + @if($cita->cliente_telefono) + {{ $cita->cliente_telefono }} + @endif +
+
{{ $cita->servicio }} + @switch($cita->estado) + @case('pendiente') + Pendiente + @break + @case('confirmada') + Confirmada + @break + @case('completada') + Completada + @break + @case('cancelada') + Cancelada + @break + @endswitch + +
+ + + + + + + + + +
+ @csrf + @method('DELETE') + +
+
+
+
+ + +
+ {{ $citas->withQueryString()->links() }} +
+ @endif +
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/citas/show.blade.php b/resources/views/admin/citas/show.blade.php new file mode 100644 index 0000000..8166292 --- /dev/null +++ b/resources/views/admin/citas/show.blade.php @@ -0,0 +1,199 @@ +@extends('admin.layouts.master') + +@section('title', 'Ver Cita - Lash Vanshy') + +@section('page-title', 'Detalle de Cita') + +@section('content') + + + +
+
+
+
+ + Cita de {{ $cita->cliente_nombre }} + + @switch($cita->estado) + @case('pendiente') + Pendiente + @break + @case('confirmada') + Confirmada + @break + @case('completada') + Completada + @break + @case('cancelada') + Cancelada + @break + @endswitch +
+
+ +
+
+ +

+ + {{ $cita->fecha->format('d/m/Y') }} +

+
+
+ +

+ + {{ \Carbon\Carbon::parse($cita->hora_inicio)->format('h:i A') }} + - + {{ \Carbon\Carbon::parse($cita->hora_fin)->format('h:i A') }} +

+ Duración: 60 minutos +
+
+ +
+ + +
+
+ +

{{ $cita->cliente_nombre }}

+
+
+ +

+ {{ $cita->cliente_email }} +

+
+ @if($cita->cliente_telefono) +
+ +

+ {{ $cita->cliente_telefono }} +

+
+ @endif +
+ +

{{ $cita->servicio }}

+
+
+ + @if($cita->notas) +
+ + +
+ +
+ {{ $cita->notas }} +
+
+ @endif + +
+ + +
+
+ +

{{ $cita->created_at->format('d/m/Y H:i') }}

+
+
+ +

{{ $cita->updated_at->format('d/m/Y H:i') }}

+
+
+ + + +
+
+
+ + +
+ +
+
+ Cambiar Estado +
+
+
+ @csrf + @method('PATCH') + + + + + +
+
+
+ + + @if($cita->mensaje) +
+
+ Mensaje Relacionado +
+
+

{{ $cita->mensaje->nombre }}

+

{{ $cita->mensaje->mensaje }}

+ + Ver Mensaje + +
+
+ @endif + + +
+
+ Zona de Peligro +
+
+

+ Eliminar esta cita es una acción permanente. ¿Estás seguro? +

+
+ @csrf + @method('DELETE') + +
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/horarios/create.blade.php b/resources/views/admin/horarios/create.blade.php new file mode 100644 index 0000000..911886a --- /dev/null +++ b/resources/views/admin/horarios/create.blade.php @@ -0,0 +1,209 @@ +@extends('admin.layouts.master') + +@section('title', 'Bloquear Horario - Lash Vanshy') + +@section('page-title', 'Bloquear Horario') + +@section('content') + + + +
+
+
+
+ Bloquear Horario +
+
+
+ @csrf + +
+ +
+ + + @error('fecha') +
{{ $message }}
+ @else + + + No se podrán agendar citas en esta fecha + + @enderror +
+
+ +
+ +
+ + + @error('hora_inicio') +
{{ $message }}
+ @enderror +
+ + +
+ + + @error('hora_fin') +
{{ $message }}
+ @else + + El bloqueo dura hasta esta hora + + @enderror +
+
+ + +
+ + + @error('motivo') +
{{ $message }}
+ @else + + Opcional. Ej: "Vacaciones de Semana Santa" + + @enderror +
+ + +
+ +
+ + + +
+
+ +
+ + Volver + + +
+
+
+
+
+ + +
+
+
+ Información +
+
+
    +
  • Los horarios bloqueados prevents the agendamiento de citas.
  • +
  • Puedes bloquear días completos o solo tramos específicos.
  • +
  • El bloqueo no afecta las citas ya existentes.
  • +
  • Los clientes no podrán ver estos horarios.
  • +
+
+
+ +
+
+ Horario de Atención +
+
+
    +
  • Lunes a Sábado: 9:00 AM - 7:00 PM
  • +
  • Último turno: 6:00 PM
  • +
  • Duración cita: 60 minutos
  • +
+
+
+
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/admin/horarios/edit.blade.php b/resources/views/admin/horarios/edit.blade.php new file mode 100644 index 0000000..98d0cc9 --- /dev/null +++ b/resources/views/admin/horarios/edit.blade.php @@ -0,0 +1,154 @@ +@extends('admin.layouts.master') + +@section('title', 'Editar Bloqueo - Lash Vanshy') + +@section('page-title', 'Editar Bloqueo') + +@section('content') + + + +
+
+
+
+ Editar Bloqueo +
+
+
+ @csrf + @method('PUT') + +
+ +
+ + + @error('fecha') +
{{ $message }}
+ @enderror +
+
+ +
+ +
+ + + @error('hora_inicio') +
{{ $message }}
+ @enderror +
+ + +
+ + + @error('hora_fin') +
{{ $message }}
+ @enderror +
+
+ + +
+ + +@error('motivo') +
{{ $message }}
+ @enderror +
+ +
+ + Volver + + +
+
+
+
+
+ + +
+
+
+ Información del Bloqueo +
+
+
+
Creado:
+
{{ $bloqueado->created_at->format('d/m/Y H:i') }}
+ +
Actualizado:
+
{{ $bloqueado->updated_at->format('d/m/Y H:i') }}
+
+
+
+ +
+
+ Eliminar Bloqueo +
+
+

+ ¿Estás seguro de desbloquear este horario? Los clientes podrán agendar citas en este espacio. +

+
+ @csrf + @method('DELETE') + +
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/horarios/index.blade.php b/resources/views/admin/horarios/index.blade.php new file mode 100644 index 0000000..6a5d4a4 --- /dev/null +++ b/resources/views/admin/horarios/index.blade.php @@ -0,0 +1,140 @@ +@extends('admin.layouts.master') + +@section('title', 'Horarios Bloqueados - Lash Vanshy') + +@section('page-title', 'Horarios Bloqueados') + +@section('content') + + + + +
+
+
+
+ + + + + @if(request('fecha')) + + Limpiar + + @endif +
+ + +
+
+
+ + +
+
+ + Horarios Bloqueados + + + Total: {{ $bloqueados->total() }} bloqueos + +
+
+ @if($bloqueados->isEmpty()) +
+ +

No hay horarios bloqueados

+

No se encontraron horarios bloqueados.

+ + Bloquear Primer Horario + +
+ @else +
+ + + + + + + + + + + + @foreach($bloqueados as $bloqueado) + + + + + + + + @endforeach + +
FechaHora InicioHora FinMotivoAcciones
+ + + {{ \Carbon\Carbon::parse($bloqueado->fecha)->format('d/m/Y') }} + + + {{ \Carbon\Carbon::parse($bloqueado->fecha)->format('l') }} + + {{ \Carbon\Carbon::parse($bloqueado->hora_inicio)->format('h:i A') }}{{ \Carbon\Carbon::parse($bloqueado->hora_fin)->format('h:i A') }} + @if($bloqueado->motivo) + {{ $bloqueado->motivo }} + @else + - + @endif + +
+ + + +
+ @csrf + @method('DELETE') + +
+
+
+
+ + +
+ {{ $bloqueados->withQueryString()->links() }} +
+ @endif +
+
+ + +
+ + Nota: Los horarios bloqueados impedirán que se agenden citas en esos horarios. + Utilice esta función para bloquear días completos o tramos específicos (ej: vacaciones, mantenimiento). +
+@endsection \ No newline at end of file diff --git a/resources/views/admin/layouts/master.blade.php b/resources/views/admin/layouts/master.blade.php index c62154e..d3e0783 100755 --- a/resources/views/admin/layouts/master.blade.php +++ b/resources/views/admin/layouts/master.blade.php @@ -75,6 +75,31 @@ + + @if(Auth::guard('admin')->user()->rol === 'super_admin')