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
+64 -23
View File
@@ -29,7 +29,7 @@ export const clients: Client[] = [
{ id: 'c10', name: 'V. Costa', initials: 'VC', age: 36, sex: 'F', tags: ['ansia', 'lavoro'], email: 'v.costa@esempio.it', since: '2024-07-01', lastMeasurement: '2026-07-10', stress: 58, alerts: 1 },
];
export interface Measurement {
export type Measurement = {
date: string;
stress: number;
recovery: number;
@@ -120,12 +120,13 @@ export const studioKpis = {
retentionRate: 92,
};
// `fill` colora la singola barra: le fasce basse sono "buone", le alte critiche.
export const scoreDistribution = [
{ range: '0-20', count: 0 },
{ range: '21-40', count: 2 },
{ range: '41-60', count: 3 },
{ range: '61-80', count: 4 },
{ range: '81-100', count: 1 },
{ range: '0-20', count: 0, fill: '#6f8f6a' },
{ range: '21-40', count: 2, fill: '#6f8f6a' },
{ range: '41-60', count: 3, fill: '#c08a3e' },
{ range: '61-80', count: 4, fill: '#b05a4e' },
{ range: '81-100', count: 1, fill: '#8d3f34' },
];
export const segments = [
@@ -146,26 +147,66 @@ export const criticalClients = [
{ clientId: 'c6', name: 'E. Ricci', stress: 70 },
];
export const sessions = [
{ id: 's1', athleteId: 'c2', date: '2026-07-11', type: 'Corsa', durationMin: 45, load: 62, trimp: 58, hrAvg: 142, dfaA1: 0.72 },
{ id: 's2', athleteId: 'c5', date: '2026-07-08', type: 'Ciclismo', durationMin: 90, load: 74, trimp: 96, hrAvg: 138, dfaA1: 0.61 },
{ id: 's3', athleteId: 'c8', date: '2026-07-12', type: 'Nuoto', durationMin: 40, load: 50, trimp: 45, hrAvg: 128, dfaA1: 0.8 },
{ id: 's4', athleteId: 'c2', date: '2026-07-04', type: 'Pesi', durationMin: 60, load: 68, trimp: 70, hrAvg: 145, dfaA1: 0.55 },
{ id: 's5', athleteId: 'c5', date: '2026-07-01', type: 'Corsa', durationMin: 50, load: 58, trimp: 52, hrAvg: 135, dfaA1: 0.75 },
// Ultimo giorno coperto dai dati dimostrativi dello Sport.
const OGGI_SPORT = '2026-07-12';
export type Session = {
id: string;
athleteId: string;
/** Inizio della sessione, data e ora locali. */
startedAt: string;
durationSec: number;
/** Lo sport può non essere indicato: l'app lo ricava dal sensore solo se dichiarato. */
sport: string | null;
hrAvg: number;
hrMax: number;
trimp: number;
/** DFA Alpha1 a fine sessione; assente sulle registrazioni troppo brevi. */
dfaA1: number | null;
/** Sforzo percepito 1-10, inserito a mano dall'atleta. */
rpe: number | null;
};
export type Athlete = { clientId: string; name: string; sport: string | null };
export const athletes: Athlete[] = [
{ clientId: 'c2', name: 'A. Neri', sport: 'Corsa' },
{ clientId: 'c5', name: 'P. Colombo', sport: 'Ciclismo' },
{ clientId: 'c8', name: 'C. Greco', sport: null },
];
export const athletes = [
{ clientId: 'c2', name: 'A. Neri', sport: 'Corsa', weeklyLoad: 130 },
{ clientId: 'c5', name: 'P. Colombo', sport: 'Ciclismo', weeklyLoad: 164 },
{ clientId: 'c8', name: 'C. Greco', sport: 'Nuoto', weeklyLoad: 90 },
];
// Venti sessioni distribuite sugli ultimi due mesi, generate in modo deterministico dallo
// stesso schema seno/coseno usato per le misurazioni: nessun valore casuale, così server e
// client disegnano gli stessi grafici.
function buildSessions(): Session[] {
const out: Session[] = [];
const fine = new Date(`${OGGI_SPORT}T00:00:00`);
for (let i = 0; i < 20; i++) {
const atleta = athletes[i % athletes.length];
const giorno = new Date(fine);
giorno.setDate(giorno.getDate() - Math.round(i * 2.6));
const seed = i * 7 + atleta.clientId.charCodeAt(1);
const ora = 7 + (i % 12);
const minuti = (seed * 13) % 60;
const durata = i % 9 === 0 ? 5 + (seed % 40) : 540 + Math.round(Math.abs(Math.sin(seed)) * 4400);
const hrAvg = 95 + Math.round(Math.abs(Math.cos(seed)) * 55);
out.push({
id: `s${i + 1}`,
athleteId: atleta.clientId,
startedAt: `${giorno.toISOString().slice(0, 10)}T${String(ora).padStart(2, '0')}:${String(minuti).padStart(2, '0')}`,
durationSec: durata,
sport: atleta.sport,
hrAvg,
hrMax: hrAvg + 15 + (seed % 25),
trimp: durata < 300 ? 0 : Math.max(4, Math.round((durata / 60) * (hrAvg / 110))),
dfaA1: durata < 300 ? null : Math.round((0.85 + Math.abs(Math.sin(seed * 1.7)) * 0.85) * 100) / 100,
rpe: i % 7 === 2 ? null : 3 + (seed % 6),
});
}
return out.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
}
export const sessionsPerWeek = [
{ week: 'S1', count: 4 }, { week: 'S2', count: 5 }, { week: 'S3', count: 3 },
{ week: 'S4', count: 6 }, { week: 'S5', count: 4 }, { week: 'S6', count: 5 },
{ week: 'S7', count: 2 }, { week: 'S8', count: 5 }, { week: 'S9', count: 6 },
{ week: 'S10', count: 4 }, { week: 'S11', count: 5 }, { week: 'S12', count: 3 },
];
export const sessions: Session[] = buildSessions();
// "Oggi" della piattaforma: i dati dimostrativi sono ancorati a questa data, quindi i conteggi
// "giorni fa" restano coerenti invece di crescere col passare del tempo reale.