Actualizar proteccion.html
This commit is contained in:
704
proteccion.html
704
proteccion.html
@@ -71,7 +71,6 @@
|
||||
<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>
|
||||
@@ -92,17 +91,6 @@
|
||||
<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">
|
||||
@@ -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 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>
|
||||
<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><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>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
@@ -186,12 +151,8 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -202,46 +163,22 @@
|
||||
</main>
|
||||
</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 class="bg-white w-full max-w-2xl rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
|
||||
<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 pointer-events-auto">
|
||||
<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><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" 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">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>
|
||||
<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>
|
||||
</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>
|
||||
@@ -250,54 +187,23 @@
|
||||
</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 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 pointer-events-auto">
|
||||
<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><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</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 class="flex gap-2">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
const STORAGE_KEY = 'integra_seguros_saas_demo_v1';
|
||||
let allClients = [];
|
||||
|
||||
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 }
|
||||
]
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAllData();
|
||||
document.getElementById('searchClientInput').addEventListener('input', renderClients);
|
||||
if(window.lucide) lucide.createIcons();
|
||||
});
|
||||
|
||||
async function fetchAPI(endpoint, method = 'GET', body = null) {
|
||||
const options = { method, headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' }};
|
||||
if (body) options.body = JSON.stringify(body);
|
||||
const res = await fetch(`${API_URL}${endpoint}`, options);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadAllData() {
|
||||
const [dashRes, plansRes, clientsRes, confRes] = await Promise.all([
|
||||
fetchAPI('/protection/dashboard'),
|
||||
fetchAPI('/protection/plans'),
|
||||
fetchAPI('/protection/subscribers'),
|
||||
fetchAPI('/protection/config')
|
||||
]);
|
||||
|
||||
if(dashRes.ok) {
|
||||
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
|
||||
};
|
||||
|
||||
function cryptoRandom() {
|
||||
return Math.random().toString(36).slice(2, 10);
|
||||
if(!data.name) return toast('Falta el nombre');
|
||||
await fetchAPI('/protection/plans', 'POST', data);
|
||||
closeModals(); loadAllData(); toast('Plan creado');
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
async function saveClient() {
|
||||
const data = {
|
||||
plan_id: document.getElementById('client_plan').value,
|
||||
name: document.getElementById('client_name').value,
|
||||
dni: document.getElementById('client_dni').value,
|
||||
phone: document.getElementById('client_phone').value,
|
||||
payment_status: document.getElementById('client_payment_status').value,
|
||||
status: document.getElementById('client_status').value,
|
||||
renewal_date: document.getElementById('client_renewal').value,
|
||||
bricos_used: document.getElementById('client_bricos').value,
|
||||
urgencies_used: document.getElementById('client_urg').value
|
||||
};
|
||||
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();
|
||||
|
||||
function persist() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
async function saveConfig() {
|
||||
const data = {
|
||||
name: document.getElementById('cfg_name').value,
|
||||
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) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
||||
@@ -370,342 +388,14 @@
|
||||
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() { document.getElementById('modalBackdrop').classList.remove('hidden'); document.getElementById('planModal').classList.remove('hidden'); }
|
||||
function openClientModal() { document.getElementById('modalBackdrop').classList.remove('hidden'); document.getElementById('clientModal').classList.remove('hidden'); }
|
||||
function closeModals() { document.getElementById('modalBackdrop').classList.add('hidden'); document.getElementById('planModal').classList.add('hidden'); document.getElementById('clientModal').classList.add('hidden'); }
|
||||
function toast(msg) {
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user