{# templates/home/index.html.twig #}
{% extends 'common/base.html.twig' %}
{# 1. VARIABLES DE ESTADO GLOBALES #}
{% set isRestricted = app.user and app.user.team|upper|trim == 'LEGENDS VIEW' %}
{% set accessType = accessType|default((branding_current is not null) ? branding_current.accessType : 'venues') %}
{% set primaryColor = (branding_current is not null) ? branding_current.primaryColor : '#bdbf09' %}
{% block pageid %}home-page{% endblock %}
{% block pageheadercontent %}
<div class="page-title">
<h4 class="text-mint_cream">
<i class="icon-home position-left"></i>
<span class="text-semibold">Dashboard</span> -
{% if accessType == 'venues' %} VENUES {% elseif accessType == 'catering' %} CATERING {% else %} AV {% endif %}
</h4>
</div>
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css" rel="stylesheet">
<style>
:root {
--dash-primary: {{ primaryColor }};
}
{#/* Ajustes para que la hora no se corte */#}
{#.fc-daygrid-event {#}
{# white-space: nowrap !important; /* Evita que el texto salte de línea */#}
{# overflow: visible !important; /* Permite que el contenido respire */#}
{#}#}
{#.fc-event-time {#}
{# flex-shrink: 0 !important; /* Impide que el título "apriete" a la hora */#}
{# margin-right: 5px !important; /* Espacio sutil entre hora y sala */#}
{# font-weight: 700 !important; /* Mejora la legibilidad */#}
{#}#}
/* Permite que el texto respete los saltos de línea \n */
.fc-event-title {
white-space: pre-wrap !important;
font-weight: 500 !important;
line-height: 1.2 !important; /* Junta un poco el texto para que no ocupe tanto alto */
}
.fc-daygrid-event {
white-space: normal !important;
}
{#/* Opcional: Aumentar ligeramente la altura de los eventos para que se vean mejor en Bootstrap 3 */#}
{#.fc-v-event .fc-event-main {#}
{# padding: 2px 4px !important;#}
{#}#}
</style>
{% endblock %}
{% block content %}
<div id="appLoader" class="app-loader is-visible">
<div class="app-loader__box">
{% set loaderLogo = 'assets/images/platform/logo.png' %}
{% if accessType == 'venues' %}{% set loaderLogo = 'assets/images/platform/logo_ManteVenues.png' %}
{% elseif accessType == 'catering' %}{% set loaderLogo = 'assets/images/platform/Logo_ManteCatering.png' %}
{% elseif accessType == 'av' %}{% set loaderLogo = 'assets/images/platform/logo_ManteAv.png' %}
{% endif %}
<img src="{{ asset(loaderLogo) }}" width="300" alt="Cargando...">
<h4 class="text-white" style="margin-top: 25px; font-weight: 300;">Preparando tu espacio de trabajo...</h4>
</div>
</div>
<div class="row">
<div class="col-md-9">
<div class="panel panel-default" style="border-radius: 8px; border: none; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
<div class="panel-body">
<div class="calendar-wrapper">
<div id="calendarLoader"><div class="spinner"></div></div>
<div id="calendar"></div>
</div>
</div>
</div>
</div>
{% set kpiClass = (accessType == 'venues') ? 'col-xs-4' : 'col-xs-6' %}
<div class="col-md-3">
{# 1. KPIs SUPERIORES #}
<div class="row">
<div class="{{ kpiClass }}">
<div class="kpi-card bg-dash-1">
<i class="icon-calendar52 kpi-icon"></i>
<div class="kpi-data">
<span class="kpi-number">{{ upcomingReservations|length }}</span>
</div>
<span class="kpi-desc text-center">Reservas</span>
</div>
</div>
{% if accessType == 'venues' %}
<div class="{{ kpiClass }}">
<div class="kpi-card bg-dash-2">
<i class="icon-users4 kpi-icon"></i>
<div class="kpi-data">
<span class="kpi-number">{{ upcomingVisits|length }}</span>
</div>
<span class="kpi-desc text-center">Visitas</span>
</div>
</div>
{% endif %}
<div class="{{ kpiClass }}">
<div class="kpi-card bg-dash-3">
<i class="icon-alarm kpi-icon"></i>
<div class="kpi-data">
<span class="kpi-number">{{ notifications|length }}</span>
</div>
<span class="kpi-desc text-center">Notificaciones</span>
</div>
</div>
</div>
{# 1.5 PANEL DE ALERTAS (Solo en Venues) #}
{% if accessType == 'venues' %}
<div class="panel panel-flat" style="border-radius: 8px; margin-bottom: 20px; border-left: 4px solid {% if userAlertsCount > 0 %}#F44336{% else %}#4CAF50{% endif %};">
<div class="panel-heading" style="padding-bottom: 10px;">
<h6 class="panel-title text-semibold {% if userAlertsCount > 0 %}text-danger{% else %}text-success{% endif %}">
{% if userAlertsCount > 0 %}
<i class="icon-warning22 position-left"></i> Alertas de expedientes afectados
{% else %}
<i class="icon-checkmark-circle position-left"></i> Todo al día
{% endif %}
</h6>
</div>
<div class="panel-body text-center" style="padding-top: 5px; padding-bottom: 20px;">
{% if userAlertsCount > 0 %}
<p class="text-muted" style="margin-bottom: 15px; font-size: 13px;">
Tienes <strong style="font-size: 16px; color: #F44336;">{{ userAlertsCount }}</strong> {% if userAlertsCount == 1 %}expediente{% else %}expedientes{% endif %} con salas que han perdido disponibilidad.
</p>
<a href="/venues/availability/" class="btn btn-block text-semibold" style="border-radius: 6px; background-color: #F44336; color: #ffffff; border: 1px solid #d32f2f;">
Resolver mis {{ userAlertsCount }} alertas <i class="icon-arrow-right14 position-right" style="color: #ffffff;"></i>
</a>
{% else %}
<p class="text-muted" style="margin-bottom: 15px; font-size: 13px;">
Sin alertas. No tienes ningún expediente afectado en este momento.
</p>
<a href="/venues/availability/" class="btn btn-default btn-block text-semibold" style="border-radius: 6px; color: #666;">
Ver alertas de compañeros <i class="icon-arrow-right14 position-right"></i>
</a>
{% endif %}
</div>
</div>
{% endif %}
{# 2. PANEL DE VISITAS (Solo en Venues) #}
{% if accessType == 'venues' %}
<div class="panel panel-flat" style="border-radius: 8px; margin-bottom: 20px;">
<div class="panel-heading">
<h6 class="panel-title text-semibold">
<i class="icon-users4 position-left"></i> Visitas para hoy
</h6>
</div>
<div class="panel-body scrollable-panel" style="padding: 0; max-height: 300px; overflow-y: auto;">
<table class="table table-hover">
<tbody>
{% for v in upcomingVisits %}
<tr class="{{ isRestricted ? 'is-restricted-row' : '' }}">
<td style="padding: 12px 15px;"
{% if not isRestricted %}
class="js-event-link"
data-url="{{ r.url|default('#')|e('html_attr') }}"
{% endif %}>
<div class="text-semibold" style="color: #32aaf0">
{{ v.dateStart|date('d/m H:i') }}
</div>
<div class="text-muted">{{ v.title }}</div>
<div class="text-muted" style="font-size: 11px;">
<i class="icon-location3" style="font-size: 10px;"></i> {{ v.room|default('General') }}
</div>
</td>
</tr>
{% else %}
<tr><td class="text-center text-muted p-20">Sin visitas</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# 3. PANEL DE ACTIVIDAD / RESERVAS #}
<div class="panel panel-flat" style="border-radius: 8px;">
<div class="panel-heading">
<h6 class="panel-title text-semibold">
<i class="icon-list position-left"></i> Actividad de hoy
</h6>
</div>
<div class="panel-body scrollable-panel" style="padding: 0; max-height: 400px; overflow-y: auto;">
<table class="table table-hover">
<tbody>
{% set listData = upcomingReservations|default([]) %}
{% for r in listData %}
<tr>
<td style="padding: 12px 15px;"
{% if not isRestricted %}
class="js-event-link"
data-url="{{ r.url|default('#')|e('html_attr') }}"
{% elseif r.status in ['Invoiced', 'Facturado'] and r.invoiceUrl %}
class="js-event-link"
data-url="{{ r.invoiceUrl|e('html_attr') }}"
{% else %}
class="js-event-link"
data-url="{{ r.showUrl|default('#')|e('html_attr') }}"
{% endif %}>
<div class="text-semibold" style="color: var(--dash-primary)">
{{ r.dateStart|date('d/m H:i') }}
</div>
<div class="text-bold">{{ r.title }}</div>
<div class="text-muted" style="font-size: 11px;">
<i class="icon-office" style="font-size: 10px;"></i> {{ r.room|default('General') }}
</div>
</td>
</tr>
{% else %}
<tr><td class="text-center text-muted p-20">Sin actividad</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{# Modal de Notificaciones #}
{% set hasNotifications = notifications is defined and notifications|length > 0 %}
<div id="notificationsModal" class="modal fade" tabindex="-1" aria-labelledby="notificationsModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h5 class="modal-title">Notificaciones</h5>
</div>
{% if hasNotifications %}
<ul class="list-group" id="notificationsList">
{% for n in notifications %}
<li class="list-group-item" data-id="{{ n.id }}">
<div class="clearfix">
<div class="pull-left">
<div>
<strong>{{ (n.category|lower|capitalize)|trans({}) }}</strong>
</div>
<div>{{ n.text }}</div>
{% if n.linkUrl %}
<div class="m-t-5">
<a href="{{ path('reservations_venues_edit_simple', {'id': n.linkUrl}) }}" class="btn btn-link btn-xs" target="_blank" rel="noopener">
{{ n.linkLabel|default('Abrir') }}
</a>
</div>
{% endif %}
</div>
<div class="pull-right">
<button type="button" class="btn btn-default btn-xs js-mark-seen" data-url="{{ path('me_notifications_mark_seen', {'id': n.id}) }}">
Marcar como vista
</button>
</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">No hay notificaciones pendientes.</p>
{% endif %}
</div>
</div>
</div>
{# Disparador oculto por si queremos abrir sin jQuery (fallback simple) #}
<button type="button" id="openNotifModalTrigger" class="hidden" data-toggle="modal" data-target="#notificationsModal"></button>
{# Añade esto justo antes del bloque de scripts #}
<div id="debug-access" data-type="{{ accessType }}" style="display:none;"></div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales-all.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const isRestricted = {{ isRestricted ? 'true' : 'false' }};
const appLoader = document.getElementById('appLoader');
const calendarLoader = document.getElementById('calendarLoader');
const calendarEl = document.getElementById('calendar');
let eventSources = [];
{% if accessType == 'venues' or isRestricted %}
eventSources.push({ url: '{{ path("calendar_global_reservations") }}', color: '{{ primaryColor }}' });
eventSources.push({ url: '{{ path("calendar_global_visits") }}', color: '#32aaf0' });
{% elseif accessType == 'catering' %}
// 1. Eventos específicos de Higo & Trigo (Forzamos Rojo)
eventSources.push({
url: '{{ path("calendar_ht_events") }}',
color: '#FAAFC3',
textColor: '#ffffff'
});
// 2. Traemos también las Reservas/Montajes de Venues
eventSources.push({
url: '{{ path("calendar_catering_reservations") }}',
color: '{{ primaryColor }}'
});
// 3. NUEVO: Pruebas de menú (Menu Testings)
eventSources.push({
url: '{{ path("calendar_catering_menu_testings") }}',
color: '#ccad11',
textColor: '#ffffff'
});
{% elseif accessType == 'av' %}
eventSources.push({ url: '{{ path("calendar_av_events") }}', color: '#F8A437' });
{% endif %}
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
locale: 'es',
height: 'auto',
contentHeight: 'auto',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek'
},
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
// ORDENACIÓN: Prioridad absoluta al campo orderPriority sobre la hora
eventOrder: "orderPriority,start",
eventSources: eventSources,
eventDataTransform: function(eventData) {
// Elevamos la prioridad a la raíz para que eventOrder la detecte
if (eventData.extendedProps && eventData.extendedProps.orderPriority !== undefined) {
eventData.orderPriority = eventData.extendedProps.orderPriority;
} else {
eventData.orderPriority = 99;
}
if (eventData.url) {
eventData.extendedProps = eventData.extendedProps || {};
eventData.extendedProps.hiddenUrl = eventData.url;
delete eventData.url;
}
return eventData;
},
// UNIFICADO: Una sola declaración de eventDidMount
eventDidMount: function(info) {
if (isRestricted) {
const status = info.event.extendedProps.status;
if (status !== 'Invoiced' && status !== 'Facturado') {
// info.el.classList.add('is-restricted-event'); // Quitamos la clase que lo ocultaba visualmente o lo bloqueaba
info.el.style.cursor = 'pointer';
} else {
info.el.style.cursor = 'pointer';
}
}
// Forzar visualización de la hora
if (info.event.start) {
const timeEl = info.el.querySelector('.fc-event-time');
if (timeEl) timeEl.style.fontWeight = 'bold';
}
// Atributos para resaltado de grupos
const groupId = info.event.extendedProps.groupId;
if (groupId) {
info.el.setAttribute('data-group-id', groupId);
}
// Tooltip con Bootstrap 3
if (info.event.extendedProps.tooltipHtml) {
$(info.el).tooltip({
title: info.event.extendedProps.tooltipHtml,
html: true,
container: 'body',
trigger: 'hover'
});
}
},
eventClick: function(info) {
info.jsEvent.preventDefault();
if (isRestricted) {
const status = info.event.extendedProps.status;
const invoiceUrl = info.event.extendedProps.invoiceUrl;
const showUrl = info.event.extendedProps.showUrl;
// Si es Legends y está facturada, permitimos ver la factura
if ((status === 'Invoiced' || status === 'Facturado') && invoiceUrl) {
window.open(invoiceUrl, '_blank', 'noopener');
} else if (showUrl) {
// Si no, permitimos ver el detalle (Show)
window.open(showUrl, '_blank', 'noopener');
}
return false;
}
const url = info.event.extendedProps.hiddenUrl;
if (url) window.open(url, '_blank', 'noopener');
},
loading: function(isLoading) {
if (!isLoading) {
if (calendarLoader) calendarLoader.style.display = 'none';
if (appLoader) appLoader.classList.remove('is-visible');
}
},
eventMouseEnter: function(info) {
info.el.style.zIndex = 10000;
const groupId = info.event.extendedProps.groupId;
if (groupId) {
const events = document.querySelectorAll('.fc-event[data-group-id="' + groupId + '"]');
if (events.length > 1) {
document.querySelectorAll('.fc-event').forEach(el => el.classList.add('evento-difuminado'));
events.forEach(el => {
el.classList.remove('evento-difuminado');
el.classList.add('evento-resaltado');
});
}
}
},
eventMouseLeave: function(info) {
info.el.style.zIndex = '';
document.querySelectorAll('.fc-event').forEach(el => {
el.classList.remove('evento-difuminado', 'evento-resaltado');
});
}
});
calendar.render();
document.querySelectorAll('.js-event-link').forEach(function(el) {
el.addEventListener('click', function() {
const url = this.getAttribute('data-url');
if (url) window.open(url, '_blank', 'noopener');
});
});
// --- NUEVO CÓDIGO AQUÍ: Listener para los botones "Marcar como vista" ---
document.querySelectorAll('.js-mark-seen').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
const url = this.getAttribute('data-url');
const listItem = this.closest('li.list-group-item');
const button = this;
// Deshabilitamos el botón temporalmente
button.disabled = true;
button.textContent = 'Marcando...';
// Hacemos la petición AJAX
fetch(url, {
method: 'POST', // Asegúrate de que tu ruta en Symfony soporta POST
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (response.ok) {
// Animación de salida y borrado
if (listItem) {
listItem.style.transition = 'opacity 0.3s';
listItem.style.opacity = '0';
setTimeout(() => {
listItem.remove();
// Comprobar si quedan notificaciones
const remaining = document.querySelectorAll('#notificationsList .list-group-item');
if (remaining.length === 0) {
$('#notificationsModal').modal('hide');
}
}, 300);
}
} else {
console.error('Error al marcar como vista');
button.disabled = false;
button.textContent = 'Marcar como vista';
}
})
.catch(error => {
console.error('Error:', error);
button.disabled = false;
button.textContent = 'Marcar como vista';
});
});
});
// --- FIN DEL NUEVO CÓDIGO ---
{% if notifications|length > 0 %}
if (typeof jQuery !== 'undefined' && jQuery.fn.modal) {
jQuery('#notificationsModal').modal('show');
}
{% endif %}
});
console.log("Access Type detectado:", document.getElementById('debug-access').dataset.type);
</script>
{% endblock %}