Files
InsanityLab-web/apps/platforms/src/pages/longevity/gestionale/cliente/[code].astro
T
Adriano 2231a476c7 longevity: sezione Giornale di bordo nella scheda cliente
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>
2026-09-27 22:24:22 +02:00

242 lines
9.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
// 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>