longevity: verifica misure fuori range in scheda cliente, link a crea accesso

Scheda cliente ([code].astro): sezione "Misure da verificare" da
misureDaVerificare (id, range) al posto di misureFuoriRange (nessun id,
nessuna azione); ogni riga ha due form verso /api/longevity/gestionale/verifica
(conferma/scarta), messaggio dopo la decisione, e link "Crea accesso al
referto" quando il cliente non ha ancora un accesso.

gestionale.ts: rimossa misureFuoriRange (e il tipo MisuraFuoriRange), sostituita
ovunque da misureDaVerificare (misure.ts), l'unica con id e range utile alle
azioni. accesso.astro: link "Torna alla scheda" in tutti gli stati.

Refs #7
Refs #8

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 13:11:38 +00:00
parent 6cbb205064
commit 2c9a74f849
5 changed files with 67 additions and 37 deletions
@@ -132,27 +132,3 @@ export function sessioniDi(db: Database.Database, code: string): SessioneElenco[
}; };
}); });
} }
export type MisuraFuoriRange = {
test_id: string;
etichetta: string;
valore: number | null;
unita: string | null;
data: string;
};
/** Le misure di un cliente marcate fuori dal range di plausibilità del registro
* (`misure.fuori_range = 1`, scritto da `registraMisure`): quelle che oggi non contano per
* nessun calcolo (`misureValideAlla` le esclude) ma restano da mostrare e, un domani,
* confermare (#7). Più recente prima. */
export function misureFuoriRange(db: Database.Database, code: string): MisuraFuoriRange[] {
return db.prepare(
`SELECT m.test_id AS test_id, r.etichetta AS etichetta, m.valore_num AS valore,
m.unita AS unita, s.data AS data
FROM misure m
JOIN sessioni s ON s.id = m.sessione_id
JOIN registro_test r ON r.test_id = m.test_id
WHERE m.client_code = ? AND m.fuori_range = 1
ORDER BY s.data DESC, s.id DESC, m.id DESC`
).all(code) as MisuraFuoriRange[];
}
@@ -8,7 +8,8 @@ import Radar from '../../../../components/longevity/Radar';
import MacroScore from '../../../../components/longevity/MacroScore.astro'; import MacroScore from '../../../../components/longevity/MacroScore.astro';
import { getLongevityDb } from '../../../../lib/longevity/db'; import { getLongevityDb } from '../../../../lib/longevity/db';
import { clienteDaCodice, etaDelCliente } from '../../../../lib/longevity/anagrafica'; import { clienteDaCodice, etaDelCliente } from '../../../../lib/longevity/anagrafica';
import { sessioniDi, misureFuoriRange } from '../../../../lib/longevity/gestionale'; import { sessioniDi } from '../../../../lib/longevity/gestionale';
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';
@@ -22,7 +23,7 @@ const db = getLongevityDb();
const oggi = new Date().toISOString().slice(0, 10); const oggi = new Date().toISOString().slice(0, 10);
const eta = etaDelCliente(cliente.client_code, oggi); const eta = etaDelCliente(cliente.client_code, oggi);
const sessioni = sessioniDi(db, cliente.client_code); const sessioni = sessioniDi(db, cliente.client_code);
const fuoriRange = misureFuoriRange(db, cliente.client_code); 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;
@@ -37,6 +38,13 @@ const params = Astro.url.searchParams;
const salvato = params.get('salvato'); const salvato = params.get('salvato');
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 MESSAGGIO_VERIFICA: Record<'confermata' | 'scartata', string> = {
confermata: 'Misura confermata: punteggi ricalcolati.',
scartata: 'Misura scartata.',
};
const messaggioVerifica =
verificata === 'confermata' || verificata === 'scartata' ? MESSAGGIO_VERIFICA[verificata] : null;
--- ---
<Longevity <Longevity
title={`${cliente.cognome} ${cliente.nome}`} title={`${cliente.cognome} ${cliente.nome}`}
@@ -51,11 +59,17 @@ const errore = params.get('errore');
accesso {cliente.ha_accesso ? 'attivo' : 'non ancora attivato'} accesso {cliente.ha_accesso ? 'attivo' : 'non ancora attivato'}
</p> </p>
</div> </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> <a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup`}>Nuovo check-up</a>
</div> </div>
</div>
{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>}
{messaggioVerifica && <p class="lg-card">{messaggioVerifica}</p>}
{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
@@ -129,18 +143,33 @@ const errore = params.get('errore');
)} )}
</div> </div>
{fuoriRange.length > 0 && ( {daVerificare.length > 0 && (
<div class="lg-card"> <div class="lg-card">
<p class="lg-score__label">Misure fuori range</p> <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"> <table class="lg-table">
<thead><tr><th>Data</th><th>Test</th><th>Valore</th></tr></thead> <thead><tr><th>Data</th><th>Test</th><th>Valore</th><th>Range atteso</th><th>Decisione</th></tr></thead>
<tbody> <tbody>
{fuoriRange.map((m) => ( {daVerificare.map((m) => (
<tr> <tr>
<td>{formattaData(m.data)}</td> <td>{formattaData(m.data)}</td>
<td>{m.etichetta}</td> <td>{m.etichetta}</td>
<td class="lg-misura--fuori-range"> <td class="lg-misura--fuori-range">
{m.valore !== null ? formattaNumero(m.valore) : '—'}{m.unita ? ` ${m.unita}` : ''} {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> </td>
</tr> </tr>
))} ))}
@@ -34,12 +34,14 @@ if (fascicolo && fascicolo.userId === null && Astro.request.method === 'POST') {
{!fascicolo && ( {!fascicolo && (
<div class="lg-card"> <div class="lg-card">
<p>Nessun fascicolo con codice <strong>{code}</strong>.</p> <p>Nessun fascicolo con codice <strong>{code}</strong>.</p>
<p><a href={`/longevity/gestionale/cliente/${code}`}>Torna alla scheda</a></p>
</div> </div>
)} )}
{fascicolo && fascicolo.userId !== null && ( {fascicolo && fascicolo.userId !== null && (
<div class="lg-card"> <div class="lg-card">
<p>Questo fascicolo ha già un accesso collegato: non se ne crea un secondo.</p> <p>Questo fascicolo ha già un accesso collegato: non se ne crea un secondo.</p>
<p><a href={`/longevity/gestionale/cliente/${code}`}>Torna alla scheda</a></p>
</div> </div>
)} )}
@@ -48,6 +50,7 @@ if (fascicolo && fascicolo.userId === null && Astro.request.method === 'POST') {
<p><strong>Accesso creato.</strong> Comunicala al cliente: non verrà mostrata di nuovo.</p> <p><strong>Accesso creato.</strong> Comunicala al cliente: non verrà mostrata di nuovo.</p>
<p>Utente: <strong>{creato.username}</strong></p> <p>Utente: <strong>{creato.username}</strong></p>
<p>Password: <strong>{creato.password}</strong></p> <p>Password: <strong>{creato.password}</strong></p>
<p><a href={`/longevity/gestionale/cliente/${code}`}>Torna alla scheda</a></p>
</div> </div>
)} )}
@@ -67,6 +70,7 @@ if (fascicolo && fascicolo.userId === null && Astro.request.method === 'POST') {
/> />
</div> </div>
<div class="lg-form__nav"> <div class="lg-form__nav">
<a href={`/longevity/gestionale/cliente/${code}`}>Torna alla scheda</a>
<button type="submit">Crea accesso</button> <button type="submit">Crea accesso</button>
</div> </div>
</form> </form>
@@ -9,7 +9,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db'; import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db';
import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro'; import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro';
import { salvaCheckup, sessioniDi, misureFuoriRange } from '../../src/lib/longevity/gestionale'; import { salvaCheckup, sessioniDi } from '../../src/lib/longevity/gestionale';
import { misureDaVerificare } from '../../src/lib/longevity/misure';
import { salvaDalForm, refertoDi } from '../../src/lib/longevity/vista'; import { salvaDalForm, refertoDi } from '../../src/lib/longevity/vista';
import { import {
creaCliente, elencoClientiParam, clienteDaCodiceParam, etaDelClienteParam, apriFascicolo, creaCliente, elencoClientiParam, clienteDaCodiceParam, etaDelClienteParam, apriFascicolo,
@@ -125,7 +126,7 @@ describe('salvaCheckup rifiuta prima di scrivere: nessuna traccia parziale', ()
}); });
describe('le misure fuori range restano nel fascicolo, marcate e leggibili', () => { describe('le misure fuori range restano nel fascicolo, marcate e leggibili', () => {
it('una misura oltre il range di plausibilità (f_peso_kg, max 250) si salva, si marca, ed esce da misureFuoriRange', () => { it('una misura oltre il range di plausibilità (f_peso_kg, max 250) si salva, si marca, ed esce da misureDaVerificare', () => {
const db = dbPronto(); const db = dbPronto();
const esito = salvaCheckup(db, { const esito = salvaCheckup(db, {
client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n', client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n',
@@ -140,9 +141,9 @@ describe('le misure fuori range restano nel fascicolo, marcate e leggibili', ()
).get(esito.sessioneId) as { fuori_range: number }; ).get(esito.sessioneId) as { fuori_range: number };
expect(riga.fuori_range).toBe(1); expect(riga.fuori_range).toBe(1);
const elenco = misureFuoriRange(db, 'ISL-0001'); const elenco = misureDaVerificare(db, 'ISL-0001');
expect(elenco.length).toBe(1); expect(elenco.length).toBe(1);
expect(elenco[0]).toMatchObject({ test_id: 'f_peso_kg', valore: 500, data: '2026-09-26' }); expect(elenco[0]).toMatchObject({ test_id: 'f_peso_kg', valore_num: 500, data: '2026-09-26' });
}); });
}); });
+21 -1
View File
@@ -1,6 +1,6 @@
// tests/longevity/pagine.test.ts // tests/longevity/pagine.test.ts
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { readFileSync, existsSync } from 'node:fs'; import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
const leggi = (p: string) => readFileSync(join(process.cwd(), p), 'utf8'); const leggi = (p: string) => readFileSync(join(process.cwd(), p), 'utf8');
@@ -146,4 +146,24 @@ describe('il gestionale del trainer (Unità 2b)', () => {
expect(src).toMatch(/admin/); expect(src).toMatch(/admin/);
expect(src).toMatch(/Astro\.redirect\(\s*['"`]\/longevity\/io['"`]/); expect(src).toMatch(/Astro\.redirect\(\s*['"`]\/longevity\/io['"`]/);
}); });
it('la scheda cliente porta le azioni di verifica di una misura fuori range (#7) e il link per creare l\'accesso (#8)', () => {
const src = leggi('src/pages/longevity/gestionale/cliente/[code].astro');
expect(src).toMatch(/action=["']\/api\/longevity\/gestionale\/verifica["']/);
expect(src).toMatch(/name=["']misura_id["']/);
expect(src).toMatch(/value=["']confermata["']/);
expect(src).toMatch(/value=["']scartata["']/);
expect(src).toMatch(/\/longevity\/gestionale\/cliente\/\$\{cliente\.client_code\}\/accesso/);
});
it('nessuna pagina longevity usa piu misureFuoriRange, rimossa da gestionale.ts a favore di misureDaVerificare (#7)', () => {
// Scandisce TUTTE le pagine sotto src/pages/longevity/, stesso criterio di
// referto.test.ts: un elenco scritto qui non protegge una pagina nuova.
const dir = join(process.cwd(), 'src/pages/longevity');
const pagine = (readdirSync(dir, { recursive: true }) as string[]).filter((f) => f.endsWith('.astro'));
expect(pagine.length).toBeGreaterThan(0);
for (const f of pagine) {
expect(readFileSync(join(dir, f), 'utf8'), `src/pages/longevity/${f}`).not.toMatch(/misureFuoriRange/);
}
});
}); });