longevity: modulo check-up con sezioni e alternative del manuale (#13)
strutturaCheckup in gestionale.ts (ordine Pre-test..Strumenti, sezioni ad alternative con soglia testuale, «Altre misure» per i test dismessi in correzione, pressione/SpO2 esclusi); checkup.astro con radio button e fieldset (niente <select>); API checkup.ts scarta i campi delle alternative non scelte e aggiunge una voce di giornale dalla nota operatore sul check-up nuovo. Refs #13 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,11 @@ import type { APIRoute } from 'astro';
|
||||
import { getLongevityDb } from '../../../../lib/longevity/db';
|
||||
import { etaDelCliente } from '../../../../lib/longevity/anagrafica';
|
||||
import { testAttivi, type VoceRegistro } from '../../../../lib/longevity/registro';
|
||||
import { salvaCheckup, correggiCheckup } from '../../../../lib/longevity/gestionale';
|
||||
import {
|
||||
salvaCheckup, correggiCheckup, strutturaCheckup, testIdScartatiDalleAlternative,
|
||||
type SezioneCheckup,
|
||||
} from '../../../../lib/longevity/gestionale';
|
||||
import { aggiungiVoce } from '../../../../lib/longevity/giornale';
|
||||
import type { MisuraIn } from '../../../../lib/longevity/misure';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -52,6 +56,22 @@ export function misureDalForm(
|
||||
return { ok: true, misure };
|
||||
}
|
||||
|
||||
/**
|
||||
* Le scelte del trainer nelle sezioni ad alternative del modulo (#13): legge il radio
|
||||
* `alt_<chiave sezione>` 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.
|
||||
*/
|
||||
function sceltePerAlternative(sezioni: SezioneCheckup[], form: FormData): Map<string, string> {
|
||||
const scelte = new Map<string, string>();
|
||||
for (const s of sezioni) {
|
||||
if (s.tipo !== 'alternative') continue;
|
||||
const v = form.get(`alt_${s.chiave}`);
|
||||
if (typeof v === 'string' && v !== '') scelte.set(s.chiave, v);
|
||||
}
|
||||
return scelte;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const form = await request.formData();
|
||||
const clientCodeForm = String(form.get('client_code') ?? '').trim();
|
||||
@@ -105,22 +125,36 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const parse = misureDalForm(form, campiAttivi);
|
||||
if (!parse.ok) return erroreAllaScheda(parse.errore);
|
||||
|
||||
// Sezioni ad alternative (#13): si tengono solo i campi dell'alternativa scelta dalla
|
||||
// radio, i compilati di altre alternative della stessa sezione si scartano — anche se il
|
||||
// trainer li ha riempiti (JS disattivato, o un fieldset lasciato visibile per errore).
|
||||
const { sezioni } = strutturaCheckup(db, data);
|
||||
const scartati = testIdScartatiDalleAlternative(sezioni, sceltePerAlternative(sezioni, form));
|
||||
const misure = parse.misure.filter((m) => !scartati.has(m.test_id));
|
||||
|
||||
// L'operatore è SEMPRE quello della sessione loggata (garantita da RULES + middleware:
|
||||
// solo admin/trainer arrivano qui), mai un campo del form — lo stesso principio del
|
||||
// client_code in pages/api/longevity/questionario.ts, applicato al campo che qui conta.
|
||||
const operatore = locals.user!.username;
|
||||
const eta = etaDelCliente(clientCode, data);
|
||||
// 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();
|
||||
|
||||
if (sessioneId !== null) {
|
||||
const esito = correggiCheckup(db, { sessioneId, data, eta, operatore, misure: parse.misure });
|
||||
const esito = correggiCheckup(db, { sessioneId, data, eta, operatore, misure });
|
||||
if (!esito.ok) return erroreAllaScheda(esito.errore);
|
||||
const params = new URLSearchParams({ corretto: String(esito.sessioneId) });
|
||||
if (esito.fuoriRange.length > 0) params.set('fuori', String(esito.fuoriRange.length));
|
||||
return redirect(`/longevity/gestionale/cliente/${clientCode}?${params.toString()}`);
|
||||
}
|
||||
|
||||
const esito = salvaCheckup(db, { client_code: clientCode, data, eta, operatore, misure: parse.misure });
|
||||
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.
|
||||
if (nota !== '') aggiungiVoce(db, { clientCode, data, autore: operatore, testo: nota });
|
||||
|
||||
const params = new URLSearchParams({ salvato: String(esito.sessioneId) });
|
||||
if (esito.fuoriRange.length > 0) params.set('fuori', String(esito.fuoriRange.length));
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
import Longevity from '../../../../../layouts/Longevity.astro';
|
||||
import { getLongevityDb } from '../../../../../lib/longevity/db';
|
||||
import { clienteDaCodice } from '../../../../../lib/longevity/anagrafica';
|
||||
import { campiDelCheckup, misureDellaSessione } from '../../../../../lib/longevity/gestionale';
|
||||
import {
|
||||
misureDellaSessione, strutturaCheckup, alternativaSelezionata, valoriUltimoCheckup,
|
||||
} from '../../../../../lib/longevity/gestionale';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -30,8 +32,13 @@ if (sessioneId !== null && (!sessione || sessione.client_code !== cliente.client
|
||||
}
|
||||
|
||||
const dataForm = sessione?.data ?? oggi;
|
||||
const gruppi = campiDelCheckup(db, dataForm);
|
||||
const modulo = strutturaCheckup(db, dataForm);
|
||||
const valori = new Map(sessione?.misure.map((m) => [m.test_id, m.valore_num]) ?? []);
|
||||
// Preselezione (#13): in correzione le misure della sessione, in un check-up nuovo l'ultimo
|
||||
// check-up del cliente — altrimenti la prima alternativa (il livello neofita).
|
||||
const valoriPreselezione = sessione
|
||||
? new Map([...valori.entries()].filter((kv): kv is [string, number] => kv[1] !== null))
|
||||
: valoriUltimoCheckup(db, cliente.client_code);
|
||||
const errore = Astro.url.searchParams.get('errore');
|
||||
---
|
||||
<Longevity
|
||||
@@ -55,17 +62,73 @@ const errore = Astro.url.searchParams.get('errore');
|
||||
<input type="date" name="data" value={dataForm} required />
|
||||
</label>
|
||||
|
||||
{gruppi.map((g) => (
|
||||
{modulo.sezioni.map((s) => (
|
||||
<section class="lg-form__blocco">
|
||||
<h2>{g.asse}</h2>
|
||||
{g.campi.map((c) => (
|
||||
<h2>{s.titolo}</h2>
|
||||
{s.tipo === 'campi' ? s.campi.map((c) => (
|
||||
<label class="lg-field">
|
||||
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
||||
<input type="text" inputmode="decimal" name={c.id} value={valori.get(c.id) ?? ''} />
|
||||
</label>
|
||||
)) : (
|
||||
<div class="lg-form__alternative" data-alt-sezione={s.chiave}>
|
||||
{s.alternative.map((alt) => (
|
||||
<fieldset class="lg-form__alternativa" data-alt-fieldset={alt.chiave}>
|
||||
<legend>
|
||||
<label>
|
||||
<input
|
||||
type="radio" name={`alt_${s.chiave}`} value={alt.chiave}
|
||||
checked={alt.chiave === alternativaSelezionata(s, valoriPreselezione)}
|
||||
/>
|
||||
{alt.nome}
|
||||
</label>
|
||||
</legend>
|
||||
{alt.soglia && <p class="lg-form__soglia">{alt.soglia}</p>}
|
||||
{alt.campi.map((c) => (
|
||||
<label class="lg-field">
|
||||
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
||||
<input type="text" inputmode="decimal" name={c.id} value={valori.get(c.id) ?? ''} />
|
||||
</label>
|
||||
))}
|
||||
{alt.dettagli && alt.dettagli.length > 0 && (
|
||||
<details>
|
||||
<summary>Frequenza cardiaca per step (7,5-14,5 km/h)</summary>
|
||||
{alt.dettagli.map((c) => (
|
||||
<label class="lg-field">
|
||||
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
||||
<input type="text" inputmode="decimal" name={c.id} value={valori.get(c.id) ?? ''} />
|
||||
</label>
|
||||
))}
|
||||
</details>
|
||||
)}
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
|
||||
{modulo.altreMisure.length > 0 && (
|
||||
<section class="lg-form__blocco">
|
||||
<h2>Altre misure</h2>
|
||||
{modulo.altreMisure.map((c) => (
|
||||
<label class="lg-field">
|
||||
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
||||
<input type="text" inputmode="decimal" name={c.id} value={valori.get(c.id) ?? ''} />
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
)}
|
||||
|
||||
{!sessione && (
|
||||
<section class="lg-form__blocco">
|
||||
<h2>Note operatore</h2>
|
||||
<label class="lg-field">
|
||||
<span>Nota per il giornale di bordo (facoltativa)</span>
|
||||
<textarea name="nota" rows="4"></textarea>
|
||||
</label>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div class="lg-form__nav">
|
||||
<a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Annulla</a>
|
||||
@@ -73,3 +136,19 @@ const errore = Astro.url.searchParams.get('errore');
|
||||
</div>
|
||||
</form>
|
||||
</Longevity>
|
||||
|
||||
<script>
|
||||
// Nasconde i fieldset delle alternative non scelte (#13): senza JS restano tutti visibili
|
||||
// e il form funziona lo stesso — lo scarto vero è server-side (checkup.ts), questo è solo
|
||||
// per non far compilare al trainer due protocolli della stessa sotto-metrica per sbaglio.
|
||||
document.querySelectorAll<HTMLElement>('[data-alt-sezione]').forEach((sezioneEl) => {
|
||||
const aggiorna = () => {
|
||||
const scelta = sezioneEl.querySelector<HTMLInputElement>('input[type=radio]:checked')?.value;
|
||||
sezioneEl.querySelectorAll<HTMLElement>('[data-alt-fieldset]').forEach((fs) => {
|
||||
fs.hidden = fs.dataset.altFieldset !== scelta;
|
||||
});
|
||||
};
|
||||
sezioneEl.addEventListener('change', aggiorna);
|
||||
aggiorna();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user