Files
Portal/index2.html
2026-03-29 14:31:09 +00:00

1212 lines
74 KiB
HTML

<!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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></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-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-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-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-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>
<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">
<button onclick="toggleHistory()" class="w-full bg-white border border-slate-200 p-5 rounded-[2rem] flex justify-between items-center shadow-sm active:scale-95 transition-all">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-slate-50 text-slate-400 rounded-xl flex items-center justify-center border border-slate-100"><i data-lucide="archive" class="w-5 h-5"></i></div>
<div class="text-left">
<span class="block text-xs font-black text-slate-700 uppercase tracking-widest">Finalizados</span>
<span class="text-[10px] font-bold text-slate-400" id="labelHistoryCount">0 servicios finalizados</span>
</div>
</div>
<i data-lucide="chevron-down" id="historyChevron" class="w-5 h-5 text-slate-300 transition-transform duration-300"></i>
</button>
<div id="historyServicesContainer" class="hidden space-y-4 mt-4 pb-4 transition-all"></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 max-h-[90vh] overflow-y-auto no-scrollbar" 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="border-t border-slate-200 pt-3 mt-3 mb-3">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">Desglose de Conceptos</p>
<ul id="qmItemsList" class="space-y-2 text-xs font-medium text-slate-600">
</ul>
</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 Presupuestado</span>
<span class="text-3xl font-black text-blue-600" id="qmAmount">0.00€</span>
</div>
</div>
<div id="qmActionsContainer" class="mb-4">
<div id="qmDecisionButtons" class="flex gap-3">
<button onclick="openSignatureArea()" class="flex-1 bg-emerald-50 text-emerald-600 border border-emerald-200 font-black py-4 rounded-2xl flex flex-col items-center justify-center gap-1 uppercase tracking-widest text-[10px] active:scale-95 transition-all hover:bg-emerald-500 hover:text-white">
<i data-lucide="thumbs-up" class="w-5 h-5"></i> Aceptar
</button>
<button onclick="rejectBudget()" class="flex-1 bg-rose-50 text-rose-600 border border-rose-200 font-black py-4 rounded-2xl flex flex-col items-center justify-center gap-1 uppercase tracking-widest text-[10px] active:scale-95 transition-all hover:bg-rose-500 hover:text-white">
<i data-lucide="thumbs-down" class="w-5 h-5"></i> Rechazar
</button>
</div>
<div id="qmStatusMessage" class="hidden text-center p-4 rounded-xl font-black text-xs uppercase tracking-widest"></div>
</div>
<div id="qmSignatureArea" class="hidden mb-6 bg-slate-50 p-4 rounded-2xl border border-slate-200">
<p class="text-[10px] font-black text-emerald-600 uppercase tracking-widest mb-2 text-center"><i data-lucide="pen-tool" class="w-3 h-3 inline"></i> Dibuja tu firma para confirmar</p>
<div class="border-2 border-dashed border-slate-300 bg-white rounded-xl overflow-hidden mb-3">
<canvas id="signatureCanvas" class="w-full h-32 touch-none"></canvas>
</div>
<div class="flex gap-2">
<button onclick="clearSignature()" class="px-3 py-3 bg-slate-200 text-slate-600 rounded-xl text-xs font-bold uppercase active:scale-95"><i data-lucide="eraser" class="w-4 h-4"></i></button>
<button onclick="confirmAcceptBudget()" class="flex-1 bg-emerald-500 text-white font-black py-3 rounded-xl shadow-md flex items-center justify-center gap-2 uppercase tracking-widest text-xs active:scale-95 transition-transform">
Confirmar y Enviar
</button>
</div>
<button onclick="closeSignatureArea()" class="w-full mt-2 text-[10px] font-bold text-slate-400 p-2 uppercase tracking-widest">Cancelar</button>
</div>
<button id="btnDownloadPdf" class="w-full bg-slate-800 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 Copia en PDF
</button>
</div>
</div>
<div id="toast" class="fixed bottom-24 left-1/2 -translate-x-1/2 bg-slate-900 text-white px-6 py-3 rounded-full shadow-2xl hidden z-[200] font-bold text-xs uppercase tracking-widest text-center transition-all whitespace-nowrap"></div>
<div id="pdf-wrapper" class="hidden">
<div id="pdf-content" style="background-color: white; color: #1e293b; font-family: Arial, Helvetica, sans-serif; width: 800px; min-height: 1122px; display: flex; flex-direction: column; padding: 30px 40px 40px 40px; box-sizing: border-box;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #e2e8f0; padding-bottom: 15px; margin-bottom: 20px;">
<div style="max-width: 250px; max-height: 80px;">
<img id="pdf-company-logo" src="" alt="Logo Empresa" style="max-width: 100%; max-height: 80px; object-fit: contain; display: none;">
<h2 id="pdf-company-name-fallback" style="margin:0; color:#2563eb; font-size: 22px; font-weight: 900; display: none;">IntegraRepara</h2>
</div>
<div style="text-align: right; font-size: 11px; color: #64748b; line-height: 1.4;">
<div id="pdf-company-name" style="font-size: 14px; font-weight: 900; color: #0f172a; margin-bottom: 2px;">Empresa S.L.</div>
<div id="pdf-company-dni">CIF: B12345678</div>
<div id="pdf-company-address">Calle Falsa 123</div>
<div id="pdf-company-location">28000 Madrid (Madrid)</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<div>
<h1 style="font-size: 24px; font-weight: 900; color: #2563eb; margin: 0 0 5px 0; letter-spacing: -1px;">PRESUPUESTO</h1>
<div style="font-size: 12px; color: #64748b; line-height: 1.4;">
<strong>Referencia:</strong> <span id="pdf-budget-id">#PRE-000</span><br>
<strong>Fecha:</strong> <span id="pdf-date">01/01/2026</span><br>
<strong>Validez:</strong> 30 días<br>
</div>
</div>
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; padding: 12px 15px; border-radius: 8px; width: 280px;">
<h3 style="margin: 0 0 5px 0; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; font-weight: 800;">Presupuestado a:</h3>
<p id="pdf-client-name" style="font-size: 14px; font-weight: 800; color: #0f172a; margin: 0 0 4px 0;">Nombre Cliente</p>
<p style="font-size: 12px; margin: 0; color: #475569; line-height: 1.4;">
<span id="pdf-client-phone">Teléfono</span><br>
<span id="pdf-client-address">Dirección</span>
</p>
</div>
</div>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<thead>
<tr>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: left; border-bottom: 2px solid #cbd5e1; border-radius: 6px 0 0 0;">Concepto</th>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: center; border-bottom: 2px solid #cbd5e1;">Cant.</th>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: right; border-bottom: 2px solid #cbd5e1;">Precio Ud.</th>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: right; border-bottom: 2px solid #cbd5e1; border-radius: 0 6px 0 0;">Subtotal</th>
</tr>
</thead>
<tbody id="pdf-items">
</tbody>
</table>
<div style="width: 250px; margin-left: auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; padding: 8px 15px; font-size: 11px; border-bottom: 1px solid #f1f5f9; color: #475569;">
<span style="font-weight: 600; text-transform: uppercase;">Base Imponible</span>
<span id="pdf-subtotal" style="font-weight: 800; color: #0f172a;">0.00 €</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 15px; font-size: 11px; border-bottom: 1px solid #f1f5f9; color: #475569;">
<span style="font-weight: 600; text-transform: uppercase;">IVA (21%)</span>
<span id="pdf-tax" style="font-weight: 800; color: #0f172a;">0.00 €</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 10px 15px; background: #2563eb; color: white;">
<span style="font-weight: 900; font-size: 13px;">TOTAL A PAGAR</span>
<span id="pdf-total" style="font-weight: 900; font-size: 16px;">0.00 €</span>
</div>
</div>
<div style="margin-top: auto; padding-top: 20px;">
<div id="pdf-bank-info" style="margin-bottom: 10px; padding: 12px 15px; background-color: #f0fdf4; border-left: 4px solid #22c55e; border-radius: 0 6px 6px 0; font-size: 11px; color: #166534; display: none;">
<strong>INFORMACIÓN DE PAGO</strong><br>
Para confirmar el presupuesto, por favor realice una transferencia a la siguiente cuenta:<br>
<span id="pdf-bank-account" style="font-size: 14px; font-weight: 900; letter-spacing: 1px; margin-top: 4px; display: inline-block;">ES00 0000 0000 0000 0000</span>
</div>
<div id="pdf-obs-info" style="margin-bottom: 15px; padding: 12px 15px; background-color: #fefce8; border-left: 4px solid #f59e0b; border-radius: 0 6px 6px 0; font-size: 10px; color: #713f12; display: none;">
<strong>OBSERVACIONES Y CONDICIONES</strong><br>
<span id="pdf-obs-text"></span>
</div>
<div style="text-align: center; font-size: 9px; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 10px;">
Documento generado por IntegraRepara - Sistema de Gestión de Asistencias
</div>
</div>
</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 = [];
let globalCompanyData = null; // 🛑 NUEVO: Guarda los datos para el PDF
// 🛑 NUEVO: Diccionario de logos de compañías
const companyLogos = {
'REPSOL': 'https://cdn.sanity.io/images/rn4tswnp/production/1bc5be0207b732bd18dd0fc38e063d5701267068-1000x832.png?rect=6,0,994,832&h=320&auto=format&dpr=2',
'MUTUA': 'https://www.google.com/s2/favicons?domain=mutua.es&sz=128',
'ALLIANZ': 'https://www.google.com/s2/favicons?domain=allianz.es&sz=128',
'CASER': 'https://www.google.com/s2/favicons?domain=caser.es&sz=128',
'SEGURCAIXA': 'https://unijepol.eu/wp-content/uploads/2021/01/Segur-Caixa-Adeslas.jpg',
'LA CAIXA': 'https://www.google.com/s2/favicons?domain=segurcaixaadeslas.es&sz=128',
'AXA': 'https://www.google.com/s2/favicons?domain=axa.es&sz=128',
'LDA': 'https://www.google.com/s2/favicons?domain=lineadirecta.com&sz=128',
'LINEA DIRECTA': 'https://www.google.com/s2/favicons?domain=lineadirecta.com&sz=128',
'HOMESERVE': 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTeO7TsnpOYqLGgJm0puUyUMLi657od4mGWKQ&s',
'RGA': 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRemEVB4iYmTGoZL0nrBQJkZ1vNlDNtKfGEug&s',
'RURAL': 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRemEVB4iYmTGoZL0nrBQJkZ1vNlDNtKfGEug&s',
'SANTANDER': 'https://www.google.com/s2/favicons?domain=santander.com&sz=128',
'BANSABADELL': 'https://www.google.com/s2/favicons?domain=bancsabadell.com&sz=128',
'GENERALI': 'https://cdn.worldvectorlogo.com/logos/generali-logo.svg',
'MAPFRE': 'https://www.google.com/s2/favicons?domain=mapfre.es&sz=128',
'DEFAULT': 'https://cdn-icons-png.flaticon.com/512/2875/2875438.png'
};
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 {
// Obtenemos SIEMPRE todos los servicios de ese cliente, aunque haya '?service=XX' en la URL
let fetchUrl = `${API_URL}/public/portal/${urlToken}`;
const res = await fetch(fetchUrl);
const data = await res.json();
if (!data.ok) throw new Error("Token inválido");
const servicesList = data.services || [];
currentQuotes = data.quotes || [];
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();
}
});
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 ---
function toggleHistory() {
const container = document.getElementById('historyServicesContainer');
const chevron = document.getElementById('historyChevron');
if (container.classList.contains('hidden')) {
container.classList.remove('hidden');
container.classList.add('fade-in');
chevron.classList.add('rotate-180');
} else {
container.classList.add('hidden');
container.classList.remove('fade-in');
chevron.classList.remove('rotate-180');
}
}
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
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++;
// Formateamos las variables reales de la BD
let refStr = q.quote_ref || q.ref || q.id || "S/N";
let titleStr = q.title || "Presupuesto de Reparación";
let amountStr = parseFloat(q.total || q.amount || 0).toFixed(2);
let dateStr = q.created_at || q.date || "";
if(dateStr && dateStr.includes('T')) dateStr = dateStr.split('T')[0].split('-').reverse().join('/');
// 🛑 FIX: Calcar los colores y estados del escritorio
let statusBadge = "";
if (!q.status || q.status === 'pending') {
statusBadge = `<span class="bg-amber-100 text-amber-700 px-2 py-1 rounded-md text-[8px] font-black uppercase tracking-widest flex items-center gap-1 w-fit mt-1.5"><i data-lucide="clock" class="w-3 h-3"></i> Pte. Resolver</span>`;
} else if (q.status === 'accepted') {
statusBadge = `<span class="bg-blue-100 text-blue-600 px-2 py-1 rounded-md text-[8px] font-black uppercase tracking-widest flex items-center gap-1 w-fit mt-1.5"><i data-lucide="check" class="w-3 h-3"></i> Aceptado</span>`;
} else if (q.status === 'converted') {
statusBadge = `<span class="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-[8px] font-black uppercase tracking-widest flex items-center gap-1 w-fit mt-1.5"><i data-lucide="briefcase" class="w-3 h-3"></i> Finalizado</span>`;
} else if (q.status === 'rejected') {
statusBadge = `<span class="bg-rose-100 text-rose-700 px-2 py-1 rounded-md text-[8px] font-black uppercase tracking-widest flex items-center gap-1 w-fit mt-1.5"><i data-lucide="x" class="w-3 h-3"></i> Rechazado</span>`;
}
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 class="flex-1 min-w-0">
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest leading-none mb-1">REF #${refStr}</p>
<h3 class="font-black text-slate-800 text-sm leading-tight pr-6 truncate">${titleStr}</h3>
${statusBadge}
</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> ${dateStr}</span>
<span class="text-lg font-black text-slate-800">${amountStr}€</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');
}
}
let signaturePad = null;
let currentBudgetIdForSignature = null;
function openQuoteModal(id) {
const q = currentQuotes.find(x => x.id === id);
if (!q) return;
currentBudgetIdForSignature = id;
localStorage.setItem(`quote_viewed_${id}`, 'true');
renderQuotes();
document.getElementById('qmRef').innerText = `REF #${q.quote_ref || q.ref || q.id || "S/N"}`;
document.getElementById('qmTitle').innerText = q.title || "Presupuesto de Reparación";
let fDate = q.created_at || q.date || "";
if(fDate && fDate.includes('T')) fDate = fDate.split('T')[0].split('-').reverse().join('/');
document.getElementById('qmDate').innerText = fDate;
document.getElementById('qmAmount').innerText = parseFloat(q.total || q.amount || 0).toFixed(2) + "€";
// PINTAR LÍNEAS DE CONCEPTOS
let itemsArr = [];
if (typeof q.items === 'string') {
try { itemsArr = JSON.parse(q.items); } catch(e) { itemsArr = []; }
} else if (Array.isArray(q.items)) {
itemsArr = q.items;
}
let itemsHtml = '';
if(itemsArr.length > 0) {
itemsArr.forEach(i => {
itemsHtml += `<li class="flex justify-between border-b border-slate-100 pb-2 mb-2"><span class="w-2/3 pr-2 leading-tight">${i.qty}x ${i.concept}</span> <span class="font-black text-slate-800 shrink-0 text-right">${(i.qty * i.price).toFixed(2)}€</span></li>`;
});
} else {
itemsHtml = `<li class="text-slate-400 italic">Desglose general aplicado en el importe total.</li>`;
}
document.getElementById('qmItemsList').innerHTML = itemsHtml;
// CONTROL DE ESTADOS (ACEPTADO / RECHAZADO)
const btnContainer = document.getElementById('qmDecisionButtons');
const msgContainer = document.getElementById('qmStatusMessage');
const sigArea = document.getElementById('qmSignatureArea');
sigArea.classList.add('hidden');
if (!q.status || q.status === 'pending') {
btnContainer.classList.remove('hidden');
msgContainer.classList.add('hidden');
} else if (q.status === 'accepted' || q.status === 'converted') {
btnContainer.classList.add('hidden');
msgContainer.classList.remove('hidden');
msgContainer.className = "text-center p-4 rounded-xl font-black text-xs uppercase tracking-widest bg-emerald-50 text-emerald-600 border border-emerald-100";
msgContainer.innerHTML = '<i data-lucide="check-circle-2" class="w-5 h-5 inline mb-1"></i><br>Presupuesto Aceptado';
} else if (q.status === 'rejected') {
btnContainer.classList.add('hidden');
msgContainer.classList.remove('hidden');
msgContainer.className = "text-center p-4 rounded-xl font-black text-xs uppercase tracking-widest bg-rose-50 text-rose-600 border border-rose-100";
msgContainer.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5 inline mb-1"></i><br>Presupuesto Rechazado';
}
document.getElementById('btnDownloadPdf').setAttribute('onclick', `generatePDF(${q.id})`);
const modal = document.getElementById('quoteModal');
const sheet = document.getElementById('quoteModalSheet');
modal.classList.remove('hidden');
modal.classList.add('flex');
lucide.createIcons();
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);
}
// --- SISTEMA DE FIRMA Y RESPUESTA ---
function openSignatureArea() {
document.getElementById('qmDecisionButtons').classList.add('hidden');
document.getElementById('qmSignatureArea').classList.remove('hidden');
const canvas = document.getElementById('signatureCanvas');
if(!signaturePad) {
signaturePad = new SignaturePad(canvas, { backgroundColor: 'rgb(255, 255, 255)' });
}
signaturePad.clear();
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
}
function closeSignatureArea() {
document.getElementById('qmSignatureArea').classList.add('hidden');
document.getElementById('qmDecisionButtons').classList.remove('hidden');
}
function clearSignature() {
if(signaturePad) signaturePad.clear();
}
async function confirmAcceptBudget() {
if(signaturePad.isEmpty()) return showToast("⚠️ Por favor, dibuja tu firma para aceptar.");
const signatureBase64 = signaturePad.toDataURL("image/png");
showToast("⏳ Guardando aceptación...");
try {
const res = await fetch(`${API_URL}/public/portal/${urlToken}/budget/${currentBudgetIdForSignature}/respond`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'accept', signature: signatureBase64 })
});
const data = await res.json();
if(data.ok) {
const b = currentQuotes.find(x => x.id === currentBudgetIdForSignature);
if(b) b.status = 'accepted';
showToast("✅ ¡Presupuesto Aceptado! La oficina ha sido notificada.");
closeQuoteModal();
renderQuotes();
}
} catch(e) { showToast("❌ Error de conexión al guardar."); }
}
async function rejectBudget() {
if(!confirm("¿Estás seguro de que deseas rechazar este presupuesto?")) return;
showToast("⏳ Registrando...");
try {
const res = await fetch(`${API_URL}/public/portal/${urlToken}/budget/${currentBudgetIdForSignature}/respond`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'reject' })
});
const data = await res.json();
if(data.ok) {
const b = currentQuotes.find(x => x.id === currentBudgetIdForSignature);
if(b) b.status = 'rejected';
showToast("❌ Presupuesto rechazado. Gracias por avisarnos.");
closeQuoteModal();
renderQuotes();
}
} catch(e) { showToast("❌ Error de conexión al guardar."); }
}
// --- LÓGICA DE PDF AÑADIDA ---
let allServicesGlobalBackup = []; // <--- Variable para no perder los datos del cliente
async function generatePDF(id) {
const budget = currentQuotes.find(b => b.id === id);
if(!budget) return showToast("❌ Error: Presupuesto no encontrado");
showToast("⏳ Generando PDF, espera unos segundos...");
// 1. FIX DE PARSEO: A veces PostgreSQL manda el JSON como String. Lo forzamos a Objeto.
let bSet = globalCompanyData?.billing_settings || {};
if (typeof bSet === 'string') {
try { bSet = JSON.parse(bSet); } catch(e) { bSet = {}; }
}
let empNombre = globalCompanyData?.name || "Empresa Reparadora";
let empLogo = globalCompanyData?.logo || null;
let empDni = bSet.dni ? `CIF/NIF: ${bSet.dni}` : "";
let empAddress = bSet.address || "";
let empCity = bSet.city || "";
let empState = bSet.state || "";
let empZip = bSet.zip || "";
let empIban = bSet.iban || null;
let empObs = bSet.obs || null;
document.getElementById('pdf-company-name').innerText = empNombre;
document.getElementById('pdf-company-dni').innerText = empDni;
document.getElementById('pdf-company-address').innerText = empAddress;
const locText = [empZip, empCity, empState ? `(${empState})` : ''].filter(Boolean).join(' ');
document.getElementById('pdf-company-location').innerText = locText;
const logoImg = document.getElementById('pdf-company-logo');
const logoTxt = document.getElementById('pdf-company-name-fallback');
if (empLogo) {
logoImg.src = empLogo;
logoImg.style.display = 'block';
logoTxt.style.display = 'none';
} else {
logoImg.style.display = 'none';
logoTxt.innerText = empNombre;
logoTxt.style.display = 'block';
}
document.getElementById('pdf-budget-id').innerText = `#${budget.quote_ref || budget.ref || budget.id}`;
let fDate = budget.created_at || budget.date || "";
if(fDate && fDate.includes('T')) fDate = fDate.split('T')[0].split('-').reverse().join('/');
document.getElementById('pdf-date').innerText = fDate;
// 2. FIX DE CLIENTE: Si el cliente viene vacío o como la palabra "null", tira del expediente base
let fallbackClient = {};
if (allServicesGlobalBackup && allServicesGlobalBackup.length > 0) {
fallbackClient = allServicesGlobalBackup[0]?.raw_data || {};
}
let finalName = String(budget.client_name || "");
if(finalName.trim() === "" || finalName === "null") {
finalName = fallbackClient["Nombre Cliente"] || fallbackClient["CLIENTE"] || document.getElementById('clientName').innerText || "Cliente";
}
let finalPhone = String(budget.client_phone || "");
if(finalPhone.trim() === "" || finalPhone === "null") {
finalPhone = fallbackClient["Teléfono"] || fallbackClient["TELEFONO"] || fallbackClient["TELEFONOS"] || "-";
}
let finalAddress = String(budget.client_address || "");
if(finalAddress.trim() === "" || finalAddress === "null") {
let fallbackAddr = fallbackClient["Dirección"] || fallbackClient["DOMICILIO"] || "";
let fallbackCp = fallbackClient["Código Postal"] || fallbackClient["C.P."] || "";
let fallbackPob = fallbackClient["Población"] || fallbackClient["POBLACION-PROVINCIA"] || "";
finalAddress = `${fallbackAddr}, ${fallbackCp} ${fallbackPob}`.trim();
if (finalAddress === "," || finalAddress === "") finalAddress = "Dirección no especificada";
}
document.getElementById('pdf-client-name').innerText = finalName;
document.getElementById('pdf-client-phone').innerText = finalPhone;
document.getElementById('pdf-client-address').innerText = finalAddress;
// 3. FIX DE ITEMS: Forzamos parseo por si viene como String de BD
let itemsArr = budget.items || [];
if (typeof itemsArr === 'string') {
try { itemsArr = JSON.parse(itemsArr); } catch(e) { itemsArr = []; }
}
if (!Array.isArray(itemsArr)) itemsArr = [];
let itemsHtml = '';
if (itemsArr.length > 0) {
itemsHtml = itemsArr.map(item => `
<tr>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155;">${item.concept}</td>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: center;">${item.qty}</td>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: right;">${parseFloat(item.price).toFixed(2)} €</td>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #0f172a; text-align: right;">${(item.qty * item.price).toFixed(2)} €</td>
</tr>
`).join('');
} else {
itemsHtml = `<tr>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155;">${budget.title || "Presupuesto General de Reparación"}</td>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: center;">1</td>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: right;">${parseFloat(budget.total || budget.amount || 0).toFixed(2)} €</td>
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #0f172a; text-align: right;">${parseFloat(budget.total || budget.amount || 0).toFixed(2)} €</td>
</tr>`;
}
document.getElementById('pdf-items').innerHTML = itemsHtml;
let tot = parseFloat(budget.total || budget.amount || 0);
let sub = budget.subtotal ? parseFloat(budget.subtotal) : tot / 1.21;
let tx = budget.tax ? parseFloat(budget.tax) : tot - sub;
document.getElementById('pdf-subtotal').innerText = sub.toFixed(2) + " €";
document.getElementById('pdf-tax').innerText = tx.toFixed(2) + " €";
document.getElementById('pdf-total').innerText = tot.toFixed(2) + " €";
// 4. MOSTRAR BLOQUES OCULTOS (IBAN / OBS)
const bankContainer = document.getElementById('pdf-bank-info');
if (empIban && empIban.trim() !== "") {
document.getElementById('pdf-bank-account').innerText = empIban;
bankContainer.style.display = 'block';
} else {
bankContainer.style.display = 'none';
}
const obsContainer = document.getElementById('pdf-obs-info');
if (empObs && empObs.trim() !== "") {
document.getElementById('pdf-obs-text').innerHTML = String(empObs).replace(/\n/g, '<br>');
obsContainer.style.display = 'block';
} else {
obsContainer.style.display = 'none';
}
// REVELAR, DIBUJAR Y ESCONDER
const wrapper = document.getElementById('pdf-wrapper');
wrapper.classList.remove('hidden');
wrapper.style.position = 'absolute';
wrapper.style.left = '-9999px';
wrapper.style.top = '0'; // 🛑 FIX: Evita que herede margen superior si el usuario hizo scroll
const element = document.getElementById('pdf-content');
const opt = {
margin: 0,
filename: `Presupuesto_${budget.quote_ref || budget.id}.pdf`,
image: { type: 'jpeg', quality: 1 },
// 🛑 FIX: Añadimos scrollY: 0 y windowY: 0 para que haga la "foto" clavada arriba
html2canvas: { scale: 2, useCORS: true, logging: false, scrollY: 0, windowY: 0 },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
try {
await new Promise(r => setTimeout(r, 500));
await html2pdf().set(opt).from(element).save();
showToast("✅ PDF Descargado con éxito");
} catch (error) {
showToast("❌ Error al generar el PDF");
} finally {
wrapper.classList.add('hidden');
wrapper.style.position = '';
wrapper.style.left = '';
}
}
function showToast(msg) {
const t = document.getElementById('toast');
t.innerHTML = msg;
t.classList.remove('hidden');
setTimeout(() => t.classList.add('hidden'), 3000);
}
function showToast(msg) {
const t = document.getElementById('toast');
t.innerHTML = msg;
t.classList.remove('hidden');
setTimeout(() => t.classList.add('hidden'), 3000);
}
// --- FUNCIONES DE TEXTO Y FECHAS ---
function summarizeDescription(rawText) {
if (!rawText) return "Revisión técnica en el domicilio.";
let text = String(rawText);
// 1. Limpieza inicial: Quitar fechas y coletillas exactas que ensucian el corte
text = text.replace(/\(?\d{2}\/\d{2}\/\d{4}.{0,12}\d{2}:\d{2}\)?\s*-?/g, " "); // (23/02/2026 - 11:42)
text = text.replace(/\d{2}\/\d{2}\/\d{4}\s*-\s*/g, " "); // 17/02/2026 -
text = text.replace(/Descripción de la Averia/gi, " ");
text = text.replace(/Suceso:/gi, " ");
text = text.replace(/Texto manual/gi, " ");
text = text.replace(/DAñO_[0-9]:/gi, " ");
text = text.replace(/\(posible cobertura\)/gi, " ");
// 2. Separar por saltos de línea reales O por dobles barras (//) típicas de Multiasistencia
let lines = text.split(/[\n]|(?:\/\/?)/);
let validLines = [];
// 3. LISTA NEGRA: Si la línea tiene alguna de estas palabras, es basura interna y se borra entera
const junkKeywords = [
"cobro banco", "cobro contado", "servicio asignado", "sms", "enviado sms",
"cambio de estado", "este servicio requiere", "atención: el importe",
"el tipo de cobro", "siniestro comunicado", "cita confirmada", "visita al cliente",
"introducción ptto", "aplazamiento pte", "fecha y hora", "para el ofrecimiento",
"enviar factura", "declarante", "aviso nocita", "límite máximo", "importante",
"conectese via", "servicio con prof", "servicio sin prof", "servicio con la fecha",
"servicio desasignado", "comentario de desasignacion", "webservice", "alta servicio",
"alta avería", "alta reparación", "alta daños", "alta sin luz", "alta cambio",
"contacto con cliente", "fin previsto", "entrega presupuesto", "reparación con expediente",
"abierta reclamación", "llamada autom", "voicebot", "cliente informado por whatsapp",
"a espera de cita", "llamada entrante", "llamada saliente", "se ha introducido un importe",
"intento de llamada", "llamada de asegurado", "llama asegurad", "llamo a",
"servicio generado por proceso automático", "cubre", "cobertura", "daños propios",
"tipo de averia", "franja horaria", "agente", "oficina bbva", "repasar",
"revisado por perito", "perito tasa", "cita deseada", "rango horario"
];
for (let line of lines) {
let cleanLine = line.trim();
if (cleanLine.length < 12) continue; // Ignorar códigos o números sueltos
let isJunk = junkKeywords.some(keyword => cleanLine.toLowerCase().includes(keyword));
if (!isJunk) {
// Limpiar asteriscos y basura suelta
cleanLine = cleanLine.replace(/[\*\_]+/g, "").trim();
if(cleanLine.length > 5) validLines.push(cleanLine);
}
}
// Si después de la escabechina nos quedamos sin nada, ponemos el texto base
if (validLines.length === 0) return "Revisión técnica en el domicilio.";
// 4. EL MAGO: Nos quedamos única y exclusivamente con la primera frase válida
let finalSummary = validLines[0];
// Limpieza estética final
finalSummary = finalSummary.replace(/[\/]+/g, ", ")
.replace(/[-_]{2,}/g, " ")
.replace(/\s{2,}/g, " ")
.trim();
// Transformar MAYÚSCULAS en texto normal para que no parezca que gritamos
finalSummary = finalSummary.toLowerCase();
finalSummary = finalSummary.charAt(0).toUpperCase() + finalSummary.slice(1);
// Cortar si es escandalosamente largo (más de 150 caracteres)
if (finalSummary.length > 150) {
finalSummary = finalSummary.substring(0, 150).trim() + "...";
}
return finalSummary.endsWith('.') || finalSummary.endsWith('...') ? finalSummary : finalSummary + ".";
}
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) {
globalCompanyData = company; // 🛑 NUEVO: Guardamos la empresa
allServicesGlobalBackup = allServices; // 🛑 NUEVO: Guardamos una copia de los avisos para el PDF
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;
const activeContainer = document.getElementById('activeServicesContainer');
const historyContainerWrapper = document.getElementById('historyContainerWrapper');
const historyContainer = document.getElementById('historyServicesContainer');
activeContainer.innerHTML = '';
historyContainer.innerHTML = '';
// Contadores
let countAct = 0;
let countHist = 0;
let countInc = 0;
let countPend = 0;
allServices.forEach(srv => {
let raw = srv.raw_data || {};
let stNameLower = (srv.status_name || "").toLowerCase();
// 🛑 FIX: Ampliamos el radar para cazar todos los archivados y anulados
let isFinalized = srv.is_final === true || srv.status === 'archived' || stNameLower.includes('finalizado') || stNameLower.includes('anulado');
let descLimpia = summarizeDescription(srv.description);
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'));
// 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";
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.";
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')) {
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 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 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-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++;
}
// 🛑 NUEVO: Extraer la compañía y el logotipo
let compName = (raw["Compañía"] || raw["COMPAÑIA"] || raw["Aseguradora"] || raw["Cliente"] || "").toUpperCase();
let compLogo = companyLogos['DEFAULT'];
let isPrivate = false;
// Si no hay compañía o es particular
if (!compName || compName === "PARTICULAR" || compName === "PRIVADO") {
isPrivate = true;
compName = "PARTICULAR";
} else {
// Buscar coincidencia en el diccionario
for (const [key, url] of Object.entries(companyLogos)) {
if (compName.includes(key)) {
compLogo = url;
break;
}
}
}
// Si es particular, hereda el logo de tu empresa (si existe)
if (isPrivate && globalCompanyData?.logo) {
compLogo = globalCompanyData.logo;
}
// Generación de la tarjeta Blanca
let cardHtml = `
<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 justify-between items-center mb-5 pb-4 border-b border-slate-100">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-slate-50 border border-slate-100 flex items-center justify-center p-1 shrink-0 overflow-hidden">
<img src="${compLogo}" class="w-full h-full object-contain">
</div>
<span class="text-xs font-black text-slate-700 uppercase tracking-widest truncate max-w-[130px]" title="${compName}">${compName}</span>
</div>
<span class="text-[10px] font-bold text-slate-400 bg-slate-50 px-2.5 py-1.5 rounded-lg border border-slate-100 tracking-widest shrink-0">REF #${srv.service_ref}</span>
</div>
<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>
${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>
<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;
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;
// 🛑 FIX: Le pasamos el dato real al botón del acordeón
if (document.getElementById('labelHistoryCount')) {
document.getElementById('labelHistoryCount').innerText = `${countHist} servicios finalizados`;
}
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 ---
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>