Actualizar panel.html
This commit is contained in:
238
panel.html
238
panel.html
@@ -27,20 +27,20 @@
|
||||
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-8">
|
||||
<div class="fade-in max-w-[1400px] mx-auto space-y-8 text-left">
|
||||
|
||||
<div class="flex justify-between items-end">
|
||||
<div class="flex justify-between items-end border-b border-gray-200 pb-6">
|
||||
<div>
|
||||
<h2 class="text-3xl font-black text-slate-800 tracking-tight flex items-center gap-3">
|
||||
<span class="bg-blue-600 p-2.5 rounded-2xl text-white shadow-lg shadow-blue-200"><i data-lucide="layout-dashboard" class="w-6 h-6"></i></span>
|
||||
DASHBOARD
|
||||
</h2>
|
||||
<p class="text-sm text-slate-500 mt-2 font-medium">Resumen del estado de todos los expedientes y citas de hoy.</p>
|
||||
<p class="text-sm text-slate-500 mt-2 font-medium">Resumen del estado de todos los expedientes y métricas de hoy.</p>
|
||||
</div>
|
||||
<div class="text-right hidden md:block">
|
||||
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest" id="todayDate">Cargando fecha...</p>
|
||||
<p class="text-xs font-bold text-blue-600 uppercase tracking-widest bg-blue-50 px-4 py-2 rounded-xl" id="todayDate">Cargando fecha...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-5 text-left">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-5 text-left">
|
||||
|
||||
<div class="stat-card bg-white p-5 rounded-[2rem] shadow-sm flex flex-col justify-between">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="bg-amber-50 text-amber-600 p-2 rounded-xl"><i data-lucide="zap" class="w-4 h-4"></i></div>
|
||||
</div>
|
||||
<h3 class="text-3xl font-black text-slate-800" id="kpiQueue">-</h3>
|
||||
<p class="text-[10px] text-slate-500 font-bold uppercase mt-2">Buscando Operario</p>
|
||||
<p class="text-[10px] text-slate-500 font-bold uppercase mt-2">Buscando Operario (WA)</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card bg-white p-5 rounded-[2rem] shadow-sm flex flex-col justify-between">
|
||||
@@ -80,26 +80,77 @@
|
||||
|
||||
<div class="stat-card bg-white p-5 rounded-[2rem] shadow-sm flex flex-col justify-between border-l-4 border-l-blue-500">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest">Citas de Hoy</p>
|
||||
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest">Citas Hoy</p>
|
||||
<div class="bg-blue-600 text-white p-2 rounded-xl shadow-md shadow-blue-200"><i data-lucide="calendar-check" class="w-4 h-4"></i></div>
|
||||
</div>
|
||||
<h3 class="text-3xl font-black text-slate-800" id="kpiToday">-</h3>
|
||||
<p class="text-[10px] text-slate-500 font-bold uppercase mt-2">Programadas para hoy</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card bg-red-50 p-5 rounded-[2rem] shadow-sm flex flex-col justify-between border border-red-100">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<p class="text-[10px] font-black text-red-600 uppercase tracking-widest">Urgencias</p>
|
||||
<div class="bg-red-600 text-white p-2 rounded-xl shadow-md shadow-red-200 animate-pulse"><i data-lucide="siren" class="w-4 h-4"></i></div>
|
||||
</div>
|
||||
<h3 class="text-3xl font-black text-red-600" id="kpiUrgent">-</h3>
|
||||
<p class="text-[10px] text-red-500 font-bold uppercase mt-2">Atención prioritaria</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||
|
||||
<div class="xl:col-span-2 space-y-8">
|
||||
|
||||
<div class="bg-white rounded-[2.5rem] shadow-sm border border-slate-100 p-8 text-left">
|
||||
<div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-50">
|
||||
<i data-lucide="calendar" class="text-blue-500 w-5 h-5"></i>
|
||||
<div class="bg-blue-50 p-2 rounded-xl text-blue-600"><i data-lucide="calendar" class="w-5 h-5"></i></div>
|
||||
<h3 class="font-black text-lg text-slate-800 uppercase tracking-tight">Agenda de Visitas para Hoy</h3>
|
||||
</div>
|
||||
|
||||
<div id="todaySchedule" class="space-y-3">
|
||||
<p class="text-slate-400 text-sm font-medium animate-pulse">Cargando agenda...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-[2.5rem] shadow-sm border border-red-100 p-8 text-left relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 p-8 opacity-5 pointer-events-none">
|
||||
<i data-lucide="siren" class="w-40 h-40 text-red-600"></i>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-50 relative z-10">
|
||||
<div class="bg-red-50 p-2 rounded-xl text-red-600"><i data-lucide="alert-triangle" class="w-5 h-5"></i></div>
|
||||
<h3 class="font-black text-lg text-slate-800 uppercase tracking-tight">Avisos Urgentes Activos</h3>
|
||||
</div>
|
||||
<div id="urgentSchedule" class="space-y-3 relative z-10">
|
||||
<p class="text-slate-400 text-sm font-medium animate-pulse">Buscando urgencias...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
|
||||
<div class="bg-white rounded-[2.5rem] shadow-sm border border-slate-100 p-6 text-left">
|
||||
<div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-50">
|
||||
<div class="bg-indigo-50 p-2 rounded-xl text-indigo-600"><i data-lucide="pie-chart" class="w-5 h-5"></i></div>
|
||||
<h3 class="font-black text-sm text-slate-800 uppercase tracking-tight">Volumen por Aseguradora</h3>
|
||||
</div>
|
||||
<div id="companyDistribution" class="space-y-4">
|
||||
<p class="text-slate-400 text-xs font-medium animate-pulse">Calculando...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-800 rounded-[2.5rem] shadow-lg p-6 text-left text-white border border-slate-700">
|
||||
<div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-700">
|
||||
<div class="bg-slate-700 p-2 rounded-xl text-emerald-400"><i data-lucide="activity" class="w-5 h-5"></i></div>
|
||||
<h3 class="font-black text-sm text-white uppercase tracking-tight">Últimas 5 Entradas</h3>
|
||||
</div>
|
||||
<div id="latestActivity" class="space-y-4">
|
||||
<p class="text-slate-400 text-xs font-medium animate-pulse">Cargando actividad...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -122,16 +173,12 @@
|
||||
|
||||
async function loadDashboardData(token) {
|
||||
try {
|
||||
// 1. Pedimos los datos del Panel Operativo (los que ya están asignados o citados)
|
||||
const resActive = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${token}` } });
|
||||
const dataActive = await resActive.json();
|
||||
|
||||
// 2. Pedimos los datos de Proveedores (para saber cuántos están en bandeja y en cola)
|
||||
// 1. Pedimos todos los datos (el endpoint de scraped trae todos los del panel también)
|
||||
const resScraped = await fetch(`${API_URL}/providers/scraped`, { headers: { "Authorization": `Bearer ${token}` } });
|
||||
const dataScraped = await resScraped.json();
|
||||
|
||||
if (dataActive.ok && dataScraped.ok) {
|
||||
processDashboard(dataActive.services, dataScraped.services);
|
||||
if (dataScraped.ok) {
|
||||
processDashboard(dataScraped.services);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error cargando dashboard:", e);
|
||||
@@ -139,83 +186,88 @@
|
||||
}
|
||||
}
|
||||
|
||||
function processDashboard(activeServices, scrapedServices) {
|
||||
// Fecha de hoy en formato YYYY-MM-DD para comparar
|
||||
function processDashboard(allServices) {
|
||||
// Filtrar SOLO LOS ACTIVOS reales (Ignoramos los archivados)
|
||||
const activeServices = allServices.filter(s => s.status !== 'archived');
|
||||
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
|
||||
// --- CÁLCULO DE KPIs ---
|
||||
// --- CÁLCULO DE KPIs CORREGIDO ---
|
||||
|
||||
// 1. SIN ASIGNAR (En bandeja de entrada, status pending, sin mandar a la rueda automatizada)
|
||||
const unassignedCount = scrapedServices.filter(s => s.status === 'pending' && s.automation_status !== 'in_progress').length;
|
||||
// 1. Total (Solución al fallo de la suma doble)
|
||||
const totalActive = activeServices.length;
|
||||
|
||||
// 2. EN RUEDA (Buscando operario en automático)
|
||||
const queueCount = scrapedServices.filter(s => s.status === 'pending' && s.automation_status === 'in_progress').length;
|
||||
// 2. Sin Asignar
|
||||
const unassignedCount = activeServices.filter(s => s.status === 'pending' && s.automation_status !== 'in_progress').length;
|
||||
|
||||
// 3. ASIGNADOS PERO SIN FECHA (En el Panel Operativo, columna izquierda)
|
||||
const pendingCount = activeServices.filter(s => s.estado_operativo === 'asignado_operario').length;
|
||||
// 3. En Rueda
|
||||
const queueCount = activeServices.filter(s => s.status === 'pending' && s.automation_status === 'in_progress').length;
|
||||
|
||||
// 4. CITAS DE HOY (Tienen programada la visita para la fecha de hoy)
|
||||
// 4. Asignados sin cita (Pendientes)
|
||||
const pendingCount = activeServices.filter(s => s.status === 'imported' && (!s.raw_data.scheduled_date || s.raw_data.scheduled_date === "")).length;
|
||||
|
||||
// 5. Citas de hoy
|
||||
const todayVisits = activeServices.filter(s => s.raw_data && s.raw_data.scheduled_date === todayStr);
|
||||
|
||||
// 5. TOTAL GENERAL (Todo lo que no está archivado ni terminado de forma permanente)
|
||||
const totalActive = activeServices.length + queueCount + unassignedCount;
|
||||
// 6. Urgencias (NUEVO)
|
||||
const urgentVisits = activeServices.filter(s => {
|
||||
const raw = s.raw_data || {};
|
||||
return raw['Urgente'] === 'Sí' || raw['Urgente'] === 'true' || raw['URGENTE'] === 'SI';
|
||||
});
|
||||
|
||||
// Pintar los números con animación
|
||||
// Pintar los números
|
||||
animateValue("kpiTotal", totalActive);
|
||||
animateValue("kpiUnassigned", unassignedCount);
|
||||
animateValue("kpiQueue", queueCount);
|
||||
animateValue("kpiPending", pendingCount);
|
||||
animateValue("kpiToday", todayVisits.length);
|
||||
animateValue("kpiUrgent", urgentVisits.length);
|
||||
|
||||
// --- RENDERIZAR AGENDA DE HOY ---
|
||||
// --- RENDERIZAR WIDGETS ---
|
||||
renderTodaySchedule(todayVisits);
|
||||
renderUrgentSchedule(urgentVisits);
|
||||
renderCompanyDistribution(activeServices);
|
||||
renderLatestActivity(allServices); // Aquí pasamos todos para ver los recién entrados
|
||||
}
|
||||
|
||||
// WIDGET 1: Agenda Hoy
|
||||
function renderTodaySchedule(visits) {
|
||||
const container = document.getElementById("todaySchedule");
|
||||
|
||||
if (todayVisits.length === 0) {
|
||||
if (visits.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-10 border-2 border-dashed border-slate-100 rounded-[2rem]">
|
||||
<i data-lucide="coffee" class="w-10 h-10 text-slate-300 mx-auto mb-3"></i>
|
||||
<p class="text-slate-500 text-sm font-bold uppercase tracking-widest">No hay visitas programadas para hoy</p>
|
||||
</div>`;
|
||||
lucide.createIcons();
|
||||
return;
|
||||
lucide.createIcons(); return;
|
||||
}
|
||||
|
||||
// Ordenar citas por hora
|
||||
todayVisits.sort((a, b) => {
|
||||
const timeA = a.raw_data.scheduled_time || "23:59";
|
||||
const timeB = b.raw_data.scheduled_time || "23:59";
|
||||
return timeA.localeCompare(timeB);
|
||||
});
|
||||
visits.sort((a, b) => (a.raw_data.scheduled_time || "23:59").localeCompare(b.raw_data.scheduled_time || "23:59"));
|
||||
|
||||
container.innerHTML = todayVisits.map(s => {
|
||||
container.innerHTML = visits.map(s => {
|
||||
const raw = s.raw_data;
|
||||
const time = raw.scheduled_time ? raw.scheduled_time.substring(0,5) : "--:--";
|
||||
const op = s.assigned_name || "Sin Asignar";
|
||||
const op = s.current_worker_name || raw.assigned_to_name || "Sin Asignar";
|
||||
const name = raw['Nombre Cliente'] || raw['CLIENTE'] || "Asegurado";
|
||||
const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || "Dirección no especificada";
|
||||
const ref = s.service_ref;
|
||||
|
||||
// Color según estado (por si está trabajando o ya terminó el de hoy)
|
||||
let statusColor = "bg-blue-50 text-blue-600";
|
||||
if(raw.status_operativo === 'trabajando') statusColor = "bg-amber-50 text-amber-600";
|
||||
if(raw.status_operativo === 'incidencia') statusColor = "bg-red-50 text-red-600";
|
||||
if(raw.status_operativo === 'terminado') statusColor = "bg-emerald-50 text-emerald-600";
|
||||
|
||||
return `
|
||||
<div class="flex items-center justify-between p-4 rounded-2xl bg-slate-50 border border-slate-100 hover:border-blue-200 transition-all text-left group">
|
||||
<div class="flex items-center justify-between p-4 rounded-2xl bg-slate-50 border border-slate-100 hover:border-blue-200 transition-all group">
|
||||
<div class="flex items-center gap-4 flex-1 min-w-0">
|
||||
<div class="w-16 h-12 rounded-xl ${statusColor} flex items-center justify-center shrink-0 font-black text-lg tracking-tighter shadow-inner">
|
||||
${time}
|
||||
</div>
|
||||
<div class="w-16 h-12 rounded-xl ${statusColor} flex items-center justify-center shrink-0 font-black text-lg tracking-tighter shadow-inner">${time}</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="font-black text-slate-800 uppercase text-sm truncate">${name}</h4>
|
||||
<span class="text-[8px] bg-white border border-slate-200 text-slate-400 px-2 py-0.5 rounded-full font-black">#${ref}</span>
|
||||
<span class="text-[8px] bg-white border border-slate-200 text-slate-400 px-2 py-0.5 rounded-full font-black">#${s.service_ref}</span>
|
||||
</div>
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase truncate mt-0.5"><i data-lucide="map-pin" class="w-3 h-3 inline mr-1"></i>${pop}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end shrink-0 pl-4 border-l border-slate-200 ml-4 hidden md:flex">
|
||||
<span class="text-[9px] font-black uppercase text-slate-400 tracking-widest mb-1">Operario</span>
|
||||
<div class="flex items-center gap-1.5 bg-white px-3 py-1.5 rounded-lg border border-slate-100 shadow-sm">
|
||||
@@ -225,15 +277,97 @@
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Efecto visual para que los números cuenten de 0 al valor real
|
||||
// WIDGET 2: Urgencias
|
||||
function renderUrgentSchedule(visits) {
|
||||
const container = document.getElementById("urgentSchedule");
|
||||
if (visits.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-6 bg-emerald-50/50 rounded-2xl border border-emerald-100">
|
||||
<i data-lucide="shield-check" class="w-8 h-8 text-emerald-400 mx-auto mb-2"></i>
|
||||
<p class="text-emerald-600 text-xs font-bold uppercase tracking-widest">Todo bajo control. Sin urgencias.</p>
|
||||
</div>`;
|
||||
lucide.createIcons(); return;
|
||||
}
|
||||
|
||||
container.innerHTML = visits.map(s => {
|
||||
const raw = s.raw_data;
|
||||
const name = raw['Nombre Cliente'] || raw['CLIENTE'] || "Asegurado";
|
||||
const op = s.current_worker_name || raw.assigned_to_name || "Buscando Operario...";
|
||||
return `
|
||||
<div class="p-4 rounded-2xl bg-red-50/50 border border-red-100 flex justify-between items-center">
|
||||
<div>
|
||||
<h4 class="font-black text-slate-800 uppercase text-xs truncate">${name}</h4>
|
||||
<p class="text-[10px] font-bold text-red-500 uppercase mt-1"><i data-lucide="info" class="w-3 h-3 inline mr-1"></i>REF: #${s.service_ref}</p>
|
||||
</div>
|
||||
<span class="text-[9px] font-black uppercase bg-white text-slate-600 px-2 py-1 rounded-md border border-slate-200 shadow-sm">${op}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// WIDGET 3: Distribución Compañías
|
||||
function renderCompanyDistribution(activeServices) {
|
||||
const container = document.getElementById("companyDistribution");
|
||||
if (activeServices.length === 0) { container.innerHTML = "<p class="text-xs text-slate-400">Sin datos</p>"; return; }
|
||||
|
||||
const counts = {};
|
||||
activeServices.forEach(s => {
|
||||
const raw = s.raw_data || {};
|
||||
let comp = (raw['Compañía'] || raw['COMPAÑIA'] || raw['Procedencia'] || (s.provider === 'MANUAL' ? 'PARTICULAR' : 'OTRA')).toUpperCase().trim();
|
||||
if(comp.includes("HOMESERVE")) comp = "HOMESERVE";
|
||||
counts[comp] = (counts[comp] || 0) + 1;
|
||||
});
|
||||
|
||||
const sorted = Object.entries(counts).sort((a,b) => b[1] - a[1]);
|
||||
const max = sorted[0][1];
|
||||
|
||||
container.innerHTML = sorted.map(([comp, count]) => {
|
||||
const percent = Math.round((count / max) * 100);
|
||||
return `
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs font-black uppercase">
|
||||
<span class="text-slate-600">${comp}</span>
|
||||
<span class="text-indigo-600">${count} expedientes</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 rounded-full h-2">
|
||||
<div class="bg-indigo-500 h-2 rounded-full" style="width: ${percent}%"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// WIDGET 4: Última Actividad
|
||||
function renderLatestActivity(allServices) {
|
||||
const container = document.getElementById("latestActivity");
|
||||
// Ordenar por ID o Created At descendente y coger 5
|
||||
const latest = allServices.sort((a, b) => b.id - a.id).slice(0, 5);
|
||||
|
||||
if (latest.length === 0) { container.innerHTML = "<p class='text-xs text-slate-400'>Sin actividad reciente</p>"; return; }
|
||||
|
||||
container.innerHTML = latest.map(s => {
|
||||
const name = s.raw_data['Nombre Cliente'] || s.raw_data['CLIENTE'] || "Nuevo Cliente";
|
||||
const isNew = s.status === 'pending';
|
||||
const icon = isNew ? '<i data-lucide="download-cloud" class="w-3.5 h-3.5 text-blue-400"></i>' : '<i data-lucide="check" class="w-3.5 h-3.5 text-emerald-400"></i>';
|
||||
return `
|
||||
<div class="flex gap-3 items-start border-b border-slate-700/50 pb-3 last:border-0 last:pb-0">
|
||||
<div class="mt-0.5">${icon}</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">REF: #${s.service_ref}</p>
|
||||
<p class="text-xs font-bold text-slate-200 truncate">${name}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Animación de contador de KPIs
|
||||
function animateValue(id, end) {
|
||||
const obj = document.getElementById(id);
|
||||
let start = 0;
|
||||
const duration = 1000;
|
||||
const duration = 800;
|
||||
const range = end - start;
|
||||
if(range === 0) { obj.innerText = "0"; return; }
|
||||
const minTimer = 50;
|
||||
|
||||
Reference in New Issue
Block a user