Actualizar index.html

This commit is contained in:
2026-03-04 19:07:51 +00:00
parent 65d1502b98
commit 7cfe88693e

View File

@@ -38,7 +38,6 @@
</div>
<main id="mainContent" class="hidden w-full max-w-md mx-auto min-h-screen flex flex-col relative pb-10 z-10">
<div class="pt-10 pb-6 px-6 flex flex-col items-center text-center fade-in" style="animation-delay: 0.1s;">
<div id="companyLogoContainer" class="hidden relative mb-5">
<div class="absolute inset-0 bg-blue-500 rounded-3xl blur-xl opacity-20"></div>
@@ -52,16 +51,13 @@
<div class="px-5 flex-1 flex flex-col gap-6 fade-in" style="animation-delay: 0.2s;">
<h2 class="text-3xl font-black text-slate-800 tracking-tight pl-2">Hola, <span id="clientName" class="text-blue-600">Cliente</span></h2>
<div id="activeServicesContainer" class="space-y-6 mt-2"></div>
<div id="noActiveServices" class="hidden text-center p-6 bg-white/80 backdrop-blur-xl rounded-[2.5rem] shadow-xl shadow-slate-200/40 border border-white">
<div class="w-16 h-16 bg-slate-100 text-slate-400 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="check-circle" class="w-8 h-8"></i>
</div>
<h3 class="text-lg font-black text-slate-800 tracking-tight">Todo al día</h3>
</div>
<div id="historyContainerWrapper" class="hidden mt-4">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3 ml-2">Historial</h3>
<div id="historyServicesContainer" class="space-y-4"></div>
@@ -81,7 +77,7 @@
lucide.createIcons();
const urlParams = new URLSearchParams(window.location.search);
urlToken = urlParams.get('token');
const serviceParam = urlParams.get('service'); // Por si viene el ID del servicio
const serviceParam = urlParams.get('service');
if (!urlToken) {
showError();
@@ -89,7 +85,6 @@
}
try {
// Le pasamos el token y opcionalmente el ID del servicio si viene en la URL
let fetchUrl = `${API_URL}/public/portal/${urlToken}`;
if (serviceParam) {
fetchUrl += `?service=${serviceParam}`;
@@ -100,9 +95,7 @@
if (!data.ok) throw new Error("Token inválido");
// Si no hay servicios, no mostramos error, mostramos el portal vacío
const servicesList = data.services || [];
renderPortal(data.client, data.company, servicesList);
} catch (e) {
console.error("Error cargando portal:", e);
@@ -156,7 +149,12 @@
document.getElementById('companyLogo').src = company.logo;
document.getElementById('companyLogoContainer').classList.remove('hidden');
}
document.getElementById('clientName').innerText = client.name.split(' ')[0].toUpperCase();
let cName = "Cliente";
if (client && client.name) {
cName = client.name.split(' ')[0];
}
document.getElementById('clientName').innerText = cName.toUpperCase();
const activeContainer = document.getElementById('activeServicesContainer');
const historyContainerWrapper = document.getElementById('historyContainerWrapper');
@@ -169,29 +167,38 @@
let hasHistory = false;
allServices.forEach(srv => {
const isFinalized = srv.is_final;
const raw = srv.raw_data || {};
const descLimpia = summarizeDescription(srv.description);
let isFinalized = srv.is_final === true;
let raw = srv.raw_data || {};
let descLimpia = summarizeDescription(srv.description);
let stNameLower = "";
if (srv.status_name) {
stNameLower = srv.status_name.toLowerCase();
}
let hasDate = false;
if (srv.scheduled_date && srv.scheduled_time) {
hasDate = true;
}
// DEFINICIÓN SEGURA (Esta es la línea que rompía todo en la versión anterior)
let hasWorker = false;
if (srv.assigned_worker && srv.assigned_worker !== 'Pendiente' && srv.assigned_worker !== 'Sin asignar') {
hasWorker = true;
}
const stName = (srv.status_name || '').toLowerCase();
const hasDate = !!(srv.scheduled_date && srv.scheduled_time);
let statusHtml = '';
// ESTRUCTURA INTELIGENTE DE ESTADOS
const stNameLower = (srv.status_name || '').toLowerCase();
if (isFinalized || stNameLower.includes('finalizado') || stNameLower.includes('anulado')) {
statusHtml = `
<div class="bg-slate-50 border border-slate-200 p-4 rounded-2xl flex items-center justify-between opacity-70 grayscale">
statusHtml = `<div class="bg-slate-50 border border-slate-200 p-4 rounded-2xl flex items-center justify-between opacity-70 grayscale">
<span class="text-[10px] font-black text-slate-500 uppercase flex items-center gap-1.5"><i data-lucide="archive" class="w-3.5 h-3.5"></i> ${srv.status_name || 'Cerrado'}</span>
</div>`;
}
else if (stNameLower.includes('camino')) {
const fullAddr = `${raw["Dirección"] || ""}, ${raw["Código Postal"] || ""} ${raw["Población"] || ""}`;
let fullAddr = `${raw["Dirección"] || ""}, ${raw["Código Postal"] || ""} ${raw["Población"] || ""}`;
etasToInit.push({ id: srv.id, address: fullAddr });
statusHtml = `
<div class="bg-indigo-50 border border-indigo-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
statusHtml = `<div class="bg-indigo-50 border border-indigo-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
<div class="flex items-center gap-5 relative z-10">
<div class="w-14 h-14 bg-indigo-500 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-500/30 animate-pulse shrink-0">
<i data-lucide="truck" class="w-7 h-7"></i>
@@ -203,12 +210,10 @@
</div>
</div>
</div>
</div>
`;
</div>`;
}
else if (stNameLower.includes('trabajando') || stNameLower.includes('reparación')) {
statusHtml = `
<div class="bg-orange-50 border border-orange-200 p-6 rounded-3xl flex items-center gap-5 shadow-inner">
statusHtml = `<div class="bg-orange-50 border border-orange-200 p-6 rounded-3xl flex items-center gap-5 shadow-inner">
<div class="w-14 h-14 bg-orange-500 text-white rounded-2xl flex items-center justify-center shadow-md shrink-0"><i data-lucide="hammer" class="w-7 h-7"></i></div>
<div>
<h4 class="font-black text-orange-800 uppercase text-base tracking-tight">Trabajando</h4>
@@ -217,8 +222,7 @@
</div>`;
}
else if (stNameLower.includes('incidencia')) {
statusHtml = `
<div class="bg-rose-50 border border-rose-200 p-6 rounded-3xl flex items-center gap-5 shadow-inner">
statusHtml = `<div class="bg-rose-50 border border-rose-200 p-6 rounded-3xl flex items-center gap-5 shadow-inner">
<div class="w-14 h-14 bg-rose-500 text-white rounded-2xl flex items-center justify-center shadow-md shrink-0 animate-pulse">
<i data-lucide="alert-triangle" class="w-7 h-7"></i>
</div>
@@ -229,21 +233,19 @@
</div>`;
}
else if (hasDate && !stNameLower.includes('anulado') && !stNameLower.includes('desasignado')) {
// ESTÁ AGENDADO (COMPROBAMOS SI LLEGA TARDE)
const endT = addOneHour(srv.scheduled_time);
const now = new Date();
const schedParts = srv.scheduled_date.split('-');
const endTimeParts = endT.split(':');
let endT = addOneHour(srv.scheduled_time);
let now = new Date();
let schedParts = srv.scheduled_date.split('-');
let endTimeParts = endT.split(':');
let isLate = false;
if (schedParts.length === 3 && endTimeParts.length === 2) {
const limitDate = new Date(schedParts[0], schedParts[1] - 1, schedParts[2], endTimeParts[0], endTimeParts[1], 0);
let limitDate = new Date(schedParts[0], schedParts[1] - 1, schedParts[2], endTimeParts[0], endTimeParts[1], 0);
if (now > limitDate) isLate = true;
}
if (isLate) {
statusHtml = `
<div class="bg-amber-50 border border-amber-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
statusHtml = `<div class="bg-amber-50 border border-amber-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
<div class="flex items-center gap-4 relative z-10">
<div class="w-14 h-14 bg-amber-500 text-white rounded-2xl flex items-center justify-center shadow-md shrink-0">
<i data-lucide="clock-4" class="w-7 h-7"></i>
@@ -261,8 +263,7 @@
</div>
</div>`;
} else {
statusHtml = `
<div class="bg-gradient-to-br from-emerald-400 to-emerald-600 p-6 rounded-3xl text-white shadow-lg shadow-emerald-500/30 relative overflow-hidden">
statusHtml = `<div class="bg-gradient-to-br from-emerald-400 to-emerald-600 p-6 rounded-3xl text-white shadow-lg shadow-emerald-500/30 relative overflow-hidden">
<div class="flex items-center gap-5 mb-4">
<div class="w-14 h-14 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-md border border-white/30 shrink-0 relative z-10">
<i data-lucide="calendar-check" class="w-7 h-7 text-white"></i>
@@ -280,9 +281,7 @@
}
}
else if (raw.appointment_status === 'pending' && raw.requested_date) {
// CITA SOLICITADA PERO NO CONFIRMADA
statusHtml = `
<div class="bg-purple-50 border border-purple-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
statusHtml = `<div class="bg-purple-50 border border-purple-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
<div class="flex items-center gap-4 relative z-10">
<div class="w-14 h-14 bg-purple-500 text-white rounded-2xl flex items-center justify-center shadow-md shrink-0">
<i data-lucide="hourglass" class="w-7 h-7 animate-pulse"></i>
@@ -295,10 +294,8 @@
</div>
</div>`;
}
econst hasWorker = srv.assigned_worker && srv.assigned_worker !== 'Pendiente';lse if (stNameLower.includes('esperando') || stNameLower.includes('asignado') || (hasWorker && !hasDate)) {
// ASIGNADO / ESPERANDO AL CLIENTE -> AGENDAR CITA AHORA
statusHtml = `
<div class="bg-blue-50 border border-blue-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
else if (stNameLower.includes('esperando') || stNameLower.includes('asignado') || (hasWorker && !hasDate)) {
statusHtml = `<div class="bg-blue-50 border border-blue-200 p-6 rounded-3xl relative overflow-hidden shadow-inner">
<div class="flex items-center gap-4 relative z-10">
<div class="w-14 h-14 bg-blue-500 text-white rounded-2xl flex items-center justify-center shadow-md shrink-0">
<i data-lucide="calendar-plus" class="w-7 h-7"></i>
@@ -317,8 +314,7 @@
</div>`;
}
else if (stNameLower.includes('desasignado')) {
statusHtml = `
<div class="bg-slate-100 border border-slate-200 p-6 rounded-3xl flex items-center gap-4 shadow-inner">
statusHtml = `<div class="bg-slate-100 border border-slate-200 p-6 rounded-3xl flex items-center gap-4 shadow-inner">
<div class="w-12 h-12 bg-slate-300 text-slate-600 rounded-2xl flex items-center justify-center shrink-0">
<i data-lucide="user-x" class="w-6 h-6"></i>
</div>
@@ -330,9 +326,7 @@
</div>`;
}
else {
// PENDIENTE DE ASIGNAR / DEFAULT
statusHtml = `
<div class="bg-slate-100 border border-slate-200 p-6 rounded-3xl flex items-center gap-4 shadow-inner">
statusHtml = `<div class="bg-slate-100 border border-slate-200 p-6 rounded-3xl flex items-center gap-4 shadow-inner">
<div class="w-12 h-12 bg-slate-200 text-slate-500 rounded-2xl flex items-center justify-center shrink-0">
<i data-lucide="search" class="w-6 h-6 animate-pulse"></i>
</div>
@@ -344,78 +338,73 @@
</div>`;
}
// =====================================
// BLOQUE DE BOTONES DE CONTACTO
// =====================================
let contactHtml = '';
const hasWorker = srv.assigned_worker && srv.assigned_worker !== 'Pendiente';
if (hasWorker && !isFinalized) {
const workerPhone = srv.worker_phone ? srv.worker_phone.replace('+', '') : "34000000000";
contactHtml = `
<div class="flex gap-2 mt-5">
let workerPhone = "34000000000";
if (srv.worker_phone) {
workerPhone = srv.worker_phone.replace('+', '');
}
contactHtml = `<div class="flex gap-2 mt-5">
<a href="tel:+${workerPhone}" class="flex-1 bg-blue-50 hover:bg-blue-100 border border-blue-200 text-blue-600 font-black py-3 rounded-xl flex items-center justify-center gap-1.5 active:scale-95 transition-transform shadow-sm text-[10px] uppercase tracking-widest">
<i data-lucide="phone" class="w-4 h-4"></i> Llamar Técnico
</a>
<a href="https://wa.me/${workerPhone}" target="_blank" class="flex-1 bg-emerald-50 hover:bg-emerald-100 border border-emerald-200 text-emerald-600 font-black py-3 rounded-xl flex items-center justify-center gap-1.5 active:scale-95 transition-transform shadow-sm text-[10px] uppercase tracking-widest">
<i data-lucide="message-circle" class="w-4 h-4"></i> WhatsApp
</a>
</div>
`;
</div>`;
}
// =====================================
// DATOS DEL ASEGURADO
// =====================================
const clientDataHtml = `
<div class="bg-slate-50/50 rounded-2xl p-4 mt-5 border border-slate-100 text-left">
let cNameClient = raw["Nombre Cliente"] || raw["CLIENTE"] || cName;
let cAddr = raw["Dirección"] || raw["DOMICILIO"] || "Dirección no especificada";
let cCP = raw["Código Postal"] || "";
let cPop = raw["Población"] || raw["POBLACION-PROVINCIA"] || "";
let cComp = raw["Compañía"] || raw["COMPAÑIA"] || "Particular";
let clientDataHtml = `<div class="bg-slate-50/50 rounded-2xl p-4 mt-5 border border-slate-100 text-left">
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-3 border-b border-slate-100 pb-2">Información del Siniestro</p>
<div class="space-y-3">
<div class="flex items-start gap-2.5">
<i data-lucide="user" class="w-4 h-4 text-slate-400 mt-0.5"></i>
<p class="text-xs font-bold text-slate-700 leading-tight">${raw["Nombre Cliente"] || raw["CLIENTE"] || client.name || "Asegurado"}</p>
<p class="text-xs font-bold text-slate-700 leading-tight">${cNameClient}</p>
</div>
<div class="flex items-start gap-2.5">
<i data-lucide="map-pin" class="w-4 h-4 text-slate-400 mt-0.5"></i>
<p class="text-xs font-bold text-slate-700 leading-tight">
${raw["Dirección"] || raw["DOMICILIO"] || "Dirección no especificada"}<br>
<span class="text-[10px] text-slate-500 font-medium">${raw["Código Postal"] || ""} ${raw["Población"] || raw["POBLACION-PROVINCIA"] || ""}</span>
${cAddr}<br>
<span class="text-[10px] text-slate-500 font-medium">${cCP} ${cPop}</span>
</p>
</div>
<div class="flex items-start gap-2.5">
<i data-lucide="building" class="w-4 h-4 text-slate-400 mt-0.5"></i>
<p class="text-xs font-bold text-slate-700 leading-tight">${raw["Compañía"] || raw["COMPAÑIA"] || "Particular"}</p>
<p class="text-xs font-bold text-slate-700 leading-tight">${cComp}</p>
</div>
</div>
</div>
`;
</div>`;
// ENSAMBLAJE DE LA TARJETA FINAL
let cardHtml = `
<div class="bg-white/80 backdrop-blur-xl border border-white shadow-xl shadow-slate-200/40 rounded-[2.5rem] p-6 mb-6 relative overflow-hidden text-left">
<div class="flex justify-between items-start mb-5">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">${srv.title}</span>
${hasWorker ?
`<div class="bg-slate-50 border border-slate-100 px-3 py-1.5 rounded-lg text-center shrink-0">
let workerNameBox = '';
if (hasWorker) {
let wNameShort = srv.assigned_worker.split(' ')[0];
workerNameBox = `<div class="bg-slate-50 border border-slate-100 px-3 py-1.5 rounded-lg text-center shrink-0">
<p class="text-[8px] uppercase font-black text-slate-400 mb-0.5">Técnico</p>
<p class="text-[10px] font-bold text-blue-600">${srv.assigned_worker.split(' ')[0]}</p>
</div>` : ''
<p class="text-[10px] font-bold text-blue-600">${wNameShort}</p>
</div>`;
}
let srvTitle = srv.title || "Aviso";
let cardHtml = `<div class="bg-white/80 backdrop-blur-xl border border-white shadow-xl shadow-slate-200/40 rounded-[2.5rem] p-6 mb-6 relative overflow-hidden text-left">
<div class="flex justify-between items-start mb-5">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">${srvTitle}</span>
${workerNameBox}
</div>
<div class="mb-5">${statusHtml}</div>
${contactHtml}
${clientDataHtml}
<div class="pt-5 mt-5 border-t border-slate-100">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Motivo de la Visita</p>
<h3 class="font-black text-slate-800 text-sm leading-relaxed">${descLimpia}</h3>
</div>
</div>
`;
</div>`;
if (isFinalized) {
historyContainer.innerHTML += cardHtml;
@@ -443,9 +432,6 @@
}, 300);
}
// ==========================================
// CÁLCULO DE ETA (TIEMPO ESTIMADO) INTELIGENTE
// ==========================================
async function calculateClientETA(serviceId, destAddress) {
const container = document.getElementById(`eta-container-${serviceId}`);
if (!container) return;
@@ -483,24 +469,20 @@
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const km = R * c;
// Tiempo total estimado basado en la distancia
const totalMins = Math.round((km/35)*60) + 5;
// Extraemos la hora exacta en la que el técnico pulsó el botón
let startedAt = new Date().getTime();
if (data.location.updated_at) {
const parsed = new Date(data.location.updated_at).getTime();
if (!isNaN(parsed)) startedAt = parsed;
}
// CREAMOS UNA FUNCIÓN INTERNA PARA ACTUALIZAR EN TIEMPO REAL
function renderETA() {
const now = new Date().getTime();
const diffMins = Math.floor((now - startedAt) / 60000);
// Restamos el tiempo que ya ha pasado conduciendo
let remainingMins = totalMins - diffMins;
if (remainingMins < 1) remainingMins = 1; // Nunca bajará de 1 minuto
if (remainingMins < 1) remainingMins = 1;
let progressPercent = (diffMins / totalMins) * 100;
if (progressPercent > 95) progressPercent = 95;
@@ -523,10 +505,7 @@
lucide.createIcons();
}
// 1. Lo pintamos inmediatamente al cargar
renderETA();
// 2. Lo actualizamos automáticamente cada 60 segundos (Magia en tiempo real)
setInterval(renderETA, 60000);
} else {