From b0f30f9e4c1497c5289d857d5cd6e74113f58159 Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Sun, 27 Sep 2026 22:59:19 +0200 Subject: [PATCH] =?UTF-8?q?longevity:=20correzioni=20passo=203=20su=20#13?= =?UTF-8?q?=20unit=C3=A0=20B=20=E2=80=94=20scarto=20alternative=20e=20nota?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testIdScartatiDalleAlternative: una sezione senza scelta in `scelte` (radio assente dal form) non scarta più nulla, invece di ripiegare in silenzio sulla prima alternativa (perdeva campi come f_plank_sec senza alt_core). API checkup.ts: la nota operatore si valida (lunghezza max 5000, TESTO_MAX esportato da giornale.ts) PRIMA di salvare il check-up, non più dopo — una nota troppo lunga non faceva più fallire aggiungiVoce in silenzio ma perdeva comunque la nota col check-up già scritto. maxlength sulla textarea in checkup.astro. Suite: 314 verdi, 4 rossi di piattaforma (baseline, invariati). tsc e build puliti. Refs #13 Co-Authored-By: Claude Sonnet 5 --- .../platforms/src/lib/longevity/gestionale.ts | 10 ++++-- apps/platforms/src/lib/longevity/giornale.ts | 4 ++- .../pages/api/longevity/gestionale/checkup.ts | 19 +++++++---- .../gestionale/cliente/[code]/checkup.astro | 2 +- .../longevity/checkup-alternative-api.test.ts | 32 ++++++++++++++----- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/apps/platforms/src/lib/longevity/gestionale.ts b/apps/platforms/src/lib/longevity/gestionale.ts index 8ae508a..07fc5d5 100644 --- a/apps/platforms/src/lib/longevity/gestionale.ts +++ b/apps/platforms/src/lib/longevity/gestionale.ts @@ -228,8 +228,11 @@ export function valoriUltimoCheckup(db: Database.Database, clientCode: string): /** * I test_id da scartare perché appartengono a un'alternativa NON scelta di una sezione (#13): * `scelte` mappa la chiave di sezione alla chiave dell'alternativa scelta (dalla radio nel - * form) — assente = vale la prima alternativa (comportamento del form senza JS/radio - * mancante). Una sezione a campi fissi non contribuisce: nulla da scartare lì. + * form). Una sezione ASSENTE da `scelte` (nessuna radio arrivata: form senza JS che non ha + * ancora marcato nulla, o client non-browser) non scarta NIENTE — ripiegare sulla prima + * alternativa butterebbe via in silenzio campi compilati di un'altra (es. f_plank_sec senza + * alt_core), correzione del passo 3 su #13. Una sezione a campi fissi non contribuisce: nulla + * da scartare lì. */ export function testIdScartatiDalleAlternative( sezioni: SezioneCheckup[], scelte: Map @@ -237,7 +240,8 @@ export function testIdScartatiDalleAlternative( const scartati = new Set(); for (const s of sezioni) { if (s.tipo !== 'alternative') continue; - const scelta = scelte.get(s.chiave) ?? s.alternative[0]?.chiave; + const scelta = scelte.get(s.chiave); + if (scelta === undefined) continue; // nessuna radio per questa sezione: non si scarta nulla for (const alt of s.alternative) { if (alt.chiave === scelta) continue; for (const c of alt.campi) scartati.add(c.id); diff --git a/apps/platforms/src/lib/longevity/giornale.ts b/apps/platforms/src/lib/longevity/giornale.ts index 1e20810..27db8e1 100644 --- a/apps/platforms/src/lib/longevity/giornale.ts +++ b/apps/platforms/src/lib/longevity/giornale.ts @@ -7,7 +7,9 @@ import type Database from 'better-sqlite3'; const REGEX_DATA = /^\d{4}-\d{2}-\d{2}$/; -const TESTO_MAX = 5000; +// Esportato (#13, passo 3): l'API del check-up valida la nota PRIMA di salvare, con lo +// stesso limite di aggiungiVoce — un numero duplicato qui e lì marcirebbe silenziosamente. +export const TESTO_MAX = 5000; export type VoceGiornale = { id: number; diff --git a/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts b/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts index 330dfc9..4afce49 100644 --- a/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts +++ b/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts @@ -6,7 +6,7 @@ import { salvaCheckup, correggiCheckup, strutturaCheckup, testIdScartatiDalleAlternative, type SezioneCheckup, } from '../../../../lib/longevity/gestionale'; -import { aggiungiVoce } from '../../../../lib/longevity/giornale'; +import { aggiungiVoce, TESTO_MAX as NOTA_TESTO_MAX } from '../../../../lib/longevity/giornale'; import type { MisuraIn } from '../../../../lib/longevity/misure'; export const prerender = false; @@ -59,8 +59,9 @@ export function misureDalForm( /** * Le scelte del trainer nelle sezioni ad alternative del modulo (#13): legge il radio * `alt_` dal form. Una sezione senza radio nel form (JS disattivato che non - * ha ancora impostato nulla, o un refuso) resta assente dalla mappa — la sceglie - * `testIdScartatiDalleAlternative` per default sulla prima alternativa dell'elenco. + * ha ancora marcato nulla, o un refuso) resta assente dalla mappa: `testIdScartatiDalleAlternative` + * non scarta NULLA per quella sezione, invece di ripiegare sulla prima alternativa e buttare + * via in silenzio campi compilati di un'altra — correzione del passo 3 su #13. */ function sceltePerAlternative(sezioni: SezioneCheckup[], form: FormData): Map { const scelte = new Map(); @@ -140,6 +141,12 @@ export const POST: APIRoute = async ({ request, locals }) => { // Nota operatore (#13, #16): il form la mostra solo per un check-up nuovo (checkup.astro), // quindi in correzione la chiave è assente e questo resta vuoto — nessuna voce si aggiunge. const nota = String(form.get('nota') ?? '').trim(); + // Validata PRIMA di salvare (correzione del passo 3 su #13): se aggiungiVoce fallisse DOPO + // il check-up (nota troppo lunga), il check-up resterebbe salvato ma la nota andrebbe persa + // in silenzio — stesso limite di aggiungiVoce (giornale.ts), non un numero duplicato qui. + if (nota.length > NOTA_TESTO_MAX) { + return erroreAllaScheda(`Il testo della nota non può superare ${NOTA_TESTO_MAX} caratteri.`); + } if (sessioneId !== null) { const esito = correggiCheckup(db, { sessioneId, data, eta, operatore, misure }); @@ -151,9 +158,9 @@ export const POST: APIRoute = async ({ request, locals }) => { const esito = salvaCheckup(db, { client_code: clientCode, data, eta, operatore, misure }); if (!esito.ok) return erroreAllaScheda(esito.errore); - // Voce di giornale SOLO dopo il salvataggio riuscito (#13): se la nota fosse rifiutata - // (troppo lunga, data futura — non dovrebbe succedere, stessa data già validata sopra) - // il check-up resta comunque salvato, non si perde per un problema della nota. + // Voce di giornale SOLO dopo il salvataggio riuscito (#13): la lunghezza è già validata + // sopra, la data è la stessa già validata per il check-up — aggiungiVoce qui non dovrebbe + // più rifiutare nulla. if (nota !== '') aggiungiVoce(db, { clientCode, data, autore: operatore, testo: nota }); const params = new URLSearchParams({ salvato: String(esito.sessioneId) }); diff --git a/apps/platforms/src/pages/longevity/gestionale/cliente/[code]/checkup.astro b/apps/platforms/src/pages/longevity/gestionale/cliente/[code]/checkup.astro index 1b8ec39..8616422 100644 --- a/apps/platforms/src/pages/longevity/gestionale/cliente/[code]/checkup.astro +++ b/apps/platforms/src/pages/longevity/gestionale/cliente/[code]/checkup.astro @@ -125,7 +125,7 @@ const errore = Astro.url.searchParams.get('errore');

Note operatore

)} diff --git a/apps/platforms/tests/longevity/checkup-alternative-api.test.ts b/apps/platforms/tests/longevity/checkup-alternative-api.test.ts index 3cc7f15..16056f7 100644 --- a/apps/platforms/tests/longevity/checkup-alternative-api.test.ts +++ b/apps/platforms/tests/longevity/checkup-alternative-api.test.ts @@ -31,7 +31,7 @@ beforeAll(() => { lg = getLongevityDb(); seedRegistro(lg); seedPesi(lg, MODEL_VERSION); - for (const code of ['ISL-0001', 'ISL-0002', 'ISL-0003', 'ISL-0004', 'ISL-0005']) { + for (const code of ['ISL-0001', 'ISL-0002', 'ISL-0003', 'ISL-0004', 'ISL-0005', 'ISL-0006']) { lg.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES (?, 'F')`).run(code); } }); @@ -67,23 +67,21 @@ describe('API checkup.ts, sezioni ad alternative e nota operatore (#13)', () => expect(ids).not.toContain('f_panca_bil_rip'); }); - it('senza radio inviata, vince la prima alternativa dell\'elenco: i campi delle altre si scartano lo stesso', async () => { + it('senza radio inviata (nessun alt_*), non si scarta nulla: un campo di un\'alternativa non-prima resta salvato (correzione passo 3, #13)', async () => { const { POST } = await import('../../src/pages/api/longevity/gestionale/checkup'); const form = new FormData(); form.set('client_code', 'ISL-0002'); form.set('data', OGGI); - // nessun alt_spinta: la prima alternativa di "Forza spinta" è 'manubri' - form.set('f_panca_man_carico_kg', '20'); - form.set('f_panca_man_rip', '5'); - form.set('f_pushup_rip', '15'); // non è la prima alternativa: va scartato + // nessun alt_core: 'ginocchia' è la prima alternativa di "Core", f_plank_sec è 'pieno' + // (la seconda) — prima della correzione questo campo veniva scartato in silenzio. + form.set('f_plank_sec', '90'); const richiesta = new Request('http://localhost/api/longevity/gestionale/checkup', { method: 'POST', body: form }); const res = await POST({ request: richiesta, locals: { user: { username: 'nicola' } } } as any); const sessioneId = Number(new URL(res.headers.get('Location')!, 'http://localhost').searchParams.get('salvato')); const ids = (lg.prepare(`SELECT test_id FROM misure WHERE sessione_id = ?`).all(sessioneId) as { test_id: string }[]) .map((r) => r.test_id); - expect(ids).toContain('f_panca_man_carico_kg'); - expect(ids).not.toContain('f_pushup_rip'); + expect(ids).toContain('f_plank_sec'); }); it('nota operatore non vuota: aggiunge una voce di giornale con la data del check-up e l\'operatore come autore', async () => { @@ -137,4 +135,22 @@ describe('API checkup.ts, sezioni ad alternative e nota operatore (#13)', () => await POST({ request: richiesta, locals: { user: { username: 'nicola' } } } as any); expect(vociDelGiornale(lg, 'ISL-0005').length).toBe(0); }); + + it('nota oltre 5000 caratteri: errore PRIMA di salvare, nessuna sessione né voce di giornale (correzione passo 3, #13)', async () => { + const { POST } = await import('../../src/pages/api/longevity/gestionale/checkup'); + const form = new FormData(); + form.set('client_code', 'ISL-0006'); + form.set('data', OGGI); + form.set('f_peso_kg', '70'); + form.set('nota', 'x'.repeat(5001)); + const richiesta = new Request('http://localhost/api/longevity/gestionale/checkup', { method: 'POST', body: form }); + + const res = await POST({ request: richiesta, locals: { user: { username: 'nicola' } } } as any); + expect(res.status).toBe(303); + expect(res.headers.get('Location')).toMatch(/errore=/); + + const sessioni = lg.prepare(`SELECT COUNT(*) AS n FROM sessioni WHERE client_code = ?`).get('ISL-0006') as { n: number }; + expect(sessioni.n).toBe(0); // niente scritto: né la sessione né la misura f_peso_kg + expect(vociDelGiornale(lg, 'ISL-0006').length).toBe(0); + }); });