// /home/Autopartes/pos/static/js/dashboard.js
// Dashboard module: fetches real data from POS APIs
const Dashboard = (() => {
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');
}
/** Today as YYYY-MM-DD */
function todayStr() {
return new Date().toISOString().slice(0, 10);
}
/** Return a date N days ago as YYYY-MM-DD */
function daysAgo(n) {
const d = new Date();
d.setDate(d.getDate() - n);
return d.toISOString().slice(0, 10);
}
const DAY_NAMES_SHORT = ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'];
const MONTH_NAMES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
const DAY_NAMES_LONG = ['Domingo', 'Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado'];
// -------------------------------------------------------------------------
// Theme switcher
// -------------------------------------------------------------------------
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('nexus-theme', theme); } catch(e) {}
const btnInd = document.getElementById('btn-industrial');
const btnMod = document.getElementById('btn-modern');
if (btnInd) btnInd.classList.toggle('active', theme === 'industrial');
if (btnMod) btnMod.classList.toggle('active', theme === 'modern');
}
window.setTheme = setTheme;
// -------------------------------------------------------------------------
// Sidebar toggle (mobile)
// -------------------------------------------------------------------------
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
if (!sidebar) return;
const isOpen = sidebar.classList.contains('open');
sidebar.classList.toggle('open', !isOpen);
if (overlay) overlay.classList.toggle('open', !isOpen);
document.body.style.overflow = isOpen ? '' : 'hidden';
}
window.toggleSidebar = toggleSidebar;
function closeSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
if (sidebar) sidebar.classList.remove('open');
if (overlay) overlay.classList.remove('open');
document.body.style.overflow = '';
}
window.closeSidebar = closeSidebar;
window.addEventListener('resize', function() {
if (window.innerWidth >= 768) closeSidebar();
});
// -------------------------------------------------------------------------
// Period selector (placeholder for future use)
// -------------------------------------------------------------------------
function setPeriod(btn) {
btn.closest('.period-selector').querySelectorAll('.period-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
}
window.setPeriod = setPeriod;
// -------------------------------------------------------------------------
// Generic fetch helper
// -------------------------------------------------------------------------
async function apiFetch(url) {
try {
const resp = await fetch(url, { headers: headers() });
if (resp.status === 401) {
window.location.href = '/pos/login';
return null;
}
if (!resp.ok) return null;
return await resp.json();
} catch (err) {
console.error('Dashboard fetch error:', url, err);
return null;
}
}
// -------------------------------------------------------------------------
// Set header greeting with real date and user info
// -------------------------------------------------------------------------
function setGreeting() {
const now = new Date();
const h = now.getHours();
let saludo = 'Buenos dias';
if (h >= 12 && h < 19) saludo = 'Buenas tardes';
else if (h >= 19) saludo = 'Buenas noches';
// Try to get user name from JWT payload
let userName = '';
try {
const payload = JSON.parse(atob(token().split('.')[1]));
userName = payload.name || payload.sub || '';
} catch(e) {}
const greetEl = document.getElementById('header-greeting');
if (greetEl) greetEl.textContent = userName ? `${saludo}, ${userName}` : saludo;
const subEl = document.getElementById('header-subtitle');
if (subEl) {
const dayName = DAY_NAMES_LONG[now.getDay()];
const day = now.getDate();
const month = MONTH_NAMES[now.getMonth()];
const year = now.getFullYear();
subEl.textContent = `${dayName}, ${day} de ${month} de ${year}`;
}
}
// -------------------------------------------------------------------------
// 1. Ventas de hoy KPIs (daily summary)
// -------------------------------------------------------------------------
async function loadDailySummary() {
const today = todayStr();
const data = await apiFetch(`/pos/api/register/daily-summary?date=${today}`);
if (!data) {
setKpiError('kpi-ventas-total', 'kpi-ventas-meta');
setKpiError('kpi-tickets-count', 'kpi-tickets-meta');
setKpiError('kpi-promedio-value', 'kpi-promedio-meta');
return null;
}
// Ventas Hoy
const totalEl = document.getElementById('kpi-ventas-total');
const metaEl = document.getElementById('kpi-ventas-meta');
if (totalEl) totalEl.textContent = fmt(data.total_sales);
if (metaEl) {
const methods = Object.entries(data.sales_by_method || {})
.map(([m, v]) => `${capitalize(m)}: ${fmt(v.amount)}`)
.join(' | ');
metaEl.innerHTML = methods
? `${methods}`
: 'Sin ventas aun';
}
// Tickets Hoy
const ticketsEl = document.getElementById('kpi-tickets-count');
const ticketsMetaEl = document.getElementById('kpi-tickets-meta');
if (ticketsEl) ticketsEl.textContent = fmtInt(data.total_sales_count);
if (ticketsMetaEl) {
const cancelled = data.cancelled_count || 0;
ticketsMetaEl.innerHTML = cancelled > 0
? `${cancelled} cancelada${cancelled > 1 ? 's' : ''}de ${data.total_sales_count + cancelled} totales`
: `${data.total_sales_count} completada${data.total_sales_count !== 1 ? 's' : ''}`;
}
// Ticket Promedio
const promedioEl = document.getElementById('kpi-promedio-value');
const promedioMetaEl = document.getElementById('kpi-promedio-meta');
const avg = data.total_sales_count > 0 ? data.total_sales / data.total_sales_count : 0;
if (promedioEl) promedioEl.textContent = fmt(avg);
if (promedioMetaEl) {
promedioMetaEl.innerHTML = `sobre ${fmtInt(data.total_sales_count)} ventas`;
}
// Cajas del Día KPI
const cajasCountEl = document.getElementById('kpi-cajas-count');
const cajasMetaEl = document.getElementById('kpi-cajas-meta');
const regs = data.registers || [];
const activeRegs = regs.filter(r => r.status === 'open');
const closedRegs = regs.filter(r => r.status === 'closed');
if (cajasCountEl) cajasCountEl.textContent = regs.length;
if (cajasMetaEl) {
cajasMetaEl.innerHTML = `${activeRegs.length} abierta${activeRegs.length !== 1 ? 's' : ''}${closedRegs.length} cerrada${closedRegs.length !== 1 ? 's' : ''}`;
}
// Also populate registers list
renderRegisters(regs);
return data;
}
function setKpiError(valueId, metaId) {
const v = document.getElementById(valueId);
const m = document.getElementById(metaId);
if (v) v.textContent = '--';
if (m) m.innerHTML = 'Error al cargar';
}
function capitalize(s) {
if (!s) return '';
return s.charAt(0).toUpperCase() + s.slice(1);
}
// -------------------------------------------------------------------------
// 2. Registers list
// -------------------------------------------------------------------------
function renderRegisters(registers) {
const container = document.getElementById('registers-list');
if (!container) return;
if (!registers || registers.length === 0) {
container.innerHTML = '
Sin cajas registradas hoy
';
return;
}
const maxSale = Math.max(...registers.map(r => r.sale_total || 0), 1);
container.innerHTML = registers.map((r, i) => {
const statusClass = r.status === 'open' ? 'kpi-tag--up' : 'kpi-tag--neutral';
const statusLabel = r.status === 'open' ? 'Abierta' : 'Cerrada';
const pct = maxSale > 0 ? Math.round((r.sale_total / maxSale) * 100) : 0;
return `
${i + 1}
Caja ${r.register_number || r.id}
${r.employee_name || 'Sin cajero'} · ${r.sale_count || 0} ventas ${statusLabel}
${fmt(r.sale_total)}
`;
}).join('');
}
// -------------------------------------------------------------------------
// 3. Inventory alerts
// -------------------------------------------------------------------------
async function loadAlerts() {
const data = await apiFetch('/pos/api/inventory/alerts');
const container = document.getElementById('alerts-grid');
if (!container) return;
if (!data || !data.data || data.data.length === 0) {
container.innerHTML = `
Sin alertas
Todo el inventario dentro de parametros normales.
`;
return;
}
const alerts = data.data;
// Group by type
const zero = alerts.filter(a => a.type === 'zero');
const low = alerts.filter(a => a.type === 'low');
const over = alerts.filter(a => a.type === 'over');
let html = '';
if (zero.length > 0) {
const items = zero.slice(0, 5).map(a => `${a.name}: ${a.stock} uds`).join(' · ');
html += buildAlertCard('error', 'Stock Agotado', `${zero.length} items`, items,
'');
}
if (low.length > 0) {
const items = low.slice(0, 5).map(a => `${a.name}: ${a.stock} uds (min ${a.min_stock})`).join(' · ');
html += buildAlertCard('warning', 'Stock Bajo', `${low.length} items`, items,
'');
}
if (over.length > 0) {
const items = over.slice(0, 5).map(a => `${a.name}: ${a.stock} uds (max ${a.max_stock})`).join(' · ');
html += buildAlertCard('orange', 'Sobrestock', `${over.length} items`, items,
'');
}
container.innerHTML = html || 'Sin alertas
';
}
function buildAlertCard(severity, title, badge, desc, iconSvg) {
return `
${iconSvg}
${title}
${badge}
${desc}
Ver
`;
}
// -------------------------------------------------------------------------
// 4. Top Products (from today's sales detail)
// -------------------------------------------------------------------------
async function loadTopProducts() {
const today = todayStr();
// Fetch all today's sales with pagination
const data = await apiFetch(`/pos/api/sales?date_from=${today}&date_to=${today}&status=completed&per_page=200`);
const container = document.getElementById('top-products-list');
if (!container) return;
if (!data || !data.data || data.data.length === 0) {
container.innerHTML = 'Sin ventas hoy
';
return;
}
// Fetch detail for each sale to get items (up to 20 sales for performance)
const salesToFetch = data.data.slice(0, 20);
const details = await Promise.all(
salesToFetch.map(s => apiFetch(`/pos/api/sales/${s.id}`))
);
// Aggregate items
const productMap = {};
for (const sale of details) {
if (!sale || !sale.items) continue;
for (const item of sale.items) {
const key = item.part_number || item.name;
if (!productMap[key]) {
productMap[key] = { name: item.name, part_number: item.part_number || '', qty: 0, revenue: 0 };
}
productMap[key].qty += item.quantity || 0;
productMap[key].revenue += item.subtotal || 0;
}
}
const sorted = Object.values(productMap).sort((a, b) => b.revenue - a.revenue).slice(0, 5);
if (sorted.length === 0) {
container.innerHTML = 'Sin productos vendidos
';
return;
}
const maxRev = sorted[0].revenue || 1;
container.innerHTML = sorted.map((p, i) => {
const pct = Math.round((p.revenue / maxRev) * 100);
return `
${i + 1}
${escHtml(p.name)}
${escHtml(p.part_number)} · ${p.qty} pzas vendidas
${fmt(p.revenue)}
`;
}).join('');
}
function escHtml(s) {
const div = document.createElement('div');
div.textContent = s || '';
return div.innerHTML;
}
// -------------------------------------------------------------------------
// 5. Weekly bar chart (last 7 days)
// -------------------------------------------------------------------------
async function loadWeeklyChart() {
const chartEl = document.getElementById('bar-chart');
const totalEl = document.getElementById('chart-week-total');
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));
}
const summaries = await Promise.all(
days.map(d => apiFetch(`/pos/api/register/daily-summary?date=${d}`))
);
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(),
};
});
// Update week total
if (totalEl) {
totalEl.innerHTML = `Total semana: ${fmt(weekTotal)}`;
}
const maxVal = Math.max(...dayData.map(d => d.total), 1);
chartEl.innerHTML = dayData.map(d => {
const heightPct = Math.max(Math.round((d.total / maxVal) * 100), 2);
const todayClass = d.isToday ? ' today' : '';
const todayLabelStyle = d.isToday ? ' style="color:var(--color-primary);font-weight:700;"' : '';
const tooltip = d.isToday ? `${fmt(d.total)} ← Hoy` : fmt(d.total);
return `
`;
}).join('');
}
// -------------------------------------------------------------------------
// 6. Recent sales table
// -------------------------------------------------------------------------
async function loadRecentSales() {
const today = todayStr();
const data = await apiFetch(`/pos/api/sales?date_from=${today}&date_to=${today}&per_page=10`);
const tbody = document.getElementById('recent-sales-tbody');
if (!tbody) return;
if (!data || !data.data || data.data.length === 0) {
tbody.innerHTML = '| Sin ventas hoy |
';
return;
}
// Fetch items for first 5 sales
const salesToShow = data.data.slice(0, 5);
const details = await Promise.all(
salesToShow.map(s => apiFetch(`/pos/api/sales/${s.id}`))
);
tbody.innerHTML = salesToShow.map((sale, idx) => {
const detail = details[idx];
const time = sale.created_at ? sale.created_at.slice(11, 16) : '--:--';
const client = sale.customer_name || 'Publico General';
const total = sale.total || 0;
const method = sale.payment_method || 'efectivo';
// Build products summary from detail items
let productsSummary = '';
if (detail && detail.items && detail.items.length > 0) {
productsSummary = detail.items.slice(0, 3).map(it =>
`${escHtml(it.name)}${it.quantity > 1 ? ' (x' + it.quantity + ')' : ''}`
).join(', ');
if (detail.items.length > 3) productsSummary += '...';
}
const methodClass = getPaymentBadgeClass(method);
const methodLabel = capitalize(method);
const statusTag = sale.status === 'cancelled'
? ' [CANCELADA]' : '';
return `
| ${time} |
${escHtml(client)}${statusTag} |
${productsSummary} |
${fmt(total)} |
${methodLabel} |
`;
}).join('');
}
function getPaymentBadgeClass(method) {
const m = (method || '').toLowerCase();
if (m.includes('efectivo') || m === 'cash') return 'efectivo';
if (m.includes('tarjeta') || m === 'card') return 'tarjeta';
if (m.includes('transferencia') || m === 'transfer') return 'transferencia';
if (m.includes('credito') || m === 'credit') return 'credito';
return 'efectivo';
}
// -------------------------------------------------------------------------
// Init
// -------------------------------------------------------------------------
function init() {
if (!checkAuth()) return;
// Restore theme
try {
const saved = localStorage.getItem('nexus-theme');
if (saved === 'industrial' || saved === 'modern') {
setTheme(saved);
}
} catch(e) {}
setGreeting();
// Load all data in parallel
loadDailySummary();
loadAlerts();
loadTopProducts();
loadWeeklyChart();
loadRecentSales();
// Auto-refresh every 2 minutes
setInterval(() => {
loadDailySummary();
loadRecentSales();
}, 120000);
}
document.addEventListener('DOMContentLoaded', init);
return { init, setTheme };
})();