3cda76b4c4
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>
141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
// 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>
|
||
);
|
||
}
|