Actualizar servicios.html

This commit is contained in:
2026-03-18 22:42:40 +00:00
parent fedf73a284
commit a4fe49abf4

View File

@@ -63,28 +63,28 @@
</div> </div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div id="kpiCard-unassigned" onclick="toggleKpiFilter('unassigned')" class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4 cursor-pointer hover:shadow-md transition-all"> <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-rose-50 flex items-center justify-center text-rose-500 shrink-0"><i data-lucide="inbox" class="w-6 h-6"></i></div> <div class="w-12 h-12 rounded-full bg-rose-50 flex items-center justify-center text-rose-500 shrink-0"><i data-lucide="inbox" class="w-6 h-6"></i></div>
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Sin Asignar</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Sin Asignar</p>
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-unassigned">0</h3> <h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-unassigned">0</h3>
</div> </div>
</div> </div>
<div id="kpiCard-scheduled" onclick="toggleKpiFilter('scheduled')" class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4 cursor-pointer hover:shadow-md transition-all"> <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-emerald-50 flex items-center justify-center text-emerald-500 shrink-0"><i data-lucide="calendar-check" class="w-6 h-6"></i></div> <div class="w-12 h-12 rounded-full bg-emerald-50 flex items-center justify-center text-emerald-500 shrink-0"><i data-lucide="calendar-check" class="w-6 h-6"></i></div>
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Agendados</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Agendados</p>
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-scheduled">0</h3> <h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-scheduled">0</h3>
</div> </div>
</div> </div>
<div id="kpiCard-waiting" onclick="toggleKpiFilter('waiting')" class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4 cursor-pointer hover:shadow-md transition-all"> <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-amber-50 flex items-center justify-center text-amber-500 shrink-0"><i data-lucide="clock" class="w-6 h-6"></i></div> <div class="w-12 h-12 rounded-full bg-amber-50 flex items-center justify-center text-amber-500 shrink-0"><i data-lucide="clock" class="w-6 h-6"></i></div>
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Espera Cliente</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Espera Cliente</p>
<h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-waiting">0</h3> <h3 class="text-2xl font-black text-slate-800 leading-none mt-1" id="kpi-waiting">0</h3>
</div> </div>
</div> </div>
<div id="kpiCard-incident" onclick="toggleKpiFilter('incident')" class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-4 cursor-pointer hover:shadow-md transition-all"> <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-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> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Incidencia</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Incidencia</p>
@@ -313,10 +313,9 @@
<div class="grid grid-cols-2 gap-4 items-center"> <div class="grid grid-cols-2 gap-4 items-center">
<div> <div>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Operario Asignado</p> <p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Operario Asignado</p>
<div class="flex items-center gap-2 bg-white p-2 rounded-xl border border-slate-100 shadow-sm relative"> <div class="flex items-center gap-2 bg-white p-2 rounded-xl border border-slate-100 shadow-sm">
<div class="bg-emerald-100 w-8 h-8 rounded-full flex items-center justify-center text-emerald-600 shrink-0"><i data-lucide="hard-hat" class="w-4 h-4"></i></div> <div class="bg-emerald-100 w-8 h-8 rounded-full flex items-center justify-center text-emerald-600 shrink-0"><i data-lucide="hard-hat" class="w-4 h-4"></i></div>
<select id="detWorkerSelect" class="w-full font-black text-slate-700 text-[11px] uppercase leading-tight bg-transparent border-none outline-none appearance-none cursor-pointer pr-4 truncate"></select> <p id="detWorker" class="font-black text-slate-700 text-[11px] uppercase leading-tight truncate"></p>
<i data-lucide="chevron-down" class="w-3 h-3 text-slate-400 absolute right-2 pointer-events-none"></i>
</div> </div>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
@@ -442,46 +441,6 @@
let localData = []; let localData = [];
let systemStatuses = []; let systemStatuses = [];
let activeStatusFilter = 'ALL'; let activeStatusFilter = 'ALL';
let activeKpiFilter = 'ALL';
async function applyTheme() {
try {
let theme = JSON.parse(localStorage.getItem('app_theme'));
const res = await fetch(`${API_URL}/config/company`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if(data.ok && data.config && data.config.app_settings) {
theme = typeof data.config.app_settings === 'string' ? JSON.parse(data.config.app_settings) : data.config.app_settings;
localStorage.setItem('app_theme', JSON.stringify(theme));
}
if(theme && theme.primary) {
document.documentElement.style.setProperty('--primary', theme.primary);
document.documentElement.style.setProperty('--secondary', theme.secondary);
document.documentElement.style.setProperty('--app-bg', theme.bg || '#f4f7f9');
}
} catch (e) { console.warn("Usando tema por defecto"); }
}
function toggleKpiFilter(kpiType) {
if (activeKpiFilter === kpiType) {
activeKpiFilter = 'ALL';
} else {
activeKpiFilter = kpiType;
}
const cards = ['pte_operario', 'pte_cita', 'trabajando', 'finalizados'];
cards.forEach(c => {
const el = document.getElementById(`kpiCard-${c}`);
if (activeKpiFilter === c) {
el.classList.add('ring-2', 'ring-blue-500', 'border-blue-500', 'shadow-md');
el.classList.remove('border-slate-100');
} else {
el.classList.remove('ring-2', 'ring-blue-500', 'border-blue-500', 'shadow-md');
el.classList.add('border-slate-100');
}
});
renderLists();
}
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' },
@@ -639,10 +598,10 @@
const weekValue = document.getElementById('weekFilter').value; const weekValue = document.getElementById('weekFilter').value;
// Inicializamos contadores reales // Inicializamos contadores reales
let kpiPteOperario = 0; let kpiUnassigned = 0;
let kpiPteCita = 0; let kpiScheduled = 0;
let kpiTrabajando = 0; let kpiWaiting = 0;
let kpiFinalizados = 0; let kpiIncident = 0;
const filteredData = localData.filter(s => { const filteredData = localData.filter(s => {
const raw = s.raw_data || {}; const raw = s.raw_data || {};
@@ -651,32 +610,22 @@
const stName = stateInfo.name.toLowerCase(); const stName = stateInfo.name.toLowerCase();
// CONDICIONES CLARAS PARA CADA BLOQUE KPI if (!s.assigned_name || stateInfo.id === 'bolsa' || stName.includes('asignar')) {
const isPteOperario = !s.assigned_name || stateInfo.id === 'bolsa' || stName.includes('asignar'); kpiUnassigned++;
const isFinalizado = stateInfo.is_final || stName.includes('finalizado') || stName.includes('terminado') || stName.includes('anulado'); }
const isTrabajando = stName.includes('trabajando'); else if (stName.includes('incidencia') || stName.includes('pausa')) {
const isPteCita = s.assigned_name && !isFinalizado && !isTrabajando && (!raw.scheduled_date || raw.scheduled_date === ""); kpiIncident++;
}
// Contamos else if (raw.scheduled_date && raw.scheduled_date !== "" && !stateInfo.is_final) {
if (isPteOperario) kpiPteOperario++; kpiScheduled++;
else if (isFinalizado) kpiFinalizados++; }
else if (isTrabajando) kpiTrabajando++; else if (!stateInfo.is_final && !raw.scheduled_date) {
else if (isPteCita) kpiPteCita++; kpiWaiting++;
}
const name = (raw["Nombre Cliente"] || raw["CLIENTE"] || "").toLowerCase(); const name = (raw["Nombre Cliente"] || raw["CLIENTE"] || "").toLowerCase();
const ref = (s.service_ref || "").toLowerCase(); const ref = (s.service_ref || "").toLowerCase();
const addr = (raw["Dirección"] || raw["DOMICILIO"] || "").toLowerCase(); const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm);
const pop = (raw["Población"] || raw["POBLACION-PROVINCIA"] || "").toLowerCase();
const phone = (raw["Teléfono"] || raw["TELEFONO"] || raw["TELEFONOS"] || "").toLowerCase();
const comp = (raw["Compañía"] || raw["COMPAÑIA"] || raw["Procedencia"] || "").toLowerCase();
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" || s.assigned_name === selectedOp; const matchesOp = selectedOp === "ALL" || s.assigned_name === selectedOp;
let matchesWeek = true; let matchesWeek = true;
@@ -688,20 +637,13 @@
let matchesStatus = (activeStatusFilter === "ALL") ? true : String(stateInfo.id) === activeStatusFilter; let matchesStatus = (activeStatusFilter === "ALL") ? true : String(stateInfo.id) === activeStatusFilter;
// APLICAR FILTRO DEL KPI DE ARRIBA (EL BOTÓN DE CLIC) return matchesSearch && matchesOp && matchesWeek && matchesStatus;
let matchesKpi = true;
if (activeKpiFilter === 'pte_operario') matchesKpi = isPteOperario;
else if (activeKpiFilter === 'pte_cita') matchesKpi = isPteCita;
else if (activeKpiFilter === 'trabajando') matchesKpi = isTrabajando;
else if (activeKpiFilter === 'finalizados') matchesKpi = isFinalizado;
return matchesSearch && matchesOp && matchesWeek && matchesStatus && matchesKpi;
}); });
document.getElementById('kpi-pte-operario').innerText = kpiPteOperario; document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
document.getElementById('kpi-pte-cita').innerText = kpiPteCita; document.getElementById('kpi-scheduled').innerText = kpiScheduled;
document.getElementById('kpi-trabajando').innerText = kpiTrabajando; document.getElementById('kpi-waiting').innerText = kpiWaiting;
document.getElementById('kpi-finalizados').innerText = kpiFinalizados; document.getElementById('kpi-incident').innerText = kpiIncident;
const grid = document.getElementById('servicesGrid'); const grid = document.getElementById('servicesGrid');
grid.innerHTML = filteredData.length > 0 grid.innerHTML = filteredData.length > 0
@@ -714,33 +656,13 @@
lucide.createIcons(); lucide.createIcons();
} }
// NUEVA FUNCIÓN: Carga los operarios para poder reasignar sobre la marcha
async function loadOpsForReassign(gid, currentWorkerId) {
const sel = document.getElementById('detWorkerSelect');
if(!sel) return;
sel.innerHTML = '<option value="">-- Desasignar Operario --</option>';
try {
const endpoint = gid ? `${API_URL}/operators?guild_id=${gid}` : `${API_URL}/operators`;
const res = await fetch(endpoint, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if(data.ok) {
data.operators.forEach(op => {
const isSelected = String(op.id) === String(currentWorkerId) ? 'selected' : '';
sel.innerHTML += `<option value="${op.id}" ${isSelected}>${op.full_name}</option>`;
});
}
} catch(e) {}
}
function buildGridCard(s) { function buildGridCard(s) {
const raw = s.raw_data || {}; const raw = s.raw_data || {};
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado Sin Nombre"; const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado Sin Nombre";
const addr = raw["Dirección"] || raw["DOMICILIO"] || "---"; const addr = raw["Dirección"] || raw["DOMICILIO"] || "---";
const pop = raw["Población"] || raw["POBLACION-PROVINCIA"] || ""; const pop = raw["Población"] || raw["POBLACION-PROVINCIA"] || "";
const fullAddr = `${addr} ${pop}`.trim(); const fullAddr = `${addr} ${pop}`.trim();
// FIX: Evitamos el 'undefined' const cita = raw.scheduled_date ? `${raw.scheduled_date.split('-').reverse().slice(0,2).join('/')} | ${raw.scheduled_time}` : 'Pte. Cita';
const timeStr = raw.scheduled_time ? raw.scheduled_time : '--:--';
const cita = raw.scheduled_date ? `${raw.scheduled_date.split('-').reverse().slice(0,2).join('/')} | ${timeStr}` : 'Pte. Cita';
const companyName = raw['Compañía'] || raw['COMPAÑIA'] || raw['Procedencia'] || (s.provider === 'MANUAL' ? 'PARTICULAR' : 'ASEGURADORA'); const companyName = raw['Compañía'] || raw['COMPAÑIA'] || raw['Procedencia'] || (s.provider === 'MANUAL' ? 'PARTICULAR' : 'ASEGURADORA');
const isUrgent = s.is_urgent === true || (raw['Urgente'] && raw['Urgente'].toLowerCase() === 'sí') || (raw['URGENTE'] && raw['URGENTE'].toLowerCase() === 'si'); const isUrgent = s.is_urgent === true || (raw['Urgente'] && raw['Urgente'].toLowerCase() === 'sí') || (raw['URGENTE'] && raw['URGENTE'].toLowerCase() === 'si');
@@ -875,10 +797,7 @@
document.getElementById('panelAsignado').classList.remove('hidden'); document.getElementById('panelAsignado').classList.remove('hidden');
document.getElementById('panelSinAsignar').classList.add('hidden'); document.getElementById('panelSinAsignar').classList.add('hidden');
// CARGAMOS EL DESPLEGABLE EN VEZ DEL TEXTO FIJO document.getElementById('detWorker').innerText = s.assigned_name;
const rawGuildId = s.guild_id || raw['guild_id'] || raw.guild_id;
loadOpsForReassign(rawGuildId, s.assigned_to);
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 || "";
document.getElementById('detStatusMap').value = stateInfo.id; document.getElementById('detStatusMap').value = stateInfo.id;
@@ -933,11 +852,6 @@
const time = document.getElementById('timeInput').value; const time = document.getElementById('timeInput').value;
const statusMap = document.getElementById('detStatusMap').value; const statusMap = document.getElementById('detStatusMap').value;
// 🚨 CAPTURAMOS AL NUEVO TÉCNICO SI LO HAS CAMBIADO
const newWorkerSelect = document.getElementById('detWorkerSelect');
const newWorkerId = newWorkerSelect ? newWorkerSelect.value : null;
const newWorkerName = (newWorkerSelect && newWorkerId) ? newWorkerSelect.options[newWorkerSelect.selectedIndex].text : "";
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') && !selectedSt.name.toLowerCase().includes('asignar')) { if (selectedSt && !selectedSt.is_final && !date && !selectedSt.name.toLowerCase().includes('pausa') && !selectedSt.name.toLowerCase().includes('asignar')) {
@@ -953,19 +867,12 @@
await fetch(`${API_URL}/services/set-appointment/${id}`, { await fetch(`${API_URL}/services/set-appointment/${id}`, {
method: 'PUT', method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
// ENVIAMOS LOS DATOS DEL NUEVO TÉCNICO body: JSON.stringify({ date, time, status_operativo: statusMap })
body: JSON.stringify({
date,
time,
status_operativo: statusMap,
assigned_to: newWorkerId || "",
assigned_to_name: newWorkerName
})
}); });
closeDetailModal(); showToast("Estado actualizado"); refreshPanel(); closeDetailModal(); showToast("Estado actualizado"); refreshPanel();
} catch (e) { alert("Error"); } } catch (e) { alert("Error"); }
finally { btn.innerHTML = originalContent; btn.disabled = false; lucide.createIcons(); } finally { btn.innerHTML = originalContent; btn.disabled = false; }
} }
async function sendToAutomate() { async function sendToAutomate() {