Stress Index: Analytics e Sport rifatti sulle viste reali

Le due sezioni erano ferme al prototipo locale, più povero della
piattaforma vera. Analytics ha ora l'intervallo di date, i KPI con la
nota sotto il valore, la distribuzione dello score a quattro serie per
fascia, le classifiche per recupero medio col numero di misurazioni e il
confronto segmenti con selettore di dimensione. Sport diventa Dashboard,
Sessioni con filtri atleta/periodo/sport e schede atleta con carico e
tendenza.

I numeri non sono più costanti scritte a mano: si calcolano dai dati nel
periodo scelto. La logica sta in lib/stress-index/analytics.ts e sport.ts,
coperta da test — che hanno subito trovato uno slittamento delle settimane
dovuto alle date costruite in ora locale invece che in UTC.

Le sessioni hanno ora ora di inizio, HR max, RPE e durata al secondo, che
il prototipo non prevedeva.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:20:40 +02:00
parent c4eb598a3a
commit 3cda76b4c4
17 changed files with 907 additions and 409 deletions
@@ -1,62 +1,22 @@
---
// Analytics di studio: numeri aggregati sul periodo. Il selettore di periodo è ancora
// dimostrativo (i dati sorgente sono un'istantanea sola), come nel prototipo.
// Analytics di studio. La pagina fornisce i dati e l'intervallo iniziale; il resto — filtri,
// aggregazioni e grafici — è nell'isola, perché ogni numero dipende dai controlli.
import StressIndex from '../../../layouts/StressIndex.astro';
import Card from '../../../components/stress-index/Card.astro';
import Kpi from '../../../components/stress-index/Kpi.astro';
import StatList from '../../../components/stress-index/StatList.astro';
import DataChart from '../../../components/stress-index/charts/DataChart';
import { studioKpis, scoreDistribution, segments, topPerformers, criticalClients } from '../../../lib/stress-index/data';
import AnalyticsView from '../../../components/stress-index/AnalyticsView';
import { clients, measurementsByClient, alerts } from '../../../lib/stress-index/data';
export const prerender = false;
// Mese corrente dei dati dimostrativi.
const DA = '2026-07-01';
const A = '2026-07-31';
---
<StressIndex title="Analytics" crumbs={['Analytics']}>
<div class="si-head si-head--row">
<div>
<h1>Analytics</h1>
<p>Andamento dello studio nel periodo selezionato</p>
</div>
<label class="si-field si-field--inline">
<span class="si-visually-hidden">Intervallo</span>
<select>
<option value="7">Ultimi 7 giorni</option>
<option value="30" selected>Ultimi 30 giorni</option>
<option value="90">Ultimi 90 giorni</option>
<option value="365">Ultimo anno</option>
</select>
</label>
</div>
<div class="si-grid si-grid--kpi">
<Kpi label="Clienti attivi" value={studioKpis.totalClients} />
<Kpi label="Misurazioni nel periodo" value={studioKpis.measurementsThisMonth} />
<Kpi label="Alert attivi" value={studioKpis.activeAlerts} />
<Kpi label="Aderenza" value={`${studioKpis.retentionRate}%`} />
</div>
<div class="si-stack si-mt">
<Card title="Distribuzione dello score dei clienti">
<DataChart client:visible type="bar" data={scoreDistribution} xKey="range" series={[{ key: 'count', label: 'Clienti' }]} />
</Card>
<Card title="Confronto fra segmenti">
<div class="si-grid si-grid--halves">
{segments.map((s) => (
<div class="si-segment">
<p class="si-segment__label">{s.label}</p>
<p class="si-kpi__value">{s.avgStress}</p>
<p class="si-muted si-small">stress medio · {s.count} {s.count === 1 ? 'cliente' : 'clienti'}</p>
</div>
))}
</div>
</Card>
<div class="si-grid si-grid--halves">
<Card title="Andamento migliore">
<StatList items={topPerformers.map((c) => ({ label: c.name, value: c.stress }))} />
</Card>
<Card title="Clienti critici">
<StatList items={criticalClients.map((c) => ({ label: c.name, value: c.stress }))} />
</Card>
</div>
</div>
<AnalyticsView
client:load
clients={clients}
measurementsByClient={measurementsByClient}
alerts={alerts}
from={DA}
to={A}
/>
</StressIndex>
@@ -1,121 +1,13 @@
---
// Modulo Sport: carico di lavoro e sessioni degli atleti seguiti. Le tre viste sono già
// tutte renderizzate: il cambio scheda è solo un mostra/nascondi, non serve un'isola.
// Modulo Sport. La pagina passa i dati; filtri, schede e aggregazioni stanno nell'isola.
import StressIndex from '../../../../layouts/StressIndex.astro';
import Card from '../../../../components/stress-index/Card.astro';
import Kpi from '../../../../components/stress-index/Kpi.astro';
import DataChart from '../../../../components/stress-index/charts/DataChart';
import { sessions, athletes, sessionsPerWeek } from '../../../../lib/stress-index/data';
import SportView from '../../../../components/stress-index/SportView';
import { sessions, athletes } from '../../../../lib/stress-index/data';
export const prerender = false;
const trimpMedio = Math.round(sessions.reduce((s, x) => s + x.trimp, 0) / sessions.length);
const sessioniSettimana = sessionsPerWeek[sessionsPerWeek.length - 1]?.count ?? 0;
const nomeAtleta = (id: string) => athletes.find((a) => a.clientId === id)?.name ?? id;
const TABS = [
{ key: 'dashboard', label: 'Dashboard' },
{ key: 'sessioni', label: 'Sessioni' },
{ key: 'atleti', label: 'Atleti' },
];
// Ultimo giorno coperto dai dati dimostrativi.
const RIFERIMENTO = '2026-07-12';
---
<StressIndex title="Sport" crumbs={['Sport']}>
<div class="si-head">
<h1>Sport</h1>
<p>Sessioni, carico e monitoraggio degli atleti</p>
</div>
<div class="si-tabs" role="tablist" id="si-sport-tabs">
{TABS.map((t, i) => (
<button type="button" role="tab" class="si-tabs__btn" data-tab={t.key} aria-selected={i === 0 ? 'true' : 'false'}>{t.label}</button>
))}
</div>
<section data-panel="dashboard" class="si-stack">
<div class="si-grid si-grid--kpi">
<Kpi label="Sessioni totali" value={sessions.length} />
<Kpi label="Sessioni questa settimana" value={sessioniSettimana} />
<Kpi label="TRIMP medio" value={trimpMedio} />
<Kpi label="Atleti attivi" value={athletes.length} />
</div>
<Card title="Sessioni per settimana">
<DataChart client:visible type="bar" data={sessionsPerWeek} xKey="week" series={[{ key: 'count', label: 'Sessioni' }]} />
</Card>
<Card title="Ultime sessioni">
<div class="si-scroll-x">
<table class="si-table">
<thead><tr><th>Atleta</th><th>Data</th><th>Sport</th><th>Durata</th><th>TRIMP</th><th>HR medio</th><th>DFA A1</th></tr></thead>
<tbody>
{sessions.map((s) => (
<tr>
<td><strong>{nomeAtleta(s.athleteId)}</strong></td>
<td class="si-table__num">{s.date}</td>
<td>{s.type}</td>
<td class="si-table__num">{s.durationMin} min</td>
<td class="si-table__num">{s.trimp}</td>
<td class="si-table__num">{s.hrAvg}</td>
<td class="si-table__num">{s.dfaA1}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</section>
<section data-panel="sessioni" hidden>
<Card title="Tutte le sessioni">
<div class="si-scroll-x">
<table class="si-table">
<thead><tr><th>Atleta</th><th>Data</th><th>Sport</th><th>Durata</th><th>Carico</th><th>TRIMP</th><th>HR medio</th><th>DFA A1</th></tr></thead>
<tbody>
{sessions.map((s) => (
<tr>
<td><strong>{nomeAtleta(s.athleteId)}</strong></td>
<td class="si-table__num">{s.date}</td>
<td>{s.type}</td>
<td class="si-table__num">{s.durationMin} min</td>
<td class="si-table__num">{s.load}</td>
<td class="si-table__num">{s.trimp}</td>
<td class="si-table__num">{s.hrAvg}</td>
<td class="si-table__num">{s.dfaA1}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</section>
<section data-panel="atleti" hidden>
<Card title="Atleti monitorati">
<table class="si-table">
<thead><tr><th>Atleta</th><th>Sport</th><th>Carico settimanale</th></tr></thead>
<tbody>
{athletes.map((a) => (
<tr>
<td><a href={`/piattaforme/stress-index/clienti/${a.clientId}`}>{a.name}</a></td>
<td>{a.sport}</td>
<td class="si-table__num">{a.weeklyLoad}</td>
</tr>
))}
</tbody>
</table>
</Card>
</section>
<SportView client:load sessions={sessions} athletes={athletes} riferimento={RIFERIMENTO} />
</StressIndex>
<script>
// Schede: un solo pannello visibile alla volta, selezione riflessa su aria-selected.
const tabs = document.getElementById('si-sport-tabs');
if (tabs) {
const buttons = Array.from(tabs.querySelectorAll<HTMLButtonElement>('.si-tabs__btn'));
const panels = Array.from(document.querySelectorAll<HTMLElement>('[data-panel]'));
for (const btn of buttons) {
btn.addEventListener('click', () => {
for (const b of buttons) b.setAttribute('aria-selected', String(b === btn));
for (const p of panels) p.hidden = p.dataset.panel !== btn.dataset.tab;
});
}
}
</script>