Actualizar calendario.html
This commit is contained in:
115
calendario.html
115
calendario.html
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
<div class="p-8 space-y-6 bg-slate-50 overflow-y-auto max-h-[70vh] no-scrollbar">
|
<div class="p-8 space-y-6 bg-slate-50 overflow-y-auto max-h-[70vh] no-scrollbar">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1">Asegurado / Cliente</p>
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1" id="emClientLabel">Asegurado / Cliente</p>
|
||||||
<p class="font-black text-slate-800 text-xl uppercase leading-none" id="emName"></p>
|
<p class="font-black text-slate-800 text-xl uppercase leading-none" id="emName"></p>
|
||||||
<a href="#" id="emPhoneLink" class="inline-flex items-center gap-1.5 mt-2 bg-slate-200/50 hover:bg-emerald-100 text-emerald-700 px-3 py-1.5 rounded-lg text-xs font-black transition-all">
|
<a href="#" id="emPhoneLink" class="inline-flex items-center gap-1.5 mt-2 bg-slate-200/50 hover:bg-emerald-100 text-emerald-700 px-3 py-1.5 rounded-lg text-xs font-black transition-all">
|
||||||
<i data-lucide="phone" class="w-3.5 h-3.5"></i> <span id="emPhone"></span>
|
<i data-lucide="phone" class="w-3.5 h-3.5"></i> <span id="emPhone"></span>
|
||||||
@@ -153,19 +153,19 @@
|
|||||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="hard-hat" class="w-3 h-3 inline mr-1"></i>Operario</p>
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="hard-hat" class="w-3 h-3 inline mr-1"></i>Operario</p>
|
||||||
<p class="font-bold text-slate-700 text-xs uppercase" id="emOperator"></p>
|
<p class="font-bold text-slate-700 text-xs uppercase" id="emOperator"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm">
|
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm" id="boxCompany">
|
||||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="building-2" class="w-3 h-3 inline mr-1"></i>Compañía</p>
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="building-2" class="w-3 h-3 inline mr-1"></i>Compañía</p>
|
||||||
<p class="font-bold text-slate-700 text-xs uppercase" id="emCompany"></p>
|
<p class="font-bold text-slate-700 text-xs uppercase" id="emCompany"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm">
|
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm" id="boxAddress">
|
||||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="map-pin" class="w-3 h-3 inline mr-1"></i>Dirección Exacta</p>
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="map-pin" class="w-3 h-3 inline mr-1"></i>Dirección Exacta</p>
|
||||||
<p class="font-bold text-slate-600 text-sm uppercase leading-snug" id="emAddress"></p>
|
<p class="font-bold text-slate-600 text-sm uppercase leading-snug" id="emAddress"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-2 ml-2">Descripción de la Avería</p>
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-2 ml-2" id="emDescLabel">Descripción de la Avería</p>
|
||||||
<div class="bg-amber-50 border border-amber-100 p-4 rounded-2xl text-xs font-medium text-slate-700 leading-relaxed max-h-32 overflow-y-auto no-scrollbar" id="emDesc"></div>
|
<div class="bg-amber-50 border border-amber-100 p-4 rounded-2xl text-xs font-medium text-slate-700 leading-relaxed max-h-32 overflow-y-auto no-scrollbar" id="emDesc"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,13 +178,14 @@
|
|||||||
let calendarEvents = [];
|
let calendarEvents = [];
|
||||||
let calendarInstance = null;
|
let calendarInstance = null;
|
||||||
|
|
||||||
// Diccionario de colores según estado_operativo
|
// Diccionario de colores. AÑADIMOS EL BLOQUEO (Rojo vivo)
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
'citado': { bg: '#3b82f6', border: '#2563eb', label: 'Citado / Agendado' }, // Azul
|
'citado': { bg: '#3b82f6', border: '#2563eb', label: 'Citado / Agendado' }, // Azul
|
||||||
'de_camino': { bg: '#8b5cf6', border: '#7c3aed', label: 'De Camino' }, // Morado
|
'de_camino': { bg: '#8b5cf6', border: '#7c3aed', label: 'De Camino' }, // Morado
|
||||||
'trabajando': { bg: '#f59e0b', border: '#d97706', label: 'Trabajando en lugar' }, // Naranja
|
'trabajando': { bg: '#f59e0b', border: '#d97706', label: 'Trabajando en lugar' }, // Naranja
|
||||||
'incidencia': { bg: '#ef4444', border: '#dc2626', label: 'Pausado / Incidencia' }, // Rojo
|
'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 () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
@@ -207,20 +208,19 @@
|
|||||||
|
|
||||||
async function loadServices() {
|
async function loadServices() {
|
||||||
try {
|
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 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) {
|
||||||
// 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 !== "");
|
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 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 += `<option value="${c}">${c}</option>`);
|
uniqueComps.forEach(c => compSelect.innerHTML += `<option value="${c}">${c}</option>`);
|
||||||
|
|
||||||
// 3. Formatear para FullCalendar y renderizar
|
|
||||||
processEventsAndRender();
|
processEventsAndRender();
|
||||||
}
|
}
|
||||||
} catch (e) { console.error("Error al cargar calendario", e); }
|
} catch (e) { console.error("Error al cargar calendario", e); }
|
||||||
@@ -229,30 +229,40 @@
|
|||||||
function processEventsAndRender() {
|
function processEventsAndRender() {
|
||||||
calendarEvents = rawServices.map(svc => {
|
calendarEvents = rawServices.map(svc => {
|
||||||
const r = svc.raw_data;
|
const r = svc.raw_data;
|
||||||
|
const isBlock = svc.provider === 'SYSTEM_BLOCK';
|
||||||
|
|
||||||
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 = `${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)
|
// Formato ISO: YYYY-MM-DDTHH:mm:00
|
||||||
const dateIso = r.scheduled_date; // Suele venir en YYYY-MM-DD del input type="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}`;
|
||||||
|
|
||||||
// Configurar color según estado
|
// Calcular la fecha/hora de fin sumando los minutos (si existen, por defecto 60 min)
|
||||||
const status = r.status_operativo || 'citado';
|
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
|
||||||
|
|
||||||
|
// Color según estado (o si es bloqueo)
|
||||||
|
const status = isBlock ? 'SYSTEM_BLOCK' : (r.status_operativo || 'citado');
|
||||||
const sConf = statusConfig[status] || statusConfig['citado'];
|
const sConf = statusConfig[status] || statusConfig['citado'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: svc.id,
|
id: svc.id,
|
||||||
title: title,
|
title: title,
|
||||||
start: startStr,
|
start: startStr,
|
||||||
|
end: endStr,
|
||||||
backgroundColor: sConf.bg,
|
backgroundColor: sConf.bg,
|
||||||
borderColor: sConf.border,
|
borderColor: sConf.border,
|
||||||
extendedProps: {
|
extendedProps: {
|
||||||
originalData: svc,
|
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",
|
operatorName: svc.assigned_name || "Sin Asignar",
|
||||||
statusConf: sConf
|
statusConf: sConf,
|
||||||
|
isBlock: isBlock
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -264,15 +274,15 @@
|
|||||||
const calendarEl = document.getElementById('calendar');
|
const calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
calendarInstance = new FullCalendar.Calendar(calendarEl, {
|
calendarInstance = new FullCalendar.Calendar(calendarEl, {
|
||||||
initialView: 'timeGridWeek', // Vista por defecto semanal con horas
|
initialView: 'timeGridWeek',
|
||||||
locale: 'es',
|
locale: 'es',
|
||||||
firstDay: 1, // La semana empieza en lunes
|
firstDay: 1,
|
||||||
slotMinTime: '08:00:00', // El calendario empieza a las 8 AM
|
slotMinTime: '08:00:00',
|
||||||
slotMaxTime: '22:00:00', // Termina a las 10 PM
|
slotMaxTime: '22:00:00',
|
||||||
expandRows: true,
|
expandRows: true,
|
||||||
allDaySlot: false,
|
allDaySlot: false,
|
||||||
nowIndicator: true,
|
nowIndicator: true,
|
||||||
dayMaxEvents: true, // "Ver más" si hay demasiados en vista mensual
|
dayMaxEvents: true,
|
||||||
buttonText: {
|
buttonText: {
|
||||||
today: 'Hoy',
|
today: 'Hoy',
|
||||||
month: 'Mes',
|
month: 'Mes',
|
||||||
@@ -287,32 +297,32 @@
|
|||||||
},
|
},
|
||||||
events: calendarEvents,
|
events: calendarEvents,
|
||||||
|
|
||||||
// Diseño HTML interno de cada cajita del calendario (VERSIÓN COMPACTA)
|
|
||||||
eventContent: function(arg) {
|
eventContent: function(arg) {
|
||||||
const timeText = arg.timeText;
|
const timeText = arg.timeText;
|
||||||
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;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `
|
html: `
|
||||||
<div class="flex flex-col h-full justify-start p-1 text-white overflow-hidden font-sans">
|
<div class="flex flex-col h-full justify-start p-1 text-white overflow-hidden font-sans">
|
||||||
<div class="flex justify-between items-center mb-0.5">
|
<div class="flex justify-between items-center mb-0.5">
|
||||||
<span class="text-[9px] font-black bg-black/10 px-1.5 py-0.5 rounded-md leading-none whitespace-nowrap">${timeText}</span>
|
<span class="text-[9px] font-black bg-black/10 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="user" class="w-2.5 h-2.5 inline mr-0.5"></i>${op}</p>
|
<p class="text-[9px] font-bold opacity-90 truncate ml-1">
|
||||||
|
<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 truncate">${title}</p>
|
<p class="text-[10px] font-black leading-tight tracking-tight truncate">${title}</p>
|
||||||
</div>`
|
</div>`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Al hacer clic en una cita
|
|
||||||
eventClick: function(info) {
|
eventClick: function(info) {
|
||||||
openEventDetails(info.event.extendedProps);
|
openEventDetails(info.event.extendedProps);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
calendarInstance.render();
|
calendarInstance.render();
|
||||||
// Recargar iconos Lucide después de renderizar
|
|
||||||
setTimeout(() => lucide.createIcons(), 100);
|
setTimeout(() => lucide.createIcons(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +333,14 @@
|
|||||||
|
|
||||||
const filteredEvents = calendarEvents.filter(ev => {
|
const filteredEvents = calendarEvents.filter(ev => {
|
||||||
const props = ev.extendedProps;
|
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 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;
|
||||||
@@ -330,7 +348,6 @@
|
|||||||
return matchText && matchOp && matchComp;
|
return matchText && matchOp && matchComp;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reemplazar eventos en el calendario de forma dinámica
|
|
||||||
calendarInstance.removeAllEventSources();
|
calendarInstance.removeAllEventSources();
|
||||||
calendarInstance.addEventSource(filteredEvents);
|
calendarInstance.addEventSource(filteredEvents);
|
||||||
}
|
}
|
||||||
@@ -339,27 +356,43 @@
|
|||||||
const raw = props.originalData.raw_data;
|
const raw = props.originalData.raw_data;
|
||||||
const ref = props.originalData.service_ref;
|
const ref = props.originalData.service_ref;
|
||||||
const conf = props.statusConf;
|
const conf = props.statusConf;
|
||||||
|
const isBlock = props.isBlock;
|
||||||
|
|
||||||
// Header dinámico de color
|
|
||||||
const header = document.getElementById('emHeader');
|
const header = document.getElementById('emHeader');
|
||||||
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 = `EXP. #${ref}`;
|
document.getElementById('emRef').innerText = isBlock ? 'BLOQUEO TÉCNICO' : `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('emOperator').innerText = props.operatorName;
|
document.getElementById('emOperator').innerText = props.operatorName;
|
||||||
document.getElementById('emCompany').innerText = props.companyName;
|
|
||||||
|
|
||||||
const addr = raw['Dirección'] || raw['DOMICILIO'] || "";
|
if (isBlock) {
|
||||||
const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || "";
|
document.getElementById('emClientLabel').innerText = "TIPO DE BLOQUEO";
|
||||||
document.getElementById('emAddress').innerText = `${addr}, ${pop} (CP: ${raw['Código Postal'] || '---'})`;
|
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";
|
||||||
|
|
||||||
document.getElementById('emDesc').innerHTML = (raw['Descripción'] || raw['DESCRIPCION'] || "No hay notas de la avería.").replace(/\n/g, '<br>');
|
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'] || "Sin información.").replace(/\n/g, '<br>');
|
||||||
|
|
||||||
document.getElementById('eventModal').classList.remove('hidden');
|
document.getElementById('eventModal').classList.remove('hidden');
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|||||||
Reference in New Issue
Block a user