Files
InsanityLab-web/src/components/stress-index/AnalyticsView.tsx
T
Adriano 3cda76b4c4 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>
2026-07-31 16:20:40 +02:00

141 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Analytics di studio. È un'isola React perché tutto dipende da due controlli — l'intervallo
// di date e la dimensione di confronto — e ogni numero della pagina si ricalcola con essi.
import { useMemo, useState } from 'react';
import DataChart from './charts/DataChart';
import type { Alert, Client, Measurement } from '../../lib/stress-index/data';
import {
DIMENSIONI, METRICHE, classificaRecupero, confrontoSegmenti, distribuzione, kpis,
type Dimensione,
} from '../../lib/stress-index/analytics';
interface Props {
clients: Client[];
measurementsByClient: Record<string, Measurement[]>;
alerts: Alert[];
/** Intervallo iniziale, deciso dalla pagina. */
from: string;
to: string;
}
const dataItaliana = (iso: string) =>
new Date(`${iso}T00:00:00`).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' });
export default function AnalyticsView({ clients, measurementsByClient, alerts, from, to }: Props) {
const [periodo, setPeriodo] = useState({ from, to });
const [dimensione, setDimensione] = useState<Dimensione>('sesso');
const indicatori = useMemo(() => kpis(clients, measurementsByClient, alerts, periodo), [clients, measurementsByClient, alerts, periodo]);
const fasce = useMemo(() => distribuzione(measurementsByClient, periodo), [measurementsByClient, periodo]);
const classifica = useMemo(() => classificaRecupero(clients, measurementsByClient, periodo), [clients, measurementsByClient, periodo]);
const segmenti = useMemo(() => confrontoSegmenti(clients, measurementsByClient, periodo, dimensione), [clients, measurementsByClient, periodo, dimensione]);
const migliori = classifica.slice(0, 4);
const critici = [...classifica].reverse().slice(0, 4);
return (
<div className="si-stack">
<div className="si-head si-head--row">
<div>
<h1>Analytics di studio</h1>
<p>Confronta i tuoi clienti, individua gli andamenti, calibra il lavoro</p>
</div>
<div className="si-range">
<label className="si-field si-field--inline">
<span className="si-visually-hidden">Dal</span>
<input type="date" value={periodo.from} max={periodo.to} onChange={(e) => setPeriodo((p) => ({ ...p, from: e.target.value }))} />
</label>
<span className="si-range__sep" aria-hidden="true"></span>
<label className="si-field si-field--inline">
<span className="si-visually-hidden">Al</span>
<input type="date" value={periodo.to} min={periodo.from} onChange={(e) => setPeriodo((p) => ({ ...p, to: e.target.value }))} />
</label>
</div>
</div>
<div className="si-grid si-grid--kpi">
{indicatori.map((k) => (
<div className="si-card" key={k.label}>
<p className="si-kpi__label">{k.label}</p>
<p className="si-kpi__value">{k.value}</p>
<p className="si-muted si-small">{k.note}</p>
</div>
))}
</div>
<section className="si-card">
<div className="si-card__head">
<div>
<h2 className="si-card__title">Distribuzione score clienti</h2>
<p className="si-muted si-small">Quante misurazioni cadono in ogni fascia di valore</p>
</div>
</div>
<DataChart type="bar" data={fasce} xKey="range" series={METRICHE.map((m) => ({ ...m }))} height={300} />
</section>
<div className="si-grid si-grid--halves">
<section className="si-card">
<div className="si-card__head">
<div>
<h2 className="si-card__title">Andamento migliore</h2>
<p className="si-muted si-small">Recupero medio più alto nel periodo</p>
</div>
</div>
<ul className="si-rank">
{migliori.map((r) => (
<li key={r.clientId}>
<a href={`/piattaforme/stress-index/clienti/${r.clientId}`}>{r.name}</a>
<span>
<span className="si-rank__value si-rank__value--ok">{r.recovery}</span>
<span className="si-rank__count">{r.count} {r.count === 1 ? 'misurazione' : 'misurazioni'}</span>
</span>
</li>
))}
</ul>
</section>
<section className="si-card">
<div className="si-card__head">
<div>
<h2 className="si-card__title">Clienti critici</h2>
<p className="si-muted si-small">Recupero medio più basso nel periodo</p>
</div>
</div>
<ul className="si-rank">
{critici.map((r) => (
<li key={r.clientId}>
<a href={`/piattaforme/stress-index/clienti/${r.clientId}`}>{r.name}</a>
<span>
<span className="si-rank__value si-rank__value--bad">{r.recovery}</span>
<span className="si-rank__count">{r.count} {r.count === 1 ? 'misurazione' : 'misurazioni'}</span>
</span>
</li>
))}
</ul>
</section>
</div>
<section className="si-card">
<div className="si-card__head">
<div>
<h2 className="si-card__title">Confronto segmenti</h2>
<p className="si-muted si-small">Valori medi per {dataItaliana(periodo.from)} {dataItaliana(periodo.to)}</p>
</div>
<label className="si-field si-field--inline">
<span className="si-visually-hidden">Dimensione di confronto</span>
<select value={dimensione} onChange={(e) => setDimensione(e.target.value as Dimensione)}>
{DIMENSIONI.map((d) => <option key={d.key} value={d.key}>{d.label}</option>)}
</select>
</label>
</div>
<DataChart
type="bar" data={segmenti} xKey="gruppo" height={300}
series={[
{ key: 'stress', label: 'Stress medio', color: '#b05a4e' },
{ key: 'recovery', label: 'Recupero medio', color: '#6f8f6a' },
]}
/>
</section>
</div>
);
}