longevity: pagina del gestionale — scheda cliente (referto, sessioni, fuori range)

pages/longevity/gestionale/cliente/[code].astro: anagrafica, il referto dell'ultima
sessione (stesso Radar/MacroScore di io.astro, qui per il trainer), la tabella delle
sessioni e le misure fuori range (colore --lg-fuori-range). Un codice inesistente è
404. Gli esiti ?salvato/?fuori/?errore dell'API di check-up sono mostrati qui.

Refs #2
This commit is contained in:
2026-09-26 13:05:42 +00:00
parent fd56afdf44
commit 179aeb442a
@@ -0,0 +1,151 @@
---
// 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, misureFuoriRange } from '../../../../lib/longevity/gestionale';
import { refertoDi, ultimaSessioneDi } from '../../../../lib/longevity/vista';
import { formattaNumero } from '../../../../lib/longevity/formato';
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 fuoriRange = misureFuoriRange(db, cliente.client_code);
const ultimaId = ultimaSessioneDi(db, cliente.client_code);
const referto = ultimaId !== null ? refertoDi(db, ultimaId) : null;
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 fuoriCount = params.get('fuori');
const errore = params.get('errore');
---
<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>
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup`}>Nuovo check-up</a>
</div>
{errore && <p class="lg-form__errore">{errore}</p>}
{salvato && <p class="lg-card">Check-up salvato (sessione #{salvato}).</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>
{!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>{formattaData(s.data)}</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>
{fuoriRange.length > 0 && (
<div class="lg-card">
<p class="lg-score__label">Misure fuori range</p>
<table class="lg-table">
<thead><tr><th>Data</th><th>Test</th><th>Valore</th></tr></thead>
<tbody>
{fuoriRange.map((m) => (
<tr>
<td>{formattaData(m.data)}</td>
<td>{m.etichetta}</td>
<td class="lg-misura--fuori-range">
{m.valore !== null ? formattaNumero(m.valore) : '—'}{m.unita ? ` ${m.unita}` : ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Longevity>