longevity: pagina dettaglio sessione e modifica check-up dalla scheda (#12)

Nuova pagina .../sessione/[id].astro: dettaglio misure per asse, con
link "Modifica" per un checkup. checkup.astro diventa correzione con
?sessione=<id> (campi precompilati, attivi alla data della sessione).
La scheda cliente linka ogni sessione e mostra l'esito della correzione.

Refs #12

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkWhSnSLvAyRCoWBuCpXvp
This commit is contained in:
2026-09-27 22:13:41 +02:00
parent 9e566ad7b1
commit 6bbfcd8f3c
3 changed files with 128 additions and 10 deletions
@@ -36,6 +36,7 @@ const formattaData = (iso: string) =>
const params = Astro.url.searchParams; const params = Astro.url.searchParams;
const salvato = params.get('salvato'); const salvato = params.get('salvato');
const corretto = params.get('corretto'); // #12: redirect di correggiCheckup dopo una correzione
const fuoriCount = params.get('fuori'); const fuoriCount = params.get('fuori');
const errore = params.get('errore'); const errore = params.get('errore');
const verificata = params.get('verificata'); const verificata = params.get('verificata');
@@ -69,6 +70,7 @@ const messaggioVerifica =
{errore && <p class="lg-form__errore">{errore}</p>} {errore && <p class="lg-form__errore">{errore}</p>}
{salvato && <p class="lg-card">Check-up salvato (sessione #{salvato}).</p>} {salvato && <p class="lg-card">Check-up salvato (sessione #{salvato}).</p>}
{corretto && <p class="lg-card">Check-up corretto (sessione #{corretto}): punteggi ricalcolati.</p>}
{messaggioVerifica && <p class="lg-card">{messaggioVerifica}</p>} {messaggioVerifica && <p class="lg-card">{messaggioVerifica}</p>}
{fuoriCount && ( {fuoriCount && (
<p class="lg-misura--fuori-range"> <p class="lg-misura--fuori-range">
@@ -131,7 +133,9 @@ const messaggioVerifica =
<tbody> <tbody>
{sessioni.map((s) => ( {sessioni.map((s) => (
<tr> <tr>
<td>{formattaData(s.data)}</td> <td>
<a href={`/longevity/gestionale/cliente/${cliente.client_code}/sessione/${s.id}`}>{formattaData(s.data)}</a>
</td>
<td>{ETICHETTA_TIPO[s.tipo]}</td> <td>{ETICHETTA_TIPO[s.tipo]}</td>
<td>{s.operatore ?? '—'}</td> <td>{s.operatore ?? '—'}</td>
<td>{s.numeroMisure}</td> <td>{s.numeroMisure}</td>
@@ -2,10 +2,14 @@
// Il modulo del check-up manuale: i campi letti dal registro (campiDelCheckup), // Il modulo del check-up manuale: i campi letti dal registro (campiDelCheckup),
// raggruppati per asse come il modulo cartaceo. L'invio va all'API del gestionale, che fa // raggruppati per asse come il modulo cartaceo. L'invio va all'API del gestionale, che fa
// la validazione vera (pages/api/longevity/gestionale/checkup.ts) — qui c'è solo il form. // la validazione vera (pages/api/longevity/gestionale/checkup.ts) — qui c'è solo il form.
//
// Con ?sessione=<id> (#12) lo stesso form diventa una correzione: data e campi
// precompilati dalla sessione, hidden sessione_id in più, campi attivi alla data DELLA
// SESSIONE (non a oggi) — un test dismesso da allora resterebbe comunque compilabile.
import Longevity from '../../../../../layouts/Longevity.astro'; import Longevity from '../../../../../layouts/Longevity.astro';
import { getLongevityDb } from '../../../../../lib/longevity/db'; import { getLongevityDb } from '../../../../../lib/longevity/db';
import { clienteDaCodice } from '../../../../../lib/longevity/anagrafica'; import { clienteDaCodice } from '../../../../../lib/longevity/anagrafica';
import { campiDelCheckup } from '../../../../../lib/longevity/gestionale'; import { campiDelCheckup, misureDellaSessione } from '../../../../../lib/longevity/gestionale';
export const prerender = false; export const prerender = false;
@@ -13,28 +17,42 @@ const { code } = Astro.params;
const cliente = code ? clienteDaCodice(code) : null; const cliente = code ? clienteDaCodice(code) : null;
if (!cliente) return new Response('Cliente non trovato', { status: 404 }); if (!cliente) return new Response('Cliente non trovato', { status: 404 });
const db = getLongevityDb();
const oggi = new Date().toISOString().slice(0, 10); const oggi = new Date().toISOString().slice(0, 10);
const gruppi = campiDelCheckup(getLongevityDb(), oggi);
const sessioneParam = Astro.url.searchParams.get('sessione');
const sessioneId = sessioneParam !== null ? Number(sessioneParam) : null;
const sessione = sessioneId !== null && Number.isInteger(sessioneId) ? misureDellaSessione(db, sessioneId) : null;
// Una sessione di un altro cliente o non di tipo checkup non è un modulo modificabile da
// questa scheda: stesso trattamento di un codice cliente inesistente.
if (sessioneId !== null && (!sessione || sessione.client_code !== cliente.client_code || sessione.tipo !== 'checkup')) {
return new Response('Sessione non trovata', { status: 404 });
}
const dataForm = sessione?.data ?? oggi;
const gruppi = campiDelCheckup(db, dataForm);
const valori = new Map(sessione?.misure.map((m) => [m.test_id, m.valore_num]) ?? []);
const errore = Astro.url.searchParams.get('errore'); const errore = Astro.url.searchParams.get('errore');
--- ---
<Longevity <Longevity
title={`Check-up — ${cliente.cognome} ${cliente.nome}`} title={`${sessione ? 'Modifica' : 'Nuovo'} check-up — ${cliente.cognome} ${cliente.nome}`}
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, 'Nuovo check-up']} crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, sessione ? 'Modifica check-up' : 'Nuovo check-up']}
variante="gestionale" variante="gestionale"
> >
<div class="lg-head"> <div class="lg-head">
<h1>Nuovo check-up</h1> <h1>{sessione ? 'Modifica check-up' : 'Nuovo check-up'}</h1>
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code}. Lascia vuoto ciò che non hai misurato oggi.</p> <p>{cliente.cognome} {cliente.nome} — {cliente.client_code}. Lascia vuoto ciò che non hai misurato.</p>
</div> </div>
{errore && <p class="lg-form__errore">{errore}</p>} {errore && <p class="lg-form__errore">{errore}</p>}
<form class="lg-form" method="post" action="/api/longevity/gestionale/checkup"> <form class="lg-form" method="post" action="/api/longevity/gestionale/checkup">
<input type="hidden" name="client_code" value={cliente.client_code} /> <input type="hidden" name="client_code" value={cliente.client_code} />
{sessione && <input type="hidden" name="sessione_id" value={sessione.id} />}
<label class="lg-field"> <label class="lg-field">
<span>Data</span> <span>Data</span>
<input type="date" name="data" value={oggi} required /> <input type="date" name="data" value={dataForm} required />
</label> </label>
{gruppi.map((g) => ( {gruppi.map((g) => (
@@ -43,7 +61,7 @@ const errore = Astro.url.searchParams.get('errore');
{g.campi.map((c) => ( {g.campi.map((c) => (
<label class="lg-field"> <label class="lg-field">
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span> <span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
<input type="text" inputmode="decimal" name={c.id} /> <input type="text" inputmode="decimal" name={c.id} value={valori.get(c.id) ?? ''} />
</label> </label>
))} ))}
</section> </section>
@@ -51,7 +69,7 @@ const errore = Astro.url.searchParams.get('errore');
<div class="lg-form__nav"> <div class="lg-form__nav">
<a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Annulla</a> <a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Annulla</a>
<button type="submit">Salva check-up</button> <button type="submit">{sessione ? 'Salva correzione' : 'Salva check-up'}</button>
</div> </div>
</form> </form>
</Longevity> </Longevity>
@@ -0,0 +1,96 @@
---
// Il dettaglio di una sessione (#12): data, tipo, operatore, misure con valore/unità e
// stato di fuori-range/verifica. Da qui si apre la correzione di un check-up
// (checkup.astro?sessione=<id>) — un questionario resta di sola lettura, la sua correzione
// non è questa issue. Un id che non esiste, o di un altro cliente, è un 404: non c'è niente
// da mostrare per una sessione che non è la sua.
import Longevity from '../../../../../../layouts/Longevity.astro';
import { getLongevityDb } from '../../../../../../lib/longevity/db';
import { clienteDaCodice } from '../../../../../../lib/longevity/anagrafica';
import { misureDellaSessione, campiDelCheckup, type MisuraDiSessione } from '../../../../../../lib/longevity/gestionale';
import { formattaNumero } from '../../../../../../lib/longevity/formato';
export const prerender = false;
const { code, id } = Astro.params;
const cliente = code ? clienteDaCodice(code) : null;
if (!cliente) return new Response('Cliente non trovato', { status: 404 });
const sessioneId = id ? Number(id) : NaN;
const db = getLongevityDb();
const sessione = Number.isInteger(sessioneId) ? misureDellaSessione(db, sessioneId) : null;
if (!sessione || sessione.client_code !== cliente.client_code) {
return new Response('Sessione non trovata', { status: 404 });
}
const ETICHETTA_TIPO: Record<'checkup' | 'questionario', string> = {
checkup: 'Check-up', questionario: 'Questionario',
};
const ETICHETTA_VERIFICA: Record<'confermata' | 'scartata', string> = {
confermata: 'Confermata', scartata: 'Scartata',
};
const formattaData = (iso: string) =>
new Date(`${iso}T00:00:00`).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' });
// Un check-up si raggruppa per asse come il modulo che lo ha compilato (campiDelCheckup);
// un questionario non ha un raggruppamento del genere nel codice — resta un'unica sezione,
// già ordinata per asse da misureDellaSessione.
type Sezione = { asse: string | null; misure: MisuraDiSessione[] };
let sezioni: Sezione[];
if (sessione.tipo === 'checkup') {
const perTestId = new Map(sessione.misure.map((m) => [m.test_id, m]));
sezioni = campiDelCheckup(db, sessione.data)
.map((g) => ({
asse: g.asse,
misure: g.campi.map((c) => perTestId.get(c.id)).filter((m): m is MisuraDiSessione => m !== undefined),
}))
.filter((s) => s.misure.length > 0);
} else {
sezioni = [{ asse: null, misure: sessione.misure }];
}
---
<Longevity
title={`Sessione del ${formattaData(sessione.data)} — ${cliente.cognome} ${cliente.nome}`}
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, `Sessione del ${formattaData(sessione.data)}`]}
variante="gestionale"
>
<div class="lg-head lg-head--azioni">
<div>
<h1>{ETICHETTA_TIPO[sessione.tipo]} del {formattaData(sessione.data)}</h1>
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code} · operatore {sessione.operatore ?? '—'}</p>
</div>
<div>
{sessione.tipo === 'checkup' && (
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup?sessione=${sessione.id}`}>Modifica</a>
)}
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Torna alla scheda</a>
</div>
</div>
{sezioni.map((s) => (
<div class="lg-card">
{s.asse && <p class="lg-score__label">{s.asse}</p>}
<table class="lg-table">
<thead><tr><th>Test</th><th>Valore</th><th>Stato</th></tr></thead>
<tbody>
{s.misure.map((m) => (
<tr>
<td>{m.etichetta}</td>
<td class:list={{ 'lg-misura--fuori-range': m.fuori_range === 1 }}>
{m.valore_num !== null ? formattaNumero(m.valore_num) : (m.valore_txt ?? '—')}
{m.unita ? ` ${m.unita}` : ''}
</td>
<td>
{m.fuori_range === 0
? '—'
: m.verifica
? ETICHETTA_VERIFICA[m.verifica]
: 'Da verificare'}
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</Longevity>