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 creacion */ public function create(): View { $mensajes = Mensaje::orderBy('created_at', 'desc')->get(); $mensaje = null; return view('admin.citas.create', compact('mensajes', 'mensaje')); } /** * Mostrar formulario de creacion desde mensaje */ public function createFromMensaje(Mensaje $mensaje): View { $mensajes = Mensaje::orderBy('created_at', 'desc')->get(); return view('admin.citas.create', compact('mensajes', 'mensaje')); } /** * Guardar nueva cita */ public function store(CitaRequest $request): RedirectResponse { try { $data = $request->validated(); Log::info('Datos validados', $data); // Calcular hora_fin basada en hora_inicio y duracion $horaInicio = Carbon::parse($data['hora_inicio']); $horaFin = $horaInicio->addMinutes((int) $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 = Cita::create($data); Log::info('Cita creada', ['id' => $cita->id]); return redirect()->route('admin.citas.index')->with('success', 'Cita creada correctamente.'); } catch (\Exception $e) { Log::error('Error al crear cita: '.$e->getMessage()); return back()->withErrors(['error' => 'Error al crear la cita: '.$e->getMessage()])->withInput(); } } /** * 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((int) $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); } /** * Obtener horarios disponibles para una fecha (API) */ public function getHorariosDisponibles(Request $request): JsonResponse { $request->validate([ 'fecha' => ['required', 'date'], 'duracion' => ['nullable', 'integer', 'min:15', 'max:240'], ]); $fecha = $request->fecha; $duracion = $request->duracion ?? 60; // Verificar si es domingo (no laborable) $diaSemana = Carbon::parse($fecha)->dayOfWeek; if ($diaSemana === 0) { return response()->json([ 'error' => 'No se atienden los domingos', 'horarios' => [], ]); } // Horario de atención: 9:00 a 19:00 (último turno a las 18:00) $horaApertura = 9; $horaCierre = 18; // Obtener citas existentes para esa fecha $citasExistentes = Cita::whereDate('fecha', $fecha) ->whereIn('estado', ['pendiente', 'confirmada']) ->orderBy('hora_inicio') ->get(); // Obtener horarios bloqueados $bloqueos = HorarioBloqueado::whereDate('fecha', $fecha) ->where('activo', true) ->orderBy('hora_inicio') ->get(); // Generar todos los horarios disponibles $horarios = []; $intervalo = 30; // intervals of 30 minutes for ($hora = $horaApertura; $hora < $horaCierre; $hora++) { for ($minuto = 0; $minuto < 60; $minuto += $intervalo) { // Check if appointment fits in the schedule $horaInicioMinutos = $hora * 60 + $minuto; $horaFinMinutos = $horaInicioMinutos + $duracion; if ($horaFinMinutos > $horaCierre * 60) { continue; } $horaInicio = sprintf('%02d:%02d', $hora, $minuto); $horaFinMinutos = $horaInicioMinutos + $duracion; $horaFinHora = intdiv($horaFinMinutos, 60); $horaFinMinuto = $horaFinMinutos % 60; $horaFin = sprintf('%02d:%02d', $horaFinHora, $horaFinMinuto); // Verificar si hay conflicto con citas existentes $conflicto = $citasExistentes->contains(function ($cita) use ($horaInicio, $horaFin) { return $cita->hora_inicio < $horaFin && $cita->hora_fin > $horaInicio; }); // Verificar si hay conflicto con bloqueos $bloqueado = $bloqueos->contains(function ($bloqueo) use ($horaInicio, $horaFin) { return $bloqueo->hora_inicio < $horaFin && $bloqueo->hora_fin > $horaInicio; }); if (! $conflicto && ! $bloqueado) { $horarios[] = [ 'hora' => $horaInicio, 'hora_fin' => $horaFin, ]; } } } return response()->json([ 'fecha' => $fecha, 'duracion' => $duracion, 'horarios' => $horarios, ]); } /** * Obtener citas en un rango de fechas (API para calendario) */ public function getCitasPorFecha(Request $request): JsonResponse { $request->validate([ 'inicio' => ['required', 'date'], 'fin' => ['required', 'date'], ]); $citas = Cita::with('mensaje') ->whereBetween('fecha', [$request->inicio, $request->fin]) ->whereIn('estado', ['pendiente', 'confirmada']) ->orderBy('fecha') ->orderBy('hora_inicio') ->get(); $bloqueos = HorarioBloqueado::whereBetween('fecha', [$request->inicio, $request->fin]) ->where('activo', true) ->orderBy('fecha') ->orderBy('hora_inicio') ->get(); return response()->json([ 'citas' => $citas, 'bloqueos' => $bloqueos, ]); } }