feat(dashboard): add real-time in-app stats with Chart.js

- dashboard_stats_bp.py: endpoints /pos/api/dashboard/stats and
  /pos/api/dashboard/stats/employees (sales today/month, hourly,
  top products, employee productivity)
- dashboard-stats.js: renders hourly sales bar chart and top products
  doughnut chart using Chart.js
- chart.umd.min.js: vendored Chart.js v4.4.2
This commit is contained in:
2026-04-29 06:30:54 +00:00
parent c4db5e7550
commit 12989e30be
3 changed files with 226 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
/**
* dashboard-stats.js — In-app real-time charts using Chart.js
* Fetches /pos/api/dashboard/stats and renders hourly + top-products charts.
*/
(function () {
'use strict';
const token = localStorage.getItem('pos_token') || '';
if (!token) return;
function headers() {
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
}
function fmt(n) {
return '$' + parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
async function loadStats() {
try {
const res = await fetch('/pos/api/dashboard/stats', { headers: headers() });
if (!res.ok) return;
const data = await res.json();
renderHourlyChart(data.hourly_sales || []);
renderTopProductsChart(data.top_products || []);
} catch (e) {
console.error('[dashboard-stats] failed to load', e);
}
}
function renderHourlyChart(hourly) {
const ctx = document.getElementById('hourlySalesChart');
if (!ctx) return;
const labels = hourly.map(function (h) { return h.hour + ':00'; });
const totals = hourly.map(function (h) { return h.total; });
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Ventas ($)',
data: totals,
backgroundColor: 'rgba(245, 166, 35, 0.7)',
borderColor: 'rgba(245, 166, 35, 1)',
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#888', font: { size: 10 } }, grid: { display: false } },
y: { ticks: { color: '#888', font: { size: 10 }, callback: function (v) { return '$' + (v / 1000).toFixed(0) + 'k'; } }, grid: { color: '#333' } }
}
}
});
}
function renderTopProductsChart(topProducts) {
const ctx = document.getElementById('topProductsChart');
if (!ctx) return;
const labels = topProducts.map(function (p) { return p.name.substring(0, 20); });
const revenues = topProducts.map(function (p) { return p.revenue; });
new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: revenues,
backgroundColor: [
'#F5A623', '#E85D75', '#4ECDC4', '#556270', '#C7F464',
'#FF6B6B', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD'
],
borderWidth: 0,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#ccc', font: { size: 10 }, boxWidth: 10 }
}
}
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadStats);
} else {
loadStats();
}
})();