templates/admin/appointment/index.html.twig line 1

Open in your IDE?
  1. {% extends 'admin/base_admin.html.twig' %}
  2. {% block content %}
  3. <div class="row mb-4">
  4.     <div class="col-12">
  5.         <!-- Header con estadísticas -->
  6.         <div class="row g-3 mb-4">
  7.             <div class="col-md-3">
  8.                 <div class="card bg-primary text-white">
  9.                     <div class="card-body">
  10.                         <div class="d-flex align-items-center">
  11.                             <div class="flex-grow-1">
  12.                                 <h4 class="mb-0">{{ appointments.getTotalItemCount }}</h4>
  13.                                 <small class="opacity-75">Total Citas</small>
  14.                             </div>
  15.                             <div class="flex-shrink-0">
  16.                                 <i class="fas fa-calendar-check fa-2x opacity-50"></i>
  17.                             </div>
  18.                         </div>
  19.                     </div>
  20.                 </div>
  21.             </div>
  22.             <div class="col-md-3">
  23.                 <div class="card bg-success text-white">
  24.                     <div class="card-body">
  25.                         <div class="d-flex align-items-center">
  26.                             <div class="flex-grow-1">
  27.                                 <h4 class="mb-0">{{ appointments|filter(a => a.status == 'confirmada')|length }}</h4>
  28.                                 <small class="opacity-75">Confirmadas</small>
  29.                             </div>
  30.                             <div class="flex-shrink-0">
  31.                                 <i class="fas fa-check-circle fa-2x opacity-50"></i>
  32.                             </div>
  33.                         </div>
  34.                     </div>
  35.                 </div>
  36.             </div>
  37.             <div class="col-md-3">
  38.                 <div class="card bg-info text-white">
  39.                     <div class="card-body">
  40.                         <div class="d-flex align-items-center">
  41.                             <div class="flex-grow-1">
  42.                                 <h4 class="mb-0">{{ appointments|filter(a => a.status == 'completada')|length }}</h4>
  43.                                 <small class="opacity-75">Completadas</small>
  44.                             </div>
  45.                             <div class="flex-shrink-0">
  46.                                 <i class="fas fa-check-double fa-2x opacity-50"></i>
  47.                             </div>
  48.                         </div>
  49.                     </div>
  50.                 </div>
  51.             </div>
  52.             <div class="col-md-3">
  53.                 <div class="card bg-danger text-white">
  54.                     <div class="card-body">
  55.                         <div class="d-flex align-items-center">
  56.                             <div class="flex-grow-1">
  57.                                 <h4 class="mb-0">{{ appointments|filter(a => a.status starts with 'cancelada')|length }}</h4>
  58.                                 <small class="opacity-75">Canceladas</small>
  59.                             </div>
  60.                             <div class="flex-shrink-0">
  61.                                 <i class="fas fa-times-circle fa-2x opacity-50"></i>
  62.                             </div>
  63.                         </div>
  64.                     </div>
  65.                 </div>
  66.             </div>
  67.         </div>
  68.         <!-- Card principal -->
  69.         <div class="card border-0 shadow-lg">
  70.             <div class="card-header bg-white border-bottom-0 py-4">
  71.                 <div class="row align-items-center">
  72.                     <div class="col-md-6">
  73.                         <h4 class="mb-0 text-dark fw-bold">
  74.                             <i class="fas fa-calendar-check me-2 text-primary"></i>
  75.                             Gestión de Citas
  76.                         </h4>
  77.                         <p class="text-muted mb-0 mt-1">Administra las citas médicas del sistema</p>
  78.                     </div>
  79.                     <div class="col-md-6 text-md-end">
  80.                         <div class="d-flex flex-column flex-md-row gap-2 justify-content-md-end">
  81.                             <a href="{{ path('admin_appointment_calendar') }}" class="btn btn-outline-primary btn-lg px-4">
  82.                                 <i class="fas fa-calendar-alt me-2"></i>Vista Calendario
  83.                             </a>
  84.                             <a href="{{ path('admin_appointment_new') }}" class="btn btn-primary btn-lg px-4">
  85.                                 <i class="fas fa-plus-circle me-2"></i>Nueva Cita
  86.                             </a>
  87.                         </div>
  88.                     </div>
  89.                 </div>
  90.             </div>
  91.             <div class="card-body p-0">
  92.                     <!-- Barra de búsqueda y filtros -->
  93.                     <div class="bg-light border-bottom p-4">
  94.                         <form method="get" class="row g-3 align-items-end">
  95.                             <!-- Primera fila -->
  96.                             <div class="col-12 col-md-8 col-lg-6">
  97.                                 <label class="form-label fw-semibold text-dark">Búsqueda</label>
  98.                                 <div class="input-group">
  99.                                     <span class="input-group-text bg-white border-end-0">
  100.                                         <i class="fas fa-search text-muted"></i>
  101.                                     </span>
  102.                                     <input type="text" name="q" value="{{ q|default('') }}" 
  103.                                         class="form-control border-start-0" 
  104.                                         placeholder="Buscar por paciente, método de pago, tipo estudio, notas...">
  105.                                 </div>
  106.                             </div>
  107.                             
  108.                             <!-- Segunda fila - Filtros -->
  109.                             <div class="col-12 col-sm-6 col-md-4 col-lg-3">
  110.                                 <label class="form-label fw-semibold text-dark">Proveedor</label>
  111.                                 <select name="provider" class="form-select">
  112.                                     <option value="">Todos los proveedores</option>
  113.                                     {% for provider in providers %}
  114.                                         <option value="{{ provider.id }}" 
  115.                                             {{ (selectedProvider|default('') == provider.id) ? 'selected' : '' }}>
  116.                                             {{ provider.name }} {{ provider.lastName }}
  117.                                         </option>
  118.                                     {% endfor %}
  119.                                 </select>
  120.                             </div>
  121.                             
  122.                             {# Filtro de estado - solo mostrar cuando no hay filtro del submenú #}
  123.                             {% if active_filter == 'all' %}
  124.                             <div class="col-12 col-sm-6 col-md-4 col-lg-3">
  125.                                 <label class="form-label fw-semibold text-dark">Estado</label>
  126.                                 <select name="status" class="form-select">
  127.                                     <option value="">Todos los estados</option>
  128.                                     {% for status in statuses %}
  129.                                         <option value="{{ status }}" 
  130.                                             {{ (selectedStatus|default('') == status) ? 'selected' : '' }}>
  131.                                             {{ status }}
  132.                                         </option>
  133.                                     {% endfor %}
  134.                                 </select>
  135.                             </div>
  136.                             {% endif %}
  137.                             
  138.                             <!-- Tercera fila - Botones -->
  139.                             <div class="col-12 col-sm-6 col-md-4 col-lg-3">
  140.                                 <label class="form-label fw-semibold text-dark">&nbsp;</label>
  141.                                 <div class="d-grid gap-2 d-md-flex">
  142.                                     <button type="submit" class="btn btn-primary btn-lg flex-fill">
  143.                                         <i class="fas fa-search me-2"></i>Buscar
  144.                                     </button>
  145.                                     <a href="{{ path('admin_appointment') }}" class="btn btn-outline-secondary btn-lg flex-fill">
  146.                                         <i class="fas fa-times me-2"></i>Limpiar
  147.                                     </a>
  148.                                 </div>
  149.                             </div>
  150.                         </form>
  151.                 
  152.                         {# Información del filtro activo del submenú #}
  153.                         {% if active_filter != 'all' %}
  154.                         <div class="row mt-3">
  155.                             <div class="col-12">
  156.                                 <div class="alert alert-info py-2 mb-0">
  157.                                     <small class="d-flex align-items-center">
  158.                                         <i class="fas fa-filter me-2"></i>
  159.                                         <span>
  160.                                             Mostrando 
  161.                                              <strong class="text-dark">
  162.                                                     {% if active_filter == 'confirmada' %}
  163.                                                         citas confirmadas
  164.                                                     {% elseif active_filter == 'completada' %}
  165.                                                         citas finalizadas (incluye completadas y ausentes)
  166.                                                     {% elseif active_filter == 'cancelada' %}
  167.                                                         citas canceladas (incluye canceladas por paciente y clínica)
  168.                                                     {% endif %}
  169.                                             </strong>
  170.                                             . 
  171.                                             <a href="{{ path('admin_appointment') }}" class="alert-link ms-1">
  172.                                                 Ver todas las citas
  173.                                             </a>
  174.                                         </span>
  175.                                     </small>
  176.                                 </div>
  177.                             </div>
  178.                         </div>
  179.                         {% endif %}
  180.                     </div>
  181.                 </div>
  182.             
  183.                 <!-- Leyenda de estados -->
  184.                 <div class="p-4 border-bottom bg-white">
  185.                     <h6 class="text-muted mb-3 fw-semibold">
  186.                         <i class="fas fa-info-circle me-2 text-primary"></i> 
  187.                         Leyenda de Estados
  188.                     </h6>
  189.                     <div class="d-flex flex-wrap gap-2">
  190.                         <span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 px-3 py-2 rounded-pill">
  191.                             <i class="fas fa-check-circle me-1"></i>Confirmada
  192.                         </span>
  193.                         <span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 px-3 py-2 rounded-pill">
  194.                             <i class="fas fa-check-double me-1"></i>Completada
  195.                         </span>
  196.                         <span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 px-3 py-2 rounded-pill">
  197.                             <i class="fas fa-user-times me-1"></i>Cancelada Paciente
  198.                         </span>
  199.                         <span class="badge bg-dark bg-opacity-10 text-dark border border-dark border-opacity-25 px-3 py-2 rounded-pill">
  200.                             <i class="fas fa-clinic-medical me-1"></i>Cancelada Clínica
  201.                         </span>
  202.                         <span class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25 px-3 py-2 rounded-pill">
  203.                             <i class="fas fa-user-slash me-1"></i>Ausente
  204.                         </span>
  205.                         <span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 px-3 py-2 rounded-pill">
  206.                             <i class="fas fa-calendar-alt me-1"></i>Reprogramada
  207.                         </span>
  208.                     </div>
  209.                 </div>
  210.                 <!-- Tabla de citas ACTUALIZADA con paciente y MÉTODO DE PAGO -->
  211.                 <div class="table-responsive">
  212.                     <table class="table table-hover align-middle mb-0">
  213.                         <thead class="table-light">
  214.                             <tr>
  215.                                 <th class="ps-4 py-3 fw-semibold text-dark border-bottom-2">
  216.                                     <i class="fas fa-hashtag me-2 text-primary"></i>ID
  217.                                 </th>
  218.                                 <th class="py-3 fw-semibold text-dark border-bottom-2">
  219.                                     <i class="fas fa-user-injured me-2 text-primary"></i>Paciente
  220.                                 </th>
  221.                                 <th class="py-3 fw-semibold text-dark border-bottom-2">
  222.                                     <i class="fas fa-credit-card me-2 text-primary"></i>Método de Pago
  223.                                 </th>
  224.                                 <th class="py-3 fw-semibold text-dark border-bottom-2">
  225.                                     <i class="fas fa-clock me-2 text-primary"></i>Fecha/Hora
  226.                                 </th>
  227.                                 <th class="py-3 fw-semibold text-dark border-bottom-2">
  228.                                     <i class="fas fa-tag me-2 text-primary"></i>Estado
  229.                                 </th>
  230.                                 <th class="py-3 fw-semibold text-dark border-bottom-2">
  231.                                     <i class="fas fa-stethoscope me-2 text-primary"></i>Tipo Estudio
  232.                                 </th>
  233.                                 <th class="py-3 fw-semibold text-dark border-bottom-2">
  234.                                     <i class="fas fa-file-alt me-2 text-primary"></i>Notas
  235.                                 </th>
  236.                                 <th class="text-end pe-4 py-3 fw-semibold text-dark border-bottom-2">
  237.                                     <i class="fas fa-cogs me-2 text-primary"></i>Acciones
  238.                                 </th>
  239.                             </tr>
  240.                         </thead>
  241.                         <tbody>
  242.                             {% for appointment in appointments %}
  243.                                 <tr class="border-bottom">
  244.                                     <td class="ps-4 py-3">
  245.                                         <div class="d-flex align-items-center">
  246.                                             <div class="bg-primary bg-opacity-10 rounded p-2 me-3">
  247.                                                 <i class="fas fa-calendar text-primary"></i>
  248.                                             </div>
  249.                                             <div>
  250.                                                 <h6 class="mb-0 fw-semibold text-dark">#{{ appointment.id }}</h6>
  251.                                                 <small class="text-muted">Cita</small>
  252.                                             </div>
  253.                                         </div>
  254.                                     </td>
  255.                                     
  256.                                     <!-- Columna PACIENTE -->
  257.                                     <td class="py-3">
  258.                                         <div class="d-flex align-items-center">
  259.                                             <div class="bg-success bg-opacity-10 rounded p-2 me-2">
  260.                                                 <i class="fas fa-user-injured text-success"></i>
  261.                                             </div>
  262.                                             <div>
  263.                                                 <h6 class="mb-0 fw-semibold text-dark">{{ appointment.patient.name }} {{ appointment.patient.lastName }}</h6>
  264.                                                 <small class="text-muted">tel: {{ appointment.patient.phone ?? 'Sin teléfono' }}</small>
  265.                                                 <small class="text-success d-block">
  266.                                                     <i class="fas fa-user-md me-1"></i>
  267.                                                     Proveedor: {{ appointment.provider.name }}
  268.                                                 </small>
  269.                                                 <div>
  270.                                                     <small class="text-primary">
  271.                                                         <i class="fas fa-envelope me-1"></i>{{ appointment.provider.email }}
  272.                                                     </small>
  273.                                                 </div>
  274.                                             </div>
  275.                                         </div>
  276.                                     </td>
  277.                                     
  278.                                     <!-- Columna MÉTODO DE PAGO (REEMPLAZA PROVEEDOR) -->
  279.                                    <td class="py-3">
  280.                                         <div class="d-flex align-items-center">
  281.                                             <div class="bg-info bg-opacity-10 rounded p-2 me-2">
  282.                                                 <i class="fas fa-credit-card text-info"></i>
  283.                                             </div>
  284.                                             <div>
  285.                                                 {% if appointment.paymentMethod == 'imaging_pro' %}
  286.                                                     <h6 class="mb-0 fw-semibold text-dark">
  287.                                                         Imaging Pro
  288.                                                         {% if appointment.montoImagingPro %}
  289.                                                             <span class="text-success fw-bold ms-1">
  290.                                                                 ${{ appointment.montoImagingPro|number_format(2, '.', ',') }}
  291.                                                             </span>
  292.                                                         {% endif %}
  293.                                                     </h6>
  294.                                                     <small class="text-muted">Pago en Imaging Pro</small>
  295.                                     
  296.                                                 {% elseif appointment.paymentMethod == 'clinica' %}
  297.                                                     <h6 class="mb-0 fw-semibold text-dark">Clínica</h6>
  298.                                                     <small class="text-muted">Pago directo</small>
  299.                                     
  300.                                                 {% else %}
  301.                                                     <h6 class="mb-0 fw-semibold text-dark">No especificado</h6>
  302.                                                     <small class="text-muted">-</small>
  303.                                                 {% endif %}
  304.                                             </div>
  305.                                         </div>
  306.                                     </td>
  307.                                     
  308.                                     <td class="py-3">
  309.                                         <span class="fw-semibold text-dark">{{ appointment.horario.fecha|date("d/m/Y") }}</span>
  310.                                         <small class="text-muted d-block">{{ appointment.horario.hora|date("H:i") }}</small>
  311.                                     </td>
  312.                                     
  313.                                     <td class="py-3">
  314.                                         {% if appointment.status == 'confirmada' %}
  315.                                             <span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 px-3 py-2 rounded-pill">
  316.                                                 <i class="fas fa-check-circle me-1"></i>Confirmada
  317.                                             </span>
  318.                                         {% elseif appointment.status starts with 'cancelada_paciente' %}
  319.                                             <span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 px-3 py-2 rounded-pill">
  320.                                                 <i class="fas fa-user-times me-1"></i>Cancelada Paciente
  321.                                             </span>
  322.                                         {% elseif appointment.status starts with 'cancelada_clinica' %}
  323.                                             <span class="badge bg-dark bg-opacity-10 text-dark border border-dark border-opacity-25 px-3 py-2 rounded-pill">
  324.                                                 <i class="fas fa-clinic-medical me-1"></i>Cancelada Clínica
  325.                                             </span>
  326.                                         {% elseif appointment.status == 'completada' %}
  327.                                             <span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 px-3 py-2 rounded-pill">
  328.                                                 <i class="fas fa-check-double me-1"></i>Completada
  329.                                             </span>
  330.                                         {% elseif appointment.status == 'ausente' %}
  331.                                             <span class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25 px-3 py-2 rounded-pill">
  332.                                                 <i class="fas fa-user-slash me-1"></i>Ausente
  333.                                             </span>
  334.                                         {% elseif appointment.status == 'reprogramada' %}    
  335.                                             <span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 px-3 py-2 rounded-pill">
  336.                                                 <i class="fas fa-calendar-alt me-1"></i>Reprogramada
  337.                                             </span>
  338.                                         {% else %}
  339.                                             <span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 px-3 py-2 rounded-pill">
  340.                                                 <i class="fas fa-question-circle me-1"></i>{{ appointment.status|title }}
  341.                                             </span>
  342.                                         {% endif %}
  343.                                     </td>
  344.                                     
  345.                                     <!-- Columna TIPO ESTUDIO -->
  346.                                     <td class="py-3">
  347.                                         {% if appointment.tipoEstudio %}
  348.                                             <span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 px-3 py-2 rounded-pill">
  349.                                                 <i class="fas fa-stethoscope me-1"></i>{{ appointment.tipoEstudio }}
  350.                                             </span>
  351.                                         {% else %}
  352.                                             <span class="text-muted">-</span>
  353.                                         {% endif %}
  354.                                     </td>
  355.                                     
  356.                                     <td class="py-3">
  357.                                         {% if appointment.notes %}
  358.                                             <div style="white-space: pre-line; max-width: 200px; font-size: 0.9rem; line-height: 1.4;"
  359.                                                 data-bs-toggle="tooltip" 
  360.                                                 title="{{ appointment.notes|replace({'\n': ' / '}) }}">
  361.                                                 {{ appointment.notes|length > 50 ? appointment.notes|slice(0, 50) ~ '...' : appointment.notes }}
  362.                                             </div>
  363.                                         {% else %}
  364.                                             <span class="text-muted">-</span>
  365.                                         {% endif %}
  366.                                     </td>
  367.                                     
  368.                                     <td class="text-end pe-4 py-3">
  369.                                         <div class="d-flex justify-content-end gap-2">
  370.                                             {% if appointment.status not in ['cancelada_clinica', 'cancelada_paciente', 'completada', 'ausente'] %}
  371.                                                 <button class="btn btn-outline-danger btn-sm px-3 rounded-pill btn-cancelar"
  372.                                                     data-bs-toggle="modal" 
  373.                                                     data-bs-target="#cancelModal"
  374.                                                     data-appointment-id="{{ appointment.id }}"
  375.                                                     data-appointment-info="{{ appointment.horario.fecha|date('d/m/Y') }} {{ appointment.horario.hora|date('H:i') }} - {{ appointment.patient.name }}"
  376.                                                     data-csrf-token="{{ csrf_token('cancel_' ~ appointment.id) }}">
  377.                                                     <i class="fas fa-times-circle me-1"></i>Cancelar
  378.                                                 </button>
  379.                                                 <button class="btn btn-outline-success btn-sm px-3 rounded-pill btn-finalizar"
  380.                                                         data-bs-toggle="modal" 
  381.                                                         data-bs-target="#finalizeModal"
  382.                                                         data-appointment-id="{{ appointment.id }}"
  383.                                                         data-appointment-info="Cita #{{ appointment.id }} - {{ appointment.horario.fecha|date('d/m/Y') }} {{ appointment.horario.hora|date('H:i') }} - {{ appointment.patient.name }}"
  384.                                                         data-csrf-token="{{ csrf_token('status_' ~ appointment.id) }}">
  385.                                                     <i class="fas fa-flag-checkered me-1"></i>Finalizar
  386.                                                 </button>
  387.                                             {% endif %}
  388.                                             
  389.                                             <a href="{{ path('admin_appointment_reschedule', {id: appointment.id}) }}" 
  390.                                                class="btn btn-outline-primary btn-sm px-3 rounded-pill"
  391.                                                data-bs-toggle="tooltip" 
  392.                                                title="Reprogramar cita">
  393.                                                <i class="fas fa-calendar-alt me-1"></i>Reprogramar
  394.                                             </a>
  395.                                             
  396.                                             <button class="btn btn-outline-danger btn-sm px-3 rounded-pill"
  397.                                                     data-bs-toggle="modal" 
  398.                                                     data-bs-target="#deleteModal"
  399.                                                     data-appointment-id="{{ appointment.id }}"
  400.                                                     data-appointment-info="Cita #{{ appointment.id }} - {{ appointment.horario.fecha|date('d/m/Y') }} {{ appointment.horario.hora|date('H:i') }} - {{ appointment.patient.name }}"
  401.                                                     data-csrf-token="{{ csrf_token('delete' ~ appointment.id) }}">
  402.                                                 <i class="fas fa-trash me-1"></i>Eliminar
  403.                                             </button>
  404.                                         </div>
  405.                                     </td>
  406.                                 </tr>
  407.                             {% else %}
  408.                                 <tr>
  409.                                     <td colspan="8" class="text-center py-5">
  410.                                         <div class="py-5">
  411.                                             <i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
  412.                                             <h5 class="text-muted">No se encontraron citas</h5>
  413.                                             <p class="text-muted mb-4">No hay citas registradas en el sistema</p>
  414.                                             <a href="{{ path('admin_appointment_new') }}" class="btn btn-primary">
  415.                                                 <i class="fas fa-plus me-2"></i>Crear primera cita
  416.                                             </a>
  417.                                         </div>
  418.                                     </td>
  419.                                 </tr>
  420.                             {% endfor %}
  421.                         </tbody>
  422.                     </table>
  423.                 </div>
  424.                 <!-- Paginación (mantener igual) -->
  425.                 {% if appointments.getTotalItemCount > 0 %}
  426.                 <div class="border-top bg-light">
  427.                     <div class="row align-items-center p-3 p-md-4">
  428.                         <div class="col-12 col-md-6 mb-3 mb-md-0">
  429.                             {% set total = appointments.getTotalItemCount %}
  430.                             {% set perPage = appointments.getItemNumberPerPage %}
  431.                             {% set page = appointments.getCurrentPageNumber %}
  432.                             {% set first = total > 0 ? ((page - 1) * perPage + 1) : 0 %}
  433.                             {% set last = min(first + appointments|length - 1, total) %}
  434.                             
  435.                             <p class="mb-0 text-muted fw-semibold text-center text-md-start">
  436.                                 Mostrando <span class="text-dark">{{ first }}–{{ last }}</span> de 
  437.                                 <span class="text-dark">{{ total }}</span> citas
  438.                             </p>
  439.                         </div>
  440.                         <div class="col-12 col-md-6">
  441.                             <div class="d-flex justify-content-center justify-content-md-end align-items-center">
  442.                                 <!-- Paginación responsive -->
  443.                                 <nav aria-label="Paginación de citas">
  444.                                     <ul class="pagination pagination-sm mb-0 flex-wrap justify-content-center">
  445.                                         <!-- Primera página -->
  446.                                         <li class="page-item {% if page == 1 %}disabled{% endif %}">
  447.                                             <a class="page-link" href="{{ path('admin_appointment', app.request.query.all|merge({page: 1})) }}" 
  448.                                             aria-label="Primera página" data-bs-toggle="tooltip" title="Primera página">
  449.                                                 <i class="fas fa-angle-double-left d-none d-sm-inline"></i>
  450.                                                 <span class="d-inline d-sm-none">Primera</span>
  451.                                             </a>
  452.                                         </li>
  453.                                         
  454.                                         <!-- Página anterior -->
  455.                                         <li class="page-item {% if page == 1 %}disabled{% endif %}">
  456.                                             <a class="page-link" href="{{ path('admin_appointment', app.request.query.all|merge({page: page - 1})) }}" 
  457.                                             aria-label="Página anterior" data-bs-toggle="tooltip" title="Página anterior">
  458.                                                 <i class="fas fa-angle-left"></i>
  459.                                             </a>
  460.                                         </li>
  461.                                         <!-- Indicador de página actual en móvil -->
  462.                                         <li class="page-item d-block d-md-none disabled">
  463.                                             <span class="page-link fw-bold text-primary">
  464.                                                 {{ page }} de {{ (total / perPage)|round(0, 'ceil') }}
  465.                                             </span>
  466.                                         </li>
  467.                                         <!-- Páginas numeradas (ocultas en móvil) -->
  468.                                         {% set totalPages = (total / perPage)|round(0, 'ceil') %}
  469.                                         {% set startPage = max(1, page - 2) %}
  470.                                         {% set endPage = min(totalPages, startPage + 4) %}
  471.                                         
  472.                                         {% if startPage > 1 %}
  473.                                             <li class="page-item d-none d-md-block disabled">
  474.                                                 <span class="page-link">...</span>
  475.                                             </li>
  476.                                         {% endif %}
  477.                                         
  478.                                         {% for i in startPage..endPage %}
  479.                                             <li class="page-item d-none d-md-block {% if i == page %}active{% endif %}">
  480.                                                 <a class="page-link" href="{{ path('admin_appointment', app.request.query.all|merge({page: i})) }}">
  481.                                                     {{ i }}
  482.                                                 </a>
  483.                                             </li>
  484.                                         {% endfor %}
  485.                                         
  486.                                         {% if endPage < totalPages %}
  487.                                             <li class="page-item d-none d-md-block disabled">
  488.                                                 <span class="page-link">...</span>
  489.                                             </li>
  490.                                         {% endif %}
  491.                                         <!-- Página siguiente -->
  492.                                         <li class="page-item {% if page == totalPages %}disabled{% endif %}">
  493.                                             <a class="page-link" href="{{ path('admin_appointment', app.request.query.all|merge({page: page + 1})) }}" 
  494.                                             aria-label="Página siguiente" data-bs-toggle="tooltip" title="Página siguiente">
  495.                                                 <i class="fas fa-angle-right"></i>
  496.                                             </a>
  497.                                         </li>
  498.                                         
  499.                                         <!-- Última página -->
  500.                                         <li class="page-item {% if page == totalPages %}disabled{% endif %}">
  501.                                             <a class="page-link" href="{{ path('admin_appointment', app.request.query.all|merge({page: totalPages})) }}" 
  502.                                             aria-label="Última página" data-bs-toggle="tooltip" title="Última página">
  503.                                                 <i class="fas fa-angle-double-right d-none d-sm-inline"></i>
  504.                                                 <span class="d-inline d-sm-none">Última</span>
  505.                                             </a>
  506.                                         </li>
  507.                                     </ul>
  508.                                 </nav>
  509.                             </div>
  510.                         </div>
  511.                     </div>
  512.                 </div>
  513.                 {% endif %}
  514.             </div>
  515.         </div>
  516.     </div>
  517. </div>
  518. <!-- Modal de Cancelación - Manteniendo el mismo estilo -->
  519. <div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
  520.     <div class="modal-dialog modal-dialog-centered">
  521.         <div class="modal-content">
  522.             <div class="modal-header bg-danger text-white">
  523.                 <h5 class="modal-title" id="cancelModalLabel">
  524.                     <i class="fas fa-exclamation-triangle me-2"></i>Confirmar Cancelación
  525.                 </h5>
  526.                 <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
  527.             </div>
  528.             <div class="modal-body">
  529.                 <p>¿Estás seguro de que deseas cancelar la siguiente cita?</p>
  530.                 <div class="alert alert-warning">
  531.                     <i class="fas fa-info-circle me-2"></i>
  532.                     <strong id="appointmentInfo"></strong>
  533.                 </div>
  534.                 <div class="mb-3">
  535.                     <label for="cancelReason" class="form-label">Motivo de cancelación (opcional):</label>
  536.                     <textarea class="form-control" id="cancelReason" rows="3" placeholder="Ingresa el motivo de la cancelación..."></textarea>
  537.                 </div>
  538.             </div>
  539.             <div class="modal-footer">
  540.                 <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
  541.                     <i class="fas fa-times me-2"></i>Cancelar
  542.                 </button>
  543.                 <button type="button" class="btn btn-danger" id="confirmCancel">
  544.                     <i class="fas fa-times-circle me-2"></i>Confirmar Cancelación
  545.                 </button>
  546.             </div>
  547.         </div>
  548.     </div>
  549. </div>
  550. <!-- Modal de Finalización -->
  551. <div class="modal fade" id="finalizeModal" tabindex="-1" aria-labelledby="finalizeModalLabel" aria-hidden="true">
  552.     <div class="modal-dialog modal-dialog-centered">
  553.         <div class="modal-content">
  554.             <div class="modal-header bg-success text-white">
  555.                 <h5 class="modal-title" id="finalizeModalLabel">
  556.                     <i class="fas fa-flag-checkered me-2"></i>Finalizar Cita
  557.                 </h5>
  558.                 <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
  559.             </div>
  560.             <div class="modal-body">
  561.                 <p>Selecciona el estado final para la siguiente cita:</p>
  562.                 <div class="alert alert-info">
  563.                     <i class="fas fa-info-circle me-2"></i>
  564.                     <strong id="finalizeAppointmentInfo"></strong>
  565.                 </div>
  566.                 
  567.                 <div class="row g-3">
  568.                     <div class="col-md-6">
  569.                         <div class="card border-success h-100 text-center finalize-option" data-status="completada">
  570.                             <div class="card-body">
  571.                                 <i class="fas fa-check-double fa-2x text-success mb-3"></i>
  572.                                 <h6 class="card-title">Completada</h6>
  573.                                 <p class="card-text small text-muted">El paciente asistió y la cita se completó exitosamente</p>
  574.                             </div>
  575.                         </div>
  576.                     </div>
  577.                     <div class="col-md-6">
  578.                         <div class="card border-warning h-100 text-center finalize-option" data-status="ausente">
  579.                             <div class="card-body">
  580.                                 <i class="fas fa-user-slash fa-2x text-warning mb-3"></i>
  581.                                 <h6 class="card-title">Ausente</h6>
  582.                                 <p class="card-text small text-muted">El paciente no se presentó a la cita programada</p>
  583.                             </div>
  584.                         </div>
  585.                     </div>
  586.                 </div>
  587.                 
  588.                 <div class="mt-3">
  589.                     <label for="finalizeNotes" class="form-label">Observaciones (opcional):</label>
  590.                     <textarea class="form-control" id="finalizeNotes" rows="2" placeholder="Agregar observaciones sobre el estado final..."></textarea>
  591.                 </div>
  592.             </div>
  593.             <div class="modal-footer">
  594.                 <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
  595.                     <i class="fas fa-times me-2"></i>Cancelar
  596.                 </button>
  597.                 <button type="button" class="btn btn-success" id="confirmFinalize" disabled>
  598.                     <i class="fas fa-check me-2"></i>Confirmar
  599.                 </button>
  600.             </div>
  601.         </div>
  602.     </div>
  603. </div>
  604. <!-- Modal de Eliminación - VERSIÓN CORREGIDA -->
  605. <div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
  606.     <div class="modal-dialog modal-dialog-centered">
  607.         <div class="modal-content">
  608.             <div class="modal-header bg-danger text-white">
  609.                 <h5 class="modal-title" id="deleteModalLabel">
  610.                     <i class="fas fa-exclamation-triangle me-2"></i>Confirmar Eliminación
  611.                 </h5>
  612.                 <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
  613.             </div>
  614.             <div class="modal-body">
  615.                 <p>¿Estás seguro de que deseas eliminar la siguiente cita?</p>
  616.                 <div class="alert alert-danger">
  617.                     <i class="fas fa-exclamation-circle me-2"></i>
  618.                     <strong>¡Esta acción no se puede deshacer!</strong>
  619.                 </div>
  620.                 <div class="card border-danger">
  621.                     <div class="card-body">
  622.                         <h6 id="deleteAppointmentInfo" class="text-danger mb-2"></h6>
  623.                         <p class="mb-0 text-muted small">
  624.                             <i class="fas fa-info-circle me-1"></i>
  625.                             Se eliminará permanentemente toda la información de esta cita.
  626.                         </p>
  627.                     </div>
  628.                 </div>
  629.             </div>
  630.             <div class="modal-footer">
  631.                 <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
  632.                     <i class="fas fa-times me-2"></i>Cancelar
  633.                 </button>
  634.                 <button type="button" class="btn btn-danger" id="confirmDelete">
  635.                     <i class="fas fa-trash me-2"></i>Eliminar Permanentemente
  636.                 </button>
  637.             </div>
  638.         </div>
  639.     </div>
  640. </div>
  641. <style>
  642. .finalize-option {
  643.     cursor: pointer;
  644.     transition: all 0.3s ease;
  645. }
  646. .finalize-option:hover {
  647.     transform: translateY(-2px);
  648.     box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  649. }
  650. </style>
  651. {% endblock %}
  652. {% block javascripts %}
  653.     {{ parent() }}
  654.     <script>
  655.             document.addEventListener('DOMContentLoaded', function() {
  656.             // Tooltips
  657.             var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
  658.             var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
  659.                 return new bootstrap.Tooltip(tooltipTriggerEl)
  660.             });
  661.             // Modal de cancelación
  662.             const cancelModal = document.getElementById('cancelModal');
  663.             const cancelButtons = document.querySelectorAll('.btn-cancelar');
  664.             const confirmCancelBtn = document.getElementById('confirmCancel');
  665.             const appointmentInfo = document.getElementById('appointmentInfo');
  666.             let currentAppointmentId = null;
  667.             let currentCsrfToken = null;
  668.             cancelButtons.forEach(button => {
  669.                 button.addEventListener('click', function() {
  670.                     currentAppointmentId = this.getAttribute('data-appointment-id');
  671.                     const info = this.getAttribute('data-appointment-info');
  672.                     const csrfToken = this.getAttribute('data-csrf-token');
  673.                     appointmentInfo.textContent = `Cita #${currentAppointmentId} - ${info}`;
  674.                     currentCsrfToken = csrfToken;
  675.                 });
  676.             });
  677.             confirmCancelBtn.addEventListener('click', function() {
  678.                 if (currentAppointmentId) {
  679.                     const reason = document.getElementById('cancelReason').value;
  680.                     
  681.                     // Mostrar loading
  682.                     confirmCancelBtn.disabled = true;
  683.                     confirmCancelBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Cancelando...';
  684.                     // Endpoint real para cancelar la cita
  685.                     fetch(`/admin/appointment/${currentAppointmentId}/status`, {
  686.                         method: 'POST',
  687.                         headers: {
  688.                             'Content-Type': 'application/x-www-form-urlencoded',
  689.                             'X-Requested-With': 'XMLHttpRequest'
  690.                         },
  691.                         body: new URLSearchParams({
  692.                             'status': 'cancelada_clinica',
  693.                             'reason': reason || 'Sin motivo especificado',
  694.                             '_token': currentCsrfToken
  695.                         })
  696.                     })
  697.                     .then(response => {
  698.                         if (!response.ok) {
  699.                             throw new Error('Error en la respuesta del servidor');
  700.                         }
  701.                         return response.json();
  702.                     })
  703.                     .then(data => {
  704.                         if (data.success) {
  705.                             // Mostrar toast de éxito para cancelación
  706.                             showToast('success', 'Cita cancelada correctamente');
  707.                             
  708.                             // Cerrar modal
  709.                             const modal = bootstrap.Modal.getInstance(cancelModal);
  710.                             modal.hide();
  711.                             
  712.                             // Recargar la página después de un breve delay
  713.                             setTimeout(() => {
  714.                                 location.reload();
  715.                             }, 1500);
  716.                         } else {
  717.                             throw new Error(data.message || 'Error al cancelar la cita');
  718.                         }
  719.                     })
  720.                     .catch(error => {
  721.                         console.error('Error:', error);
  722.                         showToast('error', 'Error al cancelar la cita: ' + error.message);
  723.                         
  724.                         // Restaurar botón
  725.                         confirmCancelBtn.disabled = false;
  726.                         confirmCancelBtn.innerHTML = '<i class="fas fa-times-circle me-2"></i>Confirmar Cancelación';
  727.                     });
  728.                 }
  729.             });
  730.             // Reset modal cuando se cierra
  731.             cancelModal.addEventListener('hidden.bs.modal', function () {
  732.                 document.getElementById('cancelReason').value = '';
  733.                 currentAppointmentId = null;
  734.                 currentCsrfToken = null;
  735.                 confirmCancelBtn.disabled = false;
  736.                 confirmCancelBtn.innerHTML = '<i class="fas fa-times-circle me-2"></i>Confirmar Cancelación';
  737.             });
  738.             // Modal de Finalización
  739.             const finalizeModal = document.getElementById('finalizeModal');
  740.             const finalizeButtons = document.querySelectorAll('.btn-finalizar');
  741.             const finalizeAppointmentInfo = document.getElementById('finalizeAppointmentInfo');
  742.             const confirmFinalizeBtn = document.getElementById('confirmFinalize');
  743.             const finalizeOptions = document.querySelectorAll('.finalize-option');
  744.             let currentFinalizeAppointmentId = null;
  745.             let currentFinalizeCsrfToken = null;
  746.             let selectedStatus = null;
  747.             finalizeButtons.forEach(button => {
  748.                 button.addEventListener('click', function() {
  749.                     currentFinalizeAppointmentId = this.getAttribute('data-appointment-id');
  750.                     const appointmentInfo = this.getAttribute('data-appointment-info');
  751.                     currentFinalizeCsrfToken = this.getAttribute('data-csrf-token');
  752.                     
  753.                     finalizeAppointmentInfo.textContent = appointmentInfo;
  754.                     
  755.                     // Resetear selección
  756.                     selectedStatus = null;
  757.                     confirmFinalizeBtn.disabled = true;
  758.                     finalizeOptions.forEach(option => {
  759.                         option.classList.remove('bg-success', 'bg-warning', 'text-white');
  760.                         option.classList.add('bg-light');
  761.                     });
  762.                 });
  763.             });
  764.             // Manejar selección de opción
  765.             finalizeOptions.forEach(option => {
  766.                 option.addEventListener('click', function() {
  767.                     const status = this.getAttribute('data-status');
  768.                     
  769.                     // Remover selección anterior
  770.                     finalizeOptions.forEach(opt => {
  771.                         opt.classList.remove('bg-success', 'bg-warning', 'text-white');
  772.                         opt.classList.add('bg-light');
  773.                     });
  774.                     
  775.                     // Aplicar selección actual
  776.                     this.classList.remove('bg-light');
  777.                     if (status === 'completada') {
  778.                         this.classList.add('bg-success', 'text-white');
  779.                     } else {
  780.                         this.classList.add('bg-warning', 'text-white');
  781.                     }
  782.                     
  783.                     selectedStatus = status;
  784.                     confirmFinalizeBtn.disabled = false;
  785.                 });
  786.             });
  787.             // Confirmar finalización
  788.             confirmFinalizeBtn.addEventListener('click', function() {
  789.                 if (currentFinalizeAppointmentId && selectedStatus) {
  790.                     const notes = document.getElementById('finalizeNotes').value;
  791.                     const statusText = selectedStatus === 'completada' ? 'Completada' : 'Ausente';
  792.                     
  793.                     // Mostrar loading
  794.                     confirmFinalizeBtn.disabled = true;
  795.                     confirmFinalizeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Procesando...';
  796.                     
  797.                     // Cambiar estado de la cita
  798.                     fetch(`/admin/appointment/${currentFinalizeAppointmentId}/status`, {
  799.                         method: 'POST',
  800.                         headers: {
  801.                             'Content-Type': 'application/x-www-form-urlencoded',
  802.                             'X-Requested-With': 'XMLHttpRequest'
  803.                         },
  804.                         body: new URLSearchParams({
  805.                             'status': selectedStatus,
  806.                             'reason': notes || 'Sin observaciones',
  807.                             '_token': currentFinalizeCsrfToken
  808.                         })
  809.                     })
  810.                     .then(response => {
  811.                         if (!response.ok) {
  812.                             throw new Error('Error en la respuesta del servidor');
  813.                         }
  814.                         return response.json();
  815.                     })
  816.                     .then(data => {
  817.                         if (data.success) {
  818.                             showToast('success', `Cita marcada como ${statusText} correctamente`);
  819.                             
  820.                             // Cerrar modal
  821.                             const modal = bootstrap.Modal.getInstance(finalizeModal);
  822.                             modal.hide();
  823.                             
  824.                             // Recargar después de un breve delay
  825.                             setTimeout(() => {
  826.                                 location.reload();
  827.                             }, 1500);
  828.                         } else {
  829.                             throw new Error(data.message || `Error al marcar como ${statusText}`);
  830.                         }
  831.                     })
  832.                     .catch(error => {
  833.                         console.error('Error:', error);
  834.                         showToast('error', error.message);
  835.                         
  836.                         // Restaurar botón
  837.                         confirmFinalizeBtn.disabled = false;
  838.                         confirmFinalizeBtn.innerHTML = '<i class="fas fa-check me-2"></i>Confirmar';
  839.                     });
  840.                 }
  841.             });
  842.             // Reset modal cuando se cierra
  843.             finalizeModal.addEventListener('hidden.bs.modal', function () {
  844.                 document.getElementById('finalizeNotes').value = '';
  845.                 currentFinalizeAppointmentId = null;
  846.                 currentFinalizeCsrfToken = null;
  847.                 selectedStatus = null;
  848.                 confirmFinalizeBtn.disabled = true;
  849.                 confirmFinalizeBtn.innerHTML = '<i class="fas fa-check me-2"></i>Confirmar';
  850.                 
  851.                 // Resetear opciones
  852.                 finalizeOptions.forEach(option => {
  853.                     option.classList.remove('bg-success', 'bg-warning', 'text-white');
  854.                     option.classList.add('bg-light');
  855.                 });
  856.             });
  857.             // ========== MODAL DE ELIMINACIÓN - VERSIÓN CORREGIDA ==========
  858.             const deleteModal = document.getElementById('deleteModal');
  859.             const deleteButtons = document.querySelectorAll('[data-bs-target="#deleteModal"]');
  860.             const deleteAppointmentInfo = document.getElementById('deleteAppointmentInfo');
  861.             const confirmDeleteBtn = document.getElementById('confirmDelete');
  862.             
  863.             let currentDeleteAppointmentId = null;
  864.             let currentDeleteCsrfToken = null;
  865.             
  866.             deleteButtons.forEach(button => {
  867.                 button.addEventListener('click', function() {
  868.                     currentDeleteAppointmentId = this.getAttribute('data-appointment-id');
  869.                     const appointmentInfo = this.getAttribute('data-appointment-info');
  870.                     currentDeleteCsrfToken = this.getAttribute('data-csrf-token');
  871.                     
  872.                     deleteAppointmentInfo.textContent = appointmentInfo;
  873.                 });
  874.             });
  875.             
  876.             // Manejar el clic en el botón "Eliminar Permanentemente"
  877.             confirmDeleteBtn.addEventListener('click', function() {
  878.                 if (!currentDeleteAppointmentId || !currentDeleteCsrfToken) {
  879.                     showToast('error', 'Error: No se pudo obtener la información de la cita');
  880.                     return;
  881.                 }
  882.                 
  883.                 const url = "{{ path('admin_appointment_delete', {id: 'PLACEHOLDER'}) }}".replace('PLACEHOLDER', currentDeleteAppointmentId);
  884.                 
  885.                 // Mostrar loading en el botón y deshabilitar
  886.                 confirmDeleteBtn.disabled = true;
  887.                 confirmDeleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Eliminando...';
  888.                 
  889.                 // Hacer la petición fetch UNA sola vez
  890.                 fetch(url, {
  891.                     method: 'POST',
  892.                     headers: {
  893.                         'Content-Type': 'application/x-www-form-urlencoded',
  894.                         'X-Requested-With': 'XMLHttpRequest'
  895.                     },
  896.                     body: new URLSearchParams({
  897.                         '_token': currentDeleteCsrfToken
  898.                     })
  899.                 })
  900.                 .then(response => {
  901.                     if (!response.ok) {
  902.                         throw new Error('Error en la respuesta del servidor');
  903.                     }
  904.                     return response.json();
  905.                 })
  906.                 .then(data => {
  907.                     if (data.success) {
  908.                         showToast('success', data.message || 'Cita eliminada correctamente');
  909.                         
  910.                         // Cerrar modal
  911.                         const modal = bootstrap.Modal.getInstance(deleteModal);
  912.                         modal.hide();
  913.                         
  914.                         // Recargar la página después de un breve delay
  915.                         setTimeout(() => {
  916.                             location.reload();
  917.                         }, 1500);
  918.                     } else {
  919.                         throw new Error(data.message || 'Error al eliminar la cita');
  920.                     }
  921.                 })
  922.                 .catch(error => {
  923.                     console.error('Error:', error);
  924.                     showToast('error', error.message || 'Error al eliminar la cita');
  925.                     
  926.                     // Restaurar botón en caso de error
  927.                     confirmDeleteBtn.disabled = false;
  928.                     confirmDeleteBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Eliminar Permanentemente';
  929.                 });
  930.             });
  931.             
  932.             // Reset modal cuando se cierra
  933.             deleteModal.addEventListener('hidden.bs.modal', function () {
  934.                 currentDeleteAppointmentId = null;
  935.                 currentDeleteCsrfToken = null;
  936.                 confirmDeleteBtn.disabled = false;
  937.                 confirmDeleteBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Eliminar Permanentemente';
  938.             });
  939.             // ========== FIN MODAL DE ELIMINACIÓN ==========
  940.             // Función para mostrar toast (compartida)
  941.             function showToast(type, message) {
  942.                 // Crear contenedor de toasts si no existe
  943.                 let toastContainer = document.getElementById('toast-container');
  944.                 if (!toastContainer) {
  945.                     toastContainer = document.createElement('div');
  946.                     toastContainer.id = 'toast-container';
  947.                     toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
  948.                     toastContainer.style.zIndex = '1060';
  949.                     document.body.appendChild(toastContainer);
  950.                 }
  951.                 
  952.                 // Crear toast
  953.                 const toastId = 'toast-' + Date.now();
  954.                 const toastHtml = `
  955.                     <div id="${toastId}" class="toast align-items-center text-bg-${type} border-0" role="alert">
  956.                         <div class="d-flex">
  957.                             <div class="toast-body">
  958.                                 <i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'} me-2"></i>
  959.                                 ${message}
  960.                             </div>
  961.                             <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
  962.                         </div>
  963.                     </div>
  964.                 `;
  965.                 
  966.                 toastContainer.insertAdjacentHTML('beforeend', toastHtml);
  967.                 
  968.                 // Mostrar toast
  969.                 const toastElement = document.getElementById(toastId);
  970.                 const toast = new bootstrap.Toast(toastElement, {
  971.                     autohide: true,
  972.                     delay: 5000
  973.                 });
  974.                 toast.show();
  975.                 
  976.                 // Remover del DOM cuando se oculte
  977.                 toastElement.addEventListener('hidden.bs.toast', function() {
  978.                     this.remove();
  979.                 });
  980.             }
  981.         });
  982.     </script>
  983. {% endblock %}