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>
229 lines
10 KiB
TypeScript
229 lines
10 KiB
TypeScript
// Dati della piattaforma Stress Index. Sono ancora i dati dimostrativi del prototipo
|
|
// (nessun backend HRV collegato): stanno tutti qui, così quando arriverà la sorgente vera
|
|
// basterà sostituire queste costanti con le query, lasciando intatte pagine e componenti.
|
|
|
|
export interface Client {
|
|
id: string;
|
|
name: string;
|
|
initials: string;
|
|
age: number;
|
|
sex: 'M' | 'F';
|
|
tags: string[];
|
|
email: string;
|
|
since: string;
|
|
lastMeasurement: string;
|
|
stress: number;
|
|
alerts: number;
|
|
}
|
|
|
|
export const clients: Client[] = [
|
|
{ id: 'c1', name: 'L. Verdi', initials: 'LV', age: 34, sex: 'F', tags: ['ansia', 'sonno'], email: 'l.verdi@esempio.it', since: '2024-02-10', lastMeasurement: '2026-07-10', stress: 62, alerts: 1 },
|
|
{ id: 'c2', name: 'A. Neri', initials: 'AN', age: 45, sex: 'M', tags: ['sport', 'recupero'], email: 'a.neri@esempio.it', since: '2023-11-05', lastMeasurement: '2026-07-11', stress: 38, alerts: 0 },
|
|
{ id: 'c3', name: 'G. Rossi', initials: 'GR', age: 29, sex: 'M', tags: ['lavoro', 'stress'], email: 'g.rossi@esempio.it', since: '2024-05-20', lastMeasurement: '2026-07-09', stress: 78, alerts: 2 },
|
|
{ id: 'c4', name: 'S. Ferrari', initials: 'SF', age: 52, sex: 'F', tags: ['menopausa', 'sonno'], email: 's.ferrari@esempio.it', since: '2023-08-14', lastMeasurement: '2026-07-12', stress: 55, alerts: 0 },
|
|
{ id: 'c5', name: 'P. Colombo', initials: 'PC', age: 40, sex: 'M', tags: ['sport', 'agonismo'], email: 'p.colombo@esempio.it', since: '2024-01-30', lastMeasurement: '2026-07-08', stress: 44, alerts: 0 },
|
|
{ id: 'c6', name: 'E. Ricci', initials: 'ER', age: 61, sex: 'F', tags: ['cronico', 'riabilitazione'], email: 'e.ricci@esempio.it', since: '2022-09-01', lastMeasurement: '2026-07-11', stress: 70, alerts: 1 },
|
|
{ id: 'c7', name: 'D. Marino', initials: 'DM', age: 33, sex: 'M', tags: ['lavoro', 'burnout'], email: 'd.marino@esempio.it', since: '2024-03-18', lastMeasurement: '2026-07-07', stress: 82, alerts: 2 },
|
|
{ id: 'c8', name: 'C. Greco', initials: 'CG', age: 27, sex: 'F', tags: ['sport', 'studentessa'], email: 'c.greco@esempio.it', since: '2024-06-02', lastMeasurement: '2026-07-12', stress: 30, alerts: 0 },
|
|
{ id: 'c9', name: 'F. Bruno', initials: 'FB', age: 48, sex: 'M', tags: ['ipertensione', 'sonno'], email: 'f.bruno@esempio.it', since: '2023-04-25', lastMeasurement: '2026-07-06', stress: 66, alerts: 1 },
|
|
{ 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 type Measurement = {
|
|
date: string;
|
|
stress: number;
|
|
recovery: number;
|
|
balance: number;
|
|
energy: number;
|
|
type: string;
|
|
durationMin: number;
|
|
}
|
|
|
|
const clamp = (n: number) => Math.max(0, Math.min(100, Math.round(n)));
|
|
const TYPES = ['Riposo', 'Lavoro', 'Sport', 'Sonno'];
|
|
|
|
// Serie settimanale deterministica: l'oscillazione deriva da id + indice, così i grafici
|
|
// sono sempre uguali fra server e client (nessun Math.random che sfarfalla all'idratazione).
|
|
function buildSeries(clientId: string, baseStress: number, endDate: string, points: number): Measurement[] {
|
|
const end = new Date(endDate);
|
|
const series: Measurement[] = [];
|
|
for (let i = points - 1; i >= 0; i--) {
|
|
const d = new Date(end);
|
|
d.setDate(d.getDate() - i * 7);
|
|
const seed = clientId.charCodeAt(clientId.length - 1) + i * 13;
|
|
const wobble = Math.sin(seed) * 12;
|
|
const stress = clamp(baseStress + wobble);
|
|
series.push({
|
|
date: d.toISOString().slice(0, 10),
|
|
stress,
|
|
recovery: clamp(100 - stress + Math.cos(seed) * 8),
|
|
balance: clamp(70 - Math.abs(wobble) + Math.sin(seed * 2) * 6),
|
|
energy: clamp(65 - (stress - 50) * 0.4 + Math.cos(seed * 1.5) * 7),
|
|
type: TYPES[i % TYPES.length],
|
|
durationMin: 15 + ((i * 5) % 40),
|
|
});
|
|
}
|
|
return series;
|
|
}
|
|
|
|
export const measurementsByClient: Record<string, Measurement[]> = {
|
|
c1: buildSeries('c1', 62, '2026-07-10', 16),
|
|
c2: buildSeries('c2', 38, '2026-07-11', 14),
|
|
c3: buildSeries('c3', 78, '2026-07-09', 18),
|
|
c4: buildSeries('c4', 55, '2026-07-12', 12),
|
|
c5: buildSeries('c5', 44, '2026-07-08', 15),
|
|
c6: buildSeries('c6', 70, '2026-07-11', 20),
|
|
c7: buildSeries('c7', 82, '2026-07-07', 17),
|
|
c8: buildSeries('c8', 30, '2026-07-12', 10),
|
|
c9: buildSeries('c9', 66, '2026-07-06', 13),
|
|
c10: buildSeries('c10', 58, '2026-07-10', 11),
|
|
};
|
|
|
|
export interface Alert {
|
|
clientId: string;
|
|
clientName: string;
|
|
text: string;
|
|
severity: 'warn' | 'error';
|
|
when: string;
|
|
}
|
|
|
|
export const alerts: Alert[] = [
|
|
{ clientId: 'c7', clientName: 'D. Marino', text: 'Stress elevato per 3 misurazioni consecutive', severity: 'error', when: '2026-07-07' },
|
|
{ clientId: 'c3', clientName: 'G. Rossi', text: 'Recupero notturno in calo', severity: 'warn', when: '2026-07-09' },
|
|
{ clientId: 'c3', clientName: 'G. Rossi', text: 'Bilancio simpatico/parasimpatico sbilanciato', severity: 'warn', when: '2026-07-05' },
|
|
{ clientId: 'c1', clientName: 'L. Verdi', text: 'Qualità del sonno sotto la soglia attesa', severity: 'warn', when: '2026-07-10' },
|
|
{ clientId: 'c6', clientName: 'E. Ricci', text: 'Andamento stress in peggioramento nelle ultime 4 settimane', severity: 'warn', when: '2026-07-11' },
|
|
{ clientId: 'c9', clientName: 'F. Bruno', text: 'Nessuna misurazione da oltre 7 giorni', severity: 'warn', when: '2026-07-13' },
|
|
{ clientId: 'c10', clientName: 'V. Costa', text: 'Picco di stress rilevato dopo sessione di lavoro prolungata', severity: 'warn', when: '2026-07-10' },
|
|
{ clientId: 'c7', clientName: 'D. Marino', text: 'Energia media sotto il 30% da due settimane', severity: 'error', when: '2026-07-01' },
|
|
];
|
|
|
|
export interface Note {
|
|
clientId?: string;
|
|
text: string;
|
|
when: string;
|
|
}
|
|
|
|
export const notes: Note[] = [
|
|
{ clientId: 'c1', text: 'Consigliata routine di respirazione serale, rivalutare tra 2 settimane.', when: '2026-07-10' },
|
|
{ clientId: 'c3', text: 'Segnalato carico lavorativo elevato, proposto colloquio di follow-up.', when: '2026-07-09' },
|
|
{ clientId: 'c7', text: 'Situazione di burnout in peggioramento, valutare invio a specialista.', when: '2026-07-07' },
|
|
{ text: 'Aggiornamento protocollo misurazioni: aggiunta soglia energia minima.', when: '2026-07-05' },
|
|
{ clientId: 'c6', text: 'Programma di riabilitazione in corso, buoni progressi sul recupero.', when: '2026-07-11' },
|
|
];
|
|
|
|
export const studioKpis = {
|
|
totalClients: 10,
|
|
avgStress: 58,
|
|
activeAlerts: 8,
|
|
measurementsThisMonth: 46,
|
|
retentionRate: 92,
|
|
};
|
|
|
|
// `fill` colora la singola barra: le fasce basse sono "buone", le alte critiche.
|
|
export const scoreDistribution = [
|
|
{ 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 = [
|
|
{ label: 'Sportivi', count: 3, avgStress: 37 },
|
|
{ label: 'Stress lavorativo', count: 4, avgStress: 69 },
|
|
{ label: 'Sonno/recupero', count: 3, avgStress: 61 },
|
|
];
|
|
|
|
export const topPerformers = [
|
|
{ clientId: 'c8', name: 'C. Greco', stress: 30 },
|
|
{ clientId: 'c2', name: 'A. Neri', stress: 38 },
|
|
{ clientId: 'c5', name: 'P. Colombo', stress: 44 },
|
|
];
|
|
|
|
export const criticalClients = [
|
|
{ clientId: 'c7', name: 'D. Marino', stress: 82 },
|
|
{ clientId: 'c3', name: 'G. Rossi', stress: 78 },
|
|
{ clientId: 'c6', name: 'E. Ricci', stress: 70 },
|
|
];
|
|
|
|
// 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 },
|
|
];
|
|
|
|
// 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 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.
|
|
export const OGGI = '2026-07-13';
|
|
|
|
export function daysSince(date: string, from: string = OGGI): number {
|
|
return Math.max(0, Math.round((Date.parse(from) - Date.parse(date)) / 86_400_000));
|
|
}
|
|
|
|
export function findClient(id: string): Client | null {
|
|
return clients.find((c) => c.id === id) ?? null;
|
|
}
|
|
|
|
export const professional = {
|
|
name: 'Dr. M. Bianchi',
|
|
role: 'Osteopata',
|
|
studio: 'Studio Bianchi — Osteopatia e Benessere',
|
|
email: 'm.bianchi@esempio.it',
|
|
};
|