// /home/Autopartes/pos/static/js/reports.js
// Reports module: sales reports, inventory reports, financial reports
const Reports = (() => {
function token() {
return localStorage.getItem('pos_token') || '';
}
function checkAuth() {
if (!token()) {
window.location.href = '/pos/login';
return false;
}
return true;
}
function headers() {
return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' };
}
function fmt(n) {
return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function fmtInt(n) {
return parseInt(n || 0).toLocaleString('es-MX');
}
function fmtDate(s) {
if (!s) return '--';
var d = new Date(s);
if (isNaN(d)) return s;
return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
}
function fmtDateTime(s) {
if (!s) return '--';
var d = new Date(s);
if (isNaN(d)) return s;
return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' }) +
' ' + d.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
}
function spinner() {
return '
Cargando...
';
}
function emptyMsg(text) {
return '' + text + '
';
}
function errorMsg(text) {
return '' + text + '
';
}
// Track which tabs have been loaded
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false };
// -------------------------------------------------------------------------
// Theme switcher
// -------------------------------------------------------------------------
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('nexus-theme', theme); } catch(e) {}
var btnInd = document.getElementById('btn-industrial');
var btnMod = document.getElementById('btn-modern');
if (btnInd) btnInd.classList.toggle('is-active', theme === 'industrial');
if (btnMod) btnMod.classList.toggle('is-active', theme === 'modern');
}
window.setTheme = setTheme;
// -------------------------------------------------------------------------
// Tab switcher with lazy loading
// -------------------------------------------------------------------------
function switchTab(id, btn) {
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('is-active'); });
var panel = document.getElementById('panel-' + id);
if (panel) panel.classList.add('is-active');
if (btn) btn.classList.add('is-active');
// Lazy load on first visit
if (!loaded[id]) {
if (id === 'ventas') loadVentas();
else if (id === 'inventario') loadInventario();
else if (id === 'clientes') loadClientes();
else if (id === 'financieros') loadFinancieros();
}
}
window.switchTab = switchTab;
// -------------------------------------------------------------------------
// Live clock
// -------------------------------------------------------------------------
function updateClock() {
var el = document.getElementById('live-clock');
if (!el) return;
var now = new Date();
var pad = function(n) { return String(n).padStart(2, '0'); };
el.textContent = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds());
}
// -------------------------------------------------------------------------
// Generic fetch helper
// -------------------------------------------------------------------------
async function apiFetch(url) {
var resp = await fetch(url, { headers: headers() });
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return resp.json();
}
// -------------------------------------------------------------------------
// KPI card builder
// -------------------------------------------------------------------------
function kpiCard(label, value, sub) {
return '' +
'
' + label + '
' +
'
' + value + '
' +
(sub ? '
' + sub + '
' : '') +
'
';
}
// =========================================================================
// TAB 1: VENTAS
// =========================================================================
async function loadVentas() {
loaded.ventas = true;
var dateFrom = document.getElementById('ventas-date-from').value;
var dateTo = document.getElementById('ventas-date-to').value;
var params = new URLSearchParams();
if (dateFrom) params.set('date_from', dateFrom);
if (dateTo) params.set('date_to', dateTo);
params.set('per_page', '200');
// Show spinners
var kpiEl = document.getElementById('ventas-kpis');
var barEl = document.getElementById('ventas-bar-chart');
var vendedorEl = document.getElementById('ventas-por-vendedor');
var metodoEl = document.getElementById('ventas-por-metodo');
var detalleEl = document.getElementById('ventas-detalle');
kpiEl.innerHTML = spinner();
barEl.innerHTML = '';
vendedorEl.innerHTML = spinner();
metodoEl.innerHTML = spinner();
detalleEl.innerHTML = spinner();
try {
// Fetch all pages to get complete data for the period
var allSales = [];
var page = 1;
var totalPages = 1;
while (page <= totalPages) {
params.set('page', page);
var json = await apiFetch('/pos/api/sales?' + params.toString());
allSales = allSales.concat(json.data || []);
totalPages = json.pagination ? json.pagination.total_pages : 1;
page++;
if (page > 50) break; // safety limit
}
var sales = allSales.filter(function(s) { return s.status === 'completed'; });
// KPIs
var totalVentas = sales.reduce(function(a, s) { return a + s.total; }, 0);
var numVentas = sales.length;
var ticketProm = numVentas > 0 ? totalVentas / numVentas : 0;
kpiEl.innerHTML =
kpiCard('Total Ventas', '$' + fmt(totalVentas), numVentas + ' transacciones') +
kpiCard('Ticket Promedio', '$' + fmt(ticketProm), '') +
kpiCard('Transacciones', fmtInt(numVentas), '') +
kpiCard('Descuentos', '$' + fmt(sales.reduce(function(a, s) { return a + s.discount_total; }, 0)), '');
// Bar chart: sales by day
var byDay = {};
var dayNames = ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'];
sales.forEach(function(s) {
var d = s.created_at.substring(0, 10);
byDay[d] = (byDay[d] || 0) + s.total;
});
var days = Object.keys(byDay).sort().slice(-7);
var maxDay = Math.max.apply(null, days.map(function(d) { return byDay[d]; })) || 1;
if (days.length > 0) {
var barHtml = 'Ventas por Dia
';
days.forEach(function(d) {
var val = byDay[d];
var pct = Math.round(val / maxDay * 100);
var label = dayNames[new Date(d + 'T12:00:00').getDay()];
var valStr = val >= 1000 ? '$' + (val / 1000).toFixed(1) + 'k' : '$' + fmt(val);
barHtml += '
';
});
barHtml += '
';
barEl.innerHTML = barHtml;
}
// Ventas por vendedor
var byEmployee = {};
sales.forEach(function(s) {
var key = s.employee_id || 0;
if (!byEmployee[key]) {
byEmployee[key] = { name: s.employee_name || 'Sin asignar', count: 0, total: 0 };
}
byEmployee[key].count++;
byEmployee[key].total += s.total;
});
var empList = Object.values(byEmployee).sort(function(a, b) { return b.total - a.total; });
var empHtml = '';
empHtml += '' +
'| Vendedor | # Ventas | ' +
'Total | Ticket Prom. |
';
empList.forEach(function(e) {
var initials = e.name.split(' ').map(function(w) { return w[0]; }).join('').substring(0, 2).toUpperCase();
empHtml += '' +
' ' + initials + ' ' +
' ' + e.name + ' | ' +
'' + e.count + ' | ' +
'$' + fmt(e.total) + ' | ' +
'$' + fmt(e.count > 0 ? e.total / e.count : 0) + ' |
';
});
empHtml += '
';
vendedorEl.innerHTML = empList.length ? empHtml : emptyMsg('Sin datos de vendedores');
// Ventas por metodo de pago
var byMethod = {};
sales.forEach(function(s) {
var m = s.payment_method || 'Otro';
byMethod[m] = (byMethod[m] || 0) + s.total;
});
var methods = Object.entries(byMethod).sort(function(a, b) { return b[1] - a[1]; });
var maxMethod = methods.length > 0 ? methods[0][1] : 1;
var methodLabels = {
'cash': 'Efectivo', 'card': 'Tarjeta', 'transfer': 'Transferencia',
'credit': 'Credito', 'mixed': 'Mixto'
};
var barColors = ['', 'pay-method__bar--b', 'pay-method__bar--c', 'pay-method__bar--d'];
var metHtml = '';
metHtml += '';
methods.forEach(function(m, idx) {
var pct = totalVentas > 0 ? Math.round(m[1] / totalVentas * 100) : 0;
var label = methodLabels[m[0]] || m[0];
metHtml += '
' + label + '' +
'
' +
'
$' + fmt(m[1]) + ' ' + pct + '% ';
});
metHtml += '
';
metodoEl.innerHTML = methods.length ? metHtml : emptyMsg('Sin datos de metodos');
// Sales detail table
var dtlHtml = '';
dtlHtml += '' +
'| # | Fecha | Vendedor | Cliente | Pago | ' +
'Subtotal | Desc. | ' +
'IVA | Total | Estado | ' +
'
';
allSales.slice(0, 100).forEach(function(s) {
var statusPill = s.status === 'completed' ? 'pill--success' :
s.status === 'cancelled' ? 'pill--error' : 'pill--warning';
var statusLabel = s.status === 'completed' ? 'Completada' :
s.status === 'cancelled' ? 'Cancelada' : s.status;
dtlHtml += '| ' + s.id + ' | ' +
'' + fmtDateTime(s.created_at) + ' | ' +
'' + (s.employee_name || '--') + ' | ' +
'' + (s.customer_name || 'Mostrador') + ' | ' +
'' + (methodLabels[s.payment_method] || s.payment_method || '--') + ' | ' +
'$' + fmt(s.subtotal) + ' | ' +
'$' + fmt(s.discount_total) + ' | ' +
'$' + fmt(s.tax_total) + ' | ' +
'$' + fmt(s.total) + ' | ' +
'' + statusLabel + ' |
';
});
dtlHtml += '
';
detalleEl.innerHTML = dtlHtml;
} catch (err) {
kpiEl.innerHTML = errorMsg('Error cargando ventas: ' + err.message);
vendedorEl.innerHTML = '';
metodoEl.innerHTML = '';
detalleEl.innerHTML = '';
}
}
// =========================================================================
// TAB 2: INVENTARIO
// =========================================================================
async function loadInventario() {
loaded.inventario = true;
var kpiEl = document.getElementById('inventario-kpis');
var valEl = document.getElementById('inventario-valorizacion');
var abcEl = document.getElementById('inventario-abc');
var lowEl = document.getElementById('inventario-low-stock');
var noMoveEl = document.getElementById('inventario-no-movement');
kpiEl.innerHTML = spinner();
valEl.innerHTML = spinner();
abcEl.innerHTML = spinner();
lowEl.innerHTML = spinner();
noMoveEl.innerHTML = spinner();
try {
var [valData, abcData, lowData, noMoveData] = await Promise.all([
apiFetch('/pos/api/inventory/reports/valuation'),
apiFetch('/pos/api/inventory/reports/abc'),
apiFetch('/pos/api/inventory/reports/low-stock'),
apiFetch('/pos/api/inventory/reports/no-movement')
]);
// KPIs
kpiEl.innerHTML =
kpiCard('Valor Total Inventario', '$' + fmt(valData.grand_total), fmtInt(valData.item_count) + ' SKUs activos') +
kpiCard('Clasificacion A', fmtInt(abcData.summary.A) + ' SKUs', '80% del volumen de ventas') +
kpiCard('Stock Bajo', fmtInt(lowData.count) + ' productos', 'debajo del minimo') +
kpiCard('Sin Movimiento', fmtInt(noMoveData.count) + ' productos', '>' + noMoveData.days_threshold + ' dias');
// Valuation table (top 20)
var vItems = (valData.data || []).slice(0, 20);
var vHtml = '';
vHtml += '' +
'| Producto | No. Parte | Marca | ' +
'Stock | Costo Unit. | ' +
'Valor |
';
vItems.forEach(function(i) {
vHtml += '| ' + i.name + ' | ' +
'' + (i.part_number || '--') + ' | ' +
'' + (i.brand || '--') + ' | ' +
'' + fmtInt(i.stock) + ' | ' +
'$' + fmt(i.cost) + ' | ' +
'$' + fmt(i.value) + ' |
';
});
vHtml += '
';
valEl.innerHTML = vHtml;
// ABC table
var abcItems = (abcData.data || []).slice(0, 30);
var abcHtml = '';
abcHtml += '' +
'| Producto | No. Parte | Marca | ' +
'Vol. Ventas | % Acum. | ' +
'Clase |
';
abcItems.forEach(function(i) {
var clsPill = i.classification === 'A' ? 'pill--success' :
i.classification === 'B' ? 'pill--warning' : 'pill--muted';
abcHtml += '| ' + i.name + ' | ' +
'' + (i.part_number || '--') + ' | ' +
'' + (i.brand || '--') + ' | ' +
'' + fmtInt(i.sales_volume) + ' | ' +
'' + i.cumulative_pct + '% | ' +
'' + i.classification + ' |
';
});
abcHtml += '
';
abcEl.innerHTML = abcHtml;
// Low stock
var lowItems = lowData.data || [];
var lowHtml = '';
lowHtml += '' +
'| Producto | No. Parte | Marca | ' +
'Stock | Minimo | ' +
'Deficit |
';
lowItems.slice(0, 30).forEach(function(i) {
var stockColor = i.stock <= 0 ? 'color:var(--color-error)' :
i.stock < i.min_stock / 2 ? 'color:var(--color-error)' : 'color:var(--color-warning)';
lowHtml += '| ' + i.name + ' | ' +
'' + (i.part_number || '--') + ' | ' +
'' + (i.brand || '--') + ' | ' +
'' + fmtInt(i.stock) + ' | ' +
'' + fmtInt(i.min_stock) + ' | ' +
'' + fmtInt(i.deficit) + ' |
';
});
lowHtml += '
';
lowEl.innerHTML = lowItems.length ? lowHtml : emptyMsg('No hay productos con stock bajo');
// No movement
var noItems = noMoveData.data || [];
var noHtml = '';
noHtml += '' +
'| Producto | No. Parte | Marca | ' +
'Stock | Costo Unit. | ' +
'Ultimo Movimiento |
';
noItems.slice(0, 30).forEach(function(i) {
noHtml += '| ' + i.name + ' | ' +
'' + (i.part_number || '--') + ' | ' +
'' + (i.brand || '--') + ' | ' +
'' + fmtInt(i.stock) + ' | ' +
'$' + fmt(i.cost) + ' | ' +
'' + fmtDate(i.last_movement) + ' |
';
});
noHtml += '
';
noMoveEl.innerHTML = noItems.length ? noHtml : emptyMsg('No hay productos sin movimiento');
} catch (err) {
kpiEl.innerHTML = errorMsg('Error cargando inventario: ' + err.message);
valEl.innerHTML = '';
abcEl.innerHTML = '';
lowEl.innerHTML = '';
noMoveEl.innerHTML = '';
}
}
// =========================================================================
// TAB 3: CLIENTES (Aging report)
// =========================================================================
async function loadClientes() {
loaded.clientes = true;
var kpiEl = document.getElementById('clientes-kpis');
var agingEl = document.getElementById('clientes-aging');
kpiEl.innerHTML = spinner();
agingEl.innerHTML = spinner();
try {
var data = await apiFetch('/pos/api/accounting/aging');
var clients = data.data || [];
var totals = data.totals || {};
// KPIs
kpiEl.innerHTML =
kpiCard('Clientes con Credito', fmtInt(clients.length), 'con saldo pendiente') +
kpiCard('Saldo Total', '$' + fmt(totals.total), '') +
kpiCard('Corriente', '$' + fmt(totals.corriente), 'no vencido') +
kpiCard('Vencido >90 dias', '$' + fmt(totals.d90_plus),
totals.d90_plus > 0 ? 'requiere atencion' : '');
// Aging table
var html = '';
html += '' +
'| Cliente | RFC | ' +
'Corriente | 1-30 dias | ' +
'31-60 dias | 61-90 dias | ' +
'90+ dias | Total | ' +
'
';
clients.forEach(function(c) {
html += '| ' + c.name + ' | ' +
'' + (c.rfc || '--') + ' | ' +
'$' + fmt(c.corriente) + ' | ' +
'$' + fmt(c.d1_30) + ' | ' +
' 0 ? ' style="color:var(--color-warning)"' : '') + '>$' + fmt(c.d31_60) + ' | ' +
' 0 ? ' style="color:var(--color-error)"' : '') + '>$' + fmt(c.d61_90) + ' | ' +
' 0 ? ' style="color:var(--color-error);font-weight:700"' : '') + '>$' + fmt(c.d90_plus) + ' | ' +
'$' + fmt(c.total) + ' |
';
});
// Totals row
html += '' +
'| TOTAL | ' +
'$' + fmt(totals.corriente) + ' | ' +
'$' + fmt(totals.d1_30) + ' | ' +
'$' + fmt(totals.d31_60) + ' | ' +
'$' + fmt(totals.d61_90) + ' | ' +
'$' + fmt(totals.d90_plus) + ' | ' +
'$' + fmt(totals.total) + ' |
';
html += '
';
agingEl.innerHTML = clients.length ? html : emptyMsg('No hay saldos pendientes de credito');
} catch (err) {
kpiEl.innerHTML = errorMsg('Error cargando datos de clientes: ' + err.message);
agingEl.innerHTML = '';
}
}
// =========================================================================
// TAB 4: FINANCIEROS
// =========================================================================
async function loadFinancieros() {
loaded.financieros = true;
var monthSel = document.getElementById('fin-month');
var yearSel = document.getElementById('fin-year');
var year = parseInt(yearSel.value);
var month = parseInt(monthSel.value);
var kpiEl = document.getElementById('financieros-kpis');
var incomeEl = document.getElementById('financieros-income');
var balanceEl = document.getElementById('financieros-balance');
var trialEl = document.getElementById('financieros-trial');
var cortesEl = document.getElementById('financieros-cortes');
kpiEl.innerHTML = spinner();
incomeEl.innerHTML = spinner();
balanceEl.innerHTML = spinner();
trialEl.innerHTML = spinner();
cortesEl.innerHTML = spinner();
try {
var [incData, balData, trialData, cortesData] = await Promise.all([
apiFetch('/pos/api/accounting/income-statement?year=' + year + '&month=' + month),
apiFetch('/pos/api/accounting/balance-sheet'),
apiFetch('/pos/api/accounting/trial-balance?year=' + year + '&month=' + month),
apiFetch('/pos/api/register/history?per_page=50')
]);
// KPIs from income statement
kpiEl.innerHTML =
kpiCard('Ingresos', '$' + fmt(incData.ingresos.total), 'periodo ' + month + '/' + year) +
kpiCard('Costos', '$' + fmt(incData.costos.total), '') +
kpiCard('Utilidad Bruta', '$' + fmt(incData.utilidad_bruta), '') +
kpiCard('Utilidad Neta', '$' + fmt(incData.utilidad_neta),
incData.ingresos.total > 0 ? 'Margen: ' + (incData.utilidad_neta / incData.ingresos.total * 100).toFixed(1) + '%' : '');
// Income statement
var iHtml = '';
iHtml += '' +
'| Cuenta | Codigo | Monto |
';
// Ingresos section
iHtml += '| INGRESOS | |
';
(incData.ingresos.items || []).forEach(function(i) {
iHtml += '| ' + i.name + ' | ' +
'' + i.code + ' | ' +
'$' + fmt(i.amount) + ' |
';
});
iHtml += '| Total Ingresos | | ' +
'$' + fmt(incData.ingresos.total) + ' |
';
// Costos section
iHtml += '| COSTOS | |
';
(incData.costos.items || []).forEach(function(i) {
iHtml += '| ' + i.name + ' | ' +
'' + i.code + ' | ' +
'$' + fmt(i.amount) + ' |
';
});
iHtml += '| Total Costos | | ' +
'$' + fmt(incData.costos.total) + ' |
';
// Utilidad bruta
iHtml += '| UTILIDAD BRUTA | | ' +
'$' + fmt(incData.utilidad_bruta) + ' |
';
// Gastos section
iHtml += '| GASTOS | |
';
(incData.gastos.items || []).forEach(function(i) {
iHtml += '| ' + i.name + ' | ' +
'' + i.code + ' | ' +
'$' + fmt(i.amount) + ' |
';
});
iHtml += '| Total Gastos | | ' +
'$' + fmt(incData.gastos.total) + ' |
';
// Utilidad neta
var netColor = incData.utilidad_neta >= 0 ? 'var(--color-success)' : 'var(--color-error)';
iHtml += '| UTILIDAD NETA | | ' +
'$' + fmt(incData.utilidad_neta) + ' |
';
iHtml += '
';
incomeEl.innerHTML = iHtml;
// Balance sheet
var bHtml = '';
bHtml += '' +
'| Cuenta | Codigo | Saldo |
';
// Activo
bHtml += '| ACTIVO | |
';
(balData.activo.items || []).forEach(function(i) {
bHtml += '| ' + i.name + ' | ' +
'' + i.code + ' | ' +
'$' + fmt(i.balance) + ' |
';
});
bHtml += '| Total Activo | | ' +
'$' + fmt(balData.activo.total) + ' |
';
// Pasivo
bHtml += '| PASIVO | |
';
(balData.pasivo.items || []).forEach(function(i) {
bHtml += '| ' + i.name + ' | ' +
'' + i.code + ' | ' +
'$' + fmt(i.balance) + ' |
';
});
bHtml += '| Total Pasivo | | ' +
'$' + fmt(balData.pasivo.total) + ' |
';
// Capital
bHtml += '| CAPITAL | |
';
(balData.capital.items || []).forEach(function(i) {
bHtml += '| ' + i.name + ' | ' +
'' + i.code + ' | ' +
'$' + fmt(i.balance) + ' |
';
});
bHtml += '| Total Capital | | ' +
'$' + fmt(balData.capital.total) + ' |
';
bHtml += '| Pasivo + Capital | | ' +
'$' + fmt(balData.pasivo.total + balData.capital.total) + ' |
';
bHtml += '
';
balanceEl.innerHTML = bHtml;
// Trial balance
var tRows = trialData.data || [];
var tHtml = '';
tHtml += '' +
'| Codigo | Cuenta | Tipo | ' +
'Saldo Inicial | Cargos | ' +
'Abonos | Saldo Final | ' +
'
';
tRows.forEach(function(r) {
tHtml += '| ' + r.code + ' | ' +
'' + r.name + ' | ' +
'' + r.type + ' | ' +
'$' + fmt(r.saldo_inicial) + ' | ' +
'$' + fmt(r.cargos) + ' | ' +
'$' + fmt(r.abonos) + ' | ' +
'$' + fmt(r.saldo_final) + ' |
';
});
tHtml += '
';
trialEl.innerHTML = tRows.length ? tHtml : emptyMsg('Sin movimientos contables en este periodo');
// Cortes de caja
var regs = cortesData.data || [];
var cHtml = '';
cHtml += '' +
'| Caja | Empleado | Apertura | Cierre | ' +
'Monto Apertura | Esperado | ' +
'Cierre Real | Diferencia | ' +
'
';
regs.forEach(function(r) {
var diffColor = r.difference < 0 ? 'color:var(--color-error)' :
r.difference > 0 ? 'color:var(--color-warning)' : 'color:var(--color-success)';
cHtml += '| #' + r.register_number + ' | ' +
'' + (r.employee_name || '--') + ' | ' +
'' + fmtDateTime(r.opened_at) + ' | ' +
'' + fmtDateTime(r.closed_at) + ' | ' +
'$' + fmt(r.opening_amount) + ' | ' +
'$' + fmt(r.expected_amount) + ' | ' +
'$' + fmt(r.closing_amount) + ' | ' +
'$' + fmt(r.difference) + ' |
';
});
cHtml += '
';
cortesEl.innerHTML = regs.length ? cHtml : emptyMsg('No hay cortes de caja registrados');
} catch (err) {
kpiEl.innerHTML = errorMsg('Error cargando reportes financieros: ' + err.message);
incomeEl.innerHTML = '';
balanceEl.innerHTML = '';
trialEl.innerHTML = '';
cortesEl.innerHTML = '';
}
}
// -------------------------------------------------------------------------
// Init
// -------------------------------------------------------------------------
function init() {
if (!checkAuth()) return;
// Restore theme
try {
var saved = localStorage.getItem('nexus-theme') || 'industrial';
setTheme(saved);
} catch(e) {}
// Start clock
updateClock();
setInterval(updateClock, 1000);
// Set default date range: first day of current month to today
var now = new Date();
var firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
var fromEl = document.getElementById('ventas-date-from');
var toEl = document.getElementById('ventas-date-to');
if (fromEl) fromEl.value = firstDay.toISOString().substring(0, 10);
if (toEl) toEl.value = now.toISOString().substring(0, 10);
// Populate financial period selectors
var monthSel = document.getElementById('fin-month');
var yearSel = document.getElementById('fin-year');
if (monthSel) {
var monthNames = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
for (var m = 0; m < 12; m++) {
var opt = document.createElement('option');
opt.value = m + 1;
opt.textContent = monthNames[m];
if (m === now.getMonth()) opt.selected = true;
monthSel.appendChild(opt);
}
}
if (yearSel) {
for (var y = now.getFullYear(); y >= now.getFullYear() - 3; y--) {
var opt = document.createElement('option');
opt.value = y;
opt.textContent = y;
yearSel.appendChild(opt);
}
}
// Load the default active tab (ventas)
loadVentas();
}
document.addEventListener('DOMContentLoaded', init);
return {
init, setTheme, switchTab,
loadVentas, loadInventario, loadClientes, loadFinancieros, fmt
};
})();
// ── Global: Export visible table as CSV (Excel-compatible) ──
function exportReportCSV() {
var tables = document.querySelectorAll('table');
// Find the first visible table
var table = null;
for (var i = 0; i < tables.length; i++) {
var t = tables[i];
if (t.offsetParent !== null && t.querySelector('tbody tr')) {
table = t;
break;
}
}
if (!table) {
alert('No hay tabla de datos para exportar en esta vista.');
return;
}
var rows = [];
var ths = table.querySelectorAll('thead th');
if (ths.length) {
rows.push(Array.from(ths).map(function(th) { return '"' + th.textContent.trim().replace(/"/g, '""') + '"'; }).join(','));
}
table.querySelectorAll('tbody tr').forEach(function(tr) {
var cells = tr.querySelectorAll('td');
rows.push(Array.from(cells).map(function(td) { return '"' + td.textContent.trim().replace(/"/g, '""') + '"'; }).join(','));
});
if (rows.length <= 1) { alert('La tabla esta vacia.'); return; }
var csv = rows.join('\n');
var blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'reporte_nexus_' + new Date().toISOString().slice(0, 10) + '.csv';
a.click();
URL.revokeObjectURL(url);
}