- Cleaned 137+ fake engine-displacement models from supplier imports (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.) - Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs, empty names, trailing-year variants) - Migrated supplier tables to master DB (supplier_catalog, supplier_catalog_compat, supplier_catalog_interchange) - Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from master DB so supplier-only vehicles appear for all tenants - Added fuzzy model matcher with parenthesis stripping, noise suffix removal, compact matching, prefix/substring fallback, model aliases, and ±3 year proximity - Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500, LUK +477, RAYBESTOS +1,743 - Added KNADIAN catalog importer with year-range expansion and future-year filtering - Added VAZLO catalog importer with position parsing and SKU-in-model cleanup - Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers - Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*, nexus:brand_mye_counts:*) Final match rates: - KEEP GREEN: 90.3% - VAZLO: 93.6% - YOKOMITSU: 100.0% - KNADIAN: 57.4% - LUK: 51.0% - RAYBESTOS: 55.9%
132 lines
3.9 KiB
JavaScript
132 lines
3.9 KiB
JavaScript
/**
|
|
* 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;
|
|
|
|
let hourlyChart = null;
|
|
let topProductsChart = null;
|
|
|
|
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;
|
|
if (hourlyChart) { hourlyChart.destroy(); hourlyChart = null; }
|
|
const labels = hourly.map(function (h) { return h.hour + ':00'; });
|
|
const totals = hourly.map(function (h) { return h.total; });
|
|
|
|
hourlyChart = 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;
|
|
if (topProductsChart) { topProductsChart.destroy(); topProductsChart = null; }
|
|
if (!topProducts || topProducts.length === 0) {
|
|
// No sales today — render a friendly empty-state mini chart so the canvas
|
|
// doesn't collapse or leave a blank hole.
|
|
topProductsChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Sin ventas hoy'],
|
|
datasets: [{
|
|
data: [1],
|
|
backgroundColor: ['rgba(136, 136, 136, 0.25)'],
|
|
borderWidth: 0,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: { color: '#888', font: { size: 10 }, boxWidth: 10 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const labels = topProducts.map(function (p) { return p.name.substring(0, 20); });
|
|
const revenues = topProducts.map(function (p) { return p.revenue; });
|
|
|
|
topProductsChart = 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();
|
|
}
|
|
})();
|