longevity: #14 unità B, grafici dei test per cliente e trainer
AndamentoTest.tsx (recharts, isola React come Progressione.tsx): un LineChart per grafico del catalogo di andamento.ts, asse X temporale vero (timestamp, scala 'time'), dx/sx come due linee distinte per tratteggio (mai colore), tooltip con valore+unità ed extra (etichetta/unità dal registro), righe di cambio test in gg/mm/aaaa. progressione.astro: sezione «I tuoi test» (andamentoTest trainer:false), visibile anche con meno di 2 sessioni — il grafico degli assi resta con la sua condizione ≥2 invariata. Nuova gestionale/cliente/[code]/andamento.astro (trainer:true, + grafico degli assi se ≥2 punti), link «Andamento» in [code].astro accanto a «Nuovo check-up» (.lg-btn, già ≥44px). Modifica ammessa in andamento.ts: LineaAndamento.extra (etichetta/unità dell'extraTestId dal registro) così il tooltip non indovina — test in andamento.test.ts. Refs #14 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ToarVnK21BEig85QQowuw
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
// L'andamento nel tempo dei VALORI DEI TEST (issue #14, unità B): isola React come
|
||||
// Progressione.tsx e Radar.tsx (recharts ha bisogno del DOM). Un piccolo LineChart per
|
||||
// grafico del catalogo (`andamento.ts`), sotto il titolo della sua sezione, con le righe di
|
||||
// cambio test in cima — stesso linguaggio visivo del resto del referto, niente di nuovo:
|
||||
// - asse X TEMPORALE vero: `ts` (epoch ms) su una scala 'time', non una categoria — un buco
|
||||
// fra due check-up si vede come DISTANZA, non come una tacca uguale alle altre (DECISIONI
|
||||
// d dell'issue #14). Le etichette dell'asse e del tooltip sono in italiano.
|
||||
// - `connectNulls={false}` su ogni linea: una sessione senza quel test è un buco onesto,
|
||||
// mai un segmento che la attraversa (stesso principio di Progressione.tsx).
|
||||
// - dx/sx (o gli otto step del MUTT) sono più linee nello stesso grafico, distinte per
|
||||
// tratteggio/spessore — MAI per colore: un solo accento, come ovunque nel referto.
|
||||
// - il tooltip mostra data, valore (con l'unità del grafico) ed `extra` (le ripetizioni del
|
||||
// 5RM, la FC della camminata 2 km) con la SUA etichetta/unità dal registro — mai
|
||||
// indovinata (unità A, `LineaAndamento.extra`).
|
||||
// - un grafico con un solo punto mostra comunque il punto: `dot` resta acceso, il dominio
|
||||
// dell'asse X viene allargato apposta (altrimenti min===max collasserebbe la scala).
|
||||
import {
|
||||
ResponsiveContainer, LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip, Legend,
|
||||
} from 'recharts';
|
||||
import type { TooltipContentProps } from 'recharts';
|
||||
import type { SezioneAndamento, GraficoAndamento, LineaAndamento } from '../../lib/longevity/andamento';
|
||||
import { formattaNumero } from '../../lib/longevity/formato';
|
||||
|
||||
interface Props {
|
||||
sezioni: SezioneAndamento[];
|
||||
}
|
||||
|
||||
// Stessi colori esadecimali di Progressione.tsx/Radar.tsx (recharts vuole attributi SVG,
|
||||
// non var(--...)): non sono colori nuovi, sono la stessa copia.
|
||||
const ACCENTO = '#9c8b70';
|
||||
const LINEA = '#e4dfd7';
|
||||
const TESTO_CHIARO = '#8a8a8a';
|
||||
const TESTO_SCURO = '#2b2b2b';
|
||||
|
||||
// Tratteggio e spessore per distinguere le linee di uno stesso grafico (dx/sx, gli otto
|
||||
// step del MUTT) senza un colore nuovo per ciascuna — stessa idea di STILI_SERIE in
|
||||
// Progressione.tsx, con più varianti perché "MUTT, frequenza cardiaca per step" ne ha otto.
|
||||
const STILI_LINEA: { dash?: string; width: number }[] = [
|
||||
{ dash: undefined, width: 2.2 },
|
||||
{ dash: '6 3', width: 2 },
|
||||
{ dash: '2 2', width: 1.6 },
|
||||
{ dash: '8 3 2 3', width: 1.6 },
|
||||
{ dash: '1 3', width: 1.6 },
|
||||
{ dash: '10 2 2 2', width: 1.4 },
|
||||
{ dash: '4 2', width: 1.4 },
|
||||
{ dash: '3 1 1 1', width: 1.4 },
|
||||
];
|
||||
|
||||
const UN_GIORNO_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** 'AAAA-MM-GG' -> epoch ms a mezzanotte locale, come Progressione.tsx/[code].astro (mai
|
||||
* `Date.parse` diretto sulla stringa, che in alcuni motori interpreta la data come UTC e
|
||||
* la sposta di un giorno rispetto al fuso del browser). */
|
||||
function timestamp(dataIso: string): number {
|
||||
return new Date(`${dataIso}T00:00:00`).getTime();
|
||||
}
|
||||
|
||||
/** gg/mm/aaaa (analisi dell'issue #14: «Dal 12/03/2027: Trazioni (prima: Rematore)»).
|
||||
* Usato nelle righe di cambio e nel tooltip, dove lo spazio non è stretto come sull'asse. */
|
||||
function formattaDataEstesa(dataIso: string): string {
|
||||
return new Date(`${dataIso}T00:00:00`).toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
/** Formato compatto per i tick dell'asse X (gg/mm/aa, anno a due cifre): a 360px di
|
||||
* larghezza gg/mm/aaaa per ogni tick non ci sta, e recharts scarta comunque i tick che si
|
||||
* sovrappongono. */
|
||||
function formattaDataCompatta(ts: number): string {
|
||||
return new Date(ts).toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||
}
|
||||
|
||||
/** Come formattaDataEstesa, ma da un epoch ms (il `label` del tooltip, già calcolato da
|
||||
* `timestamp()` a mezzanotte locale) invece che da una stringa AAAA-MM-GG: evita un
|
||||
* andata-e-ritorno per ISO che a cavallo di mezzanotte UTC sposterebbe il giorno. */
|
||||
function formattaTsEsteso(ts: number): string {
|
||||
return new Date(ts).toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
type RigaGrafico = { ts: number; data: string } & Record<string, number | string | undefined>;
|
||||
|
||||
/** Una riga per sessione (unione di tutte le sessioni con almeno una misura di una linea
|
||||
* del grafico), in ordine cronologico: il formato che LineChart si aspetta, con una chiave
|
||||
* per test_id (il valore, `undefined` = buco) e una `${test_id}__extra` per il tooltip. */
|
||||
function righeDelGrafico(grafico: GraficoAndamento): RigaGrafico[] {
|
||||
const perSessione = new Map<number, RigaGrafico>();
|
||||
for (const linea of grafico.linee) {
|
||||
for (const p of linea.punti) {
|
||||
let riga = perSessione.get(p.sessioneId);
|
||||
if (!riga) {
|
||||
riga = { ts: timestamp(p.data), data: p.data };
|
||||
perSessione.set(p.sessioneId, riga);
|
||||
}
|
||||
riga[linea.testId] = p.valore;
|
||||
if (p.extra !== undefined) riga[`${linea.testId}__extra`] = p.extra;
|
||||
}
|
||||
}
|
||||
return [...perSessione.values()].sort((a, b) => a.ts - b.ts);
|
||||
}
|
||||
|
||||
/** Il dominio dell'asse X: min/max delle date, allargato quando collasserebbe su un solo
|
||||
* istante (un solo punto, o più punti tutti nella stessa sessione) — altrimenti la scala
|
||||
* 'time' non avrebbe ampiezza e il punto finirebbe al bordo o non renderizzerebbe. */
|
||||
function dominioTemporale(righe: RigaGrafico[]): [number, number] {
|
||||
const min = Math.min(...righe.map((r) => r.ts));
|
||||
const max = Math.max(...righe.map((r) => r.ts));
|
||||
if (min === max) return [min - UN_GIORNO_MS * 7, max + UN_GIORNO_MS * 7];
|
||||
const margine = Math.max((max - min) * 0.05, UN_GIORNO_MS);
|
||||
return [min - margine, max + margine];
|
||||
}
|
||||
|
||||
/** Il tooltip di UN grafico: data (estesa), valore + unità per ogni linea attiva, e — se la
|
||||
* linea ha un `extra` (unità A) — la sua etichetta/unità lette dal registro, mai indovinate. */
|
||||
function tooltipDelGrafico(grafico: GraficoAndamento) {
|
||||
const extraDiTestId = new Map<string, LineaAndamento['extra']>(
|
||||
grafico.linee.filter((l) => l.extra).map((l) => [l.testId, l.extra])
|
||||
);
|
||||
return function ContenutoTooltip({ active, payload, label }: TooltipContentProps) {
|
||||
if (!active || !payload || payload.length === 0 || typeof label !== 'number') return null;
|
||||
return (
|
||||
<div style={{ background: '#fff', border: `1px solid ${LINEA}`, padding: '8px 10px', fontSize: 12 }}>
|
||||
<p style={{ margin: '0 0 4px', color: TESTO_SCURO, fontWeight: 600 }}>{formattaTsEsteso(label)}</p>
|
||||
{payload.map((voce) => {
|
||||
const testId = String(voce.dataKey);
|
||||
const extra = extraDiTestId.get(testId);
|
||||
const valoreExtra = extra ? (voce.payload as RigaGrafico)[`${testId}__extra`] : undefined;
|
||||
return (
|
||||
<p key={testId} style={{ margin: '2px 0', color: TESTO_SCURO }}>
|
||||
{voce.name}: {formattaNumero(voce.value as number)}{grafico.unita ? ` ${grafico.unita}` : ''}
|
||||
{extra && typeof valoreExtra === 'number' && (
|
||||
<> — {extra.etichetta}: {formattaNumero(valoreExtra)}{extra.unita ? ` ${extra.unita}` : ''}</>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function GraficoTest({ grafico }: { grafico: GraficoAndamento }) {
|
||||
const righe = righeDelGrafico(grafico);
|
||||
if (righe.length === 0) return null; // andamentoTest garantisce ≥1 misura, rete secondaria
|
||||
const dominio = dominioTemporale(righe);
|
||||
const ContenutoTooltip = tooltipDelGrafico(grafico);
|
||||
|
||||
return (
|
||||
<div className="lg-andamento__grafico">
|
||||
<p className="lg-andamento__grafico-titolo">{grafico.titolo}{grafico.unita ? ` (${grafico.unita})` : ''}</p>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={righe} margin={{ top: 8, right: 12, bottom: 0, left: -18 }}>
|
||||
<CartesianGrid stroke={LINEA} strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="ts" type="number" scale="time" domain={dominio}
|
||||
tickFormatter={formattaDataCompatta} stroke={TESTO_CHIARO} fontSize={10} tickLine={false}
|
||||
/>
|
||||
<YAxis stroke={TESTO_CHIARO} fontSize={11} tickLine={false} axisLine={false} domain={['auto', 'auto']} />
|
||||
<Tooltip content={ContenutoTooltip} />
|
||||
{grafico.linee.length > 1 && <Legend iconType="plainline" wrapperStyle={{ fontSize: 11 }} />}
|
||||
{grafico.linee.map((linea, i) => {
|
||||
const stile = STILI_LINEA[i % STILI_LINEA.length]!;
|
||||
return (
|
||||
<Line
|
||||
key={linea.testId}
|
||||
type="monotone"
|
||||
dataKey={linea.testId}
|
||||
name={linea.etichetta}
|
||||
stroke={ACCENTO}
|
||||
strokeWidth={stile.width}
|
||||
strokeDasharray={stile.dash}
|
||||
dot={{ r: 3 }}
|
||||
connectNulls={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AndamentoTest({ sezioni }: Props) {
|
||||
if (sezioni.length === 0) {
|
||||
return <p className="lg-muted">Nessun test ancora registrato.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lg-andamento">
|
||||
{sezioni.map((sezione) => (
|
||||
<section className="lg-andamento__sezione" key={sezione.titolo}>
|
||||
<h3 className="lg-andamento__sezione-titolo">{sezione.titolo}</h3>
|
||||
{sezione.cambi.length > 0 && (
|
||||
<ul className="lg-andamento__cambi">
|
||||
{sezione.cambi.map((c) => (
|
||||
<li key={`${c.data}-${c.a}`}>
|
||||
Dal {formattaDataEstesa(c.data)}: {c.a} (prima: {c.da})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="lg-andamento__grafici">
|
||||
{sezione.grafici.map((grafico) => (
|
||||
<GraficoTest key={grafico.titolo} grafico={grafico} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -120,7 +120,13 @@ const CATALOGO: SezioneCatalogo[] = [
|
||||
const TITOLO_ALTRE_MISURE = 'Altre misure';
|
||||
|
||||
export type PuntoTest = { data: string; sessioneId: number; valore: number; extra?: number };
|
||||
export type LineaAndamento = { testId: string; etichetta: string; punti: PuntoTest[] };
|
||||
/** `extra`, quando la linea ha un `extraTestId` nel catalogo (es. le ripetizioni del 5RM,
|
||||
* la FC della camminata 2 km): etichetta e unità del valore che `punti[].extra` porta, lette
|
||||
* da `registro_test` come tutto il resto — il tooltip (unità B) le mostra invece di
|
||||
* indovinarle dal solo numero nudo. Assente quando la linea non ha un `extraTestId`. */
|
||||
export type LineaAndamento = {
|
||||
testId: string; etichetta: string; punti: PuntoTest[]; extra?: { etichetta: string; unita: string | null };
|
||||
};
|
||||
export type GraficoAndamento = { titolo: string; unita: string | null; linee: LineaAndamento[] };
|
||||
export type CambioSezione = { data: string; da: string; a: string };
|
||||
export type SezioneAndamento = { titolo: string; grafici: GraficoAndamento[]; cambi: CambioSezione[] };
|
||||
@@ -219,6 +225,7 @@ export function andamentoTest(
|
||||
if (!punti || punti.length === 0) return null;
|
||||
const voce = registro.get(l.testId);
|
||||
const mappaExtra = l.extraTestId ? extraPerTestESessione.get(l.extraTestId) : undefined;
|
||||
const voceExtra = l.extraTestId ? registro.get(l.extraTestId) : undefined;
|
||||
return {
|
||||
testId: l.testId,
|
||||
etichetta: voce?.etichetta ?? l.testId,
|
||||
@@ -226,6 +233,7 @@ export function andamentoTest(
|
||||
const extra = mappaExtra?.get(p.sessioneId);
|
||||
return extra === undefined ? { ...p } : { ...p, extra };
|
||||
}),
|
||||
...(voceExtra ? { extra: { etichetta: voceExtra.etichetta, unita: voceExtra.unita } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ const flusso = params.get('flusso');
|
||||
{!cliente.ha_accesso && (
|
||||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/accesso`}>Crea accesso al referto</a>
|
||||
)}
|
||||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/andamento`}>Andamento</a>
|
||||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup`}>Nuovo check-up</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
// L'andamento nel tempo per il trainer (issue #14, unità B): gli stessi grafici del
|
||||
// cliente più quelli soloTrainer (`andamentoTest(..., { trainer: true })` — FC a riposo,
|
||||
// dettaglio MUTT, FC per step, recupero cardiaco storico), e il grafico degli assi
|
||||
// (progressioneDi, come in progressione.astro) quando ci sono almeno due sessioni con
|
||||
// punteggi congelati. Stessa protezione/struttura delle pagine sorelle: checkup.astro e
|
||||
// consulenza.astro — nessuna query qui, tutto passa da lib/longevity.
|
||||
import Longevity from '../../../../../layouts/Longevity.astro';
|
||||
import Progressione from '../../../../../components/longevity/Progressione';
|
||||
import AndamentoTest from '../../../../../components/longevity/AndamentoTest';
|
||||
import { getLongevityDb } from '../../../../../lib/longevity/db';
|
||||
import { clienteDaCodice } from '../../../../../lib/longevity/anagrafica';
|
||||
import { progressioneDi } from '../../../../../lib/longevity/progressione';
|
||||
import { andamentoTest } from '../../../../../lib/longevity/andamento';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const { code } = Astro.params;
|
||||
const cliente = code ? clienteDaCodice(code) : null;
|
||||
if (!cliente) return new Response('Cliente non trovato', { status: 404 });
|
||||
|
||||
const db = getLongevityDb();
|
||||
const sezioniTest = andamentoTest(db, cliente.client_code, { trainer: true });
|
||||
const punti = progressioneDi(db, cliente.client_code);
|
||||
---
|
||||
<Longevity
|
||||
title={`Andamento — ${cliente.cognome} ${cliente.nome}`}
|
||||
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, 'Andamento']}
|
||||
variante="gestionale"
|
||||
>
|
||||
<div class="lg-head">
|
||||
<h1>Andamento</h1>
|
||||
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code}.</p>
|
||||
</div>
|
||||
|
||||
<section class="lg-card">
|
||||
<h2>Test</h2>
|
||||
<AndamentoTest client:load sezioni={sezioniTest} />
|
||||
</section>
|
||||
|
||||
{punti.length >= 2 && (
|
||||
<section class="lg-card">
|
||||
<h2>Assi, macro e Fitness Age</h2>
|
||||
<Progressione client:load punti={punti} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="lg-card">
|
||||
<p><a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Torna alla scheda cliente</a>.</p>
|
||||
</section>
|
||||
</Longevity>
|
||||
@@ -5,11 +5,17 @@
|
||||
// sotto src/pages/longevity/), il codice cliente arriva da codicePerUtenteLoggato
|
||||
// (anagrafica.ts, l'unico modulo autorizzato a toccare identity), i punti da progressioneDi
|
||||
// (progressione.ts, che legge SOLO score — mai un ricalcolo).
|
||||
// «I tuoi test» (issue #14, unità B) usa andamento.ts con `trainer: false`: il cliente vede
|
||||
// i suoi test (compresi i dismessi, sono la sua storia), mai i grafici soloTrainer. Sezione
|
||||
// indipendente dal grafico degli assi sotto: quella resta con la sua condizione ≥2 sessioni
|
||||
// (DECISIONI dell'issue), questa è visibile da 1 misura in su (DECISIONI g).
|
||||
import Longevity from '../../layouts/Longevity.astro';
|
||||
import Progressione from '../../components/longevity/Progressione';
|
||||
import AndamentoTest from '../../components/longevity/AndamentoTest';
|
||||
import { getLongevityDb } from '../../lib/longevity/db';
|
||||
import { codicePerUtenteLoggato } from '../../lib/longevity/anagrafica';
|
||||
import { progressioneDi } from '../../lib/longevity/progressione';
|
||||
import { andamentoTest } from '../../lib/longevity/andamento';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -17,6 +23,7 @@ const user = Astro.locals.user!;
|
||||
const codice = codicePerUtenteLoggato(user.id);
|
||||
const db = getLongevityDb();
|
||||
const punti = codice ? progressioneDi(db, codice) : [];
|
||||
const sezioniTest = codice ? andamentoTest(db, codice, { trainer: false }) : [];
|
||||
---
|
||||
<Longevity title="Andamento nel tempo" crumbs={['Andamento nel tempo']}>
|
||||
{!codice && (
|
||||
@@ -25,6 +32,13 @@ const punti = codice ? progressioneDi(db, codice) : [];
|
||||
</div>
|
||||
)}
|
||||
|
||||
{codice && (
|
||||
<section class="lg-card">
|
||||
<h2>I tuoi test</h2>
|
||||
<AndamentoTest client:load sezioni={sezioniTest} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{codice && punti.length < 2 && (
|
||||
<div class="lg-card lg-card--incompleto">
|
||||
<p>Serve almeno un secondo check-up o questionario per vedere un andamento.</p>
|
||||
|
||||
@@ -127,6 +127,21 @@
|
||||
}
|
||||
.lg-progressione__table th { color: var(--c-text-light); font-weight: 600; }
|
||||
|
||||
/* --- l'andamento di ogni test (#14): il grafico vive nell'isola React
|
||||
(AndamentoTest.tsx), qui il titolo di sezione, le righe di cambio test e la griglia dei
|
||||
grafici piccoli — un grafico per test, leggibile anche a 360px (una colonna sola). --- */
|
||||
.lg-andamento { display: flex; flex-direction: column; gap: 28px; }
|
||||
.lg-andamento__sezione { display: flex; flex-direction: column; gap: 10px; }
|
||||
.lg-andamento__sezione-titolo {
|
||||
margin: 0; font-family: var(--font-heading); font-size: .95rem; font-weight: 600;
|
||||
color: var(--c-heading); border-bottom: 1px solid var(--lg-line); padding-bottom: 6px;
|
||||
}
|
||||
.lg-andamento__cambi { margin: 0; padding-left: 18px; font-size: .82rem; color: var(--c-text-light); }
|
||||
.lg-andamento__cambi li { margin-bottom: 2px; }
|
||||
.lg-andamento__grafici { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
||||
.lg-andamento__grafico { border: 1px solid var(--lg-line); padding: 10px 12px 4px; }
|
||||
.lg-andamento__grafico-titolo { margin: 0 0 4px; font-size: .82rem; font-weight: 600; color: var(--c-heading); }
|
||||
|
||||
/* --- le carte dei macro-score --- */
|
||||
.lg-macro { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; }
|
||||
.lg-macro__nota { margin: 8px 0 0; font-size: .78rem; }
|
||||
|
||||
@@ -74,6 +74,31 @@ describe('andamentoTest: serie nel tempo, buchi e doppie linee', () => {
|
||||
expect(rematore.linee[0]!.testId).toBe('f_row_carico_kg');
|
||||
expect(rematore.linee[0]!.punti[0]).toEqual({ data: '2026-01-10', sessioneId: s1, valore: 60, extra: 5 });
|
||||
});
|
||||
|
||||
it('la linea con extraTestId porta etichetta e unità del valore in tooltip, lette dal registro (unità B non indovina)', () => {
|
||||
const db = dbPronto();
|
||||
const s1 = checkup(db, '2026-01-10');
|
||||
registraMisure(db, s1, 'manuale', [
|
||||
{ test_id: 'f_row_carico_kg', valore_num: 60 },
|
||||
{ test_id: 'f_row_rip', valore_num: 5 },
|
||||
{ test_id: 'f_walk2km_min', valore_num: 18 },
|
||||
{ test_id: 'f_walk2km_fc_bpm', valore_num: 140 },
|
||||
{ test_id: 'f_peso_kg', valore_num: 80 },
|
||||
]);
|
||||
|
||||
const sezioni = andamentoTest(db, 'ISL-0001', { trainer: true });
|
||||
|
||||
const rematore = sezioni.find((s) => s.titolo === 'Trazione')!.grafici.find((g) => g.titolo === 'Rematore')!;
|
||||
// f_row_rip in registro.ts non ha `unita`: deve arrivare null, mai undefined (silenzioso).
|
||||
expect(rematore.linee[0]!.extra).toEqual({ etichetta: 'Trazione/rematore, ripetizioni (test 5RM)', unita: null });
|
||||
|
||||
const camminata = sezioni.find((s) => s.titolo === 'Aerobico')!.grafici.find((g) => g.titolo === 'Camminata 2 km')!;
|
||||
expect(camminata.linee[0]!.extra).toEqual({ etichetta: 'Camminata 2 km (UKK), frequenza cardiaca finale', unita: 'bpm' });
|
||||
|
||||
// Una linea senza extraTestId (es. Peso) non ha proprietà `extra`.
|
||||
const peso = sezioni.find((s) => s.titolo === 'Pre-test')!.grafici.find((g) => g.titolo === 'Peso')!;
|
||||
expect(peso.linee[0]!.extra).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('andamentoTest: cambio di test dentro la stessa sezione (Trazione)', () => {
|
||||
|
||||
@@ -121,6 +121,13 @@ describe('il guscio della piattaforma', () => {
|
||||
// in un file fuori dai due moduli autorizzati.
|
||||
expect(src).not.toContain('Identity');
|
||||
});
|
||||
|
||||
it('progressione.astro usa andamento.ts con trainer:false (#14): il cliente non deve vedere i grafici riservati', () => {
|
||||
const p = join(process.cwd(), 'src/pages/longevity/progressione.astro');
|
||||
const src = readFileSync(p, 'utf8');
|
||||
expect(src).toMatch(/from ['"`].*lib\/longevity\/andamento['"`]/);
|
||||
expect(src).toMatch(/andamentoTest\([^)]*trainer:\s*false/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('il gestionale del trainer (Unità 2b)', () => {
|
||||
@@ -130,6 +137,7 @@ describe('il gestionale del trainer (Unità 2b)', () => {
|
||||
'src/pages/longevity/gestionale/cliente/[code].astro',
|
||||
'src/pages/longevity/gestionale/cliente/[code]/checkup.astro',
|
||||
'src/pages/longevity/gestionale/cliente/[code]/consulenza.astro', // #15
|
||||
'src/pages/longevity/gestionale/cliente/[code]/andamento.astro', // #14
|
||||
];
|
||||
|
||||
it('ogni pagina del gestionale esiste e usa la variante "gestionale" del layout', () => {
|
||||
@@ -170,6 +178,17 @@ describe('il gestionale del trainer (Unità 2b)', () => {
|
||||
expect(src).toMatch(/\/longevity\/gestionale\/cliente\/\$\{cliente\.client_code\}\/accesso/);
|
||||
});
|
||||
|
||||
it('la scheda cliente porta il link "Andamento" accanto a "Nuovo check-up" (#14)', () => {
|
||||
const src = leggi('src/pages/longevity/gestionale/cliente/[code].astro');
|
||||
expect(src).toMatch(/\/longevity\/gestionale\/cliente\/\$\{cliente\.client_code\}\/andamento/);
|
||||
});
|
||||
|
||||
it('la pagina andamento del gestionale usa andamento.ts con trainer:true (#14): il trainer vede anche i grafici riservati', () => {
|
||||
const src = leggi('src/pages/longevity/gestionale/cliente/[code]/andamento.astro');
|
||||
expect(src).toMatch(/from ['"`].*lib\/longevity\/andamento['"`]/);
|
||||
expect(src).toMatch(/andamentoTest\([^)]*trainer:\s*true/);
|
||||
});
|
||||
|
||||
it('nessuna pagina longevity usa piu misureFuoriRange, rimossa da gestionale.ts a favore di misureDaVerificare (#7)', () => {
|
||||
// Scandisce TUTTE le pagine sotto src/pages/longevity/, stesso criterio di
|
||||
// referto.test.ts: un elenco scritto qui non protegge una pagina nuova.
|
||||
|
||||
Reference in New Issue
Block a user