diff --git a/calendario.html b/calendario.html
index ea9f593..3fe8162 100644
--- a/calendario.html
+++ b/calendario.html
@@ -141,7 +141,7 @@
-
-
+
-
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();