Actualizar proteccion.html

This commit is contained in:
2026-04-02 20:01:22 +00:00
parent ee45a43d1f
commit 0adc42aabf

View File

@@ -71,7 +71,6 @@
<h3 class="text-sm font-black text-gray-800">Actividad reciente</h3> <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> <p class="text-[11px] text-gray-400 font-semibold">Altas, cobros, renovaciones y uso de coberturas</p>
</div> </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>
<div class="flex-1 overflow-y-auto no-scrollbar p-4 space-y-3" id="activityList"></div> <div class="flex-1 overflow-y-auto no-scrollbar p-4 space-y-3" id="activityList"></div>
</div> </div>
@@ -92,17 +91,6 @@
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-2.5 text-gray-400"></i> <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"> <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>
<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>
<div class="flex-1 bg-white rounded-2xl shadow-sm border border-gray-200 flex flex-col overflow-hidden"> <div class="flex-1 bg-white rounded-2xl shadow-sm border border-gray-200 flex flex-col overflow-hidden">
@@ -130,50 +118,27 @@
<div id="tab-config" class="tab-content hidden flex-1 min-h-0 fade-in overflow-y-auto no-scrollbar pb-10"> <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="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-1 space-y-6"> <div class="lg:col-span-1 space-y-6">
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm"> <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"> <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>
<i data-lucide="building-2" class="w-4 h-4 text-blue-600"></i> Datos Empresa
</h3>
<div class="space-y-4"> <div class="space-y-4">
<div> <div><label class="text-[10px] font-black text-gray-400 uppercase">Nombre</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>
<label class="text-[10px] font-black text-gray-400 uppercase">Nombre Comercial</label> <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>
<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><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>
<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> </div>
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm"> <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"> <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>
<i data-lucide="refresh-cw" class="w-4 h-4"></i> Renovaciones
</h3>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between bg-gray-50 rounded-xl px-4 py-3"> <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"></div>
<span class="text-xs font-black text-gray-600 uppercase">Automática</span> <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">Aviso previo</span><input id="cfg_pre_notice" type="checkbox" class="w-5 h-5"></div>
<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> </div>
<div class="bg-white p-6 rounded-2xl border border-gray-200 shadow-sm"> <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"> <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>
<i data-lucide="credit-card" class="w-4 h-4"></i> Cobro
</h3>
<div> <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"> <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="stripe">Stripe</option>
<option value="recibo">Recibo</option> <option value="recibo">Recibo</option>
@@ -186,12 +151,8 @@
<div class="lg:col-span-2"> <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="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"> <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"> <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> Contrato Global</h3>
<i data-lucide="file-signature" class="w-4 h-4 text-blue-600"></i> Clausulado del Contrato <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>
</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> </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> <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>
@@ -202,46 +163,22 @@
</main> </main>
</div> </div>
<div id="modalBackdrop" class="hidden fixed inset-0 z-40 modal-backdrop"></div> <div id="modalBackdrop" class="hidden fixed inset-0 z-40 modal-backdrop" onclick="closeModals()"></div>
<div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"> <div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
<div class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden"> <div class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden pointer-events-auto">
<div class="p-5 border-b border-gray-100 flex items-center justify-between"> <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> <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> <button onclick="closeModals()" class="text-gray-400 hover:text-gray-700"><i data-lucide="x" class="w-5 h-5"></i></button>
</div> </div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <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>
<label class="text-[10px] font-black text-gray-400 uppercase">Nombre</label> <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>
<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><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> <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> <div><label class="text-[10px] font-black text-gray-400 uppercase">Urgencias / año</label><input id="plan_urgencies" 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>
<label class="text-[10px] font-black text-gray-400 uppercase">Tipo</label> <div><label class="text-[10px] font-black text-gray-400 uppercase">Bricos / año</label><input id="plan_bricos" 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>
<select id="plan_type" class="w-full mt-1 px-4 py-2.5 bg-gray-50 border rounded-xl font-bold text-sm"> <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"></div>
<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>
<div class="p-5 border-t border-gray-100 flex justify-end gap-2 bg-gray-50/50"> <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="closeModals()" class="px-4 py-2 rounded-lg border border-gray-200 text-[10px] font-black uppercase tracking-widest">Cancelar</button>
@@ -250,54 +187,23 @@
</div> </div>
</div> </div>
<div id="clientModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"> <div id="clientModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
<div class="bg-white w-full max-w-3xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden"> <div class="bg-white w-full max-w-3xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden pointer-events-auto">
<div class="p-5 border-b border-gray-100 flex items-center justify-between"> <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> <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> <button onclick="closeModals()" class="text-gray-400 hover:text-gray-700"><i data-lucide="x" class="w-5 h-5"></i></button>
</div> </div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <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>
<label class="text-[10px] font-black text-gray-400 uppercase">Nombre completo</label> <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>
<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><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> <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> <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>
<label class="text-[10px] font-black text-gray-400 uppercase">DNI</label> <div><label class="text-[10px] font-black text-gray-400 uppercase">Estado</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>
<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><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> <div class="flex gap-2">
<div> <div class="flex-1"><label class="text-[10px] font-black text-gray-400 uppercase">Bricos</label><input id="client_bricos" 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>
<label class="text-[10px] font-black text-gray-400 uppercase">Teléfono</label> <div class="flex-1"><label class="text-[10px] font-black text-gray-400 uppercase">Urg.</label><input id="client_urg" 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>
<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> </div>
<div class="p-5 border-t border-gray-100 flex justify-end gap-2 bg-gray-50/50"> <div class="p-5 border-t border-gray-100 flex justify-end gap-2 bg-gray-50/50">
@@ -309,60 +215,172 @@
<script src="js/layout.js"></script> <script src="js/layout.js"></script>
<script> <script>
const STORAGE_KEY = 'integra_seguros_saas_demo_v1'; let allClients = [];
const defaultState = { document.addEventListener('DOMContentLoaded', () => {
config: { loadAllData();
name: 'Integra Protección Hogar', document.getElementById('searchClientInput').addEventListener('input', renderClients);
email: 'seguros@integrarepara.es', if(window.lucide) lucide.createIcons();
phone: '956 000 111', });
autoRenew: true,
preNotice: true, async function fetchAPI(endpoint, method = 'GET', body = null) {
billingMethod: 'stripe', const options = { method, headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' }};
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.` if (body) options.body = JSON.stringify(body);
}, const res = await fetch(`${API_URL}${endpoint}`, options);
plans: [ return res.json();
{ 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'] } async function loadAllData() {
], const [dashRes, plansRes, clientsRes, confRes] = await Promise.all([
clients: [ fetchAPI('/protection/dashboard'),
{ 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 }, fetchAPI('/protection/plans'),
{ 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 } fetchAPI('/protection/subscribers'),
], fetchAPI('/protection/config')
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 }, if(dashRes.ok) {
{ id: cryptoRandom(), type: 'alerta', text: 'María Ortega Ruiz ha agotado urgencias del plan', createdAt: Date.now() - 720000 } renderStats(dashRes.stats);
] renderActivity(dashRes.activity);
renderTopPlans(dashRes.topPlans);
}
if(plansRes.ok) {
renderPlans(plansRes.plans);
const sel = document.getElementById('client_plan');
sel.innerHTML = plansRes.plans.map(p => `<option value="${p.id}">${p.name} (${p.price}€)</option>`).join('');
}
if(clientsRes.ok) {
allClients = clientsRes.subscribers;
renderClients();
}
if(confRes.ok && confRes.config) {
document.getElementById('cfg_name').value = confRes.config.name || '';
document.getElementById('cfg_email').value = confRes.config.email || '';
document.getElementById('cfg_phone').value = confRes.config.phone || '';
document.getElementById('cfg_auto_renew').checked = !!confRes.config.auto_renew;
document.getElementById('cfg_pre_notice').checked = !!confRes.config.pre_notice;
document.getElementById('cfg_billing_method').value = confRes.config.billing_method || 'stripe';
document.getElementById('cfg_contract').value = confRes.config.contract_text || '';
}
if(window.lucide) lucide.createIcons();
}
async function savePlan() {
const data = {
name: document.getElementById('plan_name').value,
type: document.getElementById('plan_type').value,
price: parseFloat(document.getElementById('plan_price').value || 0),
renewal: parseFloat(document.getElementById('plan_renewal').value || 0),
urgencies: parseInt(document.getElementById('plan_urgencies').value || 0),
bricos: parseInt(document.getElementById('plan_bricos').value || 0),
coverages: document.getElementById('plan_coverages').value
}; };
if(!data.name) return toast('Falta el nombre');
function cryptoRandom() { await fetchAPI('/protection/plans', 'POST', data);
return Math.random().toString(36).slice(2, 10); closeModals(); loadAllData(); toast('Plan creado');
} }
function loadState() { async function saveClient() {
try { const data = {
const raw = localStorage.getItem(STORAGE_KEY); plan_id: document.getElementById('client_plan').value,
if (!raw) { name: document.getElementById('client_name').value,
const first = JSON.parse(JSON.stringify(defaultState)); dni: document.getElementById('client_dni').value,
first.clients[0].planId = first.plans[0].id; phone: document.getElementById('client_phone').value,
first.clients[1].planId = first.plans[1].id; payment_status: document.getElementById('client_payment_status').value,
localStorage.setItem(STORAGE_KEY, JSON.stringify(first)); status: document.getElementById('client_status').value,
return first; renewal_date: document.getElementById('client_renewal').value,
} bricos_used: document.getElementById('client_bricos').value,
return JSON.parse(raw); urgencies_used: document.getElementById('client_urg').value
} catch { };
return JSON.parse(JSON.stringify(defaultState)); if(!data.name || !data.plan_id) return toast('Faltan datos clave');
} await fetchAPI('/protection/subscribers', 'POST', data);
closeModals(); loadAllData(); toast('Suscriptor creado');
} }
let state = loadState(); async function saveConfig() {
const data = {
function persist() { name: document.getElementById('cfg_name').value,
localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); email: document.getElementById('cfg_email').value,
phone: document.getElementById('cfg_phone').value,
auto_renew: document.getElementById('cfg_auto_renew').checked,
pre_notice: document.getElementById('cfg_pre_notice').checked,
billing_method: document.getElementById('cfg_billing_method').value,
contract_text: document.getElementById('cfg_contract').value
};
await fetchAPI('/protection/config', 'POST', data);
toast('Configuración guardada');
} }
async function toggleStatus(id, field, currentValue) {
const newValue = (field === 'payment_status')
? (currentValue === 'pagado' ? 'impagado' : 'pagado')
: (currentValue === 'activo' ? 'suspendido' : 'activo');
await fetchAPI(`/protection/subscribers/${id}/toggle`, 'PUT', { field, value: newValue });
loadAllData();
}
function renderStats(s) {
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 || 0}</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">${Number(s.mrr || 0).toFixed(2)}€</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 || 0} / ${s.briUsed || 0}</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 || 0}</p></div>
`;
}
function renderActivity(acts) {
document.getElementById('activityList').innerHTML = acts.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="${a.type==='alta'?'user-plus':(a.type==='cobro'?'credit-card':'alert-circle')}" class="w-4 h-4"></i></div>
<div><p class="text-sm font-black text-gray-800">${a.description}</p><p class="text-[11px] font-semibold text-gray-400">${new Date(a.created_at).toLocaleDateString()}</p></div>
</div>
`).join('') || '<p class="text-xs text-gray-400">Sin actividad</p>';
}
function renderTopPlans(plans) {
document.getElementById('topPlansList').innerHTML = plans.map(p => `
<div class="p-4 rounded-2xl border border-gray-100 bg-gray-50 flex items-center justify-between">
<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()} · ${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>
`).join('') || '<p class="text-xs text-gray-400">Sin planes</p>';
}
function renderPlans(plans) {
document.getElementById('plansGrid').innerHTML = plans.map(p => `
<div class="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
<h3 class="text-lg font-black text-gray-900">${p.name}</h3><p class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-4">${p.type}</p>
<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">${p.price}€</span></div>
<div class="flex justify-between"><span>Renovación</span><span class="text-amber-600">${p.renewal_price}€</span></div>
<div class="flex justify-between"><span>Urg / Bricos</span><span>${p.urgencies_limit} / ${p.bricos_limit}</span></div>
</div>
</div>
`).join('');
}
function renderClients() {
const search = document.getElementById('searchClientInput').value.toLowerCase();
const filtered = allClients.filter(c => c.client_name.toLowerCase().includes(search) || (c.client_dni||'').toLowerCase().includes(search));
document.getElementById('clientsTableBody').innerHTML = filtered.map(c => {
const prog = c.urgencies_limit ? Math.min(100, (c.urgencies_used / c.urgencies_limit) * 100) : 0;
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 font-black uppercase">${c.client_name.substring(0,2)}</div><div class="flex flex-col"><span class="text-sm font-black">${c.client_name}</span><span class="text-[9px] font-medium text-gray-400">DNI: ${c.client_dni||'-'}${c.client_phone||'-'}</span></div></div></td>
<td class="px-6 py-4 text-xs font-black text-gray-500"><span class="bg-blue-50 text-blue-700 px-2 py-1 rounded-md uppercase">${c.plan_name||'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.bricos_used}/${c.bricos_limit||0} | Urg: ${c.urgencies_used}/${c.urgencies_limit||0}</span><div class="w-24 h-1.5 bg-gray-100 rounded-full overflow-hidden"><div class="h-full bg-blue-500" style="width:${prog}%"></div></div></div></td>
<td class="px-6 py-4"><span class="${c.payment_status==='pagado'?'text-emerald-500':'text-rose-500'} font-black text-[10px] uppercase tracking-widest flex items-center gap-1.5"><div class="w-1.5 h-1.5 rounded-full ${c.payment_status==='pagado'?'bg-emerald-500':'bg-rose-500'} animate-pulse"></div>${c.payment_status}</span></td>
<td class="px-6 py-4 text-xs font-black text-gray-500">${c.renewal_date ? new Date(c.renewal_date).toLocaleDateString() : '-'}</td>
<td class="px-6 py-4 text-right">
<button onclick="toggleStatus(${c.id}, 'payment_status', '${c.payment_status}')" class="p-2 text-gray-400 hover:text-blue-600" title="Cambiar Pago"><i data-lucide="credit-card" class="w-4 h-4"></i></button>
<button onclick="toggleStatus(${c.id}, 'status', '${c.status}')" class="p-2 text-gray-400 hover:text-amber-600" title="Cambiar Estado"><i data-lucide="power" class="w-4 h-4"></i></button>
</td>
</tr>`;
}).join('');
if(window.lucide) lucide.createIcons();
}
// Utilidades UI
function switchTab(tab) { function switchTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden')); document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
@@ -370,342 +388,14 @@
document.getElementById('btn-tab-' + tab).classList.add('active'); document.getElementById('btn-tab-' + tab).classList.add('active');
if (window.lucide) lucide.createIcons(); if (window.lucide) lucide.createIcons();
} }
function openPlanModal() { document.getElementById('modalBackdrop').classList.remove('hidden'); document.getElementById('planModal').classList.remove('hidden'); }
function openModal(id) { function openClientModal() { document.getElementById('modalBackdrop').classList.remove('hidden'); document.getElementById('clientModal').classList.remove('hidden'); }
document.getElementById('modalBackdrop').classList.remove('hidden'); function closeModals() { document.getElementById('modalBackdrop').classList.add('hidden'); document.getElementById('planModal').classList.add('hidden'); document.getElementById('clientModal').classList.add('hidden'); }
document.getElementById(id).classList.remove('hidden'); function toast(msg) {
if (window.lucide) lucide.createIcons(); const t = document.createElement('div');
t.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';
t.textContent = msg; document.body.appendChild(t); setTimeout(() => t.remove(), 2500);
} }
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> </script>
</body> </body>
</html> </html>