// Calcoli e formattazioni del modulo Sport, tenuti fuori dalla vista perché sono la parte // con una logica propria (finestre temporali, aggregazioni per settimana, riepiloghi atleta). import type { Athlete, Session } from './data'; /** "1h 21m" oltre l'ora, "54m 35s" sopra il minuto, "5s" sotto. */ export function durata(sec: number): string { if (sec >= 3600) return `${Math.floor(sec / 3600)}h ${String(Math.floor((sec % 3600) / 60)).padStart(2, '0')}m`; if (sec >= 60) return `${Math.floor(sec / 60)}m ${String(sec % 60).padStart(2, '0')}s`; return `${sec}s`; } const MESI = ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic']; /** "30 lug 2026" */ export function dataBreve(iso: string): string { const [y, m, d] = iso.slice(0, 10).split('-'); return `${Number(d)} ${MESI[Number(m) - 1]} ${y}`; } /** "30 lug 2026 alle 11:27" */ export function dataOra(iso: string): string { return `${dataBreve(iso)} alle ${iso.slice(11, 16)}`; } const giorniFra = (a: string, b: string) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / 86_400_000); /** Sessioni degli ultimi `giorni` rispetto alla data di riferimento. */ export function ultimi(sessions: Session[], giorni: number, riferimento: string): Session[] { return sessions.filter((s) => { const d = giorniFra(s.startedAt, riferimento); return d >= 0 && d < giorni; }); } export interface KpiSport { label: string; value: number; note?: string } export function kpiSport(sessions: Session[], riferimento: string): KpiSport[] { const settimana = ultimi(sessions, 7, riferimento); const mese = ultimi(sessions, 30, riferimento); const trimpSettimanale = settimana.reduce((n, s) => n + s.trimp, 0); const atletiAttivi = new Set(mese.map((s) => s.athleteId)).size; return [ { label: 'Sessioni totali', value: sessions.length }, { label: 'Questa settimana', value: settimana.length, note: 'ultimi 7gg' }, { label: 'TRIMP medio', value: trimpSettimanale, note: 'settimanale' }, { label: 'Atleti attivi', value: atletiAttivi, note: 'ultimi 30gg' }, ]; } /** Conteggio sessioni per settimana, etichettato col lunedì di ciascuna (dd/mm). */ export function perSettimana(sessions: Session[], settimane: number, riferimento: string): Record[] { // Tutta l'aritmetica sta in UTC: con date costruite in ora locale, `toISOString` riporta // indietro di un giorno ovunque il fuso sia positivo, e le settimane slittano al giorno prima. const GIORNO = 86_400_000; const rif = Date.parse(riferimento.slice(0, 10)); // getUTCDay(): domenica = 0, quindi la domenica torna indietro di 6 giorni, non di -1. const lunedi = rif - ((new Date(rif).getUTCDay() + 6) % 7) * GIORNO; const righe: Record[] = []; for (let i = settimane - 1; i >= 0; i--) { const inizio = lunedi - i * 7 * GIORNO; const da = new Date(inizio).toISOString().slice(0, 10); const a = new Date(inizio + 7 * GIORNO).toISOString().slice(0, 10); righe.push({ settimana: `${da.slice(8, 10)}/${da.slice(5, 7)}`, sessioni: sessions.filter((s) => s.startedAt.slice(0, 10) >= da && s.startedAt.slice(0, 10) < a).length, }); } return righe; } export interface RiepilogoAtleta { clientId: string; name: string; sport: string | null; sessioni30: number; trimp7: number; ultimoTrimp: number; ultimaSessione: string | null; /** Confronto fra il carico dell'ultima settimana e quello della precedente. */ trend: 'in crescita' | 'stabile' | 'in calo'; } export function riepilogoAtleti(sessions: Session[], athletes: Athlete[], riferimento: string): RiepilogoAtleta[] { return athletes.map((a) => { const sue = sessions.filter((s) => s.athleteId === a.clientId); const trimp7 = ultimi(sue, 7, riferimento).reduce((n, s) => n + s.trimp, 0); const settimanaPrima = sue.filter((s) => { const d = giorniFra(s.startedAt, riferimento); return d >= 7 && d < 14; }).reduce((n, s) => n + s.trimp, 0); const ultima = sue[0] ?? null; const scarto = trimp7 - settimanaPrima; return { clientId: a.clientId, name: a.name, sport: a.sport, sessioni30: ultimi(sue, 30, riferimento).length, trimp7, ultimoTrimp: ultima?.trimp ?? 0, ultimaSessione: ultima?.startedAt ?? null, trend: Math.abs(scarto) <= 10 ? 'stabile' : scarto > 0 ? 'in crescita' : 'in calo', }; }); }