diff --git a/servicios.html b/servicios.html
index 866d72a..d6e0a20 100644
--- a/servicios.html
+++ b/servicios.html
@@ -65,17 +65,17 @@
-
+
-
En Curso
-
0
+
Espera Cliente
+
0
-
+
-
Finalizados
-
0
+
Incidencia
+
0
@@ -86,12 +86,16 @@
-
+
+
+
+
+
@@ -314,6 +318,23 @@
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
async function loadStatuses() {
try {
@@ -371,7 +392,9 @@
});
const data = await res.json();
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();
renderLists();
}
@@ -388,44 +411,34 @@
if (html.includes(`value="${currentValue}"`)) opSelect.value = currentValue;
}
- // ==========================================
- // 🚀 LÓGICA INTELIGENTE DE ENRUTAMIENTO (ESTADOS) - VERSIÓN OPCIÓN 2
+ // ==========================================
+ // 🚀 LÓGICA INTELIGENTE DE ENRUTAMIENTO (ESTADOS)
// ==========================================
function getServiceStateInfo(s) {
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')) {
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')) {
const found = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0];
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));
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 === "")) {
const asignado = systemStatuses.find(st => st.name.toLowerCase() === 'asignado');
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 };
-
- // 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 };
-
- // Fallback por defecto si no encaja en lo anterior
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();
if (stLower === 'citado' || (s.assigned_name && raw.scheduled_date && !dbStat)) {
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 === '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 };
}
@@ -446,11 +458,12 @@
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
const selectedOp = document.getElementById('opFilter').value;
+ const weekValue = document.getElementById('weekFilter').value; // Ej: "2023-W12"
let kpiUnassigned = 0;
let kpiScheduled = 0;
- let kpiActive = 0;
- let kpiFinished = 0;
+ let kpiWaiting = 0;
+ let kpiIncident = 0;
const filteredData = localData.filter(s => {
const raw = s.raw_data || {};
@@ -461,44 +474,59 @@
const comp = (raw["Compañía"] || raw["COMPAÑIA"] || raw["Procedencia"] || "").toLowerCase();
const ref = (s.service_ref || "").toLowerCase();
const assigned = s.assigned_name || "";
+ const dateRaw = raw.scheduled_date || "";
- // Calculamos el estado real y lo inyectamos
const stateInfo = getServiceStateInfo(s);
s._stateInfo = stateInfo;
- // Lógica de KPIs de suma agrupada
+ // --- NUEVO REPARTO DE KPIs ---
const stName = stateInfo.name.toLowerCase();
+
+ // 1. SIN ASIGNAR
if (stateInfo.id === 'bolsa' || stName.includes('pendiente de asignar') || stName.includes('desasignado')) {
kpiUnassigned++;
- } else if (stateInfo.is_final || stName.includes('terminado') || stName.includes('anulado') || stName.includes('finalizado')) {
- kpiFinished++;
- } else if (stName === 'asignado' || stName.includes('esperando') || stName.includes('pendiente de cita') || stName.includes('citado')) {
+ }
+ // 2. INCIDENCIA (Roba prioridad a Terminados en el dashboard)
+ else if (stName.includes('incidencia') || stName.includes('pausa')) {
+ kpiIncident++;
+ }
+ // 3. AGENDADOS (Debe tener fecha)
+ else if (dateRaw !== "") {
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 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;
if (activeStatusFilter === "ALL") {
- // Ocultamos los Finalizados por defecto en la vista general para limpiar la pantalla
if (stateInfo.is_final && searchTerm === "") matchesStatus = false;
else matchesStatus = true;
} else {
matchesStatus = String(stateInfo.id) === activeStatusFilter;
}
- return matchesSearch && matchesOp && matchesStatus;
+ return matchesSearch && matchesOp && matchesWeek && matchesStatus;
});
// Actualizamos Dashboards Superiores
document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
document.getElementById('kpi-scheduled').innerText = kpiScheduled;
- document.getElementById('kpi-active').innerText = kpiActive;
- document.getElementById('kpi-finished').innerText = kpiFinished;
+ document.getElementById('kpi-waiting').innerText = kpiWaiting;
+ document.getElementById('kpi-incident').innerText = kpiIncident;
const grid = document.getElementById('servicesGrid');
grid.innerHTML = filteredData.length > 0
@@ -632,7 +660,6 @@
const stateInfo = s._stateInfo;
- // --- LÓGICA DE VISIBILIDAD DE PANELES MODIFICADA ---
if (s.automation_status === 'in_progress') {
document.getElementById('panelEnBolsa').classList.remove('hidden');
document.getElementById('panelAsignado').classList.add('hidden');
@@ -692,7 +719,6 @@
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(!confirm("No has asignado Fecha para este estado. ¿Deseas continuar?")) return;
}
@@ -751,14 +777,12 @@
const name = select.options[select.selectedIndex].text;
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}`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ automation_status: 'completed' })
});
- // 2. SEGUNDA LLAMADA: Guardamos el operario y el estado "Asignado"
await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },