257 lines
15 KiB
HTML
257 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Panel - IntegraRepara</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
.fade-in { animation: fadeIn 0.5s ease-in-out; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
.stat-card { transition: all 0.2s; border: 1px solid #f1f5f9; }
|
|
.stat-card:hover { transform: translateY(-3px); box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05); }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-800 font-sans antialiased text-left">
|
|
|
|
<div class="flex h-screen overflow-hidden">
|
|
|
|
<div id="sidebar-container" class="h-full shrink-0"></div>
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden relative">
|
|
|
|
<div id="header-container"></div>
|
|
|
|
<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>
|
|
<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>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 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">
|
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Activos Totales</p>
|
|
<div class="bg-blue-50 text-blue-600 p-2 rounded-xl"><i data-lucide="folder-open" class="w-4 h-4"></i></div>
|
|
</div>
|
|
<h3 class="text-3xl font-black text-slate-800" id="kpiTotal">-</h3>
|
|
<p class="text-[10px] text-slate-500 font-bold uppercase mt-2">Expedientes en marcha</p>
|
|
</div>
|
|
|
|
<div class="stat-card bg-white p-5 rounded-[2rem] shadow-sm flex flex-col justify-between border-b-4 border-b-rose-400">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<p class="text-[10px] font-black text-rose-500 uppercase tracking-widest">Sin Asignar</p>
|
|
<div class="bg-rose-50 text-rose-600 p-2 rounded-xl"><i data-lucide="inbox" class="w-4 h-4"></i></div>
|
|
</div>
|
|
<h3 class="text-3xl font-black text-slate-800" id="kpiUnassigned">-</h3>
|
|
<p class="text-[10px] text-slate-500 font-bold uppercase mt-2">Muertos en bandeja</p>
|
|
</div>
|
|
|
|
<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">
|
|
<p class="text-[10px] font-black text-amber-500 uppercase tracking-widest">En Rueda</p>
|
|
<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>
|
|
</div>
|
|
|
|
<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">
|
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Sin Agendar</p>
|
|
<div class="bg-slate-100 text-slate-500 p-2 rounded-xl"><i data-lucide="clock" class="w-4 h-4"></i></div>
|
|
</div>
|
|
<h3 class="text-3xl font-black text-slate-800" id="kpiPending">-</h3>
|
|
<p class="text-[10px] text-slate-500 font-bold uppercase mt-2">Asignados sin cita</p>
|
|
</div>
|
|
|
|
<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>
|
|
<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>
|
|
|
|
<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>
|
|
<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>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/layout.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const token = localStorage.getItem("token");
|
|
if(!token) window.location.href = "index.html";
|
|
|
|
// Pintar la fecha de hoy bonita
|
|
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
|
document.getElementById('todayDate').innerText = new Date().toLocaleDateString('es-ES', options);
|
|
|
|
lucide.createIcons();
|
|
loadDashboardData(token);
|
|
});
|
|
|
|
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)
|
|
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);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error cargando dashboard:", e);
|
|
document.getElementById('todaySchedule').innerHTML = '<p class="text-red-500 text-sm font-bold">Error de conexión al cargar la agenda.</p>';
|
|
}
|
|
}
|
|
|
|
function processDashboard(activeServices, scrapedServices) {
|
|
// Fecha de hoy en formato YYYY-MM-DD para comparar
|
|
const todayStr = new Date().toISOString().split('T')[0];
|
|
|
|
// --- CÁLCULO DE KPIs ---
|
|
|
|
// 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;
|
|
|
|
// 2. EN RUEDA (Buscando operario en automático)
|
|
const queueCount = scrapedServices.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;
|
|
|
|
// 4. CITAS DE HOY (Tienen programada la visita para la fecha 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;
|
|
|
|
// Pintar los números con animación
|
|
animateValue("kpiTotal", totalActive);
|
|
animateValue("kpiUnassigned", unassignedCount);
|
|
animateValue("kpiQueue", queueCount);
|
|
animateValue("kpiPending", pendingCount);
|
|
animateValue("kpiToday", todayVisits.length);
|
|
|
|
// --- RENDERIZAR AGENDA DE HOY ---
|
|
const container = document.getElementById("todaySchedule");
|
|
|
|
if (todayVisits.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;
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
|
|
container.innerHTML = todayVisits.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 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 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="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>
|
|
</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">
|
|
<i data-lucide="hard-hat" class="w-3.5 h-3.5 text-slate-500"></i>
|
|
<span class="text-[10px] font-black text-slate-700 uppercase">${op}</span>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
lucide.createIcons();
|
|
}
|
|
|
|
// Efecto visual para que los números cuenten de 0 al valor real
|
|
function animateValue(id, end) {
|
|
const obj = document.getElementById(id);
|
|
let start = 0;
|
|
const duration = 1000;
|
|
const range = end - start;
|
|
if(range === 0) { obj.innerText = "0"; return; }
|
|
const minTimer = 50;
|
|
let stepTime = Math.abs(Math.floor(duration / range));
|
|
stepTime = Math.max(stepTime, minTimer);
|
|
const startTime = new Date().getTime();
|
|
const endTime = startTime + duration;
|
|
let timer;
|
|
function run() {
|
|
const now = new Date().getTime();
|
|
const remaining = Math.max((endTime - now) / duration, 0);
|
|
const value = Math.round(end - (remaining * range));
|
|
obj.innerText = value;
|
|
if (value == end) clearInterval(timer);
|
|
}
|
|
timer = setInterval(run, stepTime);
|
|
run();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |