Piattaforme riservate: ruolo dedicato e port della dashboard Stress Index

Il ruolo `campus` diventa `piattaforme` e non apre più solo la sezione
Biohacking: copre tutte le aree riservate, con migrazione guardata sugli
utenti esistenti. Nell'header la vecchia voce Biohacking lascia il posto a
un menu Piattaforme con i due link, visibile solo a chi ha il ruolo, e
/piattaforme diventa la pagina di atterraggio.

Stress Index è portata dentro il sito: le nove viste dell'area
professionisti diventano pagine Astro sotto /piattaforme/stress-index,
con lo stile del sito al posto del design system del prototipo. React
resta solo dove serve davvero — grafici recharts e scheda cliente — il
resto è Astro con un filo di JS. I dati sono ancora quelli dimostrativi,
raccolti in lib/stress-index/data.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:49:47 +02:00
parent e8e24e086b
commit c4eb598a3a
32 changed files with 2908 additions and 74 deletions
@@ -0,0 +1,56 @@
// Andamento medio dello studio, con selettore di periodo. L'aggregazione media stress e
// recupero di tutti i clienti sugli ultimi N rilievi settimanali.
import { useState } from 'react';
import DataChart from './DataChart';
import type { Measurement } from '../../../lib/stress-index/data';
const PERIODS = [
{ key: '30g', label: '30 giorni', points: 5 },
{ key: '90g', label: '90 giorni', points: 13 },
{ key: '6m', label: '6 mesi', points: 20 },
{ key: 'tutto', label: 'Tutto', points: 20 },
] as const;
interface Props { measurementsByClient: Record<string, Measurement[]> }
function aggregate(byClient: Record<string, Measurement[]>, points: number) {
const out: { date: string; stress: number; recovery: number }[] = [];
for (let fromEnd = points; fromEnd >= 1; fromEnd--) {
let stress = 0, recovery = 0, count = 0, date = '';
for (const series of Object.values(byClient)) {
const point = series[series.length - fromEnd];
if (!point) continue;
stress += point.stress;
recovery += point.recovery;
count += 1;
date = point.date;
}
if (count) out.push({ date, stress: Math.round(stress / count), recovery: Math.round(recovery / count) });
}
return out;
}
export default function AndamentoChart({ measurementsByClient }: Props) {
const [period, setPeriod] = useState<string>('90g');
const active = PERIODS.find((p) => p.key === period) ?? PERIODS[1];
return (
<div>
<div className="si-chips">
{PERIODS.map((p) => (
<button key={p.key} type="button" className="si-chip" aria-pressed={p.key === period} onClick={() => setPeriod(p.key)}>
{p.label}
</button>
))}
</div>
<DataChart
data={aggregate(measurementsByClient, active.points)}
xKey="date"
series={[
{ key: 'stress', label: 'Stress medio' },
{ key: 'recovery', label: 'Recupero medio' },
]}
/>
</div>
);
}