Actualizar panel.html
This commit is contained in:
239
panel.html
239
panel.html
@@ -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">
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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-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>
|
||||
|
||||
<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>
|
||||
<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(e); }
|
||||
} 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 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];
|
||||
|
||||
// --- CÁLCULO DE KPIs ---
|
||||
|
||||
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>`;
|
||||
// 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>
|
||||
|
||||
Reference in New Issue
Block a user