Actualizar servicios.html

This commit is contained in:
2026-02-22 11:55:06 +00:00
parent 2da3b8054e
commit 7d2c6feda2

View File

@@ -65,17 +65,17 @@
</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-amber-50 flex items-center justify-center text-amber-500 shrink-0"><i data-lucide="wrench" class="w-6 h-6"></i></div> <div class="w-12 h-12 rounded-full bg-amber-50 flex items-center justify-center text-amber-500 shrink-0"><i data-lucide="clock" class="w-6 h-6"></i></div>
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">En Curso</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Espera Cliente</p>
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-active">0</h3> <h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-waiting">0</h3>
</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-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 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> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Finalizados</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Incidencia</p>
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-finished">0</h3> <h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-incident">0</h3>
</div> </div>
</div> </div>
</div> </div>
@@ -86,12 +86,16 @@
<i data-lucide="search" class="w-4 h-4 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i> <i data-lucide="search" class="w-4 h-4 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input type="text" id="searchFilter" oninput="renderLists()" placeholder="Buscar por cliente, REF, población, compañía, teléfono..." class="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-blue-500 outline-none transition-all"> <input type="text" id="searchFilter" oninput="renderLists()" placeholder="Buscar por cliente, REF, población, compañía, teléfono..." class="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div> </div>
<div class="relative w-full md:w-64"> <div class="relative w-full md:w-48">
<select id="opFilter" onchange="renderLists()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 uppercase tracking-widest text-slate-600 appearance-none pr-10 cursor-pointer"> <select id="opFilter" onchange="renderLists()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 uppercase tracking-widest text-slate-600 appearance-none pr-10 cursor-pointer">
<option value="ALL">TODOS LOS OPERARIOS</option> <option value="ALL">OPERARIOS</option>
</select> </select>
<i data-lucide="chevron-down" class="w-4 h-4 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i> <i data-lucide="chevron-down" class="w-4 h-4 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
</div> </div>
<div class="relative w-full md:w-56">
<input type="week" id="weekFilter" onchange="renderLists()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-slate-600 cursor-pointer" title="Filtrar por semana">
</div>
<button onclick="document.getElementById('weekFilter').value = ''; renderLists();" class="text-xs font-bold text-slate-400 hover:text-red-500 px-2 py-3">Limpiar Semana</button>
</div> </div>
<div class="flex flex-wrap gap-2 items-center border-t border-slate-100 pt-3" id="statusPills"> <div class="flex flex-wrap gap-2 items-center border-t border-slate-100 pt-3" id="statusPills">
@@ -314,6 +318,23 @@
setInterval(refreshPanel, 20000); setInterval(refreshPanel, 20000);
}); });
// Helpers de Fechas para el filtro de semanas
function getWeekNumber(d) {
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
var weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
return { year: d.getUTCFullYear(), week: weekNo };
}
function isDateInWeekString(dateStr, weekStr) {
if (!dateStr || !weekStr) return false;
const targetDate = new Date(dateStr);
const { year, week } = getWeekNumber(targetDate);
const targetWeekStr = `${year}-W${String(week).padStart(2, '0')}`;
return targetWeekStr === weekStr;
}
// 1. CARGAMOS LOS ESTADOS DEL SISTEMA // 1. CARGAMOS LOS ESTADOS DEL SISTEMA
async function loadStatuses() { async function loadStatuses() {
try { try {
@@ -371,7 +392,9 @@
}); });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
localData = data.services; // FILTRAR BLOQUEOS PARA QUE NO ENSUCIEN EL PANEL NI LAS ESTADÍSTICAS
localData = data.services.filter(s => s.provider !== 'SYSTEM_BLOCK');
updateOperatorFilter(); updateOperatorFilter();
renderLists(); renderLists();
} }
@@ -389,43 +412,33 @@
} }
// ========================================== // ==========================================
// 🚀 LÓGICA INTELIGENTE DE ENRUTAMIENTO (ESTADOS) - VERSIÓN OPCIÓN 2 // 🚀 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; // Aquí viene el ID del estado o texto antiguo const dbStat = raw.status_operativo;
// 1. Prioridad Máxima: Bolsa de Trabajo / Robot WhatsApp
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: false, is_final: false }; return { id: 'bolsa', name: s.automation_status === 'in_progress' ? 'Buscando Operario' : 'Fallo en Bolsa', color: s.automation_status === 'in_progress' ? 'amber' : 'red', isBlocked: false, is_final: false };
} }
// 2. Si viene limpio del scraper sin estado -> Pendiente de Asignar
if (!s.assigned_name && (!dbStat || dbStat === 'sin_asignar')) { if (!s.assigned_name && (!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 { ...found, isBlocked: false }; return { ...found, isBlocked: false };
} }
// 3. Match Directo por ID (Si ya se guardó con el nuevo sistema de IDs numéricos)
const foundById = systemStatuses.find(st => String(st.id) === String(dbStat)); const foundById = systemStatuses.find(st => String(st.id) === String(dbStat));
if (foundById) return { ...foundById, isBlocked: false }; if (foundById) return { ...foundById, isBlocked: false };
// 4. Lógica de "Asignado" vs "Esperando Cliente" (Si tiene operario pero NO fecha)
if (s.assigned_name && (!raw.scheduled_date || raw.scheduled_date === "")) { if (s.assigned_name && (!raw.scheduled_date || raw.scheduled_date === "")) {
const asignado = systemStatuses.find(st => st.name.toLowerCase() === 'asignado'); const asignado = systemStatuses.find(st => st.name.toLowerCase() === 'asignado');
const esperando = systemStatuses.find(st => st.name.toLowerCase().includes('esperando')); const esperando = systemStatuses.find(st => st.name.toLowerCase().includes('esperando'));
// Si en BD es el texto antiguo 'esperando_cliente'
if (dbStat === 'esperando_cliente' && esperando) return { ...esperando, isBlocked: false }; if (dbStat === 'esperando_cliente' && esperando) return { ...esperando, isBlocked: false };
// Si es el texto antiguo 'asignado_operario' o no tiene estado definido pero sí tiene operario
if ((dbStat === 'asignado_operario' || !dbStat) && asignado) return { ...asignado, isBlocked: false }; if ((dbStat === 'asignado_operario' || !dbStat) && asignado) return { ...asignado, isBlocked: false };
// Fallback por defecto si no encaja en lo anterior
if (asignado) return { ...asignado, isBlocked: false }; if (asignado) return { ...asignado, isBlocked: false };
} }
// 5. Fallbacks históricos de texto (Para que los servicios antiguos no se rompan)
const stLower = String(dbStat).toLowerCase(); const stLower = String(dbStat).toLowerCase();
if (stLower === 'citado' || (s.assigned_name && raw.scheduled_date && !dbStat)) { if (stLower === 'citado' || (s.assigned_name && raw.scheduled_date && !dbStat)) {
const citado = systemStatuses.find(st => st.name.toLowerCase().includes('citado')); const citado = systemStatuses.find(st => st.name.toLowerCase().includes('citado'));
@@ -437,7 +450,6 @@
if (stLower === 'desasignado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('desasignado')), isBlocked: false }; if (stLower === 'desasignado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('desasignado')), isBlocked: false };
if (stLower === 'terminado' || stLower === 'finalizado') return { ...systemStatuses.find(st => st.is_final), isBlocked: false }; if (stLower === 'terminado' || stLower === 'finalizado') return { ...systemStatuses.find(st => st.is_final), isBlocked: false };
// Fallback de seguridad
return { id: 'unknown', name: 'Desconocido', color: 'gray', isBlocked: false, is_final: false }; return { id: 'unknown', name: 'Desconocido', color: 'gray', isBlocked: false, is_final: false };
} }
@@ -446,11 +458,12 @@
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;
const weekValue = document.getElementById('weekFilter').value; // Ej: "2023-W12"
let kpiUnassigned = 0; let kpiUnassigned = 0;
let kpiScheduled = 0; let kpiScheduled = 0;
let kpiActive = 0; let kpiWaiting = 0;
let kpiFinished = 0; let kpiIncident = 0;
const filteredData = localData.filter(s => { const filteredData = localData.filter(s => {
const raw = s.raw_data || {}; const raw = s.raw_data || {};
@@ -461,44 +474,59 @@
const comp = (raw["Compañía"] || raw["COMPAÑIA"] || raw["Procedencia"] || "").toLowerCase(); const comp = (raw["Compañía"] || raw["COMPAÑIA"] || raw["Procedencia"] || "").toLowerCase();
const ref = (s.service_ref || "").toLowerCase(); const ref = (s.service_ref || "").toLowerCase();
const assigned = s.assigned_name || ""; const assigned = s.assigned_name || "";
const dateRaw = raw.scheduled_date || "";
// Calculamos el estado real y lo inyectamos
const stateInfo = getServiceStateInfo(s); const stateInfo = getServiceStateInfo(s);
s._stateInfo = stateInfo; s._stateInfo = stateInfo;
// Lógica de KPIs de suma agrupada // --- NUEVO REPARTO DE KPIs ---
const stName = stateInfo.name.toLowerCase(); const stName = stateInfo.name.toLowerCase();
// 1. SIN ASIGNAR
if (stateInfo.id === 'bolsa' || stName.includes('pendiente de asignar') || stName.includes('desasignado')) { if (stateInfo.id === 'bolsa' || stName.includes('pendiente de asignar') || stName.includes('desasignado')) {
kpiUnassigned++; kpiUnassigned++;
} else if (stateInfo.is_final || stName.includes('terminado') || stName.includes('anulado') || stName.includes('finalizado')) { }
kpiFinished++; // 2. INCIDENCIA (Roba prioridad a Terminados en el dashboard)
} else if (stName === 'asignado' || stName.includes('esperando') || stName.includes('pendiente de cita') || stName.includes('citado')) { else if (stName.includes('incidencia') || stName.includes('pausa')) {
kpiIncident++;
}
// 3. AGENDADOS (Debe tener fecha)
else if (dateRaw !== "") {
kpiScheduled++; kpiScheduled++;
} else { }
kpiActive++; // De Camino, Trabajando, Incidencia y los personalizados // 4. ESPERA CLIENTE / EN CURSO (Tiene operario pero no fecha)
else if (!stateInfo.is_final && !stName.includes('terminado')) {
kpiWaiting++;
} }
// Aplicar Filtros Visuales // --- FILTROS VISUALES (Buscador, Operario, Semana, Estado) ---
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;
let matchesWeek = true;
if (weekValue !== "") {
// Si hemos filtrado por semana, mostramos los de esa semana O los que no tengan fecha para no perderlos de vista
if (dateRaw) {
matchesWeek = isDateInWeekString(dateRaw, weekValue);
}
}
let matchesStatus = false; let matchesStatus = false;
if (activeStatusFilter === "ALL") { if (activeStatusFilter === "ALL") {
// Ocultamos los Finalizados por defecto en la vista general para limpiar la pantalla
if (stateInfo.is_final && searchTerm === "") matchesStatus = false; if (stateInfo.is_final && searchTerm === "") matchesStatus = false;
else matchesStatus = true; else matchesStatus = true;
} else { } else {
matchesStatus = String(stateInfo.id) === activeStatusFilter; matchesStatus = String(stateInfo.id) === activeStatusFilter;
} }
return matchesSearch && matchesOp && matchesStatus; return matchesSearch && matchesOp && matchesWeek && matchesStatus;
}); });
// Actualizamos Dashboards Superiores // Actualizamos Dashboards Superiores
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-waiting').innerText = kpiWaiting;
document.getElementById('kpi-finished').innerText = kpiFinished; document.getElementById('kpi-incident').innerText = kpiIncident;
const grid = document.getElementById('servicesGrid'); const grid = document.getElementById('servicesGrid');
grid.innerHTML = filteredData.length > 0 grid.innerHTML = filteredData.length > 0
@@ -632,7 +660,6 @@
const stateInfo = s._stateInfo; const stateInfo = s._stateInfo;
// --- LÓGICA DE VISIBILIDAD DE PANELES MODIFICADA ---
if (s.automation_status === 'in_progress') { if (s.automation_status === 'in_progress') {
document.getElementById('panelEnBolsa').classList.remove('hidden'); document.getElementById('panelEnBolsa').classList.remove('hidden');
document.getElementById('panelAsignado').classList.add('hidden'); document.getElementById('panelAsignado').classList.add('hidden');
@@ -692,7 +719,6 @@
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap)); const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
// 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 (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;
} }
@@ -751,14 +777,12 @@
const name = select.options[select.selectedIndex].text; const name = select.options[select.selectedIndex].text;
const estadoAsignado = systemStatuses.find(st => st.name.toLowerCase() === 'asignado') || systemStatuses[1]; const estadoAsignado = systemStatuses.find(st => st.name.toLowerCase() === 'asignado') || systemStatuses[1];
// 1. PRIMERA LLAMADA: Detenemos la bolsa de trabajo automática
await fetch(`${API_URL}/providers/scraped/${id}`, { await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT', method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ automation_status: 'completed' }) body: JSON.stringify({ automation_status: 'completed' })
}); });
// 2. SEGUNDA LLAMADA: Guardamos el operario y el estado "Asignado"
await fetch(`${API_URL}/providers/scraped/${id}`, { await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT', method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },