Add dashboard with recent invoices, top suppliers, and summary stats
Clicking a supplier navigates to facturas filtered by that provider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ const { articulosRouter } = require('./routes/articulos');
|
||||
const { catalogoRouter } = require('./routes/catalogo');
|
||||
const { configRouter } = require('./routes/config');
|
||||
const { printRouter } = require('./routes/print');
|
||||
const { dashboardRouter } = require('./routes/dashboard');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -20,6 +21,7 @@ app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// API Routes
|
||||
app.use('/api/dashboard', dashboardRouter(db));
|
||||
app.use('/api/facturas', facturasRouter(db));
|
||||
app.use('/api/articulos', articulosRouter(db));
|
||||
app.use('/api/catalogo', catalogoRouter(db));
|
||||
|
||||
@@ -686,6 +686,107 @@ input[type="checkbox"] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ---- Dashboard ---- */
|
||||
.dashboard-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card-accent {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-card-accent .stat-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stat-card-accent .stat-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.proveedores-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.proveedor-chip {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.proveedor-chip:hover {
|
||||
background: var(--primary-light);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.proveedor-nombre {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.proveedor-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.proveedor-count {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.proveedor-monto {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
@@ -761,6 +862,14 @@ input[type="checkbox"] {
|
||||
.search-bar {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
<h1 class="sidebar-title">Portal Refaccionaria</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#dashboard" class="nav-link" data-route="dashboard">
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span class="nav-text">Inicio</span>
|
||||
</a>
|
||||
<a href="#facturas" class="nav-link" data-route="facturas">
|
||||
<span class="nav-icon">📄</span>
|
||||
<span class="nav-text">Facturas</span>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
// ---- Router ----
|
||||
function router() {
|
||||
var hash = window.location.hash || '#facturas';
|
||||
var hash = window.location.hash || '#dashboard';
|
||||
var parts = hash.substring(1).split('/');
|
||||
var route = parts[0];
|
||||
var param = parts[1] || null;
|
||||
@@ -96,8 +96,11 @@
|
||||
showLoading();
|
||||
|
||||
switch (route) {
|
||||
case 'dashboard':
|
||||
renderDashboard();
|
||||
break;
|
||||
case 'facturas':
|
||||
renderFacturas();
|
||||
renderFacturas(parts[1] === 'proveedor' ? decodeURIComponent(parts[2] || '') : null);
|
||||
break;
|
||||
case 'factura':
|
||||
renderFacturaDetail(param);
|
||||
@@ -112,14 +115,148 @@
|
||||
renderConfig();
|
||||
break;
|
||||
default:
|
||||
renderFacturas();
|
||||
renderDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VIEW: Dashboard
|
||||
// ============================================
|
||||
async function renderDashboard() {
|
||||
try {
|
||||
var data = await fetchJSON('/api/dashboard');
|
||||
var stats = data.stats;
|
||||
|
||||
// Stats cards
|
||||
var statsHtml =
|
||||
'<div class="dashboard-stats">' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-value">' + stats.total_facturas + '</div>' +
|
||||
'<div class="stat-label">Facturas</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-value">' + stats.total_proveedores + '</div>' +
|
||||
'<div class="stat-label">Proveedores</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card">' +
|
||||
'<div class="stat-value">' + stats.total_articulos + '</div>' +
|
||||
'<div class="stat-label">Art\u00edculos</div>' +
|
||||
'</div>' +
|
||||
'<div class="stat-card stat-card-accent">' +
|
||||
'<div class="stat-value">' + currencyFmt.format(stats.monto_total) + '</div>' +
|
||||
'<div class="stat-label">Monto Total</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Recent invoices
|
||||
var recientesHtml = '';
|
||||
if (data.recientes.length) {
|
||||
var rows = data.recientes.map(function (f) {
|
||||
return '<tr class="clickable-row" data-id="' + f.id + '">' +
|
||||
'<td>' + escapeHtml(formatDate(f.fecha)) + '</td>' +
|
||||
'<td><code>' + escapeHtml(truncateUUID(f.uuid)) + '</code></td>' +
|
||||
'<td>' + escapeHtml(f.nombre_emisor) + '</td>' +
|
||||
'<td class="currency">' + currencyFmt.format(f.total || 0) + '</td>' +
|
||||
'<td class="text-center">' + (f.num_conceptos || 0) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
|
||||
recientesHtml =
|
||||
'<div class="card">' +
|
||||
'<div class="card-body">' +
|
||||
'<h3>Facturas Recientes</h3>' +
|
||||
'<p class="text-muted" style="margin-top:4px">Facturas del \u00faltimo d\u00eda registrado</p>' +
|
||||
'</div>' +
|
||||
'<div class="table-container">' +
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th>Fecha</th>' +
|
||||
'<th>UUID</th>' +
|
||||
'<th>Emisor</th>' +
|
||||
'<th class="text-right">Total</th>' +
|
||||
'<th class="text-center"># Art.</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
} else {
|
||||
recientesHtml =
|
||||
'<div class="card">' +
|
||||
'<div class="card-body">' +
|
||||
'<h3>Facturas Recientes</h3>' +
|
||||
'</div>' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon">📄</div>' +
|
||||
'<p>No hay facturas importadas a\u00fan. Coloca archivos XML en la carpeta configurada.</p>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Top suppliers
|
||||
var proveedoresHtml = '';
|
||||
if (data.proveedores.length) {
|
||||
var chips = data.proveedores.map(function (p) {
|
||||
return '<div class="proveedor-chip" data-nombre="' + escapeHtml(p.nombre_emisor) + '">' +
|
||||
'<div class="proveedor-nombre">' + escapeHtml(p.nombre_emisor) + '</div>' +
|
||||
'<div class="proveedor-stats">' +
|
||||
'<span class="proveedor-count">' + p.total_facturas + ' factura' + (p.total_facturas !== 1 ? 's' : '') + '</span>' +
|
||||
'<span class="proveedor-monto">' + currencyFmt.format(p.monto_total || 0) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
proveedoresHtml =
|
||||
'<div class="card">' +
|
||||
'<div class="card-body">' +
|
||||
'<h3>Proveedores Principales</h3>' +
|
||||
'<p class="text-muted" style="margin-top:4px">Haz clic en un proveedor para ver sus facturas</p>' +
|
||||
'</div>' +
|
||||
'<div class="card-body" style="padding-top:0">' +
|
||||
'<div class="proveedores-grid">' + chips + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
contentEl.innerHTML =
|
||||
'<div class="page-header">' +
|
||||
'<h2>Dashboard</h2>' +
|
||||
'<p>Resumen general del portal de facturaci\u00f3n</p>' +
|
||||
'</div>' +
|
||||
statsHtml +
|
||||
'<div class="dashboard-grid">' +
|
||||
recientesHtml +
|
||||
proveedoresHtml +
|
||||
'</div>';
|
||||
|
||||
// Wire click on recent invoices
|
||||
contentEl.querySelectorAll('.clickable-row').forEach(function (row) {
|
||||
row.addEventListener('click', function () {
|
||||
window.location.hash = '#factura/' + row.getAttribute('data-id');
|
||||
});
|
||||
});
|
||||
|
||||
// Wire click on supplier chips -> navigate to facturas filtered by supplier
|
||||
contentEl.querySelectorAll('.proveedor-chip').forEach(function (chip) {
|
||||
chip.addEventListener('click', function () {
|
||||
var nombre = chip.getAttribute('data-nombre');
|
||||
window.location.hash = '#facturas/proveedor/' + encodeURIComponent(nombre);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
contentEl.innerHTML =
|
||||
'<div class="page-header"><h2>Dashboard</h2></div>' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon">⚠️</div>' +
|
||||
'<p>Error al cargar el dashboard: ' + escapeHtml(err.message) + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VIEW: Facturas List
|
||||
// ============================================
|
||||
async function renderFacturas() {
|
||||
async function renderFacturas(proveedorFilter) {
|
||||
contentEl.innerHTML =
|
||||
'<div class="page-header">' +
|
||||
'<h2>Facturas</h2>' +
|
||||
@@ -153,6 +290,11 @@
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Pre-fill supplier filter if coming from dashboard
|
||||
if (proveedorFilter) {
|
||||
document.getElementById('f-proveedor').value = proveedorFilter;
|
||||
}
|
||||
|
||||
// Wire up filter button and enter key
|
||||
document.getElementById('f-btn-buscar').addEventListener('click', loadFacturas);
|
||||
['f-fecha-inicio', 'f-fecha-fin', 'f-proveedor', 'f-buscar'].forEach(function (id) {
|
||||
@@ -791,7 +933,7 @@
|
||||
window.addEventListener('hashchange', router);
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
if (!window.location.hash) {
|
||||
window.location.hash = '#facturas';
|
||||
window.location.hash = '#dashboard';
|
||||
} else {
|
||||
router();
|
||||
}
|
||||
|
||||
57
src/routes/dashboard.js
Normal file
57
src/routes/dashboard.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const { Router } = require('express');
|
||||
|
||||
function dashboardRouter(db) {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
// Recent invoices: last day based on most recent invoice date in DB
|
||||
const latest = db.prepare('SELECT MAX(fecha) as max_fecha FROM facturas').get();
|
||||
let recientes = [];
|
||||
if (latest && latest.max_fecha) {
|
||||
const maxDate = latest.max_fecha.substring(0, 10);
|
||||
recientes = db.prepare(`
|
||||
SELECT f.*, COUNT(c.id) as num_conceptos
|
||||
FROM facturas f
|
||||
LEFT JOIN conceptos c ON c.factura_id = f.id
|
||||
WHERE f.fecha >= ?
|
||||
GROUP BY f.id
|
||||
ORDER BY f.fecha DESC
|
||||
`).all(maxDate);
|
||||
}
|
||||
|
||||
// Top suppliers by invoice count
|
||||
const proveedores = db.prepare(`
|
||||
SELECT nombre_emisor, rfc_emisor, COUNT(*) as total_facturas,
|
||||
SUM(total) as monto_total
|
||||
FROM facturas
|
||||
GROUP BY rfc_emisor
|
||||
ORDER BY total_facturas DESC
|
||||
LIMIT 10
|
||||
`).all();
|
||||
|
||||
// Summary stats
|
||||
const stats = db.prepare(`
|
||||
SELECT COUNT(*) as total_facturas,
|
||||
COUNT(DISTINCT rfc_emisor) as total_proveedores,
|
||||
COALESCE(SUM(total), 0) as monto_total
|
||||
FROM facturas
|
||||
`).get();
|
||||
|
||||
const totalArticulos = db.prepare('SELECT COUNT(*) as total FROM conceptos').get();
|
||||
|
||||
res.json({
|
||||
recientes,
|
||||
proveedores,
|
||||
stats: {
|
||||
total_facturas: stats.total_facturas,
|
||||
total_proveedores: stats.total_proveedores,
|
||||
monto_total: stats.monto_total,
|
||||
total_articulos: totalArticulos.total
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = { dashboardRouter };
|
||||
Reference in New Issue
Block a user