Actualizar servicios2.html
This commit is contained in:
355
servicios2.html
355
servicios2.html
@@ -68,71 +68,69 @@
|
|||||||
<div class="flex flex-wrap gap-4 items-center mt-2">
|
<div class="flex flex-wrap gap-4 items-center mt-2">
|
||||||
<div class="relative flex-1 min-w-[250px] max-w-md">
|
<div class="relative flex-1 min-w-[250px] max-w-md">
|
||||||
<i data-lucide="search" class="w-4 h-4 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
<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 por cliente, REF, población, teléfono..." 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">
|
<input type="text" id="searchFilter" oninput="renderLists()" placeholder="Buscar por cliente, REF, población, teléfono..." 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>
|
</div>
|
||||||
<div class="relative w-full md:w-48">
|
<div class="relative w-full md:w-48">
|
||||||
<select id="opFilter" onchange="renderKanban()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 uppercase tracking-widest text-slate-600 appearance-none pr-10 cursor-pointer">
|
<select id="opFilter" onchange="renderLists()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 uppercase tracking-widest text-slate-600 appearance-none pr-10 cursor-pointer">
|
||||||
<option value="ALL">OPERARIOS</option>
|
<option value="ALL">OPERARIOS</option>
|
||||||
</select>
|
</select>
|
||||||
<i data-lucide="chevron-down" class="w-4 h-4 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
|
<i data-lucide="chevron-down" class="w-4 h-4 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-full md:w-56">
|
<div class="relative w-full md:w-56">
|
||||||
<input type="week" id="weekFilter" onchange="renderKanban()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-slate-600 cursor-pointer" title="Filtrar por semana">
|
<input type="week" id="weekFilter" onchange="renderLists()" class="w-full bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-slate-600 cursor-pointer" title="Filtrar por semana">
|
||||||
</div>
|
</div>
|
||||||
<button onclick="document.getElementById('weekFilter').value = ''; renderKanban();" class="text-xs font-bold text-slate-400 hover:text-red-500 px-2 py-3">Limpiar Sem.</button>
|
<button onclick="document.getElementById('weekFilter').value = ''; renderLists();" class="text-xs font-bold text-slate-400 hover:text-red-500 px-2 py-3">Limpiar Sem.</button>
|
||||||
<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 ml-auto">
|
<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 ml-auto">
|
||||||
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 items-center pt-2" id="statusPills">
|
<div class="flex flex-wrap gap-2 items-center pt-2 border-b border-slate-100 pb-4 mb-4" id="statusPills">
|
||||||
<span class="text-[10px] font-black text-slate-400 uppercase"><i data-lucide="loader-2" class="w-3 h-3 animate-spin inline"></i> Cargando estados...</span>
|
<span class="text-[10px] font-black text-slate-400 uppercase"><i data-lucide="loader-2" class="w-3 h-3 animate-spin inline"></i> Cargando estados...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mt-4">
|
||||||
|
<div onclick="setKpiFilter('unassigned')" class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-3 cursor-pointer hover:bg-slate-50 transition-all group" id="box-unassigned">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-rose-50 flex items-center justify-center text-rose-500 shrink-0 group-hover:scale-110 transition-transform"><i data-lucide="inbox" class="w-5 h-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Sin Asignar</p>
|
||||||
|
<h3 class="text-xl font-black text-slate-800 leading-none mt-0.5" id="kpi-unassigned">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="setKpiFilter('unscheduled')" class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-3 cursor-pointer hover:bg-slate-50 transition-all group" id="box-unscheduled">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-amber-50 flex items-center justify-center text-amber-500 shrink-0 group-hover:scale-110 transition-transform"><i data-lucide="calendar-x" class="w-5 h-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Sin Cita</p>
|
||||||
|
<h3 class="text-xl font-black text-slate-800 leading-none mt-0.5" id="kpi-unscheduled">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="setKpiFilter('pending_start')" class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-3 cursor-pointer hover:bg-slate-50 transition-all group" id="box-pending-start">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center text-blue-500 shrink-0 group-hover:scale-110 transition-transform"><i data-lucide="play-circle" class="w-5 h-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Pte. Inicio</p>
|
||||||
|
<h3 class="text-xl font-black text-slate-800 leading-none mt-0.5" id="kpi-pending-start">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="setKpiFilter('working')" class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-3 cursor-pointer hover:bg-slate-50 transition-all group" id="box-working">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-emerald-50 flex items-center justify-center text-emerald-500 shrink-0 group-hover:scale-110 transition-transform"><i data-lucide="hammer" class="w-5 h-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Trabajando</p>
|
||||||
|
<h3 class="text-xl font-black text-slate-800 leading-none mt-0.5" id="kpi-working">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onclick="setKpiFilter('finished')" class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-3 cursor-pointer hover:bg-slate-50 transition-all group opacity-80" id="box-finished">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 shrink-0 group-hover:scale-110 transition-transform"><i data-lucide="archive" class="w-5 h-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Anul/Finaliz</p>
|
||||||
|
<h3 class="text-xl font-black text-slate-800 leading-none mt-0.5" id="kpi-finished">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-6 overflow-x-auto no-scrollbar flex-1 pb-6 w-full fade-in min-w-max items-start">
|
<div id="servicesGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 pb-12 w-full fade-in">
|
||||||
|
|
||||||
<div class="kanban-col bg-slate-100/70 rounded-[2rem] p-4 border border-slate-200/60 shadow-inner">
|
|
||||||
<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.5 h-2.5 rounded-full bg-red-500"></span> Sin Asignar</h3>
|
|
||||||
<span id="count-c1" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2.5 py-1 rounded-lg shadow-sm">0</span>
|
|
||||||
</div>
|
|
||||||
<div id="col-1" class="kanban-cards space-y-3 no-scrollbar"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="kanban-col bg-slate-100/70 rounded-[2rem] p-4 border border-slate-200/60 shadow-inner">
|
|
||||||
<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.5 h-2.5 rounded-full bg-amber-500 animate-pulse"></span> Sin Cita</h3>
|
|
||||||
<span id="count-c2" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2.5 py-1 rounded-lg shadow-sm">0</span>
|
|
||||||
</div>
|
|
||||||
<div id="col-2" class="kanban-cards space-y-3 no-scrollbar"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="kanban-col bg-slate-100/70 rounded-[2rem] p-4 border border-slate-200/60 shadow-inner">
|
|
||||||
<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.5 h-2.5 rounded-full bg-blue-500"></span> Pte. Inicio</h3>
|
|
||||||
<span id="count-c3" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2.5 py-1 rounded-lg shadow-sm">0</span>
|
|
||||||
</div>
|
|
||||||
<div id="col-3" class="kanban-cards space-y-3 no-scrollbar"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="kanban-col bg-slate-100/70 rounded-[2rem] p-4 border border-slate-200/60 shadow-inner">
|
|
||||||
<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.5 h-2.5 rounded-full bg-emerald-500"></span> Trabajando</h3>
|
|
||||||
<span id="count-c4" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2.5 py-1 rounded-lg shadow-sm">0</span>
|
|
||||||
</div>
|
|
||||||
<div id="col-4" class="kanban-cards space-y-3 no-scrollbar"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="kanban-col bg-slate-100/70 rounded-[2rem] p-4 border border-slate-200/60 shadow-inner">
|
|
||||||
<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.5 h-2.5 rounded-full bg-purple-500"></span> Incidencias</h3>
|
|
||||||
<span id="count-c5" class="bg-white border border-slate-200 text-slate-600 text-[10px] font-black px-2.5 py-1 rounded-lg shadow-sm">0</span>
|
|
||||||
</div>
|
|
||||||
<div id="col-5" class="kanban-cards space-y-3 no-scrollbar"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,6 +454,7 @@
|
|||||||
let systemStatuses = [];
|
let systemStatuses = [];
|
||||||
let systemGuilds = [];
|
let systemGuilds = [];
|
||||||
let activeStatusFilter = 'ALL';
|
let activeStatusFilter = 'ALL';
|
||||||
|
let activeKpiFilter = 'ACTIVE'; // Muestra todos menos los cerrados por defecto
|
||||||
|
|
||||||
const colorDict = {
|
const colorDict = {
|
||||||
'gray': { bg: 'bg-slate-100', text: 'text-slate-600', dot: 'bg-slate-500', border: 'border-slate-200' },
|
'gray': { bg: 'bg-slate-100', text: 'text-slate-600', dot: 'bg-slate-500', border: 'border-slate-200' },
|
||||||
@@ -545,10 +544,16 @@
|
|||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setKpiFilter(cat) {
|
||||||
|
if (activeKpiFilter === cat) activeKpiFilter = 'ACTIVE';
|
||||||
|
else activeKpiFilter = cat;
|
||||||
|
renderLists();
|
||||||
|
}
|
||||||
|
|
||||||
function setStatusFilter(id) {
|
function setStatusFilter(id) {
|
||||||
activeStatusFilter = id;
|
activeStatusFilter = id;
|
||||||
renderStatusPills();
|
renderStatusPills();
|
||||||
renderKanban();
|
renderLists();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshData() {
|
async function refreshData() {
|
||||||
@@ -558,7 +563,7 @@
|
|||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
localData = data.services;
|
localData = data.services;
|
||||||
updateOperatorFilter();
|
updateOperatorFilter();
|
||||||
renderKanban();
|
renderLists();
|
||||||
}
|
}
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
@@ -573,26 +578,14 @@
|
|||||||
if (html.includes(`value="${currentValue}"`)) opSelect.value = currentValue;
|
if (html.includes(`value="${currentValue}"`)) opSelect.value = currentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🤖 "IA" LIMPIADORA DE TEXTOS
|
|
||||||
function limpiarPaja(texto) {
|
function limpiarPaja(texto) {
|
||||||
if (!texto) return "Sin descripción detallada.";
|
if (!texto) return "Sin notas de avería.";
|
||||||
let res = texto.replace(/(\r\n|\n|\r)/gm, " ");
|
let res = texto.replace(/(\r\n|\n|\r)/gm, " ").replace(/\d{2}\/\d{2}\/\d{4}\s*-\s*/g, ' ');
|
||||||
res = res.replace(/\d{2}\/\d{2}\/\d{4}\s*-\s*/g, ' ');
|
[/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].forEach(rx => res = res.replace(rx, ' '));
|
||||||
const basura = [
|
res = res.replace(/\s+/g, ' ').replace(/^[,\.\-\s]+/, '').trim();
|
||||||
/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;
|
return res.length > 5 ? res.charAt(0).toUpperCase() + res.slice(1) : texto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// MOTOR KANBAN E INTELIGENCIA DE ESTADOS
|
|
||||||
// =====================================
|
|
||||||
|
|
||||||
function getServiceStateInfo(s) {
|
function getServiceStateInfo(s) {
|
||||||
const raw = s.raw_data || {};
|
const raw = s.raw_data || {};
|
||||||
const dbStat = raw.status_operativo;
|
const dbStat = raw.status_operativo;
|
||||||
@@ -602,7 +595,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!s.assigned_name && (!dbStat || dbStat === 'sin_asignar')) {
|
if (!s.assigned_name && (!dbStat || dbStat === 'sin_asignar')) {
|
||||||
return systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0] || {id: 'sin_asignar', name: 'Sin Asignar', color: 'gray'};
|
return systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0] || {id: 'sin_asignar', name: 'Sin Asignar', color: 'gray', is_final: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundById = systemStatuses.find(st => String(st.id) === String(dbStat));
|
const foundById = systemStatuses.find(st => String(st.id) === String(dbStat));
|
||||||
@@ -620,153 +613,169 @@
|
|||||||
const citado = systemStatuses.find(st => st.name.toLowerCase().includes('citado'));
|
const citado = systemStatuses.find(st => st.name.toLowerCase().includes('citado'));
|
||||||
if(citado) return citado;
|
if(citado) return citado;
|
||||||
}
|
}
|
||||||
if (stLower === 'de_camino') return systemStatuses.find(st => st.name.toLowerCase().includes('camino')) || {name: 'De Camino', color: 'blue'};
|
if (stLower === 'de_camino') return systemStatuses.find(st => st.name.toLowerCase().includes('camino')) || {name: 'De Camino', color: 'blue', is_final: false};
|
||||||
if (stLower === 'trabajando') return systemStatuses.find(st => st.name.toLowerCase().includes('trabajando')) || {name: 'Trabajando', color: 'emerald'};
|
if (stLower === 'trabajando') return systemStatuses.find(st => st.name.toLowerCase().includes('trabajando')) || {name: 'Trabajando', color: 'emerald', is_final: false};
|
||||||
if (stLower === 'incidencia') return systemStatuses.find(st => st.name.toLowerCase().includes('incidencia')) || {name: 'Incidencia', color: 'purple'};
|
if (stLower === 'incidencia') return systemStatuses.find(st => st.name.toLowerCase().includes('incidencia')) || {name: 'Incidencia', color: 'purple', is_final: false};
|
||||||
|
|
||||||
return { id: 'unknown', name: 'Desconocido', color: 'gray', is_final: false };
|
return { id: 'unknown', name: 'Desconocido', color: 'gray', is_final: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderKanban() {
|
function renderLists() {
|
||||||
|
if(systemStatuses.length === 0) return;
|
||||||
|
|
||||||
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
|
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
|
||||||
const selectedOp = document.getElementById('opFilter').value;
|
const selectedOp = document.getElementById('opFilter').value;
|
||||||
const weekValue = document.getElementById('weekFilter').value;
|
const weekValue = document.getElementById('weekFilter').value;
|
||||||
|
|
||||||
const cols = { c1: [], c2: [], c3: [], c4: [], c5: [] };
|
let kpiUnassigned = 0;
|
||||||
|
let kpiUnscheduled = 0;
|
||||||
localData.forEach(s => {
|
let kpiPendingStart = 0;
|
||||||
if (s.status === 'archived' || s.provider === 'SYSTEM_BLOCK') return;
|
let kpiWorking = 0;
|
||||||
|
let kpiFinished = 0;
|
||||||
|
|
||||||
|
const filteredData = localData.filter(s => {
|
||||||
const raw = s.raw_data || {};
|
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();
|
|
||||||
const phone = (raw["Teléfono"] || "").toLowerCase();
|
|
||||||
|
|
||||||
// Filtro Buscador
|
|
||||||
if (searchTerm && !name.includes(searchTerm) && !ref.includes(searchTerm) && !pop.includes(searchTerm) && !phone.includes(searchTerm)) return;
|
|
||||||
|
|
||||||
// Filtro Operario
|
|
||||||
if (selectedOp !== "ALL" && s.assigned_name !== selectedOp) return;
|
|
||||||
|
|
||||||
// Filtro Semana
|
|
||||||
if (weekValue !== "") {
|
|
||||||
if (!raw.scheduled_date || !isDateInWeekString(raw.scheduled_date, weekValue)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// INTELIGENCIA DE ESTADOS
|
|
||||||
const stateInfo = getServiceStateInfo(s);
|
const stateInfo = getServiceStateInfo(s);
|
||||||
s._stateInfo = stateInfo; // Guardamos para la tarjeta
|
s._stateInfo = stateInfo;
|
||||||
|
|
||||||
const stName = (stateInfo.name || "").toLowerCase();
|
const stName = (stateInfo.name || "").toLowerCase();
|
||||||
|
|
||||||
// Filtro de Pastillas
|
|
||||||
if (activeStatusFilter !== "ALL" && String(stateInfo.id) !== activeStatusFilter) return;
|
|
||||||
|
|
||||||
// Ocultar finalizados
|
|
||||||
if (stateInfo.is_final || stName.includes('finaliza') || stName.includes('anulad') || stName.includes('terminad')) return;
|
|
||||||
|
|
||||||
const isWorking = stName.includes('trabaja') || stName.includes('camino');
|
const isWorking = stName.includes('trabaja') || stName.includes('camino');
|
||||||
const hasDate = raw.scheduled_date && raw.scheduled_date.trim() !== "";
|
const hasDate = raw.scheduled_date && raw.scheduled_date.trim() !== "";
|
||||||
const isIncident = stName.includes('incidencia') || stName.includes('pausa') || stName.includes('espera');
|
const isFinal = stateInfo.is_final || stName.includes('finaliza') || stName.includes('anulad') || stName.includes('terminad') || s.status === 'archived';
|
||||||
|
|
||||||
// REPARTO KANBAN INTELIGENTE (5 COLUMNAS)
|
let category = 'none';
|
||||||
if (!s.assigned_to) {
|
|
||||||
cols.c1.push(s);
|
if (isFinal) { kpiFinished++; category = 'finished'; }
|
||||||
} else if (isIncident) {
|
else if (!s.assigned_to || stateInfo.id === 'bolsa' || stName.includes('asignar')) { kpiUnassigned++; category = 'unassigned'; }
|
||||||
cols.c5.push(s);
|
else if (!hasDate) { kpiUnscheduled++; category = 'unscheduled'; }
|
||||||
} else if (!hasDate) {
|
else if (!isWorking) { kpiPendingStart++; category = 'pending_start'; }
|
||||||
cols.c2.push(s);
|
else { kpiWorking++; category = 'working'; }
|
||||||
} else if (!isWorking) {
|
|
||||||
cols.c3.push(s);
|
const name = (raw["Nombre Cliente"] || raw["CLIENTE"] || "").toLowerCase();
|
||||||
|
const ref = (s.service_ref || "").toLowerCase();
|
||||||
|
const phone = (raw["Teléfono"] || "").toLowerCase();
|
||||||
|
const pop = (raw["Población"] || "").toLowerCase();
|
||||||
|
|
||||||
|
const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm) || phone.includes(searchTerm) || pop.includes(searchTerm);
|
||||||
|
const matchesOp = selectedOp === "ALL" || s.assigned_name === selectedOp;
|
||||||
|
|
||||||
|
let matchesWeek = true;
|
||||||
|
if (weekValue !== "" && raw.scheduled_date) {
|
||||||
|
matchesWeek = isDateInWeekString(raw.scheduled_date, weekValue);
|
||||||
|
} else if (weekValue !== "") {
|
||||||
|
matchesWeek = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchesStatus = (activeStatusFilter === "ALL") ? true : String(stateInfo.id) === activeStatusFilter;
|
||||||
|
|
||||||
|
let matchesKpi = false;
|
||||||
|
if (activeKpiFilter === 'ACTIVE') {
|
||||||
|
matchesKpi = category !== 'finished';
|
||||||
} else {
|
} else {
|
||||||
cols.c4.push(s);
|
matchesKpi = category === activeKpiFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchesSearch && matchesOp && matchesWeek && matchesStatus && matchesKpi;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
|
||||||
|
document.getElementById('kpi-unscheduled').innerText = kpiUnscheduled;
|
||||||
|
document.getElementById('kpi-pending-start').innerText = kpiPendingStart;
|
||||||
|
document.getElementById('kpi-working').innerText = kpiWorking;
|
||||||
|
document.getElementById('kpi-finished').innerText = kpiFinished;
|
||||||
|
|
||||||
|
['unassigned', 'unscheduled', 'pending_start', 'working', 'finished'].forEach(k => {
|
||||||
|
const box = document.getElementById(`box-${k}`);
|
||||||
|
if (box) {
|
||||||
|
if (activeKpiFilter === k) box.classList.add('ring-2', 'ring-blue-500', 'bg-blue-50');
|
||||||
|
else box.classList.remove('ring-2', 'ring-blue-500', 'bg-blue-50');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('count-c1').innerText = cols.c1.length;
|
const grid = document.getElementById('servicesGrid');
|
||||||
document.getElementById('count-c2').innerText = cols.c2.length;
|
grid.innerHTML = filteredData.length > 0
|
||||||
document.getElementById('count-c3').innerText = cols.c3.length;
|
? filteredData.map(s => buildGridCard(s)).join('')
|
||||||
document.getElementById('count-c4').innerText = cols.c4.length;
|
: `<div class="col-span-full py-20 text-center bg-white rounded-[2rem] border-2 border-dashed border-slate-200">
|
||||||
document.getElementById('count-c5').innerText = cols.c5.length;
|
<i data-lucide="layout-grid" class="w-12 h-12 text-slate-300 mx-auto mb-3"></i>
|
||||||
|
<p class="text-slate-400 font-bold uppercase tracking-widest text-sm">No hay servicios que coincidan</p>
|
||||||
document.getElementById('col-1').innerHTML = cols.c1.map(s => buildCard(s, 1)).join('');
|
</div>`;
|
||||||
document.getElementById('col-2').innerHTML = cols.c2.map(s => buildCard(s, 2)).join('');
|
|
||||||
document.getElementById('col-3').innerHTML = cols.c3.map(s => buildCard(s, 3)).join('');
|
|
||||||
document.getElementById('col-4').innerHTML = cols.c4.map(s => buildCard(s, 4)).join('');
|
|
||||||
document.getElementById('col-5').innerHTML = cols.c5.map(s => buildCard(s, 5)).join('');
|
|
||||||
|
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCard(s, colType) {
|
function buildGridCard(s) {
|
||||||
const raw = s.raw_data || {};
|
const raw = s.raw_data || {};
|
||||||
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado";
|
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado Sin Nombre";
|
||||||
const addr = raw["Dirección"] || "Sin dirección";
|
const addr = raw["Dirección"] || raw["DOMICILIO"] || "---";
|
||||||
const pop = raw["Población"] || "";
|
const pop = raw["Población"] || raw["POBLACION-PROVINCIA"] || "";
|
||||||
const comp = (raw["Compañía"] || raw["Procedencia"] || "Particular").split('-')[0].trim().substring(0,10);
|
const fullAddr = `${addr} ${pop}`.trim();
|
||||||
|
const cita = raw.scheduled_date ? `${raw.scheduled_date.split('-').reverse().slice(0,2).join('/')} | ${raw.scheduled_time}` : 'Pte. Cita';
|
||||||
|
const companyName = raw['Compañía'] || raw['COMPAÑIA'] || raw['Procedencia'] || (s.provider === 'MANUAL' ? 'PARTICULAR' : 'ASEGURADORA');
|
||||||
|
|
||||||
const guildObj = systemGuilds.find(g => String(g.id) === String(s.guild_id || raw.guild_id));
|
const isUrgent = s.is_urgent === true || (raw['Urgente'] && String(raw['Urgente']).toLowerCase() === 'sí') || (raw['URGENTE'] && String(raw['URGENTE']).toLowerCase() === 'si');
|
||||||
const guildName = guildObj ? guildObj.name : 'Reparación';
|
|
||||||
|
const stateInfo = s._stateInfo || {name: 'Desconocido', color: 'gray'};
|
||||||
|
const colorData = colorDict[stateInfo.color] || colorDict['gray'];
|
||||||
|
|
||||||
|
let iconEstado = 'tag';
|
||||||
|
if(raw.scheduled_date) iconEstado = 'calendar';
|
||||||
|
if(stateInfo.name.toLowerCase().includes('camino')) iconEstado = 'car';
|
||||||
|
if(stateInfo.name.toLowerCase().includes('reparaci') || stateInfo.name.toLowerCase().includes('trabaja')) iconEstado = 'wrench';
|
||||||
|
if(stateInfo.name.toLowerCase().includes('incidencia') || stateInfo.name.toLowerCase().includes('pausa')) iconEstado = 'alert-triangle';
|
||||||
|
|
||||||
|
const inProgressBadge = s.automation_status === 'in_progress' ? `<span class="absolute -top-3 -right-3 bg-amber-400 text-white px-3 py-1 rounded-full text-[10px] font-black uppercase shadow-lg border-2 border-white flex items-center gap-1 animate-pulse z-20"><i data-lucide="bot" class="w-3 h-3"></i> En Bolsa</span>` : '';
|
||||||
|
|
||||||
|
// 🤖 IA Y ALERTAS
|
||||||
const descLimpia = limpiarPaja(raw["Descripción"] || raw["DESCRIPCION"] || raw["Averia"]);
|
const descLimpia = limpiarPaja(raw["Descripción"] || raw["DESCRIPCION"] || raw["Averia"]);
|
||||||
|
|
||||||
// Alertas (Candado y Ojos)
|
|
||||||
const hasLock = raw.has_lock === true || String(raw.has_lock) === 'true';
|
const hasLock = raw.has_lock === true || String(raw.has_lock) === 'true';
|
||||||
const hasEyes = raw.has_eyes === true || String(raw.has_eyes) === 'true';
|
const hasEyes = raw.has_eyes === true || String(raw.has_eyes) === 'true';
|
||||||
let alerts = '';
|
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 (hasLock) alerts += `<span class="bg-slate-800 text-white px-1.5 py-0.5 rounded shadow-sm inline-flex items-center"><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>`;
|
if (hasEyes) alerts += `<span class="bg-amber-500 text-white px-1.5 py-0.5 rounded animate-pulse shadow-sm inline-flex items-center"><i data-lucide="eye" class="w-3 h-3"></i></span>`;
|
||||||
|
|
||||||
// Identificar el estado exacto actual leyendo la IA guardada
|
// 👻 FANTASMA DE CERRADOS
|
||||||
const stateInfo = s._stateInfo || {name: 'Desconocido', color: 'gray'};
|
const isFinal = stateInfo.is_final || stateInfo.name.toLowerCase().includes('finaliza') || stateInfo.name.toLowerCase().includes('anulad') || s.status === 'archived';
|
||||||
const cMap = colorDict[stateInfo.color] || colorDict['gray'];
|
const opacityClass = isFinal ? 'opacity-60 grayscale-[40%]' : '';
|
||||||
|
const watermark = isFinal ? `<div class="absolute inset-0 z-0 flex items-center justify-center pointer-events-none overflow-hidden"><span class="text-4xl font-black text-slate-500 opacity-15 -rotate-12 uppercase tracking-widest">${stateInfo.name.toLowerCase().includes('anulad') ? 'Anulado' : 'Cerrado'}</span></div>` : '';
|
||||||
// Pie de tarjeta dinámico
|
|
||||||
let bottomInfo = '';
|
|
||||||
if (colType === 1) {
|
|
||||||
if (s.automation_status === 'in_progress') {
|
|
||||||
bottomInfo = `<span class="text-[10px] font-black text-amber-600 bg-amber-50 border border-amber-200 px-2 py-1 rounded-lg uppercase flex items-center gap-1 animate-pulse"><i data-lucide="bot" class="w-3 h-3"></i> Buscando...</span>`;
|
|
||||||
} else {
|
|
||||||
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> Falta Técnico</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 `
|
return `
|
||||||
<div onclick="openDetail(${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">
|
<div class="${opacityClass} bg-white p-5 rounded-3xl border border-slate-200 shadow-sm card-hover text-left flex flex-col justify-between relative cursor-pointer gap-3" onclick="openDetail(${s.id})">
|
||||||
${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 z-10">Urgente</div>' : ''}
|
${watermark}
|
||||||
|
${inProgressBadge}
|
||||||
|
|
||||||
<div class="flex justify-between items-start gap-2 pr-12">
|
<div class="flex items-start justify-between w-full gap-2 relative z-10">
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5 flex-1 items-center">
|
||||||
<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-[9px] font-black ${colorData.bg} ${colorData.text} px-2.5 py-1.5 rounded-lg uppercase tracking-wider flex items-center gap-1.5 truncate border ${colorData.border}">
|
||||||
<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>
|
<div class="w-2 h-2 rounded-full ${colorData.dot}"></div>
|
||||||
|
<span class="truncate">${stateInfo.name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
${isUrgent ? `<span class="bg-red-500 text-white px-2 py-0.5 rounded-lg text-[9px] font-black uppercase shadow-sm animate-pulse shrink-0">URGENTE</span>` : `<span class="text-[10px] text-slate-400 font-bold uppercase bg-slate-50 border border-slate-100 px-2.5 py-1 rounded-lg shrink-0">#${s.service_ref}</span>`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="flex items-center gap-1.5 mb-1">
|
||||||
|
<span class="text-[8px] font-black text-blue-500 uppercase tracking-widest truncate">${companyName}</span>
|
||||||
${alerts}
|
${alerts}
|
||||||
</div>
|
</div>
|
||||||
|
<h4 class="font-black text-slate-800 text-base leading-tight line-clamp-2" title="${name}">${name}</h4>
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 mt-1 uppercase flex items-center gap-1 truncate"><i data-lucide="map-pin" class="w-3 h-3 shrink-0"></i> ${fullAddr}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="bg-slate-50/50 p-2.5 rounded-xl border border-slate-100 relative z-10">
|
||||||
<h4 class="font-black text-slate-800 text-sm uppercase leading-tight line-clamp-2">${name}</h4>
|
<p class="text-[10px] text-slate-600 font-medium line-clamp-2" title="${descLimpia}">${descLimpia}</p>
|
||||||
<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>
|
</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 gap-2 relative z-10">
|
||||||
|
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-between border-t border-slate-100 pt-3 mt-1">
|
<div class="w-6 h-6 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 shrink-0 border border-slate-200"><i data-lucide="hard-hat" class="w-3 h-3"></i></div>
|
||||||
${bottomInfo}
|
<span class="text-[10px] font-black text-slate-600 uppercase truncate flex-1" title="${s.assigned_name || 'Sin asignar'}">${s.assigned_name || 'Sin asignar'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 text-blue-600 shrink-0 bg-blue-50 px-2 py-1 rounded-md border border-blue-100">
|
||||||
|
<i data-lucide="${iconEstado}" class="w-3 h-3"></i>
|
||||||
|
<span class="text-[9px] font-black uppercase whitespace-nowrap">${raw.scheduled_date ? cita.split('|')[0] : 'Pte. Cita'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user