Files
Portal/plan-tranquilidad.html

322 lines
18 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>Mi Portal - Asistencia Técnica</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { background-color: #f8fafc; -webkit-tap-highlight-color: transparent; }
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.glass-card { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); }
</style>
</head>
<body class="text-slate-800 font-sans antialiased pb-10">
<header class="bg-white shadow-sm border-b border-slate-100 sticky top-0 z-50">
<div class="px-6 py-4 flex items-center gap-4">
<div id="company-logo-container" class="w-10 h-10 bg-slate-100 rounded-xl overflow-hidden flex items-center justify-center shrink-0">
<i data-lucide="building-2" class="w-5 h-5 text-slate-400"></i>
</div>
<div>
<h1 id="company-name" class="font-black text-slate-800 tracking-tight leading-none text-lg">Cargando...</h1>
<p id="client-greeting" class="text-xs font-bold text-slate-400 mt-0.5">Bienvenido</p>
</div>
</div>
</header>
<main class="p-6 space-y-6 max-w-lg mx-auto">
<div id="loader" class="text-center py-20 space-y-4">
<i data-lucide="loader-2" class="w-8 h-8 text-blue-600 animate-spin mx-auto"></i>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Conectando...</p>
</div>
<div id="content" class="hidden space-y-8">
<div id="subscription-container"></div>
<div id="quotes-container"></div>
<div id="services-container" class="space-y-4">
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2 ml-2">
<i data-lucide="clipboard-list" class="w-4 h-4"></i> Tus Averías y Avisos
</h2>
<div id="services-list" class="space-y-4"></div>
</div>
</div>
</main>
<script>
// CONFIGURACIÓN API
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:3000'
: 'https://integrarepara-api.integrarepara.es';
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const serviceId = urlParams.get('service') || '';
document.addEventListener('DOMContentLoaded', loadPortalData);
async function loadPortalData() {
if (!token) {
return showError("Enlace inválido o caducado.");
}
try {
const res = await fetch(`${API_URL}/public/portal/${token}?service=${serviceId}`);
const data = await res.json();
if (!data.ok) throw new Error(data.error || "No se pudo cargar la información.");
// 1. Rellenar Cabecera
document.getElementById('company-name').innerText = data.company.name;
document.getElementById('client-greeting').innerText = `Hola, ${data.client.name}`;
if (data.company.logo) {
document.getElementById('company-logo-container').innerHTML = `<img src="${data.company.logo}" class="w-full h-full object-cover">`;
}
// 2. Renderizar Módulo de Seguro (SaaS)
renderSubscription(data.subscription);
// 3. Renderizar Presupuestos
renderQuotes(data.quotes);
// 4. Renderizar Averías
renderServices(data.services);
// Mostrar contenido
document.getElementById('loader').classList.add('hidden');
document.getElementById('content').classList.remove('hidden');
lucide.createIcons();
} catch (error) {
showError(error.message);
}
}
// ==========================================
// 🛡️ RENDERIZADO DEL SEGURO
// ==========================================
function renderSubscription(sub) {
const container = document.getElementById('subscription-container');
if (sub) {
// EL CLIENTE TIENE UN PLAN ACTIVO
const isImpagado = sub.payment_status === 'impagado';
const bgClass = isImpagado ? 'bg-rose-50 border-rose-200' : 'bg-gradient-to-br from-blue-600 to-blue-800 text-white border-blue-700 shadow-xl shadow-blue-900/20';
const textClass = isImpagado ? 'text-rose-900' : 'text-white';
const muteTextClass = isImpagado ? 'text-rose-500' : 'text-blue-200';
const pctBricos = sub.bricos_limit > 0 ? Math.min(100, (sub.bricos_used / sub.bricos_limit) * 100) : 0;
const pctUrg = sub.urgencies_limit > 0 ? Math.min(100, (sub.urgencies_used / sub.urgencies_limit) * 100) : 0;
container.innerHTML = `
<div class="${bgClass} p-6 rounded-3xl border relative overflow-hidden fade-in">
${!isImpagado ? '<div class="absolute -right-10 -top-10 w-32 h-32 bg-white/10 rounded-full blur-2xl"></div>' : ''}
<div class="flex justify-between items-start relative z-10 mb-6">
<div>
<h3 class="text-[10px] font-black uppercase tracking-widest ${muteTextClass} mb-1 flex items-center gap-1">
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i> Tu Plan Activo
</h3>
<p class="text-2xl font-black ${textClass} leading-none tracking-tight">${sub.plan_name}</p>
</div>
${isImpagado
? `<span class="bg-rose-100 text-rose-700 px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest border border-rose-200 flex items-center gap-1"><div class="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse"></div> IMPAGO</span>`
: `<span class="bg-white/20 px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest backdrop-blur-sm border border-white/10 text-white">AL DÍA</span>`
}
</div>
<div class="space-y-4 relative z-10">
<div>
<div class="flex justify-between text-xs font-bold mb-1.5 ${textClass}">
<span>Urgencias 24h</span>
<span>${sub.urgencies_used} / ${sub.urgencies_limit}</span>
</div>
<div class="w-full bg-black/20 rounded-full h-1.5 overflow-hidden">
<div class="${isImpagado ? 'bg-rose-500' : 'bg-white'} h-full rounded-full transition-all" style="width: ${pctUrg}%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs font-bold mb-1.5 ${textClass}">
<span>Servicio Manitas</span>
<span>${sub.bricos_used} / ${sub.bricos_limit}</span>
</div>
<div class="w-full bg-black/20 rounded-full h-1.5 overflow-hidden">
<div class="${isImpagado ? 'bg-rose-500' : 'bg-white'} h-full rounded-full transition-all" style="width: ${pctBricos}%"></div>
</div>
</div>
</div>
${sub.renewal_date ? `
<div class="mt-5 pt-4 border-t ${isImpagado ? 'border-rose-200 text-rose-500' : 'border-white/20 text-blue-100'} text-[9px] font-bold uppercase tracking-widest flex justify-between items-center">
<span>Próxima Renovación</span>
<span>${new Date(sub.renewal_date).toLocaleDateString('es-ES')}</span>
</div>` : ''}
${isImpagado ? `
<div class="mt-4 bg-rose-100/50 p-3 rounded-xl text-xs font-bold text-rose-700 text-center">
Por favor, contacta con nosotros para regularizar el pago y reactivar tus coberturas.
</div>` : ''}
</div>
`;
} else {
// EL CLIENTE NO TIENE PLAN (Módulo de Ventas / Upselling)
container.innerHTML = `
<div class="bg-gradient-to-r from-slate-800 to-slate-900 p-6 rounded-3xl shadow-xl text-white relative overflow-hidden fade-in flex flex-col items-center text-center">
<div class="w-12 h-12 bg-white/10 rounded-2xl flex items-center justify-center mb-3 backdrop-blur-sm">
<i data-lucide="zap" class="w-6 h-6 text-amber-400"></i>
</div>
<h3 class="text-lg font-black tracking-tight mb-2">Protege tu hogar todo el año</h3>
<p class="text-xs text-slate-300 font-medium mb-5 px-4">Olvídate de las averías imprevistas con nuestro Plan Tranquilidad. Incluye urgencias gratuitas y revisión anual.</p>
<a href="plan-tranquilidad.html?token=${token}" class="bg-white text-slate-900 font-black px-6 py-3 rounded-xl text-[10px] uppercase tracking-widest w-full active:scale-95 transition-transform shadow-lg">Ver Planes y Precios</a>
</div>
`;
}
}
// ==========================================
// 📄 RENDERIZADO DE PRESUPUESTOS
// ==========================================
function renderQuotes(quotes) {
const container = document.getElementById('quotes-container');
if (!quotes || quotes.length === 0) return;
const html = quotes.map(q => {
let statusBadge = '';
if(q.status === 'pending') statusBadge = '<span class="bg-amber-100 text-amber-700 px-2 py-1 rounded text-[9px] font-black uppercase">Pendiente</span>';
else if(q.status === 'accepted' || q.status === 'converted') statusBadge = '<span class="bg-emerald-100 text-emerald-700 px-2 py-1 rounded text-[9px] font-black uppercase">Aceptado</span>';
else if(q.status === 'rejected') statusBadge = '<span class="bg-rose-100 text-rose-700 px-2 py-1 rounded text-[9px] font-black uppercase">Rechazado</span>';
else if(q.status === 'paid') statusBadge = '<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-[9px] font-black uppercase">Pagado</span>';
let itemsHtml = q.items.map(i => `<div class="flex justify-between text-xs text-slate-500 mb-1"><span>${i.qty}x ${i.concept}</span><span>${parseFloat(i.price * i.qty).toFixed(2)}€</span></div>`).join('');
let actionsHtml = '';
if(q.status === 'pending') {
actionsHtml = `
<div class="flex gap-2 mt-4">
<button onclick="respondQuote(${q.id}, 'reject')" class="flex-1 bg-rose-50 text-rose-600 font-bold py-2 rounded-xl text-xs active:scale-95 transition-transform border border-rose-100">Rechazar</button>
<button onclick="respondQuote(${q.id}, 'accept')" class="flex-1 bg-blue-600 text-white font-bold py-2 rounded-xl text-xs active:scale-95 transition-transform shadow-md">Aceptar Presupuesto</button>
</div>
`;
}
return `
<div class="bg-white rounded-3xl p-5 shadow-sm border border-slate-200 mb-4 fade-in">
<div class="flex justify-between items-start mb-4 border-b border-slate-100 pb-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">${q.quote_ref}</p>
<h3 class="font-black text-slate-800 text-sm mt-0.5">${q.title}</h3>
</div>
${statusBadge}
</div>
<div class="mb-4">
${itemsHtml}
</div>
<div class="flex justify-between items-center pt-3 border-t border-slate-100">
<span class="text-xs font-bold text-slate-400 uppercase">Total (IVA Incl.)</span>
<span class="text-xl font-black text-slate-800">${q.amount}€</span>
</div>
${actionsHtml}
</div>`;
}).join('');
container.innerHTML = `
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2 ml-2 mb-4 mt-8">
<i data-lucide="file-text" class="w-4 h-4"></i> Tus Presupuestos
</h2>
${html}
`;
}
// ==========================================
// 🛠️ RENDERIZADO DE AVERÍAS
// ==========================================
function renderServices(services) {
const container = document.getElementById('services-list');
if (!services || services.length === 0) {
container.innerHTML = `<div class="bg-white p-6 rounded-3xl border border-slate-200 text-center"><p class="text-sm font-bold text-slate-400">No tienes ninguna avería en curso.</p></div>`;
return;
}
container.innerHTML = services.map(s => {
const dateText = s.scheduled_date
? `<i data-lucide="calendar" class="w-3.5 h-3.5 inline"></i> Cita: ${s.scheduled_date.split('-').reverse().join('/')} ${s.scheduled_time ? `a las ${s.scheduled_time}` : ''}`
: `<i data-lucide="clock" class="w-3.5 h-3.5 inline"></i> Pendiente de agendar`;
return `
<div class="bg-white p-5 rounded-3xl shadow-sm border border-slate-200 fade-in">
<div class="flex justify-between items-start mb-3">
<div class="pr-4">
<h3 class="font-black text-slate-800 text-sm leading-tight">${s.title}</h3>
<p class="text-[10px] font-bold text-blue-600 uppercase tracking-widest mt-1 bg-blue-50 w-fit px-2 py-0.5 rounded-md">${s.status_name}</p>
</div>
</div>
<p class="text-xs text-slate-500 font-medium leading-relaxed mb-4 bg-slate-50 p-3 rounded-xl border border-slate-100">${s.description}</p>
<div class="flex items-center gap-2 text-[11px] font-bold ${s.scheduled_date ? 'text-emerald-600' : 'text-amber-500'} bg-slate-50 px-3 py-2 rounded-lg">
${dateText}
</div>
${s.assigned_worker ? `
<div class="mt-4 pt-4 border-t border-slate-100 flex items-center gap-3">
<div class="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-slate-400 shrink-0"><i data-lucide="user" class="w-4 h-4"></i></div>
<div>
<p class="text-[9px] font-black uppercase tracking-widest text-slate-400">Técnico Asignado</p>
<p class="text-xs font-bold text-slate-700">${s.assigned_worker}</p>
</div>
</div>` : ''}
</div>`;
}).join('');
}
// ==========================================
// ⚙️ ACCIONES
// ==========================================
async function respondQuote(id, action) {
const btnText = action === 'accept' ? 'Aceptando...' : 'Rechazando...';
if(!confirm(`¿Estás seguro de que deseas ${action === 'accept' ? 'ACEPTAR' : 'RECHAZAR'} este presupuesto?`)) return;
try {
const res = await fetch(`${API_URL}/public/portal/${token}/budget/${id}/respond`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action })
});
if(res.ok) {
if (action === 'accept') {
alert("✅ Presupuesto aceptado. Te redirigimos a la pasarela de pago para formalizarlo.");
// Lógica de Stripe si está habilitada, llamando al checkout
const checkoutRes = await fetch(`${API_URL}/public/portal/${token}/budget/${id}/checkout`, { method: 'POST' });
const checkData = await checkoutRes.json();
if (checkData.ok && checkData.checkout_url) window.location.href = checkData.checkout_url;
else {
alert("El técnico ha sido avisado. Se pondrán en contacto contigo para agendar.");
window.location.reload();
}
} else {
alert("Presupuesto rechazado. Gracias por informarnos.");
window.location.reload();
}
}
} catch (e) { alert("Error de conexión"); }
}
function showError(msg) {
document.getElementById('loader').innerHTML = `
<div class="w-12 h-12 bg-rose-100 text-rose-600 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="alert-triangle" class="w-6 h-6"></i>
</div>
<p class="text-sm font-black text-slate-800">${msg}</p>
`;
lucide.createIcons();
}
</script>
</body>
</html>