2231a476c7
Form (data, nota) ed elenco voci dopo Anagrafica, cancella visibile solo ad autore o admin, messaggi per ?giornale=aggiunta|cancellata. Refs #16 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
242 lines
9.8 KiB
Plaintext
242 lines
9.8 KiB
Plaintext
---
|
||
// La scheda di un cliente: anagrafica, il referto dell'ultima sessione (lo stesso che il
|
||
// cliente vede da solo su /longevity/io, qui per il trainer), l'elenco delle sessioni e le
|
||
// misure fuori range. Un codice che non esiste in anagrafica è un 404, non uno schermo
|
||
// vuoto — non c'è niente da mostrare per un fascicolo che non c'è.
|
||
import Longevity from '../../../../layouts/Longevity.astro';
|
||
import Radar from '../../../../components/longevity/Radar';
|
||
import MacroScore from '../../../../components/longevity/MacroScore.astro';
|
||
import { getLongevityDb } from '../../../../lib/longevity/db';
|
||
import { clienteDaCodice, etaDelCliente } from '../../../../lib/longevity/anagrafica';
|
||
import { sessioniDi } from '../../../../lib/longevity/gestionale';
|
||
import { misureDaVerificare } from '../../../../lib/longevity/misure';
|
||
import { refertoDi, ultimaSessioneDi } from '../../../../lib/longevity/vista';
|
||
import { formattaNumero } from '../../../../lib/longevity/formato';
|
||
import { vociDelGiornale } from '../../../../lib/longevity/giornale';
|
||
|
||
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 oggi = new Date().toISOString().slice(0, 10);
|
||
const eta = etaDelCliente(cliente.client_code, oggi);
|
||
const sessioni = sessioniDi(db, cliente.client_code);
|
||
const daVerificare = misureDaVerificare(db, cliente.client_code);
|
||
const ultimaId = ultimaSessioneDi(db, cliente.client_code);
|
||
const referto = ultimaId !== null ? refertoDi(db, ultimaId) : null;
|
||
const voci = vociDelGiornale(db, cliente.client_code); // #16: giornale di bordo, solo staff
|
||
|
||
const ETICHETTA_TIPO: Record<'checkup' | 'questionario', string> = {
|
||
checkup: 'Check-up',
|
||
questionario: 'Questionario',
|
||
};
|
||
const formattaData = (iso: string) =>
|
||
new Date(`${iso}T00:00:00`).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' });
|
||
|
||
const params = Astro.url.searchParams;
|
||
const salvato = params.get('salvato');
|
||
const corretto = params.get('corretto'); // #12: redirect di correggiCheckup dopo una correzione
|
||
const fuoriCount = params.get('fuori');
|
||
const errore = params.get('errore');
|
||
const verificata = params.get('verificata');
|
||
const MESSAGGIO_VERIFICA: Record<'confermata' | 'scartata', string> = {
|
||
confermata: 'Misura confermata: punteggi ricalcolati.',
|
||
scartata: 'Misura scartata.',
|
||
};
|
||
const messaggioVerifica =
|
||
verificata === 'confermata' || verificata === 'scartata' ? MESSAGGIO_VERIFICA[verificata] : null;
|
||
const giornaleEsito = params.get('giornale'); // #16: 'aggiunta' | 'cancellata', dal redirect dell'API
|
||
const MESSAGGIO_GIORNALE: Record<'aggiunta' | 'cancellata', string> = {
|
||
aggiunta: 'Nota aggiunta al giornale di bordo.',
|
||
cancellata: 'Nota cancellata dal giornale di bordo.',
|
||
};
|
||
const messaggioGiornale =
|
||
giornaleEsito === 'aggiunta' || giornaleEsito === 'cancellata' ? MESSAGGIO_GIORNALE[giornaleEsito] : null;
|
||
---
|
||
<Longevity
|
||
title={`${cliente.cognome} ${cliente.nome}`}
|
||
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`]}
|
||
variante="gestionale"
|
||
>
|
||
<div class="lg-head lg-head--azioni">
|
||
<div>
|
||
<h1>{cliente.cognome} {cliente.nome}</h1>
|
||
<p>
|
||
{cliente.client_code} · {eta !== null ? `${eta} anni` : 'età non registrata'} ·
|
||
accesso {cliente.ha_accesso ? 'attivo' : 'non ancora attivato'}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
{!cliente.ha_accesso && (
|
||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/accesso`}>Crea accesso al referto</a>
|
||
)}
|
||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup`}>Nuovo check-up</a>
|
||
</div>
|
||
</div>
|
||
|
||
{errore && <p class="lg-form__errore">{errore}</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>}
|
||
{messaggioGiornale && <p class="lg-card">{messaggioGiornale}</p>}
|
||
{fuoriCount && (
|
||
<p class="lg-misura--fuori-range">
|
||
{fuoriCount} {Number(fuoriCount) === 1 ? 'misura è' : 'misure sono'} fuori dal range di
|
||
plausibilità: verificale più sotto.
|
||
</p>
|
||
)}
|
||
|
||
<div class="lg-card">
|
||
<p class="lg-score__label">Anagrafica</p>
|
||
<p>Email: {cliente.email ?? '—'}</p>
|
||
<p>Telefono: {cliente.telefono ?? '—'}</p>
|
||
<p>Data di nascita: {cliente.data_nascita ? formattaData(cliente.data_nascita) : '—'}</p>
|
||
</div>
|
||
|
||
<div class="lg-card" id="giornale">
|
||
<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>
|
||
|
||
<form class="lg-form" method="post" action="/api/longevity/gestionale/giornale">
|
||
<input type="hidden" name="azione" value="aggiungi" />
|
||
<input type="hidden" name="client_code" value={cliente.client_code} />
|
||
<label class="lg-field">
|
||
<span>Data</span>
|
||
<input type="date" name="data" value={oggi} max={oggi} required />
|
||
</label>
|
||
<label class="lg-field">
|
||
<span>Nota</span>
|
||
<textarea name="testo" rows="3" required></textarea>
|
||
</label>
|
||
<div class="lg-form__nav">
|
||
<button type="submit">Aggiungi nota</button>
|
||
</div>
|
||
</form>
|
||
|
||
{voci.length === 0 ? (
|
||
<p class="lg-muted">Nessuna nota ancora.</p>
|
||
) : (
|
||
<table class="lg-table">
|
||
<thead><tr><th>Data</th><th>Autore</th><th>Nota</th><th></th></tr></thead>
|
||
<tbody>
|
||
{voci.map((v) => (
|
||
<tr>
|
||
<td>{formattaData(v.data)}</td>
|
||
<td>{v.autore}</td>
|
||
<td style="white-space: pre-wrap;">{v.testo}</td>
|
||
<td>
|
||
{(v.autore === Astro.locals.user!.username || Astro.locals.user!.role === 'admin') && (
|
||
<form method="post" action="/api/longevity/gestionale/giornale">
|
||
<input type="hidden" name="azione" value="cancella" />
|
||
<input type="hidden" name="id" value={v.id} />
|
||
<button type="submit">Cancella</button>
|
||
</form>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
|
||
{!referto && (
|
||
<div class="lg-card lg-card--incompleto">
|
||
<p>Nessuna sessione ancora: apri un check-up per cominciare il fascicolo.</p>
|
||
</div>
|
||
)}
|
||
|
||
{referto && (
|
||
<div class="lg-referto">
|
||
<section class:list={['lg-card', { 'lg-card--incompleto': referto.fitnessAge === null }]}>
|
||
<p class="lg-score__label">Fitness Age</p>
|
||
<div class="lg-score">
|
||
<span class="lg-score__value lg-num">
|
||
{referto.fitnessAge !== null ? formattaNumero(referto.fitnessAge) : '—'}
|
||
</span>
|
||
{referto.fitnessAge !== null && <span class="lg-score__max">anni</span>}
|
||
</div>
|
||
<p class="lg-muted lg-macro__nota">
|
||
Copertura dati: {Math.round(referto.fitnessAgeCopertura * 100)}%
|
||
{referto.fitnessAge === null && ' — non ancora sufficiente per un numero.'}
|
||
</p>
|
||
</section>
|
||
|
||
<section class="lg-card">
|
||
<h2>Radar</h2>
|
||
<Radar client:load assi={referto.assi} />
|
||
</section>
|
||
|
||
<section class="lg-macro">
|
||
{referto.macro.map((m) => (
|
||
<MacroScore nome={m.nome} valore={m.valore} copertura={m.copertura} />
|
||
))}
|
||
</section>
|
||
</div>
|
||
)}
|
||
|
||
<div class="lg-card">
|
||
<p class="lg-score__label">Sessioni</p>
|
||
{sessioni.length === 0 ? (
|
||
<p class="lg-muted">Nessuna sessione.</p>
|
||
) : (
|
||
<table class="lg-table">
|
||
<thead>
|
||
<tr><th>Data</th><th>Tipo</th><th>Operatore</th><th>Misure</th><th>Fitness Age</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{sessioni.map((s) => (
|
||
<tr>
|
||
<td>
|
||
<a href={`/longevity/gestionale/cliente/${cliente.client_code}/sessione/${s.id}`}>{formattaData(s.data)}</a>
|
||
</td>
|
||
<td>{ETICHETTA_TIPO[s.tipo]}</td>
|
||
<td>{s.operatore ?? '—'}</td>
|
||
<td>{s.numeroMisure}</td>
|
||
<td>{s.fitnessAge !== null ? formattaNumero(s.fitnessAge) : '—'}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
|
||
{daVerificare.length > 0 && (
|
||
<div class="lg-card">
|
||
<p class="lg-score__label">Misure da verificare</p>
|
||
<p class="lg-muted">
|
||
Una misura confermata è un dato vero: entra nei punteggi e ricalcola quella sessione e
|
||
le successive. Una scartata è un errore di strumento o protocollo: resta fuori per sempre.
|
||
</p>
|
||
<table class="lg-table">
|
||
<thead><tr><th>Data</th><th>Test</th><th>Valore</th><th>Range atteso</th><th>Decisione</th></tr></thead>
|
||
<tbody>
|
||
{daVerificare.map((m) => (
|
||
<tr>
|
||
<td>{formattaData(m.data)}</td>
|
||
<td>{m.etichetta}</td>
|
||
<td class="lg-misura--fuori-range">
|
||
{m.valore_num !== null ? formattaNumero(m.valore_num) : '—'}{m.unita ? ` ${m.unita}` : ''}
|
||
</td>
|
||
<td>{m.range_min ?? '—'}–{m.range_max ?? '—'}</td>
|
||
<td>
|
||
<form method="post" action="/api/longevity/gestionale/verifica">
|
||
<input type="hidden" name="misura_id" value={m.id} />
|
||
<button type="submit" name="esito" value="confermata">Conferma: il dato è vero</button>
|
||
</form>
|
||
<form method="post" action="/api/longevity/gestionale/verifica">
|
||
<input type="hidden" name="misura_id" value={m.id} />
|
||
<button type="submit" name="esito" value="scartata">Scarta: è un errore</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</Longevity>
|