1d6bdd0b3d
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
96 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|