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:
@@ -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' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user