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:
@@ -0,0 +1,141 @@
|
||||
// Calcoli della vista Analytics. Stanno fuori dal componente perché sono la parte con una
|
||||
// logica vera — e quindi l'unica che vale la pena testare — mentre la vista si limita a
|
||||
// disegnare ciò che esce da qui.
|
||||
import type { Alert, Client, Measurement } from './data';
|
||||
|
||||
export interface Periodo { from: string; to: string }
|
||||
|
||||
export const METRICHE = [
|
||||
{ key: 'stress', label: 'Stress', color: '#b05a4e' },
|
||||
{ key: 'recovery', label: 'Recupero', color: '#6f8f6a' },
|
||||
{ key: 'balance', label: 'Equilibrio', color: '#7d9b96' },
|
||||
{ key: 'energy', label: 'Energia', color: '#c08a3e' },
|
||||
] as const;
|
||||
|
||||
export const FASCE = ['0-20', '21-40', '41-60', '61-80', '81-100'];
|
||||
|
||||
/** Fascia di appartenenza di uno score 0-100. */
|
||||
export function fascia(value: number): string {
|
||||
if (value <= 20) return FASCE[0];
|
||||
if (value <= 40) return FASCE[1];
|
||||
if (value <= 60) return FASCE[2];
|
||||
if (value <= 80) return FASCE[3];
|
||||
return FASCE[4];
|
||||
}
|
||||
|
||||
const dentro = (date: string, p: Periodo) => date >= p.from && date <= p.to;
|
||||
|
||||
/** Misurazioni del periodo, cliente per cliente. */
|
||||
export function misurazioniNelPeriodo(
|
||||
byClient: Record<string, Measurement[]>,
|
||||
periodo: Periodo
|
||||
): Record<string, Measurement[]> {
|
||||
const out: Record<string, Measurement[]> = {};
|
||||
for (const [id, serie] of Object.entries(byClient)) {
|
||||
out[id] = serie.filter((m) => dentro(m.date, periodo));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface Kpi { label: string; value: string | number; note: string }
|
||||
|
||||
export function kpis(
|
||||
clients: Client[],
|
||||
byClient: Record<string, Measurement[]>,
|
||||
alerts: Alert[],
|
||||
periodo: Periodo
|
||||
): Kpi[] {
|
||||
const nelPeriodo = misurazioniNelPeriodo(byClient, periodo);
|
||||
const attivi = clients.filter((c) => (nelPeriodo[c.id] ?? []).length > 0).length;
|
||||
const misurazioni = Object.values(nelPeriodo).reduce((n, s) => n + s.length, 0);
|
||||
const alertAttivi = alerts.filter((a) => dentro(a.when, periodo)).length;
|
||||
const aderenza = clients.length ? Math.round((attivi / clients.length) * 100) : 0;
|
||||
return [
|
||||
{ label: 'Clienti attivi', value: attivi, note: `/ ${clients.length} totali` },
|
||||
{ label: 'Misurazioni', value: misurazioni, note: 'nel periodo' },
|
||||
{ label: 'Alert attivi', value: alertAttivi, note: 'nel periodo' },
|
||||
{ label: 'Aderenza', value: `${aderenza}%`, note: 'hanno misurato' },
|
||||
];
|
||||
}
|
||||
|
||||
/** Quante misurazioni cadono in ogni fascia, separatamente per ciascuna metrica. */
|
||||
export function distribuzione(
|
||||
byClient: Record<string, Measurement[]>,
|
||||
periodo: Periodo
|
||||
): Record<string, number | string>[] {
|
||||
const righe = FASCE.map((f) => {
|
||||
const riga: Record<string, number | string> = { range: f };
|
||||
for (const m of METRICHE) riga[m.key] = 0;
|
||||
return riga;
|
||||
});
|
||||
for (const serie of Object.values(misurazioniNelPeriodo(byClient, periodo))) {
|
||||
for (const misura of serie) {
|
||||
for (const metrica of METRICHE) {
|
||||
const riga = righe[FASCE.indexOf(fascia(misura[metrica.key]))];
|
||||
riga[metrica.key] = (riga[metrica.key] as number) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return righe;
|
||||
}
|
||||
|
||||
export interface RigaClassifica { clientId: string; name: string; recovery: number; count: number }
|
||||
|
||||
/** Clienti ordinati per recupero medio nel periodo: i migliori in testa. */
|
||||
export function classificaRecupero(
|
||||
clients: Client[],
|
||||
byClient: Record<string, Measurement[]>,
|
||||
periodo: Periodo
|
||||
): RigaClassifica[] {
|
||||
const nelPeriodo = misurazioniNelPeriodo(byClient, periodo);
|
||||
return clients
|
||||
.map((c) => {
|
||||
const serie = nelPeriodo[c.id] ?? [];
|
||||
const media = serie.length ? serie.reduce((n, m) => n + m.recovery, 0) / serie.length : 0;
|
||||
return { clientId: c.id, name: c.name, recovery: Math.round(media), count: serie.length };
|
||||
})
|
||||
.filter((r) => r.count > 0)
|
||||
.sort((a, b) => b.recovery - a.recovery);
|
||||
}
|
||||
|
||||
export type Dimensione = 'sesso' | 'eta' | 'tag';
|
||||
|
||||
export const DIMENSIONI: { key: Dimensione; label: string }[] = [
|
||||
{ key: 'sesso', label: 'Sesso' },
|
||||
{ key: 'eta', label: 'Età' },
|
||||
{ key: 'tag', label: 'Tag principale' },
|
||||
];
|
||||
|
||||
function gruppoDi(client: Client, dimensione: Dimensione): string {
|
||||
if (dimensione === 'sesso') return client.sex === 'F' ? 'Donne' : 'Uomini';
|
||||
if (dimensione === 'eta') return client.age < 35 ? 'Fino a 34' : client.age < 50 ? '35-49' : '50 e oltre';
|
||||
return client.tags[0] ?? 'Senza tag';
|
||||
}
|
||||
|
||||
/** Stress e recupero medi per gruppo, secondo la dimensione scelta. */
|
||||
export function confrontoSegmenti(
|
||||
clients: Client[],
|
||||
byClient: Record<string, Measurement[]>,
|
||||
periodo: Periodo,
|
||||
dimensione: Dimensione
|
||||
): Record<string, number | string>[] {
|
||||
const nelPeriodo = misurazioniNelPeriodo(byClient, periodo);
|
||||
const gruppi = new Map<string, { stress: number; recovery: number; n: number }>();
|
||||
for (const client of clients) {
|
||||
const serie = nelPeriodo[client.id] ?? [];
|
||||
if (!serie.length) continue;
|
||||
const nome = gruppoDi(client, dimensione);
|
||||
const acc = gruppi.get(nome) ?? { stress: 0, recovery: 0, n: 0 };
|
||||
for (const m of serie) {
|
||||
acc.stress += m.stress;
|
||||
acc.recovery += m.recovery;
|
||||
acc.n += 1;
|
||||
}
|
||||
gruppi.set(nome, acc);
|
||||
}
|
||||
return [...gruppi.entries()].map(([gruppo, a]) => ({
|
||||
gruppo,
|
||||
stress: Math.round(a.stress / a.n),
|
||||
recovery: Math.round(a.recovery / a.n),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user