diff --git a/calendario.html b/calendario.html index ea9f593..3fe8162 100644 --- a/calendario.html +++ b/calendario.html @@ -141,7 +141,7 @@
-

Asegurado / Cliente

+

Asegurado / Cliente

@@ -153,19 +153,19 @@

Operario

-
+

Compañía

-
+

Dirección Exacta

-

Descripción de la Avería

+

Descripción de la Avería

@@ -178,13 +178,14 @@ let calendarEvents = []; let calendarInstance = null; - // Diccionario de colores según estado_operativo + // Diccionario de colores. AÑADIMOS EL BLOQUEO (Rojo vivo) const statusConfig = { 'citado': { bg: '#3b82f6', border: '#2563eb', label: 'Citado / Agendado' }, // Azul 'de_camino': { bg: '#8b5cf6', border: '#7c3aed', label: 'De Camino' }, // Morado 'trabajando': { bg: '#f59e0b', border: '#d97706', label: 'Trabajando en lugar' }, // Naranja 'incidencia': { bg: '#ef4444', border: '#dc2626', label: 'Pausado / Incidencia' }, // Rojo - 'terminado': { bg: '#10b981', border: '#059669', label: 'Trabajo Terminado' } // Verde + 'terminado': { bg: '#10b981', border: '#059669', label: 'Trabajo Terminado' }, // Verde + 'SYSTEM_BLOCK': { bg: '#f43f5e', border: '#e11d48', label: 'BLOQUEO DE AGENDA' } // Rosa fuerte }; document.addEventListener("DOMContentLoaded", async () => { @@ -207,20 +208,19 @@ async function loadServices() { try { - // Usamos la ruta /services/active que nos trae la info operativa (con assigned_name y estado_operativo) + // Traemos los servicios activos const res = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const data = await res.json(); if (data.ok) { - // 1. Filtrar solo los que están "CITADOS" (tienen fecha de visita guardada en raw_data) + // 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 !== ""); - // 2. Extraer compañías únicas para el filtro + // Extraemos compañías (ignorando los bloqueos) const compSelect = document.getElementById('compFilter'); - const uniqueComps = [...new Set(rawServices.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(); uniqueComps.forEach(c => compSelect.innerHTML += ``); - // 3. Formatear para FullCalendar y renderizar processEventsAndRender(); } } catch (e) { console.error("Error al cargar calendario", e); } @@ -229,30 +229,40 @@ function processEventsAndRender() { calendarEvents = rawServices.map(svc => { const r = svc.raw_data; + const isBlock = svc.provider === 'SYSTEM_BLOCK'; + const name = r['Nombre Cliente'] || r['CLIENTE'] || "Sin Nombre"; const pop = r['Población'] || r['POBLACION-PROVINCIA'] || ""; - const title = `${name} - ${pop}`; + const title = isBlock ? `BLOQUEADO: ${r['Descripción'] || 'Motivo no especificado'}` : `${name} - ${pop}`; - // Construir formato de fecha ISO 8601 (YYYY-MM-DDTHH:mm:00) - const dateIso = r.scheduled_date; // Suele venir en YYYY-MM-DD del input type="date" + // Formato ISO: YYYY-MM-DDTHH:mm:00 + const dateIso = r.scheduled_date; const timeIso = r.scheduled_time ? r.scheduled_time + ':00' : '09:00:00'; const startStr = `${dateIso}T${timeIso}`; + + // Calcular la fecha/hora de fin sumando los minutos (si existen, por defecto 60 min) + const startObj = new Date(startStr); + const duration = parseInt(r.duration_minutes || 60); + const endObj = new Date(startObj.getTime() + duration * 60000); + const endStr = endObj.toISOString().slice(0, 19); // YYYY-MM-DDTHH:mm:ss - // Configurar color según estado - const status = r.status_operativo || 'citado'; + // Color según estado (o si es bloqueo) + const status = isBlock ? 'SYSTEM_BLOCK' : (r.status_operativo || 'citado'); const sConf = statusConfig[status] || statusConfig['citado']; return { id: svc.id, title: title, start: startStr, + end: endStr, backgroundColor: sConf.bg, borderColor: sConf.border, extendedProps: { originalData: svc, - companyName: (r['Compañía'] || r['COMPAÑIA'] || "Particular").toUpperCase(), + companyName: isBlock ? 'SISTEMA' : (r['Compañía'] || r['COMPAÑIA'] || "Particular").toUpperCase(), operatorName: svc.assigned_name || "Sin Asignar", - statusConf: sConf + statusConf: sConf, + isBlock: isBlock } }; }); @@ -264,15 +274,15 @@ const calendarEl = document.getElementById('calendar'); calendarInstance = new FullCalendar.Calendar(calendarEl, { - initialView: 'timeGridWeek', // Vista por defecto semanal con horas + initialView: 'timeGridWeek', locale: 'es', - firstDay: 1, // La semana empieza en lunes - slotMinTime: '08:00:00', // El calendario empieza a las 8 AM - slotMaxTime: '22:00:00', // Termina a las 10 PM + firstDay: 1, + slotMinTime: '08:00:00', + slotMaxTime: '22:00:00', expandRows: true, allDaySlot: false, nowIndicator: true, - dayMaxEvents: true, // "Ver más" si hay demasiados en vista mensual + dayMaxEvents: true, buttonText: { today: 'Hoy', month: 'Mes', @@ -287,32 +297,32 @@ }, events: calendarEvents, - // Diseño HTML interno de cada cajita del calendario (VERSIÓN COMPACTA) eventContent: function(arg) { const timeText = arg.timeText; const title = arg.event.title; const op = arg.event.extendedProps.operatorName; + const isBlock = arg.event.extendedProps.isBlock; return { html: `
${timeText} -

${op}

+

+ ${op} +

${title}

` }; }, - // Al hacer clic en una cita eventClick: function(info) { openEventDetails(info.event.extendedProps); } }); calendarInstance.render(); - // Recargar iconos Lucide después de renderizar setTimeout(() => lucide.createIcons(), 100); } @@ -323,6 +333,14 @@ const filteredEvents = calendarEvents.filter(ev => { const props = ev.extendedProps; + + // Si es un bloqueo, lo mostramos siempre si cumple el filtro de operario, + // e ignoramos el texto/compañía para que el admin siempre vea los bloqueos de esa persona. + if (props.isBlock) { + return (opFilter === 'ALL' || props.operatorName === opFilter); + } + + // Filtrado normal const matchText = ev.title.toUpperCase().includes(textFilter) || props.originalData.service_ref.toUpperCase().includes(textFilter); const matchOp = opFilter === 'ALL' || props.operatorName === opFilter; const matchComp = compFilter === 'ALL' || props.companyName === compFilter; @@ -330,7 +348,6 @@ return matchText && matchOp && matchComp; }); - // Reemplazar eventos en el calendario de forma dinámica calendarInstance.removeAllEventSources(); calendarInstance.addEventSource(filteredEvents); } @@ -339,27 +356,43 @@ const raw = props.originalData.raw_data; const ref = props.originalData.service_ref; const conf = props.statusConf; + const isBlock = props.isBlock; - // Header dinámico de color const header = document.getElementById('emHeader'); header.style.backgroundColor = conf.bg; document.getElementById('emStatusLabel').innerText = conf.label; - document.getElementById('emRef').innerText = `EXP. #${ref}`; - - // Datos - 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"; - document.getElementById('emPhone').innerText = phone; - document.getElementById('emPhoneLink').href = phone !== "Sin Teléfono" ? `tel:+34${phone}` : '#'; + document.getElementById('emRef').innerText = isBlock ? 'BLOQUEO TÉCNICO' : `EXP. #${ref}`; document.getElementById('emOperator').innerText = props.operatorName; - document.getElementById('emCompany').innerText = props.companyName; - const addr = raw['Dirección'] || raw['DOMICILIO'] || ""; - const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || ""; - document.getElementById('emAddress').innerText = `${addr}, ${pop} (CP: ${raw['Código Postal'] || '---'})`; + if (isBlock) { + document.getElementById('emClientLabel').innerText = "TIPO DE BLOQUEO"; + document.getElementById('emName').innerText = raw.blocked_guild_name ? `SOLO GREMIO: ${raw.blocked_guild_name}` : "BLOQUEO TOTAL (NO CITAR)"; + document.getElementById('emPhoneLink').classList.add('hidden'); + document.getElementById('boxCompany').classList.add('hidden'); + document.getElementById('boxAddress').classList.add('hidden'); + document.getElementById('emDescLabel').innerText = "MOTIVO / NOTAS DEL BLOQUEO"; + } else { + document.getElementById('emClientLabel').innerText = "Asegurado / Cliente"; + 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"; + document.getElementById('emPhone').innerText = phone; + document.getElementById('emPhoneLink').classList.remove('hidden'); + document.getElementById('emPhoneLink').href = phone !== "Sin Teléfono" ? `tel:+34${phone}` : '#'; + + document.getElementById('boxCompany').classList.remove('hidden'); + document.getElementById('emCompany').innerText = props.companyName; + + document.getElementById('boxAddress').classList.remove('hidden'); + const addr = raw['Dirección'] || raw['DOMICILIO'] || ""; + const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || ""; + document.getElementById('emAddress').innerText = `${addr}, ${pop} (CP: ${raw['Código Postal'] || '---'})`; + + document.getElementById('emDescLabel').innerText = "Descripción de la Avería"; + } - document.getElementById('emDesc').innerHTML = (raw['Descripción'] || raw['DESCRIPCION'] || "No hay notas de la avería.").replace(/\n/g, '
'); + document.getElementById('emDesc').innerHTML = (raw['Descripción'] || raw['DESCRIPCION'] || "Sin información.").replace(/\n/g, '
'); document.getElementById('eventModal').classList.remove('hidden'); lucide.createIcons();