fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
- 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:
@@ -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,
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user