Añadir index2.html
This commit is contained in:
588
index2.html
Normal file
588
index2.html
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<title>Portal del Cliente - IntegraRepara</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root { --app-bg: #f8fafc; }
|
||||||
|
body { background-color: var(--app-bg); -webkit-tap-highlight-color: transparent; }
|
||||||
|
|
||||||
|
/* Animaciones fluidas */
|
||||||
|
.fade-in { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; opacity: 0; }
|
||||||
|
.fade-in-delay-1 { animation-delay: 0.1s; }
|
||||||
|
.fade-in-delay-2 { animation-delay: 0.2s; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
|
||||||
|
/* Efecto Glassmorphism */
|
||||||
|
.glass-nav { background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }
|
||||||
|
|
||||||
|
/* Estilos de pestañas */
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; animation: fadeIn 0.3s ease forwards; }
|
||||||
|
|
||||||
|
.nav-btn { color: #94a3b8; transition: all 0.2s; }
|
||||||
|
.nav-btn.active { color: #2563eb; }
|
||||||
|
.nav-btn.active i { transform: translateY(-2px); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="text-slate-800 font-sans antialiased min-h-screen flex flex-col relative overflow-x-hidden pb-24">
|
||||||
|
|
||||||
|
<div id="loader" class="fixed inset-0 bg-white z-[100] flex flex-col items-center justify-center transition-opacity duration-500">
|
||||||
|
<div class="relative w-16 h-16 flex items-center justify-center mb-4">
|
||||||
|
<div class="absolute inset-0 border-4 border-slate-100 border-t-blue-600 rounded-full animate-spin"></div>
|
||||||
|
<i data-lucide="home" class="w-6 h-6 text-blue-600 animate-pulse"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs font-black tracking-widest uppercase text-slate-400">Preparando tu portal...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorScreen" class="hidden w-full max-w-md mx-auto p-6 flex-col items-center justify-center min-h-screen text-center z-10 relative">
|
||||||
|
<div class="w-20 h-20 bg-rose-50 text-rose-500 rounded-[2rem] flex items-center justify-center mb-6 shadow-sm border border-rose-100 rotate-12">
|
||||||
|
<i data-lucide="shield-alert" class="w-10 h-10 -rotate-12"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-black text-slate-800 mb-2 tracking-tight">Acceso Caducado</h2>
|
||||||
|
<p class="text-sm text-slate-500 font-medium leading-relaxed px-4">Por seguridad, los enlaces tienen un tiempo límite. Por favor, solicita uno nuevo a tu técnico o gestor.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<img id="companyLogo" src="" class="w-full h-full object-contain">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="w-full relative z-10">
|
||||||
|
|
||||||
|
<div id="tabAvisos" class="tab-content active px-5 pt-6 pb-6">
|
||||||
|
|
||||||
|
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4 ml-2 fade-in fade-in-delay-1">Tus Reparaciones</h2>
|
||||||
|
|
||||||
|
<div id="activeServicesContainer" class="space-y-5 fade-in fade-in-delay-1"></div>
|
||||||
|
|
||||||
|
<div id="noActiveServices" class="hidden text-center p-8 bg-white rounded-[2.5rem] shadow-sm border border-slate-100 mt-2">
|
||||||
|
<div class="w-16 h-16 bg-emerald-50 text-emerald-500 rounded-full flex items-center justify-center mx-auto mb-4"><i data-lucide="check-circle-2" class="w-8 h-8"></i></div>
|
||||||
|
<h3 class="text-lg font-black text-slate-800 tracking-tight">Todo perfecto</h3>
|
||||||
|
<p class="text-xs text-slate-400 font-medium mt-1">No tienes reparaciones activas en este momento.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="historyContainerWrapper" class="hidden mt-8 fade-in fade-in-delay-2">
|
||||||
|
<div class="flex items-center gap-3 mb-4 ml-2">
|
||||||
|
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest">Historial</h3>
|
||||||
|
<div class="h-px flex-1 bg-slate-200"></div>
|
||||||
|
</div>
|
||||||
|
<div id="historyServicesContainer" class="space-y-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tabPresupuestos" class="tab-content px-5 pt-6 pb-6">
|
||||||
|
<div class="flex justify-between items-center mb-4 ml-2 mr-2">
|
||||||
|
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest">Tus Presupuestos</h2>
|
||||||
|
<span id="badgeQuotesCount" class="text-[9px] font-black bg-blue-100 text-blue-600 px-2 py-0.5 rounded-md hidden">0 NUEVOS</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="quotesContainer" class="space-y-4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="fixed bottom-0 w-full glass-nav border-t border-slate-200 pb-safe pt-2 z-50 transition-transform duration-300 translate-y-full" id="bottomNav">
|
||||||
|
<div class="flex justify-around items-center max-w-lg mx-auto pb-3 pt-1">
|
||||||
|
<button id="btnNavAvisos" onclick="switchTab('Avisos')" class="nav-btn active flex flex-col items-center gap-1 w-24 relative">
|
||||||
|
<i data-lucide="wrench" class="w-6 h-6"></i>
|
||||||
|
<span class="text-[10px] font-black uppercase tracking-widest">Avisos</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="btnNavPresupuestos" onclick="switchTab('Presupuestos')" class="nav-btn flex flex-col items-center gap-1 w-24 relative">
|
||||||
|
<div class="relative">
|
||||||
|
<i data-lucide="file-text" class="w-6 h-6"></i>
|
||||||
|
<span id="badgeNavPresupuestos" class="absolute -top-1 -right-1.5 w-3 h-3 bg-red-500 border-2 border-white rounded-full hidden animate-pulse"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-black uppercase tracking-widest">Presup.</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="quoteModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[100] hidden flex-col justify-end">
|
||||||
|
<div class="bg-white w-full rounded-t-[2.5rem] p-6 pt-8 pb-10 transition-transform transform translate-y-full duration-300" id="quoteModalSheet">
|
||||||
|
<div class="flex justify-between items-start mb-6">
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-100 px-2 py-1 rounded-md" id="qmRef">REF</span>
|
||||||
|
<h2 class="text-xl font-black text-slate-800 mt-2 leading-tight" id="qmTitle">Título</h2>
|
||||||
|
</div>
|
||||||
|
<button onclick="closeQuoteModal()" class="bg-slate-100 p-2.5 rounded-full text-slate-600 active:scale-90 transition-transform"><i data-lucide="x" class="w-5 h-5"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-50 border border-slate-100 rounded-2xl p-5 mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<span class="text-xs font-bold text-slate-500">Fecha de emisión</span>
|
||||||
|
<span class="text-xs font-black text-slate-800" id="qmDate">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center border-t border-slate-200 pt-3">
|
||||||
|
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">Total Estimado</span>
|
||||||
|
<span class="text-3xl font-black text-blue-600" id="qmAmount">0.00€</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="w-full bg-blue-600 text-white font-black py-4 rounded-2xl shadow-xl flex items-center justify-center gap-2 uppercase tracking-widest text-xs active:scale-95 transition-transform">
|
||||||
|
<i data-lucide="download" class="w-4 h-4"></i> Descargar PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://integrarepara-api.integrarepara.es';
|
||||||
|
|
||||||
|
let urlToken = "";
|
||||||
|
let etasToInit = [];
|
||||||
|
let currentQuotes = []; // Lista global de presupuestos
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
lucide.createIcons();
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlToken = urlParams.get('token');
|
||||||
|
const serviceParam = urlParams.get('service');
|
||||||
|
|
||||||
|
if (!urlToken) { showError(); return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
let fetchUrl = `${API_URL}/public/portal/${urlToken}`;
|
||||||
|
if (serviceParam) fetchUrl += `?service=${serviceParam}`;
|
||||||
|
|
||||||
|
const res = await fetch(fetchUrl);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.ok) throw new Error("Token inválido");
|
||||||
|
|
||||||
|
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' }
|
||||||
|
];
|
||||||
|
|
||||||
|
renderPortal(data.client, data.company, servicesList);
|
||||||
|
renderQuotes();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error cargando portal:", e);
|
||||||
|
showError();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showError() {
|
||||||
|
document.getElementById('loader').classList.add('opacity-0', 'pointer-events-none');
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('loader').classList.add('hidden');
|
||||||
|
document.getElementById('errorScreen').classList.remove('hidden');
|
||||||
|
document.getElementById('errorScreen').classList.add('flex');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SISTEMA DE PESTAÑAS (NAVEGACIÓN INFERIOR) ---
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTIÓN DE PRESUPUESTOS ---
|
||||||
|
function renderQuotes() {
|
||||||
|
const container = document.getElementById('quotesContainer');
|
||||||
|
let unseenCount = 0;
|
||||||
|
|
||||||
|
if (currentQuotes.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-center p-10 bg-white rounded-[2rem] border border-slate-100 shadow-sm">
|
||||||
|
<div class="w-16 h-16 bg-slate-50 text-slate-300 rounded-full flex items-center justify-center mx-auto mb-3"><i data-lucide="file-x" class="w-8 h-8"></i></div>
|
||||||
|
<h3 class="text-base font-black text-slate-800">Sin Presupuestos</h3>
|
||||||
|
<p class="text-xs text-slate-400 font-medium mt-1">No tienes presupuestos pendientes de revisar.</p>
|
||||||
|
</div>`;
|
||||||
|
updateQuotesBadges(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
currentQuotes.forEach(q => {
|
||||||
|
const isViewed = localStorage.getItem(`quote_viewed_${q.id}`) === 'true';
|
||||||
|
if (!isViewed) unseenCount++;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div onclick="openQuoteModal(${q.id})" class="bg-white p-5 rounded-[2rem] shadow-sm border ${isViewed ? 'border-slate-100' : 'border-blue-400 ring-2 ring-blue-50'} relative cursor-pointer active:scale-95 transition-all text-left">
|
||||||
|
${!isViewed ? '<div class="absolute top-5 right-5 w-3 h-3 bg-red-500 rounded-full animate-pulse shadow-sm border-2 border-white"></div>' : ''}
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<div class="w-10 h-10 rounded-xl ${isViewed ? 'bg-slate-50 text-slate-400' : 'bg-blue-50 text-blue-600'} flex items-center justify-center shrink-0">
|
||||||
|
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest leading-none mb-1">REF #${q.ref}</p>
|
||||||
|
<h3 class="font-black text-slate-800 text-sm leading-tight pr-6 truncate w-[200px]">${q.title}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-end border-t border-slate-100 pt-3 mt-1">
|
||||||
|
<span class="text-[10px] font-bold text-slate-500 flex items-center gap-1"><i data-lucide="calendar" class="w-3 h-3"></i> ${q.date}</span>
|
||||||
|
<span class="text-lg font-black text-slate-800">${q.amount}€</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
updateQuotesBadges(unseenCount);
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuotesBadges(count) {
|
||||||
|
const badgeNav = document.getElementById('badgeNavPresupuestos');
|
||||||
|
const badgeTab = document.getElementById('badgeQuotesCount');
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
badgeNav.classList.remove('hidden');
|
||||||
|
badgeTab.classList.remove('hidden');
|
||||||
|
badgeTab.innerText = `${count} NUEVO${count > 1 ? 'S' : ''}`;
|
||||||
|
} else {
|
||||||
|
badgeNav.classList.add('hidden');
|
||||||
|
badgeTab.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQuoteModal(id) {
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
setTimeout(() => sheet.classList.remove('translate-y-full'), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeQuoteModal() {
|
||||||
|
const modal = document.getElementById('quoteModal');
|
||||||
|
const sheet = document.getElementById('quoteModalSheet');
|
||||||
|
sheet.classList.add('translate-y-full');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FUNCIONES DE TEXTO Y FECHAS ---
|
||||||
|
function summarizeDescription(rawText) {
|
||||||
|
if (!rawText) return "Revisión técnica de avería en el domicilio.";
|
||||||
|
let text = String(rawText).replace(/(\r\n|\n|\r)/gm, " ");
|
||||||
|
const regexCorte = /(\bM\.O\b|\bMATERIAL\b|\bASEGURADO\b|\bTELEFONO\b|\bTEL\b|\bURGENTE\b|\bFRANQUICIA\b|\[PROPUESTA)/i;
|
||||||
|
let cutPos = text.search(regexCorte);
|
||||||
|
if (cutPos > 15) text = text.substring(0, cutPos);
|
||||||
|
text = text.replace(/^(Aver[ií]a|Descripci[oó]n|Motivo|Siniestro|Detalle)[\s:]*/i, '').trim();
|
||||||
|
if (text.length > 0) text = text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
let words = text.split(/\s+/);
|
||||||
|
if (words.length > 20) text = words.slice(0, 20).join(" ") + "...";
|
||||||
|
if (text.length < 5) return "Revisión general en el domicilio.";
|
||||||
|
return text.endsWith('.') || text.endsWith('...') ? text : text + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOneHour(timeStr) {
|
||||||
|
if(!timeStr) return "";
|
||||||
|
let [h, m] = timeStr.split(':').map(Number);
|
||||||
|
let tm = h * 60 + m + 60;
|
||||||
|
return `${String(Math.floor(tm / 60)).padStart(2,'0')}:${String(tm % 60).padStart(2,'0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return "";
|
||||||
|
try {
|
||||||
|
const parts = dateStr.split('-');
|
||||||
|
if(parts.length !== 3) return dateStr;
|
||||||
|
const d = new Date(parts[0], parts[1]-1, parts[2]);
|
||||||
|
return d.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'short' });
|
||||||
|
} catch(e) { return dateStr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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;
|
||||||
|
document.getElementById('companyLogoContainer').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
activeContainer.innerHTML = '';
|
||||||
|
historyContainer.innerHTML = '';
|
||||||
|
|
||||||
|
let countAct = 0;
|
||||||
|
let countHist = 0;
|
||||||
|
|
||||||
|
allServices.forEach(srv => {
|
||||||
|
let isFinalized = srv.is_final === true;
|
||||||
|
let raw = srv.raw_data || {};
|
||||||
|
let descLimpia = summarizeDescription(srv.description);
|
||||||
|
let stNameLower = (srv.status_name || "").toLowerCase();
|
||||||
|
let hasDate = (srv.scheduled_date && srv.scheduled_time);
|
||||||
|
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)
|
||||||
|
let headerColor = "bg-slate-50 text-slate-500";
|
||||||
|
let icon = "clock";
|
||||||
|
let tagTitle = "Estado";
|
||||||
|
let mainTitle = srv.status_name || "En Proceso";
|
||||||
|
let subDesc = "Buscando información...";
|
||||||
|
let extras = "";
|
||||||
|
|
||||||
|
if (isFinalized || stNameLower.includes('finalizado') || stNameLower.includes('anulado')) {
|
||||||
|
headerColor = "bg-slate-100 text-slate-400"; icon = "archive"; tagTitle = "Terminado"; mainTitle = "Archivo"; subDesc = "Este servicio ya ha sido concluido o archivado.";
|
||||||
|
}
|
||||||
|
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 });
|
||||||
|
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')) {
|
||||||
|
headerColor = "bg-orange-100 text-orange-600"; icon = "hammer"; tagTitle = "En Domicilio"; mainTitle = "Trabajando"; subDesc = "El técnico está realizando la reparación.";
|
||||||
|
}
|
||||||
|
else if (stNameLower.includes('incidencia')) {
|
||||||
|
headerColor = "bg-rose-100 text-rose-600"; icon = "alert-triangle"; tagTitle = "Atención"; mainTitle = "En Incidencia"; subDesc = "Ha surgido un contratiempo técnico que estamos gestionando.";
|
||||||
|
}
|
||||||
|
else if (stNameLower.includes('compañ') || stNameLower.includes('perito')) {
|
||||||
|
headerColor = "bg-slate-100 text-slate-600"; icon = "building"; tagTitle = "Trámite"; mainTitle = "Espera de Compañía"; subDesc = "A la espera de autorización por parte de tu aseguradora.";
|
||||||
|
}
|
||||||
|
else if (raw.appointment_status === 'pending' && raw.requested_date) {
|
||||||
|
headerColor = "bg-purple-100 text-purple-600"; icon = "hourglass"; tagTitle = "Revisión"; mainTitle = "Cita Solicitada"; subDesc = `Has solicitado cita para el <b>${formatDate(raw.requested_date)}</b>. Esperando confirmación.`;
|
||||||
|
}
|
||||||
|
else if (hasDate && !stNameLower.includes('anulado') && !stNameLower.includes('desasignado')) {
|
||||||
|
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) {
|
||||||
|
let limitDate = new Date(schedParts[0], schedParts[1] - 1, schedParts[2], endTimeParts[0], endTimeParts[1], 0);
|
||||||
|
if (now > limitDate) isLate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
} 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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.";
|
||||||
|
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.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 class="flex items-start gap-4 mb-5">
|
||||||
|
<div class="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0 ${headerColor}">
|
||||||
|
<i data-lucide="${icon}" class="w-6 h-6"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 pt-0.5">
|
||||||
|
<p class="text-[9px] font-black uppercase tracking-widest text-slate-400 mb-0.5">${tagTitle}</p>
|
||||||
|
<h4 class="font-black text-slate-800 text-lg leading-tight truncate">${mainTitle}</h4>
|
||||||
|
<p class="text-xs font-medium text-slate-500 mt-1 leading-snug">${subDesc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${extras}
|
||||||
|
|
||||||
|
<hr class="border-slate-100 my-5">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
${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>
|
||||||
|
<a href="https://wa.me/${srv.worker_phone.replace('+','')}" target="_blank" class="flex-1 bg-emerald-50 border border-emerald-200 text-emerald-600 font-black py-2.5 rounded-xl flex items-center justify-center gap-1.5 text-[10px] uppercase tracking-widest"><i data-lucide="message-circle" class="w-3.5 h-3.5"></i> WhatsApp</a>` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isFinalized) {
|
||||||
|
historyContainer.innerHTML += cardHtml;
|
||||||
|
countHist++;
|
||||||
|
} else {
|
||||||
|
activeContainer.innerHTML += cardHtml;
|
||||||
|
countAct++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actualizar contadores cabecera
|
||||||
|
document.getElementById('countActive').innerText = countAct;
|
||||||
|
document.getElementById('countHistory').innerText = countHist;
|
||||||
|
|
||||||
|
if (countAct === 0) document.getElementById('noActiveServices').classList.remove('hidden');
|
||||||
|
else document.getElementById('noActiveServices').classList.add('hidden');
|
||||||
|
|
||||||
|
if (countHist > 0) document.getElementById('historyContainerWrapper').classList.remove('hidden');
|
||||||
|
else document.getElementById('historyContainerWrapper').classList.add('hidden');
|
||||||
|
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
// Quitar Loader, mostrar App y Menú inferior
|
||||||
|
document.getElementById('loader').classList.add('opacity-0', 'pointer-events-none');
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('loader').classList.add('hidden');
|
||||||
|
document.getElementById('mainContent').classList.remove('hidden');
|
||||||
|
document.getElementById('bottomNav').classList.remove('translate-y-full');
|
||||||
|
|
||||||
|
etasToInit.forEach(item => calculateClientETA(item.id, item.address));
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SISTEMA GPS (Igual que el anterior) ---
|
||||||
|
async function calculateClientETA(serviceId, destAddress) {
|
||||||
|
const container = document.getElementById(`eta-container-${serviceId}`);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/public/portal/${urlToken}/location/${serviceId}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.ok || !data.location) {
|
||||||
|
container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está en camino hacia tu domicilio.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wLat = parseFloat(data.location.lat);
|
||||||
|
const wLon = parseFloat(data.location.lng);
|
||||||
|
|
||||||
|
let geoRes = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(destAddress + ', España')}`);
|
||||||
|
let geoData = await geoRes.json();
|
||||||
|
|
||||||
|
if (!geoData || geoData.length === 0) {
|
||||||
|
const parts = destAddress.split(',');
|
||||||
|
const fallbackDest = parts.length > 1 ? parts[parts.length - 1].trim() : destAddress;
|
||||||
|
geoRes = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(fallbackDest + ', España')}`);
|
||||||
|
geoData = await geoRes.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geoData && geoData.length > 0) {
|
||||||
|
const cLat = parseFloat(geoData[0].lat);
|
||||||
|
const cLon = parseFloat(geoData[0].lon);
|
||||||
|
|
||||||
|
const R = 6371;
|
||||||
|
const dLat = (cLat - wLat) * Math.PI / 180;
|
||||||
|
const dLon = (cLon - wLon) * Math.PI / 180;
|
||||||
|
const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(wLat * Math.PI / 180) * Math.cos(cLat * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||||
|
const km = R * c;
|
||||||
|
|
||||||
|
const totalMins = Math.round((km/35)*60) + 5;
|
||||||
|
|
||||||
|
let startedAt = new Date().getTime();
|
||||||
|
if (data.location.updated_at) {
|
||||||
|
const parsed = new Date(data.location.updated_at).getTime();
|
||||||
|
if (!isNaN(parsed)) startedAt = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderETA() {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const diffMins = Math.floor((now - startedAt) / 60000);
|
||||||
|
|
||||||
|
let remainingMins = totalMins - diffMins;
|
||||||
|
if (remainingMins < 1) remainingMins = 1;
|
||||||
|
|
||||||
|
let progressPercent = (diffMins / totalMins) * 100;
|
||||||
|
if (progressPercent > 95) progressPercent = 95;
|
||||||
|
if (progressPercent < 5) progressPercent = 5;
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<p class="text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
||||||
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i> Llegada en aprox. <span class="text-indigo-700 text-sm ml-0.5">${remainingMins} min</span>
|
||||||
|
</p>
|
||||||
|
<div class="w-full bg-white rounded-full h-2 overflow-hidden shadow-inner border border-indigo-100">
|
||||||
|
<div class="bg-indigo-500 h-full rounded-full transition-all duration-1000 relative" style="width: ${progressPercent}%">
|
||||||
|
<div class="absolute right-0 top-0 bottom-0 w-4 bg-white/40 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center mt-1.5 px-1">
|
||||||
|
<p class="text-[8px] font-black uppercase text-indigo-300">Saliendo</p>
|
||||||
|
<p class="text-[8px] font-black uppercase text-indigo-400">A ${km.toFixed(1)} km</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderETA();
|
||||||
|
setInterval(renderETA, 60000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está de camino.</p>`;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
if (container) container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está de camino.</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user