Files
web/proteccion.html
2026-04-02 19:24:25 +00:00

711 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Seguros SaaS - IntegraRepara</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.fade-in { animation: fadeIn 0.2s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.tab-btn { transition: all 0.3s ease; border-bottom: 2px solid transparent; }
.tab-btn.active { color: #2563eb; border-color: #2563eb; background-color: #eff6ff; }
.modal-backdrop { background: rgba(15, 23, 42, 0.45); backdrop-filter: blur(4px); }
</style>
</head>
<body class="bg-gray-100 text-gray-800 font-sans h-screen overflow-hidden flex">
<div id="sidebar-container" class="h-full shrink-0"></div>
<div class="flex-1 flex flex-col h-full min-w-0">
<div id="header-container"></div>
<main class="flex-1 flex flex-col overflow-hidden relative p-6 space-y-6">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-white p-6 rounded-2xl shadow-sm border border-gray-200 shrink-0">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<i data-lucide="shield-check" class="w-7 h-7"></i>
</div>
<div>
<h1 class="text-xl font-black text-gray-800 tracking-tight">Seguros / Planes de Protección</h1>
<p class="text-gray-400 text-xs font-bold uppercase tracking-widest">MÓDULO SAAS PARA ASEGURADORAS Y SUSCRIPCIONES</p>
</div>
</div>
<div class="flex flex-wrap gap-2">
<button onclick="openPlanModal()" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-blue-700 transition-all flex items-center gap-2">
<i data-lucide="plus" class="w-3.5 h-3.5"></i> Nuevo Plan
</button>
<button onclick="openClientModal()" class="bg-white border border-gray-200 text-gray-700 px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest shadow-sm hover:border-blue-300 transition-all flex items-center gap-2">
<i data-lucide="user-plus" class="w-3.5 h-3.5"></i> Nuevo Suscriptor
</button>
</div>
</div>
<div class="flex bg-gray-100 p-1 rounded-xl shrink-0 w-fit">
<button onclick="switchTab('dashboard')" id="btn-tab-dashboard" class="tab-btn active px-5 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
<i data-lucide="layout-dashboard" class="w-3 h-3"></i> Resumen
</button>
<button onclick="switchTab('clients')" id="btn-tab-clients" class="tab-btn px-5 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
<i data-lucide="users" class="w-3 h-3"></i> Suscriptores
</button>
<button onclick="switchTab('plans')" id="btn-tab-plans" class="tab-btn px-5 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
<i data-lucide="badge-euro" class="w-3 h-3"></i> Planes
</button>
<button onclick="switchTab('config')" id="btn-tab-config" class="tab-btn px-5 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest flex items-center gap-2">
<i data-lucide="settings" class="w-3 h-3"></i> Configuración
</button>
</div>
<div id="tab-dashboard" class="tab-content flex-1 flex flex-col min-h-0 fade-in overflow-hidden">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mb-6 shrink-0" id="statsGrid"></div>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 flex-1 min-h-0">
<div class="xl:col-span-2 bg-white rounded-2xl shadow-sm border border-gray-200 flex flex-col overflow-hidden">
<div class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50 shrink-0">
<div>
<h3 class="text-sm font-black text-gray-800">Actividad reciente</h3>
<p class="text-[11px] text-gray-400 font-semibold">Altas, cobros, renovaciones y uso de coberturas</p>
</div>
<button onclick="seedDemoData()" class="text-[10px] font-black uppercase tracking-widest bg-white border border-gray-200 px-3 py-2 rounded-lg hover:border-blue-300">Cargar demo</button>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar p-4 space-y-3" id="activityList"></div>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 flex flex-col overflow-hidden">
<div class="p-4 border-b border-gray-100 bg-gray-50/50">
<h3 class="text-sm font-black text-gray-800">Planes más vendidos</h3>
<p class="text-[11px] text-gray-400 font-semibold">Resumen rápido del catálogo</p>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar p-4 space-y-4" id="topPlansList"></div>
</div>
</div>
</div>
<div id="tab-clients" class="tab-content hidden flex-1 flex flex-col min-h-0 fade-in">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6 shrink-0">
<div class="relative w-full md:w-80">
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-2.5 text-gray-400"></i>
<input id="searchClientInput" type="text" placeholder="Buscar suscriptor..." class="w-full pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-lg text-xs font-bold outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex gap-2">
<select id="filterStatus" class="px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-bold outline-none">
<option value="all">Todos los estados</option>
<option value="activo">Activos</option>
<option value="impagado">Impagados</option>
<option value="suspendido">Suspendidos</option>
</select>
<button onclick="openClientModal()" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-blue-700 transition-all flex items-center gap-2">
<i data-lucide="plus" class="w-3.5 h-3.5"></i> Nuevo
</button>
</div>
</div>
<div class="flex-1 bg-white rounded-2xl shadow-sm border border-gray-200 flex flex-col overflow-hidden">
<div class="flex-1 overflow-y-auto no-scrollbar">
<table class="w-full text-left">
<thead class="sticky top-0 bg-white shadow-sm text-[10px] font-black text-gray-400 uppercase tracking-widest">
<tr>
<th class="px-6 py-4">Socio / Datos</th>
<th class="px-6 py-4">Plan / Cuota</th>
<th class="px-6 py-4 text-center">Uso (Bricos/Urg)</th>
<th class="px-6 py-4">Estado Pago</th>
<th class="px-6 py-4">Renovación</th>
<th class="px-6 py-4 text-right">Acciones</th>
</tr>
</thead>
<tbody id="clientsTableBody" class="text-sm font-bold text-gray-600 divide-y divide-gray-50"></tbody>
</table>
</div>
</div>
</div>
<div id="tab-plans" class="tab-content hidden flex-1 min-h-0 fade-in overflow-y-auto no-scrollbar pb-10">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6" id="plansGrid"></div>
</div>
<div id="tab-config" class="tab-content hidden flex-1 min-h-0 fade-in overflow-y-auto no-scrollbar pb-10">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-1 space-y-6">
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm">
<h3 class="text-xs font-black text-gray-800 uppercase tracking-widest mb-4 flex items-center gap-2 border-b pb-3">
<i data-lucide="building-2" class="w-4 h-4 text-blue-600"></i> Datos Empresa
</h3>
<div class="space-y-4">
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Nombre Comercial</label>
<input id="cfg_name" type="text" class="w-full px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Email</label>
<input id="cfg_email" type="text" class="w-full px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Teléfono</label>
<input id="cfg_phone" type="text" class="w-full px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
</div>
</div>
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm">
<h3 class="text-xs font-black text-gray-800 uppercase tracking-widest mb-4 flex items-center gap-2 border-b pb-3 text-amber-600">
<i data-lucide="refresh-cw" class="w-4 h-4"></i> Renovaciones
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between bg-gray-50 rounded-xl px-4 py-3">
<span class="text-xs font-black text-gray-600 uppercase">Automática</span>
<input id="cfg_auto_renew" type="checkbox" class="w-5 h-5" checked>
</div>
<div class="flex items-center justify-between bg-gray-50 rounded-xl px-4 py-3">
<span class="text-xs font-black text-gray-600 uppercase">WhatsApp previo</span>
<input id="cfg_pre_notice" type="checkbox" class="w-5 h-5" checked>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm">
<h3 class="text-xs font-black text-gray-800 uppercase tracking-widest mb-4 flex items-center gap-2 border-b pb-3 text-blue-600">
<i data-lucide="credit-card" class="w-4 h-4"></i> Cobro
</h3>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Método principal</label>
<select id="cfg_billing_method" class="w-full px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
<option value="stripe">Stripe</option>
<option value="recibo">Recibo</option>
<option value="transferencia">Transferencia</option>
</select>
</div>
</div>
</div>
<div class="lg:col-span-2">
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm h-full flex flex-col">
<div class="flex justify-between items-center mb-4 border-b pb-3">
<h3 class="text-xs font-black text-gray-800 uppercase tracking-widest flex items-center gap-2">
<i data-lucide="file-signature" class="w-4 h-4 text-blue-600"></i> Clausulado del Contrato
</h3>
<button onclick="saveConfig()" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-blue-700 transition-all flex items-center gap-2">
<i data-lucide="save" class="w-3.5 h-3.5"></i> Guardar Cambios
</button>
</div>
<textarea id="cfg_contract" class="flex-1 w-full p-6 bg-gray-50 border rounded-2xl text-xs font-medium leading-relaxed text-gray-500 focus:ring-2 focus:ring-blue-500 outline-none min-h-[400px]"></textarea>
</div>
</div>
</div>
</div>
</main>
</div>
<div id="modalBackdrop" class="hidden fixed inset-0 z-40 modal-backdrop"></div>
<div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
<div class="p-5 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-sm font-black text-gray-800 uppercase tracking-widest">Nuevo Plan</h3>
<button onclick="closeModals()" class="text-gray-400 hover:text-gray-700"><i data-lucide="x" class="w-5 h-5"></i></button>
</div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Nombre</label>
<input id="plan_name" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Tipo</label>
<select id="plan_type" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
<option value="mensual">Mensual</option>
<option value="anual">Anual</option>
</select>
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Precio Oferta (€)</label>
<input id="plan_price" type="number" step="0.01" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Precio Renovación (€)</label>
<input id="plan_renewal" type="number" step="0.01" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Urgencias / año</label>
<input id="plan_urgencies" type="number" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Bricos / año</label>
<input id="plan_bricos" type="number" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-black text-gray-400 uppercase">Coberturas (separadas por coma)</label>
<input id="plan_coverages" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm" placeholder="Electricidad, Fontanería, Cerrajería">
</div>
</div>
<div class="p-5 border-t border-gray-100 flex justify-end gap-2 bg-gray-50/50">
<button onclick="closeModals()" class="px-4 py-2 rounded-lg border border-gray-200 text-[10px] font-black uppercase tracking-widest">Cancelar</button>
<button onclick="savePlan()" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest shadow-md">Guardar</button>
</div>
</div>
</div>
<div id="clientModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="bg-white w-full max-w-3xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
<div class="p-5 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-sm font-black text-gray-800 uppercase tracking-widest">Nuevo Suscriptor</h3>
<button onclick="closeModals()" class="text-gray-400 hover:text-gray-700"><i data-lucide="x" class="w-5 h-5"></i></button>
</div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Nombre completo</label>
<input id="client_name" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">DNI</label>
<input id="client_dni" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Teléfono</label>
<input id="client_phone" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Plan</label>
<select id="client_plan" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm"></select>
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Estado pago</label>
<select id="client_payment_status" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
<option value="pagado">Pagado</option>
<option value="impagado">Impagado</option>
</select>
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Estado suscripción</label>
<select id="client_status" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
<option value="activo">Activo</option>
<option value="suspendido">Suspendido</option>
</select>
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Renovación</label>
<input id="client_renewal" type="date" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Bricos usados</label>
<input id="client_bricos_used" type="number" value="0" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
<div>
<label class="text-[10px] font-black text-gray-400 uppercase">Urgencias usadas</label>
<input id="client_urg_used" type="number" value="0" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm">
</div>
</div>
<div class="p-5 border-t border-gray-100 flex justify-end gap-2 bg-gray-50/50">
<button onclick="closeModals()" class="px-4 py-2 rounded-lg border border-gray-200 text-[10px] font-black uppercase tracking-widest">Cancelar</button>
<button onclick="saveClient()" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest shadow-md">Guardar</button>
</div>
</div>
</div>
<script src="js/layout.js"></script>
<script>
const STORAGE_KEY = 'integra_seguros_saas_demo_v1';
const defaultState = {
config: {
name: 'Integra Protección Hogar',
email: 'seguros@integrarepara.es',
phone: '956 000 111',
autoRenew: true,
preNotice: true,
billingMethod: 'stripe',
contract: `1. OBJETO: Prestación de servicios de urgencia y manitas.\n2. COBERTURAS: Máximo 2 urgencias al año y servicios de bricolaje según plan.\n3. CARENCIA: 15 días naturales desde firma.\n4. RENOVACIÓN: Automática salvo oposición expresa.\n5. EXCLUSIONES: Daños preexistentes, reformas integrales y servicios fuera de cobertura.`
},
plans: [
{ id: cryptoRandom(), name: 'Básico', type: 'anual', price: 59.95, renewal: 89.00, urgencies: 1, bricos: 1, coverages: ['Electricidad', 'Fontanería'] },
{ id: cryptoRandom(), name: 'Hogar Plus', type: 'mensual', price: 6.90, renewal: 8.90, urgencies: 2, bricos: 2, coverages: ['Electricidad', 'Fontanería', 'Cerrajería'] },
{ id: cryptoRandom(), name: 'Premium 24/7', type: 'anual', price: 129.00, renewal: 149.00, urgencies: 4, bricos: 4, coverages: ['Electricidad', 'Fontanería', 'Cerrajería', 'Cristalería'] }
],
clients: [
{ id: cryptoRandom(), name: 'Juan Pérez García', dni: '12345678X', phone: '600112233', planId: '', paymentStatus: 'pagado', status: 'activo', renewalDate: '2026-12-15', bricosUsed: 0, urgenciesUsed: 1, createdAt: Date.now() - 1000000 },
{ id: cryptoRandom(), name: 'María Ortega Ruiz', dni: '44556677P', phone: '600223344', planId: '', paymentStatus: 'impagado', status: 'suspendido', renewalDate: '2026-04-15', bricosUsed: 1, urgenciesUsed: 2, createdAt: Date.now() - 500000 }
],
activity: [
{ id: cryptoRandom(), type: 'alta', text: 'Alta manual creada para Juan Pérez García', createdAt: Date.now() - 120000 },
{ id: cryptoRandom(), type: 'cobro', text: 'Cobro confirmado de renovación anual', createdAt: Date.now() - 360000 },
{ id: cryptoRandom(), type: 'alerta', text: 'María Ortega Ruiz ha agotado urgencias del plan', createdAt: Date.now() - 720000 }
]
};
function cryptoRandom() {
return Math.random().toString(36).slice(2, 10);
}
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
const first = JSON.parse(JSON.stringify(defaultState));
first.clients[0].planId = first.plans[0].id;
first.clients[1].planId = first.plans[1].id;
localStorage.setItem(STORAGE_KEY, JSON.stringify(first));
return first;
}
return JSON.parse(raw);
} catch {
return JSON.parse(JSON.stringify(defaultState));
}
}
let state = loadState();
function persist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function switchTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
document.getElementById('tab-' + tab).classList.remove('hidden');
document.getElementById('btn-tab-' + tab).classList.add('active');
if (window.lucide) lucide.createIcons();
}
function openModal(id) {
document.getElementById('modalBackdrop').classList.remove('hidden');
document.getElementById(id).classList.remove('hidden');
if (window.lucide) lucide.createIcons();
}
function openPlanModal() {
clearPlanModal();
openModal('planModal');
}
function openClientModal() {
populatePlanSelect();
clearClientModal();
openModal('clientModal');
}
function closeModals() {
document.getElementById('modalBackdrop').classList.add('hidden');
document.getElementById('planModal').classList.add('hidden');
document.getElementById('clientModal').classList.add('hidden');
}
function clearPlanModal() {
['plan_name','plan_price','plan_renewal','plan_urgencies','plan_bricos','plan_coverages'].forEach(id => document.getElementById(id).value = '');
document.getElementById('plan_type').value = 'mensual';
}
function clearClientModal() {
['client_name','client_dni','client_phone','client_renewal'].forEach(id => document.getElementById(id).value = '');
document.getElementById('client_payment_status').value = 'pagado';
document.getElementById('client_status').value = 'activo';
document.getElementById('client_bricos_used').value = 0;
document.getElementById('client_urg_used').value = 0;
}
function populatePlanSelect() {
const select = document.getElementById('client_plan');
select.innerHTML = state.plans.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
}
function savePlan() {
const name = document.getElementById('plan_name').value.trim();
const type = document.getElementById('plan_type').value;
const price = parseFloat(document.getElementById('plan_price').value || '0');
const renewal = parseFloat(document.getElementById('plan_renewal').value || '0');
const urgencies = parseInt(document.getElementById('plan_urgencies').value || '0', 10);
const bricos = parseInt(document.getElementById('plan_bricos').value || '0', 10);
const coverages = document.getElementById('plan_coverages').value.split(',').map(s => s.trim()).filter(Boolean);
if (!name) return toast('Ponle nombre al plan, artista.');
state.plans.unshift({ id: cryptoRandom(), name, type, price, renewal, urgencies, bricos, coverages });
state.activity.unshift({ id: cryptoRandom(), type: 'alta', text: `Nuevo plan creado: ${name}`, createdAt: Date.now() });
persist();
renderAll();
closeModals();
toast('Plan guardado correctamente.');
}
function saveClient() {
const name = document.getElementById('client_name').value.trim();
const dni = document.getElementById('client_dni').value.trim();
const phone = document.getElementById('client_phone').value.trim();
const planId = document.getElementById('client_plan').value;
const paymentStatus = document.getElementById('client_payment_status').value;
const status = document.getElementById('client_status').value;
const renewalDate = document.getElementById('client_renewal').value;
const bricosUsed = parseInt(document.getElementById('client_bricos_used').value || '0', 10);
const urgenciesUsed = parseInt(document.getElementById('client_urg_used').value || '0', 10);
if (!name || !phone || !planId) return toast('Faltan datos del suscriptor.');
state.clients.unshift({ id: cryptoRandom(), name, dni, phone, planId, paymentStatus, status, renewalDate, bricosUsed, urgenciesUsed, createdAt: Date.now() });
state.activity.unshift({ id: cryptoRandom(), type: 'alta', text: `Nuevo suscriptor dado de alta: ${name}`, createdAt: Date.now() });
persist();
renderAll();
closeModals();
toast('Suscriptor guardado correctamente.');
}
function saveConfig() {
state.config.name = document.getElementById('cfg_name').value.trim();
state.config.email = document.getElementById('cfg_email').value.trim();
state.config.phone = document.getElementById('cfg_phone').value.trim();
state.config.autoRenew = document.getElementById('cfg_auto_renew').checked;
state.config.preNotice = document.getElementById('cfg_pre_notice').checked;
state.config.billingMethod = document.getElementById('cfg_billing_method').value;
state.config.contract = document.getElementById('cfg_contract').value;
persist();
toast('Configuración guardada correctamente.');
}
function seedDemoData() {
localStorage.removeItem(STORAGE_KEY);
state = loadState();
renderAll();
toast('Datos demo recargados.');
}
function deletePlan(id) {
const inUse = state.clients.some(c => c.planId === id);
if (inUse) return toast('Ese plan está en uso. Primero cambia a los clientes.');
state.plans = state.plans.filter(p => p.id !== id);
persist();
renderAll();
toast('Plan eliminado.');
}
function toggleClientPayment(id) {
const c = state.clients.find(x => x.id === id);
if (!c) return;
c.paymentStatus = c.paymentStatus === 'pagado' ? 'impagado' : 'pagado';
if (c.paymentStatus === 'impagado') c.status = 'suspendido';
state.activity.unshift({ id: cryptoRandom(), type: 'cobro', text: `Estado de pago actualizado para ${c.name}: ${c.paymentStatus}`, createdAt: Date.now() });
persist();
renderAll();
}
function toggleClientStatus(id) {
const c = state.clients.find(x => x.id === id);
if (!c) return;
c.status = c.status === 'activo' ? 'suspendido' : 'activo';
state.activity.unshift({ id: cryptoRandom(), type: 'alerta', text: `Estado de suscripción cambiado para ${c.name}: ${c.status}`, createdAt: Date.now() });
persist();
renderAll();
}
function planById(id) {
return state.plans.find(p => p.id === id) || null;
}
function initials(name) {
return name.split(' ').slice(0,2).map(n => n[0] || '').join('').toUpperCase();
}
function money(v) {
return `${Number(v || 0).toFixed(2).replace('.', ',')}`;
}
function relativeTime(ts) {
const diff = Math.max(1, Math.floor((Date.now() - ts) / 60000));
if (diff < 60) return `hace ${diff} min`;
const h = Math.floor(diff / 60);
if (h < 24) return `hace ${h} h`;
const d = Math.floor(h / 24);
return `hace ${d} d`;
}
function getStats() {
const total = state.clients.length;
const paid = state.clients.filter(c => c.paymentStatus === 'pagado').length;
const unpaid = total - paid;
const mrr = state.clients.reduce((acc, c) => {
const p = planById(c.planId);
return acc + (p ? Number(p.price || 0) : 0);
}, 0);
const urgUsed = state.clients.reduce((acc, c) => acc + Number(c.urgenciesUsed || 0), 0);
const briUsed = state.clients.reduce((acc, c) => acc + Number(c.bricosUsed || 0), 0);
return { total, paid, unpaid, mrr, urgUsed, briUsed };
}
function renderStats() {
const s = getStats();
document.getElementById('statsGrid').innerHTML = `
<div class="bg-white p-4 rounded-2xl border border-gray-200 shadow-sm">
<p class="text-[9px] font-black text-gray-400 uppercase mb-1">Total Asegurados</p>
<p class="text-xl font-black text-gray-800">${s.total}</p>
</div>
<div class="bg-white p-4 rounded-2xl border border-gray-200 shadow-sm">
<p class="text-[9px] font-black text-gray-400 uppercase mb-1">Ingresos MRR</p>
<p class="text-xl font-black text-blue-600">${money(s.mrr)}</p>
</div>
<div class="bg-white p-4 rounded-2xl border border-gray-200 shadow-sm">
<p class="text-[9px] font-black text-gray-400 uppercase mb-1">Urgencias/Bricos Mes</p>
<p class="text-xl font-black text-amber-500">${s.urgUsed} / ${s.briUsed}</p>
</div>
<div class="bg-white p-4 rounded-2xl border border-gray-200 shadow-sm">
<p class="text-[9px] font-black text-gray-400 uppercase mb-1">Pagos Fallidos</p>
<p class="text-xl font-black text-rose-500">${s.unpaid}</p>
</div>
`;
}
function renderActivity() {
const iconMap = { alta: 'user-plus', cobro: 'credit-card', alerta: 'triangle-alert' };
document.getElementById('activityList').innerHTML = state.activity.slice(0, 10).map(a => `
<div class="p-4 rounded-xl border border-gray-100 bg-white flex items-start gap-3">
<div class="w-9 h-9 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center shrink-0">
<i data-lucide="${iconMap[a.type] || 'circle'}" class="w-4 h-4"></i>
</div>
<div>
<p class="text-sm font-black text-gray-800">${a.text}</p>
<p class="text-[11px] font-semibold text-gray-400">${relativeTime(a.createdAt)}</p>
</div>
</div>
`).join('') || `<div class="text-sm text-gray-400 font-bold">Sin actividad todavía.</div>`;
}
function renderTopPlans() {
const counts = state.plans.map(p => ({ ...p, users: state.clients.filter(c => c.planId === p.id).length }));
document.getElementById('topPlansList').innerHTML = counts.map(p => `
<div class="p-4 rounded-2xl border border-gray-100 bg-gray-50">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-black text-gray-800">${p.name}</p>
<p class="text-[11px] text-gray-400 font-semibold">${p.type.toUpperCase()} · ${money(p.price)}</p>
</div>
<span class="px-2 py-1 rounded-lg bg-white text-blue-700 text-[10px] font-black uppercase tracking-widest">${p.users} socios</span>
</div>
</div>
`).join('') || `<div class="text-sm text-gray-400 font-bold">No hay planes.</div>`;
}
function renderPlans() {
document.getElementById('plansGrid').innerHTML = state.plans.map(p => `
<div class="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
<div class="flex justify-between items-start gap-3 mb-4">
<div>
<h3 class="text-lg font-black text-gray-900">${p.name}</h3>
<p class="text-xs font-bold text-gray-400 uppercase tracking-widest">${p.type}</p>
</div>
<button onclick="deletePlan('${p.id}')" class="p-2 text-gray-400 hover:text-rose-600"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
</div>
<div class="space-y-3 text-sm font-bold text-gray-600">
<div class="flex justify-between"><span>Cuota</span><span class="text-blue-600">${money(p.price)}</span></div>
<div class="flex justify-between"><span>Renovación</span><span class="text-amber-600">${money(p.renewal)}</span></div>
<div class="flex justify-between"><span>Urgencias</span><span>${p.urgencies}/año</span></div>
<div class="flex justify-between"><span>Bricos</span><span>${p.bricos}/año</span></div>
<div class="flex justify-between"><span>Socios</span><span>${state.clients.filter(c => c.planId === p.id).length}</span></div>
</div>
<div class="mt-5 pt-4 border-t border-gray-100 flex flex-wrap gap-2">
${(p.coverages || []).map(c => `<span class="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-[10px] font-black uppercase tracking-widest">${c}</span>`).join('')}
</div>
</div>
`).join('') || `<div class="text-sm font-bold text-gray-400">No hay planes dados de alta.</div>`;
}
function renderClients() {
const search = (document.getElementById('searchClientInput')?.value || '').trim().toLowerCase();
const statusFilter = document.getElementById('filterStatus')?.value || 'all';
const rows = state.clients.filter(c => {
const matchesSearch = !search || [c.name, c.dni, c.phone].join(' ').toLowerCase().includes(search);
const matchesStatus = statusFilter === 'all' || c.status === statusFilter || (statusFilter === 'impagado' && c.paymentStatus === 'impagado');
return matchesSearch && matchesStatus;
}).map(c => {
const plan = planById(c.planId);
const bricosLimit = plan?.bricos || 0;
const urgLimit = plan?.urgencies || 0;
const progress = urgLimit ? Math.min(100, (Number(c.urgenciesUsed || 0) / urgLimit) * 100) : 0;
const paymentHtml = c.paymentStatus === 'pagado'
? `<span class="text-emerald-600 flex items-center gap-1.5"><div class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div> PAGADO</span>`
: `<span class="text-rose-600 flex items-center gap-1.5"><div class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></div> IMPAGADO</span>`;
return `
<tr class="hover:bg-blue-50/50 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs">${initials(c.name)}</div>
<div class="flex flex-col">
<span>${c.name}</span>
<span class="text-[9px] font-medium text-gray-400">DNI: ${c.dni || '-'}${c.phone || '-'}</span>
</div>
</div>
</td>
<td class="px-6 py-4 text-xs">
<span class="bg-blue-50 text-blue-700 px-2 py-1 rounded-md">${plan ? `${plan.name.toUpperCase()} (${money(plan.price)})` : 'SIN PLAN'}</span>
</td>
<td class="px-6 py-4">
<div class="flex flex-col items-center gap-1">
<span class="text-[10px] text-gray-400">Bricos: ${c.bricosUsed}/${bricosLimit} | Urg: ${c.urgenciesUsed}/${urgLimit}</span>
<div class="w-24 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div class="h-full bg-blue-500" style="width:${progress}%"></div>
</div>
</div>
</td>
<td class="px-6 py-4">${paymentHtml}</td>
<td class="px-6 py-4 text-xs font-black text-gray-500">${c.renewalDate || '-'}</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<button onclick="toggleClientPayment('${c.id}')" class="p-2 text-gray-400 hover:text-blue-600 transition-colors" title="Cambiar pago"><i data-lucide="credit-card" class="w-4 h-4"></i></button>
<button onclick="toggleClientStatus('${c.id}')" class="p-2 text-gray-400 hover:text-amber-600 transition-colors" title="Cambiar estado"><i data-lucide="refresh-cw" class="w-4 h-4"></i></button>
</div>
</td>
</tr>
`;
}).join('');
document.getElementById('clientsTableBody').innerHTML = rows || `
<tr>
<td colspan="6" class="px-6 py-8 text-center text-sm font-bold text-gray-400">No hay suscriptores que coincidan con el filtro.</td>
</tr>
`;
}
function renderConfig() {
document.getElementById('cfg_name').value = state.config.name || '';
document.getElementById('cfg_email').value = state.config.email || '';
document.getElementById('cfg_phone').value = state.config.phone || '';
document.getElementById('cfg_auto_renew').checked = !!state.config.autoRenew;
document.getElementById('cfg_pre_notice').checked = !!state.config.preNotice;
document.getElementById('cfg_billing_method').value = state.config.billingMethod || 'stripe';
document.getElementById('cfg_contract').value = state.config.contract || '';
}
function renderAll() {
renderStats();
renderActivity();
renderTopPlans();
renderPlans();
renderClients();
renderConfig();
populatePlanSelect();
if (window.lucide) lucide.createIcons();
}
function toast(message) {
const existing = document.getElementById('app-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'app-toast';
toast.className = 'fixed bottom-5 right-5 bg-slate-900 text-white px-4 py-3 rounded-xl shadow-2xl text-xs font-black uppercase tracking-widest z-[999] fade-in';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2200);
}
document.addEventListener('DOMContentLoaded', () => {
renderAll();
document.getElementById('searchClientInput').addEventListener('input', renderClients);
document.getElementById('filterStatus').addEventListener('change', renderClients);
document.getElementById('modalBackdrop').addEventListener('click', closeModals);
if (window.lucide) lucide.createIcons();
});
</script>
</body>
</html>