Añadir servicios2.html

This commit is contained in:
2026-03-22 11:17:45 +00:00
parent dc36d8d23f
commit d4c7828aae

293
servicios2.html Normal file
View File

@@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Panel de Servicios - IntegraRepara</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.fade-in { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Estilos del Tablero Kanban */
.kanban-col { min-width: 320px; max-width: 400px; display: flex; flex-direction: column; max-height: calc(100vh - 180px); }
.kanban-cards { flex: 1; overflow-y: auto; padding-bottom: 2rem; }
/* Efecto de las tarjetas */
.kanban-card { transition: all 0.2s ease; cursor: pointer; border-left: 4px solid transparent; }
.kanban-card:hover { transform: translateY(-3px); box-shadow: 0 12px 20px -5px rgba(0, 0, 0, 0.08); }
/* Colores de borde izquierdo según la columna */
.border-col-1 { border-left-color: #ef4444; } /* Rojo: Sin Asignar */
.border-col-2 { border-left-color: #f59e0b; } /* Ambar: Sin Cita */
.border-col-3 { border-left-color: #3b82f6; } /* Azul: Pendiente Inicio */
.border-col-4 { border-left-color: #10b981; } /* Verde: Trabajando */
/* Estilos base para formularios del modal (Oculto por defecto para no alargar el código, puedes integrar el tuyo) */
.input-modern { @apply w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-semibold text-slate-700 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100; }
.label-modern { @apply block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1; }
</style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans antialiased text-left overflow-hidden">
<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-auto overflow-y-hidden bg-slate-50/50 p-6 no-scrollbar relative flex flex-col">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center bg-white p-6 rounded-[2rem] shadow-sm border border-slate-200 gap-4 shrink-0 mb-6 w-full max-w-[1600px] mx-auto fade-in">
<div>
<h2 class="text-2xl font-black text-slate-800 tracking-tight flex items-center gap-3">
<span class="bg-slate-900 p-2.5 rounded-xl text-white shadow-lg"><i data-lucide="kanban"></i></span>
TABLERO OPERATIVO
</h2>
<p class="text-sm text-slate-500 mt-1 font-medium">Control en tiempo real del flujo de trabajo.</p>
</div>
<div class="flex items-center gap-3 w-full md:w-auto">
<div class="relative flex-1 min-w-[250px]">
<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="renderKanban()" placeholder="Buscar cliente, REF, población..." 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>
<button onclick="refreshData()" class="bg-white border border-slate-200 text-slate-600 hover:text-blue-600 px-4 py-3 rounded-xl shadow-sm transition-all active:scale-95 shrink-0 flex items-center gap-2">
<i data-lucide="refresh-cw" class="w-4 h-4"></i> <span class="hidden md:inline text-xs font-black uppercase tracking-widest">Recargar</span>
</button>
</div>
</div>
<div class="flex gap-6 overflow-x-auto no-scrollbar flex-1 pb-6 max-w-[1600px] mx-auto w-full fade-in">
<div class="kanban-col bg-slate-100/50 rounded-[2rem] p-4 border border-slate-200/60">
<div class="flex justify-between items-center mb-4 px-2">
<h3 class="font-black text-slate-700 uppercase tracking-widest text-xs flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-red-500"></span> Sin Asignar
</h3>
<span id="count-unassigned" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2 py-1 rounded-lg shadow-sm">0</span>
</div>
<div id="col-unassigned" class="kanban-cards space-y-3 no-scrollbar"></div>
</div>
<div class="kanban-col bg-slate-100/50 rounded-[2rem] p-4 border border-slate-200/60">
<div class="flex justify-between items-center mb-4 px-2">
<h3 class="font-black text-slate-700 uppercase tracking-widest text-xs flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span> Sin Cita
</h3>
<span id="count-unscheduled" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2 py-1 rounded-lg shadow-sm">0</span>
</div>
<div id="col-unscheduled" class="kanban-cards space-y-3 no-scrollbar"></div>
</div>
<div class="kanban-col bg-slate-100/50 rounded-[2rem] p-4 border border-slate-200/60">
<div class="flex justify-between items-center mb-4 px-2">
<h3 class="font-black text-slate-700 uppercase tracking-widest text-xs flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-blue-500"></span> Pte. Inicio
</h3>
<span id="count-pending-start" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2 py-1 rounded-lg shadow-sm">0</span>
</div>
<div id="col-pending-start" class="kanban-cards space-y-3 no-scrollbar"></div>
</div>
<div class="kanban-col bg-slate-100/50 rounded-[2rem] p-4 border border-slate-200/60">
<div class="flex justify-between items-center mb-4 px-2">
<h3 class="font-black text-slate-700 uppercase tracking-widest text-xs flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span> Trabajando
</h3>
<span id="count-working" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2 py-1 rounded-lg shadow-sm">0</span>
</div>
<div id="col-working" class="kanban-cards space-y-3 no-scrollbar"></div>
</div>
</div>
</main>
</div>
</div>
<script src="js/layout.js"></script>
<script>
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:3000'
: 'https://integrarepara-api.integrarepara.es';
let localData = [];
let systemStatuses = [];
let systemGuilds = [];
document.addEventListener("DOMContentLoaded", async () => {
if (!localStorage.getItem("token")) window.location.href = "index.html";
await Promise.all([loadStatuses(), loadGuilds()]);
refreshData();
setInterval(refreshData, 30000); // Refresco cada 30s
});
async function loadStatuses() {
try {
const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) systemStatuses = data.statuses;
} catch (e) { console.error(e); }
}
async function loadGuilds() {
try {
const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) systemGuilds = data.guilds;
} catch (e) { console.error(e); }
}
async function refreshData() {
try {
const res = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) {
localData = data.services;
renderKanban();
}
} catch (e) { console.error(e); }
}
// 🤖 "IA" LIMPIADORA DE TEXTOS
function limpiarPaja(texto) {
if (!texto) return "Sin descripción detallada.";
let res = texto.replace(/(\r\n|\n|\r)/gm, " ");
res = res.replace(/\d{2}\/\d{2}\/\d{4}\s*-\s*/g, ' ');
const basura = [
/Llama asegurad[oa]\s*\d*/gi, /solicita (operario|profesional) para/gi,
/Cobro banco.*?(?=\.|\s-|$)/gi, /El servicio dispone de hasta.*?(?=\.|\s-|$)/gi,
/Servicio asignado a:.*?(?=\.|\s-|$)/gi, /Cambio de estado:.*?(?=\.|\s-|$)/gi,
/en espera de profesional.*?(?=\.|\s-|$)/gi
];
basura.forEach(regex => { res = res.replace(regex, ' '); });
res = res.replace(/\s+/g, ' ').replace(/\s,\s/g, ', ').replace(/^[,\.\-\s]+/, '').trim();
return res.length > 5 ? res.charAt(0).toUpperCase() + res.slice(1) : texto;
}
function renderKanban() {
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
const cols = { unassigned: [], unscheduled: [], pending_start: [], working: [] };
// LÓGICA DE CLASIFICACIÓN EXACTA
localData.forEach(s => {
if (s.status === 'archived' || s.provider === 'SYSTEM_BLOCK') return;
const raw = s.raw_data || {};
const name = (raw["Nombre Cliente"] || raw["CLIENTE"] || "").toLowerCase();
const ref = (s.service_ref || "").toLowerCase();
const pop = (raw["Población"] || "").toLowerCase();
// Filtro de búsqueda
if (searchTerm && !name.includes(searchTerm) && !ref.includes(searchTerm) && !pop.includes(searchTerm)) return;
const dbStat = raw.status_operativo;
const statusObj = systemStatuses.find(st => String(st.id) === String(dbStat));
const stName = (statusObj?.name || "").toLowerCase();
// Si está finalizado o anulado, lo ocultamos del tablero activo
if (statusObj?.is_final || stName.includes('finaliza') || stName.includes('anulad')) return;
const isWorking = stName.includes('trabaja') || stName.includes('camino');
const hasDate = raw.scheduled_date && raw.scheduled_date.trim() !== "";
// REGLAS DE REPARTO
if (!s.assigned_to) {
cols.unassigned.push(s);
} else if (!hasDate) {
cols.unscheduled.push(s);
} else if (!isWorking) {
cols.pending_start.push(s);
} else {
cols.working.push(s);
}
});
// Actualizar contadores
document.getElementById('count-unassigned').innerText = cols.unassigned.length;
document.getElementById('count-unscheduled').innerText = cols.unscheduled.length;
document.getElementById('count-pending-start').innerText = cols.pending_start.length;
document.getElementById('count-working').innerText = cols.working.length;
// Renderizar tarjetas
document.getElementById('col-unassigned').innerHTML = cols.unassigned.map(s => buildCard(s, 1)).join('');
document.getElementById('col-unscheduled').innerHTML = cols.unscheduled.map(s => buildCard(s, 2)).join('');
document.getElementById('col-pending-start').innerHTML = cols.pending_start.map(s => buildCard(s, 3)).join('');
document.getElementById('col-working').innerHTML = cols.working.map(s => buildCard(s, 4)).join('');
lucide.createIcons();
}
function buildCard(s, colType) {
const raw = s.raw_data || {};
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado";
const addr = raw["Dirección"] || "Sin dirección";
const pop = raw["Población"] || "";
const comp = (raw["Compañía"] || raw["Procedencia"] || "Particular").split('-')[0].trim().substring(0,10);
const guildObj = systemGuilds.find(g => String(g.id) === String(s.guild_id || raw.guild_id));
const guildName = guildObj ? guildObj.name : 'Reparación';
const descLimpia = limpiarPaja(raw["Descripción"] || raw["DESCRIPCION"] || raw["Averia"]);
// Iconos de Alerta
const hasLock = raw.has_lock === true || String(raw.has_lock) === 'true';
const hasEyes = raw.has_eyes === true || String(raw.has_eyes) === 'true';
let alerts = '';
if (hasLock) alerts += `<span class="bg-slate-800 text-white p-1 rounded shadow-sm"><i data-lucide="lock" class="w-3 h-3"></i></span>`;
if (hasEyes) alerts += `<span class="bg-amber-500 text-white p-1 rounded shadow-sm animate-pulse"><i data-lucide="eye" class="w-3 h-3"></i></span>`;
// Información inferior según columna
let bottomInfo = '';
if (colType === 1) {
bottomInfo = `<span class="text-[10px] font-black text-red-500 bg-red-50 px-2 py-1 rounded-lg uppercase border border-red-100 flex items-center gap-1"><i data-lucide="user-plus" class="w-3 h-3"></i> Faltan Técnicos</span>`;
} else if (colType === 2) {
bottomInfo = `
<div class="flex items-center gap-1.5 bg-slate-50 px-2 py-1 rounded-lg border border-slate-200">
<div class="w-4 h-4 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center shrink-0"><i data-lucide="hard-hat" class="w-2.5 h-2.5"></i></div>
<span class="text-[10px] font-bold text-slate-600 truncate max-w-[120px]">${s.assigned_name}</span>
</div>`;
} else {
const fDate = raw.scheduled_date ? raw.scheduled_date.split('-').reverse().slice(0,2).join('/') : '';
bottomInfo = `
<div class="flex items-center gap-1.5 bg-blue-50 px-2 py-1 rounded-lg border border-blue-100">
<i data-lucide="calendar-clock" class="w-3 h-3 text-blue-600"></i>
<span class="text-[10px] font-black text-blue-700">${fDate} | ${raw.scheduled_time || ''}</span>
</div>
<div class="w-5 h-5 rounded-full bg-slate-200 flex items-center justify-center text-[8px] font-black text-slate-600 ml-auto border border-slate-300" title="${s.assigned_name}">
${s.assigned_name ? s.assigned_name.substring(0,2).toUpperCase() : '?'}
</div>`;
}
return `
<div onclick="console.log('Abrir Modal para', ${s.id})" class="kanban-card bg-white p-4 rounded-2xl border border-slate-200 shadow-sm relative border-col-${colType} flex flex-col gap-3">
${s.is_urgent ? '<div class="absolute top-0 right-0 bg-red-500 text-white text-[8px] font-black px-2 py-1 rounded-bl-lg uppercase tracking-widest">Urgente</div>' : ''}
<div class="flex justify-between items-start gap-2">
<div class="flex flex-wrap gap-1.5">
<span class="text-[8px] font-black bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded uppercase border border-slate-200">#${s.service_ref}</span>
<span class="text-[8px] font-black bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded uppercase border border-blue-100">${comp}</span>
${alerts}
</div>
</div>
<div>
<h4 class="font-black text-slate-800 text-sm uppercase leading-tight truncate">${name}</h4>
<p class="text-[9px] font-bold text-slate-400 mt-0.5 truncate uppercase flex items-center gap-1"><i data-lucide="map-pin" class="w-2.5 h-2.5"></i> ${addr}, ${pop}</p>
</div>
<p class="text-[10px] text-slate-500 font-medium leading-snug line-clamp-2 bg-slate-50 p-2 rounded-lg border border-slate-100">${descLimpia}</p>
<div class="flex items-center justify-between border-t border-slate-100 pt-3 mt-1">
${bottomInfo}
</div>
</div>`;
}
</script>
</body>
</html>