Actualizar servicios.html

This commit is contained in:
2026-02-20 17:46:39 +00:00
parent 78dc631d78
commit 6256e286a8

View File

@@ -11,7 +11,7 @@
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.card-hover:hover { transform: translateY(-2px); transition: all 0.2s; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05); } .card-hover:hover { transform: translateY(-3px); transition: all 0.2s; box-shadow: 0 12px 25px -5px rgba(0, 0, 0, 0.1); border-color: #93c5fd; }
/* ANIMACIÓN DE TEMBLOR (SHAKE) PARA TARJETAS BLOQUEADAS */ /* ANIMACIÓN DE TEMBLOR (SHAKE) PARA TARJETAS BLOQUEADAS */
.shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; } .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
@@ -72,10 +72,10 @@
</div> </div>
</div> </div>
<div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4"> <div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4">
<div class="w-12 h-12 rounded-full bg-red-50 flex items-center justify-center text-red-500 shrink-0"><i data-lucide="alert-triangle" class="w-6 h-6"></i></div> <div class="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center text-purple-500 shrink-0"><i data-lucide="check-circle" class="w-6 h-6"></i></div>
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Incidencias</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Finalizados</p>
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-issues">0</h3> <h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-finished">0</h3>
</div> </div>
</div> </div>
</div> </div>
@@ -235,7 +235,7 @@
<div id="panelSinAsignar" class="bg-rose-50/40 p-6 rounded-[1.5rem] border border-rose-100 flex flex-col shadow-sm h-full hidden"> <div id="panelSinAsignar" class="bg-rose-50/40 p-6 rounded-[1.5rem] border border-rose-100 flex flex-col shadow-sm h-full hidden">
<div class="flex items-center gap-2 mb-6"> <div class="flex items-center gap-2 mb-6">
<div class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></div> <div class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></div>
<p class="text-[10px] font-black text-rose-600 uppercase tracking-widest">Pendiente de Asignar</p> <p class="text-[10px] font-black text-rose-600 uppercase tracking-widest">Opciones de Asignación</p>
</div> </div>
<div class="space-y-5 flex-1"> <div class="space-y-5 flex-1">
@@ -325,9 +325,6 @@
`; `;
systemStatuses.forEach(st => { systemStatuses.forEach(st => {
// Por defecto no mostramos los finales en la botonera principal para no saturar, salvo que haya muchos.
if(st.is_final) return;
const isActive = activeStatusFilter === String(st.id); const isActive = activeStatusFilter === String(st.id);
const colorData = colorDict[st.color] || colorDict['gray']; const colorData = colorDict[st.color] || colorDict['gray'];
@@ -375,44 +372,37 @@
} }
// ========================================== // ==========================================
// 🚀 CÁLCULO DE ESTADOS Y RENDERIZADO GRID // 🚀 LÓGICA INTELIGENTE DE ENRUTAMIENTO (ESTADOS)
// ========================================== // ==========================================
function getServiceStateInfo(s) { function getServiceStateInfo(s) {
const raw = s.raw_data || {}; const raw = s.raw_data || {};
const dbStat = raw.status_operativo; const dbStat = raw.status_operativo;
// 1. Si está en la bolsa buscando (Prioridad máxima visual) // 1. Bolsa de Trabajo (Máxima prioridad visual)
if (!s.assigned_name && (s.automation_status === 'in_progress' || s.automation_status === 'failed')) { if (!s.assigned_name && (s.automation_status === 'in_progress' || s.automation_status === 'failed')) {
return { id: 'bolsa', name: s.automation_status === 'in_progress' ? 'Buscando Operario' : 'Fallo en Bolsa', color: s.automation_status === 'in_progress' ? 'amber' : 'red', isBlocked: true }; return { id: 'bolsa', name: s.automation_status === 'in_progress' ? 'Buscando Operario' : 'Fallo en Bolsa', color: s.automation_status === 'in_progress' ? 'amber' : 'red', isBlocked: true, is_final: false };
} }
// 2. Fallbacks históricos para compatibilidad // 2. Si viene limpio del scraper sin estado -> Pendiente de Asignar
if (!s.assigned_name || dbStat === 'sin_asignar') { if (!dbStat || dbStat === 'sin_asignar') {
const found = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0]; const found = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0];
return { id: found?.id, name: found?.name || 'Sin Asignar', color: found?.color || 'gray', isBlocked: false }; return { ...found, isBlocked: false };
} }
if (dbStat === 'asignado_operario') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('asignado')) || systemStatuses[1], isBlocked: false };
if (dbStat === 'citado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('citado')) || systemStatuses[3], isBlocked: false };
if (dbStat === 'de_camino') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('camino')) || systemStatuses[4], isBlocked: false };
if (dbStat === 'trabajando') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('trabajando')) || systemStatuses[5], isBlocked: false };
if (dbStat === 'incidencia') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('incidencia')) || systemStatuses[6], isBlocked: false };
if (dbStat === 'terminado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('terminado')) || systemStatuses[7], isBlocked: false };
// 3. Match directo por ID Numérico (El nuevo estándar) // 3. Match directo por ID Numérico (El nuevo estándar)
const foundObj = systemStatuses.find(st => String(st.id) === String(dbStat)); const foundObj = systemStatuses.find(st => String(st.id) === String(dbStat));
if (foundObj) return { ...foundObj, isBlocked: false }; if (foundObj) return { ...foundObj, isBlocked: false };
// 4. Último recurso inferido // 4. Fallbacks históricos de texto (Para que los servicios viejos no se rompan)
if (s.assigned_name && (!raw.scheduled_date || raw.scheduled_date === "")) { if (dbStat === 'asignado_operario') return { ...systemStatuses.find(st => st.name.toLowerCase() === 'asignado'), isBlocked: false };
const f = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de cita') || st.name.toLowerCase().includes('falta fecha')) || systemStatuses[2]; if (dbStat === 'citado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('citado')), isBlocked: false };
return { ...f, isBlocked: false }; if (dbStat === 'de_camino') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('camino')), isBlocked: false };
} if (dbStat === 'trabajando') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('trabajando')), isBlocked: false };
if (s.assigned_name && raw.scheduled_date) { if (dbStat === 'incidencia') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('incidencia')), isBlocked: false };
const f = systemStatuses.find(st => st.name.toLowerCase().includes('citado')) || systemStatuses[3]; if (dbStat === 'terminado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('terminado') || st.name.toLowerCase().includes('finalizado')), isBlocked: false };
return { ...f, isBlocked: false };
}
return { id: 'unknown', name: 'Desconocido', color: 'gray', isBlocked: false }; // 5. Fallback final
return { id: 'unknown', name: 'Desconocido', color: 'gray', isBlocked: false, is_final: false };
} }
function renderLists() { function renderLists() {
@@ -421,11 +411,10 @@
const searchTerm = document.getElementById('searchFilter').value.toLowerCase(); const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
const selectedOp = document.getElementById('opFilter').value; const selectedOp = document.getElementById('opFilter').value;
// KPIs
let kpiUnassigned = 0; let kpiUnassigned = 0;
let kpiScheduled = 0; let kpiScheduled = 0;
let kpiActive = 0; let kpiActive = 0;
let kpiIssues = 0; let kpiFinished = 0;
const filteredData = localData.filter(s => { const filteredData = localData.filter(s => {
const raw = s.raw_data || {}; const raw = s.raw_data || {};
@@ -437,22 +426,34 @@
const ref = (s.service_ref || "").toLowerCase(); const ref = (s.service_ref || "").toLowerCase();
const assigned = s.assigned_name || ""; const assigned = s.assigned_name || "";
// Calculamos el estado real // Calculamos el estado real y lo inyectamos
const stateInfo = getServiceStateInfo(s); const stateInfo = getServiceStateInfo(s);
s._stateInfo = stateInfo; // Lo guardamos para pintar la tarjeta s._stateInfo = stateInfo;
// Lógica de KPIs // Lógica de KPIs de suma agrupada
const stName = stateInfo.name.toLowerCase(); const stName = stateInfo.name.toLowerCase();
if (stName.includes('asignar') || stateInfo.isBlocked) kpiUnassigned++; if (stateInfo.id === 'bolsa' || stName.includes('pendiente de asignar') || stName.includes('desasignado')) {
else if (stName.includes('citado')) kpiScheduled++; kpiUnassigned++;
else if (stName.includes('camino') || stName.includes('trabajando')) kpiActive++; } else if (stateInfo.is_final || stName.includes('terminado') || stName.includes('anulado') || stName.includes('finalizado')) {
else if (stName.includes('incidencia') || stName.includes('pausa')) kpiIssues++; kpiFinished++;
} else if (stName === 'asignado' || stName.includes('pendiente de cita') || stName.includes('citado')) {
kpiScheduled++;
} else {
kpiActive++; // De Camino, Trabajando, Incidencia y los personalizados
}
// Aplicar Filtros Visuales
const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm) || addr.includes(searchTerm) || pop.includes(searchTerm) || phone.includes(searchTerm) || comp.includes(searchTerm); const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm) || addr.includes(searchTerm) || pop.includes(searchTerm) || phone.includes(searchTerm) || comp.includes(searchTerm);
const matchesOp = selectedOp === "ALL" || assigned === selectedOp; const matchesOp = selectedOp === "ALL" || assigned === selectedOp;
// Filtro por la botonera de chips let matchesStatus = false;
const matchesStatus = activeStatusFilter === "ALL" || String(stateInfo.id) === activeStatusFilter; if (activeStatusFilter === "ALL") {
// Si no hay búsqueda activa, ocultamos los Finalizados por limpieza visual
if (stateInfo.is_final && searchTerm === "") matchesStatus = false;
else matchesStatus = true;
} else {
matchesStatus = String(stateInfo.id) === activeStatusFilter;
}
return matchesSearch && matchesOp && matchesStatus; return matchesSearch && matchesOp && matchesStatus;
}); });
@@ -461,13 +462,13 @@
document.getElementById('kpi-unassigned').innerText = kpiUnassigned; document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
document.getElementById('kpi-scheduled').innerText = kpiScheduled; document.getElementById('kpi-scheduled').innerText = kpiScheduled;
document.getElementById('kpi-active').innerText = kpiActive; document.getElementById('kpi-active').innerText = kpiActive;
document.getElementById('kpi-issues').innerText = kpiIssues; document.getElementById('kpi-finished').innerText = kpiFinished;
const grid = document.getElementById('servicesGrid'); const grid = document.getElementById('servicesGrid');
grid.innerHTML = filteredData.length > 0 grid.innerHTML = filteredData.length > 0
? filteredData.map(s => buildGridCard(s)).join('') ? filteredData.map(s => buildGridCard(s)).join('')
: `<div class="col-span-full py-20 text-center bg-white rounded-[2rem] border-2 border-dashed border-slate-200"> : `<div class="col-span-full py-20 text-center bg-white rounded-[2rem] border-2 border-dashed border-slate-200">
<i data-lucide="inbox" class="w-12 h-12 text-slate-300 mx-auto mb-3"></i> <i data-lucide="layout-grid" class="w-12 h-12 text-slate-300 mx-auto mb-3"></i>
<p class="text-slate-400 font-bold uppercase tracking-widest text-sm">No hay servicios que coincidan con los filtros</p> <p class="text-slate-400 font-bold uppercase tracking-widest text-sm">No hay servicios que coincidan con los filtros</p>
</div>`; </div>`;
@@ -492,7 +493,7 @@
if(raw.scheduled_date) iconEstado = 'calendar'; if(raw.scheduled_date) iconEstado = 'calendar';
if(stateInfo.name.toLowerCase().includes('camino')) iconEstado = 'car'; if(stateInfo.name.toLowerCase().includes('camino')) iconEstado = 'car';
if(stateInfo.name.toLowerCase().includes('reparaci') || stateInfo.name.toLowerCase().includes('trabaja')) iconEstado = 'wrench'; if(stateInfo.name.toLowerCase().includes('reparaci') || stateInfo.name.toLowerCase().includes('trabaja')) iconEstado = 'wrench';
if(stateInfo.name.toLowerCase().includes('incidencia')) iconEstado = 'alert-triangle'; if(stateInfo.name.toLowerCase().includes('incidencia') || stateInfo.name.toLowerCase().includes('pausado')) iconEstado = 'alert-triangle';
const isBlocked = stateInfo.isBlocked; const isBlocked = stateInfo.isBlocked;
const clickAction = isBlocked ? `shakeCard(this, '${s.automation_status}'); event.stopPropagation();` : `openDetail(${s.id})`; const clickAction = isBlocked ? `shakeCard(this, '${s.automation_status}'); event.stopPropagation();` : `openDetail(${s.id})`;
@@ -597,7 +598,8 @@
const stateInfo = s._stateInfo; const stateInfo = s._stateInfo;
if (s.assigned_name && stateInfo.id !== 'bolsa' && !stateInfo.name.toLowerCase().includes('asignar')) { // Mostrar u ocultar paneles de asignación dependiendo del estado
if (s.assigned_name && stateInfo.id !== 'bolsa' && !stateInfo.name.toLowerCase().includes('asignar') && !stateInfo.name.toLowerCase().includes('desasignado')) {
document.getElementById('panelAsignado').classList.remove('hidden'); document.getElementById('panelAsignado').classList.remove('hidden');
document.getElementById('panelSinAsignar').classList.add('hidden'); document.getElementById('panelSinAsignar').classList.add('hidden');
@@ -605,7 +607,6 @@
document.getElementById('dateInput').value = raw.scheduled_date || ""; document.getElementById('dateInput').value = raw.scheduled_date || "";
document.getElementById('timeInput').value = raw.scheduled_time || ""; document.getElementById('timeInput').value = raw.scheduled_time || "";
// Mapeo del estado calculado al selector
document.getElementById('detStatusMap').value = stateInfo.id; document.getElementById('detStatusMap').value = stateInfo.id;
} else { } else {
document.getElementById('panelAsignado').classList.add('hidden'); document.getElementById('panelAsignado').classList.add('hidden');
@@ -633,7 +634,8 @@
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap)); const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
if (selectedSt && !selectedSt.is_final && !date && !selectedSt.name.toLowerCase().includes('pausa')) { // Avisar si guarda sin fecha en un estado que debería tenerla
if (selectedSt && !selectedSt.is_final && !date && !selectedSt.name.toLowerCase().includes('pausa') && !selectedSt.name.toLowerCase().includes('asignar')) {
if(!confirm("No has asignado Fecha para este estado. ¿Deseas continuar?")) return; if(!confirm("No has asignado Fecha para este estado. ¿Deseas continuar?")) return;
} }