// /home/Autopartes/pos/static/js/invoicing.js // 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'; function token() { return localStorage.getItem('pos_token') || ''; } function headers() { return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' }; } async function api(path, opts = {}) { const res = await fetch(`${API}${path}`, { headers: headers(), ...opts }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); throw new Error(err.error || 'Request failed'); } return res.json(); } function fmt(n) { return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // ---- Auth check ---- function checkAuth() { if (!token()) { window.location.href = '/pos/login'; return false; } return true; } // ---- 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(); } // ---- Badge helpers ---- function statusBadge(status) { const map = { pending: { css: 'badge--pendiente', label: 'Pendiente' }, pendiente: { css: 'badge--pendiente', label: 'Pendiente' }, sending: { css: 'badge--proceso', label: 'Enviando' }, stamped: { css: 'badge--timbrada', label: 'Timbrada' }, timbrada: { css: 'badge--timbrada', label: 'Timbrada' }, failed: { css: 'badge--rechazada', label: 'Fallido' }, cancelled: { css: 'badge--cancelada', label: 'Cancelada' }, cancelada: { css: 'badge--cancelada', label: 'Cancelada' }, ppd: { css: 'badge--ppd', label: 'PPD' }, proceso: { css: 'badge--proceso', label: 'En proceso' }, aceptada: { css: 'badge--aceptada', label: 'Aceptada SAT' }, rechazada: { css: 'badge--rechazada', label: 'Rechazada SAT' }, }; const s = map[status] || { css: '', label: status || '' }; return `${s.label}`; } // ---- Facturas (Tab 1) — loads from CFDI queue with type=Ingreso ---- async function loadFacturas() { const panel = document.getElementById('panel-facturas'); if (!panel) return; const tbody = panel.querySelector('.data-table tbody'); if (!tbody) return; try { const res = await api('/queue?per_page=50&type=Ingreso'); const items = res.data || []; if (!items.length) { tbody.innerHTML = 'No hay facturas en este periodo.'; return; } tbody.innerHTML = items.map(item => ` ${item.provisional_folio || item.id || '-'} ${item.serie || '-'} ${item.customer_name || '-'} ${item.rfc || '-'} $${fmt(item.subtotal)} $${fmt(item.tax)} $${fmt(item.total)} ${item.uso_cfdi || '-'} ${statusBadge(item.status)}
${item.sale_id ? `PDF` : ''} ${item.status === 'stamped' ? `` : ''}
`).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 = `Error: ${e.message}`; } } // ---- Notas de Credito (Tab 2) — loads from CFDI queue with type=Egreso ---- async function loadNotas() { const panel = document.getElementById('panel-notas'); if (!panel) return; const tbody = panel.querySelector('.data-table tbody'); if (!tbody) return; try { const res = await api('/queue?per_page=50&type=Egreso'); const items = res.data || []; if (!items.length) { tbody.innerHTML = 'No hay notas de credito.'; return; } tbody.innerHTML = items.map(item => ` ${item.provisional_folio || '-'} ${item.related_folio || '-'} ${item.customer_name || '-'} ${item.description || '-'} $${fmt(item.total)} ${statusBadge(item.status)}
${item.sale_id ? `PDF` : ''}
`).join(''); } catch (e) { tbody.innerHTML = `Error: ${e.message}`; } } // ---- Complementos de Pago (Tab 3) — loads from CFDI queue with type=Pago ---- async function loadComplementos() { const panel = document.getElementById('panel-complementos'); if (!panel) return; const tbody = panel.querySelector('.data-table tbody'); if (!tbody) return; try { const res = await api('/queue?per_page=50&type=Pago'); const items = res.data || []; if (!items.length) { tbody.innerHTML = 'No hay complementos de pago.'; return; } tbody.innerHTML = items.map(item => ` ${item.provisional_folio || '-'} ${item.related_folio || '-'} ${item.customer_name || '-'} $${fmt(item.total)} ${item.payment_method || '-'} ${item.created_at ? new Date(item.created_at).toLocaleDateString('es-MX') : '-'} ${statusBadge(item.status)}
${item.status === 'stamped' ? `` : ''}
`).join(''); } catch (e) { tbody.innerHTML = `Error: ${e.message}`; } } // ---- Cancelaciones (Tab 4) — loads cancelled/cancelling CFDIs ---- async function loadCancelaciones() { const panel = document.getElementById('panel-cancelaciones'); if (!panel) return; const grid = panel.querySelector('.cancel-grid'); if (!grid) return; try { const res = await api('/queue?per_page=50&status=cancelled'); const items = res.data || []; // Also try to get in-process cancellations let processingItems = []; try { const res2 = await api('/queue?per_page=50&status=cancelling'); processingItems = res2.data || []; } catch (_) { /* ignore */ } const allItems = [...processingItems, ...items]; if (!allItems.length) { grid.innerHTML = '

No hay solicitudes de cancelacion.

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

Error: ${e.message}

`; } } // ---- Detail modal (uses modalDetalleOverlay) ---- async function showDetail(cfdiId) { const overlay = document.getElementById('modalDetalleOverlay'); if (!overlay) return; try { const item = await api(`/queue/${cfdiId}`); const modalCard = overlay.querySelector('.modal-card'); if (!modalCard) return; // Update header const headerTitle = modalCard.querySelector('div > div:first-child > div:first-child'); const headerSub = modalCard.querySelector('div > div:first-child > div:nth-child(2)'); if (headerTitle) headerTitle.textContent = 'Detalle de Factura'; if (headerSub) headerSub.textContent = `${item.provisional_folio || 'CFDI-' + item.id} \u2014 ${item.status === 'stamped' ? 'Timbrada' : item.status === 'cancelled' ? 'Cancelada' : item.status === 'pending' ? 'Pendiente' : item.status || ''}`; // Update detail grid const detailGrid = modalCard.querySelector('div:nth-child(2)'); if (detailGrid) { detailGrid.innerHTML = `
Emisor
${item.emisor_name || 'Nexus Autoparts SA de CV'}
${item.emisor_rfc || ''}
Receptor
${item.customer_name || '-'}
${item.rfc || ''}
UUID
${item.uuid_fiscal || 'Sin timbrar'}
Total
$${fmt(item.total)}
${item.error_message ? `

Error: ${escapeHtml(item.error_message)}

` : ''} ${(item.xml_signed || item.xml_unsigned) ? `
Vista previa XML
${escapeHtml(item.xml_signed || item.xml_unsigned)}
` : ''}`; } // 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'; } // 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 al cargar detalle: ' + e.message); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ---- Cancel modal (uses modalCancelOverlay) ---- let cancelTargetId = null; function showCancelModal(cfdiId) { cancelTargetId = cfdiId; const overlay = document.getElementById('modalCancelOverlay'); if (overlay) overlay.style.display = 'flex'; } async function confirmCancel() { if (!cancelTargetId) return; const overlay = document.getElementById('modalCancelOverlay'); if (!overlay) 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; try { const body = { motive }; if (replacementUuid) body.replacement_uuid = replacementUuid; 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); } } // ---- 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); } } // ---- 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); } // ---- 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); // ---- Nueva Factura modal ---- function showNewInvoiceModal() { const overlay = document.getElementById('newInvoiceModalOverlay'); if (!overlay) return; const input = overlay.querySelector('#invoiceSaleId'); if (input) input.value = ''; document.getElementById('invoiceResult').innerHTML = ''; overlay.style.display = 'flex'; } function closeNewInvoiceModal() { const overlay = document.getElementById('newInvoiceModalOverlay'); if (overlay) overlay.style.display = 'none'; } async function submitNewInvoice() { const saleId = parseInt(document.getElementById('invoiceSaleId').value); const resultEl = document.getElementById('invoiceResult'); if (!saleId) { resultEl.innerHTML = 'Ingrese un ID de venta valido.'; return; } try { const result = await api('/invoice', { method: 'POST', body: JSON.stringify({ sale_id: saleId }), }); resultEl.innerHTML = 'Factura generada: ' + (result.provisional_folio || 'CFDI-' + (result.id || '')) + ''; loadFacturas(); setTimeout(() => closeNewInvoiceModal(), 1500); } catch (e) { resultEl.innerHTML = 'Error: ' + e.message + ''; } } // ---- Nota de Credito placeholder ---- function notaCreditoPlaceholder() { alert('Nota de credito: proximamente'); } // Expose switchTab globally for onclick handlers in HTML window.switchTab = switchTab; window.showNewInvoiceModal = showNewInvoiceModal; window.closeNewInvoiceModal = closeNewInvoiceModal; window.submitNewInvoice = submitNewInvoice; window.notaCreditoPlaceholder = notaCreditoPlaceholder; return { switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, showDetail, showCancelModal, confirmCancel, processQueue, showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder, }; // Register Cmd+K items if (typeof registerCmdKItem === "function") { registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" }); registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" }); registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" }); registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" }); } })();