Actualizar servicios.html
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
.card-hover:hover { transform: translateY(-2px); transition: all 0.2s; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05); }
|
.card-hover:hover { transform: translateY(-3px); transition: all 0.2s; box-shadow: 0 12px 25px -5px rgba(0, 0, 0, 0.1); border-color: #93c5fd; }
|
||||||
|
|
||||||
/* ANIMACIÓN DE TEMBLOR (SHAKE) PARA TARJETAS BLOQUEADAS */
|
/* ANIMACIÓN DE TEMBLOR (SHAKE) PARA TARJETAS BLOQUEADAS */
|
||||||
.shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
|
.shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
|
||||||
@@ -72,10 +72,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4">
|
<div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4">
|
||||||
<div class="w-12 h-12 rounded-full bg-red-50 flex items-center justify-center text-red-500 shrink-0"><i data-lucide="alert-triangle" class="w-6 h-6"></i></div>
|
<div class="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center text-purple-500 shrink-0"><i data-lucide="check-circle" class="w-6 h-6"></i></div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Incidencias</p>
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Finalizados</p>
|
||||||
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-issues">0</h3>
|
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-finished">0</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
<div id="panelSinAsignar" class="bg-rose-50/40 p-6 rounded-[1.5rem] border border-rose-100 flex flex-col shadow-sm h-full hidden">
|
<div id="panelSinAsignar" class="bg-rose-50/40 p-6 rounded-[1.5rem] border border-rose-100 flex flex-col shadow-sm h-full hidden">
|
||||||
<div class="flex items-center gap-2 mb-6">
|
<div class="flex items-center gap-2 mb-6">
|
||||||
<div class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></div>
|
<div class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></div>
|
||||||
<p class="text-[10px] font-black text-rose-600 uppercase tracking-widest">Pendiente de Asignar</p>
|
<p class="text-[10px] font-black text-rose-600 uppercase tracking-widest">Opciones de Asignación</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5 flex-1">
|
<div class="space-y-5 flex-1">
|
||||||
@@ -325,9 +325,6 @@
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
systemStatuses.forEach(st => {
|
systemStatuses.forEach(st => {
|
||||||
// Por defecto no mostramos los finales en la botonera principal para no saturar, salvo que haya muchos.
|
|
||||||
if(st.is_final) return;
|
|
||||||
|
|
||||||
const isActive = activeStatusFilter === String(st.id);
|
const isActive = activeStatusFilter === String(st.id);
|
||||||
const colorData = colorDict[st.color] || colorDict['gray'];
|
const colorData = colorDict[st.color] || colorDict['gray'];
|
||||||
|
|
||||||
@@ -375,44 +372,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 🚀 CÁLCULO DE ESTADOS Y RENDERIZADO GRID
|
// 🚀 LÓGICA INTELIGENTE DE ENRUTAMIENTO (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;
|
||||||
|
|
||||||
// 1. Si está en la bolsa buscando (Prioridad máxima visual)
|
// 1. Bolsa de Trabajo (Máxima prioridad visual)
|
||||||
if (!s.assigned_name && (s.automation_status === 'in_progress' || s.automation_status === 'failed')) {
|
if (!s.assigned_name && (s.automation_status === 'in_progress' || s.automation_status === 'failed')) {
|
||||||
return { id: 'bolsa', name: s.automation_status === 'in_progress' ? 'Buscando Operario' : 'Fallo en Bolsa', color: s.automation_status === 'in_progress' ? 'amber' : 'red', isBlocked: true };
|
return { id: 'bolsa', name: s.automation_status === 'in_progress' ? 'Buscando Operario' : 'Fallo en Bolsa', color: s.automation_status === 'in_progress' ? 'amber' : 'red', isBlocked: true, is_final: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallbacks históricos para compatibilidad
|
// 2. Si viene limpio del scraper sin estado -> Pendiente de Asignar
|
||||||
if (!s.assigned_name || dbStat === 'sin_asignar') {
|
if (!dbStat || dbStat === 'sin_asignar') {
|
||||||
const found = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0];
|
const found = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0];
|
||||||
return { id: found?.id, name: found?.name || 'Sin Asignar', color: found?.color || 'gray', isBlocked: false };
|
return { ...found, isBlocked: false };
|
||||||
}
|
}
|
||||||
if (dbStat === 'asignado_operario') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('asignado')) || systemStatuses[1], isBlocked: false };
|
|
||||||
if (dbStat === 'citado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('citado')) || systemStatuses[3], isBlocked: false };
|
|
||||||
if (dbStat === 'de_camino') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('camino')) || systemStatuses[4], isBlocked: false };
|
|
||||||
if (dbStat === 'trabajando') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('trabajando')) || systemStatuses[5], isBlocked: false };
|
|
||||||
if (dbStat === 'incidencia') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('incidencia')) || systemStatuses[6], isBlocked: false };
|
|
||||||
if (dbStat === 'terminado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('terminado')) || systemStatuses[7], isBlocked: false };
|
|
||||||
|
|
||||||
// 3. Match directo por ID Numérico (El nuevo estándar)
|
// 3. Match directo por ID Numérico (El nuevo estándar)
|
||||||
const foundObj = systemStatuses.find(st => String(st.id) === String(dbStat));
|
const foundObj = systemStatuses.find(st => String(st.id) === String(dbStat));
|
||||||
if (foundObj) return { ...foundObj, isBlocked: false };
|
if (foundObj) return { ...foundObj, isBlocked: false };
|
||||||
|
|
||||||
// 4. Último recurso inferido
|
// 4. Fallbacks históricos de texto (Para que los servicios viejos no se rompan)
|
||||||
if (s.assigned_name && (!raw.scheduled_date || raw.scheduled_date === "")) {
|
if (dbStat === 'asignado_operario') return { ...systemStatuses.find(st => st.name.toLowerCase() === 'asignado'), isBlocked: false };
|
||||||
const f = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de cita') || st.name.toLowerCase().includes('falta fecha')) || systemStatuses[2];
|
if (dbStat === 'citado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('citado')), isBlocked: false };
|
||||||
return { ...f, isBlocked: false };
|
if (dbStat === 'de_camino') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('camino')), isBlocked: false };
|
||||||
}
|
if (dbStat === 'trabajando') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('trabajando')), isBlocked: false };
|
||||||
if (s.assigned_name && raw.scheduled_date) {
|
if (dbStat === 'incidencia') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('incidencia')), isBlocked: false };
|
||||||
const f = systemStatuses.find(st => st.name.toLowerCase().includes('citado')) || systemStatuses[3];
|
if (dbStat === 'terminado') return { ...systemStatuses.find(st => st.name.toLowerCase().includes('terminado') || st.name.toLowerCase().includes('finalizado')), isBlocked: false };
|
||||||
return { ...f, isBlocked: false };
|
|
||||||
}
|
// 5. Fallback final
|
||||||
|
return { id: 'unknown', name: 'Desconocido', color: 'gray', isBlocked: false, is_final: false };
|
||||||
return { id: 'unknown', name: 'Desconocido', color: 'gray', isBlocked: false };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLists() {
|
function renderLists() {
|
||||||
@@ -421,11 +411,10 @@
|
|||||||
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;
|
||||||
|
|
||||||
// KPIs
|
|
||||||
let kpiUnassigned = 0;
|
let kpiUnassigned = 0;
|
||||||
let kpiScheduled = 0;
|
let kpiScheduled = 0;
|
||||||
let kpiActive = 0;
|
let kpiActive = 0;
|
||||||
let kpiIssues = 0;
|
let kpiFinished = 0;
|
||||||
|
|
||||||
const filteredData = localData.filter(s => {
|
const filteredData = localData.filter(s => {
|
||||||
const raw = s.raw_data || {};
|
const raw = s.raw_data || {};
|
||||||
@@ -437,22 +426,34 @@
|
|||||||
const ref = (s.service_ref || "").toLowerCase();
|
const ref = (s.service_ref || "").toLowerCase();
|
||||||
const assigned = s.assigned_name || "";
|
const assigned = s.assigned_name || "";
|
||||||
|
|
||||||
// Calculamos el estado real
|
// Calculamos el estado real y lo inyectamos
|
||||||
const stateInfo = getServiceStateInfo(s);
|
const stateInfo = getServiceStateInfo(s);
|
||||||
s._stateInfo = stateInfo; // Lo guardamos para pintar la tarjeta
|
s._stateInfo = stateInfo;
|
||||||
|
|
||||||
// Lógica de KPIs
|
// Lógica de KPIs de suma agrupada
|
||||||
const stName = stateInfo.name.toLowerCase();
|
const stName = stateInfo.name.toLowerCase();
|
||||||
if (stName.includes('asignar') || stateInfo.isBlocked) kpiUnassigned++;
|
if (stateInfo.id === 'bolsa' || stName.includes('pendiente de asignar') || stName.includes('desasignado')) {
|
||||||
else if (stName.includes('citado')) kpiScheduled++;
|
kpiUnassigned++;
|
||||||
else if (stName.includes('camino') || stName.includes('trabajando')) kpiActive++;
|
} else if (stateInfo.is_final || stName.includes('terminado') || stName.includes('anulado') || stName.includes('finalizado')) {
|
||||||
else if (stName.includes('incidencia') || stName.includes('pausa')) kpiIssues++;
|
kpiFinished++;
|
||||||
|
} else if (stName === 'asignado' || stName.includes('pendiente de cita') || stName.includes('citado')) {
|
||||||
|
kpiScheduled++;
|
||||||
|
} else {
|
||||||
|
kpiActive++; // De Camino, Trabajando, Incidencia y los personalizados
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar Filtros Visuales
|
||||||
const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm) || addr.includes(searchTerm) || pop.includes(searchTerm) || phone.includes(searchTerm) || comp.includes(searchTerm);
|
const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm) || addr.includes(searchTerm) || pop.includes(searchTerm) || phone.includes(searchTerm) || comp.includes(searchTerm);
|
||||||
const matchesOp = selectedOp === "ALL" || assigned === selectedOp;
|
const matchesOp = selectedOp === "ALL" || assigned === selectedOp;
|
||||||
|
|
||||||
// Filtro por la botonera de chips
|
let matchesStatus = false;
|
||||||
const matchesStatus = activeStatusFilter === "ALL" || String(stateInfo.id) === activeStatusFilter;
|
if (activeStatusFilter === "ALL") {
|
||||||
|
// Si no hay búsqueda activa, ocultamos los Finalizados por limpieza visual
|
||||||
|
if (stateInfo.is_final && searchTerm === "") matchesStatus = false;
|
||||||
|
else matchesStatus = true;
|
||||||
|
} else {
|
||||||
|
matchesStatus = String(stateInfo.id) === activeStatusFilter;
|
||||||
|
}
|
||||||
|
|
||||||
return matchesSearch && matchesOp && matchesStatus;
|
return matchesSearch && matchesOp && matchesStatus;
|
||||||
});
|
});
|
||||||
@@ -461,13 +462,13 @@
|
|||||||
document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
|
document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
|
||||||
document.getElementById('kpi-scheduled').innerText = kpiScheduled;
|
document.getElementById('kpi-scheduled').innerText = kpiScheduled;
|
||||||
document.getElementById('kpi-active').innerText = kpiActive;
|
document.getElementById('kpi-active').innerText = kpiActive;
|
||||||
document.getElementById('kpi-issues').innerText = kpiIssues;
|
document.getElementById('kpi-finished').innerText = kpiFinished;
|
||||||
|
|
||||||
const grid = document.getElementById('servicesGrid');
|
const grid = document.getElementById('servicesGrid');
|
||||||
grid.innerHTML = filteredData.length > 0
|
grid.innerHTML = filteredData.length > 0
|
||||||
? filteredData.map(s => buildGridCard(s)).join('')
|
? filteredData.map(s => buildGridCard(s)).join('')
|
||||||
: `<div class="col-span-full py-20 text-center bg-white rounded-[2rem] border-2 border-dashed border-slate-200">
|
: `<div class="col-span-full py-20 text-center bg-white rounded-[2rem] border-2 border-dashed border-slate-200">
|
||||||
<i data-lucide="inbox" class="w-12 h-12 text-slate-300 mx-auto mb-3"></i>
|
<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 con los filtros</p>
|
<p class="text-slate-400 font-bold uppercase tracking-widest text-sm">No hay servicios que coincidan con los filtros</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
@@ -492,7 +493,7 @@
|
|||||||
if(raw.scheduled_date) iconEstado = 'calendar';
|
if(raw.scheduled_date) iconEstado = 'calendar';
|
||||||
if(stateInfo.name.toLowerCase().includes('camino')) iconEstado = 'car';
|
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('reparaci') || stateInfo.name.toLowerCase().includes('trabaja')) iconEstado = 'wrench';
|
||||||
if(stateInfo.name.toLowerCase().includes('incidencia')) iconEstado = 'alert-triangle';
|
if(stateInfo.name.toLowerCase().includes('incidencia') || stateInfo.name.toLowerCase().includes('pausado')) iconEstado = 'alert-triangle';
|
||||||
|
|
||||||
const isBlocked = stateInfo.isBlocked;
|
const isBlocked = stateInfo.isBlocked;
|
||||||
const clickAction = isBlocked ? `shakeCard(this, '${s.automation_status}'); event.stopPropagation();` : `openDetail(${s.id})`;
|
const clickAction = isBlocked ? `shakeCard(this, '${s.automation_status}'); event.stopPropagation();` : `openDetail(${s.id})`;
|
||||||
@@ -597,7 +598,8 @@
|
|||||||
|
|
||||||
const stateInfo = s._stateInfo;
|
const stateInfo = s._stateInfo;
|
||||||
|
|
||||||
if (s.assigned_name && stateInfo.id !== 'bolsa' && !stateInfo.name.toLowerCase().includes('asignar')) {
|
// Mostrar u ocultar paneles de asignación dependiendo del estado
|
||||||
|
if (s.assigned_name && stateInfo.id !== 'bolsa' && !stateInfo.name.toLowerCase().includes('asignar') && !stateInfo.name.toLowerCase().includes('desasignado')) {
|
||||||
document.getElementById('panelAsignado').classList.remove('hidden');
|
document.getElementById('panelAsignado').classList.remove('hidden');
|
||||||
document.getElementById('panelSinAsignar').classList.add('hidden');
|
document.getElementById('panelSinAsignar').classList.add('hidden');
|
||||||
|
|
||||||
@@ -605,7 +607,6 @@
|
|||||||
document.getElementById('dateInput').value = raw.scheduled_date || "";
|
document.getElementById('dateInput').value = raw.scheduled_date || "";
|
||||||
document.getElementById('timeInput').value = raw.scheduled_time || "";
|
document.getElementById('timeInput').value = raw.scheduled_time || "";
|
||||||
|
|
||||||
// Mapeo del estado calculado al selector
|
|
||||||
document.getElementById('detStatusMap').value = stateInfo.id;
|
document.getElementById('detStatusMap').value = stateInfo.id;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('panelAsignado').classList.add('hidden');
|
document.getElementById('panelAsignado').classList.add('hidden');
|
||||||
@@ -633,7 +634,8 @@
|
|||||||
|
|
||||||
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
|
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
|
||||||
|
|
||||||
if (selectedSt && !selectedSt.is_final && !date && !selectedSt.name.toLowerCase().includes('pausa')) {
|
// Avisar si guarda sin fecha en un estado que debería tenerla
|
||||||
|
if (selectedSt && !selectedSt.is_final && !date && !selectedSt.name.toLowerCase().includes('pausa') && !selectedSt.name.toLowerCase().includes('asignar')) {
|
||||||
if(!confirm("No has asignado Fecha para este estado. ¿Deseas continuar?")) return;
|
if(!confirm("No has asignado Fecha para este estado. ¿Deseas continuar?")) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user