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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAccountsTree() {
|
||||
const container = document.getElementById('accounts-tree');
|
||||
if (!accounts.length) { container.innerHTML = '<p>No hay cuentas.</p>'; 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 = '<ul class="tree">';
|
||||
for (const acct of children) {
|
||||
const hasChildren = byParent[acct.id] && byParent[acct.id].length > 0;
|
||||
const balClass = acct.balance < 0 ? 'negative' : '';
|
||||
html += '<li>';
|
||||
if (hasChildren) {
|
||||
html += `<span class="tree-toggle open" onclick="this.classList.toggle('open');this.nextElementSibling.style.display=this.classList.contains('open')?'block':'none'">`;
|
||||
} else {
|
||||
html += '<span style="display:inline-block;width:1rem;">';
|
||||
}
|
||||
html += `<span class="tree-code">${acct.code}</span> ${acct.name}`;
|
||||
html += `<span class="tree-balance ${balClass}">$${fmt(acct.balance)}</span>`;
|
||||
html += '</span>';
|
||||
if (hasChildren) {
|
||||
html += `<div>${buildUl(acct.id)}</div>`;
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
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 = '<option value="">-- Sin padre --</option>';
|
||||
accounts.forEach(a => {
|
||||
sel.innerHTML += `<option value="${a.id}">${a.code} - ${a.name}</option>`;
|
||||
});
|
||||
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 `<span class="badge ${map[status] || ''}">${label || status}</span>`;
|
||||
}
|
||||
|
||||
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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEntries(entries) {
|
||||
const container = document.getElementById('entries-list');
|
||||
if (!entries.length) { container.innerHTML = '<p>No hay polizas en este periodo.</p>'; return; }
|
||||
|
||||
let html = `<table class="report-table">
|
||||
<thead><tr>
|
||||
<th>#</th><th>Fecha</th><th>Tipo</th><th>Descripcion</th>
|
||||
<th>Referencia</th><th class="amount">Monto</th><th>Auto</th>
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
for (const e of entries) {
|
||||
html += `<tr style="cursor:pointer;" onclick="Accounting.showEntryDetail(${e.id})">
|
||||
<td>${e.entry_number}</td>
|
||||
<td>${e.date || ''}</td>
|
||||
<td>${e.type || ''}</td>
|
||||
<td>${e.description || ''}</td>
|
||||
<td>${e.reference_type ? `${e.reference_type} #${e.reference_id}` : ''}</td>
|
||||
<td class="amount">$${fmt(e.total_amount)}</td>
|
||||
<td>${e.is_auto ? 'Si' : 'No'}</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function showEntryDetail(entryId) {
|
||||
try {
|
||||
const entry = await api(`/entries/${entryId}`);
|
||||
let html = `<h3>Poliza #${entry.entry_number}</h3>
|
||||
<div class="entry-header">
|
||||
<span><strong>Fecha:</strong> ${entry.date}</span>
|
||||
<span><strong>Tipo:</strong> ${entry.type}</span>
|
||||
<span><strong>Estado:</strong> ${entry.status}</span>
|
||||
</div>
|
||||
<p>${entry.description || ''}</p>
|
||||
<table class="report-table">
|
||||
<thead><tr>
|
||||
<th>Cuenta</th><th>Nombre</th><th class="amount">Cargo</th>
|
||||
<th class="amount">Abono</th><th>Nota</th>
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
for (const l of entry.lines) {
|
||||
html += `<tr>
|
||||
<td class="tree-code">${l.account_code}</td>
|
||||
<td>${l.account_name}</td>
|
||||
<td class="amount">${l.debit ? '$' + fmt(l.debit) : ''}</td>
|
||||
<td class="amount">${l.credit ? '$' + fmt(l.credit) : ''}</td>
|
||||
<td>${l.description || ''}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
html += `<tr class="total-row">
|
||||
<td colspan="2">Totales</td>
|
||||
<td class="amount">$${fmt(entry.total_debit)}</td>
|
||||
<td class="amount">$${fmt(entry.total_credit)}</td>
|
||||
<td></td>
|
||||
</tr></tbody></table>`;
|
||||
|
||||
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 = '<option value="">Seleccionar</option>';
|
||||
accounts.forEach(a => {
|
||||
if (a.parent_id) { // Only leaf accounts
|
||||
acctOptions += `<option value="${a.id}">${a.code} - ${a.name}</option>`;
|
||||
}
|
||||
});
|
||||
|
||||
tr.innerHTML = `
|
||||
<td><select onchange="Accounting.updateEntryBalance()">${acctOptions}</select></td>
|
||||
<td><input type="number" step="0.01" min="0" value="0" onchange="Accounting.updateEntryBalance()"></td>
|
||||
<td><input type="number" step="0.01" min="0" value="0" onchange="Accounting.updateEntryBalance()"></td>
|
||||
<td><button onclick="this.closest('tr').remove();Accounting.updateEntryBalance();"
|
||||
style="border:none;background:none;color:var(--danger);cursor:pointer;font-size:1.1rem;">x</button></td>`;
|
||||
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 = `<span style="color:var(--success);">Cuadrada: Cargos $${fmt(totalDebit)} = Abonos $${fmt(totalCredit)}</span>`;
|
||||
} else {
|
||||
el.innerHTML = `<span style="color:var(--danger);">Descuadre: $${fmt(Math.abs(diff))} (Cargos $${fmt(totalDebit)} / Abonos $${fmt(totalCredit)})</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTrialBalance(data) {
|
||||
const rows = data.data || [];
|
||||
let html = `<table class="report-table">
|
||||
<thead><tr>
|
||||
<th>Codigo</th><th>Cuenta</th>
|
||||
<th class="amount">Saldo Inicial</th><th class="amount">Cargos</th>
|
||||
<th class="amount">Abonos</th><th class="amount">Saldo Final</th>
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
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 += `<tr>
|
||||
<td class="tree-code">${r.code}</td><td>${r.name}</td>
|
||||
<td class="amount">$${fmt(r.saldo_inicial)}</td>
|
||||
<td class="amount">$${fmt(r.cargos)}</td>
|
||||
<td class="amount">$${fmt(r.abonos)}</td>
|
||||
<td class="amount">$${fmt(r.saldo_final)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
html += `<tr class="total-row">
|
||||
<td colspan="2">Totales</td>
|
||||
<td class="amount">$${fmt(totals.si)}</td>
|
||||
<td class="amount">$${fmt(totals.c)}</td>
|
||||
<td class="amount">$${fmt(totals.a)}</td>
|
||||
<td class="amount">$${fmt(totals.sf)}</td>
|
||||
</tr></tbody></table>`;
|
||||
|
||||
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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderIncomeStatement(data) {
|
||||
let html = '<table class="report-table"><tbody>';
|
||||
|
||||
// Ingresos
|
||||
html += '<tr class="subtotal-row"><td colspan="2"><strong>INGRESOS</strong></td></tr>';
|
||||
for (const item of data.ingresos.items) {
|
||||
html += `<tr><td style="padding-left:2rem;">${item.code} - ${item.name}</td><td class="amount">$${fmt(item.amount)}</td></tr>`;
|
||||
}
|
||||
html += `<tr class="subtotal-row"><td>Total Ingresos</td><td class="amount">$${fmt(data.ingresos.total)}</td></tr>`;
|
||||
|
||||
// Costos
|
||||
html += '<tr class="subtotal-row"><td colspan="2"><strong>COSTOS</strong></td></tr>';
|
||||
for (const item of data.costos.items) {
|
||||
html += `<tr><td style="padding-left:2rem;">${item.code} - ${item.name}</td><td class="amount">$${fmt(item.amount)}</td></tr>`;
|
||||
}
|
||||
html += `<tr class="subtotal-row"><td>Total Costos</td><td class="amount">$${fmt(data.costos.total)}</td></tr>`;
|
||||
|
||||
// Utilidad bruta
|
||||
html += `<tr class="total-row"><td><strong>UTILIDAD BRUTA</strong></td><td class="amount"><strong>$${fmt(data.utilidad_bruta)}</strong></td></tr>`;
|
||||
|
||||
// Gastos
|
||||
html += '<tr class="subtotal-row"><td colspan="2"><strong>GASTOS</strong></td></tr>';
|
||||
for (const item of data.gastos.items) {
|
||||
html += `<tr><td style="padding-left:2rem;">${item.code} - ${item.name}</td><td class="amount">$${fmt(item.amount)}</td></tr>`;
|
||||
}
|
||||
html += `<tr class="subtotal-row"><td>Total Gastos</td><td class="amount">$${fmt(data.gastos.total)}</td></tr>`;
|
||||
|
||||
// Utilidad neta
|
||||
const netColor = data.utilidad_neta >= 0 ? 'var(--success)' : 'var(--danger)';
|
||||
html += `<tr class="total-row"><td><strong>UTILIDAD NETA</strong></td>
|
||||
<td class="amount" style="color:${netColor};"><strong>$${fmt(data.utilidad_neta)}</strong></td></tr>`;
|
||||
|
||||
html += '</tbody></table>';
|
||||
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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderBalanceSheet(data) {
|
||||
const balancedBadge = data.balanced
|
||||
? '<span class="badge badge-open">Cuadrado</span>'
|
||||
: '<span class="badge badge-closed">Descuadrado</span>';
|
||||
|
||||
let html = `<p>Al ${data.as_of} ${balancedBadge}</p>
|
||||
<table class="report-table"><tbody>`;
|
||||
|
||||
// Activo
|
||||
html += '<tr class="subtotal-row"><td colspan="2"><strong>ACTIVO</strong></td></tr>';
|
||||
for (const item of data.activo.items) {
|
||||
html += `<tr><td style="padding-left:2rem;">${item.code} - ${item.name}</td><td class="amount">$${fmt(item.balance)}</td></tr>`;
|
||||
}
|
||||
html += `<tr class="total-row"><td>Total Activo</td><td class="amount">$${fmt(data.activo.total)}</td></tr>`;
|
||||
|
||||
// Pasivo
|
||||
html += '<tr class="subtotal-row"><td colspan="2"><strong>PASIVO</strong></td></tr>';
|
||||
for (const item of data.pasivo.items) {
|
||||
html += `<tr><td style="padding-left:2rem;">${item.code} - ${item.name}</td><td class="amount">$${fmt(item.balance)}</td></tr>`;
|
||||
}
|
||||
html += `<tr class="total-row"><td>Total Pasivo</td><td class="amount">$${fmt(data.pasivo.total)}</td></tr>`;
|
||||
|
||||
// Capital
|
||||
html += '<tr class="subtotal-row"><td colspan="2"><strong>CAPITAL</strong></td></tr>';
|
||||
for (const item of data.capital.items) {
|
||||
html += `<tr><td style="padding-left:2rem;">${item.code} - ${item.name}</td><td class="amount">$${fmt(item.balance)}</td></tr>`;
|
||||
}
|
||||
html += `<tr class="total-row"><td>Total Capital</td><td class="amount">$${fmt(data.capital.total)}</td></tr>`;
|
||||
|
||||
html += `<tr class="total-row"><td><strong>Pasivo + Capital</strong></td>
|
||||
<td class="amount"><strong>$${fmt(data.pasivo.total + data.capital.total)}</strong></td></tr>`;
|
||||
|
||||
html += '</tbody></table>';
|
||||
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 = '<tr><td colspan="9" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay cuentas por cobrar.</td></tr>';
|
||||
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 `<tr>
|
||||
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
|
||||
<td class="td--primary">${r.name || r.customer_name || '-'}</td>
|
||||
<td>${r.issue_date ? new Date(r.issue_date).toLocaleDateString('es-MX') : '-'}</td>
|
||||
<td>${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'}</td>
|
||||
<td class="td--amount">$${fmt(r.total)}</td>
|
||||
<td class="td--amount">$${fmt(r.paid || 0)}</td>
|
||||
<td class="td--amount">$${fmt(r.balance || r.total)}</td>
|
||||
<td>${statusBadge(status, label)}</td>
|
||||
<td><button class="btn btn--ghost btn--sm">${r.balance > 0 ? 'Cobrar' : 'Ver'}</button></td>
|
||||
</tr>`;
|
||||
}).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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
tbody.innerHTML = `<tr><td colspan="9" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAging(data) {
|
||||
const rows = data.data || [];
|
||||
let html = `<table class="report-table">
|
||||
<thead><tr>
|
||||
<th>Cliente</th><th class="amount">Corriente</th>
|
||||
<th class="amount">1-30d</th><th class="amount">31-60d</th>
|
||||
<th class="amount">61-90d</th><th class="amount">90+d</th>
|
||||
<th class="amount">Total</th>
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
for (const r of rows) {
|
||||
html += `<tr>
|
||||
<td>${r.name}</td>
|
||||
<td class="amount">$${fmt(r.corriente)}</td>
|
||||
<td class="amount">$${fmt(r.d1_30)}</td>
|
||||
<td class="amount">$${fmt(r.d31_60)}</td>
|
||||
<td class="amount">$${fmt(r.d61_90)}</td>
|
||||
<td class="amount">$${fmt(r.d90_plus)}</td>
|
||||
<td class="amount"><strong>$${fmt(r.total)}</strong></td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const t = data.totals || {};
|
||||
html += `<tr class="total-row">
|
||||
<td>Totales</td>
|
||||
<td class="amount">$${fmt(t.corriente)}</td>
|
||||
<td class="amount">$${fmt(t.d1_30)}</td>
|
||||
<td class="amount">$${fmt(t.d31_60)}</td>
|
||||
<td class="amount">$${fmt(t.d61_90)}</td>
|
||||
<td class="amount">$${fmt(t.d90_plus)}</td>
|
||||
<td class="amount"><strong>$${fmt(t.total)}</strong></td>
|
||||
</tr></tbody></table>`;
|
||||
|
||||
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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPeriods(periods) {
|
||||
const months = ['', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
||||
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
|
||||
let html = `<table class="report-table">
|
||||
<thead><tr><th>Periodo</th><th>Estado</th><th>Cerrado por</th><th>Fecha cierre</th></tr></thead><tbody>`;
|
||||
|
||||
for (const p of periods) {
|
||||
const badge = p.status === 'closed'
|
||||
? '<span class="badge badge-closed">Cerrado</span>'
|
||||
: '<span class="badge badge-open">Abierto</span>';
|
||||
html += `<tr>
|
||||
<td>${months[p.month]} ${p.year}</td>
|
||||
<td>${badge}</td>
|
||||
<td>${p.closed_by_name || '-'}</td>
|
||||
<td>${p.closed_at ? new Date(p.closed_at).toLocaleDateString('es-MX') : '-'}</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
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 = '<tr><td colspan="9" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay cuentas por pagar.</td></tr>';
|
||||
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 `<tr>
|
||||
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
|
||||
<td class="td--primary">${r.name || r.vendor_name || '-'}</td>
|
||||
<td>${r.receipt_date ? new Date(r.receipt_date).toLocaleDateString('es-MX') : '-'}</td>
|
||||
<td>${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'}</td>
|
||||
<td class="td--amount">$${fmt(r.total)}</td>
|
||||
<td class="td--amount">$${fmt(r.paid || 0)}</td>
|
||||
<td class="td--amount">$${fmt(r.balance || r.total)}</td>
|
||||
<td>${statusBadge(status, label)}</td>
|
||||
<td><button class="btn btn--ghost btn--sm">${r.balance > 0 ? 'Pagar' : 'Ver'}</button></td>
|
||||
</tr>`;
|
||||
}).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 = `<tr><td colspan="9" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 = `<div class="finance-card__header"><div class="finance-card__title">Activos</div></div>`;
|
||||
if (res.activo && res.activo.items) {
|
||||
for (const item of res.activo.items) {
|
||||
const isNeg = item.balance < 0;
|
||||
html += `<div class="finance-card__row finance-card__row--indent">
|
||||
<span class="finance-card__row-label">${item.code ? item.code + ' ' : ''}${item.name}</span>
|
||||
<span class="finance-card__row-value ${isNeg ? 'finance-card__row-value--negative' : ''}">$${fmt(item.balance)}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += `<div class="finance-card__row finance-card__row--total">
|
||||
<span class="finance-card__row-label">Total Activos</span>
|
||||
<span class="finance-card__row-value">$${fmt(res.activo?.total || 0)}</span>
|
||||
</div>`;
|
||||
activoCard.innerHTML = html;
|
||||
}
|
||||
|
||||
if (pasivoCard) {
|
||||
let html = `<div class="finance-card__header"><div class="finance-card__title">Pasivo + Capital</div></div>`;
|
||||
// Pasivo
|
||||
if (res.pasivo && res.pasivo.items) {
|
||||
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Pasivo</span><span></span></div>`;
|
||||
for (const item of res.pasivo.items) {
|
||||
html += `<div class="finance-card__row finance-card__row--indent">
|
||||
<span class="finance-card__row-label">${item.code ? item.code + ' ' : ''}${item.name}</span>
|
||||
<span class="finance-card__row-value">$${fmt(item.balance)}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Pasivo</span><span class="finance-card__row-value">$${fmt(res.pasivo.total)}</span></div>`;
|
||||
}
|
||||
// Capital
|
||||
if (res.capital && res.capital.items) {
|
||||
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Capital Contable</span><span></span></div>`;
|
||||
for (const item of res.capital.items) {
|
||||
const isPos = item.balance > 0;
|
||||
html += `<div class="finance-card__row finance-card__row--indent">
|
||||
<span class="finance-card__row-label">${item.code ? item.code + ' ' : ''}${item.name}</span>
|
||||
<span class="finance-card__row-value ${isPos ? 'finance-card__row-value--positive' : ''}">$${fmt(item.balance)}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Capital</span><span class="finance-card__row-value">$${fmt(res.capital.total)}</span></div>`;
|
||||
}
|
||||
html += `<div class="finance-card__row finance-card__row--total">
|
||||
<span class="finance-card__row-label">Total Pasivo + Capital</span>
|
||||
<span class="finance-card__row-value">$${fmt((res.pasivo?.total || 0) + (res.capital?.total || 0))}</span>
|
||||
</div>`;
|
||||
pasivoCard.innerHTML = html;
|
||||
}
|
||||
|
||||
// Update period selector text
|
||||
const sel = panel.querySelector('.select-filter');
|
||||
if (sel && res.as_of) {
|
||||
sel.innerHTML = `<option>Al ${res.as_of}</option>`;
|
||||
}
|
||||
} catch (e) {
|
||||
grid.innerHTML = `<p style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 = `<div class="finance-card__header"><div class="finance-card__title">Estado de Resultados</div></div>`;
|
||||
|
||||
// Ingresos
|
||||
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Ingresos</span><span></span></div>`;
|
||||
if (res.ingresos && res.ingresos.items) {
|
||||
for (const item of res.ingresos.items) {
|
||||
html += `<div class="finance-card__row finance-card__row--indent">
|
||||
<span class="finance-card__row-label">${item.name}</span>
|
||||
<span class="finance-card__row-value">$${fmt(item.amount)}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Ingresos</span><span class="finance-card__row-value">$${fmt(res.ingresos?.total || 0)}</span></div>`;
|
||||
|
||||
// Costos
|
||||
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Costo de Ventas</span><span></span></div>`;
|
||||
if (res.costos && res.costos.items) {
|
||||
for (const item of res.costos.items) {
|
||||
html += `<div class="finance-card__row finance-card__row--indent">
|
||||
<span class="finance-card__row-label">${item.name}</span>
|
||||
<span class="finance-card__row-value finance-card__row-value--negative">-$${fmt(Math.abs(item.amount))}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Utilidad Bruta</span><span class="finance-card__row-value finance-card__row-value--positive">$${fmt(res.utilidad_bruta || 0)}</span></div>`;
|
||||
|
||||
// Gastos
|
||||
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Gastos de Operacion</span><span></span></div>`;
|
||||
if (res.gastos && res.gastos.items) {
|
||||
for (const item of res.gastos.items) {
|
||||
html += `<div class="finance-card__row finance-card__row--indent">
|
||||
<span class="finance-card__row-label">${item.name}</span>
|
||||
<span class="finance-card__row-value finance-card__row-value--negative">-$${fmt(Math.abs(item.amount))}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Gastos Operacion</span><span class="finance-card__row-value finance-card__row-value--negative">-$${fmt(Math.abs(res.gastos?.total || 0))}</span></div>`;
|
||||
|
||||
// Utilidad neta
|
||||
const netColor = (res.utilidad_neta || 0) >= 0 ? 'finance-card__row-value--positive' : 'finance-card__row-value--negative';
|
||||
html += `<div class="finance-card__row finance-card__row--total">
|
||||
<span class="finance-card__row-label">Utilidad Neta</span>
|
||||
<span class="finance-card__row-value ${netColor}">$${fmt(res.utilidad_neta || 0)}</span>
|
||||
</div>`;
|
||||
|
||||
card.innerHTML = html;
|
||||
} catch (e) {
|
||||
grid.innerHTML = `<p style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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 `<span class="badge ${s.css}">${s.label}</span>`;
|
||||
}
|
||||
|
||||
// ---- 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 =
|
||||
`<p style="color:var(--danger);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="stat-card"><div class="number">${counts.pending}</div><div class="label">Pendientes</div></div>
|
||||
<div class="stat-card"><div class="number">${counts.sending}</div><div class="label">Enviando</div></div>
|
||||
<div class="stat-card"><div class="number">${counts.stamped}</div><div class="label">Timbrados</div></div>
|
||||
<div class="stat-card"><div class="number">${counts.failed}</div><div class="label">Fallidos</div></div>
|
||||
<div class="stat-card"><div class="number">${counts.cancelled}</div><div class="label">Cancelados</div></div>`;
|
||||
}
|
||||
|
||||
function renderQueue(items) {
|
||||
const container = document.getElementById('queue-list');
|
||||
if (!items.length) { container.innerHTML = '<p>No hay CFDIs en la cola.</p>'; return; }
|
||||
|
||||
let html = `<table class="queue-table">
|
||||
<thead><tr>
|
||||
<th>#</th><th>Venta</th><th>Tipo</th><th>Folio</th>
|
||||
<th>UUID</th><th>Estado</th><th>Reintentos</th><th>Fecha</th><th>Acciones</th>
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
for (const item of items) {
|
||||
const uuid = item.uuid_fiscal
|
||||
? `${item.uuid_fiscal.substring(0, 8)}...`
|
||||
: '-';
|
||||
html += `<tr>
|
||||
<td>${item.id}</td>
|
||||
<td>#${item.sale_id}</td>
|
||||
<td>${item.type}</td>
|
||||
<td>${item.provisional_folio || '-'}</td>
|
||||
<td title="${item.uuid_fiscal || ''}">${uuid}</td>
|
||||
<td><span class="badge ${badgeClass(item.status)}">${badgeLabel(item.status)}</span></td>
|
||||
<td>${item.retry_count || 0}</td>
|
||||
<td>${item.created_at ? new Date(item.created_at).toLocaleDateString('es-MX') : ''}</td>
|
||||
const res = await api('/queue?per_page=50&type=Ingreso');
|
||||
const items = res.data || [];
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay facturas en este periodo.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = items.map(item => `<tr>
|
||||
<td class="td--mono">${item.provisional_folio || item.id || '-'}</td>
|
||||
<td class="td--primary">${item.serie || '-'}</td>
|
||||
<td class="td--primary">${item.customer_name || '-'}</td>
|
||||
<td class="td--mono">${item.rfc || '-'}</td>
|
||||
<td class="td--amount">$${fmt(item.subtotal)}</td>
|
||||
<td class="td--amount">$${fmt(item.tax)}</td>
|
||||
<td class="td--amount">$${fmt(item.total)}</td>
|
||||
<td style="font-size:var(--text-caption);">${item.uso_cfdi || '-'}</td>
|
||||
<td>${statusBadge(item.status)}</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary" style="padding:0.2rem 0.4rem;font-size:0.8rem;"
|
||||
onclick="Invoicing.showDetail(${item.id})">Ver</button>
|
||||
${item.status === 'stamped' ? `<button class="btn btn-danger" style="padding:0.2rem 0.4rem;font-size:0.8rem;"
|
||||
onclick="Invoicing.showCancelModal(${item.id})">Cancelar</button>` : ''}
|
||||
${item.sale_id ? `<a href="/pos/api/invoicing/${item.sale_id}/pdf" target="_blank"
|
||||
class="btn btn-secondary" style="padding:0.2rem 0.4rem;font-size:0.8rem;">PDF</a>` : ''}
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">Ver</button>
|
||||
${item.sale_id ? `<a href="${API}/${item.sale_id}/pdf" target="_blank" class="btn btn--ghost btn--sm">PDF</a>` : ''}
|
||||
${item.status === 'stamped' ? `<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">XML</button>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
</tr>`).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 = `<tr><td colspan="10" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
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 = '<tr><td colspan="7" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay notas de credito.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = items.map(item => `<tr>
|
||||
<td class="td--mono">${item.provisional_folio || '-'}</td>
|
||||
<td class="td--mono" style="color:var(--color-text-accent);">${item.related_folio || '-'}</td>
|
||||
<td class="td--primary">${item.customer_name || '-'}</td>
|
||||
<td>${item.description || '-'}</td>
|
||||
<td class="td--amount">$${fmt(item.total)}</td>
|
||||
<td>${statusBadge(item.status)}</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">Ver</button>
|
||||
${item.sale_id ? `<a href="${API}/${item.sale_id}/pdf" target="_blank" class="btn btn--ghost btn--sm">PDF</a>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
} catch (e) {
|
||||
tbody.innerHTML = `<tr><td colspan="7" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 = '<tr><td colspan="8" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay complementos de pago.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = items.map(item => `<tr>
|
||||
<td class="td--mono">${item.provisional_folio || '-'}</td>
|
||||
<td class="td--mono" style="color:var(--color-text-accent);">${item.related_folio || '-'}</td>
|
||||
<td class="td--primary">${item.customer_name || '-'}</td>
|
||||
<td class="td--amount">$${fmt(item.total)}</td>
|
||||
<td style="font-size:var(--text-caption);">${item.payment_method || '-'}</td>
|
||||
<td>${item.created_at ? new Date(item.created_at).toLocaleDateString('es-MX') : '-'}</td>
|
||||
<td>${statusBadge(item.status)}</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">Ver</button>
|
||||
${item.status === 'stamped' ? `<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">XML</button>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
} catch (e) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 = '<p style="padding:var(--space-6);color:var(--color-text-muted);text-align:center;">No hay solicitudes de cancelacion.</p>';
|
||||
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 `<div class="cancel-card ${cardClass}">
|
||||
<div class="cancel-card__header">
|
||||
<span class="cancel-card__folio">${item.provisional_folio || `CFDI-${item.id}`}</span>
|
||||
${badgeText}
|
||||
</div>
|
||||
<div class="cancel-card__body">
|
||||
<div class="cancel-card__row">
|
||||
<span class="cancel-card__row-label">Cliente</span>
|
||||
<span class="cancel-card__row-value">${item.customer_name || '-'}</span>
|
||||
</div>
|
||||
<div class="cancel-card__row">
|
||||
<span class="cancel-card__row-label">RFC</span>
|
||||
<span class="cancel-card__row-value" style="font-family:var(--font-mono);font-size:0.8rem;">${item.rfc || '-'}</span>
|
||||
</div>
|
||||
<div class="cancel-card__row">
|
||||
<span class="cancel-card__row-label">Motivo</span>
|
||||
<span class="cancel-card__row-value">${item.cancel_motive || '-'}</span>
|
||||
</div>
|
||||
<div class="cancel-card__row">
|
||||
<span class="cancel-card__row-label">Monto</span>
|
||||
<span class="cancel-card__row-value" style="font-family:var(--font-mono);font-weight:600;color:var(--color-text-primary);">$${fmt(item.total)} MXN</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cancel-card__footer">
|
||||
<span style="font-size:var(--text-caption);color:var(--color-text-muted);">${item.cancelled_at ? 'Cancelada: ' + new Date(item.cancelled_at).toLocaleDateString('es-MX') : item.created_at ? 'Solicitada: ' + new Date(item.created_at).toLocaleDateString('es-MX') : ''}</span>
|
||||
<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">Ver detalle</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
grid.innerHTML = `<p style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 = `<h3>CFDI #${item.id}</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item"><label>Venta</label><span>#${item.sale_id}</span></div>
|
||||
<div class="detail-item"><label>Tipo</label><span>${item.type}</span></div>
|
||||
<div class="detail-item"><label>Estado</label><span class="badge ${badgeClass(item.status)}">${badgeLabel(item.status)}</span></div>
|
||||
<div class="detail-item"><label>Folio Provisional</label><span>${item.provisional_folio || '-'}</span></div>
|
||||
<div class="detail-item"><label>UUID Fiscal</label><span>${item.uuid_fiscal || '-'}</span></div>
|
||||
<div class="detail-item"><label>Reintentos</label><span>${item.retry_count}</span></div>
|
||||
<div class="detail-item"><label>Creado</label><span>${item.created_at || '-'}</span></div>
|
||||
<div class="detail-item"><label>Timbrado</label><span>${item.stamped_at || '-'}</span></div>
|
||||
</div>`;
|
||||
const modalCard = overlay.querySelector('.modal-card');
|
||||
if (!modalCard) return;
|
||||
|
||||
if (item.error_message) {
|
||||
html += `<p style="color:var(--danger);"><strong>Error:</strong> ${item.error_message}</p>`;
|
||||
}
|
||||
if (item.cancel_motive) {
|
||||
html += `<p><strong>Motivo cancelacion:</strong> ${item.cancel_motive}</p>`;
|
||||
// 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 = `
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-4); margin-bottom:var(--space-6);">
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Emisor</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${item.emisor_name || 'Nexus Autoparts SA de CV'}</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">${item.emisor_rfc || ''}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Receptor</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${item.customer_name || '-'}</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">${item.rfc || ''}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">UUID</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-caption); color:var(--color-text-accent); word-break:break-all;">${item.uuid_fiscal || 'Sin timbrar'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Total</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-h4); font-weight:var(--font-weight-bold); color:var(--color-text-primary);">$${fmt(item.total)}</div>
|
||||
</div>
|
||||
</div>
|
||||
${item.error_message ? `<p style="color:var(--color-error);margin-bottom:var(--space-3);"><strong>Error:</strong> ${escapeHtml(item.error_message)}</p>` : ''}
|
||||
${(item.xml_signed || item.xml_unsigned) ? `
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>
|
||||
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.xml_unsigned)}</pre>
|
||||
` : ''}`;
|
||||
}
|
||||
|
||||
// XML preview
|
||||
const xml = item.xml_signed || item.xml_unsigned;
|
||||
if (xml) {
|
||||
html += `<h4>XML</h4><div class="xml-preview">${escapeHtml(xml)}</div>`;
|
||||
// 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,
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user