<?php
namespace App\Controller;
use App\Entity\Appointment;
use App\Entity\Horario;
use App\Form\AppointmentType;
use App\Form\RescheduleAppointmentType;
use App\Repository\AppointmentRepository;
use App\Repository\HorarioRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Service\AppointmentNotifier;
use App\Service\ZonitelSmsService;
/**
* @Route("admin/appointment")
*/
class AppointmentController extends AbstractController
{
private $em;
private AppointmentNotifier $appointmentNotifier;
private $appointmentNotifierSMS;
public function __construct(EntityManagerInterface $em, AppointmentNotifier $appointmentNotifier, ZonitelSmsService $appointmentNotifierSMS)
{
$this->em = $em;
$this->appointmentNotifier = $appointmentNotifier;
$this->appointmentNotifierSMS = $appointmentNotifierSMS;
}
/**
* @Route("/new", name="admin_appointment_new", methods={"GET", "POST"})
*/
public function new(Request $request, EntityManagerInterface $entityManager, HorarioRepository $horarioRepo): Response
{
$appointment = new Appointment();
// FORZAR ESTADO VÁLIDO ANTES de crear el formulario
$appointment->setStatus('confirmada');
$appointment->setPaymentMethod('imaging_pro');
// Asignar automáticamente el admin como proveedor
$appointment->setProvider($this->getUser());
$form = $this->createForm(AppointmentType::class, $appointment, [
'provider' => null,
'is_edit' => false,
'context' => 'admin'
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$appointment->setProvider($this->getUser());
// Verificar y asegurar el estado nuevamente
if (!$appointment->getStatus()) {
$appointment->setStatus('confirmada');
}
$appointment->setPaymentMethod('imaging_pro');
// Obtener el horario directamente del appointment
$horario = $appointment->getHorario();
// Verificar que el horario existe
if (!$horario) {
$this->addFlash('error', 'El horario seleccionado no es válido.');
return $this->redirectToRoute('admin_appointment_new');
}
// Verificar que el horario está disponible
if ($horario->getEstado() !== 'disponible') {
$this->addFlash('error', 'El horario seleccionado no está disponible.');
return $this->redirectToRoute('admin_appointment_new');
}
// Actualizar el estado del horario
$horario->setEstado('ocupado');
// Persistir
$entityManager->persist($appointment);
// Flush
$entityManager->flush();
$language = $request->getSession()->get('_locale', 'es');
// Notificar si el servicio está disponible
if ($this->appointmentNotifier) {
$this->appointmentNotifier->sendAppointmentNotification($appointment, 'create', $language);
}
// Notificar via sms
try {
$patient = $appointment->getPatient();
if ($patient && $patient->getPhone()) {
$to = $patient->getPhone();
$patientName = $patient->getName() . ' ' . $patient->getLastName();
// OBTENER FECHA Y HORA DESDE EL HORARIO
$horario = $appointment->getHorario();
$fechaHorario = $horario->getFecha();
$horaInicio = $horario->getHora();
// Formatear fecha y hora
$appointmentDate = $fechaHorario->format('m/d/Y');
$appointmentTime = $horaInicio->format('H:i');
// Así se usa - CORRECTO ✅
$text = $this->appointmentNotifierSMS->getAppointmentConfirmationText($patientName, $appointmentDate, $appointmentTime);
$this->appointmentNotifierSMS->sendQuickMessage($to, $text);
$this->addFlash('success', 'SMS de confirmación enviado correctamente.');
} else {
$this->addFlash('warning', 'Cita creada pero no se pudo enviar SMS: paciente sin teléfono registrado.');
}
} catch (\Exception $smsException) {
// Log del error pero no interrumpir el flujo
error_log("Error enviando SMS: " . $smsException->getMessage());
$this->addFlash('warning', 'Cita creada pero hubo un error al enviar el SMS de confirmación.');
}
$this->addFlash('success', 'Cita creada correctamente.');
return $this->redirectToRoute('admin_appointment', [], Response::HTTP_SEE_OTHER);
} catch (\Exception $e) {
$this->addFlash('error', 'Error al crear la cita: ' . $e->getMessage());
}
} elseif ($form->isSubmitted() && !$form->isValid()) {
// Mostrar errores de validación
$errors = [];
foreach ($form->getErrors(true) as $error) {
$errors[] = $error->getMessage();
}
$this->addFlash('error', 'Errores en el formulario: ' . implode(', ', $errors));
}
return $this->render('admin/appointment/form.html.twig', [
'appointment' => $appointment,
'form' => $form->createView(),
'modo' => 'new',
'seccion' => 'appointments',
'titulo_pagina' => 'Nueva Cita'
]);
}
private function getAppointmentConfirmationText(string $patientName, string $date, string $time): string
{
$nameParts = explode(' ', $patientName);
$apellido = end($nameParts);
$template = "Imaging Pro\n" .
"Sr./Sra. %s, confirmamos su cita médica: %s a las %s\n" .
"800 W Airport Fwy, Suite 1033\n" .
"Estacionar atrás gratis\n" .
"Elevador al piso 10";
return sprintf($template, $apellido, $date, $time);
}
/**
* @Route("/calendar", name="admin_appointment_calendar", methods={"GET"})
*/
public function calendar(): Response
{
return $this->render('admin/appointment/calendar.html.twig', [
'seccion' => 'appointments',
'titulo_pagina' => 'Calendario de Citas'
]);
}
// /**
// * @Route("/events", name="admin_appointment_events", methods={"GET"})
// */
// public function events(Request $request, AppointmentRepository $repo): JsonResponse
// {
// $start = new \DateTime($request->query->get('start'));
// $end = new \DateTime($request->query->get('end'));
// $mode = $request->query->get('mode', 'preview');
// $appointments = $repo->findBetween($start, $end);
// $estadoColorMap = [
// 'confirmada' => '#28a745',
// 'cancelada_paciente' => '#dc3545',
// 'cancelada_clinica' => '#842029',
// 'completada' => '#0d6efd',
// 'ausente' => '#6c757d',
// ];
// $grouped = [];
// foreach ($appointments as $a) {
// $horario = $a->getHorario();
// if (!$horario || !$horario->getFecha() || !$horario->getHora()) {
// continue;
// }
// $fecha = $horario->getFecha()->format('Y-m-d');
// $hora = $horario->getHora();
// $startDateTime = (new \DateTime())
// ->setDate(
// (int)$horario->getFecha()->format('Y'),
// (int)$horario->getFecha()->format('m'),
// (int)$horario->getFecha()->format('d')
// )
// ->setTime(
// (int)$hora->format('H'),
// (int)$hora->format('i'),
// (int)$hora->format('s')
// );
// // Duración dinámica desde la configuración del horario
// $duracion = method_exists($horario, 'getDuracion') && is_numeric($horario->getDuracion())
// ? (int)$horario->getDuracion()
// : 30; // valor por defecto en minutos
// $endDateTime = (clone $startDateTime)->modify("+{$duracion} minutes");
// $endOfDay = (clone $startDateTime)->setTime(23, 59, 59);
// if ($endDateTime > $endOfDay) {
// $endDateTime = $endOfDay;
// }
// $event = [
// 'title' => sprintf('🩺 %s', $a->getPatient()->getName()),
// 'start' => $startDateTime->format('Y-m-d\TH:i:s'),
// 'end' => $endDateTime->format('Y-m-d\TH:i:s'),
// 'backgroundColor' => $estadoColorMap[$a->getStatus()] ?? '#000000',
// 'status' => $a->getStatus(),
// ];
// $grouped[$fecha][] = $event;
// }
// if ($mode === 'full') {
// // Devolver todos los eventos planos (sin "Ver más")
// return new JsonResponse(array_merge(...array_values($grouped)));
// }
// // Modo preview (solo para vista mensual)
// $response = [];
// foreach ($grouped as $day => $events) {
// $response[] = [
// 'date' => $day,
// 'preview' => array_slice($events, 0, 1),
// 'total' => count($events),
// 'hasMore' => count($events) > 1,
// ];
// }
// return new JsonResponse($response);
// }
/**
* @Route("/events", name="admin_appointment_events", methods={"GET"})
*/
public function events(Request $request, AppointmentRepository $repo): JsonResponse
{
$start = new \DateTime($request->query->get('start'));
$end = new \DateTime($request->query->get('end'));
$mode = $request->query->get('mode', 'preview');
$appointments = $repo->findBetween($start, $end);
// MAPEO PROFESIONAL DE TIPOS DE ESTUDIO
$studyTypeMapping = [
'rayos-x' => 'Rayos X Digital',
'ultrasonido'=> 'Ultrasonido 4D',
'tomografia'=> 'Tomografía Computarizada',
'resonancia'=> 'Resonancia Magnética',
'mamografia'=> 'Mamografía Digital',
'nuclear'=> 'Medicina Nuclear',
'radiografia'=> 'Radiografía',
'ultrasonido-abdominal'=> 'Ultrasonido abdominal',
'ultrasonido-mama'=> 'Ultrasonido de mama',
'ecocardiograma'=> 'Ecocardiograma',
'ultrasonido-musculoesqueletico'=> 'Ultrasonido musculoesquelético',
'ultrasonido-obstetrico'=> 'Ultrasonido obstétrico',
'arteriografia-venografia'=> 'Arteriografía y venografía',
'ultrasonido-intravascular'=> 'Ultrasonido intravascular (IVU)',
'ultrasonido-vascular-periferico'=> 'Ultrasonido vascular periférico',
'ultrasonido-vascular'=> 'Ultrasonido vascular',
'ultrasonido-pelvico'=> '>Ultrasonido pélvico',
'ultrasonido-prostata'=> 'Ultrasonido de próstata',
'ultrasonido-renal'=> 'Ultrasonido renal',
'ultrasonido-escrotal'=> 'Ultrasonido escrotal',
'ultrasonido-tiroideo'=> 'Ultrasonido tiroideo'
];
$events = [];
foreach ($appointments as $a) {
$horario = $a->getHorario();
if (!$horario || !$horario->getFecha() || !$horario->getHora()) {
continue;
}
$fecha = $horario->getFecha()->format('Y-m-d');
$hora = $horario->getHora();
$startDateTime = (new \DateTime())
->setDate(
(int)$horario->getFecha()->format('Y'),
(int)$horario->getFecha()->format('m'),
(int)$horario->getFecha()->format('d')
)
->setTime(
(int)$hora->format('H'),
(int)$hora->format('i'),
(int)$hora->format('s')
);
// Duración dinámica desde la configuración del horario
$duracion = method_exists($horario, 'getDuracion') && is_numeric($horario->getDuracion())
? (int)$horario->getDuracion()
: 30;
$endDateTime = (clone $startDateTime)->modify("+{$duracion} minutes");
$endOfDay = (clone $startDateTime)->setTime(23, 59, 59);
if ($endDateTime > $endOfDay) {
$endDateTime = $endOfDay;
}
$patient = $a->getPatient();
// OBTENER Y FORMATEAR EL TIPO DE ESTUDIO
$rawStudyType = method_exists($a, 'getTipoEstudio') ? $a->getTipoEstudio() : null;
$formattedStudyType = $this->formatStudyType($rawStudyType, $studyTypeMapping);
$event = [
'id' => $a->getId(),
'title' => $patient ? $this->formatPatientName($patient->getName()) : 'Paciente no especificado',
'start' => $startDateTime->format('Y-m-d\TH:i:s'),
'end' => $endDateTime->format('Y-m-d\TH:i:s'),
'date' => $fecha,
'time' => $hora->format('H:i'),
'duration' => $duracion,
'status' => $a->getStatus(),
'patient' => $patient ? [
'id' => $patient->getId(),
'name' => $this->formatPatientName($patient->getName()),
'email' => method_exists($patient, 'getEmail') ? $patient->getEmail() : null,
'phone' => method_exists($patient, 'getPhone') ? $patient->getPhone() : null,
] : null,
'professional' => method_exists($a, 'getProfessional') ? [
'id' => $a->getProfessional()->getId(),
'name' => $this->formatProfessionalName($a->getProfessional()->getName()),
] : null,
'notes' => method_exists($a, 'getNotes') ? $a->getNotes() : null,
'service' => $formattedStudyType,
'raw_service' => $rawStudyType, // Mantener el original para referencia
];
$events[] = $event;
}
if ($mode === 'grouped') {
$grouped = [];
foreach ($events as $event) {
$grouped[$event['date']][] = $event;
}
$response = [];
foreach ($grouped as $day => $dayEvents) {
$response[] = [
'date' => $day,
'appointments' => $dayEvents,
'total' => count($dayEvents),
];
}
return new JsonResponse($response);
}
return new JsonResponse([
'success' => true,
'total' => count($events),
'appointments' => $events,
'date_range' => [
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d')
]
]);
}
/**
* Formatea el tipo de estudio según el mapeo profesional - CORREGIDO
*/
private function formatStudyType(?string $rawStudyType, array $mapping): string
{
if (empty($rawStudyType)) {
return 'Consulta General'; // Valor por defecto
}
// Normalizar el texto para comparación EXACTA
$normalized = strtolower(trim($rawStudyType));
$normalized = preg_replace('/[^a-z0-9\-]/', '-', $normalized);
// Buscar coincidencia EXACTA primero
if (isset($mapping[$normalized])) {
return $mapping[$normalized];
}
// Si no hay coincidencia exacta, buscar por coincidencia parcial PERO con prioridad
// Ordenar por longitud de clave (más específico primero) para evitar que "ultrasonido" genérico capture todo
$sortedKeys = array_keys($mapping);
usort($sortedKeys, function($a, $b) {
return strlen($b) - strlen($a); // Más largo primero
});
foreach ($sortedKeys as $key) {
if (strpos($normalized, $key) !== false) {
return $mapping[$key];
}
}
// Si no se encuentra, formatear el texto original
return $this->formatFallbackStudyType($rawStudyType);
}
/**
* Formatea nombres de estudio que no están en el mapeo
*/
private function formatFallbackStudyType(string $studyType): string
{
// Limpiar y capitalizar
$cleaned = trim($studyType);
$cleaned = ucwords(strtolower($cleaned));
// Reemplazar caracteres no deseados
$cleaned = preg_replace('/[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ\s]/', ' ', $cleaned);
$cleaned = preg_replace('/\s+/', ' ', $cleaned);
return $cleaned;
}
/**
* Formatea el nombre del paciente
*/
private function formatPatientName(?string $name): string
{
if (empty($name)) {
return 'Paciente no especificado';
}
return ucwords(strtolower(trim($name)));
}
/**
* Formatea el nombre del profesional
*/
private function formatProfessionalName(?string $name): string
{
if (empty($name)) {
return 'Profesional no asignado';
}
// Asumir formato "Dr. Juan Pérez" o similar
$formatted = trim($name);
// Si no tiene título, agregar "Dr." por defecto
if (!preg_match('/^(dr|dra|dr\.|dra\.)\s+/i', $formatted)) {
$formatted = 'Dr. ' . $formatted;
}
return $formatted;
}
/**
* @Route("/cancel/token/{token}", name="appointment_cancel_token_no", methods={"GET", "POST"})
*/
public function cancelByToken(
string $token,
AppointmentRepository $appointmentRepo,
AppointmentNotifier $notifier,
HorarioRepository $horario,
Request $request
): Response {
$appointment = $appointmentRepo->findOneBy(['cancelToken' => $token]);
// Validaciones centralizadas
$validationResult = $this->validateTokenAccess($appointment, 'cancelada');
if ($validationResult) {
return $validationResult;
}
// Si es POST (confirmación), procesar cancelación
if ($request->isMethod('POST')) {
return $this->processCancellation($appointment, $notifier, $horario);
}
// Si es GET, mostrar página de confirmación
return $this->render('admin/appointment/cancel_confirm.html.twig', [
'appointment' => $appointment,
'token' => $token
]);
}
/**
* @Route("/reprogram/token/{token}", name="appointment_reprogram_token_no", methods={"GET", "POST"})
*/
public function reprogramByToken(
string $token,
AppointmentRepository $appointmentRepo,
Request $request,
HorarioRepository $horario,
AppointmentNotifier $notifier
): Response {
$appointment = $appointmentRepo->findOneBy(['reprogramToken' => $token]);
// Validaciones centralizadas
$validationResult = $this->validateTokenAccess($appointment, 'reprogramada');
if ($validationResult) {
return $validationResult;
}
// Si es POST, procesar reprogramación
if ($request->isMethod('POST')) {
return $this->processReprogramming($appointment, $request, $horario, $notifier);
}
// Si es GET, mostrar formulario de reprogramación
return $this->render('admin/appointment/reprogram_form.html.twig', [
'appointment' => $appointment,
'token' => $token
]);
}
private function validateTokenAccess(?Appointment $appointment, string $action): ?Response
{
// Token no válido o ya usado
if (!$appointment) {
return $this->render('admin/appointment/token_invalid.html.twig', [
'action' => $action
]);
}
// Ya procesada anteriormente
if (str_starts_with($appointment->getStatus(), $action)) {
return $this->render('admin/appointment/already_processed.html.twig', [
'appointment' => $appointment,
'action' => $action
]);
}
// Cita ya cancelada (para evitar conflictos)
if (str_starts_with($appointment->getStatus(), 'cancelada') && $action !== 'cancelada') {
return $this->render('admin/appointment/already_cancelled.html.twig', [
'appointment' => $appointment
]);
}
return null; // Todo válido
}
private function processCancellation(Appointment $appointment, AppointmentNotifier $notifier, HorarioRepository $horarioRepo): Response
{
try {
$this->em->beginTransaction();
// Marcar como cancelada por paciente
$appointment->setStatus('cancelada_paciente');
// Liberar horario
$horario = $horarioRepo->find($appointment->getHorario()->getId());
$horario->setEstado('disponible');
$appointment->setCancelToken(null); // Invalidar token
$this->em->flush();
$this->em->commit();
// Notificar cancelación
$language = $request->getSession()->get('_locale', 'es');
$notifier->sendAppointmentNotification($appointment, 'cancel', $language);
// Registrar en logs
$this->addFlash('success', 'Cita cancelada exitosamente.');
return $this->render('admin/appointment/cancel_success.html.twig', [
'appointment' => $appointment
]);
} catch (\Exception $e) {
$this->em->rollback();
$this->addFlash('error', 'Error al cancelar la cita. Por favor, intente nuevamente.');
return $this->render('admin/appointment/cancel_confirm.html.twig', [
'appointment' => $appointment,
'token' => $appointment->getCancelToken(),
'error' => true
]);
}
}
private function processReprogramming(
Appointment $appointment,
Request $request,
HorarioRepository $horarioRepo,
AppointmentNotifier $notifier
): Response
{
try {
$newDate = $request->request->get('new_date');
$newTime = $request->request->get('new_time');
// Validar datos del formulario
if (!$newDate || !$newTime) {
throw new \InvalidArgumentException('Fecha y hora son requeridos');
}
// Validar que la fecha no sea en el pasado
$newDateTime = new \DateTime($newDate . ' ' . $newTime);
$now = new \DateTime();
if ($newDateTime < $now) {
throw new \InvalidArgumentException('No puede reprogramar para una fecha/hora en el pasado');
}
$this->em->beginTransaction();
// Buscar horario disponible para la nueva fecha y hora
$nuevoHorario = $horarioRepo->findAvailableByDateTime($newDate, $newTime);
if (!$nuevoHorario) {
throw new \InvalidArgumentException('El horario seleccionado no está disponible');
}
// Validar que no sea el mismo horario actual
$horarioActual = $appointment->getHorario();
if ($nuevoHorario->getId() === $horarioActual->getId()) {
throw new \InvalidArgumentException('No puede reprogramar al mismo horario actual');
}
// Liberar el horario actual (marcar como disponible)
$horarioActual->setEstado('disponible');
// Ocupar el nuevo horario
$nuevoHorario->setEstado('ocupado');
// Actualizar la cita con el nuevo horario
$appointment->setHorario($nuevoHorario);
$appointment->setStatus('reprogramada');
$appointment->setReprogramToken(null); // Invalidar token
$this->em->flush();
$this->em->commit();
// Notificar reprogramación
$language = $request->getSession()->get('_locale', 'es');
$notifier->sendAppointmentNotification($appointment, 'reschedule', $language);
$this->addFlash('success', 'Cita reprogramada exitosamente.');
return $this->render('admin/appointment/reprogram_success.html.twig', [
'appointment' => $appointment,
'newDate' => $newDateTime,
'nuevoHorario' => $nuevoHorario
]);
} catch (\Exception $e) {
if ($this->em->getConnection()->isTransactionActive()) {
$this->em->rollback();
}
$this->addFlash('error', 'Error al reprogramar la cita: ' . $e->getMessage());
return $this->render('admin/appointment/reprogram_form.html.twig', [
'appointment' => $appointment,
'token' => $appointment->getReprogramToken(),
'error' => true,
'error_message' => $e->getMessage()
]);
}
}
/**
* @Route("/{id}/status", name="admin_appointment_status", methods={"POST"})
*/
public function changeStatus(Appointment $appointment, Request $request, EntityManagerInterface $em): JsonResponse
{
$newStatus = $request->request->get('status');
$reason = $request->request->get('reason', '');
// Validación más robusta para el estado
if ($newStatus === null || $newStatus === '') {
return new JsonResponse([
'success' => false,
'message' => 'El estado no puede estar vacío'
], 400);
}
// Convertir a string y limpiar
$newStatus = (string) trim($newStatus);
// Validar estados permitidos
$allowedStatuses = ['confirmada', 'completada', 'cancelada_paciente', 'cancelada_clinica', 'ausente'];
if (!in_array($newStatus, $allowedStatuses)) {
return new JsonResponse([
'success' => false,
'message' => 'Estado no válido: ' . $newStatus
], 400);
}
try {
// Cambiar estado de la cita
$appointment->setStatus($newStatus);
$currentNotes = $appointment->getNotes() ?? '';
$timestamp = (new \DateTime())->format('d/m/Y H:i');
// Manejar observaciones según el tipo de estado
if (!empty($reason) && $reason !== 'Sin observaciones') {
$statusLabels = [
'completada' => 'COMPLETADA',
'ausente' => 'AUSENTE',
'cancelada_paciente' => 'CANCELACIÓN PACIENTE',
'cancelada_clinica' => 'CANCELACIÓN CLÍNICA'
];
$statusLabel = $statusLabels[$newStatus] ?? strtoupper($newStatus);
if (!empty($currentNotes)) {
$newNotes = $currentNotes . "\n\n--- {$statusLabel} ({$timestamp}) ---\n" . $reason;
} else {
$newNotes = "--- {$statusLabel} ({$timestamp}) ---\n" . $reason;
}
$appointment->setNotes($newNotes);
}
// Si es cancelada, liberar horario
if (str_starts_with($newStatus, 'cancelada')) {
$horario = $appointment->getHorario();
if ($horario) {
$horario->setEstado('disponible');
}
}
$em->flush();
return new JsonResponse([
'success' => true,
'status' => $newStatus,
'message' => 'Estado actualizado correctamente'
]);
} catch (\Exception $e) {
return new JsonResponse([
'success' => false,
'message' => 'Error al actualizar el estado: ' . $e->getMessage()
], 500);
}
}
/**
* @Route("/{id}", name="admin_appointment_show", methods={"GET"})
*/
public function show(Appointment $appointment): Response
{
return $this->render('admin/appointment/show.html.twig', [
'appointment' => $appointment,
'seccion' => 'appointments',
'titulo_pagina' => 'Mostrar Cita'
]);
}
// /**
// * @Route("/{id}/reprogramar", name="admin_appointment_reschedule", methods={"GET", "POST"})
// */
// public function reschedule(Request $request, Appointment $appointment, EntityManagerInterface $entityManager, HorarioRepository $horarioRepo) : Response
// {
// $originalHorario = $appointment->getHorario();
// $originalDate = $originalHorario->getFecha();
// $originalTime = $originalHorario->getHora();
// $provider = $this->getUser();
// // SOLUCIÓN: Eliminar las opciones no definidas
// $form = $this->createForm(RescheduleAppointmentType::class, $appointment, [
// 'provider' => $provider,
// 'is_reschedule' => true,
// 'context' => 'admin',
// ]);
// $form->handleRequest($request);
// if ($form->isSubmitted() && $form->isValid()) {
// try {
// // Liberar el horario anterior si existe
// if ($originalHorario && $originalHorario->getId() !== $appointment->getHorario()->getId()) {
// $originalHorario->setEstado('disponible');
// $entityManager->persist($originalHorario);
// }
// // Ocupar el nuevo horario
// $nuevoHorario = $appointment->getHorario();
// $nuevoHorario->setEstado('ocupado');
// // Actualizar estado y datos
// $appointment->setStatus('reprogramada');
// $appointment->setProvider($this->getUser());
// $entityManager->flush();
// $language = $request->getSession()->get('_locale', 'es');
// if ($this->appointmentNotifier) {
// $this->appointmentNotifier->sendAppointmentNotification($appointment, 'reschedule', $language);
// }
// $this->addFlash('success', 'Cita reprogramada correctamente.');
// return $this->redirectToRoute('admin_appointment', [], Response::HTTP_SEE_OTHER);
// } catch (\Exception $e) {
// $this->addFlash('error', 'Error al reprogramar la cita: ' . $e->getMessage());
// }
// }
// return $this->renderForm('admin/appointment/reschedule.html.twig', [
// 'appointment' => $appointment,
// 'form' => $form,
// 'original_date' => $originalDate,
// 'original_time' => $originalTime,
// 'modo' => 'reschedule',
// 'seccion' => 'appointments',
// 'titulo_pagina' => 'Reprogramar Cita'
// ]);
// }
/**
* @Route("/{id}/reprogramar", name="admin_appointment_reschedule", methods={"GET", "POST"})
*/
public function reschedule(Request $request, Appointment $appointment, EntityManagerInterface $entityManager, HorarioRepository $horarioRepo): Response
{
$originalHorario = $appointment->getHorario();
$originalDate = clone $originalHorario->getFecha();
$originalTime = clone $originalHorario->getHora();
$provider = $this->getUser();
// Crear formulario
$form = $this->createForm(RescheduleAppointmentType::class, $appointment, [
'provider' => $provider,
'is_reschedule' => true,
'context' => 'admin',
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
// Obtener datos del formulario
$horarioSeleccionado = $appointment->getHorario();
$notas = $form->get('notes')->getData() ?? '';
// NO uses $nuevaFecha = $form->get('fecha')->getData();
// La fecha CORRECTA está en: $horarioSeleccionado->getFecha()
// La hora CORRECTA está en: $horarioSeleccionado->getHora()
if (!$horarioSeleccionado) {
$this->addFlash('error', 'Debe seleccionar un nuevo horario.');
return $this->redirectToRoute('admin_appointment_reschedule', [
'id' => $appointment->getId()
]);
}
// 1. VERIFICAR SI ES EL MISMO HORARIO (comparando IDs)
if ($horarioSeleccionado->getId() === $originalHorario->getId()) {
$this->addFlash('warning', 'Seleccionó el mismo horario actual.');
return $this->redirectToRoute('admin_appointment_reschedule', ['id' => $appointment->getId()]);
}
// 2. VERIFICAR QUE EL HORARIO SELECCIONADO ESTÉ DISPONIBLE
if ($horarioSeleccionado->getEstado() !== 'disponible') {
$this->addFlash('error', 'El horario seleccionado ya no está disponible. Por favor, seleccione otro.');
return $this->redirectToRoute('admin_appointment_reschedule', ['id' => $appointment->getId()]);
}
// 3. OCUPAR EL NUEVO HORARIO
$horarioSeleccionado->setEstado('ocupado');
$entityManager->persist($horarioSeleccionado);
// 4. LIBERAR EL HORARIO ANTERIOR
$originalHorario->setEstado('disponible');
$entityManager->persist($originalHorario);
// 5. ACTUALIZAR LA CITA CON EL NUEVO HORARIO (¡con su fecha y hora!)
$appointment->setHorario($horarioSeleccionado); // ← Esto actualiza AMBOS: fecha y hora
$appointment->setStatus('reprogramada');
$appointment->setUpdatedAt(new \DateTimeImmutable());
// 6. AGREGAR NOTAS DE REPROGRAMACIÓN
$notasReprogramacion = sprintf(
"\n=== REPROGRAMACIÓN ===\n" .
"Fecha cambio: %s\n" .
"Original: %s %s\n" .
"Nuevo: %s %s\n" . // ← Usa la fecha del horario seleccionado
"Motivo: %s\n" .
"========================\n",
(new \DateTime())->format('d/m/Y H:i'),
$originalDate->format('d/m/Y'),
$originalTime->format('H:i'),
$horarioSeleccionado->getFecha()->format('d/m/Y'), // ← AQUÍ
$horarioSeleccionado->getHora()->format('H:i'), // ← AQUÍ
$notas ?: 'Sin motivo especificado'
);
$notasActuales = $appointment->getNotes() ?? '';
$appointment->setNotes($notasReprogramacion . $notasActuales);
// 7. GUARDAR
$entityManager->flush();
// 8. DEBUG: Verificar que se guardó correctamente
error_log("Reprogramación exitosa:");
error_log("- Cita ID: " . $appointment->getId());
error_log("- Nuevo Horario ID: " . $horarioSeleccionado->getId());
error_log("- Nueva Fecha: " . $horarioSeleccionado->getFecha()->format('Y-m-d'));
error_log("- Nueva Hora: " . $horarioSeleccionado->getHora()->format('H:i'));
error_log("- Estado: " . $appointment->getStatus());
// 9. ENVIAR NOTIFICACIÓN
$language = $request->getSession()->get('_locale', 'es');
if ($this->appointmentNotifier) {
$this->appointmentNotifier->sendAppointmentNotification($appointment, 'reschedule', $language);
}
$this->addFlash('success', 'Cita reprogramada correctamente.');
return $this->redirectToRoute('admin_appointment', [], Response::HTTP_SEE_OTHER);
} catch (\Exception $e) {
error_log('ERROR REPROGRAMACIÓN: ' . $e->getMessage());
error_log('Trace: ' . $e->getTraceAsString());
$this->addFlash('error', 'Error al reprogramar la cita: ' . $e->getMessage());
}
}
return $this->render('admin/appointment/reschedule.html.twig', [
'appointment' => $appointment,
'form' => $form->createView(),
'original_date' => $originalDate,
'original_time' => $originalTime,
'modo' => 'reschedule',
'seccion' => 'appointments',
'titulo_pagina' => 'Reprogramar Cita'
]);
}
/**
* @Route("/{id}/edit", name="admin_appointment_edit", methods={"GET", "POST"})
*/
public function edit(Request $request, Appointment $appointment, EntityManagerInterface $entityManager, HorarioRepository $horarioRepo): Response
{
$form = $this->createForm(AppointmentType::class, $appointment, [
'provider' => null,
'is_edit' => true,
'context' => 'admin'
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Asignar automáticamente el admin como proveedor
$appointment->setProvider($this->getUser());
// Asignar automáticamente el admin como proveedor
$appointment->setStatus('confirmada');
$appointment->setPaymentMethod('imaging_pro');
$horario = $horarioRepo->find($appointment->getHorario()->getId());
$horario->setEstado('ocupado');
$appointment->setUpdatedAt(new \DateTimeImmutable());
$entityManager->flush();
// Notificar
$language = $request->getSession()->get('_locale', 'es');
$this->appointmentNotifier->sendAppointmentNotification($appointment, 'reschedule', $language);
$this->addFlash('info', 'Cita actualizado correctamente.');
return $this->redirectToRoute('admin_appointment', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('admin/appointment/form.html.twig', [
'appointment' => $appointment,
'form' => $form,
'modo' => 'edit',
'seccion' => 'appointments',
'titulo_pagina' => 'Editar Cita'
]);
}
/**
* @Route("/{id}", name="admin_appointment_delete", methods={"POST"})
*/
public function delete(Request $request, $id, EntityManagerInterface $entityManager, HorarioRepository $horarioRepo): Response
{
// Buscar la cita manualmente en lugar de usar ParamConverter
$appointment = $entityManager->getRepository(Appointment::class)->find($id);
// Verificar si la cita existe
if (!$appointment) {
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'success' => false,
'message' => 'La cita no existe o ya fue eliminada'
], 404);
}
$this->addFlash('warning', 'La cita no existe o ya fue eliminada.');
return $this->redirectToRoute('admin_appointment');
}
// Verificar token CSRF
if (!$this->isCsrfTokenValid('delete'.$appointment->getId(), $request->request->get('_token'))) {
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'success' => false,
'message' => 'Token CSRF inválido'
], 400);
}
$this->addFlash('error', 'Token CSRF inválido');
return $this->redirectToRoute('admin_appointment');
}
try {
$appointmentId = $appointment->getId();
$entityManager->remove($appointment);
$horario = $horarioRepo->find($appointment->getHorario()->getId());
$horario->setEstado('disponible');
$entityManager->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'success' => true,
'message' => 'Cita eliminada correctamente',
'appointment_id' => $appointmentId
]);
}
$this->addFlash('success', 'Cita eliminada correctamente');
} catch (\Exception $e) {
// En caso de error durante la eliminación
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'success' => false,
'message' => 'Error al eliminar la cita: ' . $e->getMessage()
], 500);
}
$this->addFlash('error', 'Error al eliminar la cita: ' . $e->getMessage());
}
return $this->redirectToRoute('admin_appointment');
}
}