longevity: #15 pagine del flusso guidato: consulenza, check-up, scheda cliente

Nuova pagina consulenza.astro (radio per livello e obiettivo, niente
select). checkup.astro mostra "Passo 3 di 4" e porta avanti flusso=nuovo,
con «Salta» verso la scheda. La scheda cliente ([code].astro) mostra la
sezione "Prima consulenza" (letta, Modifica/Compila) e, con
flusso=questionario, il riquadro "Passo 4 di 4" che rimanda a "Crea
accesso" se il cliente non lo ha ancora.

Refs #15
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 08:21:33 +02:00
parent 575a3988b7
commit 8aeb25f930
3 changed files with 169 additions and 1 deletions
@@ -13,6 +13,7 @@ import { misureDaVerificare } from '../../../../lib/longevity/misure';
import { refertoDi, ultimaSessioneDi } from '../../../../lib/longevity/vista'; import { refertoDi, ultimaSessioneDi } from '../../../../lib/longevity/vista';
import { formattaNumero } from '../../../../lib/longevity/formato'; import { formattaNumero } from '../../../../lib/longevity/formato';
import { vociDelGiornale } from '../../../../lib/longevity/giornale'; import { vociDelGiornale } from '../../../../lib/longevity/giornale';
import { consulenzaDi, OBIETTIVI } from '../../../../lib/longevity/consulenza';
export const prerender = false; export const prerender = false;
@@ -28,6 +29,8 @@ const daVerificare = misureDaVerificare(db, cliente.client_code);
const ultimaId = ultimaSessioneDi(db, cliente.client_code); const ultimaId = ultimaSessioneDi(db, cliente.client_code);
const referto = ultimaId !== null ? refertoDi(db, ultimaId) : null; const referto = ultimaId !== null ? refertoDi(db, ultimaId) : null;
const voci = vociDelGiornale(db, cliente.client_code); // #16: giornale di bordo, solo staff const voci = vociDelGiornale(db, cliente.client_code); // #16: giornale di bordo, solo staff
const consulenza = consulenzaDi(db, cliente.client_code); // #15: prima consulenza, solo staff
const ETICHETTA_OBIETTIVO = new Map(OBIETTIVI.map((o) => [o.chiave, o.etichetta]));
const ETICHETTA_TIPO: Record<'checkup' | 'questionario', string> = { const ETICHETTA_TIPO: Record<'checkup' | 'questionario', string> = {
checkup: 'Check-up', checkup: 'Check-up',
@@ -55,6 +58,10 @@ const MESSAGGIO_GIORNALE: Record<'aggiunta' | 'cancellata', string> = {
}; };
const messaggioGiornale = const messaggioGiornale =
giornaleEsito === 'aggiunta' || giornaleEsito === 'cancellata' ? MESSAGGIO_GIORNALE[giornaleEsito] : null; giornaleEsito === 'aggiunta' || giornaleEsito === 'cancellata' ? MESSAGGIO_GIORNALE[giornaleEsito] : null;
const consulenzaEsito = params.get('consulenza'); // #15: 'salvata', dal redirect dell'API consulenza
// Passo 4 del flusso guidato per un cliente nuovo (#15): dopo il check-up si torna qui con
// ?flusso=questionario — il questionario resta del cliente, quindi qui c'è solo l'indicazione.
const flusso = params.get('flusso');
--- ---
<Longevity <Longevity
title={`${cliente.cognome} ${cliente.nome}`} title={`${cliente.cognome} ${cliente.nome}`}
@@ -82,6 +89,20 @@ const messaggioGiornale =
{corretto && <p class="lg-card">Check-up corretto (sessione #{corretto}): punteggi ricalcolati.</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>}
{messaggioGiornale && <p class="lg-card">{messaggioGiornale}</p>} {messaggioGiornale && <p class="lg-card">{messaggioGiornale}</p>}
{consulenzaEsito === 'salvata' && <p class="lg-card">Prima consulenza salvata.</p>}
{flusso === 'questionario' && (
<div class="lg-card">
<p class="lg-score__label">Passo 4 di 4 — Questionario</p>
{cliente.ha_accesso ? (
<p>Il questionario lo compila il cliente dal suo accesso, anche sul tablet del centro.</p>
) : (
<>
<p>Il questionario lo compila il cliente dal suo accesso, anche sul tablet del centro: crealo per continuare.</p>
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/accesso`}>Crea accesso al referto</a>
</>
)}
</div>
)}
{fuoriCount && ( {fuoriCount && (
<p class="lg-misura--fuori-range"> <p class="lg-misura--fuori-range">
{fuoriCount} {Number(fuoriCount) === 1 ? 'misura è' : 'misure sono'} fuori dal range di {fuoriCount} {Number(fuoriCount) === 1 ? 'misura è' : 'misure sono'} fuori dal range di
@@ -96,6 +117,31 @@ const messaggioGiornale =
<p>Data di nascita: {cliente.data_nascita ? formattaData(cliente.data_nascita) : '—'}</p> <p>Data di nascita: {cliente.data_nascita ? formattaData(cliente.data_nascita) : '—'}</p>
</div> </div>
<div class="lg-card" id="consulenza">
<div class="lg-head--azioni">
<p class="lg-score__label">Prima consulenza</p>
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/consulenza`}>
{consulenza ? 'Modifica' : 'Compila'}
</a>
</div>
{!consulenza ? (
<p class="lg-muted">Non ancora compilata.</p>
) : (
<>
<p>Livello: {consulenza.livello ?? '—'}</p>
<p>Obiettivo primario: {consulenza.obiettivo_primario ? ETICHETTA_OBIETTIVO.get(consulenza.obiettivo_primario) ?? consulenza.obiettivo_primario : '—'}</p>
<p>Obiettivo specifico: {consulenza.obiettivo_specifico ?? '—'}</p>
<p>Data target: {consulenza.data_target ? formattaData(consulenza.data_target) : '—'}</p>
<p>Note mediche: {consulenza.note_mediche ?? '—'}</p>
<p>Storia sportiva: {consulenza.storia_sportiva ?? '—'}</p>
<p>Limitazioni fisiche attuali: {consulenza.limitazioni ?? '—'}</p>
<p>Disponibilità: {consulenza.disponibilita_giorni ?? '—'} giorni/settimana</p>
<p>Fascia oraria preferita: {consulenza.fascia_oraria ?? '—'}</p>
<p>Note screening: {consulenza.note_screening ?? '—'}</p>
</>
)}
</div>
<div class="lg-card" id="giornale"> <div class="lg-card" id="giornale">
<p class="lg-score__label">Giornale di bordo</p> <p class="lg-score__label">Giornale di bordo</p>
<p class="lg-muted">Solo lo staff lo vede. Una nota sbagliata si cancella e si riscrive: non si modifica.</p> <p class="lg-muted">Solo lo staff lo vede. Una nota sbagliata si cancella e si riscrive: non si modifica.</p>
@@ -40,6 +40,9 @@ const valoriPreselezione = sessione
? new Map([...valori.entries()].filter((kv): kv is [string, number] => kv[1] !== null)) ? new Map([...valori.entries()].filter((kv): kv is [string, number] => kv[1] !== null))
: valoriUltimoCheckup(db, cliente.client_code); : valoriUltimoCheckup(db, cliente.client_code);
const errore = Astro.url.searchParams.get('errore'); const errore = Astro.url.searchParams.get('errore');
// Passo 3 del flusso guidato per un cliente nuovo (?flusso=nuovo, #15): solo per un
// check-up NUOVO — in correzione (?sessione=<id>) non si è mai dentro il flusso guidato.
const flusso = !sessione ? Astro.url.searchParams.get('flusso') ?? '' : '';
--- ---
<Longevity <Longevity
title={`${sessione ? 'Modifica' : 'Nuovo'} check-up — ${cliente.cognome} ${cliente.nome}`} title={`${sessione ? 'Modifica' : 'Nuovo'} check-up — ${cliente.cognome} ${cliente.nome}`}
@@ -49,6 +52,7 @@ const errore = Astro.url.searchParams.get('errore');
<div class="lg-head"> <div class="lg-head">
<h1>{sessione ? 'Modifica check-up' : '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.</p> <p>{cliente.cognome} {cliente.nome} — {cliente.client_code}. Lascia vuoto ciò che non hai misurato.</p>
{flusso === 'nuovo' && <p class="lg-muted">Passo 3 di 4 — Scheda Rapida</p>}
</div> </div>
{errore && <p class="lg-form__errore">{errore}</p>} {errore && <p class="lg-form__errore">{errore}</p>}
@@ -56,6 +60,7 @@ const errore = Astro.url.searchParams.get('errore');
<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} />} {sessione && <input type="hidden" name="sessione_id" value={sessione.id} />}
{flusso === 'nuovo' && <input type="hidden" name="flusso" value="nuovo" />}
<label class="lg-field"> <label class="lg-field">
<span>Data</span> <span>Data</span>
@@ -131,7 +136,11 @@ const errore = Astro.url.searchParams.get('errore');
)} )}
<div class="lg-form__nav"> <div class="lg-form__nav">
{flusso === 'nuovo' ? (
<a href={`/longevity/gestionale/cliente/${cliente.client_code}?flusso=questionario`}>Salta</a>
) : (
<a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Annulla</a> <a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Annulla</a>
)}
<button type="submit">{sessione ? 'Salva correzione' : 'Salva check-up'}</button> <button type="submit">{sessione ? 'Salva correzione' : 'Salva check-up'}</button>
</div> </div>
</form> </form>
@@ -0,0 +1,113 @@
---
// La scheda di prima consulenza (#15): livello, obiettivo, note dello screening iniziale.
// Precompilata se esiste già — correggere e salvare SOVRASCRIVE, nessuno storico (decisione
// di Adriano, 27/09/2026). L'invio va all'API del gestionale, che fa la validazione vera
// (pages/api/longevity/gestionale/consulenza.ts) — qui c'è solo il form.
//
// Passo 2 del flusso guidato per un cliente nuovo (?flusso=nuovo, #15): il parametro passa
// di pagina in pagina fino al questionario, e ogni passo ha un link «Salta».
import Longevity from '../../../../../layouts/Longevity.astro';
import { getLongevityDb } from '../../../../../lib/longevity/db';
import { clienteDaCodice } from '../../../../../lib/longevity/anagrafica';
import { consulenzaDi, OBIETTIVI } from '../../../../../lib/longevity/consulenza';
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 scheda = consulenzaDi(db, cliente.client_code);
const errore = Astro.url.searchParams.get('errore');
const flusso = Astro.url.searchParams.get('flusso') ?? '';
const query = flusso ? `?flusso=${encodeURIComponent(flusso)}` : '';
---
<Longevity
title={`Prima consulenza — ${cliente.cognome} ${cliente.nome}`}
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, 'Prima consulenza']}
variante="gestionale"
>
<div class="lg-head">
<h1>Prima consulenza</h1>
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code}. Tutti i campi sono facoltativi.</p>
{flusso === 'nuovo' && <p class="lg-muted">Passo 2 di 4 — Prima consulenza</p>}
</div>
{errore && <p class="lg-form__errore">{errore}</p>}
<form class="lg-form" method="post" action="/api/longevity/gestionale/consulenza">
<input type="hidden" name="client_code" value={cliente.client_code} />
<input type="hidden" name="flusso" value={flusso} />
<section class="lg-form__blocco">
<fieldset class="lg-field lg-field--consenso">
<legend>Livello</legend>
{[1, 2, 3].map((n) => (
<label>
<input type="radio" name="livello" value={n} checked={scheda?.livello === n} /> {n}
</label>
))}
</fieldset>
<fieldset class="lg-field lg-field--consenso">
<legend>Obiettivo primario</legend>
{OBIETTIVI.map((o) => (
<label>
<input
type="radio" name="obiettivo_primario" value={o.chiave}
checked={scheda?.obiettivo_primario === o.chiave}
/> {o.etichetta}
</label>
))}
</fieldset>
<label class="lg-field">
<span>Obiettivo specifico concordato</span>
<textarea name="obiettivo_specifico" rows="3">{scheda?.obiettivo_specifico ?? ''}</textarea>
</label>
<label class="lg-field">
<span>Data target</span>
<input type="date" name="data_target" value={scheda?.data_target ?? ''} />
</label>
</section>
<section class="lg-form__blocco">
<h2>Screening</h2>
<label class="lg-field">
<span>Note mediche, allergie, infortuni pregressi</span>
<textarea name="note_mediche" rows="3">{scheda?.note_mediche ?? ''}</textarea>
</label>
<label class="lg-field">
<span>Storia sportiva</span>
<textarea name="storia_sportiva" rows="3">{scheda?.storia_sportiva ?? ''}</textarea>
</label>
<label class="lg-field">
<span>Limitazioni fisiche attuali</span>
<textarea name="limitazioni" rows="3">{scheda?.limitazioni ?? ''}</textarea>
</label>
<label class="lg-field">
<span>Disponibilità (giorni/settimana)</span>
<input type="text" inputmode="numeric" name="disponibilita_giorni" value={scheda?.disponibilita_giorni ?? ''} />
</label>
<label class="lg-field">
<span>Fascia oraria preferita</span>
<input type="text" name="fascia_oraria" value={scheda?.fascia_oraria ?? ''} />
</label>
<label class="lg-field">
<span>Note screening</span>
<textarea name="note_screening" rows="3">{scheda?.note_screening ?? ''}</textarea>
</label>
</section>
<div class="lg-form__nav">
{flusso === 'nuovo' ? (
<a href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup${query}`}>Salta</a>
) : (
<a href={`/longevity/gestionale/cliente/${cliente.client_code}#consulenza`}>Annulla</a>
)}
<button type="submit">Salva</button>
</div>
</form>
</Longevity>