fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- Arregla @require_auth, permisos, race conditions, locks de caja/stock
- Elimina N+1 en layaway, flotilla, dashboard y global_invoice
- Asegura folios atomicos para CFDI, ordenes de servicio y polizas
- Protege client_secret de MercadoLibre en backend
- Conecta botones/filtros de config, customers, accounting e invoicing
- Mejora accesibilidad (labels/aria-label) y estados de carga/vacio
- Limpia accounting.js obsoleto y consolida accounting.v9.js
- Actualiza cache busting a v32 y Service Worker a v32
- Documenta todo en docs/AUDIT_Y_MEJORAS_2026-06-15.md

Tests: 35 passed
This commit is contained in:
2026-06-29 23:54:58 +00:00
parent 59a4893e84
commit 2bdeb2973a
61 changed files with 2879 additions and 706 deletions

View File

@@ -86,46 +86,93 @@ const Invoicing = (() => {
}
// ---- Facturas (Tab 1) — loads from CFDI queue with type=Ingreso ----
let facturasCache = [];
async function loadFacturas() {
const panel = document.getElementById('panel-facturas');
if (!panel) return;
const tbody = panel.querySelector('.data-table tbody');
if (!tbody) return;
try {
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>
<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>`).join('');
const statusFilter = document.getElementById('facturas-status-filter');
const status = statusFilter ? statusFilter.value : '';
const url = status ? `/queue?per_page=50&type=ingreso&status=${status}` : '/queue?per_page=50&type=ingreso';
// 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`;
tbody.innerHTML = '<tr><td colspan="10" style="padding:var(--space-6);">' + renderLoadingState({ message: 'Cargando facturas...' }) + '</td></tr>';
try {
const res = await api(url);
facturasCache = res.data || [];
renderFacturas(facturasCache, res.pagination?.total || facturasCache.length);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="10" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
tbody.innerHTML = '<tr><td colspan="10" style="padding:var(--space-6);">' + renderEmptyState({ title: 'Error', subtitle: e.message }) + '</td></tr>';
}
}
function renderFacturas(items, total) {
const panel = document.getElementById('panel-facturas');
const tbody = panel.querySelector('.data-table tbody');
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="10" style="padding:var(--space-6);">' + renderEmptyState({ title: 'Sin facturas', subtitle: '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_total)}</td>
<td class="td--amount">$${fmt(item.total)}</td>
<td style="font-size:var(--text-caption);">${item.payment_method || '-'}</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>` : ''}
${item.status === 'stamped' ? `<button class="btn btn--ghost btn--sm" onclick="Invoicing.showDetail(${item.id})">XML</button>` : ''}
</div>
</td>
</tr>`).join('');
const footer = panel.querySelector('.table-footer span');
if (footer) footer.textContent = `Mostrando 1\u2013${items.length} de ${total} facturas`;
}
function filterFacturas() {
const q = (document.getElementById('facturas-search')?.value || '').toLowerCase();
const filtered = facturasCache.filter(item =>
(item.provisional_folio || '').toLowerCase().includes(q) ||
(item.customer_name || '').toLowerCase().includes(q) ||
(item.rfc || '').toLowerCase().includes(q)
);
renderFacturas(filtered, facturasCache.length);
}
function exportFacturasCSV() {
if (typeof window.exportVisibleTableCSV === 'function') {
window.exportVisibleTableCSV('facturas');
} else {
alert('Exportador no disponible');
}
}
function exportNotasCSV() {
if (typeof window.exportVisibleTableCSV === 'function') {
window.exportVisibleTableCSV('notas_de_credito');
} else {
alert('Exportador no disponible');
}
}
function newCreditNote() {
alert('Nueva nota de crédito — próximamente');
}
function newPaymentComplement() {
alert('Nuevo complemento de pago — próximamente');
}
// ---- Notas de Credito (Tab 2) — loads from CFDI queue with type=Egreso ----
async function loadNotas() {
const panel = document.getElementById('panel-notas');
@@ -133,18 +180,20 @@ const Invoicing = (() => {
const tbody = panel.querySelector('.data-table tbody');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="7" style="padding:var(--space-6);">' + renderLoadingState({ message: 'Cargando notas de credito...' }) + '</td></tr>';
try {
const res = await api('/queue?per_page=50&type=Egreso');
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>';
tbody.innerHTML = '<tr><td colspan="7" style="padding:var(--space-6);">' + renderEmptyState({ title: 'Sin notas de credito', subtitle: 'No hay notas de credito registradas.' }) + '</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>${item.cancel_motive || '-'}</td>
<td class="td--amount">$${fmt(item.total)}</td>
<td>${statusBadge(item.status)}</td>
<td>
@@ -155,7 +204,7 @@ const Invoicing = (() => {
</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>`;
tbody.innerHTML = '<tr><td colspan="7" style="padding:var(--space-6);">' + renderEmptyState({ title: 'Error', subtitle: e.message }) + '</td></tr>';
}
}
@@ -166,33 +215,47 @@ const Invoicing = (() => {
const tbody = panel.querySelector('.data-table tbody');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="8" style="padding:var(--space-6);">' + renderLoadingState({ message: 'Cargando complementos...' }) + '</td></tr>';
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('');
const res = await api('/queue?per_page=50&type=pago');
complementosCache = res.data || [];
renderComplementos(complementosCache);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="8" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
tbody.innerHTML = '<tr><td colspan="8" style="padding:var(--space-6);">' + renderEmptyState({ title: 'Error', subtitle: e.message }) + '</td></tr>';
}
}
let complementosCache = [];
function renderComplementos(items) {
const panel = document.getElementById('panel-complementos');
const tbody = panel.querySelector('.data-table tbody');
const methodFilter = document.getElementById('complementos-method-filter');
const method = methodFilter ? methodFilter.value : '';
const filtered = method ? items.filter(item => (item.payment_method || '').startsWith(method)) : items;
if (!filtered.length) {
tbody.innerHTML = '<tr><td colspan="8" style="padding:var(--space-6);">' + renderEmptyState({ title: 'Sin complementos', subtitle: 'No hay complementos de pago registrados.' }) + '</td></tr>';
return;
}
tbody.innerHTML = filtered.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('');
}
// ---- Cancelaciones (Tab 4) — loads cancelled/cancelling CFDIs ----
async function loadCancelaciones() {
const panel = document.getElementById('panel-cancelaciones');
@@ -674,13 +737,6 @@ const Invoicing = (() => {
window.submitNewInvoice = submitNewInvoice;
window.notaCreditoPlaceholder = notaCreditoPlaceholder;
return {
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, loadFacturapiStatus,
showDetail, showCancelModal, confirmCancel, processQueue,
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice, setupFacturapi,
uploadCsd, resetCsdForm,
};
// Register Cmd+K items
if (typeof registerCmdKItem === "function") {
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
@@ -689,4 +745,14 @@ const Invoicing = (() => {
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
}
return {
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, loadFacturapiStatus,
showDetail, showCancelModal, confirmCancel, processQueue,
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice, setupFacturapi,
uploadCsd, resetCsdForm,
filterFacturas, exportFacturasCSV, exportNotasCSV,
newCreditNote, newPaymentComplement,
};
})();