- Add Facturapi REST service (invoices, customers, orgs, cancel, downloads) - Add JSON payload builder for ingreso/egreso/pago/global invoices - Replace XML queue with Facturapi JSON queue (payload_unsigned, external_id) - Update invoicing blueprint with Facturapi config and download endpoints - Update global invoice service to use Facturapi payloads - Add migration v4.3_facturapi.sql and tenant rollout script - Update invoicing UI: payload preview, PDF/XML downloads, PAC status panel - Add FACTURAPI_USER_KEY to .env.example
584 lines
29 KiB
JavaScript
584 lines
29 KiB
JavaScript
// /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();
|
|
if (name === 'config') loadFacturapiStatus();
|
|
}
|
|
|
|
// ---- 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;
|
|
|
|
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('');
|
|
|
|
// 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>`;
|
|
}
|
|
}
|
|
|
|
// ---- 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>`;
|
|
}
|
|
}
|
|
|
|
// ---- Facturapi status (config tab) ----
|
|
async function loadFacturapiStatus() {
|
|
const container = document.getElementById('facturapi-status');
|
|
if (!container) return;
|
|
container.innerHTML = 'Cargando...';
|
|
try {
|
|
const status = await api('/facturapi/status');
|
|
if (!status.configured) {
|
|
container.innerHTML = `<p style="color:var(--color-error);">Facturapi no configurado. Configura la llave API en Configuración.</p>`;
|
|
return;
|
|
}
|
|
container.innerHTML = `
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
|
|
<div>
|
|
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">Organización</div>
|
|
<div style="font-weight:var(--font-weight-semibold);">${status.legal_name || '-'}</div>
|
|
<div style="font-family:var(--font-mono);font-size:var(--text-body-sm);">${status.tax_id || ''}</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">CSD</div>
|
|
<div style="font-weight:var(--font-weight-semibold);">${status.has_csd ? '<span style="color:var(--color-success);">Activo</span>' : '<span style="color:var(--color-error);">Pendiente</span>'}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
container.innerHTML = `<p style="color:var(--color-error);">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}`);
|
|
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 = `
|
|
<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.payload_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);">${item.xml_signed ? 'Vista previa XML' : 'Payload Facturapi'}</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.payload_unsigned)}</pre>
|
|
` : ''}
|
|
${item.status === 'stamped' && item.external_id ? `
|
|
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-4);">
|
|
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/xml" target="_blank">Descargar XML</a>
|
|
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/pdf" target="_blank">Descargar PDF</a>
|
|
</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';
|
|
}
|
|
|
|
// 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 = '<span style="color:var(--color-error);">Ingrese un ID de venta valido.</span>';
|
|
return;
|
|
}
|
|
try {
|
|
const result = await api('/invoice', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ sale_id: saleId }),
|
|
});
|
|
resultEl.innerHTML = '<span style="color:var(--color-success);">Factura generada: ' + (result.provisional_folio || 'CFDI-' + (result.id || '')) + '</span>';
|
|
loadFacturas();
|
|
setTimeout(() => closeNewInvoiceModal(), 1500);
|
|
} catch (e) {
|
|
resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + e.message + '</span>';
|
|
}
|
|
}
|
|
|
|
// ---- Nota de Credito placeholder ----
|
|
function notaCreditoPlaceholder() {
|
|
alert('Nota de credito: proximamente');
|
|
}
|
|
|
|
// ---- Global Invoice ----
|
|
function openGlobalInvoiceModal() {
|
|
const now = new Date();
|
|
document.getElementById('global-year').value = now.getFullYear();
|
|
document.getElementById('global-month').value = now.getMonth() + 1;
|
|
document.getElementById('global-preview').innerHTML = 'Presiona "Vista previa" para ver ventas elegibles.';
|
|
document.getElementById('modalGlobalInvoice').style.display = 'flex';
|
|
}
|
|
|
|
async function previewGlobalInvoice() {
|
|
const year = document.getElementById('global-year').value;
|
|
const month = document.getElementById('global-month').value;
|
|
const preview = document.getElementById('global-preview');
|
|
preview.innerHTML = 'Cargando...';
|
|
try {
|
|
const res = await api(`/global-invoice/eligible-sales?year=${year}&month=${month}`);
|
|
preview.innerHTML = `<strong>${res.count} ventas elegibles</strong> — Total: $${fmt(res.total)}<br><small>${res.sales.map(s => '#' + s.id).join(', ')}</small>`;
|
|
} catch (e) {
|
|
preview.innerHTML = '<span style="color:var(--color-error);">Error: ' + e.message + '</span>';
|
|
}
|
|
}
|
|
|
|
async function generateGlobalInvoice() {
|
|
const year = parseInt(document.getElementById('global-year').value, 10);
|
|
const month = parseInt(document.getElementById('global-month').value, 10);
|
|
const btn = document.querySelector('#modalGlobalInvoice .btn--primary');
|
|
const originalText = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Generando...';
|
|
try {
|
|
const res = await api('/global-invoice', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ year, month })
|
|
});
|
|
alert(`Factura global generada: ${res.provisional_folio} (${res.sales_count} ventas, $${fmt(res.total)})`);
|
|
document.getElementById('modalGlobalInvoice').style.display = 'none';
|
|
loadFacturas();
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = originalText;
|
|
}
|
|
}
|
|
|
|
// 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, loadFacturapiStatus,
|
|
showDetail, showCancelModal, confirmCancel, processQueue,
|
|
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
|
|
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,
|
|
};
|
|
// 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: "📊" });
|
|
}
|
|
|
|
})();
|