'; var win = window.open('','_blank'); win.document.write(html); win.document.close(); setTimeout(function(){ win.print(); }, 500); } function renderIRPFAnual() { var anio = (document.getElementById('irpf-anio-sel') || {}).value || String(new Date().getFullYear()); var trimestres = [ {id:'T1', label:'T1 (Ene-Mar)', meses:[1,2,3]}, {id:'T2', label:'T2 (Abr-Jun)', meses:[4,5,6]}, {id:'T3', label:'T3 (Jul-Sep)', meses:[7,8,9]}, {id:'T4', label:'T4 (Oct-Dic)', meses:[10,11,12]}, ]; var totalBase = 0, totalIRPF = 0, totalFacs = 0; var filas = ''; trimestres.forEach(function(t) { var facs = facturasUnicasParaCalculo(FACTURAS_HISTORIAL.filter(function(f) { if (f.tipo !== 'ingreso' || !f.fecha) return false; var p = f.fecha.split('-'); return p.length === 3 && p[0] === anio && t.meses.indexOf(parseInt(p[1])) !== -1; })); var baseT = facs.reduce(function(s,f){return s+(parseFloat(f.base_imponible)||0);},0); var irpfT = facs.reduce(function(s,f){return s+Math.abs(parseFloat(f.irpf)||0);},0); totalBase += baseT; totalIRPF += irpfT; totalFacs += facs.length; filas += '' + '' + t.label + '' + '' + fmtEur(baseT,'') + '' + '' + fmtEur(irpfT,'') + '' + '' + facs.length + ''; }); filas += '' + 'Total ' + anio + '' + '' + fmtEur(totalBase,'') + '' + '' + fmtEur(totalIRPF,'') + '' + '' + totalFacs + ''; var tbody = document.getElementById('irpf-tabla-body'); if (tbody) tbody.innerHTML = filas; var pctMedio = totalBase > 0 ? Math.round(totalIRPF / totalBase * 100 * 10) / 10 : 0; var elRet = document.getElementById('irpf-total-ret'); var elBase = document.getElementById('irpf-base-ing'); var elPct = document.getElementById('irpf-pct-medio'); if (elRet) elRet.textContent = fmtEur(totalIRPF,''); if (elBase) elBase.textContent = fmtEur(totalBase,''); if (elPct) elPct.textContent = pctMedio + '%'; } function exportarIRPFPDF() { var anio = (document.getElementById('irpf-anio-sel') || {}).value || String(new Date().getFullYear()); var ing = facturasUnicasParaCalculo(FACTURAS_HISTORIAL.filter(function(f){ return f.tipo==='ingreso' && f.fecha && f.fecha.startsWith(anio); })); var totalBase = ing.reduce(function(s,f){return s+(parseFloat(f.base_imponible)||0);},0); var totalIRPF = ing.reduce(function(s,f){return s+Math.abs(parseFloat(f.irpf)||0);},0); var pct = totalBase > 0 ? Math.round(totalIRPF/totalBase*100*10)/10 : 0; var html = 'IRPF ' + anio + '' + '
' + '

Retenciones IRPF — ' + anio + '

' + '
Generado desde Contabilify · ' + new Date().toLocaleDateString('es-ES') + '
Resumen anual
' + '
⚠️ Estimación orientativa. Contabilify es una herramienta de apoyo, no un contable ni asesor fiscal. Incluye solo las facturas subidas a Contabilify y puede contener errores. Consulta con tu gestor o asesor fiscal antes de presentar tu declaración de la renta.
' + '' + ing.filter(function(f){ return Math.abs(parseFloat(f.irpf)||0) > 0; }).map(function(f){ return '' + '' + ''; }).join('') + '' + '
FechaN FacturaClienteBaseIRPF retenido
' + (f.fecha||'—') + '' + (f.numero_factura||'—') + '' + (f.receptor||f.emisor||'—') + '' + fmtEur(parseFloat(f.base_imponible)||0,'') + '' + fmtEur(Math.abs(parseFloat(f.irpf)||0),'') + '
TOTAL ' + anio + '' + fmtEur(totalBase,'') + '' + fmtEur(totalIRPF,'') + '
' + '
' + 'Resumen: Tus clientes te han retenido ' + fmtEur(totalIRPF,'') + ' en concepto de IRPF (' + pct + '% sobre ' + fmtEur(totalBase,'') + ' de base). ' + 'Este importe aparece en el modelo 190 que te envian tus clientes y reduce tu cuota en la declaración anual.
' + '
'; var win = window.open('','_blank'); win.document.write(html); win.document.close(); setTimeout(function(){ win.print(); }, 500); } function renderLibro() { var tipo = (document.getElementById('libro-tipo-sel') || {}).value || 'todos'; var mes = (document.getElementById('libro-mes-sel') || {}).value || 'todos'; var anio = (document.getElementById('libro-anio-sel') || {}).value || 'todos'; var tbody = document.getElementById('libro-tabla-body'); var totalesEl = document.getElementById('libro-totales'); if (!tbody) return; var filas = FACTURAS_HISTORIAL.filter(function(f) { if (tipo !== 'todos' && f.tipo !== tipo) return false; if (f.fecha) { var p = f.fecha.split('-'); if (p.length === 3) { if (anio !== 'todos' && p[0] !== anio) return false; if (mes !== 'todos' && p[1] !== mes) return false; } } return true; }).sort(function(a,b){ return (a.fecha||'').localeCompare(b.fecha||''); }); if (!filas.length) { tbody.innerHTML = 'No hay facturas con los filtros seleccionados'; if (totalesEl) totalesEl.innerHTML = ''; return; } var totalBase = 0, totalIVA = 0, totalIRPF = 0, totalTotal = 0; tbody.innerHTML = filas.map(function(f, i) { var esIng = f.tipo === 'ingreso'; var base = parseFloat(f.base_imponible) || 0; var iva = parseFloat(f.cuota_iva) || 0; var irpf = Math.abs(parseFloat(f.irpf) || 0); var total = parseFloat(f.total) || 0; totalBase += base; totalIVA += iva; if (esIng) totalIRPF += irpf; totalTotal += esIng ? total : -total; var fechaDisplay = f.fecha ? f.fecha.split('-').reverse().join('/') : '—'; var contraparte = esIng ? (f.receptor || '—') : (f.emisor || '—'); return '' + '' + fechaDisplay + '' + '' + (f.numero_factura||'—') + '' + '' + contraparte + '' + '' + (esIng?'Ingreso':'Gasto') + '' + '' + fmtEur(base,'') + '' + '' + fmtEur(iva,'') + '' + '' + (irpf > 0 && esIng ? fmtEur(irpf,'') : '—') + '' + '' + (esIng?'+':'−') + fmtEur(Math.abs(total),'') + '' + ''; }).join(''); // Totales if (totalesEl) { totalesEl.innerHTML = [ '
Base total
' + fmtEur(totalBase,'') + '
', '
IVA total
' + fmtEur(totalIVA,'') + '
', totalIRPF > 0 ? '
IRPF retenido
' + fmtEur(totalIRPF,'') + '
' : '', '
Saldo neto
' + (totalTotal>=0?'+':'') + fmtEur(totalTotal,'') + '
', '
Nº facturas
' + filas.length + '
', ].join(''); } } function exportarLibroExcel() { var tipo = (document.getElementById('libro-tipo-sel') || {}).value || 'todos'; var mes = (document.getElementById('libro-mes-sel') || {}).value || 'todos'; var anio = (document.getElementById('libro-anio-sel') || {}).value || 'todos'; var filas = FACTURAS_HISTORIAL.filter(function(f) { if (tipo !== 'todos' && f.tipo !== tipo) return false; if (f.fecha) { var p = f.fecha.split('-'); if (p.length === 3) { if (anio !== 'todos' && p[0] !== anio) return false; if (mes !== 'todos' && p[1] !== mes) return false; } } return true; }).sort(function(a,b){ return (a.fecha||'').localeCompare(b.fecha||''); }); if (!filas.length) { toast('No hay facturas para exportar', 'err'); return; } trackEvent('export_excel', { source: 'fiscal_book', export_type: 'libro_registro', row_count: filas.length }); fetch('/api/export-excel', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ rows: filas, fields: ['numero_factura','fecha','emisor','cif_emisor','receptor','cif_receptor','base_imponible','iva_porcentaje','cuota_iva','irpf','total','concepto'], titulo: 'Libro de ingresos y gastos' }) }) .then(function(r){ return r.blob(); }) .then(function(blob){ var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'libro_ingresos_gastos_' + new Date().toISOString().slice(0,10) + '.xlsx'; a.click(); URL.revokeObjectURL(url); }) .catch(function(){ toast('Error al exportar', 'err'); }); } // ── ANALÍTICA: DESGLOSE POR CATEGORÍA ────────────────────────────────────── var CATEGORIAS_MAP = { 'alquiler': ['alquiler', 'arrendamiento'], 'suministros': ['electricidad', 'luz', 'agua', 'gas', 'telefono', 'internet', 'hosting', 'dominio'], 'personal': ['nomina', 'salario', 'sueldo', 'seguridad social', 'ss ', 'formacion', 'formación'], 'vehiculo': ['gasolina', 'combustible', 'renting', 'vehiculo', 'vehículo', 'parking', 'peaje'], 'material': ['material', 'oficina', 'papeleria', 'papelería', 'consumible'], 'servicios_prof': ['gestor', 'asesor', 'abogado', 'notario', 'consultoria', 'consultoría', 'servicio'], 'marketing': ['publicidad', 'marketing', 'google ads', 'social', 'seo', 'fotografia', 'fotografía'], 'software': ['software', 'suscripcion', 'suscripción', 'adobe', 'microsoft', 'saas', 'licencia', 'app'], 'seguros': ['seguro', 'seguro responsabilidad', 'mutua'], 'colegios': ['colegio profesional', 'cuota colegio', 'colegio'], 'otros': [] }; var CATEGORIAS_LABELS = { 'alquiler': 'Alquiler', 'suministros': 'Suministros', 'personal': 'Personal', 'vehiculo': 'Vehículo', 'material': 'Material', 'servicios_prof': 'Servicios prof.', 'marketing': 'Marketing', 'software': 'Software', 'seguros': 'Seguros', 'colegios': 'Colegios', 'otros': 'Otros' }; var CATEGORIAS_COLORS = { 'alquiler': '#1838D8', 'suministros': '#2049FF', 'personal': '#385DFF', 'vehiculo': '#4968FF', 'material': '#5D78FF', 'servicios_prof': '#7289FF', 'marketing': '#8195FF', 'software': '#8A9DFF', 'seguros': '#A3B2FF', 'colegios': '#BAC5FF', 'otros': '#98A2B3' }; function clasificarConcepto(concepto) { if (!concepto) return 'otros'; var c = concepto.toLowerCase(); var cats = Object.keys(CATEGORIAS_MAP); for (var i = 0; i < cats.length - 1; i++) { var keywords = CATEGORIAS_MAP[cats[i]]; for (var j = 0; j < keywords.length; j++) { if (c.indexOf(keywords[j]) !== -1) return cats[i]; } } return 'otros'; } function renderCategoriasGastos() { var gas = FACTURAS_HISTORIAL.filter(function(f) { if (f.tipo !== 'gasto') return false; if (!f.fecha) return false; if (f.fecha.slice(0,4) !== ANA_ANIO) return false; if (currentPeriod === '1a') return true; if (currentPeriod && currentPeriod.slice(0,4) === 'mes:') { return f.fecha.slice(0,7) === currentPeriod.slice(4); } var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[currentPeriod]; return ms ? ms.indexOf(parseInt(f.fecha.slice(5,7))) !== -1 : true; }); var totales = {}; gas.forEach(function(f) { var cat = clasificarConcepto(f.concepto); totales[cat] = (totales[cat] || 0) + (parseFloat(f.base_imponible) || parseFloat(f.total) || 0); }); var cats = Object.keys(totales).sort(function(a,b){ return totales[b]-totales[a]; }); var totalGlobal = cats.reduce(function(s,c){ return s+totales[c]; }, 0); destroyChart('catDonut'); var listEl = document.getElementById('cat-list'); if (!listEl) return; if (!cats.length) { listEl.innerHTML = '
Sin gastos registrados.
'; return; } // Barras horizontales con color, importe y % bien visible listEl.innerHTML = '
Total: ' + fmtEur(totalGlobal,'') + '
' + cats.map(function(c, i) { var pct = totalGlobal > 0 ? Math.round(totales[c] / totalGlobal * 100) : 0; var color = CATEGORIAS_COLORS[c] || '#9ca3af'; var label = CATEGORIAS_LABELS[c] || c; return '
' + '
' + '' + '' + label + '' + '' + fmtEur(totales[c],'') + '' + '
' + '
' + '
' + '
' + '
' + '' + pct + '%' + '
' + '
'; }).join(''); } // ── ANALÍTICA: COMPARATIVA INTERANUAL ───────────────────────────────────── function renderComparativaInteranual() { var mesActual = new Date().getMonth(); // 0-based var meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; // Agrupar por año-mes var byYearMonth = {}; var years = {}; FACTURAS_HISTORIAL.forEach(function(f) { if (!f.fecha) return; var p = f.fecha.split('-'); if (p.length < 3) return; var y = p[0], m = parseInt(p[1]) - 1; years[y] = true; var key = y + '-' + p[1]; if (!byYearMonth[key]) byYearMonth[key] = {ing:0, gas:0}; var total = parseFloat(f.total) || 0; if (f.tipo === 'ingreso') byYearMonth[key].ing += total; else byYearMonth[key].gas += total; }); var yearKeys = Object.keys(years).sort(); if (yearKeys.length < 1) return; var currentYear = String(new Date().getFullYear()); // For past years show all 12 months; for current year stop at current month var lastMonth = mesActual; // default: current month (0-based) var maxMonth = yearKeys.reduce(function(mx, y) { // Find last month with data for this set of years for (var m = 11; m >= 0; m--) { var mm = (m + 1 < 10 ? '0' : '') + (m + 1); if (byYearMonth[y + '-' + mm]) return Math.max(mx, m); } return mx; }, 0); // Show all months up to the last month that has data (or current month, whichever is greater) lastMonth = Math.max(mesActual, maxMonth); // But never show future months beyond current lastMonth = Math.min(lastMonth, 11); var labels = meses.slice(0, lastMonth + 1); var colors = ['#2049ff','#6b7cff','#8b9cff','#ff5a7a','#18a8c7']; var colorsDash = ['rgba(99,102,241,.35)','rgba(5,150,105,.35)','rgba(245,158,11,.35)']; var datasets = []; yearKeys.forEach(function(y, yi) { var ingData = []; for (var m = 0; m <= lastMonth; m++) { var mm = (m + 1 < 10 ? '0' : '') + (m + 1); var key = y + '-' + mm; ingData.push(byYearMonth[key] ? Math.round(byYearMonth[key].ing * 100) / 100 : 0); } datasets.push({ label: 'Ing. ' + y, data: ingData, borderColor: colors[yi % colors.length], backgroundColor: 'transparent', borderWidth: yi === yearKeys.length - 1 ? 2.5 : 1.5, borderDash: yi === yearKeys.length - 1 ? [] : [5,3], tension: 0.35, pointRadius: 4, fill: false }); }); destroyChart('interanual'); var ctx = document.getElementById('chartInteranual'); if (ctx && datasets.length) { charts['interanual'] = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top', labels: { font: { size: 11 }, boxWidth: 20, padding: 12 } }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(ctx) { return ' ' + ctx.dataset.label + ': ' + fmtEur(ctx.parsed.y, ''); } }} }, scales: { x: { ticks: { color: '#6B7280', font: { size: 10 } }, grid: { display: false } }, y: { ticks: { color: '#6B7280', font: { size: 10 }, callback: function(v){ return v.toLocaleString('es-ES') + '€'; } }, grid: { color: 'rgba(0,0,0,.05)' } } } } }); } // Tabla resumen interanual var tbody = document.getElementById('interanual-tbody'); if (!tbody) return; var rows = ''; yearKeys.forEach(function(y) { var totalIng = 0, totalGas = 0; for (var m = 1; m <= 12; m++) { var mm = (m < 10 ? '0' : '') + m; var d = byYearMonth[y + '-' + mm]; if (d) { totalIng += d.ing; totalGas += d.gas; } } var neto = totalIng - totalGas; var margen = totalIng > 0 ? Math.round(neto/totalIng*100) : 0; rows += '' + '' + y + '' + '+' + fmtEur(totalIng,'') + '' + '−' + fmtEur(totalGas,'') + '' + '' + (neto>=0?'+':'−') + fmtEur(Math.abs(neto),'') + '' + '' + margen + '%' + ''; }); tbody.innerHTML = rows || 'Sin datos'; } // ── ANALÍTICA: RANKING CONCEPTOS ─────────────────────────────────────────── function renderRankingConceptos() { var tipo = (document.getElementById('ranking-tipo-sel') || {}).value || 'ingreso'; var facturas = FACTURAS_HISTORIAL.filter(function(f) { if (f.tipo !== tipo) return false; if (!f.fecha) return false; if (f.fecha.slice(0,4) !== ANA_ANIO) return false; if (currentPeriod === '1a') return true; if (currentPeriod && currentPeriod.slice(0,4) === 'mes:') { return f.fecha.slice(0,7) === currentPeriod.slice(4); } var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[currentPeriod]; return ms ? ms.indexOf(parseInt(f.fecha.slice(5,7))) !== -1 : true; }); // Normalizar conceptos var conceptos = {}; facturas.forEach(function(f) { var c = (f.concepto || 'Sin concepto').trim(); // Normalizar: quitar números de factura, mayúsculas, etc. var key = c.replace(/\s+/g, ' ').substring(0, 60); if (!conceptos[key]) conceptos[key] = { total: 0, n: 0, ejemplos: [] }; conceptos[key].total += parseFloat(f.total) || 0; conceptos[key].n++; }); var sorted = Object.keys(conceptos).sort(function(a,b){ return conceptos[b].total - conceptos[a].total; }).slice(0, 8); var maxVal = sorted.length ? conceptos[sorted[0]].total : 1; var totalGlobal = Object.keys(conceptos).reduce(function(s,k){ return s+conceptos[k].total; }, 0); var color = tipo === 'ingreso' ? 'var(--green)' : 'var(--red)'; var colorBg = tipo === 'ingreso' ? '#dcfce7' : '#fee2e2'; var el = document.getElementById('ranking-list'); if (!el) return; if (!sorted.length) { el.innerHTML = '
Sin facturas de tipo ' + tipo + '.
'; return; } el.innerHTML = sorted.map(function(k, i) { var pct = Math.round(conceptos[k].total / maxVal * 100); var pctTotal = totalGlobal > 0 ? Math.round(conceptos[k].total/totalGlobal*100) : 0; return '
' + '' + (i+1) + '' + '
' + '
' + '' + k + '' + '' + fmtEur(conceptos[k].total,'') + '' + '
' + '
' + '
' + '
' + '
' + '' + conceptos[k].n + ' fac. · ' + pctTotal + '%' + '
' + '
' + '
'; }).join(''); } // ── ANALÍTICA: SEMÁFORO FISCAL ──────────────────────────────────────────── function renderSemaforoFiscal() { var card = document.getElementById('semaforo-card'); var now = new Date(); var mes = now.getMonth() + 1; // 1-12 var anio = now.getFullYear(); // Hide the semaforo if viewing a past year if (ANA_ANIO !== String(anio)) { if (card) card.style.display = 'none'; return; } if (card) card.style.display = ''; // Trimestre actual var trimActual = mes <= 3 ? 'T1' : mes <= 6 ? 'T2' : mes <= 9 ? 'T3' : 'T4'; var trimMeses = { T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12] }; var trimLabels = { T1:'1 (Ene-Mar)', T2:'2 (Abr-Jun)', T3:'3 (Jul-Sep)', T4:'4 (Oct-Dic)' }; // Fecha límite presentación (20 del mes siguiente al cierre del trimestre) var trimCierre = { T1: new Date(anio,3,20), T2: new Date(anio,6,20), T3: new Date(anio,9,20), T4: new Date(anio+1,0,20) }; var diasRestantes = Math.ceil((trimCierre[trimActual] - now) / (1000*60*60*24)); var mesesTrim = trimMeses[trimActual]; var facsTrim = FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; var p = f.fecha.split('-'); return p.length >= 2 && p[0] === String(anio) && mesesTrim.indexOf(parseInt(p[1])) !== -1; }); var ivaRep = facsTrim.filter(function(f){return f.tipo==='ingreso';}).reduce(function(s,f){return s+(parseFloat(f.cuota_iva)||0);},0); var ivaSop = facsTrim.filter(function(f){return f.tipo==='gasto';}).reduce(function(s,f){return s+(parseFloat(f.cuota_iva)||0);},0); var ivaNet = ivaRep - ivaSop; var irpfTrim = facsTrim.filter(function(f){return f.tipo==='ingreso';}).reduce(function(s,f){return s+Math.abs(parseFloat(f.irpf)||0);},0); // Progreso del trimestre: qué mes del trimestre estamos var idxMesEnTrim = mesesTrim.indexOf(mes); // 0,1,2 o -1 si ya pasó var progreso = idxMesEnTrim === -1 ? 100 : Math.round((idxMesEnTrim + 1) / 3 * 100); var diasAlerta = diasRestantes <= 15 ? 'urgente' : diasRestantes <= 30 ? 'warn' : 'ok'; var colorAlerta = diasAlerta === 'urgente' ? '#DC2626' : diasAlerta === 'warn' ? '#f59e0b' : '#059669'; var el = document.getElementById('semaforo-content'); if (!el) return; el.innerHTML = '
' + '
' + '
Trimestre ' + trimLabels[trimActual] + '
' + '
Progreso del trimestre
' + '
' + '
' + '
' + '
' + progreso + '% del trimestre completado
' + '
' + '
' + '
Plazo Modelo 303
' + '
' + Math.max(0, diasRestantes) + ' días
' + '
Límite: ' + trimCierre[trimActual].toLocaleDateString('es-ES') + '
' + '
' + '
' + '
' + '
IVA Trimestre ' + trimActual + ' · ' + anio + ' (facturas subidas)
' + '
' + '
' + '
Repercutido
' + '
+' + fmtEur(ivaRep,'') + '
' + '
' + '
' + '
Soportado
' + '
−' + fmtEur(ivaSop,'') + '
' + '
' + '
' + '
' + (ivaNet>=0?'A pagar (M.303)':'A devolver') + '
' + '
' + (ivaNet>=0?'+':'') + fmtEur(ivaNet,'') + '
' + '
' + '
' + (irpfTrim > 0 ? '
💼 IRPF retenido este trimestre: ' + fmtEur(irpfTrim,'') + ' (descuenta del Modelo 130)
' : '') + '
' + '
⚠ Estimación orientativa basada en las facturas subidas a Contabilify. Consulta con tu gestor.
'; } // ── ANALÍTICA: DÍAS DE COBRO / CONCENTRACIÓN TEMPORAL ──────────────────── function renderConcentracionTemporal() { var ing = FACTURAS_HISTORIAL.filter(function(f) { if (f.tipo !== 'ingreso' || !f.fecha) return false; if (f.fecha.slice(0,4) !== ANA_ANIO) return false; if (currentPeriod === '1a') return true; if (currentPeriod && currentPeriod.slice(0,4) === 'mes:') { return f.fecha.slice(0,7) === currentPeriod.slice(4); } var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[currentPeriod]; return ms ? ms.indexOf(parseInt(f.fecha.slice(5,7))) !== -1 : true; }); if (!ing.length) { var el = document.getElementById('concentracion-content'); if (el) el.innerHTML = '
Sin ingresos registrados.
'; return; } // Distribución por día del mes var porDia = {}; ing.forEach(function(f) { var d = parseInt(f.fecha.split('-')[2]) || 0; var semana = d <= 7 ? '1-7' : d <= 14 ? '8-14' : d <= 21 ? '15-21' : '22-31'; porDia[semana] = (porDia[semana] || 0) + (parseFloat(f.total)||0); }); var semanas = ['1-7','8-14','15-21','22-31']; var totalIng = ing.reduce(function(s,f){return s+(parseFloat(f.base_imponible)||parseFloat(f.total)||0);},0); // Ticket medio var ticketMedio = totalIng / ing.length; // Mes con más ingresos var porMes = {}; ing.forEach(function(f) { var m = f.fecha.slice(0,7); porMes[m] = (porMes[m]||0) + (parseFloat(f.total)||0); }); var mejorMes = Object.keys(porMes).sort(function(a,b){return porMes[b]-porMes[a];})[0] || ''; var mesesNom = {'01':'Ene','02':'Feb','03':'Mar','04':'Abr','05':'May','06':'Jun','07':'Jul','08':'Ago','09':'Sep','10':'Oct','11':'Nov','12':'Dic'}; var mejorMesLabel = mejorMes ? (mesesNom[mejorMes.split('-')[1]] + ' ' + mejorMes.split('-')[0]) : '—'; var el = document.getElementById('concentracion-content'); if (!el) return; var barras = semanas.map(function(s) { var v = porDia[s] || 0; var pct = totalIng > 0 ? Math.round(v/totalIng*100) : 0; return '
' + 'Días
' + s + '
' + '
' + '
' + (pct>15?'' + pct + '%':'') + '
' + '
' + '' + fmtEur(v,'') + '' + '
'; }).join(''); el.innerHTML = '
' + '
' + '
Ticket medio
' + '
' + fmtEur(ticketMedio,'') + '
' + '
por factura emitida
' + '
' + '
' + '
Mejor mes
' + '
' + mejorMesLabel + '
' + '
' + fmtEur(porMes[mejorMes]||0,'') + '
' + '
' + '
' + '
¿Cuándo cobras? Distribución por semana del mes
' + barras; } // Llamar todas las funciones de analítica al renderizar // ── Comparador en analítica (usa currentPeriod / ANA_ANIO) ──── var ANA_COMP_A = 'T1'; var ANA_COMP_B = 'T2'; function renderComparadorAnalytics() { var el = document.getElementById('ana-comparador-wrap'); if (!el) return; var facs = FACTURAS_HISTORIAL || []; var anios = getAniosDisponibles(facs); if (!anios.length) { el.innerHTML = '

Sin datos.

'; return; } var periodos = ['T1','T2','T3','T4','anio']; var labels = {T1:'T1 (Ene–Mar)',T2:'T2 (Abr–Jun)',T3:'T3 (Jul–Sep)',T4:'T4 (Oct–Dic)',anio:'Año completo'}; function selHTML(id, val, onchg) { return ''; } function anioSelHTML(id, val) { return ''; } var kpiA = calcularKPIs(facs, ANA_COMP_A, ANA_ANIO); var kpiB = calcularKPIs(facs, ANA_COMP_B, ANA_ANIO); function diff(a, b) { var d = b - a, pct = a !== 0 ? Math.round(Math.abs(d/a)*100) : null; var col = d >= 0 ? 'var(--green)' : 'var(--red)'; var arrow = d >= 0 ? '▲' : '▼'; return ' '+arrow+' '+(pct !== null ? pct+'%' : fmtEur(Math.abs(d),''))+''; } function row(label, valA, valB) { return '' + ''+label+'' + ''+fmtEur(valA,'')+'' + ''+fmtEur(valB,'')+diff(valA,valB)+'' + ''; } el.innerHTML = '
' + 'Año:' + anioSelHTML('ana-comp-anio', ANA_ANIO) + 'Periodo A:' + selHTML('ana-comp-a', ANA_COMP_A, 'ANA_COMP_A=this.value;renderComparadorAnalytics()') + 'vs B:' + selHTML('ana-comp-b', ANA_COMP_B, 'ANA_COMP_B=this.value;renderComparadorAnalytics()') + '
' + '
' + '' + '' + '' + '' + '' + '' + row('Ingresos (base)', kpiA.totalIng, kpiB.totalIng) + row('Gastos (base)', kpiA.totalGas, kpiB.totalGas) + row('Beneficio neto', kpiA.neto, kpiB.neto) + row('IVA repercutido', kpiA.ivaRep, kpiB.ivaRep) + row('IVA soportado', kpiA.ivaSop, kpiB.ivaSop) + row('IVA resultado (M.303)', kpiA.ivaNet, kpiB.ivaNet) + row('Nº facturas', kpiA.totalFacs, kpiB.totalFacs) + '
Métrica'+(labels[ANA_COMP_A]||ANA_COMP_A)+''+(labels[ANA_COMP_B]||ANA_COMP_B)+' vs A
' + '
'; } // ── PDF desde analítica (usa periodo activo de analítica) ── function generarInformePDFAnalytics() { var facs = FACTURAS_HISTORIAL || []; if (!facs.length) { toast('Sin facturas para generar informe', 'err'); return; } // Convertir currentPeriod (T1/T2/T3/T4/1a/mes:YYYY-MM) a formato PERFIL_PERIODO var per = currentPeriod; if (per === '1a' || (per && per.slice(0,4) === 'mes:')) per = 'anio'; var _bkFacs = FACTURAS_HISTORIAL; var _bkPer = PERFIL_PERIODO; var _bkAnio = PERFIL_ANIO; PERFIL_PERIODO = per; PERFIL_ANIO = ANA_ANIO; generarInformePDF(); PERFIL_PERIODO = _bkPer; PERFIL_ANIO = _bkAnio; } function renderAllAnalytics() { renderCategoriasGastos(); renderComparativaInteranual(); renderRankingConceptos(); renderSemaforoFiscal(); renderConcentracionTemporal(); renderComparadorAnalytics(); } function marcarNavActivo(screen) { ['analytics','fiscal','historial'].forEach(function(s) { var btn = document.getElementById('btn-nav-' + s); if (btn) btn.classList.toggle('activo', s === screen); }); } function mostrarBannerCliente(screenId, _clienteSnap) { // _clienteSnap: captura del cliente en el momento de la llamada para evitar race conditions var _c = _clienteSnap || CLIENTE_ACTIVO; var esGestor = CURRENT_PROFILE && (planIsGestor(CURRENT_PROFILE.plan) || CURRENT_PROFILE.plan === 'staff'); if (!_c && esGestor) return; if (!_c && !CURRENT_PROFILE) return; var bannerId = 'cliente-banner-' + screenId; var existing = document.getElementById(bannerId); if (existing) existing.remove(); // re-render con nombre actualizado var container = document.querySelector('#s-' + screenId + ' .page-wrap'); if (!container) return; var banner = document.createElement('div'); banner.id = bannerId; banner.className = 'cliente-screen-banner'; var isIndividual = !_c; if (isIndividual) { banner.innerHTML = '' + (CURRENT_PROFILE ? CURRENT_PROFILE.nombre : 'Mis datos') + '' + ''; } else { banner.innerHTML = '' + '' + 'Datos de ' + _c.nombre + '' + '' + ''; } container.insertBefore(banner, container.firstChild); } function volverACliente() { marcarNavActivo(''); // clear all active marks // If no active client, go back to perfil (individual plan) if (!CLIENTE_ACTIVO) { ['analytics','fiscal','historial'].forEach(function(id) { var b = document.getElementById('cliente-banner-' + id); if (b) b.remove(); }); // Sync period back from analytics state PERFIL_ANIO = ANA_ANIO; PERFIL_PERIODO = (currentPeriod === '1a') ? 'anio' : (currentPeriod && currentPeriod.slice(0,4) === 'mes:' ? 'anio' : currentPeriod) || 'anio'; SS('perfil'); renderPerfilIndividual(); return; } ['analytics','fiscal','historial'].forEach(function(id) { var b = document.getElementById('cliente-banner-' + id); if (b) b.remove(); }); var legacyBanner = document.getElementById('cliente-analitica-banner'); if (legacyBanner) legacyBanner.remove(); SS('gestoria'); // Re-render sidebar with active client highlighted, then render client data if (CLIENTES_LIST.length) renderClientesList(); if (CLIENTE_ACTIVO) renderClienteMain(); } function irAFiscal() { if (_clienteLoading) { var wid = setInterval(function(){ if (!_clienteLoading) { clearInterval(wid); irAFiscal(); } }, 100); return; } SS('fiscal'); renderFiscalScreen(); var _snapF = CLIENTE_ACTIVO; setTimeout(function(){ mostrarBannerCliente('fiscal', _snapF); marcarNavActivo('fiscal'); }, 0); } function irAHistorial() { if (_clienteLoading) { var wid2 = setInterval(function(){ if (!_clienteLoading) { clearInterval(wid2); irAHistorial(); } }, 100); return; } SS('historial'); loadHistorial(); var _snapH = CLIENTE_ACTIVO; setTimeout(function(){ mostrarBannerCliente('historial', _snapH); marcarNavActivo('historial'); }, 0); } function renderFiscalScreen() { // Poblar selectores con años reales populateMonthSelect(); // Autoseleccionar trimestre actual var mesActual = new Date().getMonth() + 1; var trimActual = mesActual <= 3 ? 'T1' : mesActual <= 6 ? 'T2' : mesActual <= 9 ? 'T3' : 'T4'; var selTrim = document.getElementById('iva-trimestre-sel'); if (selTrim) selTrim.value = trimActual; // Autoseleccionar año actual en IRPF var anioActual = String(new Date().getFullYear()); var selIrpf = document.getElementById('irpf-anio-sel'); if (selIrpf) { // Buscar si existe el año actual, si no el más reciente var found = false; for (var i = 0; i < selIrpf.options.length; i++) { if (selIrpf.options[i].value === anioActual) { selIrpf.value = anioActual; found = true; break; } } if (!found && selIrpf.options.length > 0) selIrpf.value = selIrpf.options[0].value; } renderIVATrimestral(); renderIRPFAnual(); renderLibro(); } function renderAlerts() { var facturas = FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; if (f.fecha.slice(0,4) !== ANA_ANIO) return false; if (currentPeriod === '1a') return true; if (currentPeriod && currentPeriod.slice(0,4) === 'mes:') { return f.fecha.slice(0,7) === currentPeriod.slice(4); } var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[currentPeriod]; return ms ? ms.indexOf(parseInt(f.fecha.slice(5,7))) !== -1 : true; }); var ing = facturas.filter(function(f){ return f.tipo==='ingreso'; }); var gas = facturas.filter(function(f){ return f.tipo==='gasto'; }); var alertsIng = []; if (!ing.length) { alertsIng.push('info|Aún no tienes ingresos registrados. Sube tus facturas emitidas para ver el análisis.'); } else { var totalIng = ing.reduce(function(s,f){ return s+(parseFloat(f.base_imponible)||parseFloat(f.total)||0); },0); var clientes = {}; ing.forEach(function(f){ var n=f.receptor||f.emisor||'?'; clientes[n]=(clientes[n]||0)+(parseFloat(f.base_imponible)||parseFloat(f.total)||0); }); var topCliArr = Object.keys(clientes).sort(function(a,b){ return clientes[b]-clientes[a]; }); var topCli = topCliArr[0]; var pctTop = totalIng > 0 ? Math.round(clientes[topCli]/totalIng*100) : 0; if (pctTop >= 80) { alertsIng.push('warn|⚠ El ' + pctTop + '% de tus ingresos depende de ' + topCli + '. Riesgo alto de concentración — considera buscar nuevos clientes.'); } else if (pctTop >= 50) { alertsIng.push('warn|' + topCli + ' representa el ' + pctTop + '% de tus ingresos. Diversificar reduciría tu riesgo.'); } else { alertsIng.push('ok|Buena diversificación de clientes. Ninguno supera el 50% de tus ingresos.'); } if (topCliArr.length >= 2) { var top2pct = Math.round((clientes[topCliArr[0]] + clientes[topCliArr[1]]) / totalIng * 100); alertsIng.push('info|Tus 2 principales clientes (' + topCliArr[0] + ' y ' + topCliArr[1] + ') representan el ' + top2pct + '% de tus ingresos.'); } // Media por factura var mediaIng = totalIng / ing.length; alertsIng.push('info|Ticket medio por factura emitida: ' + fmtEur(mediaIng) + '.'); } var alertsGas = []; if (!gas.length) { alertsGas.push('info|Aún no tienes gastos registrados. Sube tus facturas recibidas para ver el análisis.'); } else { var totalGas = gas.reduce(function(s,f){ return s+(parseFloat(f.base_imponible)||parseFloat(f.total)||0); },0); var ivaGas = gas.reduce(function(s,f){ return s+(parseFloat(f.cuota_iva)||0); },0); var irpfGas = gas.reduce(function(s,f){ return s+(parseFloat(f.irpf)||0); },0); var provs = {}; gas.forEach(function(f){ var n=f.emisor||'?'; provs[n]=(provs[n]||0)+(parseFloat(f.base_imponible)||parseFloat(f.total)||0); }); var topProvArr = Object.keys(provs).sort(function(a,b){ return provs[b]-provs[a]; }); var topProv = topProvArr[0]; var pctProv = totalGas > 0 ? Math.round(provs[topProv]/totalGas*100) : 0; if (pctProv >= 60) { alertsGas.push('warn|' + topProv + ' representa el ' + pctProv + '% de tus gastos. Alta dependencia de un solo proveedor.'); } else { alertsGas.push('ok|Gastos bien distribuidos. Tu mayor proveedor (' + topProv + ') solo representa el ' + pctProv + '%.'); } if (ivaGas > 0) { alertsGas.push('info|IVA soportado deducible: ' + fmtEur(ivaGas) + '. Recuerda incluirlo en tu declaración trimestral.'); } if (irpfGas > 0) { alertsGas.push('ok|Retenciones IRPF aplicadas: ' + fmtEur(irpfGas) + '. Ya están descontadas de tus pagos.'); } } // Combine all alerts and write to alerts-comp (the existing container) var allAlerts = alertsIng.concat(alertsGas); var acEl = document.getElementById('alerts-comp'); if (acEl) acEl.innerHTML = allAlerts.map(function(a) { var p = a.split('|'); return '
' + p[1] + '
'; }).join(''); } function getFacturasByPeriod(months) { var now = new Date(); var desde = new Date(now.getFullYear(), now.getMonth() - months + 1, 1); return FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; var parts = f.fecha.split('-'); if (parts.length < 3) return false; var d = new Date(parts[0], parts[1]-1, parts[2]); return d >= desde; }); } function buildPeriodData(facturas) { // Agrupar por mes var byMonth = {}; var seen = {}; facturas.forEach(function(f) { if (!facturaIncluibleEnCalculos(f)) return; var parts = f.fecha.split('-'); if (parts.length < 3) return; var keyFac = claveFacturaCalculo(f); if (seen[keyFac]) return; seen[keyFac] = true; var key = parts[0] + '-' + parts[1]; if (!byMonth[key]) byMonth[key] = {ing: 0, gas: 0, iva: 0, nFacturas: 0}; var total = parseFloat(f.total) || 0; var base = parseFloat(f.base_imponible) || total; var iva = parseFloat(f.cuota_iva) || 0; if (f.tipo === 'ingreso') { byMonth[key].ing += base; } else { byMonth[key].gas += base; } if (f.tipo === 'ingreso') { byMonth[key].ivaRep = (byMonth[key].ivaRep || 0) + iva; } else { byMonth[key].ivaSop = (byMonth[key].ivaSop || 0) + iva; } byMonth[key].nFacturas++; }); var meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; var keys = Object.keys(byMonth).sort(); // Detectar si hay más de un año para mostrar año en las etiquetas var aniosEnDatos = {}; keys.forEach(function(k){ aniosEnDatos[k.split('-')[0]] = true; }); var multiAnio = Object.keys(aniosEnDatos).length > 1; var labels = keys.map(function(k) { var parts = k.split('-'); var mes = meses[parseInt(parts[1])-1]; return multiAnio ? mes + ' ' + parts[0].slice(2) : mes; }); var ing = keys.map(function(k) { return Math.round(byMonth[k].ing * 100) / 100; }); var gas = keys.map(function(k) { return Math.round(byMonth[k].gas * 100) / 100; }); var net = []; var acum = 0; for (var i = 0; i < ing.length; i++) { acum += ing[i] - gas[i]; net.push(Math.round(acum * 100) / 100); } var margin = ing.map(function(v, i) { return v > 0 ? Math.round((v - gas[i]) / v * 100) : 0; }); var totalIng = ing.reduce(function(a,b){return a+b;},0); var totalGas = gas.reduce(function(a,b){return a+b;},0); var totalNeto = totalIng - totalGas; var totalIvaRep = keys.reduce(function(s,k){return s + (byMonth[k].ivaRep||0);},0); var totalIvaSop = keys.reduce(function(s,k){return s + (byMonth[k].ivaSop||0);},0); var totalIva = totalIvaRep - totalIvaSop; // IVA neto: repercutido - soportado var margenProm = totalIng > 0 ? Math.round(totalNeto/totalIng*100) : 0; var nIng = facturas.filter(function(f){return f.tipo==='ingreso';}).length; var nGas = facturas.filter(function(f){return f.tipo==='gasto';}).length; // Alertas automáticas var alerts = []; if (totalIng > 0) alerts.push('ok|Ingresos totales: +' + fmt(totalIng) + ' €.'); if (totalGas > 0) alerts.push('info|Gastos totales: −' + fmt(totalGas) + ' €.'); if (totalIva > 0) alerts.push('info|IVA neto acumulado: +' + fmt(totalIva) + ' €.'); if (margenProm > 70) alerts.push('ok|Buen margen: ' + margenProm + '% de media.'); else if (margenProm > 0) alerts.push('warn|Margen del ' + margenProm + '%. Revisa tus gastos.'); return { labels: labels, ing: ing, gas: gas, net: net, margin: margin, kpiComp: { ing: '+' + fmt(totalIng) + ' €', gas: '−' + fmt(totalGas) + ' €', net: (totalNeto >= 0 ? '+' : '') + fmt(Math.abs(totalNeto)) + ' €', iva: (totalIva >= 0 ? '+' : '') + fmt(totalIva) + ' €', ding: nIng + ' factura' + (nIng !== 1 ? 's' : '') + ' emitida' + (nIng !== 1 ? 's' : ''), dgas: nGas + ' factura' + (nGas !== 1 ? 's' : '') + ' recibida' + (nGas !== 1 ? 's' : ''), dnet: 'Margen del ' + margenProm + '%', diva: 'IVA repercutido − soportado' }, alerts: alerts.length ? alerts : ['info|Sube facturas para ver tu analítica.'] }; } // Datos de demo (se usan si no hay facturas reales) var periodData = { '1m': null, '3m': null, '6m': null, '1a': null }; function getPeriodData(period) { var facturas; if (period === 'T1' || period === 'T2' || period === 'T3' || period === 'T4') { // Trimestre del año que el usuario tiene seleccionado en el selector de mes // Intentar leer el año del selector, fallback al actual var selAnio = ANA_ANIO; var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var meses = trimMap[period]; facturas = FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; var p = f.fecha.split('-'); return p[0] === selAnio && meses.indexOf(parseInt(p[1])) !== -1; }); } else if (period === '1a') { // Año completo - si hay un año seleccionado en el selector úsalo, si no el actual facturas = FACTURAS_HISTORIAL.filter(function(f) { return f.fecha && f.fecha.slice(0,4) === ANA_ANIO; }); } else { // Fallback: últimos N meses (legacy) var months = {'1m':1, '3m':3, '6m':6}; facturas = getFacturasByPeriod(months[period] || 3); } if (!facturas || facturas.length === 0) { return { labels: [], ing: [], gas: [], net: [], margin: [], kpiComp: {ing:'—',gas:'—',net:'—',iva:'—',ding:'Sin datos',dgas:'Sin datos',dnet:'Sin datos',diva:'Sin datos'}, alerts: ['info|Sube facturas para ver tu analítica real aquí.'] }; } return buildPeriodData(facturas); } function setView(v, btn) { currentView = v; buildCharts(); renderCategoriasGastos(); renderRankingConceptos(); } function setPeriod(p, btn) { // Update active button var tbs = document.querySelectorAll('.tf-btn'); for (var i = 0; i < tbs.length; i++) tbs[i].classList.remove('active'); if (btn) btn.classList.add('active'); // Always track current period currentPeriod = p; var anaComp = document.getElementById('ana-comp'); var kpiArea = document.getElementById('kpi-area'); var filtered; if (p === 'T1' || p === 'T2' || p === 'T3' || p === 'T4') { var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; filtered = FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; var parts = f.fecha.split('-'); return parts[0] === ANA_ANIO && trimMap[p].indexOf(parseInt(parts[1])) !== -1; }); // Sync selector to show year var msEl = document.getElementById('month-select'); if (msEl) msEl.value = ANA_ANIO; } else if (p === '1a') { filtered = FACTURAS_HISTORIAL.filter(function(f) { return f.fecha && f.fecha.slice(0,4) === ANA_ANIO; }); var msEl2 = document.getElementById('month-select'); if (msEl2) msEl2.value = ANA_ANIO; } else { // Legacy fallback filtered = getFacturasByPeriod({'1m':1,'3m':3,'6m':6}[p] || 3); } if (!filtered || filtered.length === 0) { if (kpiArea) kpiArea.innerHTML = '
' + '
📭
' + '
Sin facturas en ' + p + ' ' + ANA_ANIO + '
' + '
No hay datos registrados para este periodo
' + '
'; if (anaComp) anaComp.style.visibility = 'hidden'; return; } if (anaComp) anaComp.style.visibility = 'visible'; var d = buildPeriodData(filtered); renderChartsWithData(d); renderTopLists(); renderAlerts(); renderAllAnalytics(); renderComparadorAnalytics(); if (p !== '1a') toast('Mostrando ' + p + ' ' + ANA_ANIO, 'info'); } function setMonth(sel) { if (!sel.value) return; var tbs = document.querySelectorAll('.tf-btn'); for (var i = 0; i < tbs.length; i++) tbs[i].classList.remove('active'); var val = sel.value; var label = sel.options[sel.selectedIndex].text; // Update ANA_ANIO from selection (works for both YYYY and YYYY-MM) ANA_ANIO = val.length >= 4 ? val.slice(0, 4) : ANA_ANIO; // Track period type so sub-charts filter correctly currentPeriod = val.length === 4 ? '1a' : 'mes:' + val; var filtered; // Si es un año completo (formato: "YYYY") o un mes (formato: "YYYY-MM") if (val.length === 4) { // Año completo filtered = FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; return f.fecha.slice(0, 4) === val; }); } else { // Mes específico filtered = FACTURAS_HISTORIAL.filter(function(f) { if (!f.fecha) return false; return f.fecha.slice(0, 7) === val; }); } if (filtered.length === 0) { var kpiAreaSM = document.getElementById('kpi-area'); if (kpiAreaSM) kpiAreaSM.innerHTML = '
' + '
📭
' + '
Sin facturas en ' + label + '
' + '
No hay datos registrados para este periodo
' + '
'; var anaCompSM = document.getElementById('ana-comp'); if (anaCompSM) anaCompSM.style.visibility = 'hidden'; return; } var anaCompSM2 = document.getElementById('ana-comp'); if (anaCompSM2) anaCompSM2.style.visibility = 'visible'; var d = buildPeriodData(filtered); renderChartsWithData(d); renderTopLists(); renderAlerts(); renderAllAnalytics(); } function populateMonthSelect() { var sel = document.getElementById('month-select'); if (!sel) return; var months = {}; var years = {}; FACTURAS_HISTORIAL.forEach(function(f) { if (!f.fecha) return; var ym = f.fecha.slice(0, 7); var y = f.fecha.slice(0, 4); if (!months[ym]) months[ym] = true; if (!years[y]) years[y] = true; }); var meses = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']; var yearKeys = Object.keys(years).sort().reverse(); var monthKeys = Object.keys(months).sort().reverse(); sel.innerHTML = ''; // Primero los años completos if (yearKeys.length > 0) { var grpYears = document.createElement('optgroup'); grpYears.label = 'Año completo'; yearKeys.forEach(function(y) { var opt = document.createElement('option'); opt.value = y; opt.textContent = y; grpYears.appendChild(opt); }); sel.appendChild(grpYears); } // Luego los meses if (monthKeys.length > 0) { var grpMonths = document.createElement('optgroup'); grpMonths.label = 'Mes específico'; monthKeys.forEach(function(ym) { var parts = ym.split('-'); var label = meses[parseInt(parts[1]) - 1] + ' ' + parts[0]; var opt = document.createElement('option'); opt.value = ym; opt.textContent = label; grpMonths.appendChild(opt); }); sel.appendChild(grpMonths); } // Poblar selector año IRPF dinámicamente var irpfSel = document.getElementById('irpf-anio-sel'); if (irpfSel && yearKeys.length > 0) { var currentVal = irpfSel.value; irpfSel.innerHTML = yearKeys.map(function(y) { return ''; }).join(''); // Seleccionar el año más reciente con facturas irpfSel.value = (currentVal && yearKeys.indexOf(currentVal) !== -1) ? currentVal : yearKeys[0]; } // Poblar selector año IVA trimestral var ivaSel = document.getElementById('iva-anio-sel'); if (ivaSel && yearKeys.length > 0) { var currentIvaVal = ivaSel.value; ivaSel.innerHTML = yearKeys.map(function(y) { return ''; }).join(''); ivaSel.value = (currentIvaVal && yearKeys.indexOf(currentIvaVal) !== -1) ? currentIvaVal : yearKeys[0]; } // Poblar selector año del libro var libroAnioSel = document.getElementById('libro-anio-sel'); if (libroAnioSel) { var currentLibroVal = libroAnioSel.value; libroAnioSel.innerHTML = '' + yearKeys.map(function(y) { return ''; }).join(''); if (currentLibroVal && (currentLibroVal === 'todos' || yearKeys.indexOf(currentLibroVal) !== -1)) { libroAnioSel.value = currentLibroVal; } } } function destroyChart(id) { if (charts[id]) { charts[id].destroy(); delete charts[id]; } } function buildCharts() { var d = getPeriodData(currentPeriod); if (!d || !d.labels || d.labels.length === 0) return; renderChartsWithData(d); } function renderChartsFallback(d) { function maxVal(values) { var m = 1; values.forEach(function(v){ m = Math.max(m, Math.abs(parseFloat(v) || 0)); }); return m; } function money(v) { return (parseFloat(v) || 0).toLocaleString('es-ES') + '€'; } var comp = document.getElementById('chartComp'); if (comp && comp.parentNode) { var maxComp = maxVal((d.ing || []).concat(d.gas || [])); comp.parentNode.innerHTML = '
' + d.labels.map(function(label, i) { var ing = parseFloat(d.ing[i]) || 0; var gas = parseFloat(d.gas[i]) || 0; return '
' + '' + label + '' + '' + '' + '' + money(ing) + ' / ' + money(gas) + '' + '
'; }).join('') + '
'; } var margin = document.getElementById('chartMargin'); if (margin && margin.parentNode) { margin.parentNode.innerHTML = '
' + d.labels.map(function(label, i) { var val = parseFloat(d.margin[i]) || 0; return '
' + '' + label + '' + '' + '' + val + '%' + '
'; }).join('') + '
'; } } function renderChartsWithData(d) { var kh = ''; var kc = d.kpiComp; kh = '
¿Cuánto he ingresado?
' + kc.ing + '
' + kc.ding + '
' + '
¿Cuánto he gastado?
' + kc.gas + '
' + kc.dgas + '
' + '
¿Cuánto me queda neto?
' + kc.net + '
' + kc.dnet + '
' + '
¿Cuánto IVA declaro?
' + kc.iva + '
' + kc.diva + '
'; document.getElementById('kpi-area').innerHTML = kh; if (typeof Chart === 'undefined') { renderChartsFallback(d); return; } var tc = '#6B7280'; var gc = 'rgba(0,0,0,.05)'; var baseOpts = function(yFmt) { return { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }, scales: { x: { ticks: { color: tc, font: { size: 10 } }, grid: { display: false } }, y: { ticks: { color: tc, font: { size: 10 }, callback: yFmt }, grid: { color: gc } } } }; }; if (currentView === 'comp') { destroyChart('comp'); var ctxC = document.getElementById('chartComp'); if (ctxC) { charts['comp'] = new Chart(ctxC, { type: 'bar', data: { labels: d.labels, datasets: [ { label: 'Ingresos', data: d.ing, backgroundColor: '#2049ff', borderRadius: 6 }, { label: 'Gastos', data: d.gas, backgroundColor: 'rgba(229,72,77,.82)', borderRadius: 6 }, ]}, options: baseOpts(function(v){ return v != null ? v.toLocaleString('es-ES') + '€' : ''; }) }); } destroyChart('net'); var ctxN = document.getElementById('chartNet'); if (ctxN) { charts['net'] = new Chart(ctxN, { type: 'line', data: { labels: d.labels, datasets: [ { label: 'Saldo neto', data: d.net, borderColor: '#2049ff', backgroundColor: 'rgba(32,73,255,.10)', fill: true, tension: .35, pointRadius: 4, pointBackgroundColor: '#2049ff', spanGaps: false } ]}, options: baseOpts(function(v){ return v != null ? v.toLocaleString('es-ES') + '€' : ''; }) }); } destroyChart('margin'); var ctxM = document.getElementById('chartMargin'); if (ctxM) { charts['margin'] = new Chart(ctxM, { type: 'bar', data: { labels: d.labels, datasets: [ { label: 'Margen %', data: d.margin, backgroundColor: d.margin.map(function(v){ return v >= 70 ? '#12906F' : '#98A2B3'; }), borderRadius: 6 } ]}, options: baseOpts(function(v){ return v != null ? v + '%' : ''; }) }); } var ac = document.getElementById('alerts-comp'); if (ac) ac.innerHTML = d.alerts.map(function(a){ var p=a.split('|'); return '
' + p[1] + '
'; }).join(''); } } // ── Generador de informe PDF ────────────────────────── function generarInformePDF() { var facs = FACTURAS_HISTORIAL || []; if (!facs.length) { toast('Sin facturas para generar informe', 'err'); return; } var anio = PERFIL_ANIO || String(new Date().getFullYear()); var per = PERFIL_PERIODO || 'anio'; var kpi = calcularKPIs(facs, per, anio); var labelPer = { anio: 'Año completo', T1: 'Q1 (Ene–Mar)', T2: 'Q2 (Abr–Jun)', T3: 'Q3 (Jul–Sep)', T4: 'Q4 (Oct–Dic)' }; var periodoLabel = labelPer[per] || per; var nombre = (CURRENT_PROFILE && CURRENT_PROFILE.nombre) || 'Mi empresa'; var nif = (CURRENT_PROFILE && CURRENT_PROFILE.cif) || ''; var hoy = new Date().toLocaleDateString('es-ES', { year:'numeric', month:'long', day:'numeric' }); // ── Datos mensuales para tabla de evolución ── var byMonth = {}; var meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; facs.forEach(function(f) { if (!f.fecha || f.deleted === true) return; if (f.status && f.status !== 'ok') return; var p = f.fecha.split('-'); if (p[0] !== anio) return; if (per !== 'anio') { var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[per]; if (!ms || ms.indexOf(parseInt(p[1])) === -1) return; } var key = p[0] + '-' + p[1]; if (!byMonth[key]) byMonth[key] = { ing: 0, gas: 0 }; var base = parseFloat(f.base_imponible) || parseFloat(f.total) || 0; if (f.tipo === 'ingreso') byMonth[key].ing += base; else byMonth[key].gas += base; }); var monthKeys = Object.keys(byMonth).sort(); var tablaEvolucion = monthKeys.length > 1 ? ( '' + monthKeys.map(function(k) { var p = k.split('-'); var mesLabel = meses[parseInt(p[1])-1] + ' ' + p[0]; var ing = byMonth[k].ing, gas = byMonth[k].gas, neto = ing - gas; return '' + '' + ''; }).join('') + '
MesIngresosGastosResultado
' + mesLabel + '+' + ing.toLocaleString('es-ES', {minimumFractionDigits:2}) + '€−' + gas.toLocaleString('es-ES', {minimumFractionDigits:2}) + '€' + (neto>=0?'+':'') + neto.toLocaleString('es-ES', {minimumFractionDigits:2}) + '€
' ) : ''; // ── Top proveedores ── var provs = {}; (kpi.gas || []).forEach(function(f) { var n = f.emisor || 'Desconocido'; provs[n] = (provs[n] || 0) + (parseFloat(f.base_imponible) || parseFloat(f.total) || 0); }); var topProv = Object.keys(provs).sort(function(a,b){ return provs[b]-provs[a]; }).slice(0,5); // ── Alertas basadas en datos ── var alertas = []; if (kpi.facsBajaConf > 0) alertas.push('⚠ ' + kpi.facsBajaConf + ' factura' + (kpi.facsBajaConf>1?'s':'') + ' con confianza baja (revisar antes de declarar)'); if (kpi.confMedia < 80) alertas.push('⚠ Confianza media del periodo: ' + kpi.confMedia + '% — algunos datos pueden requerir revisión'); if (kpi.ivaNet > 0 && kpi.ivaNet > kpi.ivaRep * 0.3) alertas.push('ℹ IVA a pagar elevado respecto a ingresos. Revisa si hay gastos deducibles sin procesar.'); // ── Helper formato ── function fmt(n) { return (n||0).toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '€'; } var html = '' + 'Informe ' + periodoLabel + ' ' + anio + '' + '
' + // Cabecera '
' + '

Informe financiero · ' + periodoLabel + ' ' + anio + '

' + '
' + nombre + (nif ? ' · ' + nif : '') + ' · Generado el ' + hoy + '
Informe PDF
' + '
' + // Resumen ejecutivo '

Resumen ejecutivo

' + '
' + '
Ingresos (base imponible)
+' + fmt(kpi.totalIng) + '
' + '
Gastos (base imponible)
−' + fmt(kpi.totalGas) + '
' + '
Beneficio neto
' + (kpi.neto>=0?'+':'') + fmt(kpi.neto) + '
' + '
Margen
' + kpi.margen + '%
' + '
' + // IVA '

IVA (estimación M.303)

' + '
' + '
IVA repercutido (ventas)
+' + fmt(kpi.ivaRep) + '
' + '
IVA soportado (gastos)
−' + fmt(kpi.ivaSop) + '
' + '
' + (kpi.ivaNet>=0 ? 'A pagar M.303' : 'A devolver') + '
' + (kpi.ivaNet>=0?'+':'') + fmt(kpi.ivaNet) + '
' + '
' + // Evolución mensual (tablaEvolucion ? '

Evolución mensual

' + tablaEvolucion : '') + // Top proveedores (topProv.length ? ( '

Top proveedores por gasto

' + '' + topProv.map(function(p, i) { return ''; }).join('') + '
#ProveedorTotal
' + (i+1) + '' + p + '−' + fmt(provs[p]) + '
' ) : '') + // Calidad de datos '

Calidad de datos

' + '
Confianza media del periodo: ' + (kpi.confMedia||100) + '%' + '
' + '
' + (kpi.facsBajaConf > 0 ? '

⚠ ' + kpi.facsBajaConf + ' factura' + (kpi.facsBajaConf>1?'s':'') + ' con confianza baja. Revísalas antes de presentar declaraciones.

' : '

✓ Todos los datos tienen confianza alta.

') + // Alertas (alertas.length ? '

Alertas

' + alertas.map(function(a){ return '
' + a + '
'; }).join('') : '') + // Footer '' + '
'; // Abrir en ventana nueva y disparar print var win = window.open('', '_blank'); if (!win) { toast('Activa las ventanas emergentes para descargar el PDF', 'err'); return; } win.document.write(html); win.document.close(); win.onload = function() { win.focus(); win.print(); }; } // ── PRICING ── var _pricingTab = 'gestoria'; function setPricingTab(tab) { _pricingTab = tab; document.getElementById('pricing-individual').style.display = tab === 'individual' ? 'grid' : 'none'; document.getElementById('pricing-gestoria').style.display = tab === 'gestoria' ? 'grid' : 'none'; document.getElementById('ptab-individual').classList.toggle('active', tab === 'individual'); document.getElementById('ptab-gestoria').classList.toggle('active', tab === 'gestoria'); } function goToPricing(tab) { _pricingTab = tab || 'gestoria'; trackEvent('view_pricing', { pricing_tab: _pricingTab }); SS('pricing'); } function toggleBilling() { billingAnnual = !billingAnnual; var sw = document.getElementById('billing-switch'); var thumb = document.getElementById('billing-thumb'); var lblM = document.getElementById('lbl-mensual'); var lblA = document.getElementById('lbl-anual'); if (billingAnnual) { sw.style.background = 'var(--brand)'; thumb.style.left = '25px'; lblM.style.color = 'var(--ink3)'; lblA.style.color = 'var(--ink)'; } else { sw.style.background = 'var(--line)'; thumb.style.left = '3px'; lblM.style.color = 'var(--ink)'; lblA.style.color = 'var(--ink3)'; } var precios = { starter: { m: 19, a: 15.83, ay: 190 }, pro: { m: 39, a: 32.50, ay: 390 }, business: { m: 69, a: 57.50, ay: 690 }, gestoria_starter: { m: 79, a: 65.83, ay: 790 }, gestoria_pro: { m: 179, a: 149.17, ay: 1790 }, gestoria_business: { m: 349, a: 290.83, ay: 3490 }, gestoria_scale: { m: 749, a: 624.17, ay: 7490 } }; ['starter','pro','business','gestoria_starter','gestoria_pro','gestoria_business','gestoria_scale'].forEach(function(plan) { var el = document.getElementById('price-' + plan); if (!el) return; var p = precios[plan]; if (billingAnnual) { el.innerHTML = '' + p.a.toFixed(2).replace('.',',') + '/mes' + p.ay + '€/año'; } else { el.innerHTML = '' + p.m + '/mes'; } }); } function goCheckout(plan) { window._checkoutPlan = plan; var pricesM = { starter: 19, pro: 39, business: 69, gestoria_starter: 79, gestoria_pro: 179, gestoria_business: 349, gestoria_scale: 749 }; var pricesA = { starter: 190, pro: 390, business: 690, gestoria_starter: 790, gestoria_pro: 1790, gestoria_business: 3490, gestoria_scale: 7490 }; var selectedValue = billingAnnual ? pricesA[plan] : pricesM[plan]; trackEvent('subscribe_click', { plan: plan, plan_id: plan, billing_period: billingAnnual ? 'annual' : 'monthly', value: selectedValue, currency: 'EUR' }); trackEvent('select_plan', { plan: plan, plan_id: plan, billing_period: billingAnnual ? 'annual' : 'monthly', value: selectedValue, currency: 'EUR' }); trackEvent('begin_checkout', { plan: plan, plan_id: plan, billing_period: billingAnnual ? 'annual' : 'monthly', value: selectedValue, currency: 'EUR' }); var names = { starter: 'Plan Starter', pro: 'Plan Pro ⭐', business: 'Plan Max', gestoria_starter: 'Gestoría Starter', gestoria_pro: 'Gestoría Pro', gestoria_business: 'Gestoría Business', gestoria_scale: 'Gestoría Scale' }; var p = billingAnnual ? pricesA[plan] : pricesM[plan]; var iva = Math.round(p * 21) / 100; document.getElementById('order-name').textContent = names[plan]; document.getElementById('order-line-name').textContent = names[plan] + (billingAnnual ? ' · Anual (2 meses gratis)' : ' · Mensual'); document.getElementById('order-price').textContent = p.toFixed(2).replace('.', ',') + ' €'; document.getElementById('order-iva').textContent = iva.toFixed(2).replace('.', ',') + ' €'; document.getElementById('order-total').textContent = (p + iva).toFixed(2).replace('.', ',') + ' €'; if (CURRENT_PROFILE && CURRENT_USER) { var nombreInput = document.querySelector('#s-checkout input[placeholder="Tu nombre completo"]'); var emailInput = document.querySelector('#s-checkout input[type="email"]'); if (nombreInput && CURRENT_PROFILE.nombre) nombreInput.value = CURRENT_PROFILE.nombre; if (emailInput && CURRENT_USER.email) emailInput.value = CURRENT_USER.email; } SS('checkout'); } function solicitarPlanDesdeCheckout() { var planName = (document.getElementById('order-name') || {}).textContent || 'un plan de Contabilify'; var total = (document.getElementById('order-total') || {}).textContent || ''; trackEvent('subscription_request', { plan: window._checkoutPlan || 'unknown', plan_name: planName, billing_period: billingAnnual ? 'annual' : 'monthly' }); abrirSoporte(); var asunto = document.getElementById('sop-asunto'); var mensaje = document.getElementById('sop-mensaje'); if (asunto) asunto.value = 'Pregunta sobre mi plan o facturación'; if (mensaje && !mensaje.value) { mensaje.value = 'Hola, quiero activar ' + planName + (total ? ' (' + total + ' con IVA)' : '') + '. Indícame cómo completar el alta mientras se conecta la pasarela de pago.'; var chars = document.getElementById('sop-chars'); if (chars) chars.textContent = mensaje.value.length; } } function toggleFaq(el) { var a = el.nextElementSibling; var sp = el.querySelector('span'); a.classList.toggle('open'); sp.textContent = a.classList.contains('open') ? '−' : '+'; } // ── CHECKOUT ── function elegirBilling(anual) { document.getElementById('modal-billing').style.display = 'none'; billingAnnual = anual; var plan = window._checkoutPlan; goCheckout(plan || 'starter'); } function completePay() { solicitarPlanDesdeCheckout(); toast('Te hemos llevado a soporte para activar el plan.', 'info'); } // ── SETTINGS ── function showPanel(id, el) { var items = document.querySelectorAll('.snav-item'); for (var i = 0; i < items.length; i++) items[i].classList.remove('active'); var panels = document.querySelectorAll('.settings-panel'); for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active'); if (el) el.classList.add('active'); var p = document.getElementById('sp-' + id); if (p) p.classList.add('active'); if (id === 'seguridad') loadSessionInfo(); } function loadSessionInfo() { var deviceEl = document.getElementById('session-device'); var infoEl = document.getElementById('session-info'); if (!deviceEl) return; // Detectar navegador y SO var ua = navigator.userAgent; var os = 'Dispositivo desconocido'; if (/Windows/.test(ua)) os = 'Windows'; else if (/Mac OS X/.test(ua)) os = 'Mac'; else if (/Android/.test(ua)) os = 'Android'; else if (/iPhone|iPad/.test(ua)) os = 'iPhone/iPad'; else if (/Linux/.test(ua)) os = 'Linux'; var browser = 'Navegador desconocido'; if (/Chrome/.test(ua) && !/Edge/.test(ua)) browser = 'Chrome'; else if (/Firefox/.test(ua)) browser = 'Firefox'; else if (/Safari/.test(ua) && !/Chrome/.test(ua)) browser = 'Safari'; else if (/Edge/.test(ua)) browser = 'Edge'; deviceEl.textContent = os + ' · ' + browser; // Obtener IP pública fetch('https://api.ipify.org?format=json') .then(function(r) { return r.json(); }) .then(function(d) { var now = new Date(); var hora = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0'); infoEl.textContent = 'Sesión actual · IP: ' + d.ip + ' · ' + hora; }) .catch(function() { infoEl.textContent = 'Sesión actual'; }); } // ── HISTORIAL ── var HISTORIAL_DATA = []; var HISTORIAL_FILTRO = 'all'; function mostrarFiltroExcelAnalytics() { var modal = document.getElementById('modal-filtro-excel'); if (!modal) return; var data = (HISTORIAL_DATA && HISTORIAL_DATA.length ? HISTORIAL_DATA : null) || (FACTURAS_HISTORIAL && FACTURAS_HISTORIAL.length ? FACTURAS_HISTORIAL : []); var years = {}; data.forEach(function(f) { var p = partesFechaFactura(f.fecha); if (p) years[p.anio] = true; }); var yearKeys = Object.keys(years).sort().reverse(); var selAnio = document.getElementById('filtro-excel-anio'); selAnio.innerHTML = ''; yearKeys.forEach(function(y) { selAnio.innerHTML += ''; }); // Pre-seleccionar año activo if (ANA_ANIO && years[ANA_ANIO]) selAnio.value = ANA_ANIO; var selMes = document.getElementById('filtro-excel-mes-sel'); if (selMes) selMes.value = 'todos'; var selTipo = document.getElementById('filtro-excel-tipo'); if (selTipo) selTipo.value = 'todos'; modal.style.display = 'flex'; } function mostrarFiltroExcelHistorial() { var modal = document.getElementById('modal-filtro-excel-historial'); if (!modal) return; var meses = {}; HISTORIAL_DATA.forEach(function(f) { var p = partesFechaFactura(f.fecha); if (p) meses[p.anio + '-' + p.mes] = true; }); var sel = document.getElementById('filtro-excel-his-mes'); sel.innerHTML = ''; var nombres = {'01':'Enero','02':'Febrero','03':'Marzo','04':'Abril','05':'Mayo','06':'Junio','07':'Julio','08':'Agosto','09':'Septiembre','10':'Octubre','11':'Noviembre','12':'Diciembre'}; Object.keys(meses).sort().forEach(function(key) { var p = key.split('-'); sel.innerHTML += ''; }); modal.style.display = 'flex'; } function descargarExcelHistorialFiltrado() { var mes = document.getElementById('filtro-excel-his-mes').value; var tipo = document.getElementById('filtro-excel-his-tipo').value; var rows = HISTORIAL_DATA.filter(function(f) { var okMes = true, okTipo = true; if (mes !== 'todos') { var p = partesFechaFactura(f.fecha); okMes = !!(p && p.anio + '-' + p.mes === mes); } if (tipo !== 'todos') okTipo = f.tipo === tipo; return okMes && okTipo; }); if (!rows.length) { toast('No hay facturas con ese filtro', 'err'); return; } document.getElementById('modal-filtro-excel-historial').style.display = 'none'; var fields = ['numero_factura','fecha','tipo','emisor','cif_emisor','receptor','cif_receptor','base_imponible','iva_porcentaje','cuota_iva','irpf','total','concepto','observaciones']; fetch('/api/export-excel', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ rows: rows, fields: fields }) }) .then(function(r){ return r.blob(); }) .then(function(blob){ var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; var sufijo = mes !== 'todos' ? '_' + mes : ''; var tipo_str = tipo !== 'todos' ? '_' + tipo + 's' : ''; a.download = 'facturas' + sufijo + tipo_str + '.xlsx'; a.click(); URL.revokeObjectURL(url); toast('Excel descargado ✓', 'ok'); }).catch(function(){ toast('Error al descargar', 'err'); }); } function loadHistorial() { if (!TOKEN) return; // En modo gestoría filtrar por cliente activo var isGestoria = !!CLIENTE_ACTIVO; // Si ya tenemos datos del cliente activo en FACTURAS_CLIENTE (gestoría), usarlos if (isGestoria && FACTURAS_CLIENTE && FACTURAS_CLIENTE.length) { HISTORIAL_DATA = FACTURAS_CLIENTE; renderHistorial(); return; } // Si ya tenemos datos de analítica individual, reutilizarlos if (!isGestoria && FACTURAS_HISTORIAL.length) { HISTORIAL_DATA = FACTURAS_HISTORIAL; renderHistorial(); return; } document.getElementById('historial-tbody').innerHTML = 'Cargando...'; var allData = []; function fetchPage(cursor) { var baseUrl = '/api/facturas'; if (isGestoria && CLIENTE_ACTIVO && CLIENTE_ACTIVO.id) baseUrl += '?cliente_id=' + CLIENTE_ACTIVO.id; var sep = baseUrl.indexOf('?') !== -1 ? '&' : '?'; var url = cursor ? baseUrl + sep + 'cursor=' + encodeURIComponent(cursor) : baseUrl; fetch(url, { headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ if (!d.ok) return; allData = allData.concat(d.data || []); if (d.next_cursor) { fetchPage(d.next_cursor); } else { HISTORIAL_DATA = allData; FACTURAS_HISTORIAL = allData; FACTURAS_HISTORIAL_GLOBAL = allData; renderHistorial(); } }).catch(function(){ if (allData.length) { HISTORIAL_DATA = allData; renderHistorial(); } }); } fetchPage(null); } function renderHistorial() { var facturas = HISTORIAL_FILTRO === 'all' ? HISTORIAL_DATA : HISTORIAL_DATA.filter(function(f){ return f.tipo === HISTORIAL_FILTRO; }); var el = document.getElementById('historial-tbody'); if (!facturas.length) { el.innerHTML = 'No hay facturas en el historial.'; return; } el.innerHTML = facturas.map(function(f) { var tag = f.tipo === 'ingreso' ? 'Ingreso' : 'Gasto'; return '' + '' + tag + '' + '' + (f.tipo==='ingreso' ? (f.receptor||f.emisor||'—') : (f.emisor||'—')) + '' + '' + (f.numero_factura || '—') + '' + '' + (f.fecha ? f.fecha.slice(0,10) : '—') + '' + '' + (f.total ? '' + fmt(f.total) + ' €' : '—') + '' + '' + '' + '' + '' + ''; }).join(''); // Actualizar contador var ing = HISTORIAL_DATA.filter(function(f){return f.tipo==='ingreso';}).length; var gas = HISTORIAL_DATA.filter(function(f){return f.tipo==='gasto';}).length; var resumen = document.getElementById('historial-resumen'); if (resumen) resumen.textContent = HISTORIAL_DATA.length + ' facturas · ' + ing + ' ingresos · ' + gas + ' gastos'; } function filtrarHistorial(tipo, btn) { HISTORIAL_FILTRO = tipo; document.querySelectorAll('.htab').forEach(function(b){ b.classList.remove('active'); }); btn.classList.add('active'); renderHistorial(); } function abrirDetalleFactura(id) { var f = HISTORIAL_DATA.find(function(x){ return x.id === id; }); if (!f) return; document.getElementById('detalle-id').value = f.id; document.getElementById('detalle-emisor').textContent = f.emisor || '—'; document.getElementById('detalle-numero').textContent = f.numero_factura || '—'; document.getElementById('detalle-fecha').textContent = f.fecha ? f.fecha.slice(0,10) : '—'; document.getElementById('detalle-base').textContent = f.base_imponible ? fmt(f.base_imponible) + ' €' : '—'; document.getElementById('detalle-iva').textContent = f.cuota_iva ? fmt(f.cuota_iva) + ' €' : '—'; document.getElementById('detalle-irpf').textContent = f.irpf ? fmt(f.irpf) + ' €' : '—'; document.getElementById('detalle-total').textContent = f.total ? '' + fmt(f.total) + ' €' : '—'; document.getElementById('detalle-concepto').textContent = f.concepto || '—'; document.getElementById('detalle-tipo').value = f.tipo || 'gasto'; document.getElementById('modal-detalle').style.display = 'flex'; } function cerrarDetalleFactura() { document.getElementById('modal-detalle').style.display = 'none'; } function guardarTipoFactura() { var id = document.getElementById('detalle-id').value; var tipo = document.getElementById('detalle-tipo').value; fetch('/api/facturas/' + id, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ tipo: tipo }) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { var f = HISTORIAL_DATA.find(function(x){ return x.id === id; }); if (f) f.tipo = tipo; cerrarDetalleFactura(); renderHistorial(); toast('Tipo actualizado ✓', 'ok'); } else { toast('Error al actualizar', 'err'); } }).catch(function(){ toast('Error de conexión', 'err'); }); } function confirmarEliminarFactura(id, nombre) { (document.getElementById('eliminar-factura-id') || {value:''}).value = id; (document.getElementById('eliminar-factura-nombre') || {textContent:''}).textContent = nombre || 'esta factura'; document.getElementById('modal-eliminar-factura').style.display = 'flex'; } function cerrarEliminarFactura() { document.getElementById('modal-eliminar-factura').style.display = 'none'; } function ejecutarEliminarFactura() { var id = (document.getElementById('eliminar-factura-id') || {value:''}).value; fetch('/api/facturas/' + id, { method: 'DELETE', headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { HISTORIAL_DATA = HISTORIAL_DATA.filter(function(f){ return f.id !== id; }); cerrarEliminarFactura(); renderHistorial(); toast('Factura eliminada ✓', 'ok'); } else { toast('Error al eliminar', 'err'); } }).catch(function(){ toast('Error de conexión', 'err'); }); } // CLIENTES_LIST y CLIENTE_ACTIVO definidos en STORE arriba var FACTURAS_CLIENTE = []; function loadGestoria() { if (!TOKEN) return; // no hacer fetch sin token fetch('/api/clientes', { headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { CLIENTES_LIST = d.data || []; renderClientesList(); if (CLIENTES_LIST.length > 0) { // Restaurar cliente guardado en localStorage o usar el primero var savedId = localStorage.getItem('_gestoria_ca_id'); var toSelect = CLIENTE_ACTIVO ? (CLIENTES_LIST.find(function(c){ return c.id === CLIENTE_ACTIVO.id; }) || CLIENTES_LIST[0]) : savedId ? (CLIENTES_LIST.find(function(c){ return c.id === savedId; }) || CLIENTES_LIST[0]) : CLIENTES_LIST[0]; selectCliente(toSelect); } else { document.getElementById('gestoria-main').innerHTML = '
👥
Aún no tienes clientes
Haz clic en "Nuevo cliente" para añadir el primero.
'; } } }).catch(function(){}); } function renderClientesList() { var el = document.getElementById('gestoria-clientes-list'); if (!el) return; if (!CLIENTES_LIST.length) { el.innerHTML = '
Sin clientes aún
'; return; } el.innerHTML = CLIENTES_LIST.map(function(c) { var initials = c.nombre.split(' ').slice(0,2).map(function(w){ return w[0]; }).join('').toUpperCase(); var active = CLIENTE_ACTIVO && CLIENTE_ACTIVO.id === c.id ? 'gestoria-client-item active' : 'gestoria-client-item'; // KPIs rápidos si hay datos precalculados var nFacs = c._nFacturas != null ? c._nFacturas : (c.nFacturas || ''); var badgeFacs = nFacs ? '' + nFacs + '' : ''; return '
' + '
' + initials + '
' + '
' + '
' + '
' + c.nombre + '
' + badgeFacs + '
' + '
' + (c.nif || '—') + '
' + '
' + '
'; }).join(''); } // Cache de facturas por cliente para evitar fetches repetidos var _FACTURAS_CACHE = {}; var _clienteLoading = false; // flag: facturas del cliente en carga function selectCliente(cliente, el) { CLIENTE_ACTIVO = typeof cliente === 'string' ? JSON.parse(cliente) : cliente; APP_STATE.clienteActivoId = CLIENTE_ACTIVO.id; // Reset period filter for new client so it auto-selects their year CLIENTE_PERIODO = 'anio'; CLIENTE_ANIO = String(new Date().getFullYear()); ANA_ANIO = String(new Date().getFullYear()); // will be updated after fetch FACTURAS_CLIENTE = []; // limpiar datos del cliente anterior // Invalidar cache de analítica para forzar datos frescos FACTURAS_HISTORIAL = []; FACTURAS_HISTORIAL_GLOBAL = []; // Limpiar historial y analítica del cliente anterior HISTORIAL_DATA = []; HISTORIAL_FILTRO = 'all'; // Invalidar caché de analítica para forzar recarga if (typeof charts !== 'undefined' && charts) { Object.values(charts).forEach(function(ch){ try { ch.destroy(); } catch(e){} }); charts = {}; } document.querySelectorAll('.gestoria-client-item').forEach(function(i){i.classList.remove('active')}); if (el) el.classList.add('active'); // Si tenemos caché reciente (< 2 min), usar directamente sin spinner var cacheKey = CLIENTE_ACTIVO.id; var cached = _FACTURAS_CACHE[cacheKey]; var ahora = Date.now(); if (cached && (ahora - cached.ts) < 120000) { // Re-sanitizar por si la caché tiene datos de antes de este fix FACTURAS_CLIENTE = (cached.data || []).filter(function(f) { if (!f) return false; if (!f.tipo || (f.tipo !== 'ingreso' && f.tipo !== 'gasto')) return false; if (!f.fecha || typeof f.fecha !== 'string' || f.fecha.length < 7) return false; if (f.deleted === true) return false; return true; }); CLIENTE_ANIO = cached.anio || CLIENTE_ANIO; _clienteLoading = false; renderClienteMain(); return; } // Sin caché - mostrar spinner y fetchear _clienteLoading = true; document.getElementById('gestoria-main').innerHTML = '
'; var fetchingForId = CLIENTE_ACTIVO.id; // snapshot to avoid race fetch('/api/clientes/' + CLIENTE_ACTIVO.id + '/facturas', { headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ // Discard result if user switched to a different client meanwhile if (!CLIENTE_ACTIVO || CLIENTE_ACTIVO.id !== fetchingForId) return; // Sanitizar datos del cliente igual que en analítica individual FACTURAS_CLIENTE = (d.data || []).filter(function(f) { if (!f) return false; if (!f.tipo || (f.tipo !== 'ingreso' && f.tipo !== 'gasto')) return false; if (!f.fecha || typeof f.fecha !== 'string' || f.fecha.length < 7) return false; if (f.deleted === true) return false; return true; }); // Auto-select most recent year for this client var aniosCliente = []; FACTURAS_CLIENTE.forEach(function(f){ if (f.fecha) { var y=f.fecha.slice(0,4); if (aniosCliente.indexOf(y)===-1) aniosCliente.push(y); }}); if (aniosCliente.length) { aniosCliente.sort().reverse(); CLIENTE_ANIO = aniosCliente[0]; } // Guardar en caché _FACTURAS_CACHE[cacheKey] = { data: FACTURAS_CLIENTE, ts: Date.now(), anio: CLIENTE_ANIO }; _clienteLoading = false; renderClienteMain(); }).catch(function(e){ console.error('Error loading client invoices:', e); FACTURAS_CLIENTE = []; _clienteLoading = false; renderClienteMain(); toast('Error al cargar las facturas del cliente', 'err'); }); } // Invalidar caché de un cliente (llamar tras subir facturas) function invalidarCacheCliente(clienteId) { if (clienteId) delete _FACTURAS_CACHE[clienteId]; else _FACTURAS_CACHE = {}; } function facturaValidaParaCliente(f) { if (!f) return false; if (!f.tipo || (f.tipo !== 'ingreso' && f.tipo !== 'gasto')) return false; if (!f.fecha || typeof f.fecha !== 'string' || f.fecha.length < 7) return false; if (f.deleted === true) return false; return true; } function claveFacturaCliente(f) { if (!f) return ''; var claveNegocio = [ normalizarNumero(f.numero_factura || ''), String(f.cif_emisor || '').trim().toUpperCase(), String(f.fecha || '').slice(0, 10), String(Math.round((parseFloat(f.total) || 0) * 100)) ].join('|'); if (claveNegocio !== '|||0') return 'biz:' + claveNegocio; if (f.id) return 'id:' + f.id; return ''; } function mezclarFacturasCliente(base, nuevas) { var out = []; var pos = {}; function add(f) { if (!facturaValidaParaCliente(f)) return; var k = claveFacturaCliente(f); if (!k || k === '|||0') k = 'obj:' + JSON.stringify(f); if (pos[k] === undefined) { pos[k] = out.length; out.push(f); } else { out[pos[k]] = f; } } (base || []).forEach(add); (nuevas || []).forEach(add); return out; } function sincronizarFacturasClienteProcesadas() { if (!CLIENTE_ACTIVO || !CLIENTE_ACTIVO.id || !FACTURAS_PROCESADAS.length) return; var mezcladas = mezclarFacturasCliente(FACTURAS_CLIENTE, FACTURAS_PROCESADAS); FACTURAS_CLIENTE = mezcladas; FACTURAS_HISTORIAL = mezcladas; FACTURAS_HISTORIAL_GLOBAL = mezcladas; var aniosCliente = []; mezcladas.forEach(function(f) { if (f.fecha) { var y = f.fecha.slice(0, 4); if (aniosCliente.indexOf(y) === -1) aniosCliente.push(y); } }); if (aniosCliente.length) { aniosCliente.sort().reverse(); CLIENTE_ANIO = aniosCliente[0]; ANA_ANIO = aniosCliente[0]; } _FACTURAS_CACHE[CLIENTE_ACTIVO.id] = { data: mezcladas, ts: Date.now(), anio: CLIENTE_ANIO }; } // ── Shared period filter for KPIs ── var PERFIL_PERIODO = 'anio'; // 'T1','T2','T3','T4','anio' var PERFIL_ANIO = String(new Date().getFullYear()); function _kpiRound(n) { return Math.round(n * 100) / 100; } function facturaIncluibleEnCalculos(f) { if (!f) return false; if (!f.fecha || typeof f.fecha !== 'string') return false; if (f.status && f.status !== 'ok') return false; if (f.deleted === true) return false; if (f.tipo !== 'ingreso' && f.tipo !== 'gasto') return false; var total = parseFloat(f.total); return !isNaN(total); } function claveFacturaCalculo(f) { var total = parseFloat(f.total) || 0; return [f.numero_factura || '', f.cif_emisor || '', f.fecha || '', Math.round(total * 100)].join('|'); } function facturasUnicasParaCalculo(facs) { var seen = {}; var out = []; (facs || []).forEach(function(f) { if (!facturaIncluibleEnCalculos(f)) return; var key = claveFacturaCalculo(f); if (seen[key]) return; seen[key] = true; out.push(f); }); return out; } function calcularKPIs(facs, periodo, anio) { if (!Array.isArray(facs)) return { ing: [], gas: [], totalIng: 0, totalGas: 0, neto: 0, margen: 0, ivaRep: 0, ivaSop: 0, ivaNet: 0, totalFacs: 0, facsBajaConf: 0, confMedia: 100 }; var seen = {}; var ing = [], gas = []; var totalIng = 0, totalGas = 0, ivaRep = 0, ivaSop = 0; facs.forEach(function(f) { if (!f) return; // Filtros básicos if (!facturaIncluibleEnCalculos(f)) return; var p = f.fecha.split('-'); if (p[0] !== anio) return; if (periodo !== 'anio') { var mes = parseInt(p[1]); var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; if (!trimMap[periodo] || trimMap[periodo].indexOf(mes) === -1) return; } var total = parseFloat(f.total); if (isNaN(total)) return; // rechazar total inválido var tipo = f.tipo === 'ingreso' ? 'ingreso' : (f.tipo === 'gasto' ? 'gasto' : null); if (!tipo) return; // Deduplicación por clave compuesta var key = claveFacturaCalculo(f); if (seen[key]) return; seen[key] = true; // Cálculo con base_imponible var base = parseFloat(f.base_imponible) || total; var iva = parseFloat(f.cuota_iva) || 0; if (tipo === 'ingreso') { ing.push(f); totalIng += base; ivaRep += iva; } else { gas.push(f); totalGas += base; ivaSop += iva; } }); totalIng = _kpiRound(totalIng); totalGas = _kpiRound(totalGas); var neto = _kpiRound(totalIng - totalGas); var margen = totalIng > 0 ? Math.round(neto / totalIng * 100) : 0; ivaRep = _kpiRound(ivaRep); ivaSop = _kpiRound(ivaSop); var ivaNet = _kpiRound(ivaRep - ivaSop); // KPI de calidad: facturas con confidence bajo o medio var totalFacs = ing.length + gas.length; var facsBajaConf = (ing.concat(gas)).filter(function(f) { var c = f._confidence; return c && (c.level === 'low' || c.level === 'medium'); }).length; var confMedia = totalFacs > 0 ? Math.round((ing.concat(gas)).reduce(function(s,f){ return s + ((f._confidence && f._confidence.score) ? f._confidence.score : 100); }, 0) / totalFacs) : 100; return { ing: ing, gas: gas, totalIng: totalIng, totalGas: totalGas, neto: neto, margen: margen, ivaRep: ivaRep, ivaSop: ivaSop, ivaNet: ivaNet, totalFacs: totalFacs, facsBajaConf: facsBajaConf, confMedia: confMedia, }; } function getAniosDisponibles(facs) { var anios = {}; facs.forEach(function(f){ if (f.fecha) anios[f.fecha.split('-')[0]] = true; }); return Object.keys(anios).sort().reverse(); } function periodoSelectorHTML(idPrefix, periodo, anio, anios) { var periodos = ['T1','T2','T3','T4','anio']; var labels = {T1:'T1',T2:'T2',T3:'T3',T4:'T4',anio:'Año'}; var btns = periodos.map(function(p) { var active = p === periodo; var onclickFn = (idPrefix === 'perfil') ? 'cambiarPeriodoPerfil_perfil' : 'cambiarPeriodoPerfil_cliente'; return ''; }).join(''); var anioOpts = anios.map(function(a){ return ''; }).join(''); return '
' + 'Periodo:' + btns + '' + '
'; } function cambiarPeriodoPerfil_perfil(periodo) { PERFIL_PERIODO = periodo; // Sync analytics state so navigating to Analítica respects this period currentPeriod = (periodo === 'anio') ? '1a' : periodo; renderPerfilIndividual(); } function cambiarPeriodoPerfil_cliente(periodo) { CLIENTE_PERIODO = periodo; renderClienteMain(); } // ── Gráfica mensual en perfil individual ────────────── var _chartMensualPerfil = null; function renderGraficaMensualPerfil(facs) { var el = document.getElementById('grafica-mensual-perfil'); if (!el) return; // Agrupar por mes (mismo sistema que buildPeriodData) var byMonth = {}; var meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; facs.forEach(function(f) { if (!f.fecha || f.deleted === true) return; if (f.status && f.status !== 'ok') return; var p = f.fecha.split('-'); if (p.length < 2) return; var key = p[0] + '-' + p[1]; if (!byMonth[key]) byMonth[key] = { ing: 0, gas: 0 }; var base = parseFloat(f.base_imponible) || parseFloat(f.total) || 0; if (f.tipo === 'ingreso') byMonth[key].ing += base; else byMonth[key].gas += base; }); var keys = Object.keys(byMonth).sort(); if (!keys.length) { el.innerHTML = '

Sin datos mensuales.

'; return; } var anios = {}; keys.forEach(function(k){ anios[k.split('-')[0]] = true; }); var multi = Object.keys(anios).length > 1; var labels = keys.map(function(k) { var p = k.split('-'); return multi ? (meses[parseInt(p[1])-1] + ' ' + p[0].slice(2)) : meses[parseInt(p[1])-1]; }); var ing = keys.map(function(k){ return Math.round(byMonth[k].ing * 100) / 100; }); var gas = keys.map(function(k){ return Math.round(byMonth[k].gas * 100) / 100; }); el.innerHTML = ''; var ctx = document.getElementById('canvas-mensual-perfil'); if (!ctx) return; if (_chartMensualPerfil) { _chartMensualPerfil.destroy(); _chartMensualPerfil = null; } _chartMensualPerfil = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Ingresos', data: ing, backgroundColor: 'rgba(32,73,255,.88)', borderRadius: 6 }, { label: 'Gastos', data: gas, backgroundColor: 'rgba(229,72,77,.78)', borderRadius: 6 }, ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'bottom', labels: { font: { size: 11 }, boxWidth: 12 } }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString('es-ES') + '€'; } } } }, scales: { x: { ticks: { font: { size: 10 }, color: '#6B7280' }, grid: { display: false } }, y: { ticks: { font: { size: 10 }, color: '#6B7280', callback: function(v){ return v.toLocaleString('es-ES') + '€'; } }, grid: { color: 'rgba(0,0,0,.05)' } } } } }); } // ── Comparador de periodos ────────────────────────────── var COMPARADOR_A = 'T1'; var COMPARADOR_B = 'T2'; var COMPARADOR_ANIO = String(new Date().getFullYear()); function renderComparador(facs) { var el = document.getElementById('comparador-wrap'); if (!el) return; var anios = getAniosDisponibles(facs); if (!anios.length) { el.innerHTML = '

Sin datos para comparar.

'; return; } if (anios.indexOf(COMPARADOR_ANIO) === -1) COMPARADOR_ANIO = anios[0]; var periodos = ['T1','T2','T3','T4','anio']; var labels = {T1:'T1 (Ene–Mar)',T2:'T2 (Abr–Jun)',T3:'T3 (Jul–Sep)',T4:'T4 (Oct–Dic)',anio:'Año completo'}; function selectHTML(id, val, onchg) { return ''; } function anioSelectHTML(id, val) { return ''; } var kpiA = calcularKPIs(facs, COMPARADOR_A, COMPARADOR_ANIO); var kpiB = calcularKPIs(facs, COMPARADOR_B, COMPARADOR_ANIO); function diff(a, b, pct) { var d = b - a; if (pct && a !== 0) return ' ' + (d>=0?'▲':'▼') + ' ' + Math.abs(Math.round(d/a*100)) + '%'; return ' ' + (d>=0?'+':'') + fmtEur(d,'') + ''; } function row(label, valA, valB, isEur, pct) { var fmt = isEur ? function(v){ return fmtEur(v,''); } : function(v){ return v + '%'; }; return '' + '' + label + '' + '' + fmt(valA) + '' + '' + fmt(valB) + diff(valA, valB, pct) + '' + ''; } el.innerHTML = '
' + 'Año:' + anioSelectHTML('comp-anio', COMPARADOR_ANIO) + 'Periodo A:' + selectHTML('comp-a', COMPARADOR_A, 'COMPARADOR_A=this.value;renderComparador(FACTURAS_HISTORIAL||[])') + 'vs Periodo B:' + selectHTML('comp-b', COMPARADOR_B, 'COMPARADOR_B=this.value;renderComparador(FACTURAS_HISTORIAL||[])') + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + row('Ingresos (base)', kpiA.totalIng, kpiB.totalIng, true, true) + row('Gastos (base)', kpiA.totalGas, kpiB.totalGas, true, true) + row('Beneficio neto', kpiA.neto, kpiB.neto, true, false) + row('Margen (%)', kpiA.margen, kpiB.margen, false, false) + row('IVA repercutido', kpiA.ivaRep, kpiB.ivaRep, true, false) + row('IVA soportado', kpiA.ivaSop, kpiB.ivaSop, true, false) + row('IVA resultado (M.303)', kpiA.ivaNet, kpiB.ivaNet, true, false) + row('Nº facturas', kpiA.totalFacs, kpiB.totalFacs, false, false) + '' + '
Métrica' + (labels[COMPARADOR_A]||COMPARADOR_A) + '' + (labels[COMPARADOR_B]||COMPARADOR_B) + ' vs A
' + '
'; } function cambiarPeriodoPerfil(prefix, periodo) { if (prefix === 'perfil') { PERFIL_PERIODO = periodo; renderPerfilIndividual(); } else { CLIENTE_PERIODO = periodo; renderClienteMain(); } } function cambiarAnioPerfil_perfil(anio) { PERFIL_ANIO = anio; ANA_ANIO = anio; // sync so analytics uses the same year renderPerfilIndividual(); } function cambiarAnioPerfil_cliente(anio) { CLIENTE_ANIO = anio; renderClienteMain(); } function cambiarAnioPerfil(prefix, anio) { if (prefix === 'perfil') { PERFIL_ANIO = anio; renderPerfilIndividual(); } else { CLIENTE_ANIO = anio; renderClienteMain(); } } var CLIENTE_PERIODO = 'anio'; var CLIENTE_ANIO = String(new Date().getFullYear()); function irASubir() { SS('upload'); } function acceptCookies() { var b = document.getElementById('cookie-banner'); if (b) b.style.display='none'; localStorage.setItem('cookies_accepted','1'); enableAnalyticsConsent(); } function rejectCookies() { var b = document.getElementById('cookie-banner'); if (b) b.style.display='none'; localStorage.setItem('cookies_accepted','0'); } function showCookieBanner() { var b = document.getElementById('cookie-banner'); if (b) b.style.display = 'flex'; } function initCookieBanner() { var b = document.getElementById('cookie-banner'); if (!b) return; var saved = null; try { saved = localStorage.getItem('cookies_accepted'); } catch(e) {} if (saved !== '1' && saved !== '0') b.style.display = 'flex'; } function descargarExcelTodos() { downloadExcelFromHistorial(''); } function descargarExcelIngresos() { downloadExcelFromHistorial('ingreso'); } function descargarExcelGastos() { downloadExcelFromHistorial('gasto'); } function downloadExcelFromHistorial(tipo) { var rows = tipo ? FACTURAS_HISTORIAL.filter(function(f){ return f.tipo===tipo; }) : FACTURAS_HISTORIAL; if (!rows.length) { toast('No hay facturas para descargar', 'err'); return; } var fields = ['numero_factura','fecha','tipo','emisor','cif_emisor','receptor','cif_receptor','base_imponible','iva_porcentaje','cuota_iva','irpf','total','concepto','observaciones']; fetch('/api/export-excel', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ rows: rows, fields: fields }) }) .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.blob(); }) .then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'facturas_' + (tipo||'todas') + '_' + new Date().toISOString().slice(0,10) + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Excel descargado ✓', 'ok'); }) .catch(function(e){ toast('Error al descargar Excel: ' + e.message, 'err'); }); } function irAPerfil_analytics() { if (window.__PREVIEW_MODE__) { var previewData = (FACTURAS_HISTORIAL_GLOBAL && FACTURAS_HISTORIAL_GLOBAL.length) ? FACTURAS_HISTORIAL_GLOBAL : previewFacturasDemo(); SS('analytics'); _renderAnalyticsConDatos(previewData); setTimeout(function(){ if (document.getElementById('s-analytics') && document.getElementById('s-analytics').style.display !== 'none') mostrarBannerCliente('analytics'); }, 300); return; } if (CLIENTE_ACTIVO) { // Gestoria: load client analytics abrirAnaliticaCliente(); } else { // Individual: sync period state then load analytics ANA_ANIO = PERFIL_ANIO; currentPeriod = (PERFIL_PERIODO === 'anio') ? '1a' : PERFIL_PERIODO; SS('analytics'); loadAnalytics(); setTimeout(function(){ if (document.getElementById('s-analytics') && document.getElementById('s-analytics').style.display !== 'none') mostrarBannerCliente('analytics'); }, 300); } } function renderPerfilIndividual() { if (!CURRENT_PROFILE) return; var nombre = getNombrePerfil(CURRENT_PROFILE) || 'Mi espacio'; var nif = getCifPerfil(CURRENT_PROFILE) || '—'; var initials = inicialesPerfil(nombre); // Calcular KPIs desde FACTURAS_HISTORIAL con filtro de periodo var facs = FACTURAS_HISTORIAL || []; var anios = getAniosDisponibles(facs); if (anios.length && anios.indexOf(PERFIL_ANIO) === -1) PERFIL_ANIO = anios[0]; var kpi = calcularKPIs(facs, PERFIL_PERIODO, PERFIL_ANIO); var ing = kpi.ing, gas = kpi.gas, totalIng = kpi.totalIng, totalGas = kpi.totalGas; var neto = kpi.neto, margen = kpi.margen; var ivaRep = kpi.ivaRep, ivaSop = kpi.ivaSop, ivaNet = kpi.ivaNet; var facsBajaConf = kpi.facsBajaConf, confMedia = kpi.confMedia, totalFacs = kpi.totalFacs; // Filter recent invoices by active period (same as KPIs) var facsFiltered = facs.filter(function(f) { if (!f.fecha) return false; if (f.fecha.slice(0,4) !== PERFIL_ANIO) return false; if (PERFIL_PERIODO === 'anio') return true; var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[PERFIL_PERIODO]; return ms ? ms.indexOf(parseInt(f.fecha.slice(5,7))) !== -1 : true; }); var ultimas = facsFiltered.slice().sort(function(a,b){ return (b.fecha||'').localeCompare(a.fecha||''); }).slice(0,5); var tableFacs = ultimas.length ? ultimas.map(function(f){ var esIng = f.tipo==='ingreso'; var contraparte = esIng ? (f.receptor||f.emisor||'—') : (f.emisor||'—'); var fecha = f.fecha ? f.fecha.split('-').reverse().join('/') : '—'; return '' + ''+fecha+'' + ''+contraparte+'' + ''+(esIng?'Ingreso':'Gasto')+'' + ''+( esIng?'+':'−')+fmtEur(Math.abs(parseFloat(f.total)||0),'')+'' + ''; }).join('') : 'Sin facturas aún'; var el = document.getElementById('perfil-main'); if (!el) return; // Settings vars var _plan = CURRENT_PROFILE.plan || 'free'; var _planNames = {free:'Gratuito', starter:'Starter', pro:'Pro', business:'Business', gestoria_trial:'Gestoría prueba', gestoria_starter:'Gestoría Starter', gestoria_pro:'Gestoría Pro', gestoria_business:'Gestoría Business', gestoria_scale:'Gestoría Scale', gestoria:'Gestoría', cortesia:'Cortesía', staff:'Staff'}; var planLabel = (_planNames[_plan] || _plan); var _limite = {free:10,starter:60,pro:250,business:800,gestoria_trial:20,gestoria_starter:1000, gestoria_pro:5000,gestoria_business:15000,gestoria_scale:40000,gestoria:999999, cortesia:50,staff:999999}; var usadas = CURRENT_PROFILE.facturas_mes || 0; var limiteStr = (_limite[_plan] >= 999999) ? '∞' : String(_limite[_plan]); el.innerHTML = // Header: avatar + nombre + grid 4 botones '
' + '
' + '
'+initials+'
' + '
' + '
'+nombre+'
' + '
'+nif+' · IA calibrada
' + '
' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + // Fila exportar '
' + 'Exportar:' + '' + '' + '' + '' + '
' + // Period selector + KPIs periodoSelectorHTML('perfil', PERFIL_PERIODO, PERFIL_ANIO, anios.length ? anios : [PERFIL_ANIO]) + '
' + '
' + '
Ingresos
' + '
+'+fmtEur(totalIng,'')+'
' + '
'+ing.length+' factura'+(ing.length!==1?'s':'')+'
' + '
' + '
' + '
Gastos
' + '
−'+fmtEur(totalGas,'')+'
' + '
'+gas.length+' factura'+(gas.length!==1?'s':'')+'
' + '
' + '
' + '
Neto
' + '
'+( neto>=0?'+':'−')+fmtEur(Math.abs(neto),'')+'
' + '
Margen '+margen+'%
' + '
' + '
' + '
IVA trimestre
' + '
'+(ivaNet>=0?'+':'')+ fmtEur(ivaNet,'')+'
' + '
'+(ivaNet>=0?'A pagar (M.303)':'Te devuelven (M.303)')+'
' + '
' + 'Rep: +'+fmtEur(ivaRep,'')+'' + '·' + 'Sop: −'+fmtEur(ivaSop,'')+'' + '
' + '
' + '
' + // ── KPI de calidad de datos ── (facsBajaConf > 0 ? ( '
' + '' + (confMedia < 60 ? '⚠️' : '〜') + '' + '
' + '
' + facsBajaConf + ' factura' + (facsBajaConf > 1 ? 's' : '') + ' con confianza baja
' + '
Confianza media del periodo: ' + confMedia + '%. Revisa las facturas marcadas antes de declarar.
' + '
' + '
' ) : '') + // Tabla últimas facturas // KPI de confianza gestoría (facsBajaConf > 0 ? ( '
' + '' + (confMedia < 60 ? '⚠️' : '〜') + '' + '
' + '
' + facsBajaConf + ' factura' + (facsBajaConf > 1 ? 's' : '') + ' con confianza baja
' + '
Confianza media: ' + confMedia + '%. Revisa antes de declarar.
' + '
' + '
' ) : '') + '
' + '
' + 'Últimas facturas' + ''+(facsFiltered.length !== facs.length ? facsFiltered.length+' en periodo · '+facs.length+' total' : facs.length+' en total')+'' + '
' + '
'+tableFacs+'
' + (facsFiltered.length > 5 ? '
' : '') + '
' + '
¿Tienes algún problema o consulta?
' + '' + '
' + '
' + ''; // Force re-fetch if coming back from upload to show newly processed facturas if (_lastScreen === 'upload' || _lastScreen === 'results') { FACTURAS_HISTORIAL = []; FACTURAS_HISTORIAL_GLOBAL = []; facs = []; window._perfil_loading = false; _lastScreen = 'perfil'; } // Load data if not already loaded (guard against multiple fetches) if (!facs.length && !window._perfil_loading) { window._perfil_loading = true; var _perfilAllData = []; function _fetchPerfilPage(cursor) { var url = '/api/facturas' + (cursor ? '?cursor=' + encodeURIComponent(cursor) : ''); fetch(url, { headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok && d.data) { _perfilAllData = _perfilAllData.concat(d.data); if (d.next_cursor) { _fetchPerfilPage(d.next_cursor); } else { window._perfil_loading = false; var _sanitized = _perfilAllData.filter(function(f) { if (!f) return false; if (!f.tipo || (f.tipo !== 'ingreso' && f.tipo !== 'gasto')) return false; if (!f.fecha || typeof f.fecha !== 'string' || f.fecha.length < 7) return false; if (f.deleted === true) return false; return true; }); FACTURAS_HISTORIAL = _sanitized; FACTURAS_HISTORIAL_GLOBAL = _sanitized; renderPerfilIndividual(); } } else { window._perfil_loading = false; toast('Error al cargar tus datos', 'err'); } }).catch(function(){ window._perfil_loading = false; toast('Error al cargar tus datos', 'err'); }); } _fetchPerfilPage(null); } } // ── Gráfica mensual gestoría ────────────────────────── var _chartMensualCliente = null; function renderGraficaMensualCliente(facs) { var el = document.getElementById('grafica-mensual-cliente'); if (!el) return; var byMonth = {}; var meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; facs.forEach(function(f) { if (!f.fecha || f.deleted === true) return; if (f.status && f.status !== 'ok') return; var p = f.fecha.split('-'); if (p.length < 2) return; var key = p[0] + '-' + p[1]; if (!byMonth[key]) byMonth[key] = { ing: 0, gas: 0 }; var base = parseFloat(f.base_imponible) || parseFloat(f.total) || 0; if (f.tipo === 'ingreso') byMonth[key].ing += base; else byMonth[key].gas += base; }); var keys = Object.keys(byMonth).sort(); if (!keys.length) { el.innerHTML = '

Sin datos mensuales.

'; return; } var anios = {}; keys.forEach(function(k){ anios[k.split('-')[0]] = true; }); var multi = Object.keys(anios).length > 1; var labels = keys.map(function(k) { var p = k.split('-'); return multi ? (meses[parseInt(p[1])-1] + ' ' + p[0].slice(2)) : meses[parseInt(p[1])-1]; }); var ing = keys.map(function(k){ return Math.round(byMonth[k].ing * 100) / 100; }); var gas = keys.map(function(k){ return Math.round(byMonth[k].gas * 100) / 100; }); el.innerHTML = ''; var ctx = document.getElementById('canvas-mensual-cliente'); if (!ctx) return; if (_chartMensualCliente) { _chartMensualCliente.destroy(); _chartMensualCliente = null; } _chartMensualCliente = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Ingresos', data: ing, backgroundColor: 'rgba(32,73,255,.88)', borderRadius: 6 }, { label: 'Gastos', data: gas, backgroundColor: 'rgba(255,90,122,.78)', borderRadius: 6 }, ]}, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'bottom', labels: { font: { size: 11 }, boxWidth: 12 } }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(c){ return c.dataset.label + ': ' + c.parsed.y.toLocaleString('es-ES') + '€'; } } } }, scales: { x: { ticks: { font: { size: 10 }, color: '#6B7280' }, grid: { display: false } }, y: { ticks: { font: { size: 10 }, color: '#6B7280', callback: function(v){ return v.toLocaleString('es-ES') + '€'; } }, grid: { color: 'rgba(0,0,0,.05)' } } } } }); } // ── Comparador gestoría ──────────────────────────────── var COMPARADOR_CLI_A = 'T1'; var COMPARADOR_CLI_B = 'T2'; var COMPARADOR_CLI_ANIO = String(new Date().getFullYear()); function renderComparadorCliente(facs) { var el = document.getElementById('comparador-wrap-cliente'); if (!el) return; var anios = getAniosDisponibles(facs); if (!anios.length) { el.innerHTML = '

Sin datos para comparar.

'; return; } if (anios.indexOf(COMPARADOR_CLI_ANIO) === -1) COMPARADOR_CLI_ANIO = anios[0]; var periodos = ['T1','T2','T3','T4','anio']; var labels = {T1:'T1 (Ene–Mar)',T2:'T2 (Abr–Jun)',T3:'T3 (Jul–Sep)',T4:'T4 (Oct–Dic)',anio:'Año completo'}; function selHTML(id, val, onchg) { return ''; } function anioSelHTML(id, val) { return ''; } var kpiA = calcularKPIs(facs, COMPARADOR_CLI_A, COMPARADOR_CLI_ANIO); var kpiB = calcularKPIs(facs, COMPARADOR_CLI_B, COMPARADOR_CLI_ANIO); function diff(a, b) { var d = b - a; return ' '+(d>=0?'▲':'▼')+' '+fmtEur(Math.abs(d),'')+''; } function row(label, valA, valB) { return '' + ''+label+'' + ''+fmtEur(valA,'')+'' + ''+fmtEur(valB,'')+diff(valA,valB)+'' + ''; } el.innerHTML = '
' + 'Año:' + anioSelHTML('comp-cli-anio', COMPARADOR_CLI_ANIO) + 'A:' + selHTML('comp-cli-a', COMPARADOR_CLI_A, 'COMPARADOR_CLI_A=this.value;renderComparadorCliente(FACTURAS_CLIENTE||[])') + 'vs B:' + selHTML('comp-cli-b', COMPARADOR_CLI_B, 'COMPARADOR_CLI_B=this.value;renderComparadorCliente(FACTURAS_CLIENTE||[])') + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + row('Ingresos', kpiA.totalIng, kpiB.totalIng) + row('Gastos', kpiA.totalGas, kpiB.totalGas) + row('Beneficio neto', kpiA.neto, kpiB.neto) + row('IVA repercutido', kpiA.ivaRep, kpiB.ivaRep) + row('IVA soportado', kpiA.ivaSop, kpiB.ivaSop) + row('IVA resultado (M.303)', kpiA.ivaNet, kpiB.ivaNet) + '' + '
Métrica'+(labels[COMPARADOR_CLI_A]||COMPARADOR_CLI_A)+''+(labels[COMPARADOR_CLI_B]||COMPARADOR_CLI_B)+' vs A
' + '
'; } // ── PDF gestoría ─────────────────────────────────────── function generarInformePDFCliente() { var facs = FACTURAS_CLIENTE || []; if (!facs.length) { toast('Sin facturas para generar informe', 'err'); return; } var c = CLIENTE_ACTIVO; var nombre = c ? c.nombre : 'Cliente'; var nif = c ? (c.nif || '') : ''; // Reutilizar generarInformePDF con datos del cliente activo var _bkFacs = FACTURAS_HISTORIAL; var _bkPerfil = CURRENT_PROFILE; var _bkPer = PERFIL_PERIODO; var _bkAnio = PERFIL_ANIO; // Temporalmente sobreescribir para reutilizar generarInformePDF FACTURAS_HISTORIAL = facs; CURRENT_PROFILE = { nombre: nombre, cif: nif }; PERFIL_PERIODO = CLIENTE_PERIODO; PERFIL_ANIO = CLIENTE_ANIO; generarInformePDF(); // Restaurar FACTURAS_HISTORIAL = _bkFacs; CURRENT_PROFILE = _bkPerfil; PERFIL_PERIODO = _bkPer; PERFIL_ANIO = _bkAnio; } function renderClienteMain() { var c = CLIENTE_ACTIVO; var facs = FACTURAS_CLIENTE; var nFacturas = facs.length; var anios = getAniosDisponibles(facs); if (anios.length && anios.indexOf(CLIENTE_ANIO) === -1) CLIENTE_ANIO = anios[0]; var kpi = calcularKPIs(facs, CLIENTE_PERIODO, CLIENTE_ANIO); var ing = kpi.ing, gas = kpi.gas, totalIng = kpi.totalIng, totalGas = kpi.totalGas; var neto = kpi.neto, margen = kpi.margen; var ivaRep = kpi.ivaRep, ivaSop = kpi.ivaSop, ivaNet = kpi.ivaNet; var facsBajaConf = kpi.facsBajaConf, confMedia = kpi.confMedia, totalFacs = kpi.totalFacs; var nFacturasTotal = facs.length; var initials = c.nombre.split(' ').slice(0,2).map(function(w){return w[0];}).join('').toUpperCase(); var facsFiltered = facs.filter(function(f) { if (!f.fecha) return false; if (f.fecha.slice(0,4) !== CLIENTE_ANIO) return false; if (CLIENTE_PERIODO === 'anio') return true; var trimMap = {T1:[1,2,3], T2:[4,5,6], T3:[7,8,9], T4:[10,11,12]}; var ms = trimMap[CLIENTE_PERIODO]; return ms ? ms.indexOf(parseInt(f.fecha.slice(5,7))) !== -1 : true; }); var ultimas = facsFiltered.slice().sort(function(a,b){ return (b.fecha||'').localeCompare(a.fecha||''); }).slice(0,5); var tableFacs = ultimas.length ? ultimas.map(function(f) { var esIng = f.tipo === 'ingreso'; var contraparte = esIng ? (f.receptor||f.emisor||'—') : (f.emisor||'—'); var fecha = f.fecha ? f.fecha.split('-').reverse().join('/') : '—'; // Usar base_imponible como fuente primaria, igual que en analítica individual var importe = parseFloat(f.base_imponible) || parseFloat(f.total) || 0; return '' + '' + fecha + '' + '' + contraparte + '' + '' + (esIng?'Ingreso':'Gasto') + '' + '' + (esIng?'+':'−') + fmtEur(Math.abs(importe),'') + '' + ''; }).join('') : 'Sin facturas aún'; var nFacturas = nFacturasTotal; c._nFacturas = nFacturas; document.getElementById('gestoria-main').innerHTML = // Mobile-only client switcher bar '' + '
' + '
' + '
' + initials + '
' + '
' + '
' + c.nombre + '
' + '
' + (c.nif||'—') + ' · IA calibrada
' + '
' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + 'Exportar:' + '' + '' + '
' + '
' + '' + 'Zona de peligro' + '' + '
' + '
' + periodoSelectorHTML('cliente', CLIENTE_PERIODO, CLIENTE_ANIO, anios.length ? anios : [CLIENTE_ANIO]) + '
' + '
' + '
Ingresos
' + '
+' + fmtEur(totalIng,'') + '
' + '
' + ing.length + ' factura' + (ing.length!==1?'s':'') + '
' + '
' + '
' + '
Gastos
' + '
−' + fmtEur(totalGas,'') + '
' + '
' + gas.length + ' factura' + (gas.length!==1?'s':'') + '
' + '
' + '
' + '
Neto
' + '
' + (neto>=0?'+':'−') + fmtEur(Math.abs(neto),'') + '
' + '
Margen ' + margen + '%
' + '
' + '
' + '
IVA trimestre
' + '
' + (ivaNet>=0?'+':'') + fmtEur(ivaNet,'') + '
' + '
' + (ivaNet>=0?'A pagar (M.303)':'A devolver (M.303)') + '
' + '
' + 'Rep: +' + fmtEur(ivaRep,'') + '' + '·' + 'Sop: −' + fmtEur(ivaSop,'') + '' + '
' + '
' + '
' + // KPI de confianza gestoría (facsBajaConf > 0 ? ( '
' + '' + (confMedia < 60 ? '⚠️' : '〜') + '' + '
' + '
' + facsBajaConf + ' factura' + (facsBajaConf > 1 ? 's' : '') + ' con confianza baja
' + '
Confianza media: ' + confMedia + '%. Revisa antes de declarar.
' + '
' + '
' ) : '') + '
' + '
' + 'Últimas facturas' + '' + (facsFiltered.length !== facs.length ? facsFiltered.length + ' en periodo · ' + facs.length + ' total' : facs.length + ' en total') + '' + '
' + '
' + tableFacs + '
' + (facsFiltered.length > 5 ? '
' : '') + '
'; renderClienteAjustes(); } function abrirAnaliticaCliente() { if (!CLIENTE_ACTIVO) { irAPerfil_analytics(); return; } if (_clienteLoading) { var waitId = setInterval(function() { if (!_clienteLoading) { clearInterval(waitId); abrirAnaliticaCliente(); } }, 100); return; } SS('analytics'); var _snapA = CLIENTE_ACTIVO; var _snapId = CLIENTE_ACTIVO.id; // Siempre hacer fetch fresco para garantizar datos completos // (evita el problema de primera carga con estado inconsistente) _clienteLoading = true; fetch('/api/clientes/' + _snapId + '/facturas', { headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ _clienteLoading = false; if (!CLIENTE_ACTIVO || CLIENTE_ACTIVO.id !== _snapId) return; var datos = (d.data || []).filter(function(f) { if (!f) return false; if (!f.tipo || (f.tipo !== 'ingreso' && f.tipo !== 'gasto')) return false; if (!f.fecha || typeof f.fecha !== 'string' || f.fecha.length < 7) return false; if (f.deleted === true) return false; return true; }); // Actualizar caché y FACTURAS_CLIENTE datos = mezclarFacturasCliente(FACTURAS_CLIENTE, datos); if (FACTURAS_PROCESADAS && FACTURAS_PROCESADAS.length) { datos = mezclarFacturasCliente(datos, FACTURAS_PROCESADAS); } FACTURAS_CLIENTE = datos; _FACTURAS_CACHE[_snapId] = { data: datos, ts: Date.now(), anio: CLIENTE_ANIO }; // Calcular año más reciente var aniosCliente = []; datos.forEach(function(f){ if(f.fecha){ var y=f.fecha.slice(0,4); if(aniosCliente.indexOf(y)===-1) aniosCliente.push(y); }}); aniosCliente.sort().reverse(); var anioCliente = aniosCliente[0] || String(new Date().getFullYear()); ANA_ANIO = anioCliente; currentPeriod = '1a'; FACTURAS_HISTORIAL = datos; FACTURAS_HISTORIAL_GLOBAL = datos; populateMonthSelect(); var msEl = document.getElementById('month-select'); if (msEl) msEl.value = anioCliente; var tbs = document.querySelectorAll('.tf-btn'); for (var i = 0; i < tbs.length; i++) tbs[i].classList.remove('active'); var btnAnioEl = document.getElementById('tf-1a'); if (btnAnioEl) btnAnioEl.classList.add('active'); _renderAnalyticsConDatos(datos); mostrarBannerCliente('analytics', _snapA); marcarNavActivo('analytics'); toast('Analítica de ' + _snapA.nombre, 'info'); }) .catch(function() { _clienteLoading = false; toast('Error al cargar datos de analítica', 'err'); }); } function salirAnaliticaCliente() { volverACliente(); } function volverAClientes() { CLIENTE_ACTIVO = null; FACTURAS_CLIENTE = []; // _clienteActivoId se limpia via CLIENTE_ACTIVO = null var banner = document.getElementById('gestoria-upload-banner'); if (banner) banner.style.display = 'none'; var btnVolver = document.getElementById('btn-volver-cliente'); if (btnVolver) btnVolver.remove(); document.querySelectorAll('.gestoria-client-item').forEach(function(i){i.classList.remove('active')}); document.getElementById('gestoria-main').innerHTML = '
' + '
👥
' + '
Selecciona un cliente
' + '
Elige un cliente de la lista o crea uno nuevo.
'; } function abrirSubidaCliente() { if (!CLIENTE_ACTIVO) return; // Limpiar archivos de sesión anterior ARCHIVOS_PENDIENTES = []; FACTURAS_PROCESADAS = []; filesAdded = false; running = false; var fileList = document.getElementById('file-list'); if (fileList) fileList.innerHTML = ''; var fieldsCard = document.getElementById('fields-card'); if (fieldsCard) fieldsCard.style.display = 'none'; var progCard = document.getElementById('progress-card'); if (progCard) progCard.style.display = 'none'; var btnCta = document.getElementById('btn-cta'); if (btnCta) { btnCta.disabled = true; } var ctaTxt = document.getElementById('cta-txt'); if (ctaTxt) ctaTxt.textContent = 'Procesar facturas'; // Limpiar lista si había otro cliente activo o hay facturas pendientes if (ARCHIVOS_PENDIENTES.filter(function(f){return f!==null;}).length > 0) { ARCHIVOS_PENDIENTES = []; FACTURAS_PROCESADAS = []; var fl = document.getElementById('file-list'); if (fl) fl.innerHTML = ''; var pc = document.getElementById('progress-card'); if (pc) pc.style.display = 'none'; var btnLimpiar = document.getElementById('btn-limpiar'); if (btnLimpiar) btnLimpiar.style.display = 'none'; } SS('upload'); // _clienteActivoId se sincroniza automáticamente via defineProperty de CLIENTE_ACTIVO var banner = document.getElementById('gestoria-upload-banner'); var label = document.getElementById('gestoria-upload-label'); var avisoSinCliente = document.getElementById('gestoria-sin-cliente-aviso'); if (banner) { banner.style.display = 'flex'; } if (avisoSinCliente) { avisoSinCliente.style.display = 'none'; } if (label) label.textContent = 'Subiendo facturas para ' + CLIENTE_ACTIVO.nombre + ' · NIF: ' + (CLIENTE_ACTIVO.nif || '—'); toast('Subiendo facturas para ' + CLIENTE_ACTIVO.nombre, 'info'); } var _modalNifConfirm = null; var _modalNifCancel = null; function mostrarModalNifUsuario(filename, onClose) { document.getElementById('modal-nif-u-filename').textContent = filename || 'desconocida'; window._modalNifUsuarioClose = onClose || null; document.getElementById('modal-nif-usuario').style.display = 'flex'; } var _modalDupConfirm = null, _modalDupCancel = null; function mostrarModalDuplicado(filename, numero, onSi, onNo, detalle) { document.getElementById('modal-dup-filename').textContent = filename || 'desconocida'; document.getElementById('modal-dup-numero').textContent = numero || '—'; var det = document.getElementById('modal-dup-detail'); if (det) det.textContent = detalle || ''; _modalDupConfirm = onSi || null; _modalDupCancel = onNo || null; document.getElementById('modal-duplicado').style.display = 'flex'; } function cerrarModalDuplicado(confirmar) { document.getElementById('modal-duplicado').style.display = 'none'; if (confirmar && _modalDupConfirm) _modalDupConfirm(); else if (!confirmar && _modalDupCancel) _modalDupCancel(); _modalDupConfirm = null; _modalDupCancel = null; } function cerrarModalNifUsuario(procesar) { document.getElementById('modal-nif-usuario').style.display = 'none'; if (window._modalNifUsuarioClose) window._modalNifUsuarioClose(procesar !== false); window._modalNifUsuarioClose = null; } function mostrarModalNifError(filename, clienteNombre, nifDetectado, nifEsperado, onConfirm, onCancel) { document.getElementById('modal-nif-filename').textContent = filename || 'desconocida'; document.getElementById('modal-nif-cliente').textContent = clienteNombre; document.getElementById('modal-nif-detectado').textContent = nifDetectado || 'no detectado'; document.getElementById('modal-nif-esperado').textContent = nifEsperado; _modalNifConfirm = onConfirm || null; _modalNifCancel = onCancel || null; document.getElementById('modal-nif-error').style.display = 'flex'; } function confirmarModalNif() { document.getElementById('modal-nif-error').style.display = 'none'; if (_modalNifConfirm) _modalNifConfirm(); _modalNifConfirm = null; _modalNifCancel = null; } function cerrarModalNifError() { document.getElementById('modal-nif-error').style.display = 'none'; if (_modalNifCancel) _modalNifCancel(); _modalNifConfirm = null; _modalNifCancel = null; } function cancelarSubidaCliente() { var banner = document.getElementById('gestoria-upload-banner'); if (banner) banner.style.display = 'none'; // _clienteActivoId se limpia via CLIENTE_ACTIVO = null SS('gestoria'); if (CLIENTE_ACTIVO) selectCliente(CLIENTE_ACTIVO); } function descargarExcelCliente(filas) { var rows = filas || FACTURAS_CLIENTE; if (!rows.length) { toast('No hay facturas para exportar', 'err'); return; } trackEvent('export_excel', { source: 'gestoria_cliente', export_type: 'cliente', row_count: rows.length }); var fields = Object.keys({numero_factura:1,fecha:1,emisor:1,cif_emisor:1,receptor:1,cif_receptor:1,base_imponible:1,iva_porcentaje:1,cuota_iva:1,irpf:1,total:1,concepto:1}); fetch('/api/export-excel', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ rows: rows, fields: fields }) }) .then(function(r){ return r.blob(); }) .then(function(blob){ var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = CLIENTE_ACTIVO.nombre.replace(/\s+/g,'_') + '_facturas.xlsx'; a.click(); URL.revokeObjectURL(url); toast('Excel descargado ✓', 'ok'); }).catch(function(){ toast('Error al descargar Excel', 'err'); }); } var NOMBRES_MESES = { '01':'Enero','02':'Febrero','03':'Marzo','04':'Abril', '05':'Mayo','06':'Junio','07':'Julio','08':'Agosto', '09':'Septiembre','10':'Octubre','11':'Noviembre','12':'Diciembre' }; function partesFechaFactura(fecha) { if (!fecha) return null; var s = String(fecha).trim(); var mIso = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); if (mIso) return { anio: mIso[1], mes: mIso[2].padStart(2, '0') }; var mEs = s.match(/^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})/); if (mEs) return { anio: mEs[3], mes: mEs[2].padStart(2, '0') }; return null; } function facturaPasaFiltroExcel(f, anio, mes, tipo) { if (!f || !f.fecha) return false; var p = partesFechaFactura(f.fecha); if (!p) return false; var anioFiltro = (anio && anio !== 'todos') ? anio : ''; var mesFiltro = (mes && mes !== 'todos') ? mes : ''; if (tipo && tipo !== 'todos' && tipo !== 'all' && f.tipo !== tipo) return false; if (anioFiltro && p.anio !== anioFiltro) return false; if (mesFiltro && p.mes !== mesFiltro) return false; return true; } function mostrarFiltroExcelCliente() { var modal = document.getElementById('modal-filtro-excel'); if (!modal) return; var years = {}; FACTURAS_CLIENTE.forEach(function(f) { var p = partesFechaFactura(f.fecha); if (!p) return; if (!years[p.anio]) years[p.anio] = {}; years[p.anio][p.mes] = true; }); var yearKeys = Object.keys(years).sort().reverse(); var selAnio = document.getElementById('filtro-excel-anio'); selAnio.innerHTML = ''; yearKeys.forEach(function(y) { selAnio.innerHTML += ''; }); // Resetear selector de mes var selMes = document.getElementById('filtro-excel-mes-sel'); if (selMes) selMes.value = 'todos'; var selTipo = document.getElementById('filtro-excel-tipo'); if (selTipo) selTipo.value = 'todos'; modal.style.display = 'flex'; } function actualizarMesesFiltro() { var anio = (document.getElementById('filtro-excel-anio') || {}).value || 'todos'; var grid = document.getElementById('filtro-meses-grid'); if (!grid) return; var mesesDisp = {}; FACTURAS_CLIENTE.forEach(function(f) { var p = partesFechaFactura(f.fecha); if (!p) return; if (anio !== 'todos' && p.anio !== anio) return; mesesDisp[p.mes] = true; }); var allMeses = ['01','02','03','04','05','06','07','08','09','10','11','12']; grid.innerHTML = allMeses.map(function(m) { var disabled = !mesesDisp[m]; var borderC = disabled ? 'var(--line)' : 'var(--brand)'; var bgC = disabled ? 'var(--bg2)' : 'var(--brand-bg)'; return ''; }).join(''); actualizarContadorFiltro(); } function actualizarContadorFiltro() { var anio = (document.getElementById('filtro-excel-anio') || {}).value || 'todos'; var tipo = (document.getElementById('filtro-excel-tipo') || {}).value || 'todos'; var mesesSel = []; var cbs = document.querySelectorAll('#filtro-meses-grid input[type=checkbox]:checked'); cbs.forEach(function(cb){ mesesSel.push(cb.value); }); var count = FACTURAS_CLIENTE.filter(function(f) { if (!facturaPasaFiltroExcel(f, anio, 'todos', tipo)) return false; if (mesesSel.length > 0) { var p = partesFechaFactura(f.fecha); return p && mesesSel.indexOf(p.mes) !== -1; } return true; }).length; var el = document.getElementById('filtro-excel-count'); if (el) el.textContent = count + ' factura' + (count !== 1 ? 's' : ''); // Update toggle button var allChecked = document.querySelectorAll('#filtro-meses-grid input[type=checkbox]:not(:disabled)').length; var checkedCount = document.querySelectorAll('#filtro-meses-grid input[type=checkbox]:checked').length; var btn = document.getElementById('btn-todos-meses'); if (btn) btn.textContent = checkedCount === allChecked ? 'Deseleccionar todos' : 'Seleccionar todos'; } function toggleTodosMeses() { var cbs = document.querySelectorAll('#filtro-meses-grid input[type=checkbox]:not(:disabled)'); var allChecked = Array.prototype.every.call(cbs, function(cb){ return cb.checked; }); cbs.forEach(function(cb){ cb.checked = !allChecked; }); actualizarContadorFiltro(); } function cerrarFiltroExcelCliente() { var modal = document.getElementById('modal-filtro-excel'); if (modal) modal.style.display = 'none'; } function descargarExcelFiltrado() { var anio = (document.getElementById('filtro-excel-anio') || {}).value || 'todos'; var tipo = (document.getElementById('filtro-excel-tipo') || {}).value || 'todos'; var mesSelect = (document.getElementById('filtro-excel-mes-sel') || {}).value || 'todos'; var mesesSel = []; document.querySelectorAll('#filtro-meses-grid input[type=checkbox]:checked').forEach(function(cb){ mesesSel.push(cb.value); }); var rows = FACTURAS_CLIENTE.filter(function(f) { if (!facturaPasaFiltroExcel(f, anio, 'todos', tipo)) return false; if (mesesSel.length > 0) { var p = partesFechaFactura(f.fecha); return p && mesesSel.indexOf(p.mes) !== -1; } return facturaPasaFiltroExcel(f, anio, mesSelect, tipo); }); if (!rows.length) { toast('No hay facturas con ese filtro', 'err'); return; } cerrarFiltroExcelCliente(); descargarExcelCliente(rows); } function confirmarEliminarClienteActivo() { if (!CLIENTE_ACTIVO) return; confirmarEliminarCliente(CLIENTE_ACTIVO.id); } function confirmarEliminarCliente(id) { var nombre = CLIENTE_ACTIVO ? CLIENTE_ACTIVO.nombre : 'este cliente'; var modal = document.getElementById('modal-eliminar-cliente'); if (!modal) return; document.getElementById('modal-eliminar-nombre').textContent = nombre; document.getElementById('modal-eliminar-id').value = id; modal.style.display = 'flex'; } function cerrarModalEliminarCliente() { var modal = document.getElementById('modal-eliminar-cliente'); if (modal) modal.style.display = 'none'; } function ejecutarEliminarCliente() { var id = document.getElementById('modal-eliminar-id').value; cerrarModalEliminarCliente(); toast('Eliminando cliente...', 'info'); fetch('/api/clientes/' + id, { method: 'DELETE', headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { CLIENTES_LIST = CLIENTES_LIST.filter(function(c){ return c.id !== id; }); CLIENTE_ACTIVO = null; FACTURAS_CLIENTE = []; renderClientesList(); document.getElementById('gestoria-main').innerHTML = '
' + '
' + '
Cliente eliminado
' + '
Selecciona otro cliente o crea uno nuevo.
'; toast('Cliente eliminado correctamente', 'ok'); } else { toast('Error al eliminar el cliente. Inténtalo de nuevo.', 'err'); } }).catch(function(){ toast('Error de conexión al eliminar el cliente.', 'err'); }); } function toggleNuevoCliente() { var modal = document.getElementById('modal-nuevo-cliente'); if (!modal) return; var isOpen = modal.style.display === 'flex'; modal.style.display = isOpen ? 'none' : 'flex'; if (!isOpen) { // Limpiar campos al abrir var nc = document.getElementById('nc-nombre'); var nf = document.getElementById('nc-nif'); if (nc) { nc.value = ''; nc.focus(); } if (nf) nf.value = ''; } } function guardarNuevoCliente() { var nombre = document.getElementById('nc-nombre').value.trim(); var nif = document.getElementById('nc-nif').value.trim().toUpperCase(); if (!nombre || !nif) { toast('Nombre y DNI/CIF son obligatorios', 'err'); return; } // Límite de clientes por plan gestoría var _cliLimits = {gestoria_trial:2, gestoria_starter:10, gestoria_pro:50, gestoria_business:150, gestoria_scale:400, gestoria:999999, staff:999999}; var _planCli = CURRENT_PROFILE ? (CURRENT_PROFILE.plan || 'gestoria_starter') : 'gestoria_starter'; var _maxCli = _cliLimits[_planCli] || 10; if (CURRENT_PROFILE && CLIENTES_LIST.length >= _maxCli) { var _cliLabels = {gestoria_trial:'Gestoría prueba (máx. 2 clientes)', gestoria_starter:'Gestoría Starter (máx. 10 clientes)', gestoria_pro:'Gestoría Pro (máx. 50 clientes)', gestoria_business:'Gestoría Business (máx. 150 clientes)', gestoria_scale:'Gestoría Scale (máx. 400 clientes)'}; toast('Has alcanzado el límite de tu plan ' + (_cliLabels[_planCli] || _planCli) + '. Actualiza para añadir más.', 'err'); return; } fetch('/api/clientes', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ nombre: nombre, nif: nif }) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { document.getElementById('nc-nombre').value = ''; document.getElementById('nc-nif').value = ''; document.getElementById('modal-nuevo-cliente').style.display = 'none'; CLIENTES_LIST.push(d.data); renderClientesList(); selectCliente(d.data); trackEvent('create_client', { plan: CURRENT_PROFILE ? (CURRENT_PROFILE.plan || 'unknown') : 'unknown', client_count: CLIENTES_LIST.length }); toast('Cliente añadido ✓', 'ok'); } else { toast(d.error || 'Error al crear cliente', 'err'); } }).catch(function(){ toast('Error de conexión', 'err'); }); } function toast(msg, type) { var el = document.getElementById('toast-el'); if (!el) { console.warn('toast-el missing', msg); return; } el.textContent = msg; el.className = 'toast show ' + (type || ''); var textLen = String(msg || '').length; var timeout = Math.min(Math.max(3500, textLen * 45), 9000); clearTimeout(window.__toastTimer); window.__toastTimer = setTimeout(function() { el.className = 'toast'; }, timeout); } var chatOpen = false; var chatWaiting = false; var chatHistory = []; function toggleChat() { if (!loggedIn || !CURRENT_PROFILE) { SS('login'); toast('Inicia sesión para usar el asistente', 'info'); return; } var plan = CURRENT_PROFILE.plan || 'free'; if (plan === 'free') { toast('El asistente IA no está disponible en el plan Gratuito. Actualiza tu plan para usarlo.', 'info'); return; } chatOpen = !chatOpen; var cw = document.getElementById('chat-window'); if (cw) cw.style.display = chatOpen ? 'flex' : 'none'; if (chatOpen) setTimeout(function(){ var i=document.getElementById('chat-input'); if(i) i.focus(); }, 250); } function chatKeydown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMsg(); } } function sendSug(btn) { document.getElementById('chat-sugs').style.display='none'; sendChatText(btn.textContent); } function sendChatMsg() { var inp = document.getElementById('chat-input'); var text = inp.value.trim(); if (!text || chatWaiting) return; inp.value = ''; inp.style.height = 'auto'; document.getElementById('chat-sugs').style.display = 'none'; sendChatText(text); } function sendChatText(text) { if (!text || chatWaiting) return; chatWaiting = true; document.getElementById('chat-send').disabled = true; appendMsg('user', text); chatHistory.push({ role: 'user', content: text }); var tid = 'typing-' + Date.now(); var msgs = document.getElementById('chat-msgs'); var d = document.createElement('div'); d.className = 'msg bot'; d.id = tid; d.innerHTML = '
'; msgs.appendChild(d); msgs.scrollTop = msgs.scrollHeight; fetch('/api/chat', { method: 'POST', headers: {'Content-Type':'application/json', 'Authorization': 'Bearer ' + TOKEN}, body: JSON.stringify({ messages: chatHistory }) }) .then(function(r){ if (!r.ok) { return r.json().then(function(e) { throw new Error(e.error || 'HTTP ' + r.status); }); } return r.json(); }) .then(function(d2){ var el = document.getElementById(tid); if(el) el.remove(); if (d2.reply) { appendMsg('bot', d2.reply); chatHistory.push({ role: 'assistant', content: d2.reply }); } else if (d2.error) { appendMsg('bot', d2.error); } else { appendMsg('bot', 'Lo siento, ha habido un problema. Inténtalo de nuevo.'); } chatWaiting = false; document.getElementById('chat-send').disabled = false; }) .catch(function(err){ var el = document.getElementById(tid); if(el) el.remove(); appendMsg('bot', 'Error: ' + (err.message || 'desconocido')); chatWaiting = false; document.getElementById('chat-send').disabled = false; }); } function appendMsg(role, text) { var msgs = document.getElementById('chat-msgs'); var div = document.createElement('div'); div.className = 'msg ' + role; div.textContent = text; msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; } function toggleAjustesMenu(e) { if (e) e.stopPropagation(); var menu = document.getElementById('ajustes-menu'); if (!menu) return; var isOpen = menu.style.display === 'block'; menu.style.display = isOpen ? 'none' : 'block'; if (!isOpen && CURRENT_PROFILE && CURRENT_USER) { var nombre = document.getElementById('ajustes-menu-nombre'); var email = document.getElementById('ajustes-menu-email'); var plan = document.getElementById('ajustes-menu-plan'); var planNames = {free:'Plan Gratuito', starter:'Plan Starter', pro:'Plan Pro', business:'Plan Business', gestoria_trial:'Plan Gestoría prueba', gestoria_starter:'Gestoría Starter', gestoria_pro:'Gestoría Pro', gestoria_business:'Gestoría Business', gestoria_scale:'Gestoría Scale', gestoria:'Plan Gestoría', cortesia:'Plan Cortesía', staff:'Staff'}; if (nombre) nombre.textContent = CURRENT_PROFILE.nombre || ''; if (email) email.textContent = CURRENT_USER.email || ''; if (plan) plan.textContent = planNames[CURRENT_PROFILE.plan] || CURRENT_PROFILE.plan || ''; // Adapt "Mi espacio" label for gestoria var isGestor = ['gestoria','gestoria_trial','gestoria_starter','gestoria_pro','gestoria_business','gestoria_scale','staff'] .indexOf(CURRENT_PROFILE.plan) !== -1; var espacioLabel = document.getElementById('ajustes-menu-espacio-label'); if (espacioLabel) espacioLabel.textContent = isGestor ? 'Mis clientes' : 'Mi espacio'; } } // Close menu when clicking outside document.addEventListener('click', function(e) { var menu = document.getElementById('ajustes-menu'); var btn = document.getElementById('avatar-btn'); if (menu && menu.style.display === 'block' && !menu.contains(e.target) && e.target !== btn) { menu.style.display = 'none'; } }); function guardarNombrePerfil() { var input = document.getElementById('ajustes-nombre-input'); if (!input) return; var nombre = input.value.trim(); if (!nombre) { toast('El nombre no puede estar vacío', 'err'); return; } var btn = input.nextElementSibling; if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } fetch('/api/profile', { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ nombre: nombre }) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { if (CURRENT_PROFILE) CURRENT_PROFILE.nombre = nombre; updateNavForUser(); toast('Nombre actualizado ✓', 'ok'); if (btn) { btn.textContent = '✓ Guardado'; btn.style.background = 'var(--green)'; } setTimeout(function(){ if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; btn.style.background = ''; } }, 2500); } else { toast(d.error || 'Error al guardar', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } } }) .catch(function(){ toast('Error de conexión', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } }); } // ── FUNCIONES FALTANTES ───────────────────────────────────────────── // enviarMensaje: alias de sendChatMsg (llamado desde chat widget) function enviarMensaje() { sendChatMsg(); } // enviarResetPassword: envía enlace de reset al email del usuario function enviarResetPassword() { if (!CURRENT_USER || !CURRENT_USER.email) { toast('No se encontró tu email. Inicia sesión de nuevo.', 'err'); return; } var btn = document.querySelector('[onclick="enviarResetPassword()"]'); if (btn) { btn.disabled = true; btn.textContent = 'Enviando...'; } fetch('/api/auth/forgot', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email: CURRENT_USER.email }) }) .then(function() { toast('Enlace de cambio de contraseña enviado a ' + CURRENT_USER.email, 'ok'); if (btn) { btn.disabled = false; btn.textContent = 'Enviar enlace'; } }) .catch(function() { toast('Error al enviar el enlace. Inténtalo de nuevo.', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Enviar enlace'; } }); } // exportarLibroExcelHistorial: exporta historial con filtros del modal function exportarLibroExcelHistorial() { var mes = (document.getElementById('filtro-excel-his-mes') || {}).value || ''; var tipo = (document.getElementById('filtro-excel-his-tipo') || {}).value || 'all'; var data = HISTORIAL_DATA || FACTURAS_HISTORIAL || []; var rows = data.filter(function(f) { if (tipo !== 'all' && f.tipo !== tipo) return false; if (mes && f.fecha && f.fecha.slice(0,7) !== mes) return false; return true; }); if (!rows.length) { toast('No hay facturas con esos filtros', 'err'); return; } trackEvent('export_excel', { source: 'historial', export_type: tipo, row_count: rows.length }); document.getElementById('modal-filtro-excel-historial').style.display = 'none'; fetch('/api/export-excel', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ rows: rows, fields: ['numero_factura','fecha','tipo','emisor','cif_emisor','receptor','base_imponible','iva_porcentaje','cuota_iva','irpf','total','concepto','observaciones'] }) }) .then(function(r) { return r.blob(); }) .then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'historial_' + new Date().toISOString().slice(0,10) + '.xlsx'; a.click(); URL.revokeObjectURL(url); toast('Excel descargado ✓', 'ok'); }) .catch(function() { toast('Error al descargar Excel', 'err'); }); } // abrirSoporte: abre pantalla de soporte prefilling nombre/email si logado function abrirSoporte() { if (loggedIn && CURRENT_PROFILE) { var n = document.getElementById('sop-nombre'); var e = document.getElementById('sop-email'); if (n && CURRENT_PROFILE.nombre && !n.value) n.value = CURRENT_PROFILE.nombre; if (e && window.__PREVIEW_MODE__) e.value = ''; if (e && CURRENT_USER && CURRENT_USER.email && !e.value && !window.__PREVIEW_MODE__) e.value = CURRENT_USER.email; } var fields = document.getElementById('soporte-form-fields'); var success = document.getElementById('soporte-success'); var errEl = document.getElementById('soporte-error'); var btn = document.getElementById('sop-btn'); if (fields) fields.style.display = 'block'; if (success) success.style.display = 'none'; if (errEl) errEl.style.display = 'none'; if (btn) { btn.disabled = false; btn.textContent = 'Enviar mensaje'; } SS('soporte'); } // enviarSoporte: envía el formulario de soporte via /api/soporte function enviarSoporte() { var nombre = (document.getElementById('sop-nombre') || {}).value || ''; var email = (document.getElementById('sop-email') || {}).value || ''; var asunto = (document.getElementById('sop-asunto') || {}).value || ''; var mensaje = (document.getElementById('sop-mensaje') || {}).value || ''; var errEl = document.getElementById('soporte-error'); var btn = document.getElementById('sop-btn'); function showErr(msg) { if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; } else toast(msg, 'err'); } if (!nombre.trim()) { showErr('Por favor, introduce tu nombre.'); return; } if (!email.trim() || email.indexOf('@') < 0) { showErr('Introduce un email válido.'); return; } if (!asunto) { showErr('Selecciona un asunto.'); return; } if (mensaje.trim().length < 20) { showErr('El mensaje debe tener al menos 20 caracteres.'); return; } if (errEl) errEl.style.display = 'none'; if (btn) { btn.disabled = true; btn.textContent = 'Enviando...'; } fetch('/api/soporte', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ nombre: nombre.trim(), email: email.trim(), asunto: asunto, mensaje: mensaje.trim() }) }) .then(function(r) { return r.json(); }) .then(function(d) { if (d.ok) { var fields = document.getElementById('soporte-form-fields'); var success = document.getElementById('soporte-success'); if (fields) fields.style.display = 'none'; if (success) success.style.display = 'block'; } else { showErr(d.error || 'No se pudo enviar. Inténtalo de nuevo.'); if (btn) { btn.disabled = false; btn.textContent = 'Enviar mensaje'; } } }) .catch(function() { showErr('Error de conexión. Escríbenos a soporte@contabilify.com.'); if (btn) { btn.disabled = false; btn.textContent = 'Enviar mensaje'; } }); } // showPlanModal: muestra modal de elección de tipo de plan (empezar gratis) function showPlanModal() { trackEvent('click_start_free', { source: 'plan_type_modal' }); var m = document.getElementById('modal-plan-type'); if (m) m.style.display = 'flex'; } // closePlanModal: cierra modal de tipo de plan function closePlanModal() { var m = document.getElementById('modal-plan-type'); if (m) m.style.display = 'none'; } // doLoginWithPlan: guarda preferencia de plan y va al login function doLoginWithPlan(planType) { try { localStorage.setItem('signup_plan_type', planType || 'individual'); } catch(e) {} trackEvent('start_trial', { plan_type: planType || 'individual' }); trackEvent('start_signup', { plan_type: planType || 'individual' }); closePlanModal(); SS('login'); } function previewFacturasDemo() { return [ {id:'pv-1', tipo:'ingreso', numero_factura:'FAC-2026-042', fecha:'2026-05-12', emisor:'Contabilify SL', cif_emisor:'B88725494', receptor:'Cliente Norte SL', cif_receptor:'B12345678', base_imponible:1200, iva:21, iva_porcentaje:21, cuota_iva:252, irpf:0, total:1452, concepto:'Servicio mensual', estado:'ok', status:'ok', confidence:96}, {id:'pv-2', tipo:'gasto', numero_factura:'A-991', fecha:'2026-05-08', emisor:'Proveedor Cloud SL', cif_emisor:'B11111111', receptor:'Contabilify SL', cif_receptor:'B88725494', base_imponible:180, iva:21, iva_porcentaje:21, cuota_iva:37.8, irpf:0, total:217.8, concepto:'Software de facturacion', estado:'ok', status:'ok', confidence:93}, {id:'pv-3', tipo:'ingreso', numero_factura:'FAC-2026-038', fecha:'2026-04-19', emisor:'Contabilify SL', cif_emisor:'B88725494', receptor:'Estudio Azul', cif_receptor:'B22222222', base_imponible:860, iva:21, iva_porcentaje:21, cuota_iva:180.6, irpf:0, total:1040.6, concepto:'Automatizacion contable', estado:'ok', status:'ok', confidence:91}, {id:'pv-4', tipo:'gasto', numero_factura:'G-1204', fecha:'2026-04-02', emisor:'Gestoria Tecnica', cif_emisor:'B33333333', receptor:'Contabilify SL', cif_receptor:'B88725494', base_imponible:240, iva:21, iva_porcentaje:21, cuota_iva:50.4, irpf:0, total:290.4, concepto:'Asesoria trimestral', estado:'ok', status:'ok', confidence:89}, {id:'pv-5', tipo:'ingreso', numero_factura:'FAC-2026-031', fecha:'2026-03-14', emisor:'Contabilify SL', cif_emisor:'B88725494', receptor:'Mercado Sur SL', cif_receptor:'B44444444', base_imponible:640, iva:21, iva_porcentaje:21, cuota_iva:134.4, irpf:0, total:774.4, concepto:'Licencia anual', estado:'ok', status:'ok', confidence:95}, {id:'pv-6', tipo:'gasto', numero_factura:'INV-77', fecha:'2026-03-04', emisor:'Diseno Web Pro', cif_emisor:'B55555555', receptor:'Contabilify SL', cif_receptor:'B88725494', base_imponible:320, iva:21, iva_porcentaje:21, cuota_iva:67.2, irpf:0, total:387.2, concepto:'Diseno landing', estado:'ok', status:'ok', confidence:84} ]; } function enterPreviewMode(screen) { window.__PREVIEW_MODE__ = true; STORE.token = 'preview-token'; loggedIn = true; CURRENT_USER = { id:'preview-user', email:'', user_metadata:{ full_name:'Carla Molina' } }; CURRENT_PROFILE = { id:'preview-user', nombre:'Carla Molina', cif:'B88725494', plan: screen === 'gestoria' ? 'gestoria_pro' : 'business', facturas_mes: 38, created_at:'2026-05-01' }; var demo = previewFacturasDemo(); FACTURAS_PROCESADAS = demo.slice(0, 2); FACTURAS_HISTORIAL = demo; FACTURAS_HISTORIAL_GLOBAL = demo; HISTORIAL_DATA = demo; CLIENTE_ACTIVO = null; updateNavForUser(); if (screen === 'perfil') { SS('perfil'); renderPerfilIndividual(); return; } if (screen === 'upload') { SS('upload'); return; } if (screen === 'analytics') { SS('analytics'); _renderAnalyticsConDatos(demo); return; } if (screen === 'fiscal') { SS('fiscal'); renderFiscalScreen(); return; } if (screen === 'historial') { SS('historial'); HISTORIAL_DATA = demo; renderHistorial(); return; } if (screen === 'gestoria') { CURRENT_PROFILE.plan = 'gestoria_pro'; CLIENTES_LIST = [ {id:'cliente-a', nombre:'Cliente Norte SL', nif:'B12345678', _nFacturas:4}, {id:'cliente-b', nombre:'Estudio Azul', nif:'B22222222', _nFacturas:2}, {id:'cliente-c', nombre:'Mercado Sur SL', nif:'B44444444', _nFacturas:6} ]; CLIENTE_ACTIVO = CLIENTES_LIST[0]; FACTURAS_CLIENTE = demo.map(function(f) { return Object.assign({}, f, { cliente_id:'cliente-a' }); }); _FACTURAS_CACHE[CLIENTE_ACTIVO.id] = { data: FACTURAS_CLIENTE, ts: Date.now(), anio:'2026' }; SS('gestoria'); renderClientesList(); renderClienteMain(); return; } } function handleLandingCta() { try { var params = new URLSearchParams(window.location.search || ''); var preview = params.get('preview'); if (preview) { var allowedPreview = ['onboarding','upload','perfil','analytics','fiscal','historial','gestoria']; if (allowedPreview.indexOf(preview) !== -1) { window.history.replaceState(null, '', window.location.pathname + '?preview=' + preview); setTimeout(function() { if (preview === 'onboarding') SS('onboarding'); else enterPreviewMode(preview); }, 80); } return; } var cta = params.get('cta'); if (!cta) return; var cleanUrl = window.location.pathname + window.location.hash; window.history.replaceState(null, '', cleanUrl || '/'); setTimeout(function() { if (cta === 'pricing-gestoria') { goToPricing('gestoria'); } else if (cta === 'pricing-individual') { goToPricing('individual'); } else if (cta === 'plan-gestoria') { try { localStorage.setItem('signup_plan_type', 'gestoria'); } catch(e) {} showPlanModal(); } else if (cta === 'plan-individual') { try { localStorage.setItem('signup_plan_type', 'individual'); } catch(e) {} showPlanModal(); } }, 80); } catch(e) {} } document.addEventListener('DOMContentLoaded', handleLandingCta); document.addEventListener('DOMContentLoaded', initCookieBanner); // exportarExcelFiltrado: exporta desde modal-filtro-excel (analytics) function exportarExcelFiltrado() { var tipo = (document.getElementById('filtro-excel-tipo') || {}).value || 'todos'; var anio = (document.getElementById('filtro-excel-anio') || {}).value || ''; var mesSel= (document.getElementById('filtro-excel-mes-sel') || {}).value || 'todos'; // Determinar fuente de datos según contexto activo var data; if (CLIENTE_ACTIVO && CLIENTE_ACTIVO.id && FACTURAS_CLIENTE && FACTURAS_CLIENTE.length) { // Contexto gestoría — usar facturas del cliente activo data = FACTURAS_CLIENTE; } else { // Contexto analytics/perfil — usar historial data = (HISTORIAL_DATA && HISTORIAL_DATA.length ? HISTORIAL_DATA : null) || (FACTURAS_HISTORIAL && FACTURAS_HISTORIAL.length ? FACTURAS_HISTORIAL : []); } var rows = data.filter(function(f) { return facturaPasaFiltroExcel(f, anio, mesSel, tipo); }); if (!rows.length) { toast('No hay facturas con esos filtros', 'err'); return; } trackEvent('export_excel', { source: CLIENTE_ACTIVO ? 'gestoria_cliente' : 'analytics', export_type: tipo, row_count: rows.length }); document.getElementById('modal-filtro-excel').style.display = 'none'; fetch('/api/export-excel', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ rows: rows, fields: ['numero_factura','fecha','tipo','emisor','cif_emisor','base_imponible','iva_porcentaje','cuota_iva','irpf','total','concepto','observaciones'] }) }) .then(function(r) { return r.blob(); }) .then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'facturas_filtradas_' + new Date().toISOString().slice(0,10) + '.xlsx'; a.click(); URL.revokeObjectURL(url); toast('Excel descargado ✓', 'ok'); }) .catch(function() { toast('Error al descargar Excel', 'err'); }); } // confirmarBilling: confirma modal de billing y va a checkout function confirmarBilling() { document.getElementById('modal-billing').style.display = 'none'; SS('checkout'); } function renderAjustes() { if (!CURRENT_PROFILE) return; var nombre = getNombrePerfil(CURRENT_PROFILE) || ''; var nif = getCifPerfil(CURRENT_PROFILE) || '—'; var _plan = CURRENT_PROFILE.plan || 'free'; var _planNames = {free:'Gratuito', starter:'Starter', pro:'Pro', business:'Business', gestoria_trial:'Gestoría prueba', gestoria_starter:'Gestoría Starter', gestoria_pro:'Gestoría Pro', gestoria_business:'Gestoría Business', gestoria_scale:'Gestoría Scale', gestoria:'Gestoría', cortesia:'Cortesía', staff:'Staff'}; var planLabel = _planNames[_plan] || _plan; var _limite = {free:10,starter:60,pro:250,business:800,gestoria_trial:20,gestoria_starter:1000, gestoria_pro:5000,gestoria_business:15000,gestoria_scale:40000,gestoria:999999, cortesia:50,staff:999999}; var _cliLimite = {gestoria_trial:2, gestoria_starter:10,gestoria_pro:50,gestoria_business:150, gestoria_scale:400,gestoria:999999,staff:999999}; var usadas = CURRENT_PROFILE.facturas_mes || 0; var limiteStr = (_limite[_plan] >= 999999) ? '∞' : String(_limite[_plan]); var pct = _limite[_plan] >= 999999 ? 0 : Math.min(100, Math.round(usadas / _limite[_plan] * 100)); var barColor = pct >= 90 ? 'var(--red)' : pct >= 70 ? 'var(--amber)' : 'var(--brand)'; var isGestor = ['gestoria','gestoria_trial','gestoria_starter','gestoria_pro','gestoria_business','gestoria_scale','staff'] .indexOf(_plan) !== -1; var nClientes = (CLIENTES_LIST || []).length; var limClientes = _cliLimite[_plan] || 999999; var limClientesStr = limClientes >= 999999 ? '∞' : String(limClientes); var pctCli = limClientes >= 999999 ? 0 : Math.min(100, Math.round(nClientes / limClientes * 100)); var barColorCli = pctCli >= 90 ? 'var(--red)' : pctCli >= 70 ? 'var(--amber)' : 'var(--brand)'; var el = document.getElementById('ajustes-main'); if (!el) return; el.innerHTML = '
' + '

Ajustes de cuenta

' + // Nombre '
' + '
Nombre
' + '
' + '' + '' + '
' + '
' + // NIF/CIF — shown for all (gestoria has their company CIF too) '
' + '
' + (isGestor ? 'CIF de la gestoría' : 'NIF / CIF') + '
' + '
' + (isGestor ? 'CIF de tu empresa o gestoría. La IA lo usa internamente para identificar tu cuenta.' : 'La IA lo usa para clasificar tus facturas como ingresos o gastos.') + '
' + '
' + '' + '' + '
' + '
' + // Plan actual con barra de uso '
' + '
' + '
' + '
Plan actual
' + '
'+planLabel+'
' + '
' + '
' + '' + (_plan !== 'free' && _plan !== 'cortesia' ? '' : '') + '
' + '
' + // Facturas this month '
' + 'Facturas este mes' + ''+usadas+' / '+limiteStr+'' + '
' + '
' + '
' + '
' + // Clients count (gestoria only) (isGestor ? '
' + 'Clientes activos' + ''+nClientes+' / '+limClientesStr+'' + '
' + '
' + '
' + '
' : '') + '
' + // Facturas de Contabilify (pagos del servicio) '
' + '
' + '
Facturación del servicio
' + '
' + '
' + 'Plan activo' + '' + planLabel + '' + '
' + '
' + 'Ciclo de facturación' + 'Mensual' + '
' + '
' + 'Próxima renovación' + '' + (_plan === 'free' ? '—' : 'Se renueva automáticamente') + '' + '
' + '
' + '' + '' + '
' + '
Para descargar tus facturas de Contabilify, solicítalo por soporte indicando el período y te las enviamos por email.
' + '
' + // Contraseña '
' + '
' + '
Contraseña
' + '
Envía un enlace de cambio de contraseña a tu email
' + '
' + '' + '
' + // Cerrar sesión '
' + '
Cerrar la sesión en este dispositivo
' + '' + '
' + // Zona peligrosa '
' + '
⚠ Zona peligrosa
' + // Eliminar datos '
' + '
' + '
' + '
Eliminar mis datos
' + '
' + (isGestor ? 'Elimina todas las facturas de todos tus clientes, historial y analítica. Tu cuenta y tus clientes permanecen. No se puede deshacer.' : 'Elimina todas tus facturas, historial, analítica y datos de procesamiento. Tu cuenta permanece activa. No se puede deshacer.') + '
' + '
' + '' + '
' + '
' + // Eliminar cuenta '
' + '
' + '
' + '
Eliminar cuenta
' + '
' + (isGestor ? 'Elimina tu cuenta, todos tus clientes, sus facturas y todos los datos de forma permanente. Cancela cualquier suscripción activa. No se puede recuperar.' : 'Elimina tu cuenta, todas tus facturas y datos permanentemente. Cancela cualquier suscripción activa. No se puede recuperar.') + '
' + '
' + '' + '
' + '
' + '
' + '
'; } function guardarNifPerfil() { var input = document.getElementById('ajustes-nif-input'); if (!input) return; var nif = input.value.trim().toUpperCase(); if (!nif) { toast('El NIF/CIF no puede estar vacío', 'err'); return; } if (nif.length < 8 || nif.length > 15) { toast('El NIF/CIF no es válido', 'err'); return; } var btn = input.nextElementSibling; if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } fetch('/api/profile', { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ cif: nif }) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok) { if (CURRENT_PROFILE) CURRENT_PROFILE.cif = nif; toast('NIF/CIF actualizado ✓', 'ok'); if (btn) { btn.textContent = '✓ Guardado'; btn.style.background = 'var(--green)'; } setTimeout(function(){ if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; btn.style.background = ''; } renderAjustes(); }, 1500); } else { toast(d.error || 'Error al guardar', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } } }) .catch(function(){ toast('Error de conexión', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } }); } function irEspacioPrincipal() { var plan = CURRENT_PROFILE ? (CURRENT_PROFILE.plan || 'free') : 'free'; var isGestor = ['gestoria','gestoria_trial','gestoria_starter','gestoria_pro','gestoria_business','gestoria_scale','staff'] .indexOf(plan) !== -1; if (isGestor) { SS('gestoria'); } else { SS('perfil'); renderPerfilIndividual(); } } function irAjustes() { SS('ajustes'); renderAjustes(); } // ── AJUSTES DE CLIENTE (GESTORÍA) ─────────────────────────────────────────── function renderClienteAjustes() { var c = CLIENTE_ACTIVO; if (!c) return; var el = document.getElementById('perfil-main') || document.getElementById('cliente-main-wrap'); // Find the gestoria-main element var mainEl = document.getElementById('gestoria-main'); if (!mainEl) return; // Append settings div after the main content var existingAjustes = document.getElementById('cliente-ajustes-block'); if (existingAjustes) existingAjustes.remove(); var div = document.createElement('div'); div.id = 'cliente-ajustes-block'; div.style.cssText = 'padding:0 0 60px'; div.innerHTML = '
' + '
' + '
Ajustes del cliente
' + // NIF editable '
' + '
NIF / CIF
' + '
La IA usa este NIF para clasificar las facturas de este cliente.
' + '
' + '' + '' + '
' + '
' + // Nombre editable '
' + '
Nombre del cliente
' + '
' + '' + '' + '
' + '
' + // Zona peligrosa cliente '
' + '
⚠ Zona peligrosa
' + // Eliminar datos del cliente '
' + '
' + '
' + '
Eliminar datos del cliente
' + '
Elimina todas las facturas, historial y analítica de '+c.nombre+'. El cliente permanece en tu cuenta. No se puede deshacer.
' + '
' + '' + '
' + '
' + // Eliminar cliente completo '
' + '
' + '
' + '
Eliminar cliente
' + '
Elimina '+c.nombre+', todas sus facturas y datos permanentemente. No se puede deshacer.
' + '
' + '' + '
' + '
' + '
' + '
' + '
'; // Append to gestoria-main mainEl.appendChild(div); } function guardarNifCliente() { var input = document.getElementById('cliente-nif-input'); if (!input || !CLIENTE_ACTIVO) return; var btn = input.nextElementSibling; if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } var nif = input.value.trim().toUpperCase(); if (!nif || nif.length < 8) { toast('NIF/CIF no válido', 'err'); return; } fetch('/api/clientes/' + CLIENTE_ACTIVO.id, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ nif: nif }) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok || d.id) { CLIENTE_ACTIVO.nif = nif; // Update in CLIENTES_LIST var idx = CLIENTES_LIST.findIndex(function(c){ return c.id === CLIENTE_ACTIVO.id; }); if (idx >= 0) CLIENTES_LIST[idx].nif = nif; toast('NIF/CIF actualizado ✓', 'ok'); if (btn) { btn.textContent = '✓ Guardado'; btn.style.background = 'var(--green)'; setTimeout(function(){ btn.disabled = false; btn.textContent = 'Guardar'; btn.style.background = ''; }, 2000); } renderClienteMain(); } else { toast(d.error || 'Error al guardar', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } } }) .catch(function(){ toast('Error de conexión', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } }); } function guardarNombreCliente() { var input = document.getElementById('cliente-nombre-input'); if (!input || !CLIENTE_ACTIVO) return; var btn = input.nextElementSibling; if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } var nombre = input.value.trim(); if (!nombre) { toast('El nombre no puede estar vacío', 'err'); return; } fetch('/api/clientes/' + CLIENTE_ACTIVO.id, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ nombre: nombre }) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok || d.id) { CLIENTE_ACTIVO.nombre = nombre; var idx = CLIENTES_LIST.findIndex(function(c){ return c.id === CLIENTE_ACTIVO.id; }); if (idx >= 0) CLIENTES_LIST[idx].nombre = nombre; toast('Nombre actualizado ✓', 'ok'); if (btn) { btn.textContent = '✓ Guardado'; btn.style.background = 'var(--green)'; setTimeout(function(){ btn.disabled = false; btn.textContent = 'Guardar'; btn.style.background = ''; }, 2000); } renderClienteMain(); } else { toast(d.error || 'Error al guardar', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } } }) .catch(function(){ toast('Error de conexión', 'err'); if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } }); } function confirmarEliminarDatosCliente() { if (!CLIENTE_ACTIVO) return; if (!confirm('¿Eliminar TODAS las facturas e historial de ' + CLIENTE_ACTIVO.nombre + '? Esta acción es IRREVERSIBLE.')) return; var confirmText = prompt('Para confirmar, escribe exactamente: ELIMINAR'); if (confirmText !== 'ELIMINAR') { toast('Eliminación cancelada', 'info'); return; } toast('Eliminando datos de ' + CLIENTE_ACTIVO.nombre + '...', 'info'); fetch('/api/facturas/all?cliente_id=' + CLIENTE_ACTIVO.id, { method: 'DELETE', headers: Object.assign(getHeaders(), {'Content-Type': 'application/json'}), body: JSON.stringify({ confirm: getDeleteConfirm() }) }) .then(function(r) { return r.json(); }) .then(function(d) { if (d.ok === false) { toast(d.error || 'Error al eliminar', 'err'); return; } FACTURAS_CLIENTE = []; invalidarCacheCliente(CLIENTE_ACTIVO.id); toast('Datos de ' + CLIENTE_ACTIVO.nombre + ' eliminados', 'ok'); renderClienteMain(); }) .catch(function(){ toast('Error al eliminar datos', 'err'); }); } function irEspacioMovil() { var plan = CURRENT_PROFILE ? (CURRENT_PROFILE.plan || 'free') : 'free'; var isGestor = ['gestoria','gestoria_trial','gestoria_starter','gestoria_pro','gestoria_business','gestoria_scale','staff'] .indexOf(plan) !== -1; if (isGestor) { SS('gestoria'); } else { SS('perfil'); renderPerfilIndividual(); } } function irAGestoriaClientes() { SS('gestoria'); } function confirmarCancelarSuscripcion() { if (confirm('¿Cancelar tu suscripción? Tu plan seguirá activo hasta el final del período de facturación. Después pasarás al plan Gratuito.')) { fetch('/api/billing/cancel', { method: 'POST', headers: getHeaders() }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.ok && d.pending) { toast(d.message || 'Hemos registrado tu solicitud. Soporte te confirmará la cancelación.', 'info'); abrirSoporte(); } else if (d.ok) { toast('Suscripción cancelada. Tu plan sigue activo hasta el próximo ciclo.', 'ok'); setTimeout(function(){ renderAjustes(); }, 1500); } else { // If no billing endpoint yet, show contact info toast('Para cancelar, escríbenos a soporte@contabilify.com o usa el chat de soporte.', 'info'); } }) .catch(function(){ toast('Para cancelar tu suscripción, contacta con nosotros en soporte@contabilify.com', 'info'); }); } }
IA aplicada a la contabilidad profesional

Automatización contable con IA
para gestorías, autónomos y pymes

Sube tus facturas en PDF y Contabilify las procesa automáticamente: extrae los datos, clasifica ingresos y gastos, calcula IVA e IRPF y genera analítica financiera.

Mi espacio
24 facturas
Analítica
Actualizada
Fiscal
IVA e IRPF
8.420 EURIngresos
2.180 EURGastos
1.087 EURIVA neto
FacturaTipoIVAEstado
FAC-2026-042Ingreso21%OK
Proveedor CloudGasto21%OK
Factura duplicadaGasto10%Aviso
Extracción IA desde PDF Export Excel estructurado Clasificación automática Detector de duplicados IVA trimestral estimado Analítica financiera Informe trimestral fiscal Datos seguros · RGPD
Por qué Contabilify

Control contable automático, no solo digitalización

Contabilify entiende qué es cada factura, la clasifica, calcula su impacto fiscal y actualiza tu analítica en tiempo real.

Extracción completa
De la factura al dato en segundos
La IA lee el PDF y extrae todos los campos sin intervención manual: emisor, receptor, NIF, fecha, base imponible, IVA, IRPF y total.
Clasificación automática
Clasificación sin margen de error
El sistema compara el NIF/CIF registrado con el emisor y receptor de cada factura, y avisa si detecta alguna discrepancia entre los datos.
Control fiscal continuo
IVA e IRPF en tiempo real
Con cada factura procesada, el cálculo de IVA e IRPF se actualiza de forma automática, sin esperar al cierre del trimestre.
Integridad de datos
Cero facturas duplicadas
Antes de guardar, verifica si la factura ya existe comparando número, emisor y CIF. Si hay coincidencia, solicita confirmación al usuario.
Gestión multicliente
Cada cliente separado por completo
Cada cliente opera en su propio espacio con NIF, historial y analítica. El sistema avisa si se detecta una factura de otro cliente.
Visibilidad financiera
Analítica en tiempo real
Dashboard financiero con ingresos, gastos, margen y comparativa interanual por cliente, actualizado con cada factura procesada.
Para quién es

Diseñado para quien gestiona facturas a diario

Gestorías y asesorías
Gestiona varios clientes desde un único espacio con NIF, historial y analítica independientes. El sistema detecta y avisa si una factura no pertenece al cliente activo.
Autónomos
Controla tus ingresos y gastos sin conocimientos contables. Sube tus facturas y consulta tu situación fiscal, el Excel y el informe trimestral siempre actualizados.
Pymes y ecommerce
Gestiona un alto volumen de facturas con analítica por periodos, comparativa interanual y libro de ingresos y gastos siempre actualizado sin intervención manual.

Empieza hoy.
Sin tarjeta ni compromiso.

Prueba Contabilify con tus propias facturas antes de elegir un plan. Sin compromiso.

Bienvenido de nuevo
Entra en tu cuenta para continuar
o con tu email
Crea tu cuenta gratis
10 facturas al mes sin pagar nada
o con email
Al registrarte aceptas los Términos y la Política de privacidad.
Recuperar contraseña
Te enviamos un enlace a tu email.

¡Bienvenida a Contabilify! 🎉

Antes de empezar, necesitamos tu NIF o CIF para que la IA pueda clasificar tus facturas correctamente.

¿Por qué lo necesitamos?
La IA lee tus facturas y necesita saber tu identificación fiscal para distinguir si una factura es un ingreso (tú la emites) o un gasto (tú la recibes). Sin este dato, la clasificación puede ser incorrecta.
1
Guarda tu NIF/CIF
Es la referencia para comparar contra emisor y receptor.
2
Sube un PDF
La IA lee fecha, importes, IVA, IRPF, emisor y receptor.
3
Revisa y exporta
Verás avisos, analítica y Excel con datos revisables.
NIF si eres autónomo (8 números + letra) · CIF si eres empresa (letra + 8 números)
Este dato es obligatorio para que la IA funcione correctamente
Paso a paso

Sube tus facturas en PDF

La IA detecta automáticamente si cada factura es un gasto o un ingreso.

1
Sube tus PDFs
2
Elige los datos
3
Descarga el Excel
Arrastra tus facturas aquí
Gastos e ingresos mezclados — la IA los clasifica sola · Máx. 10 PDFs
Seleccionar archivos
Olvídate de hacerlo a mano. Contabilify genera tu Excel automáticamente con todos los datos.
La IA detecta si cada factura es un ingreso o un gasto — sin que hagas nada.
Facturas este mes
0 / 10 Ampliar
Analizando...0%
Leyendo y clasificando tus facturas

Resultados

Sube facturas para ver los resultados aquí

Aún no tienes resultados

Sube tus primeras facturas en PDF y la IA las clasificará automáticamente en ingresos y gastos.

¿Cómo se comparan mis ingresos y gastos?
Azul = ingresos · Rojo = gastos
Ingresos
Gastos
¿Quién me paga más?
Principales clientes
Cargando...
¿A quién le pago más?
Principales proveedores
Cargando...
¿En qué se va el dinero?
Desglose de gastos por categoría
Sin gastos.
Semáforo fiscal
Cuánto debes pagar y cuándo
Cargando...
¿Qué margen tuve cada mes?
Porcentaje de ingresos que se convierte en beneficio neto
¿Qué debería revisar?
Comparar periodos
Selecciona dos periodos para ver la evolución
¿Cómo voy respecto al año pasado?
Evolución de ingresos comparada entre años
Año Ingresos Gastos Neto Margen
Cargando…
¿Qué servicios generan más?
Ranking de conceptos por volumen total
Sin datos.
¿Cuándo cobras y cuánto?
Ticket medio y concentración de ingresos en el mes
Cargando...

Fiscal

IVA trimestral, retenciones IRPF y libro de ingresos y gastos

Estimación IVA trimestral
Cálculo orientativo basado en las facturas registradas en Contabilify
Estimación orientativa. Contabilify es una herramienta de apoyo, no un contable ni asesor fiscal. Este cálculo incluye solo las facturas subidas a Contabilify y puede contener errores de OCR, clasificación o interpretación. Verifica siempre los datos con tu gestor o asesor fiscal antes de presentar el Modelo 303 u otro modelo ante Hacienda.
IVA Repercutido
De tus ingresos
IVA Soportado
De tus gastos
Resultado
Concepto Base imponible Cuota IVA Nº facturas
Selecciona un trimestre
Aviso importante: Los cálculos mostrados son estimaciones orientativas basadas en las facturas procesadas por OCR. Contabilify es una herramienta tecnológica de apoyo y no presta servicios de asesoría fiscal, contable ni legal. La confianza OCR indica la calidad del reconocimiento automático, no la validez fiscal o legal de los documentos. Este módulo no sustituye la revisión de un profesional fiscal, contable o gestor autorizado antes de presentar cualquier modelo tributario a Hacienda. El usuario es responsable de revisar los datos y Contabilify no se hace responsable de errores derivados de facturas incompletas, mal clasificadas, no incluidas en el sistema o interpretadas incorrectamente.
Retenciones IRPF acumuladas
Retenciones que tus clientes te han practicado — para tu declaración de la renta (IRPF)
Estimación orientativa. Contabilify no es un asesor fiscal ni un contable. Incluye solo las facturas subidas a Contabilify y puede contener errores. Consulta siempre con tu gestor o asesor fiscal antes de presentar tu declaración de la renta.
IRPF retenido
Te han retenido tus clientes
Base imponible
Base imponible total de tus ingresos
% retención medio
Sobre tus ingresos
Trimestre Base ingresos IRPF retenido Nº facturas
Libro de ingresos y gastos
Registro de facturas en formato para tu gestor o asesor
Solo incluye facturas subidas a Contabilify. Este libro es una ayuda de trabajo y no sustituye al libro registro oficial ni a una revisión contable profesional. Compártelo con tu gestor para que lo complete y valide antes de cualquier declaración fiscal.
Fecha Nº Factura Contraparte (cliente/proveedor) Tipo Base IVA IRPF Total
Cargando...
Mis clientes
Plan Gestoría
Cargando...
👥
Cargando clientes...

Elige tu plan

Control contable automático con IA — extracción, clasificación, fiscalidad y analítica.

Sin permanencia · Cancela cuando quieras
Mensual
Anual 2 meses gratis
Prueba gratis
0€ /mes
2 clientes · 20 facturas/mes Todas las funcionalidades incluidas Sin tarjeta de crédito
Starter
79/mes
Para gestorías que empiezan a digitalizar sus clientes.
Hasta 10 clientes · 1.000 facturas/mes
Espacio independiente por cliente
Extracción automática con IA
Clasificación ingreso / gasto
Validación NIF por cliente
Detector de duplicados
Analítica financiera por cliente
Libro de ingresos y gastos
Comparativa interanual
Semáforo fiscal trimestral
Ranking de conceptos y proveedores
IVA trimestral + IRPF estimados
Export Excel estructurado
Informe trimestral fiscal
Asistente IA
Business
349/mes
Para asesorías con alto volumen de clientes y facturas.
Hasta 150 clientes · 15.000 facturas/mes
Todo lo del plan Pro
Scale
749/mes
Para grandes asesorías que gestionan un volumen muy alto.
Hasta 400 clientes · 40.000 facturas/mes
Todo lo del plan Business
Preguntas frecuentes
¿Por qué necesitáis mi DNI, NIF o CIF? +
Lo usamos para que la IA detecte automáticamente si cada factura es un ingreso o un gasto. Si eres autónomo o persona física, es tu DNI. Si tienes empresa, es tu NIF o CIF. Lo comparamos con el emisor y receptor de cada factura: si apareces como emisor es un ingreso, si apareces como receptor es un gasto. Sin él, la detección es menos precisa.
¿Cómo detecta la IA si una factura es un ingreso o un gasto? +
Compara tu DNI o CIF con el emisor y receptor de cada factura. Si lo tienes guardado en tu perfil, la precisión es casi del 100%. Si no, la IA lo deduce por el contexto — concepto, si tiene IRPF, tipo de servicio — y en caso de duda clasifica como gasto.
¿Funciona con facturas escaneadas o fotos? +
Solo con PDFs digitales por ahora. Si tienes una factura en papel, puedes convertirla a PDF con cualquier app de escáner del móvil (como Adobe Scan o CamScanner) y subirla sin problema.
¿Por qué los planes individuales solo funcionan para una persona o empresa? +
Porque cada cuenta está vinculada a un único NIF o CIF. La IA usa ese identificador para clasificar correctamente tus facturas. Si se mezclan facturas de distintas personas o empresas, la clasificación sería incorrecta y la analítica no tendría sentido. Si necesitas gestionar varios clientes, los planes Gestoría te permiten crear un espacio independiente para cada uno con su propio NIF.
¿Qué incluye exactamente el plan Gestoría? +
Los planes Gestoría están diseñados para gestorías y asesorías que gestionan las facturas de varios clientes. Puedes crear un espacio independiente para cada cliente con su propio NIF y analítica separada. El sistema detecta automáticamente si una factura subida no pertenece al cliente activo, evitando errores de clasificación entre clientes.
¿Están mis datos seguros? +
Sí. Los datos se guardan cifrados en servidores dentro de la Unión Europea, cumpliendo con el RGPD. Puedes consultar nuestra política de privacidad, aviso legal y el acuerdo de encargado del tratamiento (DPA) desde el pie de página.
¿Puedo cambiar de plan cuando quiera? +
Sí, sin permanencia. Los cambios se aplican al inicio del siguiente ciclo de facturación.
¿Cuándo se reinicia el contador de facturas? +
El mismo día del mes en que te registraste. Por ejemplo, si te registraste el día 10, tu contador se reinicia cada día 10.
¿Necesito tarjeta para el plan gratuito? +
No. Solo necesitas un email para empezar. Sin compromisos ni límite de tiempo en el plan gratuito.

Historial de facturas

Cargando...
Tipo Emisor / Cliente Número Fecha Total Acciones
Cargando...

¿En qué podemos ayudarte?

Cuéntanos tu consulta y te responderemos en un máximo de 48 horas laborables. Para incidencias urgentes escríbenos a soporte@contabilify.com.

Tiempo de respuesta
Máximo 48 h laborables.
Normalmente menos de 24 h.
Horario de soporte
Lunes a viernes,
9:00 – 18:00 h (CET).
Usaremos este email para responderte. El mensaje llega a soporte@contabilify.com.
0/2000
⚠️

Cuenta eliminada

Esta cuenta ha sido eliminada y el email queda bloqueado para evitar abuso del plan gratuito. Si quieres reactivarla, contacta con soporte.

Solicitar activación del plan
La pasarela de pago estará conectada próximamente. Mientras tanto, puedes solicitar el alta y te responderemos desde soporte@contabilify.com.
IVA (21%)
Total