templates/home/index.html.twig line 1

Open in your IDE?
  1. {# templates/home/index.html.twig #}
  2. {% extends 'common/base.html.twig' %}
  3. {# 1. VARIABLES DE ESTADO GLOBALES #}
  4. {% set isRestricted = app.user and app.user.team|upper|trim == 'LEGENDS VIEW' %}
  5. {% set accessType = accessType|default((branding_current is not null) ? branding_current.accessType : 'venues') %}
  6. {% set primaryColor = (branding_current is not null) ? branding_current.primaryColor : '#bdbf09' %}
  7. {% block pageid %}home-page{% endblock %}
  8. {% block pageheadercontent %}
  9.     <div class="page-title">
  10.         <h4 class="text-mint_cream">
  11.             <i class="icon-home position-left"></i>
  12.             <span class="text-semibold">Dashboard</span> -
  13.             {% if accessType == 'venues' %} VENUES {% elseif accessType == 'catering' %} CATERING {% else %} AV {% endif %}
  14.         </h4>
  15.     </div>
  16. {% endblock %}
  17. {% block stylesheets %}
  18.     {{ parent() }}
  19.     <link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css" rel="stylesheet">
  20.     <style>
  21.         :root {
  22.             --dash-primary: {{ primaryColor }};
  23.         }
  24.         {#/* Ajustes para que la hora no se corte */#}
  25.         {#.fc-daygrid-event {#}
  26.         {#    white-space: nowrap !important; /* Evita que el texto salte de línea */#}
  27.         {#    overflow: visible !important;   /* Permite que el contenido respire */#}
  28.         {#}#}
  29.         {#.fc-event-time {#}
  30.         {#    flex-shrink: 0 !important;      /* Impide que el título "apriete" a la hora */#}
  31.         {#    margin-right: 5px !important;   /* Espacio sutil entre hora y sala */#}
  32.         {#    font-weight: 700 !important;    /* Mejora la legibilidad */#}
  33.         {#}#}
  34.         /* Permite que el texto respete los saltos de línea \n */
  35.         .fc-event-title {
  36.             white-space: pre-wrap !important;
  37.             font-weight: 500 !important;
  38.             line-height: 1.2 !important; /* Junta un poco el texto para que no ocupe tanto alto */
  39.         }
  40.         .fc-daygrid-event {
  41.             white-space: normal !important;
  42.         }
  43.         {#/* Opcional: Aumentar ligeramente la altura de los eventos para que se vean mejor en Bootstrap 3 */#}
  44.         {#.fc-v-event .fc-event-main {#}
  45.         {#    padding: 2px 4px !important;#}
  46.         {#}#}
  47.     </style>
  48. {% endblock %}
  49. {% block content %}
  50.     <div id="appLoader" class="app-loader is-visible">
  51.         <div class="app-loader__box">
  52.             {% set loaderLogo = 'assets/images/platform/logo.png' %}
  53.             {% if accessType == 'venues' %}{% set loaderLogo = 'assets/images/platform/logo_ManteVenues.png' %}
  54.             {% elseif accessType == 'catering' %}{% set loaderLogo = 'assets/images/platform/Logo_ManteCatering.png' %}
  55.             {% elseif accessType == 'av' %}{% set loaderLogo = 'assets/images/platform/logo_ManteAv.png' %}
  56.             {% endif %}
  57.             <img src="{{ asset(loaderLogo) }}" width="300" alt="Cargando...">
  58.             <h4 class="text-white" style="margin-top: 25px; font-weight: 300;">Preparando tu espacio de trabajo...</h4>
  59.         </div>
  60.     </div>
  61.     <div class="row">
  62.         <div class="col-md-9">
  63.             <div class="panel panel-default" style="border-radius: 8px; border: none; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
  64.                 <div class="panel-body">
  65.                     <div class="calendar-wrapper">
  66.                         <div id="calendarLoader"><div class="spinner"></div></div>
  67.                         <div id="calendar"></div>
  68.                     </div>
  69.                 </div>
  70.             </div>
  71.         </div>
  72.         {% set kpiClass = (accessType == 'venues') ? 'col-xs-4' : 'col-xs-6' %}
  73.         <div class="col-md-3">
  74.             {# 1. KPIs SUPERIORES #}
  75.             <div class="row">
  76.                 <div class="{{ kpiClass }}">
  77.                     <div class="kpi-card bg-dash-1">
  78.                         <i class="icon-calendar52 kpi-icon"></i>
  79.                         <div class="kpi-data">
  80.                             <span class="kpi-number">{{ upcomingReservations|length }}</span>
  81.                         </div>
  82.                         <span class="kpi-desc text-center">Reservas</span>
  83.                     </div>
  84.                 </div>
  85.                 {% if accessType == 'venues' %}
  86.                     <div class="{{ kpiClass }}">
  87.                         <div class="kpi-card bg-dash-2">
  88.                             <i class="icon-users4 kpi-icon"></i>
  89.                             <div class="kpi-data">
  90.                                 <span class="kpi-number">{{ upcomingVisits|length }}</span>
  91.                             </div>
  92.                             <span class="kpi-desc text-center">Visitas</span>
  93.                         </div>
  94.                     </div>
  95.                 {% endif %}
  96.                 <div class="{{ kpiClass }}">
  97.                     <div class="kpi-card bg-dash-3">
  98.                         <i class="icon-alarm kpi-icon"></i>
  99.                         <div class="kpi-data">
  100.                             <span class="kpi-number">{{ notifications|length }}</span>
  101.                         </div>
  102.                         <span class="kpi-desc text-center">Notificaciones</span>
  103.                     </div>
  104.                 </div>
  105.             </div>
  106.             {# 1.5 PANEL DE ALERTAS (Solo en Venues) #}
  107.             {% if accessType == 'venues' %}
  108.                 <div class="panel panel-flat" style="border-radius: 8px; margin-bottom: 20px; border-left: 4px solid {% if userAlertsCount > 0 %}#F44336{% else %}#4CAF50{% endif %};">
  109.                     <div class="panel-heading" style="padding-bottom: 10px;">
  110.                         <h6 class="panel-title text-semibold {% if userAlertsCount > 0 %}text-danger{% else %}text-success{% endif %}">
  111.                             {% if userAlertsCount > 0 %}
  112.                                 <i class="icon-warning22 position-left"></i> Alertas de expedientes afectados
  113.                             {% else %}
  114.                                 <i class="icon-checkmark-circle position-left"></i> Todo al día
  115.                             {% endif %}
  116.                         </h6>
  117.                     </div>
  118.                     <div class="panel-body text-center" style="padding-top: 5px; padding-bottom: 20px;">
  119.                         {% if userAlertsCount > 0 %}
  120.                             <p class="text-muted" style="margin-bottom: 15px; font-size: 13px;">
  121.                                 Tienes <strong style="font-size: 16px; color: #F44336;">{{ userAlertsCount }}</strong> {% if userAlertsCount == 1 %}expediente{% else %}expedientes{% endif %} con salas que han perdido disponibilidad.
  122.                             </p>
  123.                             <a href="/venues/availability/" class="btn btn-block text-semibold" style="border-radius: 6px; background-color: #F44336; color: #ffffff; border: 1px solid #d32f2f;">
  124.                                 Resolver mis {{ userAlertsCount }} alertas <i class="icon-arrow-right14 position-right" style="color: #ffffff;"></i>
  125.                             </a>
  126.                         {% else %}
  127.                             <p class="text-muted" style="margin-bottom: 15px; font-size: 13px;">
  128.                                 Sin alertas. No tienes ningún expediente afectado en este momento.
  129.                             </p>
  130.                             <a href="/venues/availability/" class="btn btn-default btn-block text-semibold" style="border-radius: 6px; color: #666;">
  131.                                 Ver alertas de compañeros <i class="icon-arrow-right14 position-right"></i>
  132.                             </a>
  133.                         {% endif %}
  134.                     </div>
  135.                 </div>
  136.             {% endif %}
  137.             {# 2. PANEL DE VISITAS (Solo en Venues) #}
  138.             {% if accessType == 'venues' %}
  139.                 <div class="panel panel-flat" style="border-radius: 8px; margin-bottom: 20px;">
  140.                     <div class="panel-heading">
  141.                         <h6 class="panel-title text-semibold">
  142.                             <i class="icon-users4 position-left"></i> Visitas para hoy
  143.                         </h6>
  144.                     </div>
  145.                     <div class="panel-body scrollable-panel" style="padding: 0; max-height: 300px; overflow-y: auto;">
  146.                         <table class="table table-hover">
  147.                             <tbody>
  148.                             {% for v in upcomingVisits %}
  149.                                 <tr class="{{ isRestricted ? 'is-restricted-row' : '' }}">
  150.                                     <td style="padding: 12px 15px;"
  151.                                             {% if not isRestricted %}
  152.                                         class="js-event-link"
  153.                                         data-url="{{ r.url|default('#')|e('html_attr') }}"
  154.                                             {% endif %}>
  155.                                         <div class="text-semibold" style="color: #32aaf0">
  156.                                             {{ v.dateStart|date('d/m H:i') }}
  157.                                         </div>
  158.                                         <div class="text-muted">{{ v.title }}</div>
  159.                                         <div class="text-muted" style="font-size: 11px;">
  160.                                             <i class="icon-location3" style="font-size: 10px;"></i> {{ v.room|default('General') }}
  161.                                         </div>
  162.                                     </td>
  163.                                 </tr>
  164.                             {% else %}
  165.                                 <tr><td class="text-center text-muted p-20">Sin visitas</td></tr>
  166.                             {% endfor %}
  167.                             </tbody>
  168.                         </table>
  169.                     </div>
  170.                 </div>
  171.             {% endif %}
  172.             {# 3. PANEL DE ACTIVIDAD / RESERVAS #}
  173.             <div class="panel panel-flat" style="border-radius: 8px;">
  174.                 <div class="panel-heading">
  175.                     <h6 class="panel-title text-semibold">
  176.                         <i class="icon-list position-left"></i> Actividad de hoy
  177.                     </h6>
  178.                 </div>
  179.                 <div class="panel-body scrollable-panel" style="padding: 0; max-height: 400px; overflow-y: auto;">
  180.                     <table class="table table-hover">
  181.                         <tbody>
  182.                         {% set listData = upcomingReservations|default([]) %}
  183.                         {% for r in listData %}
  184.                             <tr>
  185.                                 <td style="padding: 12px 15px;"
  186.                                         {% if not isRestricted %}
  187.                                     class="js-event-link"
  188.                                     data-url="{{ r.url|default('#')|e('html_attr') }}"
  189.                                         {% elseif r.status in ['Invoiced', 'Facturado'] and r.invoiceUrl %}
  190.                                     class="js-event-link"
  191.                                     data-url="{{ r.invoiceUrl|e('html_attr') }}"
  192.                                         {% else %}
  193.                                     class="js-event-link"
  194.                                     data-url="{{ r.showUrl|default('#')|e('html_attr') }}"
  195.                                         {% endif %}>
  196.                                     <div class="text-semibold" style="color: var(--dash-primary)">
  197.                                         {{ r.dateStart|date('d/m H:i') }}
  198.                                     </div>
  199.                                     <div class="text-bold">{{ r.title }}</div>
  200.                                     <div class="text-muted" style="font-size: 11px;">
  201.                                         <i class="icon-office" style="font-size: 10px;"></i> {{ r.room|default('General') }}
  202.                                     </div>
  203.                                 </td>
  204.                             </tr>
  205.                         {% else %}
  206.                             <tr><td class="text-center text-muted p-20">Sin actividad</td></tr>
  207.                         {% endfor %}
  208.                         </tbody>
  209.                     </table>
  210.                 </div>
  211.             </div>
  212.         </div>
  213.     </div>
  214.     {# Modal de Notificaciones #}
  215.     {% set hasNotifications = notifications is defined and notifications|length > 0 %}
  216.     <div id="notificationsModal" class="modal fade" tabindex="-1" aria-labelledby="notificationsModalLabel">
  217.         <div class="modal-dialog"  role="document">
  218.             <div class="modal-content">
  219.                 <div class="modal-header">
  220.                     <button type="button" class="close" data-dismiss="modal">&times;</button>
  221.                     <h5 class="modal-title">Notificaciones</h5>
  222.                 </div>
  223.                 {% if hasNotifications %}
  224.                         <ul class="list-group" id="notificationsList">
  225.                             {% for n in notifications %}
  226.                                 <li class="list-group-item" data-id="{{ n.id }}">
  227.                                     <div class="clearfix">
  228.                                         <div class="pull-left">
  229.                                             <div>
  230.                                                 <strong>{{ (n.category|lower|capitalize)|trans({}) }}</strong>
  231.                                             </div>
  232.                                             <div>{{ n.text }}</div>
  233.                                             {% if n.linkUrl %}
  234.                                                 <div class="m-t-5">
  235.                                                     <a href="{{ path('reservations_venues_edit_simple', {'id': n.linkUrl}) }}" class="btn btn-link btn-xs" target="_blank" rel="noopener">
  236.                                                         {{ n.linkLabel|default('Abrir') }}
  237.                                                     </a>
  238.                                                 </div>
  239.                                             {% endif %}
  240.                                         </div>
  241.                                         <div class="pull-right">
  242.                                             <button type="button" class="btn btn-default btn-xs js-mark-seen" data-url="{{ path('me_notifications_mark_seen', {'id': n.id}) }}">
  243.                                                 Marcar como vista
  244.                                             </button>
  245.                                         </div>
  246.                                     </div>
  247.                                 </li>
  248.                             {% endfor %}
  249.                         </ul>
  250.                     {% else %}
  251.                         <p class="text-muted">No hay notificaciones pendientes.</p>
  252.                     {% endif %}
  253.                 </div>
  254.         </div>
  255.     </div>
  256.     {# Disparador oculto por si queremos abrir sin jQuery (fallback simple) #}
  257.     <button type="button" id="openNotifModalTrigger" class="hidden" data-toggle="modal" data-target="#notificationsModal"></button>
  258.     {# Añade esto justo antes del bloque de scripts #}
  259.     <div id="debug-access" data-type="{{ accessType }}" style="display:none;"></div>
  260. {% endblock %}
  261. {% block javascripts %}
  262.     {{ parent() }}
  263.     <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js"></script>
  264.     <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales-all.min.js"></script>
  265.     <script>
  266.         document.addEventListener('DOMContentLoaded', function() {
  267.             const isRestricted = {{ isRestricted ? 'true' : 'false' }};
  268.             const appLoader = document.getElementById('appLoader');
  269.             const calendarLoader = document.getElementById('calendarLoader');
  270.             const calendarEl = document.getElementById('calendar');
  271.             let eventSources = [];
  272.             {% if accessType == 'venues' or isRestricted %}
  273.             eventSources.push({ url: '{{ path("calendar_global_reservations") }}', color: '{{ primaryColor }}' });
  274.             eventSources.push({ url: '{{ path("calendar_global_visits") }}', color: '#32aaf0' });
  275.             {% elseif accessType == 'catering' %}
  276.             // 1. Eventos específicos de Higo & Trigo (Forzamos Rojo)
  277.             eventSources.push({
  278.                 url: '{{ path("calendar_ht_events") }}',
  279.                 color: '#FAAFC3',
  280.                 textColor: '#ffffff'
  281.             });
  282.             // 2. Traemos también las Reservas/Montajes de Venues
  283.             eventSources.push({
  284.                 url: '{{ path("calendar_catering_reservations") }}',
  285.                 color: '{{ primaryColor }}'
  286.             });
  287.             // 3. NUEVO: Pruebas de menú (Menu Testings)
  288.             eventSources.push({
  289.                 url: '{{ path("calendar_catering_menu_testings") }}',
  290.                 color: '#ccad11',
  291.                 textColor: '#ffffff'
  292.             });
  293.             {% elseif accessType == 'av' %}
  294.             eventSources.push({ url: '{{ path("calendar_av_events") }}', color: '#F8A437' });
  295.             {% endif %}
  296.             const calendar = new FullCalendar.Calendar(calendarEl, {
  297.                 initialView: 'dayGridMonth',
  298.                 locale: 'es',
  299.                 height: 'auto',
  300.                 contentHeight: 'auto',
  301.                 headerToolbar: {
  302.                     left: 'prev,next today',
  303.                     center: 'title',
  304.                     right: 'dayGridMonth,timeGridWeek'
  305.                 },
  306.                 eventTimeFormat: {
  307.                     hour: '2-digit',
  308.                     minute: '2-digit',
  309.                     hour12: false
  310.                 },
  311.                 // ORDENACIÓN: Prioridad absoluta al campo orderPriority sobre la hora
  312.                 eventOrder: "orderPriority,start",
  313.                 eventSources: eventSources,
  314.                 eventDataTransform: function(eventData) {
  315.                     // Elevamos la prioridad a la raíz para que eventOrder la detecte
  316.                     if (eventData.extendedProps && eventData.extendedProps.orderPriority !== undefined) {
  317.                         eventData.orderPriority = eventData.extendedProps.orderPriority;
  318.                     } else {
  319.                         eventData.orderPriority = 99;
  320.                     }
  321.                     if (eventData.url) {
  322.                         eventData.extendedProps = eventData.extendedProps || {};
  323.                         eventData.extendedProps.hiddenUrl = eventData.url;
  324.                         delete eventData.url;
  325.                     }
  326.                     return eventData;
  327.                 },
  328.                 // UNIFICADO: Una sola declaración de eventDidMount
  329.                 eventDidMount: function(info) {
  330.                     if (isRestricted) {
  331.                         const status = info.event.extendedProps.status;
  332.                         if (status !== 'Invoiced' && status !== 'Facturado') {
  333.                             // info.el.classList.add('is-restricted-event'); // Quitamos la clase que lo ocultaba visualmente o lo bloqueaba
  334.                             info.el.style.cursor = 'pointer';
  335.                         } else {
  336.                             info.el.style.cursor = 'pointer';
  337.                         }
  338.                     }
  339.                     // Forzar visualización de la hora
  340.                     if (info.event.start) {
  341.                         const timeEl = info.el.querySelector('.fc-event-time');
  342.                         if (timeEl) timeEl.style.fontWeight = 'bold';
  343.                     }
  344.                     // Atributos para resaltado de grupos
  345.                     const groupId = info.event.extendedProps.groupId;
  346.                     if (groupId) {
  347.                         info.el.setAttribute('data-group-id', groupId);
  348.                     }
  349.                     // Tooltip con Bootstrap 3
  350.                     if (info.event.extendedProps.tooltipHtml) {
  351.                         $(info.el).tooltip({
  352.                             title: info.event.extendedProps.tooltipHtml,
  353.                             html: true,
  354.                             container: 'body',
  355.                             trigger: 'hover'
  356.                         });
  357.                     }
  358.                 },
  359.                 eventClick: function(info) {
  360.                     info.jsEvent.preventDefault();
  361.                     
  362.                     if (isRestricted) {
  363.                         const status = info.event.extendedProps.status;
  364.                         const invoiceUrl = info.event.extendedProps.invoiceUrl;
  365.                         const showUrl = info.event.extendedProps.showUrl;
  366.                         
  367.                         // Si es Legends y está facturada, permitimos ver la factura
  368.                         if ((status === 'Invoiced' || status === 'Facturado') && invoiceUrl) {
  369.                             window.open(invoiceUrl, '_blank', 'noopener');
  370.                         } else if (showUrl) {
  371.                             // Si no, permitimos ver el detalle (Show)
  372.                             window.open(showUrl, '_blank', 'noopener');
  373.                         }
  374.                         return false;
  375.                     }
  376.                     const url = info.event.extendedProps.hiddenUrl;
  377.                     if (url) window.open(url, '_blank', 'noopener');
  378.                 },
  379.                 loading: function(isLoading) {
  380.                     if (!isLoading) {
  381.                         if (calendarLoader) calendarLoader.style.display = 'none';
  382.                         if (appLoader) appLoader.classList.remove('is-visible');
  383.                     }
  384.                 },
  385.                 eventMouseEnter: function(info) {
  386.                     info.el.style.zIndex = 10000;
  387.                     const groupId = info.event.extendedProps.groupId;
  388.                     if (groupId) {
  389.                         const events = document.querySelectorAll('.fc-event[data-group-id="' + groupId + '"]');
  390.                         if (events.length > 1) {
  391.                             document.querySelectorAll('.fc-event').forEach(el => el.classList.add('evento-difuminado'));
  392.                             events.forEach(el => {
  393.                                 el.classList.remove('evento-difuminado');
  394.                                 el.classList.add('evento-resaltado');
  395.                             });
  396.                         }
  397.                     }
  398.                 },
  399.                 eventMouseLeave: function(info) {
  400.                     info.el.style.zIndex = '';
  401.                     document.querySelectorAll('.fc-event').forEach(el => {
  402.                         el.classList.remove('evento-difuminado', 'evento-resaltado');
  403.                     });
  404.                 }
  405.             });
  406.             calendar.render();
  407.             document.querySelectorAll('.js-event-link').forEach(function(el) {
  408.                 el.addEventListener('click', function() {
  409.                     const url = this.getAttribute('data-url');
  410.                     if (url) window.open(url, '_blank', 'noopener');
  411.                 });
  412.             });
  413.             // --- NUEVO CÓDIGO AQUÍ: Listener para los botones "Marcar como vista" ---
  414.             document.querySelectorAll('.js-mark-seen').forEach(function(btn) {
  415.                 btn.addEventListener('click', function(e) {
  416.                     e.preventDefault();
  417.                     const url = this.getAttribute('data-url');
  418.                     const listItem = this.closest('li.list-group-item');
  419.                     const button = this;
  420.                     // Deshabilitamos el botón temporalmente
  421.                     button.disabled = true;
  422.                     button.textContent = 'Marcando...';
  423.                     // Hacemos la petición AJAX
  424.                     fetch(url, {
  425.                         method: 'POST', // Asegúrate de que tu ruta en Symfony soporta POST
  426.                         headers: {
  427.                             'X-Requested-With': 'XMLHttpRequest'
  428.                         }
  429.                     })
  430.                             .then(response => {
  431.                                 if (response.ok) {
  432.                                     // Animación de salida y borrado
  433.                                     if (listItem) {
  434.                                         listItem.style.transition = 'opacity 0.3s';
  435.                                         listItem.style.opacity = '0';
  436.                                         setTimeout(() => {
  437.                                             listItem.remove();
  438.                                             // Comprobar si quedan notificaciones
  439.                                             const remaining = document.querySelectorAll('#notificationsList .list-group-item');
  440.                                             if (remaining.length === 0) {
  441.                                                 $('#notificationsModal').modal('hide');
  442.                                             }
  443.                                         }, 300);
  444.                                     }
  445.                                 } else {
  446.                                     console.error('Error al marcar como vista');
  447.                                     button.disabled = false;
  448.                                     button.textContent = 'Marcar como vista';
  449.                                 }
  450.                             })
  451.                             .catch(error => {
  452.                                 console.error('Error:', error);
  453.                                 button.disabled = false;
  454.                                 button.textContent = 'Marcar como vista';
  455.                             });
  456.                 });
  457.             });
  458.             // --- FIN DEL NUEVO CÓDIGO ---
  459.             {% if notifications|length > 0 %}
  460.             if (typeof jQuery !== 'undefined' && jQuery.fn.modal) {
  461.                 jQuery('#notificationsModal').modal('show');
  462.             }
  463.             {% endif %}
  464.         });
  465.         console.log("Access Type detectado:", document.getElementById('debug-access').dataset.type);
  466.     </script>
  467. {% endblock %}