Merge branch 'main' into desarrollo_hector

This commit is contained in:
2026-06-17 14:20:48 -06:00
28 changed files with 2445 additions and 380 deletions

View File

@@ -84,13 +84,15 @@ const Dashboard = (() => {
});
// -------------------------------------------------------------------------
// Period selector (placeholder for future use)
// Period selector
// -------------------------------------------------------------------------
function setPeriod(btn) {
btn.closest('.period-selector').querySelectorAll('.period-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
const period = btn.textContent.trim().toLowerCase();
loadChart(period);
}
window.setPeriod = setPeriod;
@@ -205,6 +207,50 @@ const Dashboard = (() => {
return data;
}
// -------------------------------------------------------------------------
// 1b. Historical sales KPIs (imported data)
// -------------------------------------------------------------------------
async function loadHistoricalSummary() {
try {
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
// All historical sales
const all = await apiFetch('/pos/api/historical-sales?per_page=1');
const totalRecords = all.pagination ? all.pagination.total : 0;
// Current month historical sales
const month = await apiFetch(`/pos/api/historical-sales?date_from=${firstDay}&date_to=${lastDay}&per_page=200`);
const monthRows = month.data || [];
const monthTotal = monthRows.reduce((a, r) => a + (r.total || 0), 0);
const totalEl = document.getElementById('kpi-historico-total-value');
const totalMetaEl = document.getElementById('kpi-historico-total-meta');
if (totalEl) totalEl.textContent = fmt(monthTotal);
if (totalMetaEl) totalMetaEl.innerHTML = `<span class="kpi-meta-text">${fmtInt(totalRecords)} tickets importados</span>`;
const mesEl = document.getElementById('kpi-historico-mes-value');
const mesMetaEl = document.getElementById('kpi-historico-mes-meta');
if (mesEl) mesEl.textContent = fmt(monthTotal);
if (mesMetaEl) mesMetaEl.innerHTML = `<span class="kpi-meta-text">${monthRows.length} tickets este mes</span>`;
const countEl = document.getElementById('kpi-historico-count-value');
const countMetaEl = document.getElementById('kpi-historico-count-meta');
if (countEl) countEl.textContent = fmtInt(totalRecords);
if (countMetaEl) countMetaEl.innerHTML = `<span class="kpi-meta-text">Registros históricos</span>`;
} catch (err) {
console.error('Error loading historical summary:', err);
const ids = [
['kpi-historico-total-value', 'kpi-historico-total-meta'],
['kpi-historico-mes-value', 'kpi-historico-mes-meta'],
['kpi-historico-count-value', 'kpi-historico-count-meta'],
];
ids.forEach(([v, m]) => setKpiError(v, m));
}
}
function setKpiError(valueId, metaId) {
const v = document.getElementById(valueId);
const m = document.getElementById(metaId);
@@ -396,39 +442,159 @@ const Dashboard = (() => {
}
// -------------------------------------------------------------------------
// 5. Weekly bar chart (last 7 days)
// Helpers for chart grouping
// -------------------------------------------------------------------------
async function loadWeeklyChart() {
function isoWeek(date) {
const tmp = new Date(date.valueOf());
const dayNum = (date.getDay() + 6) % 7;
tmp.setDate(tmp.getDate() - dayNum + 3);
const firstThursday = tmp.valueOf();
tmp.setMonth(0, 1);
if (tmp.getDay() !== 4) {
tmp.setMonth(0, 1 + ((4 - tmp.getDay()) + 7) % 7);
}
return 1 + Math.ceil((firstThursday - tmp) / 604800000);
}
function weekLabel(date) {
return `Sem ${isoWeek(date)}`;
}
function monthLabel(date) {
return MONTH_NAMES[date.getMonth()].slice(0, 3);
}
// -------------------------------------------------------------------------
// 5. Sales chart (today / week / month / year)
// -------------------------------------------------------------------------
async function loadChart(period) {
const chartEl = document.getElementById('bar-chart');
const totalEl = document.getElementById('chart-week-total');
const legendEl = document.getElementById('chart-legend');
const titleEl = document.querySelector('.chart-header .section-title');
if (!chartEl) return;
// Fetch daily summary for each of last 7 days
const days = [];
for (let i = 6; i >= 0; i--) {
days.push(daysAgo(i));
period = period || 'semana';
let dateFrom, dateTo, labels = [], buckets = {}, labelOrder = [];
const now = new Date();
if (period === 'hoy') {
dateFrom = dateTo = todayStr();
labelOrder = ['Hoy'];
buckets['Hoy'] = 0;
} else if (period === 'semana') {
const days = [];
for (let i = 6; i >= 0; i--) { days.push(daysAgo(i)); }
dateFrom = days[0];
dateTo = days[6];
days.forEach(d => {
const date = new Date(d + 'T12:00:00');
const label = DAY_NAMES_SHORT[date.getDay()];
labelOrder.push(label);
buckets[label] = { total: 0, date: d };
});
} else if (period === 'mes') {
const year = now.getFullYear();
const month = now.getMonth();
const lastDay = new Date(year, month + 1, 0).getDate();
dateFrom = new Date(year, month, 1).toISOString().slice(0, 10);
dateTo = new Date(year, month, lastDay).toISOString().slice(0, 10);
for (let i = 1; i <= 4; i++) {
const label = `Sem ${i}`;
labelOrder.push(label);
buckets[label] = { total: 0, week: i };
}
} else if (period === 'año') {
for (let i = 11; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
const label = monthLabel(d);
labelOrder.push(label);
buckets[label] = 0;
}
dateFrom = new Date(now.getFullYear(), now.getMonth() - 11, 1).toISOString().slice(0, 10);
dateTo = todayStr();
}
const summaries = await Promise.all(
days.map(d => apiFetch(`/pos/api/register/daily-summary?date=${d}`))
);
// Fetch normal sales for short periods
let normalByKey = {};
if (period === 'hoy' || period === 'semana') {
const days = period === 'hoy' ? [todayStr()] : (function() {
const arr = [];
for (let i = 6; i >= 0; i--) arr.push(daysAgo(i));
return arr;
})();
const summaries = await Promise.all(
days.map(d => apiFetch(`/pos/api/register/daily-summary?date=${d}`))
);
days.forEach((d, i) => {
const date = new Date(d + 'T12:00:00');
const key = period === 'hoy' ? 'Hoy' : DAY_NAMES_SHORT[date.getDay()];
normalByKey[key] = summaries[i] ? (summaries[i].total_sales || 0) : 0;
});
}
let weekTotal = 0;
const dayData = days.map((dateStr, i) => {
const s = summaries[i];
const total = s ? (s.total_sales || 0) : 0;
weekTotal += total;
const d = new Date(dateStr + 'T12:00:00');
return {
label: DAY_NAMES_SHORT[d.getDay()],
total: total,
isToday: dateStr === todayStr(),
};
// Fetch historical sales for the range
let histRows = [];
try {
const perPage = period === 'año' ? 2000 : 1000;
const histData = await apiFetch(`/pos/api/historical-sales?date_from=${dateFrom}&date_to=${dateTo}&per_page=${perPage}`);
histRows = histData.data || [];
const totalPages = histData.pagination ? histData.pagination.total_pages : 1;
for (let p = 2; p <= totalPages && p <= 20; p++) {
const more = await apiFetch(`/pos/api/historical-sales?date_from=${dateFrom}&date_to=${dateTo}&per_page=${perPage}&page=${p}`);
histRows = histRows.concat(more.data || []);
}
} catch (e) {
histRows = [];
}
// Group historical sales
histRows.forEach(r => {
if (!r.sale_date) return;
const date = new Date(r.sale_date + 'T12:00:00');
let key;
if (period === 'hoy') key = 'Hoy';
else if (period === 'semana') key = DAY_NAMES_SHORT[date.getDay()];
else if (period === 'mes') {
const day = date.getDate();
const weekNum = day <= 7 ? 1 : day <= 14 ? 2 : day <= 21 ? 3 : 4;
key = `Sem ${weekNum}`;
} else {
key = monthLabel(date);
}
if (key) {
if (typeof buckets[key] === 'object') buckets[key].total += (r.total || 0);
else buckets[key] = (buckets[key] || 0) + (r.total || 0);
}
});
// Update week total
// Build chart data
let chartTotal = 0;
const dayData = labelOrder.map(label => {
let normalTotal = normalByKey[label] || 0;
let histTotal = 0;
if (typeof buckets[label] === 'object') {
histTotal = buckets[label].total;
} else {
histTotal = buckets[label] || 0;
}
const total = normalTotal + histTotal;
chartTotal += total;
const isToday = period === 'hoy' || (typeof buckets[label] === 'object' && buckets[label].date === todayStr());
return { label, total, isToday };
});
// Update labels
const titles = { hoy: 'Ventas de Hoy', semana: 'Ventas Semanales', mes: 'Ventas del Mes', año: 'Ventas del Año' };
const legends = { hoy: 'Total del día', semana: 'Ventas brutas (7 días)', mes: 'Ventas brutas (4 semanas)', año: 'Ventas brutas (12 meses)' };
if (titleEl) titleEl.textContent = titles[period] || 'Ventas';
if (totalEl) {
totalEl.innerHTML = `Total semana: <strong style="color:var(--color-primary);font-family:var(--font-mono);">${fmt(weekTotal)}</strong>`;
const periodLabel = period === 'hoy' ? 'Total día' : period === 'semana' ? 'Total semana' : period === 'mes' ? 'Total mes' : 'Total año';
totalEl.innerHTML = `${periodLabel}: <strong style="color:var(--color-primary);font-family:var(--font-mono);">${fmt(chartTotal)}</strong>`;
}
if (legendEl) {
legendEl.innerHTML = `<div class="legend-item"><div class="legend-dot"></div>${legends[period]}</div>`;
}
const maxVal = Math.max(...dayData.map(d => d.total), 1);
@@ -533,9 +699,10 @@ const Dashboard = (() => {
// Load all data in parallel
loadDailySummary();
loadHistoricalSummary();
loadAlerts();
loadTopProducts();
loadWeeklyChart();
loadChart('semana');
loadRecentSales();
// Auto-refresh every 2 minutes

View File

@@ -478,16 +478,140 @@
// PURCHASE / ENTRADA (purchaseModal)
// =====================================================================
let purchaseSearchTimeout = null;
let purchaseSelectedItem = null;
function showPurchaseModal() {
document.getElementById('purchaseModal').classList.add('is-open');
setTimeout(function() {
var el = document.getElementById('purchaseItemSearch');
if (el) el.focus();
}, 100);
}
function showPurchaseModalForItem(itemId) {
document.getElementById('purchaseItemId').value = itemId;
// Pre-fill by fetching item details
apiFetch(API + '/items?page=1&per_page=1').then(function() {
// We just need the item detail; use the existing list or fetch by id
apiFetch(API + '/items?page=1&per_page=1').then(function() {});
});
selectPurchaseItem({id: itemId, name: 'Producto #' + itemId});
showPurchaseModal();
}
function closePurchaseModal() {
document.getElementById('purchaseModal').classList.remove('is-open');
document.getElementById('purchaseResult').innerHTML = '';
clearPurchaseSelection();
}
function clearPurchaseSelection() {
purchaseSelectedItem = null;
var ids = ['purchaseItemId','purchaseItemSearch','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'];
ids.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
var results = document.getElementById('purchaseItemResults');
if (results) results.style.display = 'none';
var selected = document.getElementById('purchaseItemSelected');
if (selected) selected.textContent = '';
}
function selectPurchaseItem(item) {
purchaseSelectedItem = item;
document.getElementById('purchaseItemId').value = item.id;
document.getElementById('purchaseItemSearch').value = item.name || item.part_number || item.barcode || ('#' + item.id);
document.getElementById('purchaseItemResults').style.display = 'none';
document.getElementById('purchaseItemSelected').innerHTML =
'<strong>' + esc(item.name || '') + '</strong>' +
(item.part_number ? ' · No. parte: ' + esc(item.part_number) : '') +
(item.barcode ? ' · Barcode: ' + esc(item.barcode) : '');
document.getElementById('purchaseQty').focus();
}
function searchPurchaseItems(query) {
var resultsEl = document.getElementById('purchaseItemResults');
if (!query || query.length < 2) {
resultsEl.style.display = 'none';
return;
}
apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=10').then(function(res) {
var items = (res && res.data) || [];
if (!items.length) {
resultsEl.innerHTML = '<div style="padding:var(--space-3);color:var(--color-text-muted);font-size:var(--text-caption);">Sin resultados</div>';
resultsEl.style.display = 'block';
return;
}
resultsEl.innerHTML = items.map(function(it) {
return '<div class="purchase-search-result" style="padding:var(--space-3);cursor:pointer;border-bottom:1px solid var(--color-border);" ' +
'data-id="' + it.id + '">' +
'<div style="font-weight:var(--font-weight-semibold);">' + esc(it.name) + '</div>' +
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">' +
(it.part_number ? 'No. parte: ' + esc(it.part_number) + ' · ' : '') +
(it.barcode ? 'Barcode: ' + esc(it.barcode) + ' · ' : '') +
'Stock: ' + (it.stock || 0) +
'</div>' +
'</div>';
}).join('');
resultsEl.querySelectorAll('.purchase-search-result').forEach(function(row) {
row.onclick = function() {
var id = parseInt(row.dataset.id);
var item = items.find(function(x) { return x.id === id; });
if (item) selectPurchaseItem(item);
};
});
resultsEl.style.display = 'block';
}).catch(function() {
resultsEl.style.display = 'none';
});
}
function wirePurchaseSearch() {
var input = document.getElementById('purchaseItemSearch');
var resultsEl = document.getElementById('purchaseItemResults');
if (!input) return;
input.addEventListener('input', function() {
if (purchaseSelectedItem && input.value !== purchaseSelectedItem.name) {
purchaseSelectedItem = null;
document.getElementById('purchaseItemId').value = '';
document.getElementById('purchaseItemSelected').textContent = '';
}
clearTimeout(purchaseSearchTimeout);
purchaseSearchTimeout = setTimeout(function() {
searchPurchaseItems(input.value.trim());
}, 250);
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
// Try exact barcode match first
var query = input.value.trim();
if (!query) return;
apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=20').then(function(res) {
var items = (res && res.data) || [];
var exact = items.find(function(it) {
return (it.barcode || '').toLowerCase() === query.toLowerCase() ||
(it.part_number || '').toLowerCase() === query.toLowerCase();
});
if (exact) {
selectPurchaseItem(exact);
} else if (items.length === 1) {
selectPurchaseItem(items[0]);
} else {
searchPurchaseItems(query);
}
});
} else if (e.key === 'Escape') {
if (resultsEl) resultsEl.style.display = 'none';
}
});
document.addEventListener('click', function(e) {
if (resultsEl && !input.contains(e.target) && !resultsEl.contains(e.target)) {
resultsEl.style.display = 'none';
}
});
}
function recordPurchase() {
@@ -506,10 +630,6 @@
if (result && result.operation_id) {
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-success);">Compra registrada (op #' + result.operation_id + ')</span>';
closePurchaseModal();
['purchaseItemId','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
if (window.loadInventoryStats) window.loadInventoryStats();
loadItems(currentPage);
} else {
@@ -2009,4 +2129,5 @@
loadItems(1);
renderSavedFilters();
wirePurchaseSearch();
})();

View File

@@ -62,6 +62,7 @@ const Invoicing = (() => {
if (name === 'notas') loadNotas();
if (name === 'complementos') loadComplementos();
if (name === 'cancelaciones') loadCancelaciones();
if (name === 'config') loadFacturapiStatus();
}
// ---- Badge helpers ----
@@ -259,6 +260,75 @@ const Invoicing = (() => {
}
}
// ---- 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.has_key) {
container.innerHTML = `<p style="color:var(--color-error);">Falta la llave API de Facturapi. Configura <code>cfdi_facturapi_key</code> o la variable <code>FACTURAPI_USER_KEY</code>.</p>`;
return;
}
if (!status.has_org_id) {
container.innerHTML = `
<p style="color:var(--color-warning);margin-bottom:var(--space-3);">No hay organización Facturapi vinculada.</p>
<button class="btn btn--primary" onclick="Invoicing.setupFacturapi(this)">Crear / Vincular Organización</button>
`;
return;
}
let csdHtml = status.has_csd
? '<span style="color:var(--color-success);">Activo</span>'
: '<span style="color:var(--color-error);">Pendiente</span>';
let pendingHtml = '';
if (status.pending_steps && status.pending_steps.length) {
pendingHtml = '<ul style="margin:var(--space-2) 0 0 0;padding-left:var(--space-5);color:var(--color-warning);">' +
status.pending_steps.map(s => `<li>${s.description || s.type}</li>`).join('') +
'</ul>';
}
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);">${escapeHtml(status.legal_name) || '-'}</div>
<div style="font-family:var(--font-mono);font-size:var(--text-body-sm);">${status.tax_id || ''}</div>
<div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${status.org_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);">${csdHtml}</div>
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:var(--space-2);">Pasos pendientes</div>
${pendingHtml || '<span style="color:var(--color-success);">Ninguno</span>'}
</div>
</div>
${status.error ? `<p style="color:var(--color-error);margin-top:var(--space-3);">Error: ${escapeHtml(status.error)}</p>` : ''}
`;
} catch (e) {
container.innerHTML = `<p style="color:var(--color-error);">Error: ${e.message}</p>`;
}
}
async function setupFacturapi(btn) {
if (!btn) return;
btn.disabled = true;
btn.textContent = 'Configurando...';
try {
const res = await api('/facturapi/setup', { method: 'POST' });
alert('Organización vinculada: ' + res.org_id);
loadFacturapiStatus();
} catch (e) {
alert('Error: ' + e.message);
btn.disabled = false;
btn.textContent = 'Crear / Vincular Organización';
}
}
// ---- Detail modal (uses modalDetalleOverlay) ----
async function showDetail(cfdiId) {
const overlay = document.getElementById('modalDetalleOverlay');
@@ -300,10 +370,17 @@ const Invoicing = (() => {
</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>
` : ''}`;
${(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
@@ -531,10 +608,10 @@ const Invoicing = (() => {
window.notaCreditoPlaceholder = notaCreditoPlaceholder;
return {
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones,
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, loadFacturapiStatus,
showDetail, showCancelModal, confirmCancel, processQueue,
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice, setupFacturapi,
};
// Register Cmd+K items
if (typeof registerCmdKItem === "function") {

View File

@@ -54,7 +54,7 @@ const Reports = (() => {
}
// Track which tabs have been loaded
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false };
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false, historico: false };
// -------------------------------------------------------------------------
// Theme switcher
@@ -85,6 +85,7 @@ const Reports = (() => {
else if (id === 'inventario') loadInventario();
else if (id === 'clientes') loadClientes();
else if (id === 'financieros') loadFinancieros();
else if (id === 'historico') loadHistorico();
}
}
window.switchTab = switchTab;
@@ -289,6 +290,85 @@ const Reports = (() => {
}
}
// =========================================================================
// TAB 5: HISTÓRICO
// =========================================================================
async function loadHistorico() {
loaded.historico = true;
var dateFrom = document.getElementById('historico-date-from').value;
var dateTo = document.getElementById('historico-date-to').value;
var customer = document.getElementById('historico-customer').value.trim();
var params = new URLSearchParams();
if (dateFrom) params.set('date_from', dateFrom);
if (dateTo) params.set('date_to', dateTo);
if (customer) params.set('customer', customer);
params.set('per_page', '200');
var kpiEl = document.getElementById('historico-kpis');
var detalleEl = document.getElementById('historico-detalle');
kpiEl.innerHTML = spinner();
detalleEl.innerHTML = spinner();
try {
var allRows = [];
var page = 1;
var totalPages = 1;
while (page <= totalPages) {
params.set('page', page);
var json = await apiFetch('/pos/api/historical-sales?' + params.toString());
allRows = allRows.concat(json.data || []);
totalPages = json.pagination ? json.pagination.total_pages : 1;
page++;
if (page > 50) break;
}
var total = allRows.reduce(function(a, r) { return a + r.total; }, 0);
var subtotal = allRows.reduce(function(a, r) { return a + r.subtotal; }, 0);
var balance = allRows.reduce(function(a, r) { return a + r.balance; }, 0);
kpiEl.innerHTML =
kpiCard('Total Histórico', '$' + fmt(total), allRows.length + ' registros') +
kpiCard('Subtotal', '$' + fmt(subtotal), '') +
kpiCard('Saldo Pendiente', '$' + fmt(balance), '') +
kpiCard('Tickets', fmtInt(allRows.length), '');
var html = '<div class="table-card__header"><span class="table-card__title">Ventas Históricas Importadas</span>' +
'<span class="pill pill--muted">' + allRows.length + ' registros</span></div>';
html += '<div class="table-wrap"><table class="data-table"><thead><tr>' +
'<th>Fecha</th><th>Documento</th><th>Cliente</th><th>Pago</th>' +
'<th class="align-right">Subtotal</th><th class="align-right">Total</th>' +
'<th class="align-right">Pagado</th><th class="align-right">Saldo</th>' +
'</tr></thead><tbody>';
allRows.slice(0, 200).forEach(function(r) {
html += '<tr>' +
'<td>' + fmtDate(r.sale_date) + '</td>' +
'<td class="td-mono">' + esc(r.document_no || r.external_document_id || '--') + '</td>' +
'<td>' + esc(r.customer_name || '--') + '</td>' +
'<td><span class="pill pill--muted">' + esc(r.payment_method || '--') + '</span></td>' +
'<td class="align-right td-mono">$' + fmt(r.subtotal) + '</td>' +
'<td class="align-right td-mono-accent">$' + fmt(r.total) + '</td>' +
'<td class="align-right td-mono">$' + fmt(r.amount_paid) + '</td>' +
'<td class="align-right td-mono">$' + fmt(r.balance) + '</td>' +
'</tr>';
});
html += '</tbody></table></div>';
detalleEl.innerHTML = html;
} catch (err) {
kpiEl.innerHTML = errorMsg('Error cargando histórico: ' + err.message);
detalleEl.innerHTML = '';
}
}
function esc(s) {
if (s == null) return '';
return String(s).replace(/[&<>"']/g, function(c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
// =========================================================================
// TAB 2: INVENTARIO
// =========================================================================
@@ -712,7 +792,7 @@ const Reports = (() => {
return {
init, setTheme, switchTab,
loadVentas, loadInventario, loadClientes, loadFinancieros, fmt
loadVentas, loadInventario, loadClientes, loadFinancieros, loadHistorico, fmt
};
// Register Cmd+K items
if (typeof registerCmdKItem === "function") {

View File

@@ -6,7 +6,7 @@
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
// so templates can use cache-busting query params freely.
const CACHE_NAME = 'nexus-pos-v17';
const CACHE_NAME = 'nexus-pos-v18';
const APP_SHELL = [
'/pos/static/css/tokens.css',