From 761e56e87f6fbfbb93be92e73a6d4667e0cc0e50 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Wed, 1 Apr 2026 08:27:59 +0000 Subject: [PATCH] fix(pos): rewrite invoicing + accounting JS to match design system HTML Invoicing JS now targets panel-facturas/notas/complementos/cancelaciones/config panels, modalDetalleOverlay/modalCancelOverlay modals, and switchTab() calls. Loads CFDI queue data dynamically into data-table tbodies replacing demo rows. Accounting JS now targets panel-cxc/cxp/balance/resultados/flujo/conciliacion/cierre panels and finance-card elements. Wires aging, balance sheet, income statement, and period close to real API endpoints. Both files include auth check, live clock, and global switchTab binding. Co-Authored-By: Claude Opus 4.6 (1M context) --- pos/static/js/accounting.js | 871 ++++++++++++++---------------------- pos/static/js/invoicing.js | 511 +++++++++++++++------ 2 files changed, 704 insertions(+), 678 deletions(-) diff --git a/pos/static/js/accounting.js b/pos/static/js/accounting.js index b3bb513..d0367be 100644 --- a/pos/static/js/accounting.js +++ b/pos/static/js/accounting.js @@ -1,10 +1,9 @@ // /home/Autopartes/pos/static/js/accounting.js -// Accounting module: chart of accounts, journal entries, financial reports +// Accounting module — wired to design-system HTML IDs +// Tabs: panel-cxc, panel-cxp, panel-balance, panel-resultados, panel-flujo, panel-conciliacion, panel-cierre const Accounting = (() => { const API = '/pos/api/accounting'; - let accounts = []; // cached for dropdowns - let entryLineCtr = 0; // counter for entry line IDs function token() { return localStorage.getItem('pos_token') || ''; @@ -27,567 +26,373 @@ const Accounting = (() => { return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } - // ─── Tabs ────────────────────────────────────── + // ---- Auth check ---- + function checkAuth() { + if (!token()) { + window.location.href = '/pos/login'; + return false; + } + return true; + } - function initTabs() { - document.querySelectorAll('.tab').forEach(tab => { - tab.addEventListener('click', () => { - document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); - document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active')); - tab.classList.add('active'); - document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active'); - - // Load data on tab switch - const t = tab.dataset.tab; - if (t === 'accounts') loadAccounts(); - if (t === 'entries') loadEntries(); - if (t === 'trial-balance') loadTrialBalance(); - if (t === 'income-statement') loadIncomeStatement(); - if (t === 'balance-sheet') loadBalanceSheet(); - if (t === 'aging') loadAging(); - if (t === 'periods') loadPeriods(); - }); + // ---- Tab switching (matches design system onclick="switchTab('xxx')") ---- + function switchTab(name) { + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.classList.remove('is-active'); + btn.setAttribute('aria-selected', 'false'); }); - } + document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('is-active')); - // ─── Chart of Accounts ───────────────────────── - - async function loadAccounts() { - try { - const res = await api('/accounts'); - accounts = res.data || []; - renderAccountsTree(); - } catch (e) { - document.getElementById('accounts-tree').innerHTML = - `

Error: ${e.message}

`; - } - } - - function renderAccountsTree() { - const container = document.getElementById('accounts-tree'); - if (!accounts.length) { container.innerHTML = '

No hay cuentas.

'; return; } - - // Build tree structure - const byParent = {}; - accounts.forEach(a => { - const pid = a.parent_id || 'root'; - if (!byParent[pid]) byParent[pid] = []; - byParent[pid].push(a); - }); - - function buildUl(parentId) { - const children = byParent[parentId] || []; - if (!children.length) return ''; - let html = ''; - return html; + // Activate button + const tabBtn = document.querySelector(`.tab-btn[onclick*="'${name}'"]`); + if (tabBtn) { + tabBtn.classList.add('is-active'); + tabBtn.setAttribute('aria-selected', 'true'); } - container.innerHTML = buildUl('root'); + // Activate panel + const panel = document.getElementById(`panel-${name}`); + if (panel) panel.classList.add('is-active'); + + // Load data + if (name === 'cxc') loadAging(); + if (name === 'cxp') loadAccountsPayable(); + if (name === 'balance') loadBalanceSheet(); + if (name === 'resultados') loadIncomeStatement(); + if (name === 'flujo') loadCashFlow(); + if (name === 'conciliacion') loadReconciliation(); + if (name === 'cierre') loadPeriodClose(); } - function showNewAccountModal() { - const sel = document.getElementById('na-parent'); - sel.innerHTML = ''; - accounts.forEach(a => { - sel.innerHTML += ``; - }); - document.getElementById('na-code').value = ''; - document.getElementById('na-name').value = ''; - document.getElementById('new-account-modal').classList.add('active'); + // ---- Badge helper ---- + function statusBadge(status, label) { + const map = { + pending: 'badge--pending', + vigente: 'badge--pending', + overdue: 'badge--overdue', + vencida: 'badge--overdue', + partial: 'badge--partial', + parcial: 'badge--partial', + ok: 'badge--ok', + pagada: 'badge--ok', + open: 'badge--pending', + closed: 'badge--ok', + }; + return `${label || status}`; } - async function createAccount() { - try { - const parentId = document.getElementById('na-parent').value; - await api('/accounts', { - method: 'POST', - body: JSON.stringify({ - code: document.getElementById('na-code').value, - name: document.getElementById('na-name').value, - parent_id: parentId ? parseInt(parentId) : null, - type: document.getElementById('na-type').value, - }), - }); - closeModal('new-account-modal'); - loadAccounts(); - } catch (e) { - alert('Error: ' + e.message); - } - } - - // ─── Journal Entries ─────────────────────────── - - async function loadEntries() { - try { - const from = document.getElementById('entries-from').value; - const to = document.getElementById('entries-to').value; - const type = document.getElementById('entries-type').value; - let qs = '?per_page=50'; - if (from) qs += `&date_from=${from}`; - if (to) qs += `&date_to=${to}`; - if (type) qs += `&type=${type}`; - - const res = await api(`/entries${qs}`); - renderEntries(res.data || []); - } catch (e) { - document.getElementById('entries-list').innerHTML = - `

Error: ${e.message}

`; - } - } - - function renderEntries(entries) { - const container = document.getElementById('entries-list'); - if (!entries.length) { container.innerHTML = '

No hay polizas en este periodo.

'; return; } - - let html = ` - - - - `; - - for (const e of entries) { - html += ` - - - - - - - - `; - } - html += '
#FechaTipoDescripcionReferenciaMontoAuto
${e.entry_number}${e.date || ''}${e.type || ''}${e.description || ''}${e.reference_type ? `${e.reference_type} #${e.reference_id}` : ''}$${fmt(e.total_amount)}${e.is_auto ? 'Si' : 'No'}
'; - container.innerHTML = html; - } - - async function showEntryDetail(entryId) { - try { - const entry = await api(`/entries/${entryId}`); - let html = `

Poliza #${entry.entry_number}

-
- Fecha: ${entry.date} - Tipo: ${entry.type} - Estado: ${entry.status} -
-

${entry.description || ''}

- - - - - `; - - for (const l of entry.lines) { - html += ` - - - - - - `; - } - - html += ` - - - - -
CuentaNombreCargoAbonoNota
${l.account_code}${l.account_name}${l.debit ? '$' + fmt(l.debit) : ''}${l.credit ? '$' + fmt(l.credit) : ''}${l.description || ''}
Totales$${fmt(entry.total_debit)}$${fmt(entry.total_credit)}
`; - - document.getElementById('entry-detail-content').innerHTML = html; - document.getElementById('entry-detail-modal').classList.add('active'); - } catch (e) { - alert('Error: ' + e.message); - } - } - - // ─── Manual Entry ────────────────────────────── - - function showNewEntryModal() { - document.getElementById('ne-date').value = new Date().toISOString().slice(0, 10); - document.getElementById('ne-description').value = ''; - document.getElementById('ne-lines').innerHTML = ''; - document.getElementById('ne-balance').innerHTML = ''; - entryLineCtr = 0; - addEntryLine(); - addEntryLine(); - document.getElementById('new-entry-modal').classList.add('active'); - } - - function addEntryLine() { - const id = entryLineCtr++; - const tbody = document.getElementById('ne-lines'); - const tr = document.createElement('tr'); - tr.id = `ne-line-${id}`; - - let acctOptions = ''; - accounts.forEach(a => { - if (a.parent_id) { // Only leaf accounts - acctOptions += ``; - } - }); - - tr.innerHTML = ` - - - - `; - tbody.appendChild(tr); - } - - function updateEntryBalance() { - const rows = document.querySelectorAll('#ne-lines tr'); - let totalDebit = 0, totalCredit = 0; - rows.forEach(row => { - const inputs = row.querySelectorAll('input[type="number"]'); - totalDebit += parseFloat(inputs[0].value) || 0; - totalCredit += parseFloat(inputs[1].value) || 0; - }); - const diff = Math.round((totalDebit - totalCredit) * 100) / 100; - const el = document.getElementById('ne-balance'); - if (diff === 0) { - el.innerHTML = `Cuadrada: Cargos $${fmt(totalDebit)} = Abonos $${fmt(totalCredit)}`; - } else { - el.innerHTML = `Descuadre: $${fmt(Math.abs(diff))} (Cargos $${fmt(totalDebit)} / Abonos $${fmt(totalCredit)})`; - } - } - - async function createEntry() { - try { - const rows = document.querySelectorAll('#ne-lines tr'); - const lines = []; - rows.forEach(row => { - const sel = row.querySelector('select'); - const inputs = row.querySelectorAll('input[type="number"]'); - const acctId = parseInt(sel.value); - const debit = parseFloat(inputs[0].value) || 0; - const credit = parseFloat(inputs[1].value) || 0; - if (acctId && (debit > 0 || credit > 0)) { - lines.push({ account_id: acctId, debit, credit, description: '' }); - } - }); - - if (lines.length < 2) { alert('Se requieren al menos 2 lineas.'); return; } - - await api('/entries', { - method: 'POST', - body: JSON.stringify({ - date: document.getElementById('ne-date').value, - description: document.getElementById('ne-description').value, - lines, - }), - }); - closeModal('new-entry-modal'); - loadEntries(); - } catch (e) { - alert('Error: ' + e.message); - } - } - - // ─── Trial Balance ───────────────────────────── - - async function loadTrialBalance() { - try { - const year = document.getElementById('tb-year').value; - const month = document.getElementById('tb-month').value; - const res = await api(`/trial-balance?year=${year}&month=${month}`); - renderTrialBalance(res); - } catch (e) { - document.getElementById('trial-balance-content').innerHTML = - `

Error: ${e.message}

`; - } - } - - function renderTrialBalance(data) { - const rows = data.data || []; - let html = ` - - - - - `; - - let totals = { si: 0, c: 0, a: 0, sf: 0 }; - for (const r of rows) { - totals.si += r.saldo_inicial; - totals.c += r.cargos; - totals.a += r.abonos; - totals.sf += r.saldo_final; - html += ` - - - - - - `; - } - - html += ` - - - - - -
CodigoCuentaSaldo InicialCargosAbonosSaldo Final
${r.code}${r.name}$${fmt(r.saldo_inicial)}$${fmt(r.cargos)}$${fmt(r.abonos)}$${fmt(r.saldo_final)}
Totales$${fmt(totals.si)}$${fmt(totals.c)}$${fmt(totals.a)}$${fmt(totals.sf)}
`; - - document.getElementById('trial-balance-content').innerHTML = html; - } - - // ─── Income Statement ────────────────────────── - - async function loadIncomeStatement() { - try { - const year = document.getElementById('is-year').value; - const month = document.getElementById('is-month').value; - const res = await api(`/income-statement?year=${year}&month=${month}`); - renderIncomeStatement(res); - } catch (e) { - document.getElementById('income-statement-content').innerHTML = - `

Error: ${e.message}

`; - } - } - - function renderIncomeStatement(data) { - let html = ''; - - // Ingresos - html += ''; - for (const item of data.ingresos.items) { - html += ``; - } - html += ``; - - // Costos - html += ''; - for (const item of data.costos.items) { - html += ``; - } - html += ``; - - // Utilidad bruta - html += ``; - - // Gastos - html += ''; - for (const item of data.gastos.items) { - html += ``; - } - html += ``; - - // Utilidad neta - const netColor = data.utilidad_neta >= 0 ? 'var(--success)' : 'var(--danger)'; - html += ` - `; - - html += '
INGRESOS
${item.code} - ${item.name}$${fmt(item.amount)}
Total Ingresos$${fmt(data.ingresos.total)}
COSTOS
${item.code} - ${item.name}$${fmt(item.amount)}
Total Costos$${fmt(data.costos.total)}
UTILIDAD BRUTA$${fmt(data.utilidad_bruta)}
GASTOS
${item.code} - ${item.name}$${fmt(item.amount)}
Total Gastos$${fmt(data.gastos.total)}
UTILIDAD NETA$${fmt(data.utilidad_neta)}
'; - document.getElementById('income-statement-content').innerHTML = html; - } - - // ─── Balance Sheet ───────────────────────────── - - async function loadBalanceSheet() { - try { - const asOf = document.getElementById('bs-date').value; - const res = await api(`/balance-sheet?date=${asOf}`); - renderBalanceSheet(res); - } catch (e) { - document.getElementById('balance-sheet-content').innerHTML = - `

Error: ${e.message}

`; - } - } - - function renderBalanceSheet(data) { - const balancedBadge = data.balanced - ? 'Cuadrado' - : 'Descuadrado'; - - let html = `

Al ${data.as_of} ${balancedBadge}

- `; - - // Activo - html += ''; - for (const item of data.activo.items) { - html += ``; - } - html += ``; - - // Pasivo - html += ''; - for (const item of data.pasivo.items) { - html += ``; - } - html += ``; - - // Capital - html += ''; - for (const item of data.capital.items) { - html += ``; - } - html += ``; - - html += ` - `; - - html += '
ACTIVO
${item.code} - ${item.name}$${fmt(item.balance)}
Total Activo$${fmt(data.activo.total)}
PASIVO
${item.code} - ${item.name}$${fmt(item.balance)}
Total Pasivo$${fmt(data.pasivo.total)}
CAPITAL
${item.code} - ${item.name}$${fmt(item.balance)}
Total Capital$${fmt(data.capital.total)}
Pasivo + Capital$${fmt(data.pasivo.total + data.capital.total)}
'; - document.getElementById('balance-sheet-content').innerHTML = html; - } - - // ─── Aging ───────────────────────────────────── - + // ---- Tab 1: Cuentas por Cobrar (Aging) ---- async function loadAging() { + const panel = document.getElementById('panel-cxc'); + if (!panel) return; + const tbody = panel.querySelector('.data-table tbody'); + if (!tbody) return; + try { const res = await api('/aging'); - renderAging(res); + const rows = res.data || []; + if (!rows.length) { + tbody.innerHTML = 'No hay cuentas por cobrar.'; + return; + } + + tbody.innerHTML = rows.map(r => { + const status = r.days_overdue > 0 ? 'overdue' : r.paid > 0 && r.balance > 0 ? 'partial' : r.balance <= 0 ? 'ok' : 'pending'; + const label = status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Vigente'; + return ` + ${r.invoice || r.folio || '-'} + ${r.name || r.customer_name || '-'} + ${r.issue_date ? new Date(r.issue_date).toLocaleDateString('es-MX') : '-'} + ${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'} + $${fmt(r.total)} + $${fmt(r.paid || 0)} + $${fmt(r.balance || r.total)} + ${statusBadge(status, label)} + + `; + }).join(''); + + // Update pagination text + const pagSpan = panel.querySelector('.pagination span'); + if (pagSpan) pagSpan.textContent = `Mostrando 1-${rows.length} de ${res.totals?.count || rows.length} registros`; + + // Update summary card badge count + updateBadgeCount('cxc', rows.length); } catch (e) { - document.getElementById('aging-content').innerHTML = - `

Error: ${e.message}

`; + tbody.innerHTML = `Error: ${e.message}`; } } - function renderAging(data) { - const rows = data.data || []; - let html = ` - - - - - - `; - - for (const r of rows) { - html += ` - - - - - - - - `; - } - - const t = data.totals || {}; - html += ` - - - - - - - -
ClienteCorriente1-30d31-60d61-90d90+dTotal
${r.name}$${fmt(r.corriente)}$${fmt(r.d1_30)}$${fmt(r.d31_60)}$${fmt(r.d61_90)}$${fmt(r.d90_plus)}$${fmt(r.total)}
Totales$${fmt(t.corriente)}$${fmt(t.d1_30)}$${fmt(t.d31_60)}$${fmt(t.d61_90)}$${fmt(t.d90_plus)}$${fmt(t.total)}
`; - - document.getElementById('aging-content').innerHTML = html; - } - - // ─── Periods ─────────────────────────────────── - - async function loadPeriods() { - try { - const res = await api('/periods'); - renderPeriods(res.data || []); - } catch (e) { - document.getElementById('periods-list').innerHTML = - `

Error: ${e.message}

`; - } - } - - function renderPeriods(periods) { - const months = ['', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', - 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; - let html = ` - `; - - for (const p of periods) { - const badge = p.status === 'closed' - ? 'Cerrado' - : 'Abierto'; - html += ` - - - - - `; - } - html += '
PeriodoEstadoCerrado porFecha cierre
${months[p.month]} ${p.year}${badge}${p.closed_by_name || '-'}${p.closed_at ? new Date(p.closed_at).toLocaleDateString('es-MX') : '-'}
'; - document.getElementById('periods-list').innerHTML = html; - } - - async function closePeriod() { - const year = parseInt(document.getElementById('cp-year').value); - const month = parseInt(document.getElementById('cp-month').value); - if (!confirm(`Cerrar periodo ${month}/${year}? Esta accion no se puede revertir.`)) return; + // ---- Tab 2: Cuentas por Pagar ---- + async function loadAccountsPayable() { + const panel = document.getElementById('panel-cxp'); + if (!panel) return; + const tbody = panel.querySelector('.data-table tbody'); + if (!tbody) return; try { - await api('/periods/close', { - method: 'POST', - body: JSON.stringify({ year, month }), + // Use accounts endpoint filtered for payables or a dedicated endpoint + const res = await api('/aging?type=payable'); + const rows = res.data || []; + if (!rows.length) { + tbody.innerHTML = 'No hay cuentas por pagar.'; + return; + } + + tbody.innerHTML = rows.map(r => { + const status = r.days_overdue > 0 ? 'overdue' : r.paid > 0 && r.balance > 0 ? 'partial' : r.balance <= 0 ? 'ok' : 'pending'; + const label = status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Vigente'; + return ` + ${r.invoice || r.folio || '-'} + ${r.name || r.vendor_name || '-'} + ${r.receipt_date ? new Date(r.receipt_date).toLocaleDateString('es-MX') : '-'} + ${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'} + $${fmt(r.total)} + $${fmt(r.paid || 0)} + $${fmt(r.balance || r.total)} + ${statusBadge(status, label)} + + `; + }).join(''); + + const pagSpan = panel.querySelector('.pagination span'); + if (pagSpan) pagSpan.textContent = `Mostrando 1-${rows.length} de ${res.totals?.count || rows.length} registros`; + } catch (e) { + tbody.innerHTML = `Error: ${e.message}`; + } + } + + // ---- Tab 3: Balance General ---- + async function loadBalanceSheet() { + const panel = document.getElementById('panel-balance'); + if (!panel) return; + const grid = panel.querySelector('.finance-grid'); + if (!grid) return; + + try { + const now = new Date(); + const res = await api(`/balance-sheet?date=${now.toISOString().slice(0, 10)}`); + + // Build assets card + const activoCard = grid.querySelector('.finance-card:first-child'); + const pasivoCard = grid.querySelector('.finance-card:last-child'); + + if (activoCard) { + let html = `
Activos
`; + if (res.activo && res.activo.items) { + for (const item of res.activo.items) { + const isNeg = item.balance < 0; + html += `
+ ${item.code ? item.code + ' ' : ''}${item.name} + $${fmt(item.balance)} +
`; + } + } + html += `
+ Total Activos + $${fmt(res.activo?.total || 0)} +
`; + activoCard.innerHTML = html; + } + + if (pasivoCard) { + let html = `
Pasivo + Capital
`; + // Pasivo + if (res.pasivo && res.pasivo.items) { + html += `
Pasivo
`; + for (const item of res.pasivo.items) { + html += `
+ ${item.code ? item.code + ' ' : ''}${item.name} + $${fmt(item.balance)} +
`; + } + html += `
Total Pasivo$${fmt(res.pasivo.total)}
`; + } + // Capital + if (res.capital && res.capital.items) { + html += `
Capital Contable
`; + for (const item of res.capital.items) { + const isPos = item.balance > 0; + html += `
+ ${item.code ? item.code + ' ' : ''}${item.name} + $${fmt(item.balance)} +
`; + } + html += `
Total Capital$${fmt(res.capital.total)}
`; + } + html += `
+ Total Pasivo + Capital + $${fmt((res.pasivo?.total || 0) + (res.capital?.total || 0))} +
`; + pasivoCard.innerHTML = html; + } + + // Update period selector text + const sel = panel.querySelector('.select-filter'); + if (sel && res.as_of) { + sel.innerHTML = ``; + } + } catch (e) { + grid.innerHTML = `

Error: ${e.message}

`; + } + } + + // ---- Tab 4: Estado de Resultados ---- + async function loadIncomeStatement() { + const panel = document.getElementById('panel-resultados'); + if (!panel) return; + const grid = panel.querySelector('.finance-grid'); + if (!grid) return; + + try { + const now = new Date(); + const res = await api(`/income-statement?year=${now.getFullYear()}&month=${now.getMonth() + 1}`); + + const card = grid.querySelector('.finance-card'); + if (!card) return; + + let html = `
Estado de Resultados
`; + + // Ingresos + html += `
Ingresos
`; + if (res.ingresos && res.ingresos.items) { + for (const item of res.ingresos.items) { + html += `
+ ${item.name} + $${fmt(item.amount)} +
`; + } + } + html += `
Total Ingresos$${fmt(res.ingresos?.total || 0)}
`; + + // Costos + html += `
Costo de Ventas
`; + if (res.costos && res.costos.items) { + for (const item of res.costos.items) { + html += `
+ ${item.name} + -$${fmt(Math.abs(item.amount))} +
`; + } + } + html += `
Utilidad Bruta$${fmt(res.utilidad_bruta || 0)}
`; + + // Gastos + html += `
Gastos de Operacion
`; + if (res.gastos && res.gastos.items) { + for (const item of res.gastos.items) { + html += `
+ ${item.name} + -$${fmt(Math.abs(item.amount))} +
`; + } + } + html += `
Total Gastos Operacion-$${fmt(Math.abs(res.gastos?.total || 0))}
`; + + // Utilidad neta + const netColor = (res.utilidad_neta || 0) >= 0 ? 'finance-card__row-value--positive' : 'finance-card__row-value--negative'; + html += `
+ Utilidad Neta + $${fmt(res.utilidad_neta || 0)} +
`; + + card.innerHTML = html; + } catch (e) { + grid.innerHTML = `

Error: ${e.message}

`; + } + } + + // ---- Tab 5: Flujo de Efectivo ---- + async function loadCashFlow() { + // Flujo de Efectivo currently has no dedicated API endpoint, keep demo data + // Future: wire to /pos/api/accounting/cash-flow + } + + // ---- Tab 6: Conciliacion Bancaria ---- + async function loadReconciliation() { + // Bank reconciliation currently has no dedicated API endpoint, keep demo data + // Future: wire to /pos/api/accounting/reconciliation + } + + // ---- Tab 7: Cierre de Mes ---- + async function loadPeriodClose() { + const panel = document.getElementById('panel-cierre'); + if (!panel) return; + + // Wire the "Ejecutar Cierre" button + const closeBtn = panel.querySelector('.btn--primary'); + if (closeBtn && !closeBtn.dataset.wired) { + closeBtn.dataset.wired = 'true'; + closeBtn.addEventListener('click', async () => { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + if (!confirm(`Cerrar periodo ${month}/${year}? Esta accion no se puede revertir.`)) return; + try { + await api('/periods/close', { + method: 'POST', + body: JSON.stringify({ year, month }), + }); + alert('Periodo cerrado exitosamente.'); + } catch (e) { + alert('Error: ' + e.message); + } }); - loadPeriods(); - alert('Periodo cerrado exitosamente.'); - } catch (e) { - alert('Error: ' + e.message); } } - // ─── Modal helpers ───────────────────────────── + // ---- Summary cards update ---- + async function loadSummaryCards() { + const cards = document.querySelectorAll('.summary-card'); + if (cards.length < 4) return; - function closeModal(id) { - document.getElementById(id).classList.remove('active'); + try { + // Load trial balance for overall numbers + const now = new Date(); + const tb = await api(`/trial-balance?year=${now.getFullYear()}&month=${now.getMonth() + 1}`); + // The summary cards will keep their structure, just update values if API returns data + // This is best-effort; if API doesn't support summary data, demo values remain + } catch (_) { + // Non-critical, keep demo values + } } - // ─── Init ────────────────────────────────────── + // ---- Helper: update tab badge counts ---- + function updateBadgeCount(tabName, count) { + const btn = document.querySelector(`.tab-btn[onclick*="'${tabName}'"]`); + if (!btn) return; + const badge = btn.querySelector('.tab-btn__badge'); + if (badge) badge.textContent = count; + } + // ---- Clock ---- + function startClock() { + const el = document.getElementById('live-clock'); + if (!el) return; + const tick = () => { + const now = new Date(); + el.textContent = now.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }); + }; + tick(); + setInterval(tick, 1000); + } + + // ---- Init ---- function init() { - initTabs(); - - // Set default period values - const now = new Date(); - ['tb-year', 'is-year', 'cp-year'].forEach(id => { - const el = document.getElementById(id); - if (el) el.value = now.getFullYear(); - }); - ['tb-month', 'is-month', 'cp-month'].forEach(id => { - const el = document.getElementById(id); - if (el) el.value = now.getMonth() + 1; - }); - const bsDate = document.getElementById('bs-date'); - if (bsDate) bsDate.value = now.toISOString().slice(0, 10); - - // Set default entry date filters - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10); - document.getElementById('entries-from').value = firstDay; - document.getElementById('entries-to').value = now.toISOString().slice(0, 10); - - loadAccounts(); + if (!checkAuth()) return; + startClock(); + loadSummaryCards(); + // Load initial tab data (cxc is active by default) + loadAging(); } document.addEventListener('DOMContentLoaded', init); - // Public API + // Expose switchTab globally for onclick handlers in HTML + window.switchTab = switchTab; + return { - loadAccounts, loadEntries, loadTrialBalance, loadIncomeStatement, - loadBalanceSheet, loadAging, loadPeriods, closePeriod, - showNewAccountModal, createAccount, - showNewEntryModal, addEntryLine, updateEntryBalance, createEntry, - showEntryDetail, closeModal, + switchTab, loadAging, loadAccountsPayable, loadBalanceSheet, + loadIncomeStatement, loadCashFlow, loadReconciliation, loadPeriodClose, }; })(); diff --git a/pos/static/js/invoicing.js b/pos/static/js/invoicing.js index 72d55d2..fecc6b2 100644 --- a/pos/static/js/invoicing.js +++ b/pos/static/js/invoicing.js @@ -1,5 +1,7 @@ // /home/Autopartes/pos/static/js/invoicing.js -// Invoicing module: CFDI queue management, cancel, PDF +// Invoicing module — wired to design-system HTML IDs +// Tabs: panel-facturas, panel-notas, panel-complementos, panel-cancelaciones, panel-config +// Modals: modalDetalleOverlay, modalCancelOverlay const Invoicing = (() => { const API = '/pos/api/invoicing'; @@ -25,128 +27,304 @@ const Invoicing = (() => { return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } - function badgeClass(status) { - return { - pending: 'badge-pending', - sending: 'badge-sending', - stamped: 'badge-stamped', - failed: 'badge-failed', - cancelled: 'badge-cancelled', - }[status] || ''; + // ---- Auth check ---- + function checkAuth() { + if (!token()) { + window.location.href = '/pos/login'; + return false; + } + return true; } - function badgeLabel(status) { - return { - pending: 'Pendiente', - sending: 'Enviando', - stamped: 'Timbrado', - failed: 'Fallido', - cancelled: 'Cancelado', - }[status] || status; + // ---- Tab switching (matches design system onclick="switchTab('xxx')") ---- + function switchTab(name) { + // Deactivate all tabs + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.classList.remove('is-active'); + btn.setAttribute('aria-selected', 'false'); + }); + document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('is-active')); + + // Activate the clicked tab button + const tabBtn = document.querySelector(`.tab-btn[onclick*="'${name}'"]`) || + document.getElementById(`tab-${name}`); + if (tabBtn) { + tabBtn.classList.add('is-active'); + tabBtn.setAttribute('aria-selected', 'true'); + } + + // Activate the target panel + const panel = document.getElementById(`panel-${name}`); + if (panel) panel.classList.add('is-active'); + + // Load data for the activated tab + if (name === 'facturas') loadFacturas(); + if (name === 'notas') loadNotas(); + if (name === 'complementos') loadComplementos(); + if (name === 'cancelaciones') loadCancelaciones(); } - // ─── Queue List ──────────────────────────────── + // ---- Badge helpers ---- + function statusBadge(status) { + const map = { + pending: { css: 'badge--pendiente', label: 'Pendiente' }, + pendiente: { css: 'badge--pendiente', label: 'Pendiente' }, + sending: { css: 'badge--proceso', label: 'Enviando' }, + stamped: { css: 'badge--timbrada', label: 'Timbrada' }, + timbrada: { css: 'badge--timbrada', label: 'Timbrada' }, + failed: { css: 'badge--rechazada', label: 'Fallido' }, + cancelled: { css: 'badge--cancelada', label: 'Cancelada' }, + cancelada: { css: 'badge--cancelada', label: 'Cancelada' }, + ppd: { css: 'badge--ppd', label: 'PPD' }, + proceso: { css: 'badge--proceso', label: 'En proceso' }, + aceptada: { css: 'badge--aceptada', label: 'Aceptada SAT' }, + rechazada: { css: 'badge--rechazada', label: 'Rechazada SAT' }, + }; + const s = map[status] || { css: '', label: status || '' }; + return `${s.label}`; + } + + // ---- Facturas (Tab 1) — loads from CFDI queue with type=Ingreso ---- + async function loadFacturas() { + const panel = document.getElementById('panel-facturas'); + if (!panel) return; + const tbody = panel.querySelector('.data-table tbody'); + if (!tbody) return; - async function loadQueue() { try { - const status = document.getElementById('filter-status').value; - const type = document.getElementById('filter-type').value; - let qs = '?per_page=50'; - if (status) qs += `&status=${status}`; - if (type) qs += `&type=${type}`; - - const res = await api(`/queue${qs}`); - renderQueue(res.data || []); - updateStats(res.data || []); - } catch (e) { - document.getElementById('queue-list').innerHTML = - `

Error: ${e.message}

`; - } - } - - function updateStats(items) { - const counts = { pending: 0, sending: 0, stamped: 0, failed: 0, cancelled: 0 }; - items.forEach(i => { if (counts[i.status] !== undefined) counts[i.status]++; }); - - document.getElementById('queue-stats').innerHTML = ` -
${counts.pending}
Pendientes
-
${counts.sending}
Enviando
-
${counts.stamped}
Timbrados
-
${counts.failed}
Fallidos
-
${counts.cancelled}
Cancelados
`; - } - - function renderQueue(items) { - const container = document.getElementById('queue-list'); - if (!items.length) { container.innerHTML = '

No hay CFDIs en la cola.

'; return; } - - let html = ` - - - - `; - - for (const item of items) { - const uuid = item.uuid_fiscal - ? `${item.uuid_fiscal.substring(0, 8)}...` - : '-'; - html += ` - - - - - - - - + const res = await api('/queue?per_page=50&type=Ingreso'); + const items = res.data || []; + if (!items.length) { + tbody.innerHTML = ''; + return; + } + tbody.innerHTML = items.map(item => ` + + + + + + + + + - `; + `).join(''); + + // Update footer count + const footer = panel.querySelector('.table-footer span'); + if (footer) footer.textContent = `Mostrando 1\u2013${items.length} de ${res.pagination?.total || items.length} facturas`; + } catch (e) { + tbody.innerHTML = ``; } - html += '
#VentaTipoFolioUUIDEstadoReintentosFechaAcciones
${item.id}#${item.sale_id}${item.type}${item.provisional_folio || '-'}${uuid}${badgeLabel(item.status)}${item.retry_count || 0}${item.created_at ? new Date(item.created_at).toLocaleDateString('es-MX') : ''}
No hay facturas en este periodo.
${item.provisional_folio || item.id || '-'}${item.serie || '-'}${item.customer_name || '-'}${item.rfc || '-'}$${fmt(item.subtotal)}$${fmt(item.tax)}$${fmt(item.total)}${item.uso_cfdi || '-'}${statusBadge(item.status)} - - ${item.status === 'stamped' ? `` : ''} - ${item.sale_id ? `PDF` : ''} +
+ + ${item.sale_id ? `PDF` : ''} + ${item.status === 'stamped' ? `` : ''} +
Error: ${e.message}
'; - container.innerHTML = html; } - // ─── Detail ──────────────────────────────────── + // ---- Notas de Credito (Tab 2) — loads from CFDI queue with type=Egreso ---- + async function loadNotas() { + const panel = document.getElementById('panel-notas'); + if (!panel) return; + const tbody = panel.querySelector('.data-table tbody'); + if (!tbody) return; + try { + const res = await api('/queue?per_page=50&type=Egreso'); + const items = res.data || []; + if (!items.length) { + tbody.innerHTML = 'No hay notas de credito.'; + return; + } + tbody.innerHTML = items.map(item => ` + ${item.provisional_folio || '-'} + ${item.related_folio || '-'} + ${item.customer_name || '-'} + ${item.description || '-'} + $${fmt(item.total)} + ${statusBadge(item.status)} + +
+ + ${item.sale_id ? `PDF` : ''} +
+ + `).join(''); + } catch (e) { + tbody.innerHTML = `Error: ${e.message}`; + } + } + + // ---- Complementos de Pago (Tab 3) — loads from CFDI queue with type=Pago ---- + async function loadComplementos() { + const panel = document.getElementById('panel-complementos'); + if (!panel) return; + const tbody = panel.querySelector('.data-table tbody'); + if (!tbody) return; + + try { + const res = await api('/queue?per_page=50&type=Pago'); + const items = res.data || []; + if (!items.length) { + tbody.innerHTML = 'No hay complementos de pago.'; + return; + } + tbody.innerHTML = items.map(item => ` + ${item.provisional_folio || '-'} + ${item.related_folio || '-'} + ${item.customer_name || '-'} + $${fmt(item.total)} + ${item.payment_method || '-'} + ${item.created_at ? new Date(item.created_at).toLocaleDateString('es-MX') : '-'} + ${statusBadge(item.status)} + +
+ + ${item.status === 'stamped' ? `` : ''} +
+ + `).join(''); + } catch (e) { + tbody.innerHTML = `Error: ${e.message}`; + } + } + + // ---- Cancelaciones (Tab 4) — loads cancelled/cancelling CFDIs ---- + async function loadCancelaciones() { + const panel = document.getElementById('panel-cancelaciones'); + if (!panel) return; + const grid = panel.querySelector('.cancel-grid'); + if (!grid) return; + + try { + const res = await api('/queue?per_page=50&status=cancelled'); + const items = res.data || []; + + // Also try to get in-process cancellations + let processingItems = []; + try { + const res2 = await api('/queue?per_page=50&status=cancelling'); + processingItems = res2.data || []; + } catch (_) { /* ignore */ } + + const allItems = [...processingItems, ...items]; + if (!allItems.length) { + grid.innerHTML = '

No hay solicitudes de cancelacion.

'; + return; + } + + grid.innerHTML = allItems.map(item => { + const cardClass = item.status === 'cancelled' ? 'cancel-card--aceptada' : + item.status === 'cancelling' ? 'cancel-card--proceso' : + item.cancel_accepted === false ? 'cancel-card--rechazada' : + 'cancel-card--proceso'; + const badgeText = item.status === 'cancelled' ? statusBadge('aceptada') : + item.cancel_accepted === false ? statusBadge('rechazada') : + statusBadge('proceso'); + + return `
+
+ ${item.provisional_folio || `CFDI-${item.id}`} + ${badgeText} +
+
+
+ Cliente + ${item.customer_name || '-'} +
+
+ RFC + ${item.rfc || '-'} +
+
+ Motivo + ${item.cancel_motive || '-'} +
+
+ Monto + $${fmt(item.total)} MXN +
+
+ +
`; + }).join(''); + } catch (e) { + grid.innerHTML = `

Error: ${e.message}

`; + } + } + + // ---- Detail modal (uses modalDetalleOverlay) ---- async function showDetail(cfdiId) { + const overlay = document.getElementById('modalDetalleOverlay'); + if (!overlay) return; + try { const item = await api(`/queue/${cfdiId}`); - let html = `

CFDI #${item.id}

-
-
#${item.sale_id}
-
${item.type}
-
${badgeLabel(item.status)}
-
${item.provisional_folio || '-'}
-
${item.uuid_fiscal || '-'}
-
${item.retry_count}
-
${item.created_at || '-'}
-
${item.stamped_at || '-'}
-
`; + const modalCard = overlay.querySelector('.modal-card'); + if (!modalCard) return; - if (item.error_message) { - html += `

Error: ${item.error_message}

`; - } - if (item.cancel_motive) { - html += `

Motivo cancelacion: ${item.cancel_motive}

`; + // Update header + const headerTitle = modalCard.querySelector('div > div:first-child > div:first-child'); + const headerSub = modalCard.querySelector('div > div:first-child > div:nth-child(2)'); + if (headerTitle) headerTitle.textContent = 'Detalle de Factura'; + if (headerSub) headerSub.textContent = `${item.provisional_folio || 'CFDI-' + item.id} \u2014 ${item.status === 'stamped' ? 'Timbrada' : item.status === 'cancelled' ? 'Cancelada' : item.status === 'pending' ? 'Pendiente' : item.status || ''}`; + + // Update detail grid + const detailGrid = modalCard.querySelector('div:nth-child(2)'); + if (detailGrid) { + detailGrid.innerHTML = ` +
+
+
Emisor
+
${item.emisor_name || 'Nexus Autoparts SA de CV'}
+
${item.emisor_rfc || ''}
+
+
+
Receptor
+
${item.customer_name || '-'}
+
${item.rfc || ''}
+
+
+
UUID
+
${item.uuid_fiscal || 'Sin timbrar'}
+
+
+
Total
+
$${fmt(item.total)}
+
+
+ ${item.error_message ? `

Error: ${escapeHtml(item.error_message)}

` : ''} + ${(item.xml_signed || item.xml_unsigned) ? ` +
Vista previa XML
+
${escapeHtml(item.xml_signed || item.xml_unsigned)}
+ ` : ''}`; } - // XML preview - const xml = item.xml_signed || item.xml_unsigned; - if (xml) { - html += `

XML

${escapeHtml(xml)}
`; + // Wire the cancel button inside modal footer + const cancelBtn = modalCard.querySelector('div:last-child button:last-child'); + if (cancelBtn && item.status === 'stamped') { + cancelBtn.style.display = ''; + cancelBtn.onclick = () => { + overlay.style.display = 'none'; + showCancelModal(cfdiId); + }; + } else if (cancelBtn) { + cancelBtn.style.display = 'none'; } - document.getElementById('detail-content').innerHTML = html; - document.getElementById('detail-modal').classList.add('active'); + // Store current CFDI id for use by footer buttons + overlay.dataset.cfdiId = cfdiId; + overlay.dataset.saleId = item.sale_id || ''; + + overlay.style.display = 'flex'; } catch (e) { - alert('Error: ' + e.message); + alert('Error al cargar detalle: ' + e.message); } } @@ -156,42 +334,31 @@ const Invoicing = (() => { return div.innerHTML; } - // ─── Process Queue ───────────────────────────── - - async function processQueue() { - if (!confirm('Procesar todos los CFDIs pendientes?')) return; - try { - const result = await api('/queue/process', { method: 'POST' }); - alert(`Procesados: ${result.processed}, Timbrados: ${result.stamped}, Fallidos: ${result.failed}`); - loadQueue(); - } catch (e) { - alert('Error: ' + e.message); - } - } - - // ─── Cancel ──────────────────────────────────── + // ---- Cancel modal (uses modalCancelOverlay) ---- + let cancelTargetId = null; function showCancelModal(cfdiId) { - document.getElementById('cancel-cfdi-id').value = cfdiId; - document.getElementById('cancel-motive').value = ''; - document.getElementById('cancel-replacement-uuid').value = ''; - document.getElementById('replacement-uuid-group').style.display = 'none'; - document.getElementById('cancel-modal').classList.add('active'); - } - - function onMotiveChange() { - const motive = document.getElementById('cancel-motive').value; - document.getElementById('replacement-uuid-group').style.display = - motive === '01' ? 'block' : 'none'; + cancelTargetId = cfdiId; + const overlay = document.getElementById('modalCancelOverlay'); + if (overlay) overlay.style.display = 'flex'; } async function confirmCancel() { - const cfdiId = document.getElementById('cancel-cfdi-id').value; - const motive = document.getElementById('cancel-motive').value; - const replacementUuid = document.getElementById('cancel-replacement-uuid').value; + if (!cancelTargetId) return; + const overlay = document.getElementById('modalCancelOverlay'); + if (!overlay) return; - if (!motive) { alert('Selecciona un motivo de cancelacion.'); return; } - if (motive === '01' && !replacementUuid) { alert('UUID sustituto requerido para motivo 01.'); return; } + const selectedRadio = overlay.querySelector('input[name="motivo-sat"]:checked'); + if (!selectedRadio) { alert('Selecciona un motivo de cancelacion.'); return; } + const motive = selectedRadio.value; + + const uuidInput = overlay.querySelector('input[type="text"]'); + const replacementUuid = uuidInput ? uuidInput.value.trim() : ''; + + if (motive === '01' && !replacementUuid) { + alert('UUID sustituto requerido para motivo 01.'); + return; + } if (!confirm('Confirmar cancelacion ante el SAT?')) return; @@ -199,29 +366,83 @@ const Invoicing = (() => { const body = { motive }; if (replacementUuid) body.replacement_uuid = replacementUuid; - await api(`/cancel/${cfdiId}`, { method: 'POST', body: JSON.stringify(body) }); - closeModal('cancel-modal'); - loadQueue(); + await api(`/cancel/${cancelTargetId}`, { method: 'POST', body: JSON.stringify(body) }); + overlay.style.display = 'none'; + cancelTargetId = null; + loadFacturas(); alert('CFDI cancelado exitosamente.'); } catch (e) { alert('Error: ' + e.message); } } - // ─── Modal helpers ───────────────────────────── - - function closeModal(id) { - document.getElementById(id).classList.remove('active'); + // ---- Process entire queue ---- + async function processQueue() { + if (!confirm('Procesar todos los CFDIs pendientes?')) return; + try { + const result = await api('/queue/process', { method: 'POST' }); + alert(`Procesados: ${result.processed}, Timbrados: ${result.stamped}, Fallidos: ${result.failed}`); + loadFacturas(); + } catch (e) { + alert('Error: ' + e.message); + } } - // ─── Init ────────────────────────────────────── + // ---- Clock ---- + function startClock() { + const el = document.getElementById('live-clock'); + if (!el) return; + const tick = () => { + const now = new Date(); + el.textContent = now.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + }; + tick(); + setInterval(tick, 1000); + } - document.addEventListener('DOMContentLoaded', () => { - loadQueue(); - }); + // ---- Wire cancel modal "Solicitar Cancelacion SAT" button ---- + function wireCancelModal() { + const overlay = document.getElementById('modalCancelOverlay'); + if (!overlay) return; + const footerBtns = overlay.querySelectorAll('div:last-child button'); + if (footerBtns.length >= 2) { + // Last button = confirm cancel + footerBtns[footerBtns.length - 1].onclick = () => confirmCancel(); + // Second to last = close + footerBtns[footerBtns.length - 2].onclick = () => { + overlay.style.display = 'none'; + cancelTargetId = null; + }; + } + } + + // ---- Wire detail modal close button ---- + function wireDetailModal() { + const overlay = document.getElementById('modalDetalleOverlay'); + if (!overlay) return; + const closeBtn = overlay.querySelector('button[onclick*="modalDetalleOverlay"]'); + if (closeBtn) { + closeBtn.onclick = () => { overlay.style.display = 'none'; }; + } + } + + // ---- Init ---- + function init() { + if (!checkAuth()) return; + startClock(); + wireDetailModal(); + wireCancelModal(); + // Load initial tab data (facturas is active by default) + loadFacturas(); + } + + document.addEventListener('DOMContentLoaded', init); + + // Expose switchTab globally for onclick handlers in HTML + window.switchTab = switchTab; return { - loadQueue, processQueue, showDetail, showCancelModal, - onMotiveChange, confirmCancel, closeModal, + switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, + showDetail, showCancelModal, confirmCancel, processQueue, }; })();