Actualizar servicios.html
This commit is contained in:
104
servicios.html
104
servicios.html
@@ -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")}` },
|
||||||
|
|||||||
Reference in New Issue
Block a user