<?php
namespace App\Controller;
use App\Entity\Appointment;
use App\Entity\User;
use App\Repository\AppointmentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\UserRepository;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\HorarioRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Form\AdminProfileType;
use App\Form\ChangePasswordType;
use App\Form\ChangeEmailFormType;
use SebastianBergmann\Environment\Console;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Form\FormError;
/**
* @Route("/admin", name="admin")
*/
class AdminController extends AbstractController
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @Route("", name="_index")
*/
public function index(): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
return $this->render('admin/dashboard.html.twig', [
'seccion' => 'dashboard',
'titulo_pagina' => 'Panel de Administración'
]);
}
/**
* @Route("/resumen", name="_resumen", methods={"GET"})
*/
public function resumen(
AppointmentRepository $citaRepo,
UserRepository $userRepo
): JsonResponse {
// Fechas de referencia
$hoy = new \DateTime('today');
$inicioSemana = (clone $hoy)->modify('monday this week');
$inicioMes = (clone $hoy)->modify('first day of this month');
$inicioAnno = (clone $hoy)->modify('first day of january');
// Función auxiliar para contar citas por rango y estado
$contarCitas = function(\DateTime $inicio, $estado = null) use ($citaRepo) {
$fin = new \DateTime('tomorrow');
return $citaRepo->contarPorRango($inicio, $fin, $estado);
};
// Contar todos los usuarios
$contarUsuarios = function(\DateTime $inicio) use ($userRepo) {
$fin = new \DateTime('tomorrow');
return $userRepo->contarPorRango($inicio, $fin);
};
// Contar proveedores filtrando por rol
$contarProveedores = function(\DateTime $inicio) use ($userRepo) {
$fin = new \DateTime('tomorrow');
return $userRepo->contarPorRango($inicio, $fin, 'ROLE_PROVIDER');
};
$data = [
'citas' => [
'hoy' => $contarCitas($hoy),
'semana' => $contarCitas($inicioSemana),
'mes' => $contarCitas($inicioMes),
'anno' => $contarCitas($inicioAnno),
],
'usuarios' => [
'hoy' => $contarUsuarios($hoy),
'semana' => $contarUsuarios($inicioSemana),
'mes' => $contarUsuarios($inicioMes),
'anno' => $contarUsuarios($inicioAnno),
],
'proveedores' => [
'hoy' => $contarProveedores($hoy),
'semana' => $contarProveedores($inicioSemana),
'mes' => $contarProveedores($inicioMes),
'anno' => $contarProveedores($inicioAnno),
],
'completadas' => [
'hoy' => $contarCitas($hoy, 'completada'),
'semana' => $contarCitas($inicioSemana, 'completada'),
'mes' => $contarCitas($inicioMes, 'completada'),
'anno' => $contarCitas($inicioAnno, 'completada'),
],
'canceladas' => [
'hoy' => $contarCitas($hoy, 'cancelada'),
'semana' => $contarCitas($inicioSemana, 'cancelada'),
'mes' => $contarCitas($inicioMes, 'cancelada'),
'anno' => $contarCitas($inicioAnno, 'cancelada'),
],
];
return $this->json($data);
}
/**
* @Route("/users", name="_users")
*/
public function users(Request $request, UserRepository $userRepository, PaginatorInterface $paginator): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
// Capturamos el texto de búsqueda desde la URL (?q=...)
$q = $request->query->get('q');
// Creamos un QueryBuilder para poder filtrar
$queryBuilder = $userRepository->createQueryBuilder('u');
if ($q) {
$queryBuilder
->andWhere('u.name LIKE :q OR u.lastName LIKE :q OR u.email LIKE :q OR u.phone LIKE :q OR u.address LIKE :q OR u.roles LIKE :q')
->setParameter('q', "%$q%");
}
// Paginamos el resultado
$pagination = $paginator->paginate(
$queryBuilder, // Query o QueryBuilder
$request->query->getInt('page', 1), // Página actual
10, // Elementos por página
[
'params' => [
'q' => $q,
'page' => $request->query->getInt('page', 1)
]
]
);
return $this->render('admin/user/index.html.twig', [
'users' => $pagination,
'q' => $q,
'seccion' => 'users',
'titulo_pagina' => 'Gestión de Usuarios'
]);
}
/**
* @Route("/pacientes", name="_pacientes")
*/
public function pacientes(Request $request, UserRepository $userRepository, PaginatorInterface $paginator): Response
{
$q = $request->query->get('q');
$providerId = $request->query->get('provider');
$queryBuilder = $userRepository->createQueryBuilder('patient')
->select('patient')
->where('patient.roles LIKE :role')
->setParameter('role', '%ROLE_CLIENT%')
//->orderBy('patient.name', 'ASC');
->orderBy('patient.id', 'DESC'); // ← CAMBIO: Ordenar por ID descendente
if ($q) {
// Dividir la búsqueda en palabras individuales
$searchTerms = explode(' ', trim($q));
$orConditions = [];
$parameters = [];
foreach ($searchTerms as $key => $term) {
if (!empty($term)) {
$paramName = 'term_' . $key;
// Buscar en cada campo individualmente
$orConditions[] = "patient.name LIKE :$paramName";
$orConditions[] = "patient.lastName LIKE :$paramName";
$orConditions[] = "patient.email LIKE :$paramName";
$orConditions[] = "patient.phone LIKE :$paramName";
$orConditions[] = "patient.address LIKE :$paramName";
$parameters[$paramName] = '%' . $term . '%';
}
}
if (!empty($orConditions)) {
$queryBuilder->andWhere(implode(' OR ', $orConditions));
foreach ($parameters as $paramName => $paramValue) {
$queryBuilder->setParameter($paramName, $paramValue);
}
}
}
// Filtro por proveedor - INCLUYENDO ROLE_ADMIN
if ($providerId) {
$queryBuilder
->innerJoin(
'App\Entity\User',
'provider',
'WITH',
'provider.id = :providerId AND (provider.roles LIKE :providerRole OR provider.roles LIKE :adminRole)'
)
->innerJoin(
'provider.patients',
'patient_relation'
)
->andWhere('patient_relation.id = patient.id')
->setParameter('providerId', $providerId)
->setParameter('providerRole', '%ROLE_PROVIDER%')
->setParameter('adminRole', '%ROLE_ADMIN%');
}
$users = $paginator->paginate(
$queryBuilder->getQuery(),
$request->query->getInt('page', 1),
10
);
// Obtener proveedores para cada paciente
$patientsWithProviders = [];
foreach ($users as $user) {
$provider = $userRepository->findProviderForPatient($user->getId());
$patientsWithProviders[$user->getId()] = $provider;
}
// Obtener lista de proveedores para el select - INCLUYENDO ADMINS
$providers = $userRepository->createQueryBuilder('u')
->where('u.roles LIKE :providerRole OR u.roles LIKE :adminRole')
->setParameter('providerRole', '%ROLE_PROVIDER%')
->setParameter('adminRole', '%ROLE_ADMIN%')
->orderBy('u.name', 'ASC')
->getQuery()
->getResult();
return $this->render('admin/user/pacientes.html.twig', [
'users' => $users,
'patientsWithProviders' => $patientsWithProviders,
'providers' => $providers,
'seccion' => 'pacientes',
'q' => $q,
'selectedProvider' => $providerId
]);
}
/**
* @Route("/proveedores", name="_proveedores")
*/
public function proveedores(Request $request, UserRepository $userRepository, PaginatorInterface $paginator): Response
{
$q = $request->query->get('q');
$queryBuilder = $userRepository->createQueryBuilder('u')
->where('u.roles LIKE :role_provider OR u.roles LIKE :role_admin')
->setParameter('role_provider', '%ROLE_PROVIDER%')
->setParameter('role_admin', '%ROLE_ADMIN%')
->orderBy('u.id', 'DESC');
if ($q) {
$searchTerm = trim($q);
$orConditions = [];
// PRIMERO: Búsqueda por la cadena COMPLETA en todos los campos
$orConditions[] = "u.name LIKE :full_term";
$orConditions[] = "u.email LIKE :full_term";
$orConditions[] = "u.phone LIKE :full_term";
$orConditions[] = "u.address LIKE :full_term";
$queryBuilder->setParameter('full_term', '%' . $searchTerm . '%');
// SEGUNDO: Solo si el término tiene espacios, buscar por palabras individuales
if (strpos($searchTerm, ' ') !== false) {
$searchTerms = explode(' ', $searchTerm);
foreach ($searchTerms as $key => $term) {
if (!empty($term) && strlen($term) > 1) { // Solo términos con más de 1 carácter
$paramName = 'term_' . $key;
$orConditions[] = "u.name LIKE :$paramName";
$orConditions[] = "u.email LIKE :$paramName";
$orConditions[] = "u.phone LIKE :$paramName";
$orConditions[] = "u.address LIKE :$paramName";
$queryBuilder->setParameter($paramName, '%' . $term . '%');
}
}
}
if (!empty($orConditions)) {
$queryBuilder->andWhere(implode(' OR ', $orConditions));
}
}
$users = $paginator->paginate(
$queryBuilder->getQuery(),
$request->query->getInt('page', 1),
10
);
$totalPacientesAsignados = 0;
foreach ($users as $user) {
$totalPacientesAsignados += $user->getPatients()->count();
}
return $this->render('admin/user/proveedores.html.twig', [
'users' => $users,
'seccion' => 'proveedores',
'totalPacientesAsignados' => $totalPacientesAsignados,
'q' => $q
]);
}
/**
* @Route("/appointment", name="_appointment")
*/
public function appointment(Request $request, EntityManagerInterface $em, PaginatorInterface $paginator): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$q = $request->query->get('q', '');
$providerId = $request->query->get('provider', '');
$status = $request->query->get('status', '');
$filter = $request->query->get('filter', ''); // Nuevo parámetro del submenú
$qb = $em->getRepository(Appointment::class)
->createQueryBuilder('u')
->leftJoin('u.horario', 'h')->addSelect('h')
->leftJoin('u.patient', 'p')->addSelect('p')
->leftJoin('u.provider', 'pr')->addSelect('pr');
// Filtro del submenú (tiene prioridad sobre el filtro de status individual)
if ($filter) {
switch ($filter) {
case 'confirmed':
$qb->andWhere('u.status = :filterStatus')
->setParameter('filterStatus', 'confirmada');
$activeFilter = 'confirmada';
break;
case 'completed':
$qb->andWhere('u.status IN (:completedStatus)')
->setParameter('completedStatus', ['completada', 'ausente']);
$activeFilter = 'completada';
break;
case 'cancelled':
// Incluir ambos tipos de cancelación
$qb->andWhere('u.status IN (:cancelledStatuses)')
->setParameter('cancelledStatuses', ['cancelada_paciente', 'cancelada_clinica']);
$activeFilter = 'cancelada';
break;
default:
$activeFilter = 'all';
}
} else {
$activeFilter = 'all';
// Filtro por estado individual (solo si no hay filtro del submenú)
if ($status) {
$qb->andWhere('u.status = :status')
->setParameter('status', $status);
}
}
// Filtro de búsqueda general
if ($q) {
// Dividir la búsqueda en palabras individuales
$searchTerms = explode(' ', trim($q));
$orConditions = [];
$parameters = [];
foreach ($searchTerms as $key => $term) {
if (!empty($term)) {
$paramName = 'term_' . $key;
// Buscar en cada campo individualmente
$orConditions[] = "u.status LIKE :$paramName";
$orConditions[] = "u.notes LIKE :$paramName";
$orConditions[] = "p.name LIKE :$paramName";
$orConditions[] = "p.lastName LIKE :$paramName";
$orConditions[] = "pr.name LIKE :$paramName";
$orConditions[] = "pr.lastName LIKE :$paramName";
$orConditions[] = "h.fecha LIKE :$paramName";
$orConditions[] = "h.hora LIKE :$paramName";
$orConditions[] = "CONCAT(p.name, ' ', p.lastName) LIKE :$paramName";
$orConditions[] = "CONCAT(pr.name, ' ', pr.lastName) LIKE :$paramName";
$parameters[$paramName] = '%' . $term . '%';
}
}
if (!empty($orConditions)) {
$qb->andWhere(implode(' OR ', $orConditions));
foreach ($parameters as $paramName => $paramValue) {
$qb->setParameter($paramName, $paramValue);
}
}
}
// Filtro por proveedor
if ($providerId) {
$qb->andWhere('pr.id = :providerId')
->setParameter('providerId', $providerId);
}
// Orden por fecha y hora descendente
$qb->orderBy('h.fecha', 'DESC')
->addOrderBy('h.hora', 'DESC');
// Obtener lista de proveedores para el select
$providers = $em->getRepository(User::class)
->createQueryBuilder('u')
->where('u.roles LIKE :providerRole OR u.roles LIKE :adminRole')
->setParameter('providerRole', '%ROLE_PROVIDER%')
->setParameter('adminRole', '%ROLE_ADMIN%')
->orderBy('u.name', 'ASC')
->getQuery()
->getResult();
// Obtener estados únicos de las citas para el select
$statuses = $em->getRepository(Appointment::class)
->createQueryBuilder('a')
->select('DISTINCT a.status')
->where('a.status IS NOT NULL')
->orderBy('a.status', 'ASC')
->getQuery()
->getResult();
// Extraer solo los valores de status
$statusValues = array_column($statuses, 'status');
$appointments = $qb->getQuery()->getResult();
// Paginamos el resultado
$pagination = $paginator->paginate(
$appointments,
$request->query->getInt('page', 1), // Página actual
10, // Elementos por página
[
'params' => [
'q' => $q,
'provider' => $providerId,
'status' => $status,
'filter' => $filter, // Incluir el filtro en la paginación
'page' => $request->query->getInt('page', 1)
]
]
);
return $this->render('admin/appointment/index.html.twig', [
'appointments' => $pagination,
'q' => $q,
'providers' => $providers,
'statuses' => $statusValues,
'selectedProvider' => $providerId,
'selectedStatus' => $status,
'active_filter' => $activeFilter, // Nuevo parámetro para el template
'seccion' => 'appointments',
'titulo_pagina' => 'Gestión de Citas'
]);
}
/**
* @Route("/horario", name="_horario")
*/
public function horario(HorarioRepository $horarioRepository, Request $request, PaginatorInterface $paginator): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
// Capturamos los parámetros de filtro desde la URL
$fecha = $request->query->get('fecha');
$hora = $request->query->get('hora');
$estado = $request->query->get('estado');
// Creamos un QueryBuilder para poder filtrar
$queryBuilder = $horarioRepository->createQueryBuilder('h')
->orderBy('h.fecha', 'DESC')
->addOrderBy('h.hora', 'ASC');
// Filtro por fecha específica
if ($fecha) {
try {
$fechaObj = new \DateTime($fecha);
$queryBuilder
->andWhere('h.fecha = :fecha')
->setParameter('fecha', $fechaObj->format('Y-m-d'));
} catch (\Exception $e) {
// Si la fecha no es válida, ignoramos el filtro
}
}
// Filtro por hora específica - SOLUCIÓN APLICADA
if ($hora) {
try {
$horaObj = new \DateTime($hora);
$queryBuilder
->andWhere('h.hora = :hora')
->setParameter('hora', $horaObj->format('H:i:s'));
} catch (\Exception $e) {
// Si la hora no es válida, ignoramos el filtro
}
}
// Filtro por estado
if ($estado) {
$queryBuilder
->andWhere('h.estado = :estado')
->setParameter('estado', $estado);
}
// Paginamos el resultado
$pagination = $paginator->paginate(
$queryBuilder, // QueryBuilder con filtros aplicados
$request->query->getInt('page', 1), // Página actual
10, // Elementos por página
[
'params' => [
'fecha' => $fecha,
'hora' => $hora,
'estado' => $estado,
'page' => $request->query->getInt('page', 1)
]
]
);
// Obtener estadísticas para las tarjetas
$totalHorarios = $horarioRepository->createQueryBuilder('h')
->select('COUNT(h.id)')
->getQuery()
->getSingleScalarResult();
$horariosDisponibles = $horarioRepository->createQueryBuilder('h')
->select('COUNT(h.id)')
->where('h.estado = :estado')
->setParameter('estado', 'disponible')
->getQuery()
->getSingleScalarResult();
$horariosOcupados = $horarioRepository->createQueryBuilder('h')
->select('COUNT(h.id)')
->where('h.estado = :estado')
->setParameter('estado', 'ocupado')
->getQuery()
->getSingleScalarResult();
$horariosCerrados = $horarioRepository->createQueryBuilder('h')
->select('COUNT(h.id)')
->where('h.estado = :estado')
->setParameter('estado', 'cerrado')
->getQuery()
->getSingleScalarResult();
return $this->render('admin/horario/index.html.twig', [
'horarios' => $pagination,
'fecha' => $fecha,
'hora' => $hora,
'estado' => $estado,
'seccion' => 'horarios',
'titulo_pagina' => 'Gestión del Horario',
'total_horarios' => $totalHorarios,
'horarios_disponibles' => $horariosDisponibles,
'horarios_ocupados' => $horariosOcupados,
'horarios_cerrados' => $horariosCerrados
]);
}
/**
* @Route("/profile", name="_profile", methods={"GET", "POST"})
*/
public function profile(Request $request, EntityManagerInterface $entityManager): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
/** @var User $user */
$user = $this->getUser();
$form = $this->createForm(AdminProfileType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$entityManager->flush();
$this->addFlash('success', 'Perfil actualizado correctamente.');
return $this->redirectToRoute('admin_profile');
} catch (\Exception $e) {
$this->addFlash('error', 'Error al actualizar el perfil: ' . $e->getMessage());
}
}
return $this->render('admin/profile/index.html.twig', [
'user' => $user,
'form' => $form->createView(),
'titulo_pagina' => 'Mi Perfil - Administrador'
]);
}
/**
* @Route("/change-email", name="_profile_change_email", methods={"GET", "POST"})
*/
public function changeEmail(
Request $request,
EntityManagerInterface $entityManager,
UserPasswordHasherInterface $passwordHasher
): Response {
$this->denyAccessUnlessGranted('ROLE_ADMIN');
/** @var User $user */
$user = $this->getUser();
$form = $this->createForm(ChangeEmailFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$currentPassword = $form->get('currentPassword')->getData();
$newEmail = $form->get('newEmail')->getData();
$confirmEmail = $form->get('confirmEmail')->getData();
$hasErrors = false;
// Verificar contraseña actual
if (!$passwordHasher->isPasswordValid($user, $currentPassword)) {
$form->get('currentPassword')->addError(new FormError('La contraseña actual es incorrecta'));
$hasErrors = true;
}
// Verificar que los emails coincidan
if ($newEmail !== $confirmEmail) {
$form->get('confirmEmail')->addError(new FormError('Los correos electrónicos no coinciden'));
$hasErrors = true;
}
// Verificar si el email ya existe
if (!$hasErrors) {
$existingUser = $entityManager->getRepository(User::class)->findOneBy(['email' => $newEmail]);
if ($existingUser && $existingUser->getId() !== $user->getId()) {
$form->get('newEmail')->addError(new FormError('Este email ya está en uso por otro usuario'));
$hasErrors = true;
}
}
if (!$hasErrors) {
try {
$user->setEmail($newEmail);
$entityManager->flush();
$this->addFlash('success', 'Email actualizado correctamente.');
return $this->redirectToRoute('admin_profile');
} catch (\Exception $e) {
$this->addFlash('error', 'Error al actualizar el email: ' . $e->getMessage());
}
}
}
return $this->render('admin/profile/change_email.html.twig', [
'form' => $form->createView(),
'titulo_pagina' => 'Cambiar Correo Electrónico - Administrador'
]);
}
/**
* @Route("/change-password", name="_profile_change_password", methods={"GET", "POST"})
*/
public function changePassword(
Request $request,
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $entityManager
): Response {
$this->denyAccessUnlessGranted('ROLE_ADMIN');
/** @var User $user */
$user = $this->getUser();
$form = $this->createForm(ChangePasswordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$currentPassword = $form->get('currentPassword')->getData();
$newPassword = $form->get('newPassword')->getData();
// Verificar contraseña actual
if (!$passwordHasher->isPasswordValid($user, $currentPassword)) {
$form->get('currentPassword')->addError(
new FormError('La contraseña actual es incorrecta')
);
} else {
try {
// Hashear y guardar nueva contraseña
$hashedPassword = $passwordHasher->hashPassword($user, $newPassword);
$user->setPassword($hashedPassword);
$entityManager->flush();
$this->addFlash('success', 'Contraseña actualizada correctamente.');
return $this->redirectToRoute('admin_profile');
} catch (\Exception $e) {
$this->addFlash('error', 'Error al actualizar la contraseña: ' . $e->getMessage());
}
}
}
return $this->render('admin/profile/change_password.html.twig', [
'form' => $form->createView(),
'titulo_pagina' => 'Cambiar Contraseña - Administrador'
]);
}
}