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:
@@ -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>
|
||||||
Reference in New Issue
Block a user