Actualizar panel.html

This commit is contained in:
2026-02-16 19:42:03 +00:00
parent d4d3e47dcd
commit 55b471e8f6

View File

@@ -6,39 +6,92 @@
<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; } to { opacity: 1; } }</style>
<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">
<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"></div>
<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 fade-in">
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-8">
<div class="fade-in max-w-7xl mx-auto space-y-8 text-left">
<div class="flex justify-between items-center mb-8">
<h2 class="text-2xl font-bold text-gray-800">Dashboard</h2>
<button onclick="alert('Próximamente')" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg shadow-lg flex items-center gap-2">
<i data-lucide="plus-circle" class="w-5 h-5"></i> Nuevo
</button>
<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="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<p class="text-xs font-semibold text-gray-500 uppercase">Servicios Activos</p>
<h3 class="text-2xl font-bold text-gray-800 mt-1" id="countServices">...</h3>
<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="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h3 class="font-bold text-gray-800 mb-4">Servicios Recientes</h3>
<div id="servicesTableBody">Cargando...</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 text-left">
<div class="stat-card bg-white p-6 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-4xl font-black text-slate-800" id="kpiTotal">-</h3>
<p class="text-xs text-slate-500 font-medium mt-2">Expedientes en marcha</p>
</div>
<div class="stat-card bg-white p-6 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">Buscando Operario</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-4xl font-black text-slate-800" id="kpiQueue">-</h3>
<p class="text-xs text-slate-500 font-medium mt-2">En rueda de WhatsApp</p>
</div>
<div class="stat-card bg-white p-6 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">Pendiente Cita</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-4xl font-black text-slate-800" id="kpiPending">-</h3>
<p class="text-xs text-slate-500 font-medium mt-2">Asignados sin agendar</p>
</div>
<div class="stat-card bg-white p-6 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">Visitas 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-4xl font-black text-slate-800" id="kpiToday">-</h3>
<p class="text-xs text-slate-500 font-medium 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>
@@ -46,35 +99,141 @@
<script src="js/layout.js"></script>
<script>
// Esperamos a que cargue el layout y luego cargamos los datos
document.addEventListener("DOMContentLoaded", () => {
const token = localStorage.getItem("token");
if(token) loadServices(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 loadServices(token) {
async function loadDashboardData(token) {
try {
const res = await fetch(`${API_URL}/services`, {
headers: { "Authorization": `Bearer ${token}` }
});
const data = await res.json();
if (data.ok) {
document.getElementById("countServices").innerText = data.services.length;
renderTable(data.services);
// 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 cola buscando)
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>';
}
} catch (e) { console.error(e); }
}
function renderTable(services) {
const container = document.getElementById("servicesTableBody");
if(services.length === 0) { container.innerHTML = "No hay servicios."; return; }
function processDashboard(activeServices, scrapedServices) {
// Fecha de hoy en formato YYYY-MM-DD para comparar
const todayStr = new Date().toISOString().split('T')[0];
let html = '<table class="w-full text-left"><thead><tr class="text-gray-500 text-sm"><th>Cliente</th><th>Servicio</th><th>Estado</th></tr></thead><tbody>';
services.forEach(s => {
html += `<tr class="border-t"><td class="p-3">${s.client_name}</td><td>${s.title}</td><td><span class="bg-yellow-100 px-2 rounded text-xs">En Proceso</span></td></tr>`;
// --- CÁLCULO DE KPIs ---
// Expedientes en rueda automatizada (in_progress)
const queueCount = scrapedServices.filter(s => s.automation_status === 'in_progress' && s.status !== 'archived').length;
// Asignados a operario pero que aún no han puesto fecha de cita
const pendingCount = activeServices.filter(s => s.estado_operativo === 'asignado_operario').length;
// Visitas programadas exactamente para hoy
const todayVisits = activeServices.filter(s => s.raw_data && s.raw_data.scheduled_date === todayStr);
// Total general (Activos operativos + En cola)
const totalActive = activeServices.length + queueCount;
// Pintar los números
animateValue("kpiTotal", totalActive);
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);
});
html += '</tbody></table>';
container.innerHTML = html;
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";
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 === '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">
<h4 class="font-black text-slate-800 uppercase text-sm truncate">${name}</h4>
<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>