// 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, periodo: Periodo ): Record { const out: Record = {}; 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, 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, periodo: Periodo ): Record[] { const righe = FASCE.map((f) => { const riga: Record = { 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, 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, periodo: Periodo, dimensione: Dimensione ): Record[] { const nelPeriodo = misurazioniNelPeriodo(byClient, periodo); const gruppi = new Map(); 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), })); }