// /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 `
${d.label}
`; }).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 }; })();