carritoIA/templates/estadisticas.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 — CarritoIA</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>