diff --git a/pos/static/js/dashboard.js b/pos/static/js/dashboard.js
index 746b910..79611a9 100644
--- a/pos/static/js/dashboard.js
+++ b/pos/static/js/dashboard.js
@@ -1,5 +1,5 @@
// /home/Autopartes/pos/static/js/dashboard.js
-// Dashboard module: KPIs, charts, summary data
+// Dashboard module: fetches real data from POS APIs
const Dashboard = (() => {
function token() {
@@ -19,9 +19,30 @@ const Dashboard = (() => {
}
function fmt(n) {
- return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ 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
// -------------------------------------------------------------------------
@@ -63,7 +84,7 @@ const Dashboard = (() => {
});
// -------------------------------------------------------------------------
- // Period selector
+ // Period selector (placeholder for future use)
// -------------------------------------------------------------------------
function setPeriod(btn) {
btn.closest('.period-selector').querySelectorAll('.period-btn').forEach(function(b) {
@@ -74,18 +95,405 @@ const Dashboard = (() => {
window.setPeriod = setPeriod;
// -------------------------------------------------------------------------
- // Placeholder API calls
+ // Generic fetch helper
// -------------------------------------------------------------------------
- async function loadSalesSummary() {
- // TODO: call /pos/api/cashregister/summary or similar
+ 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() {
- // TODO: call /pos/api/inventory/products?sort=sold
+ 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() {
- // TODO: call /pos/api/cashregister/recent
+ 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';
}
// -------------------------------------------------------------------------
@@ -102,12 +510,23 @@ const Dashboard = (() => {
}
} catch(e) {}
- loadSalesSummary();
+ 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, loadSalesSummary, loadTopProducts, loadRecentSales, setTheme };
+ return { init, setTheme };
})();
diff --git a/pos/templates/dashboard.html b/pos/templates/dashboard.html
index f28fe29..299bfd9 100644
--- a/pos/templates/dashboard.html
+++ b/pos/templates/dashboard.html
@@ -1422,8 +1422,8 @@