Files
InsanityLab-web/src/components/longevity/Radar.tsx
T
Adriano 1d6bdd0b3d longevity: revisione finale — il motore ora si calcola, il consenso è atomico, il codice cliente è tipato fuori dal corpo
Sei correzioni dalla revisione finale del 22/08, ognuna con test scritto prima:

1. salvaDalForm calcolava mai i punteggi (calcolaSessione/salvaScore) dopo il
   salvataggio: un cliente compilava venti domande e vedeva un referto vuoto.
   Ora avviene nella stessa transazione del salvataggio.
2. Il corpo della richiesta dell'endpoint questionario è tipato
   (CorpoQuestionario): client_code non esiste in quel tipo, non è più
   prendibile per errore. Il vecchio test a lista di nomi vietati era vacuo
   (cercava body./corpo., il file usa data.) — sostituito con un test
   comportamentale end-to-end sull'endpoint vero.
3. Il consenso: aggiunto il test sul ramo affermativo (non coperto, vacuo
   quanto il punto 2); la nota di consenso ora è nella stessa transazione dei
   dati sanitari; ha una versione propria (CONSENT_VERSION) invece di quella
   del questionario.
4. La prima apertura del referto ora spiega invece di sembrare rotta: Fitness
   Age con nota quando manca, spiegazione del radar prima del grafico,
   copertura sempre visibile, invito a fare i test in sala col trainer,
   numeri formattati all'italiana.
5. Aggiunto il link per ricompilare il questionario nella pagina del referto
   (prima spariva appena esisteva una compilazione).
6. Il test anti-query ora scandisce tutte le pagine sotto src/pages/longevity/,
   non due nomi a mano. Il test anti-giunzione identity/longevity resta con lo
   stesso limite di sempre (createIdentityDb è esportata per creaCliente) —
   corretto solo il commento perché lo dichiari onestamente.

Report con comandi e output letterali:
.superpowers/sdd/2026-08-22-longevity-interfaccia/review-finale-report.md
2026-08-22 15:17:47 +02:00

96 lines
4.0 KiB
TypeScript

// Il radar a sette assi del referto: isola React perché recharts ha bisogno del DOM.
// Due serie sovrapposte sugli stessi sette assi (recharts vuole un solo `data` condiviso,
// non può disegnare un poligono che copre solo alcuni assi):
// - "noto": l'area piena, coi valori congelati che la sessione ha davvero. Dove il dato
// manca vale 0 — non si inventa un centro pieno per una misura che non c'è, si lascia
// che l'area si ritiri, onestamente, verso il centro;
// - "potenziale": il perimetro tratteggiato. Dove il dato c'è coincide col bordo di
// "noto" (stesso valore, la linea tratteggiata sparisce dietro quella piena); dove
// manca si allarga fino al massimo (100) — è la parte che CHIUDE il perimetro, il gesto
// su cui si regge l'interfaccia: non un errore, un invito a completare quell'asse.
// Sotto il grafico, l'elenco leggibile di cosa completerebbe ogni asse ancora tratteggiato,
// letto da `mancano` (calcolato in vista.ts, mai qui: questo componente non tocca il db).
import {
ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar,
} from 'recharts';
import type { AsseVista } from '../../lib/longevity/vista';
interface Props {
assi: AsseVista[];
}
// Stessi colori del sito (come in DataChart.tsx, che per lo stesso motivo — attributi SVG,
// non proprietà CSS — non passa var(--...) a recharts): l'accento e il tratteggio di
// longevity.css, non un colore nuovo.
const ACCENTO = '#9c8b70';
const TRATTEGGIO = '#d8d2c8';
const LINEA = '#e4dfd7';
const TESTO_CHIARO = '#8a8a8a';
export default function RadarAssi({ assi }: Props) {
const dati = assi.map((a) => ({
nome: a.nome,
noto: a.valore ?? 0,
potenziale: a.valore ?? 100,
}));
const incompleti = assi.filter((a) => a.mancano.length > 0);
return (
<div className="lg-radar">
{/* La spiegazione del linguaggio visivo viene PRIMA del grafico che spiega, non dopo:
chi legge deve sapere cosa sta guardando prima di guardarlo. */}
<p className="lg-radar__intro">
L'area piena è quello che sappiamo davvero; il perimetro tratteggiato è il resto
dell'asse, dove il dato non c'è ancora.
</p>
<div className="lg-radar__grafico">
<ResponsiveContainer width="100%" height={360}>
<RadarChart data={dati} outerRadius="72%">
<PolarGrid stroke={LINEA} />
<PolarAngleAxis dataKey="nome" tick={{ fill: TESTO_CHIARO, fontSize: 11 }} />
<PolarRadiusAxis domain={[0, 100]} tick={false} axisLine={false} />
{/* il perimetro tratteggiato si disegna per primo: quello pieno gli va sopra */}
<Radar
name="Perimetro completo"
dataKey="potenziale"
stroke={TRATTEGGIO}
strokeDasharray="4 3"
strokeWidth={1.5}
fill="none"
isAnimationActive={false}
/>
<Radar
name="Quello che sappiamo"
dataKey="noto"
stroke={ACCENTO}
fill={ACCENTO}
fillOpacity={0.32}
strokeWidth={2}
isAnimationActive={false}
/>
</RadarChart>
</ResponsiveContainer>
</div>
{incompleti.length > 0 && (
<div className="lg-radar__mancano">
{/* L'invito concreto: l'elenco sotto è nomi di test, da solo non dice cosa fare.
Questi test si fanno in palestra con lo staff, non a casa da soli. */}
<p className="lg-muted">
Questi test si fanno in sala, con il trainer: prenota la prossima sessione per
completare il quadro.
</p>
{incompleti.map((a) => (
<div className="lg-radar__voce" key={a.nome}>
<p className="lg-radar__voce-nome">{a.nome}</p>
<ul className="lg-radar__lista">
{a.mancano.map((m) => <li key={m}>{m}</li>)}
</ul>
</div>
))}
</div>
)}
</div>
);
}