Actualizar calendario.html

This commit is contained in:
2026-02-24 23:12:59 +00:00
parent 0d3e3b71df
commit dc6d4fcc60

View File

@@ -180,16 +180,28 @@
let calendarEvents = []; let calendarEvents = [];
let calendarInstance = null; let calendarInstance = null;
// Diccionario de colores. AÑADIMOS EL BLOQUEO (Rojo vivo) // DICCIONARIO DE COLORES E ICONOS
const statusConfig = { const statusConfig = {
'citado': { bg: '#3b82f6', border: '#2563eb', label: 'Citado / Agendado' }, // Azul 'citado': { bg: '#3b82f6', border: '#2563eb', label: 'Citado / Agendado', icon: 'calendar' },
'de_camino': { bg: '#8b5cf6', border: '#7c3aed', label: 'De Camino' }, // Morado 'de_camino': { bg: '#8b5cf6', border: '#7c3aed', label: 'Técnico de Camino', icon: 'truck' }, // COCHE/CAMIÓN
'trabajando': { bg: '#f59e0b', border: '#d97706', label: 'Trabajando en lugar' }, // Naranja 'trabajando': { bg: '#f59e0b', border: '#d97706', label: 'En Reparación', icon: 'hammer' }, // MARTILLO
'incidencia': { bg: '#ef4444', border: '#dc2626', label: 'Pausado / Incidencia' }, // Rojo 'incidencia': { bg: '#ef4444', border: '#dc2626', label: 'Pausado / Incidencia', icon: 'alert-triangle' },
'terminado': { bg: '#10b981', border: '#059669', label: 'Trabajo Terminado' }, // Verde 'terminado': { bg: '#10b981', border: '#059669', label: 'Trabajo Terminado', icon: 'check-circle' },
'SYSTEM_BLOCK': { bg: '#f43f5e', border: '#e11d48', label: 'BLOQUEO DE AGENDA' } // Rosa fuerte 'SYSTEM_BLOCK': { bg: '#f43f5e', border: '#e11d48', label: 'BLOQUEO DE AGENDA', icon: 'shield-alert' }
}; };
// Función para detectar la clave correcta aunque venga el ID o un texto variable
function getStatusKey(status) {
if (!status) return 'citado';
const s = String(status).toLowerCase();
if (s.includes('camino')) return 'de_camino';
if (s.includes('trabajando') || s.includes('reparacion')) return 'trabajando';
if (s.includes('incidencia') || s.includes('pausado')) return 'incidencia';
if (s.includes('terminado') || s.includes('finalizado')) return 'terminado';
if (s === 'system_block') return 'SYSTEM_BLOCK';
return 'citado';
}
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
if (!localStorage.getItem("token")) window.location.href = "index.html"; if (!localStorage.getItem("token")) window.location.href = "index.html";
lucide.createIcons(); lucide.createIcons();
@@ -210,17 +222,16 @@
async function loadServices() { async function loadServices() {
try { try {
// Traemos los servicios activos
const res = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
// Filtramos solo los que tienen fecha de inicio programada
rawServices = data.services.filter(s => s.raw_data && s.raw_data.scheduled_date && s.raw_data.scheduled_date !== ""); rawServices = data.services.filter(s => s.raw_data && s.raw_data.scheduled_date && s.raw_data.scheduled_date !== "");
// Extraemos compañías (ignorando los bloqueos)
const compSelect = document.getElementById('compFilter'); const compSelect = document.getElementById('compFilter');
const uniqueComps = [...new Set(rawServices.filter(s => s.provider !== 'SYSTEM_BLOCK').map(s => (s.raw_data['Compañía'] || s.raw_data['COMPAÑIA'] || "Particular").toString().toUpperCase()))].sort(); const uniqueComps = [...new Set(rawServices.filter(s => s.provider !== 'SYSTEM_BLOCK').map(s => (s.raw_data['Compañía'] || s.raw_data['COMPAÑIA'] || "Particular").toString().toUpperCase()))].sort();
compSelect.innerHTML = '<option value="ALL">TODAS LAS COMPAÑÍAS</option>';
uniqueComps.forEach(c => compSelect.innerHTML += `<option value="${c}">${c}</option>`); uniqueComps.forEach(c => compSelect.innerHTML += `<option value="${c}">${c}</option>`);
processEventsAndRender(); processEventsAndRender();
@@ -235,24 +246,19 @@
const name = r['Nombre Cliente'] || r['CLIENTE'] || "Sin Nombre"; const name = r['Nombre Cliente'] || r['CLIENTE'] || "Sin Nombre";
const pop = r['Población'] || r['POBLACION-PROVINCIA'] || ""; const pop = r['Población'] || r['POBLACION-PROVINCIA'] || "";
const title = isBlock ? `BLOQUEADO: ${r['Descripción'] || 'Motivo no especificado'}` : `${name} - ${pop}`; const title = isBlock ? `BLOQUEO: ${r['Descripción'] || 'Motivo'}` : `${name} - ${pop}`;
// Formato ISO: YYYY-MM-DDTHH:mm:00
const dateIso = r.scheduled_date; const dateIso = r.scheduled_date;
const timeIso = r.scheduled_time ? r.scheduled_time + ':00' : '09:00:00'; const timeIso = r.scheduled_time ? r.scheduled_time + ':00' : '09:00:00';
const startStr = `${dateIso}T${timeIso}`; const startStr = `${dateIso}T${timeIso}`;
// Calcular la fecha/hora de fin sumando los minutos
const startObj = new Date(startStr); const startObj = new Date(startStr);
const duration = parseInt(r.duration_minutes || 60); const duration = parseInt(r.duration_minutes || 60);
const endObj = new Date(startObj.getTime() + duration * 60000); const endObj = new Date(startObj.getTime() + duration * 60000);
// Ajuste de offset horario local para la ISO String final
const endStr = new Date(endObj.getTime() - (endObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 19); const endStr = new Date(endObj.getTime() - (endObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 19);
// Color según estado (o si es bloqueo) const statusKey = isBlock ? 'SYSTEM_BLOCK' : getStatusKey(r.status_operativo);
const status = isBlock ? 'SYSTEM_BLOCK' : (r.status_operativo || 'citado'); const sConf = statusConfig[statusKey] || statusConfig['citado'];
const sConf = statusConfig[status] || statusConfig['citado'];
return { return {
id: svc.id, id: svc.id,
@@ -271,30 +277,29 @@
}; };
}); });
if (calendarInstance) {
calendarInstance.removeAllEventSources();
calendarInstance.addEventSource(calendarEvents);
} else {
initCalendar(); initCalendar();
} }
}
function initCalendar() { function initCalendar() {
const calendarEl = document.getElementById('calendar'); const calendarEl = document.getElementById('calendar');
calendarInstance = new FullCalendar.Calendar(calendarEl, { calendarInstance = new FullCalendar.Calendar(calendarEl, {
initialView: 'timeGridWeek', initialView: 'timeGridWeek',
locale: 'es', // FORZAMOS IDIOMA ESPAÑOL PARA LA VENTANITA locale: 'es',
firstDay: 1, firstDay: 1,
slotMinTime: '08:00:00', slotMinTime: '08:00:00',
slotMaxTime: '22:00:00', slotMaxTime: '22:00:00',
expandRows: true, expandRows: true,
allDaySlot: false, allDaySlot: false,
nowIndicator: true, nowIndicator: true,
dayMaxEvents: 3, // Cuántas caben antes de mostrar "X más" dayMaxEvents: 3,
moreLinkText: "más", // TRADUCCIÓN MANUAL POR SI ACASO moreLinkText: "más",
buttonText: { buttonText: { today: 'Hoy', month: 'Mes', week: 'Semana', day: 'Día', list: 'Agenda' },
today: 'Hoy',
month: 'Mes',
week: 'Semana',
day: 'Día',
list: 'Agenda'
},
headerToolbar: { headerToolbar: {
left: 'prev,next today', left: 'prev,next today',
center: 'title', center: 'title',
@@ -302,9 +307,7 @@
}, },
events: calendarEvents, events: calendarEvents,
// INYECCIÓN DE HTML Y COLOR DENTRO DEL EVENTO
eventContent: function(arg) { eventContent: function(arg) {
// Extraemos la hora para la tarjeta
let timeText = arg.timeText; let timeText = arg.timeText;
if (!timeText && arg.event.start) { if (!timeText && arg.event.start) {
timeText = arg.event.start.toLocaleTimeString('es-ES', {hour: '2-digit', minute:'2-digit'}); timeText = arg.event.start.toLocaleTimeString('es-ES', {hour: '2-digit', minute:'2-digit'});
@@ -313,29 +316,35 @@
const title = arg.event.title; const title = arg.event.title;
const op = arg.event.extendedProps.operatorName; const op = arg.event.extendedProps.operatorName;
const isBlock = arg.event.extendedProps.isBlock; const isBlock = arg.event.extendedProps.isBlock;
const bgColor = arg.event.backgroundColor; // Recuperamos el color oficial const conf = arg.event.extendedProps.statusConf;
const bgColor = arg.event.backgroundColor;
return { return {
html: ` html: `
<div class="flex flex-col h-full w-full justify-start p-1.5 text-white overflow-hidden font-sans rounded-lg shadow-sm" style="background-color: ${bgColor};"> <div class="flex flex-col h-full w-full justify-start p-1.5 text-white overflow-hidden font-sans rounded-lg shadow-sm border-l-4" style="background-color: ${bgColor}; border-color: ${arg.event.borderColor};">
<div class="flex justify-between items-center mb-1"> <div class="flex justify-between items-center mb-1">
<span class="text-[9px] font-black bg-black/20 px-1.5 py-0.5 rounded-md leading-none whitespace-nowrap">${timeText}</span> <span class="text-[9px] font-black bg-black/20 px-1.5 py-0.5 rounded-md leading-none whitespace-nowrap">${timeText}</span>
<p class="text-[9px] font-bold opacity-90 truncate ml-1"> <i data-lucide="${conf.icon}" class="w-3 h-3 text-white"></i>
<i data-lucide="${isBlock ? 'shield-alert' : 'user'}" class="w-2.5 h-2.5 inline mr-0.5"></i>${op}
</p>
</div> </div>
<p class="text-[10px] font-black leading-tight tracking-tight whitespace-normal line-clamp-2">${title}</p> <p class="text-[10px] font-black leading-tight tracking-tight whitespace-normal line-clamp-2">${title}</p>
<div class="flex items-center gap-1 opacity-90 mt-1">
<i data-lucide="${isBlock ? 'shield-alert' : 'user'}" class="w-2.5 h-2.5"></i>
<span class="text-[8px] font-bold uppercase truncate">${op}</span>
</div>
</div>` </div>`
}; };
}, },
eventDidMount: function() {
lucide.createIcons();
},
eventClick: function(info) { eventClick: function(info) {
openEventDetails(info.event.extendedProps); openEventDetails(info.event.extendedProps);
} }
}); });
calendarInstance.render(); calendarInstance.render();
setTimeout(() => lucide.createIcons(), 100);
} }
function applyFilters() { function applyFilters() {
@@ -345,13 +354,9 @@
const filteredEvents = calendarEvents.filter(ev => { const filteredEvents = calendarEvents.filter(ev => {
const props = ev.extendedProps; const props = ev.extendedProps;
if (props.isBlock) return (opFilter === 'ALL' || props.operatorName === opFilter);
// Los bloqueos se muestran siempre si cumplen el filtro de operario const matchText = ev.title.toUpperCase().includes(textFilter) || String(props.originalData.service_ref).toUpperCase().includes(textFilter);
if (props.isBlock) {
return (opFilter === 'ALL' || props.operatorName === opFilter);
}
const matchText = ev.title.toUpperCase().includes(textFilter) || props.originalData.service_ref.toUpperCase().includes(textFilter);
const matchOp = opFilter === 'ALL' || props.operatorName === opFilter; const matchOp = opFilter === 'ALL' || props.operatorName === opFilter;
const matchComp = compFilter === 'ALL' || props.companyName === compFilter; const matchComp = compFilter === 'ALL' || props.companyName === compFilter;
@@ -360,6 +365,7 @@
calendarInstance.removeAllEventSources(); calendarInstance.removeAllEventSources();
calendarInstance.addEventSource(filteredEvents); calendarInstance.addEventSource(filteredEvents);
setTimeout(() => lucide.createIcons(), 50);
} }
function openEventDetails(props) { function openEventDetails(props) {
@@ -372,7 +378,6 @@
header.style.backgroundColor = conf.bg; header.style.backgroundColor = conf.bg;
document.getElementById('emStatusLabel').innerText = conf.label; document.getElementById('emStatusLabel').innerText = conf.label;
document.getElementById('emRef').innerText = isBlock ? 'BLOQUEO TÉCNICO' : `EXP. #${ref}`; document.getElementById('emRef').innerText = isBlock ? 'BLOQUEO TÉCNICO' : `EXP. #${ref}`;
document.getElementById('emOperator').innerText = props.operatorName; document.getElementById('emOperator').innerText = props.operatorName;
if (isBlock) { if (isBlock) {
@@ -385,25 +390,20 @@
} else { } else {
document.getElementById('emClientLabel').innerText = "Asegurado / Cliente"; document.getElementById('emClientLabel').innerText = "Asegurado / Cliente";
document.getElementById('emName').innerText = raw['Nombre Cliente'] || raw['CLIENTE'] || "Desconocido"; document.getElementById('emName').innerText = raw['Nombre Cliente'] || raw['CLIENTE'] || "Desconocido";
const phone = (raw['Teléfono'] || raw['TELEFONOS'] || raw['TELEFONO'] || "").match(/[6789]\d{8}/)?.[0] || "Sin Teléfono"; const phone = (raw['Teléfono'] || raw['TELEFONOS'] || raw['TELEFONO'] || "").match(/[6789]\d{8}/)?.[0] || "Sin Teléfono";
document.getElementById('emPhone').innerText = phone; document.getElementById('emPhone').innerText = phone;
document.getElementById('emPhoneLink').classList.remove('hidden'); document.getElementById('emPhoneLink').classList.remove('hidden');
document.getElementById('emPhoneLink').href = phone !== "Sin Teléfono" ? `tel:+34${phone}` : '#'; document.getElementById('emPhoneLink').href = phone !== "Sin Teléfono" ? `tel:+34${phone}` : '#';
document.getElementById('boxCompany').classList.remove('hidden'); document.getElementById('boxCompany').classList.remove('hidden');
document.getElementById('emCompany').innerText = props.companyName; document.getElementById('emCompany').innerText = props.companyName;
document.getElementById('boxAddress').classList.remove('hidden'); document.getElementById('boxAddress').classList.remove('hidden');
const addr = raw['Dirección'] || raw['DOMICILIO'] || ""; const addr = raw['Dirección'] || raw['DOMICILIO'] || "";
const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || ""; const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || "";
document.getElementById('emAddress').innerText = `${addr}, ${pop} (CP: ${raw['Código Postal'] || '---'})`; document.getElementById('emAddress').innerText = `${addr}, ${pop} (CP: ${raw['Código Postal'] || '---'})`;
document.getElementById('emDescLabel').innerText = "Descripción de la Avería"; document.getElementById('emDescLabel').innerText = "Descripción de la Avería";
} }
document.getElementById('emDesc').innerHTML = (raw['Descripción'] || raw['DESCRIPCION'] || "Sin información.").replace(/\n/g, '<br>'); document.getElementById('emDesc').innerHTML = (raw['Descripción'] || raw['DESCRIPCION'] || "Sin información.").replace(/\n/g, '<br>');
document.getElementById('eventModal').classList.remove('hidden'); document.getElementById('eventModal').classList.remove('hidden');
lucide.createIcons(); lucide.createIcons();
} }