Actualizar index2.html

This commit is contained in:
2026-03-28 22:11:27 +00:00
parent 416825e21a
commit 3b4837a88f

View File

@@ -53,24 +53,32 @@
<main id="mainContent" class="hidden w-full max-w-lg mx-auto flex flex-col relative z-10">
<header class="pt-10 pb-8 px-6 bg-white rounded-b-[2.5rem] shadow-[0_10px_40px_rgba(0,0,0,0.03)] border-b border-slate-100 fade-in relative z-20">
<div class="flex justify-between items-start mb-6">
<div>
<p class="text-[9px] font-black text-blue-600 uppercase tracking-widest mb-1">Bienvenido/a</p>
<h1 id="clientName" class="text-3xl font-black tracking-tight text-slate-900 leading-none truncate pr-4">Cliente</h1>
<div class="flex justify-between items-center mb-6 gap-4">
<div class="flex-1 min-w-0">
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest mb-1">Bienvenido/a</p>
<h1 id="clientName" class="text-3xl font-black tracking-tight text-slate-900 leading-none truncate">Cliente</h1>
</div>
<div id="companyLogoContainer" class="hidden shrink-0 w-12 h-12 bg-white rounded-2xl shadow-sm border border-slate-100 p-1.5 overflow-hidden">
<div id="companyLogoContainer" class="hidden shrink-0 w-20 h-20 bg-white rounded-[1.5rem] shadow-sm border border-slate-100 p-2 overflow-hidden">
<img id="companyLogo" src="" class="w-full h-full object-contain">
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="bg-amber-50 rounded-[1.5rem] p-4 border border-amber-100 flex flex-col items-start justify-center shadow-sm">
<h3 class="text-3xl font-black text-amber-600 leading-none" id="countPendientes">0</h3>
<p class="text-[9px] font-black text-amber-600 uppercase tracking-widest mt-1.5 flex items-center gap-1"><i data-lucide="calendar-plus" class="w-3 h-3"></i> Pte. Citar</p>
</div>
<div class="bg-rose-50 rounded-[1.5rem] p-4 border border-rose-100 flex flex-col items-start justify-center shadow-sm">
<h3 class="text-3xl font-black text-rose-600 leading-none" id="countIncidencias">0</h3>
<p class="text-[9px] font-black text-rose-600 uppercase tracking-widest mt-1.5 flex items-center gap-1"><i data-lucide="alert-triangle" class="w-3 h-3"></i> Incidencias</p>
</div>
<div class="bg-slate-50 rounded-[1.5rem] p-4 border border-slate-100 flex flex-col items-start justify-center">
<h3 class="text-3xl font-black text-slate-800 leading-none" id="countActive">0</h3>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-1.5 flex items-center gap-1"><i data-lucide="activity" class="w-3 h-3 text-blue-500"></i> En Proceso</p>
<p class="text-[9px] font-black text-slate-500 uppercase tracking-widest mt-1.5 flex items-center gap-1"><i data-lucide="activity" class="w-3 h-3 text-blue-500"></i> En Proceso</p>
</div>
<div class="bg-slate-50 rounded-[1.5rem] p-4 border border-slate-100 flex flex-col items-start justify-center">
<h3 class="text-3xl font-black text-slate-800 leading-none" id="countHistory">0</h3>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-1.5 flex items-center gap-1"><i data-lucide="archive" class="w-3 h-3 text-emerald-500"></i> Finalizados</p>
<p class="text-[9px] font-black text-slate-500 uppercase tracking-widest mt-1.5 flex items-center gap-1"><i data-lucide="archive" class="w-3 h-3 text-emerald-500"></i> Finalizados</p>
</div>
</div>
</header>
@@ -163,7 +171,7 @@
let urlToken = "";
let etasToInit = [];
let currentQuotes = []; // Lista global de presupuestos
let currentQuotes = [];
document.addEventListener("DOMContentLoaded", async () => {
lucide.createIcons();
@@ -174,9 +182,8 @@
if (!urlToken) { showError(); return; }
try {
// Obtenemos SIEMPRE todos los servicios de ese cliente, aunque haya '?service=XX' en la URL
let fetchUrl = `${API_URL}/public/portal/${urlToken}`;
if (serviceParam) fetchUrl += `?service=${serviceParam}`;
const res = await fetch(fetchUrl);
const data = await res.json();
@@ -184,8 +191,6 @@
const servicesList = data.services || [];
// MOCK DE PRESUPUESTOS (Para que veas la funcionalidad nueva)
// Si en el futuro tu API devuelve data.quotes, usará eso. Si no, metemos uno de prueba.
currentQuotes = data.quotes || [
{ id: 101, ref: 'PRE-2026-089', title: 'Cambio de tubería principal y alicatado', amount: '345.50', date: '28/03/2026' }
];
@@ -193,6 +198,18 @@
renderPortal(data.client, data.company, servicesList);
renderQuotes();
// Si viene un service en la URL, podríamos hacer scroll hacia él aquí
if (serviceParam) {
setTimeout(() => {
const targetCard = document.getElementById(`service-card-${serviceParam}`);
if (targetCard) {
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetCard.classList.add('ring-2', 'ring-blue-400', 'ring-offset-4');
setTimeout(() => targetCard.classList.remove('ring-2', 'ring-blue-400', 'ring-offset-4'), 3000);
}
}, 500);
}
} catch (e) {
console.error("Error cargando portal:", e);
showError();
@@ -208,13 +225,11 @@
}, 300);
}
// --- SISTEMA DE PESTAÑAS (NAVEGACIÓN INFERIOR) ---
// --- SISTEMA DE PESTAÑAS ---
function switchTab(tabName) {
// Ocultar todo
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
// Mostrar el activo
document.getElementById(`tab${tabName}`).classList.add('active');
document.getElementById(`btnNav${tabName}`).classList.add('active');
}
@@ -284,17 +299,14 @@
const q = currentQuotes.find(x => x.id === id);
if (!q) return;
// Marcamos como visto
localStorage.setItem(`quote_viewed_${id}`, 'true');
renderQuotes(); // Refrescamos lista para quitar globos rojos
renderQuotes();
// Rellenamos Modal
document.getElementById('qmRef').innerText = q.ref;
document.getElementById('qmTitle').innerText = q.title;
document.getElementById('qmDate').innerText = q.date;
document.getElementById('qmAmount').innerText = q.amount + "€";
// Mostramos Modal
const modal = document.getElementById('quoteModal');
const sheet = document.getElementById('quoteModalSheet');
modal.classList.remove('hidden');
@@ -347,7 +359,6 @@
// --- RENDER PRINCIPAL DE AVISOS ---
function renderPortal(client, company, allServices) {
// Textos de Cabecera
if (company.name) document.title = `Portal - ${company.name}`;
if (company.logo) {
document.getElementById('companyLogo').src = company.logo;
@@ -357,7 +368,6 @@
let cName = client && client.name ? client.name.split(' ')[0] : "Cliente";
document.getElementById('clientName').innerText = cName;
// Contenedores
const activeContainer = document.getElementById('activeServicesContainer');
const historyContainerWrapper = document.getElementById('historyContainerWrapper');
const historyContainer = document.getElementById('historyServicesContainer');
@@ -365,8 +375,11 @@
activeContainer.innerHTML = '';
historyContainer.innerHTML = '';
// Contadores
let countAct = 0;
let countHist = 0;
let countInc = 0;
let countPend = 0;
allServices.forEach(srv => {
let isFinalized = srv.is_final === true;
@@ -377,7 +390,27 @@
let hasWorker = (srv.assigned_worker && srv.assigned_worker !== 'Pendiente' && srv.assigned_worker !== 'Sin asignar');
let isUrgent = (srv.title && srv.title.includes('URGENTE')) || srv.is_urgent === true || (raw['Urgente'] && (raw['Urgente'].toLowerCase() === 'sí' || raw['Urgente'].toLowerCase() === 'si' || raw['Urgente'].toLowerCase() === 'true'));
// DISEÑO DE ESTADOS EN TARJETA BLANCA (NUEVO)
// Recopilar dirección limpia para mostrar
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 fullAddress = `${cAddr}, ${cCP} ${cPop}`.trim();
if(fullAddress === ",") fullAddress = "No especificada";
// DIRECCIÓN HTML (Restaurado a petición)
let addressHtml = `
<div class="mt-4 bg-slate-50/70 rounded-2xl p-4 border border-slate-100 flex items-start gap-3">
<div class="bg-white p-2 rounded-xl border border-slate-200 shadow-sm shrink-0 text-slate-400">
<i data-lucide="map-pin" class="w-4 h-4"></i>
</div>
<div>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Lugar de reparación</p>
<p class="text-xs font-bold text-slate-700 leading-snug">${fullAddress}</p>
</div>
</div>
`;
// DISEÑO DE ESTADOS EN TARJETA BLANCA
let headerColor = "bg-slate-50 text-slate-500";
let icon = "clock";
let tagTitle = "Estado";
@@ -390,8 +423,7 @@
}
else if (stNameLower.includes('camino')) {
headerColor = "bg-indigo-100 text-indigo-600"; icon = "truck"; tagTitle = "Desplazamiento"; mainTitle = "¡En camino!"; subDesc = "El técnico se dirige a tu domicilio.";
let fullAddr = `${raw["Dirección"] || ""}, ${raw["Código Postal"] || ""} ${raw["Población"] || ""}`;
etasToInit.push({ id: srv.id, address: fullAddr });
etasToInit.push({ id: srv.id, address: fullAddress });
extras = `<div id="eta-container-${srv.id}" class="mt-3 bg-indigo-50 rounded-xl p-3 border border-indigo-100"><p class="text-[10px] font-bold text-indigo-500 flex items-center gap-1.5"><i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i> Calculando ruta...</p></div>`;
}
else if (stNameLower.includes('trabajando') || stNameLower.includes('reparación')) {
@@ -420,26 +452,35 @@
if (isLate) {
headerColor = "bg-amber-100 text-amber-600"; icon = "clock-4"; tagTitle = "Demora"; mainTitle = "Técnico Retrasado"; subDesc = `La cita estaba prevista hasta las ${endT}. Llegará lo antes posible.`;
extras = `<a href="cita.html?token=${urlToken}&service=${srv.id}" class="mt-3 block text-center w-full bg-white border border-amber-200 text-amber-700 hover:bg-amber-50 font-black py-2.5 rounded-xl text-[10px] uppercase tracking-widest shadow-sm">Modificar Cita</a>`;
extras = `<a href="cita.html?token=${urlToken}&service=${srv.id}" class="mt-3 block text-center w-full bg-white border border-amber-200 text-amber-700 hover:bg-amber-50 font-black py-2.5 rounded-xl text-[10px] uppercase tracking-widest shadow-sm transition-colors">Modificar Cita</a>`;
} else {
headerColor = "bg-emerald-100 text-emerald-600"; icon = "calendar-check"; tagTitle = "Confirmada"; mainTitle = `${formatDate(srv.scheduled_date)}`; subDesc = `El técnico llegará entre las <b>${srv.scheduled_time}</b> y las <b>${endT}</b>.`;
extras = `<a href="cita.html?token=${urlToken}&service=${srv.id}" class="mt-3 block text-center w-full bg-slate-50 border border-slate-200 text-slate-600 hover:bg-slate-100 font-black py-2.5 rounded-xl text-[10px] uppercase tracking-widest shadow-sm">Gestionar Cita</a>`;
extras = `<a href="cita.html?token=${urlToken}&service=${srv.id}" class="mt-3 block text-center w-full bg-slate-50 border border-slate-200 text-slate-600 hover:bg-slate-100 font-black py-2.5 rounded-xl text-[10px] uppercase tracking-widest shadow-sm transition-colors">Gestionar Cita</a>`;
}
}
else if (isUrgent) {
headerColor = "bg-red-100 text-red-600"; icon = "flame"; tagTitle = "Urgencia"; mainTitle = "Prioridad Máxima"; subDesc = hasWorker ? "Técnico asignado de urgencia en camino." : "Buscando técnico de emergencia.";
}
else if (stNameLower.includes('esperando') || stNameLower.includes('asignado') || (hasWorker && !hasDate)) {
headerColor = "bg-blue-100 text-blue-600"; icon = "calendar-plus"; tagTitle = "Acción Requerida"; mainTitle = "Elige tu cita"; subDesc = "Técnico asignado. Selecciona cuándo quieres que vayamos.";
headerColor = "bg-amber-100 text-amber-600"; icon = "calendar-plus"; tagTitle = "Acción Requerida"; mainTitle = "Elige tu cita"; subDesc = "Técnico asignado. Selecciona cuándo quieres que vayamos.";
extras = `<a href="cita.html?token=${urlToken}&service=${srv.id}" class="mt-3 block text-center w-full bg-blue-600 text-white font-black py-3 rounded-xl text-xs uppercase tracking-widest shadow-md">Agendar Cita Ahora</a>`;
}
else if (stNameLower.includes('desasignado')) {
headerColor = "bg-slate-100 text-slate-500"; icon = "user-x"; tagTitle = "Reorganizando"; mainTitle = "Sin Técnico"; subDesc = "Buscando un nuevo técnico disponible para tu zona.";
}
// Contadores para el Dashboard superior
if (isFinalized) {
countHist++;
} else {
countAct++;
if (stNameLower.includes('incidencia')) countInc++;
if (stNameLower.includes('esperando') || stNameLower.includes('asignado') || (hasWorker && !hasDate)) countPend++;
}
// Generación de la tarjeta Blanca
let cardHtml = `
<div class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 relative text-left">
<div id="service-card-${srv.id}" class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 relative text-left transition-all duration-500">
<div class="flex items-start gap-4 mb-5">
<div class="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0 ${headerColor}">
@@ -458,6 +499,8 @@
<p class="text-[10px] font-black text-slate-300 uppercase tracking-widest mb-3">Detalle de Avería</p>
<p class="text-sm font-bold text-slate-700 leading-relaxed">${descLimpia}</p>
${addressHtml}
${hasWorker && !isFinalized ? `
<div class="flex gap-2 mt-5">
${srv.worker_phone ? `<a href="tel:+${srv.worker_phone.replace('+','')}" class="flex-1 bg-slate-50 border border-slate-200 text-slate-600 font-black py-2.5 rounded-xl flex items-center justify-center gap-1.5 text-[10px] uppercase tracking-widest"><i data-lucide="phone" class="w-3.5 h-3.5"></i> Llamar</a>
@@ -467,18 +510,15 @@
</div>
`;
if (isFinalized) {
historyContainer.innerHTML += cardHtml;
countHist++;
} else {
activeContainer.innerHTML += cardHtml;
countAct++;
}
if (isFinalized) historyContainer.innerHTML += cardHtml;
else activeContainer.innerHTML += cardHtml;
});
// Actualizar contadores cabecera
document.getElementById('countActive').innerText = countAct;
document.getElementById('countHistory').innerText = countHist;
document.getElementById('countIncidencias').innerText = countInc;
document.getElementById('countPendientes').innerText = countPend;
if (countAct === 0) document.getElementById('noActiveServices').classList.remove('hidden');
else document.getElementById('noActiveServices').classList.add('hidden');
@@ -499,7 +539,7 @@
}, 300);
}
// --- SISTEMA GPS (Igual que el anterior) ---
// --- SISTEMA GPS ---
async function calculateClientETA(serviceId, destAddress) {
const container = document.getElementById(`eta-container-${serviceId}`);
if (!container) return;