// Billing model: campañas, facturas, pagos. // Everything lives in localStorage so the admin panel (separate HTML, same origin) // can read/write the same state and changes reflect in the vendedora app. const BILLING_STORAGE_KEY = 'inova-billing-v1'; const VENDEDORAS_STORAGE_KEY = 'inova-vendedoras-v1'; const SESSION_KEY = 'inova-session-v1'; // ——— Campaign defaults (editable from admin) ——— // Each campaign lasts ~15 days; pedido day is fixed (normally Friday), and the 3 payment // cuotas are the 3 Fridays after the pedido day. function friday(date) { const d = new Date(date); const day = d.getDay(); const diff = (5 - day + 7) % 7 || 7; // next Friday (never today) d.setDate(d.getDate() + diff); d.setHours(0, 0, 0, 0); return d; } function defaultCampaigns() { // Build a rolling 6 campaigns: 3 past, current, 2 future. const base = new Date(2026, 3, 3); // April 3 2026 Friday (C06 pedido day) const out = []; for (let i = 0; i < 6; i++) { const pedidoDay = new Date(base); pedidoDay.setDate(base.getDate() + i * 15); const [c1, c2, c3] = [friday(pedidoDay), friday(friday(pedidoDay)), friday(friday(friday(pedidoDay)))]; const num = 6 + i; out.push({ id: `C${String(num).padStart(2, '0')}`, nombre: `C${String(num).padStart(2, '0')} / 2026`, pedidoDay: pedidoDay.toISOString(), cuotas: [c1.toISOString(), c2.toISOString(), c3.toISOString()], porcentajes: [34, 33, 33], // split descuentoVendedora: 20, empresaPagoMovil: '0412-3334455', empresaBanco: 'Banesco', empresaCedula: 'J-30445566-7', }); } return out; } function defaultVendedoras() { return [ { id: 'v1', nombre: 'María Pérez', telefono: '04121234567', email: 'maria@inova.ve', zona: 'Valencia', clave: '1234', puntualidadStreak: 3 /* campañas seguidas a tiempo */, badge: 'puntual' }, { id: 'v2', nombre: 'Carolina Mendoza', telefono: '04247778888', email: 'carolina@inova.ve', zona: 'Caracas', clave: '1234', puntualidadStreak: 1 }, { id: 'v3', nombre: 'Ana Rojas', telefono: '04145556677', email: 'ana@inova.ve', zona: 'Turmero', clave: '1234', puntualidadStreak: 0 }, ]; } function loadBilling() { try { const raw = localStorage.getItem(BILLING_STORAGE_KEY); if (raw) return JSON.parse(raw); } catch {} const initial = { campaigns: defaultCampaigns(), facturas: [], // {id, vendedoraId, campaignId, pedidoId, total, descuentoPct, aPagar, cuotas:[{fecha, monto, status}], createdAt} pagos: [], // {id, facturaId, vendedoraId, monto, metodo, banco, referencia, fecha, capturaDataUrl, nota, status, createdAt, verifiedAt, rejectReason} retiros: [], // {id, facturaId, codigo, retirado:bool, createdAt} }; saveBilling(initial); return initial; } function saveBilling(state) { try { localStorage.setItem(BILLING_STORAGE_KEY, JSON.stringify(state)); } catch {} } function loadVendedoras() { try { const raw = localStorage.getItem(VENDEDORAS_STORAGE_KEY); if (raw) return JSON.parse(raw); } catch {} const v = defaultVendedoras(); saveVendedoras(v); return v; } function saveVendedoras(v) { try { localStorage.setItem(VENDEDORAS_STORAGE_KEY, JSON.stringify(v)); } catch {} } function loadSession() { try { return JSON.parse(localStorage.getItem(SESSION_KEY) || 'null'); } catch { return null; } } function saveSession(s) { try { if (s) localStorage.setItem(SESSION_KEY, JSON.stringify(s)); else localStorage.removeItem(SESSION_KEY); } catch {} } // ——— Factura creation ——— function createFacturaFromPedido({ pedido, vendedoraId, campaignId }) { const state = loadBilling(); const campaign = state.campaigns.find(c => c.id === campaignId) || state.campaigns[state.campaigns.length - 1]; const total = pedido.productos.reduce((s, x) => s + x.precio * x.cantidad, 0); const descuentoPct = campaign.descuentoVendedora; const aPagar = +(total * (1 - descuentoPct / 100)).toFixed(2); const cuotaMontos = campaign.porcentajes.map(p => +(aPagar * p / 100).toFixed(2)); // Adjust rounding drift onto last cuota const drift = +(aPagar - cuotaMontos.reduce((a, b) => a + b, 0)).toFixed(2); cuotaMontos[cuotaMontos.length - 1] = +(cuotaMontos[cuotaMontos.length - 1] + drift).toFixed(2); const cuotas = campaign.cuotas.map((fecha, i) => ({ fecha, monto: cuotaMontos[i], status: 'pendiente', // pendiente | pagada | vencida })); const factura = { id: `FAC-${campaign.id}-${Date.now().toString().slice(-5)}`, vendedoraId, campaignId: campaign.id, pedidoId: pedido.id, total, descuentoPct, aPagar, ganancia: +(total - aPagar).toFixed(2), cuotas, createdAt: new Date().toISOString(), retiroCodigo: null, retirado: false, }; state.facturas.push(factura); saveBilling(state); return factura; } // ——— Deuda/Saldo helpers ——— function facturaSaldo(factura, pagos) { const verificados = pagos .filter(p => p.facturaId === factura.id && p.status === 'verificado') .reduce((s, p) => s + p.monto, 0); return +(factura.aPagar - verificados).toFixed(2); } function facturaEstado(factura, pagos) { const saldo = facturaSaldo(factura, pagos); if (saldo <= 0.01) return factura.retirado ? 'retirado' : 'pagada'; const verificados = pagos.filter(p => p.facturaId === factura.id && p.status === 'verificado').length; const pendientes = pagos.filter(p => p.facturaId === factura.id && p.status === 'pendiente').length; const now = new Date(); const hasOverdue = factura.cuotas.some((c, i) => { const cuotaFecha = new Date(c.fecha); if (cuotaFecha > now) return false; // Sum should cover up to this cuota const needed = factura.cuotas.slice(0, i + 1).reduce((s, x) => s + x.monto, 0); const paid = pagos .filter(p => p.facturaId === factura.id && p.status === 'verificado') .reduce((s, p) => s + p.monto, 0); return paid < needed - 0.01; }); if (hasOverdue) return 'vencida'; if (pendientes > 0) return 'pago-pendiente'; if (verificados > 0) return 'abonada'; return 'por-pagar'; } function totalDeuda(facturas, pagos) { return facturas .filter(f => !f.retirado) .reduce((s, f) => s + Math.max(0, facturaSaldo(f, pagos)), 0); } function totalDeudaVencida(facturas, pagos) { return facturas .filter(f => facturaEstado(f, pagos) === 'vencida') .reduce((s, f) => s + Math.max(0, facturaSaldo(f, pagos)), 0); } // ——— Payment actions ——— function registrarPago({ facturaId, vendedoraId, monto, metodo, banco, referencia, fecha, capturaDataUrl, nota }) { const state = loadBilling(); const pago = { id: `PAG-${Date.now()}`, facturaId, vendedoraId, monto: +parseFloat(monto).toFixed(2), metodo, banco, referencia, fecha, capturaDataUrl, nota, status: 'pendiente', // pendiente | verificado | rechazado createdAt: new Date().toISOString(), }; state.pagos.push(pago); saveBilling(state); return pago; } function verificarPago(pagoId, verificadorId) { const state = loadBilling(); const pago = state.pagos.find(p => p.id === pagoId); if (!pago) return; pago.status = 'verificado'; pago.verifiedAt = new Date().toISOString(); pago.verifiedBy = verificadorId; // Check if factura fully paid → set cuotas paid + generate retiro code const factura = state.facturas.find(f => f.id === pago.facturaId); if (factura) { const saldo = facturaSaldo(factura, state.pagos); if (saldo <= 0.01 && !factura.retiroCodigo) { factura.retiroCodigo = `RET-${factura.campaignId}-${String(Math.floor(Math.random() * 9999)).padStart(4, '0')}`; } // Mark cuotas paid progressively const verificado = state.pagos .filter(p => p.facturaId === factura.id && p.status === 'verificado') .reduce((s, p) => s + p.monto, 0); let covered = 0; for (const c of factura.cuotas) { covered += c.monto; c.status = verificado >= covered - 0.01 ? 'pagada' : 'pendiente'; } } saveBilling(state); } function rechazarPago(pagoId, razon, verificadorId) { const state = loadBilling(); const pago = state.pagos.find(p => p.id === pagoId); if (!pago) return; pago.status = 'rechazado'; pago.rejectReason = razon; pago.verifiedAt = new Date().toISOString(); pago.verifiedBy = verificadorId; saveBilling(state); } // ——— Seed demo data so the app doesn't look empty ——— function seedDemoIfEmpty(vendedoraId) { const state = loadBilling(); if (state.facturas.some(f => f.vendedoraId === vendedoraId)) return; const camps = state.campaigns; const activeCamp = camps[camps.length - 3]; // one in the past with partial payments const nextCamp = camps[camps.length - 2]; // Factura 1: C06 — paid ~50% const f1 = { id: `FAC-${activeCamp.id}-SEED1`, vendedoraId, campaignId: activeCamp.id, pedidoId: 'C18-042', total: 187.20, descuentoPct: 20, aPagar: 149.76, ganancia: 37.44, cuotas: activeCamp.cuotas.map((f, i) => ({ fecha: f, monto: i === 2 ? 49.42 : 50.17, status: i === 0 ? 'pagada' : 'pendiente', })), createdAt: new Date(2026, 3, 3).toISOString(), }; // Factura 2: C07 — just created, nothing paid const f2 = { id: `FAC-${nextCamp.id}-SEED2`, vendedoraId, campaignId: nextCamp.id, pedidoId: 'C18-041', total: 112.50, descuentoPct: 20, aPagar: 90.00, ganancia: 22.50, cuotas: nextCamp.cuotas.map((f, i) => ({ fecha: f, monto: i === 2 ? 29.70 : 30.15, status: 'pendiente', })), createdAt: new Date(2026, 3, 18).toISOString(), }; state.facturas.push(f1, f2); // Seed payments: one verified on f1, one pending on f1 state.pagos.push({ id: 'PAG-SEED-1', facturaId: f1.id, vendedoraId, monto: 50.17, metodo: 'Pago Móvil', banco: 'Banesco', referencia: '002344519', fecha: new Date(2026, 3, 10).toISOString(), status: 'verificado', createdAt: new Date(2026, 3, 10, 9, 30).toISOString(), verifiedAt: new Date(2026, 3, 10, 14, 0).toISOString(), }); state.pagos.push({ id: 'PAG-SEED-2', facturaId: f1.id, vendedoraId, monto: 50.17, metodo: 'Transferencia', banco: 'Mercantil', referencia: '8872341', fecha: new Date(2026, 3, 17).toISOString(), status: 'pendiente', createdAt: new Date(2026, 3, 17, 11, 0).toISOString(), }); saveBilling(state); } function fmtDate(iso) { const d = new Date(iso); return d.toLocaleDateString('es', { day: '2-digit', month: 'short' }); } function fmtMoney(n) { return '$' + (+n).toFixed(2); } Object.assign(window, { loadBilling, saveBilling, loadVendedoras, saveVendedoras, loadSession, saveSession, createFacturaFromPedido, facturaSaldo, facturaEstado, totalDeuda, totalDeudaVencida, registrarPago, verificarPago, rechazarPago, seedDemoIfEmpty, fmtDate, fmtMoney, });