330 lines
11 KiB
HTML
330 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Estadísticas — Lista de la Compra</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
<style>
|
|
/* ── Tarjetas métricas ─────────────────────────────────────────── */
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: .75rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.metric-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: .6rem;
|
|
padding: .9rem 1rem;
|
|
text-align: center;
|
|
}
|
|
.metric-card .metric-val {
|
|
font-size: 1.6rem;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
line-height: 1.1;
|
|
}
|
|
.metric-card .metric-lbl {
|
|
font-size: .72rem;
|
|
color: var(--text-muted);
|
|
margin-top: .25rem;
|
|
}
|
|
|
|
/* ── Gráfico de barras ─────────────────────────────────────────── */
|
|
.chart-section {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: .6rem;
|
|
padding: 1rem 1.25rem 1.25rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.chart-section h2 {
|
|
margin: 0 0 .9rem;
|
|
font-size: .95rem;
|
|
}
|
|
.bar-chart {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 4px;
|
|
height: 130px;
|
|
overflow-x: auto;
|
|
padding-bottom: 1.6rem; /* espacio para etiquetas */
|
|
position: relative;
|
|
}
|
|
.bar-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
flex: 1;
|
|
min-width: 28px;
|
|
height: 100%;
|
|
justify-content: flex-end;
|
|
position: relative;
|
|
}
|
|
.bar-fill {
|
|
width: 100%;
|
|
background: var(--accent);
|
|
border-radius: 3px 3px 0 0;
|
|
opacity: .85;
|
|
transition: opacity .15s;
|
|
cursor: default;
|
|
}
|
|
.bar-fill:hover { opacity: 1; }
|
|
.bar-col .bar-label {
|
|
position: absolute;
|
|
bottom: -1.4rem;
|
|
font-size: .6rem;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
transform: rotate(-40deg);
|
|
transform-origin: top left;
|
|
left: 50%;
|
|
}
|
|
.bar-col .bar-tooltip {
|
|
position: absolute;
|
|
bottom: calc(100% + 4px);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0,0,0,.75);
|
|
color: #fff;
|
|
font-size: .65rem;
|
|
padding: 2px 5px;
|
|
border-radius: 3px;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity .1s;
|
|
}
|
|
.bar-col:hover .bar-tooltip { opacity: 1; }
|
|
|
|
/* ── Tablas top productos ──────────────────────────────────────── */
|
|
.tables-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: .75rem;
|
|
}
|
|
.table-section {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: .6rem;
|
|
padding: 1rem 1.25rem;
|
|
}
|
|
.table-section h2 {
|
|
margin: 0 0 .75rem;
|
|
font-size: .95rem;
|
|
}
|
|
.top-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: .78rem;
|
|
}
|
|
.top-table th {
|
|
text-align: left;
|
|
color: var(--text-muted);
|
|
font-weight: 500;
|
|
padding-bottom: .4rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.top-table td {
|
|
padding: .35rem 0;
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: middle;
|
|
}
|
|
.top-table tr:last-child td { border-bottom: none; }
|
|
.top-table .rank {
|
|
color: var(--text-muted);
|
|
font-size: .7rem;
|
|
width: 1.4rem;
|
|
}
|
|
.top-table .prod-name {
|
|
max-width: 160px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.top-table .num {
|
|
text-align: right;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
padding-left: .5rem;
|
|
}
|
|
.bar-inline {
|
|
height: 5px;
|
|
background: var(--accent);
|
|
border-radius: 3px;
|
|
opacity: .5;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ── Loading / error ───────────────────────────────────────────── */
|
|
#estado-carga { color: var(--text-muted); font-size: .85rem; margin: 2rem 0; text-align: center; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:.25rem;">
|
|
<h1>Estadísticas</h1>
|
|
<div style="display:flex; align-items:center; gap:.75rem;">
|
|
<a href="/" class="btn btn-secondary btn-sm">← Lista de la compra</a>
|
|
<a href="/perfil" class="btn btn-secondary btn-sm">👤 Perfil</a>
|
|
<span style="font-size:.85rem; color:var(--text-muted);">{{ nombre }}</span>
|
|
<a href="/logout" class="btn btn-secondary btn-sm">Salir</a>
|
|
</div>
|
|
</div>
|
|
<p class="subtitle" id="subtitulo">Cargando estadísticas...</p>
|
|
|
|
<div id="estado-carga">⏳ Cargando datos…</div>
|
|
|
|
<div id="contenido" style="display:none;">
|
|
|
|
<!-- Tarjetas métricas -->
|
|
<div class="metrics-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-val" id="m-medio">—</div>
|
|
<div class="metric-lbl">Media mensual</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-val" id="m-actual">—</div>
|
|
<div class="metric-lbl">Mes en curso</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-val" id="m-anterior">—</div>
|
|
<div class="metric-lbl">Mes anterior</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-val" id="m-anual">—</div>
|
|
<div class="metric-lbl">Últimos 12 meses</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-val" id="m-tickets">—</div>
|
|
<div class="metric-lbl">Tickets procesados</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-val" id="m-productos">—</div>
|
|
<div class="metric-lbl">Productos únicos</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gráfico de barras mensual -->
|
|
<div class="chart-section">
|
|
<h2>Gasto mensual (€)</h2>
|
|
<div class="bar-chart" id="barChart"></div>
|
|
</div>
|
|
|
|
<!-- Tablas top -->
|
|
<div class="tables-grid">
|
|
<div class="table-section">
|
|
<h2>Top 10 por gasto total</h2>
|
|
<table class="top-table" id="tablaGasto">
|
|
<thead><tr>
|
|
<th class="rank">#</th>
|
|
<th>Producto</th>
|
|
<th class="num" style="text-align:right">€ total</th>
|
|
<th class="num" style="text-align:right">Veces</th>
|
|
</tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="table-section">
|
|
<h2>Top 10 más comprados</h2>
|
|
<table class="top-table" id="tablaFrecuencia">
|
|
<thead><tr>
|
|
<th class="rank">#</th>
|
|
<th>Producto</th>
|
|
<th class="num" style="text-align:right">Veces</th>
|
|
<th class="num" style="text-align:right">€ total</th>
|
|
</tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- #contenido -->
|
|
|
|
<script>
|
|
const fmt = v => v.toFixed(2).replace('.', ',') + ' €';
|
|
const fmtn = v => v.toFixed(2).replace('.', ',');
|
|
|
|
window.addEventListener('load', async () => {
|
|
try {
|
|
const res = await fetch('/api/estadisticas');
|
|
if (res.status === 401) { location.href = '/login'; return; }
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const d = await res.json();
|
|
|
|
document.getElementById('estado-carga').style.display = 'none';
|
|
document.getElementById('contenido').style.display = 'block';
|
|
document.getElementById('subtitulo').textContent = 'Basado en ' + d.total_tickets + ' tickets';
|
|
|
|
// Tarjetas
|
|
document.getElementById('m-medio').textContent = fmt(d.gasto_medio_mensual);
|
|
document.getElementById('m-actual').textContent = fmt(d.gasto_mes_actual);
|
|
document.getElementById('m-anterior').textContent = fmt(d.gasto_mes_anterior);
|
|
document.getElementById('m-anual').textContent = fmt(d.gasto_anual);
|
|
document.getElementById('m-tickets').textContent = d.total_tickets;
|
|
document.getElementById('m-productos').textContent= d.productos_unicos;
|
|
|
|
// Colorear mes actual si es mayor que la media
|
|
if (d.gasto_mes_actual > d.gasto_medio_mensual * 1.1) {
|
|
document.getElementById('m-actual').style.color = '#f97316';
|
|
} else if (d.gasto_mes_actual > 0 && d.gasto_mes_actual < d.gasto_medio_mensual * 0.9) {
|
|
document.getElementById('m-actual').style.color = '#22c55e';
|
|
}
|
|
|
|
// Gráfico de barras
|
|
const meses = d.meses || [];
|
|
const maxVal = Math.max(...meses.map(m => m.gasto), 1);
|
|
const chart = document.getElementById('barChart');
|
|
chart.innerHTML = '';
|
|
meses.forEach(m => {
|
|
const pct = Math.round((m.gasto / maxVal) * 100);
|
|
const col = document.createElement('div');
|
|
col.className = 'bar-col';
|
|
col.innerHTML =
|
|
'<div class="bar-tooltip">' + fmtn(m.gasto) + ' €</div>' +
|
|
'<div class="bar-fill" style="height:' + pct + '%"></div>' +
|
|
'<span class="bar-label">' + m.label + '</span>';
|
|
chart.appendChild(col);
|
|
});
|
|
|
|
// Tabla gasto
|
|
const tbGasto = document.querySelector('#tablaGasto tbody');
|
|
const maxGasto = d.top_productos_gasto[0]?.gasto_total || 1;
|
|
d.top_productos_gasto.forEach((p, i) => {
|
|
const pct = Math.round((p.gasto_total / maxGasto) * 100);
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML =
|
|
'<td class="rank">' + (i+1) + '</td>' +
|
|
'<td><div class="prod-name" title="' + p.producto + '">' + p.producto + '</div>' +
|
|
' <div class="bar-inline" style="width:' + pct + '%"></div></td>' +
|
|
'<td class="num">' + fmtn(p.gasto_total) + ' €</td>' +
|
|
'<td class="num">' + p.veces + '</td>';
|
|
tbGasto.appendChild(tr);
|
|
});
|
|
|
|
// Tabla frecuencia
|
|
const tbFreq = document.querySelector('#tablaFrecuencia tbody');
|
|
const maxVeces = d.top_productos_frecuencia[0]?.veces_comprado || 1;
|
|
d.top_productos_frecuencia.forEach((p, i) => {
|
|
const pct = Math.round((p.veces_comprado / maxVeces) * 100);
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML =
|
|
'<td class="rank">' + (i+1) + '</td>' +
|
|
'<td><div class="prod-name" title="' + p.producto + '">' + p.producto + '</div>' +
|
|
' <div class="bar-inline" style="width:' + pct + '%"></div></td>' +
|
|
'<td class="num">' + p.veces_comprado + '</td>' +
|
|
'<td class="num">' + fmtn(p.gasto_total) + ' €</td>';
|
|
tbFreq.appendChild(tr);
|
|
});
|
|
|
|
} catch(e) {
|
|
document.getElementById('estado-carga').textContent = '❌ Error al cargar estadísticas: ' + e.message;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|