longevity: correggiCheckup e misureDellaSessione, sovrascrittura misure
Aggiunge la correzione di un check-up gia' salvato: aggiorna sessione e misure (cambiate/tolte/nuove), azzera la verifica sulle misure cambiate, ricalcola i punteggi della sessione e delle successive. Estratto fuoriRangeDi in misure.ts per non duplicare il calcolo min/max. Refs #12 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkWhSnSLvAyRCoWBuCpXvp
This commit is contained in:
@@ -7,12 +7,23 @@
|
|||||||
// altro modulo, un'altra chiamata.
|
// altro modulo, un'altra chiamata.
|
||||||
import type Database from 'better-sqlite3';
|
import type Database from 'better-sqlite3';
|
||||||
import { testAttivi, esisteTest, MODEL_VERSION } from './registro';
|
import { testAttivi, esisteTest, MODEL_VERSION } from './registro';
|
||||||
import { apriSessione, registraMisure, type MisuraIn } from './misure';
|
import { apriSessione, registraMisure, fuoriRangeDi, type MisuraIn, type Fonte } from './misure';
|
||||||
import { calcolaSessione, salvaScore, leggiScore } from './motore';
|
import { calcolaSessione, salvaScore, leggiScore } from './motore';
|
||||||
import type { CampoQuestionario } from './vista';
|
import type { CampoQuestionario } from './vista';
|
||||||
|
|
||||||
export type GruppoCheckup = { asse: string; campi: CampoQuestionario[] };
|
export type GruppoCheckup = { asse: string; campi: CampoQuestionario[] };
|
||||||
|
|
||||||
|
/** L'ordine degli assi dai pesi (livello 'asse'), nell'ordine in cui compaiono per la prima
|
||||||
|
* volta — stesso criterio di `contenitoriDi` in vista.ts. Estratto qui perché lo usano sia
|
||||||
|
* `campiDelCheckup` sia `misureDellaSessione` (#12): due elenchi di assi che potrebbero
|
||||||
|
* divergere sarebbero peggio di uno solo. */
|
||||||
|
function ordineAssiDaiPesi(db: Database.Database): string[] {
|
||||||
|
return (db.prepare(
|
||||||
|
`SELECT contenitore FROM pesi WHERE model_version = ? AND livello = 'asse'
|
||||||
|
GROUP BY contenitore ORDER BY MIN(rowid)`
|
||||||
|
).all(MODEL_VERSION) as { contenitore: string }[]).map((r) => r.contenitore);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I campi del modulo di check-up alla data indicata: solo i test_id fisici (prefisso `f_`,
|
* I campi del modulo di check-up alla data indicata: solo i test_id fisici (prefisso `f_`,
|
||||||
* per lo stesso motivo per cui `campiDelQuestionario` in vista.ts prende solo `q_*`),
|
* per lo stesso motivo per cui `campiDelQuestionario` in vista.ts prende solo `q_*`),
|
||||||
@@ -26,10 +37,7 @@ export type GruppoCheckup = { asse: string; campi: CampoQuestionario[] };
|
|||||||
export function campiDelCheckup(db: Database.Database, alla: string): GruppoCheckup[] {
|
export function campiDelCheckup(db: Database.Database, alla: string): GruppoCheckup[] {
|
||||||
const fisici = testAttivi(db, alla).filter((v) => v.test_id.startsWith('f_'));
|
const fisici = testAttivi(db, alla).filter((v) => v.test_id.startsWith('f_'));
|
||||||
|
|
||||||
const ordineAssi = (db.prepare(
|
const ordineAssi = ordineAssiDaiPesi(db);
|
||||||
`SELECT contenitore FROM pesi WHERE model_version = ? AND livello = 'asse'
|
|
||||||
GROUP BY contenitore ORDER BY MIN(rowid)`
|
|
||||||
).all(MODEL_VERSION) as { contenitore: string }[]).map((r) => r.contenitore);
|
|
||||||
|
|
||||||
const perAsse = new Map<string, CampoQuestionario[]>();
|
const perAsse = new Map<string, CampoQuestionario[]>();
|
||||||
for (const v of fisici) {
|
for (const v of fisici) {
|
||||||
@@ -102,6 +110,163 @@ export function salvaCheckup(
|
|||||||
return { ok: true, sessioneId, fuoriRange };
|
return { ok: true, sessioneId, fuoriRange };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MisuraDiSessione = {
|
||||||
|
id: number; test_id: string; etichetta: string;
|
||||||
|
valore_num: number | null; valore_txt: string | null; unita: string | null;
|
||||||
|
fonte: Fonte; fuori_range: number; verifica: 'confermata' | 'scartata' | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessioneDettaglio = {
|
||||||
|
id: number; client_code: string; data: string; tipo: 'checkup' | 'questionario';
|
||||||
|
operatore: string | null; misure: MisuraDiSessione[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Il dettaglio di una sessione con le sue misure (#12, pagina di dettaglio e precompilazione
|
||||||
|
* della correzione): null se la sessione non esiste. Le misure sono ordinate per asse —
|
||||||
|
* stesso criterio di `campiDelCheckup`, via `ordineAssiDaiPesi` — poi per test_id, così il
|
||||||
|
* dettaglio di un check-up si legge negli stessi raggruppamenti del modulo che lo ha
|
||||||
|
* compilato.
|
||||||
|
*/
|
||||||
|
export function misureDellaSessione(db: Database.Database, sessioneId: number): SessioneDettaglio | null {
|
||||||
|
const sess = db.prepare(
|
||||||
|
`SELECT id, client_code, data, tipo, operatore FROM sessioni WHERE id = ?`
|
||||||
|
).get(sessioneId) as
|
||||||
|
{ id: number; client_code: string; data: string; tipo: 'checkup' | 'questionario'; operatore: string | null }
|
||||||
|
| undefined;
|
||||||
|
if (!sess) return null;
|
||||||
|
|
||||||
|
const righe = db.prepare(
|
||||||
|
`SELECT m.id AS id, m.test_id AS test_id, r.etichetta AS etichetta, r.asse AS asse,
|
||||||
|
m.valore_num AS valore_num, m.valore_txt AS valore_txt, m.unita AS unita,
|
||||||
|
m.fonte AS fonte, m.fuori_range AS fuori_range, m.verifica AS verifica
|
||||||
|
FROM misure m JOIN registro_test r ON r.test_id = m.test_id
|
||||||
|
WHERE m.sessione_id = ?`
|
||||||
|
).all(sessioneId) as (MisuraDiSessione & { asse: string | null })[];
|
||||||
|
|
||||||
|
const posizioneAsse = new Map(ordineAssiDaiPesi(db).map((a, i) => [a, i]));
|
||||||
|
righe.sort((a, b) => {
|
||||||
|
const pa = posizioneAsse.get(a.asse ?? '') ?? Infinity;
|
||||||
|
const pb = posizioneAsse.get(b.asse ?? '') ?? Infinity;
|
||||||
|
return pa - pb || a.test_id.localeCompare(b.test_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const misure: MisuraDiSessione[] = righe.map(({ asse: _asse, ...m }) => m);
|
||||||
|
return { id: sess.id, client_code: sess.client_code, data: sess.data, tipo: sess.tipo, operatore: sess.operatore, misure };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corregge un check-up già salvato (#12): stesse validazioni di `salvaCheckup`, poi
|
||||||
|
* un'unica transazione che aggiorna la sessione e le sue misure e ricalcola i punteggi.
|
||||||
|
* Nessuno storico delle misure: la correzione SOVRASCRIVE (decisione di Adriano, 27/09/2026)
|
||||||
|
* — una misura cambiata perde la verifica precedente (il trainer deve rivederla sul nuovo
|
||||||
|
* valore, non su quello vecchio), una tolta dal form si cancella, una nuova si inserisce
|
||||||
|
* con `registraMisure` (fonte 'manuale': resta l'unico INSERT di `misure`, vedi misure.ts).
|
||||||
|
*
|
||||||
|
* Rifiuta PRIMA di scrivere: nessuna misura, un test_id fuori dal registro o non fisico
|
||||||
|
* (stesso criterio di `salvaCheckup`), una sessione inesistente, o una sessione che non è
|
||||||
|
* un check-up (un questionario non si corregge da qui).
|
||||||
|
*/
|
||||||
|
export function correggiCheckup(
|
||||||
|
db: Database.Database,
|
||||||
|
input: {
|
||||||
|
sessioneId: number; data: string; eta: number | null;
|
||||||
|
operatore: string; misure: MisuraIn[];
|
||||||
|
}
|
||||||
|
): EsitoCheckup {
|
||||||
|
if (input.misure.length === 0) {
|
||||||
|
return { ok: false, errore: 'Nessuna misura da salvare.' };
|
||||||
|
}
|
||||||
|
for (const m of input.misure) {
|
||||||
|
if (!m.test_id.startsWith('f_')) {
|
||||||
|
return { ok: false, errore: `Il check-up accetta solo test fisici: ${m.test_id}` };
|
||||||
|
}
|
||||||
|
if (!esisteTest(db, m.test_id)) {
|
||||||
|
return { ok: false, errore: `Test non riconosciuto: ${m.test_id}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessione = db.prepare(
|
||||||
|
`SELECT id, client_code, data, tipo FROM sessioni WHERE id = ?`
|
||||||
|
).get(input.sessioneId) as
|
||||||
|
{ id: number; client_code: string; data: string; tipo: 'checkup' | 'questionario' } | undefined;
|
||||||
|
if (!sessione) {
|
||||||
|
return { ok: false, errore: `Sessione inesistente: ${input.sessioneId}` };
|
||||||
|
}
|
||||||
|
if (sessione.tipo !== 'checkup') {
|
||||||
|
return { ok: false, errore: 'Solo un check-up si corregge da qui, non un questionario.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo le misure fisiche (f_*) della sessione entrano nel confronto: di tipo checkup non
|
||||||
|
// ce ne sono altre, ma per sicurezza (e per lo stesso motivo di salvaCheckup) una misura
|
||||||
|
// non f_* eventualmente presente non si tocca.
|
||||||
|
const esistenti = (db.prepare(
|
||||||
|
`SELECT id, test_id, valore_num, unita FROM misure WHERE sessione_id = ?`
|
||||||
|
).all(input.sessioneId) as { id: number; test_id: string; valore_num: number | null; unita: string | null }[])
|
||||||
|
.filter((r) => r.test_id.startsWith('f_'));
|
||||||
|
const esistentiPerTestId = new Map(esistenti.map((r) => [r.test_id, r]));
|
||||||
|
const nuoviPerTestId = new Map(input.misure.map((m) => [m.test_id, m]));
|
||||||
|
|
||||||
|
const fuoriRange: string[] = [];
|
||||||
|
const daInserire: MisuraIn[] = [];
|
||||||
|
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE sessioni SET data = ?, eta_alla_data = ?, operatore = ? WHERE id = ?`
|
||||||
|
).run(input.data, input.eta ?? null, input.operatore, input.sessioneId);
|
||||||
|
|
||||||
|
const tuttiTestId = new Set([...esistentiPerTestId.keys(), ...nuoviPerTestId.keys()]);
|
||||||
|
for (const testId of tuttiTestId) {
|
||||||
|
const vecchia = esistentiPerTestId.get(testId);
|
||||||
|
const nuova = nuoviPerTestId.get(testId);
|
||||||
|
|
||||||
|
if (vecchia && !nuova) {
|
||||||
|
db.prepare(`DELETE FROM misure WHERE id = ?`).run(vecchia.id); // tolto dal form
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!vecchia && nuova) {
|
||||||
|
daInserire.push(nuova); // nuovo campo compilato: un solo INSERT via registraMisure
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (vecchia && nuova) {
|
||||||
|
const invariato = (vecchia.valore_num ?? null) === (nuova.valore_num ?? null);
|
||||||
|
if (invariato) continue; // la riga resta com'è: mantiene verifica/verificata_*
|
||||||
|
|
||||||
|
const fuori = fuoriRangeDi(db, testId, nuova.valore_num);
|
||||||
|
if (fuori) fuoriRange.push(testId);
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE misure SET valore_num = ?, unita = ?, fuori_range = ?,
|
||||||
|
verifica = NULL, verificata_da = NULL, verificata_at = NULL
|
||||||
|
WHERE id = ?`
|
||||||
|
).run(nuova.valore_num ?? null, nuova.unita ?? null, fuori ? 1 : 0, vecchia.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daInserire.length > 0) {
|
||||||
|
const { fuoriRange: fuoriNuove } = registraMisure(db, input.sessioneId, 'manuale', daInserire);
|
||||||
|
fuoriRange.push(...fuoriNuove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ricalcolo: la sessione stessa più ogni sessione del cliente dalla posizione minima fra
|
||||||
|
// quella vecchia e quella nuova (stesso criterio di verificaMisura, misure.ts) — l'id
|
||||||
|
// non cambia con una correzione, solo la data può spostare la sessione: il minimo copre
|
||||||
|
// entrambi i lati.
|
||||||
|
const dataMin = input.data < sessione.data ? input.data : sessione.data;
|
||||||
|
const daRicalcolare = db.prepare(
|
||||||
|
`SELECT id FROM sessioni
|
||||||
|
WHERE client_code = ? AND (data > ? OR (data = ? AND id >= ?))
|
||||||
|
ORDER BY data ASC, id ASC`
|
||||||
|
).all(sessione.client_code, dataMin, dataMin, input.sessioneId) as { id: number }[];
|
||||||
|
|
||||||
|
for (const s of daRicalcolare) {
|
||||||
|
salvaScore(db, s.id, calcolaSessione(db, s.id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
|
||||||
|
return { ok: true, sessioneId: input.sessioneId, fuoriRange };
|
||||||
|
}
|
||||||
|
|
||||||
export type SessioneElenco = {
|
export type SessioneElenco = {
|
||||||
id: number;
|
id: number;
|
||||||
data: string;
|
data: string;
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ export function apriSessione(
|
|||||||
return Number(r.lastInsertRowid);
|
return Number(r.lastInsertRowid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True se un valore numerico esce dal range di plausibilità del test (registro_test).
|
||||||
|
* Unico punto di calcolo: usato da `registraMisure` all'inserimento e da `correggiCheckup`
|
||||||
|
* (gestionale.ts, #12) alla correzione, così il confronto min/max non vive in due posti.
|
||||||
|
*/
|
||||||
|
export function fuoriRangeDi(db: Database.Database, testId: string, valoreNum: number | undefined): boolean {
|
||||||
|
if (valoreNum === undefined) return false;
|
||||||
|
const r = db.prepare(`SELECT range_min, range_max FROM registro_test WHERE test_id = ?`).get(testId) as
|
||||||
|
{ range_min: number | null; range_max: number | null };
|
||||||
|
return (r.range_min !== null && valoreNum < r.range_min) ||
|
||||||
|
(r.range_max !== null && valoreNum > r.range_max);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* L'unico punto in cui si scrive nella tabella `misure`.
|
* L'unico punto in cui si scrive nella tabella `misure`.
|
||||||
* Un test_id sconosciuto fa fallire l'intero lotto: meglio un errore subito che una
|
* Un test_id sconosciuto fa fallire l'intero lotto: meglio un errore subito che una
|
||||||
@@ -47,7 +60,6 @@ export function registraMisure(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = db.prepare(`SELECT range_min, range_max FROM registro_test WHERE test_id = ?`);
|
|
||||||
const ins = db.prepare(
|
const ins = db.prepare(
|
||||||
`INSERT INTO misure (sessione_id, client_code, test_id, valore_num, valore_txt, unita, fonte, fuori_range)
|
`INSERT INTO misure (sessione_id, client_code, test_id, valore_num, valore_txt, unita, fonte, fuori_range)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
@@ -56,17 +68,10 @@ export function registraMisure(
|
|||||||
|
|
||||||
const tx = db.transaction(() => {
|
const tx = db.transaction(() => {
|
||||||
for (const m of misure) {
|
for (const m of misure) {
|
||||||
const r = range.get(m.test_id) as { range_min: number | null; range_max: number | null };
|
const fuori = fuoriRangeDi(db, m.test_id, m.valore_num);
|
||||||
let fuori = 0;
|
if (fuori) fuoriRange.push(m.test_id);
|
||||||
if (m.valore_num !== undefined) {
|
|
||||||
if ((r.range_min !== null && m.valore_num < r.range_min) ||
|
|
||||||
(r.range_max !== null && m.valore_num > r.range_max)) {
|
|
||||||
fuori = 1;
|
|
||||||
fuoriRange.push(m.test_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ins.run(sessioneId, sess.client_code, m.test_id,
|
ins.run(sessioneId, sess.client_code, m.test_id,
|
||||||
m.valore_num ?? null, m.valore_txt ?? null, m.unita ?? null, fonte, fuori);
|
m.valore_num ?? null, m.valore_txt ?? null, m.unita ?? null, fonte, fuori ? 1 : 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tx();
|
tx();
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ 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 } from '../../src/lib/longevity/gestionale';
|
import { salvaCheckup, sessioniDi, correggiCheckup, misureDellaSessione } from '../../src/lib/longevity/gestionale';
|
||||||
import { misureDaVerificare } from '../../src/lib/longevity/misure';
|
import { misureDaVerificare, verificaMisura } from '../../src/lib/longevity/misure';
|
||||||
|
import { leggiScore } from '../../src/lib/longevity/motore';
|
||||||
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,
|
||||||
@@ -170,6 +171,204 @@ describe('sessioniDi', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('misureDellaSessione', () => {
|
||||||
|
it('null se la sessione non esiste; altrimenti le misure ordinate per asse come campiDelCheckup', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
expect(misureDellaSessione(db, 999999)).toBeNull();
|
||||||
|
|
||||||
|
const esito = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'nicola',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }, { test_id: 'f_handgrip_kg', valore_num: 45 }],
|
||||||
|
});
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
if (!esito.ok) throw new Error(esito.errore);
|
||||||
|
|
||||||
|
const dettaglio = misureDellaSessione(db, esito.sessioneId);
|
||||||
|
expect(dettaglio?.tipo).toBe('checkup');
|
||||||
|
expect(dettaglio?.operatore).toBe('nicola');
|
||||||
|
// 'Forza & Struttura' precede 'Composizione Corporea' nei pesi (PESI_ASSE, registro.ts):
|
||||||
|
// stesso ordine che userebbe campiDelCheckup per raggruppare il modulo.
|
||||||
|
expect(dettaglio?.misure.map((m) => m.test_id)).toEqual(['f_handgrip_kg', 'f_peso_kg']);
|
||||||
|
expect(dettaglio?.misure[1]).toMatchObject({ etichetta: 'Peso corporeo', valore_num: 80, verifica: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('correggiCheckup: sovrascrittura, nessuno storico delle misure (#12)', () => {
|
||||||
|
it('corregge un valore: misura aggiornata, score della sessione ricalcolato; la successiva viene ricalcolata, la precedente resta intatta', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const precedente = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-01', eta: 38, operatore: 'nicola',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
const target = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-10', eta: 38, operatore: 'nicola',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 82 }],
|
||||||
|
});
|
||||||
|
const successiva = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-20', eta: 38, operatore: 'nicola',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 83 }],
|
||||||
|
});
|
||||||
|
expect(precedente.ok && target.ok && successiva.ok).toBe(true);
|
||||||
|
if (!precedente.ok || !target.ok || !successiva.ok) throw new Error('setup fallito');
|
||||||
|
|
||||||
|
const contaScore = (id: number) => (db.prepare(`SELECT COUNT(*) n FROM score WHERE sessione_id = ?`).get(id) as { n: number }).n;
|
||||||
|
const primaPrecedente = contaScore(precedente.sessioneId);
|
||||||
|
const primaTarget = contaScore(target.sessioneId);
|
||||||
|
const primaSuccessiva = contaScore(successiva.sessioneId);
|
||||||
|
|
||||||
|
const esito = correggiCheckup(db, {
|
||||||
|
sessioneId: target.sessioneId, data: '2026-09-10', eta: 38, operatore: 'donata',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 90 }],
|
||||||
|
});
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
if (!esito.ok) throw new Error(esito.errore);
|
||||||
|
expect(esito.sessioneId).toBe(target.sessioneId);
|
||||||
|
|
||||||
|
const riga = db.prepare(
|
||||||
|
`SELECT valore_num FROM misure WHERE sessione_id = ? AND test_id = 'f_peso_kg'`
|
||||||
|
).get(target.sessioneId) as { valore_num: number };
|
||||||
|
expect(riga.valore_num).toBe(90);
|
||||||
|
|
||||||
|
const sessioneAggiornata = db.prepare(`SELECT operatore FROM sessioni WHERE id = ?`).get(target.sessioneId) as { operatore: string };
|
||||||
|
expect(sessioneAggiornata.operatore).toBe('donata'); // l'operatore diventa chi corregge
|
||||||
|
|
||||||
|
// score ricalcolato (righe nuove in append): target e successiva sì, precedente no.
|
||||||
|
expect(contaScore(target.sessioneId)).toBeGreaterThan(primaTarget);
|
||||||
|
expect(contaScore(successiva.sessioneId)).toBeGreaterThan(primaSuccessiva);
|
||||||
|
expect(contaScore(precedente.sessioneId)).toBe(primaPrecedente);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('campo svuotato cancella la misura; campo nuovo la inserisce con fonte manuale', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const esito = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'nicola',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
if (!esito.ok) throw new Error(esito.errore);
|
||||||
|
|
||||||
|
const corretto = correggiCheckup(db, {
|
||||||
|
sessioneId: esito.sessioneId, data: '2026-09-26', eta: 38, operatore: 'nicola',
|
||||||
|
// f_peso_kg non c'è più (campo svuotato nel form), f_handgrip_kg è nuovo.
|
||||||
|
misure: [{ test_id: 'f_handgrip_kg', valore_num: 45 }],
|
||||||
|
});
|
||||||
|
expect(corretto.ok).toBe(true);
|
||||||
|
|
||||||
|
const righe = db.prepare(`SELECT test_id, fonte FROM misure WHERE sessione_id = ?`).all(esito.sessioneId) as
|
||||||
|
{ test_id: string; fonte: string }[];
|
||||||
|
expect(righe).toEqual([{ test_id: 'f_handgrip_kg', fonte: 'manuale' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('valore invariato conserva la verifica; valore cambiato azzera la verifica e ricalcola fuori_range', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const esito = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'nicola',
|
||||||
|
// 500 kg è oltre il range di plausibilità (max 250): fuori range da subito.
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 500 }, { test_id: 'f_handgrip_kg', valore_num: 45 }],
|
||||||
|
});
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
if (!esito.ok) throw new Error(esito.errore);
|
||||||
|
|
||||||
|
const misuraPeso = db.prepare(
|
||||||
|
`SELECT id FROM misure WHERE sessione_id = ? AND test_id = 'f_peso_kg'`
|
||||||
|
).get(esito.sessioneId) as { id: number };
|
||||||
|
const conferma = verificaMisura(db, { misuraId: misuraPeso.id, esito: 'confermata', operatore: 'nicola' });
|
||||||
|
expect(conferma.ok).toBe(true);
|
||||||
|
|
||||||
|
// Correzione: f_peso_kg invariato (500), f_handgrip_kg cambiato (45 -> 50).
|
||||||
|
const corretto = correggiCheckup(db, {
|
||||||
|
sessioneId: esito.sessioneId, data: '2026-09-26', eta: 38, operatore: 'nicola',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 500 }, { test_id: 'f_handgrip_kg', valore_num: 50 }],
|
||||||
|
});
|
||||||
|
expect(corretto.ok).toBe(true);
|
||||||
|
|
||||||
|
const pesoDopo = db.prepare(
|
||||||
|
`SELECT verifica, fuori_range FROM misure WHERE id = ?`
|
||||||
|
).get(misuraPeso.id) as { verifica: string | null; fuori_range: number };
|
||||||
|
expect(pesoDopo.verifica).toBe('confermata'); // invariato: la verifica resta
|
||||||
|
expect(pesoDopo.fuori_range).toBe(1);
|
||||||
|
|
||||||
|
const handgripDopo = db.prepare(
|
||||||
|
`SELECT valore_num, fuori_range FROM misure WHERE sessione_id = ? AND test_id = 'f_handgrip_kg'`
|
||||||
|
).get(esito.sessioneId) as { valore_num: number; fuori_range: number };
|
||||||
|
expect(handgripDopo.valore_num).toBe(50);
|
||||||
|
expect(handgripDopo.fuori_range).toBe(0); // 50 è nel range (0-100): ricalcolato, non ereditato
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cambio data che sposta la sessione dopo un\'altra: entrambe ricalcolate', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const a = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-01', eta: 38, operatore: 'n',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
const b = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-15', eta: 38, operatore: 'n',
|
||||||
|
misure: [{ test_id: 'f_handgrip_kg', valore_num: 45 }],
|
||||||
|
});
|
||||||
|
expect(a.ok && b.ok).toBe(true);
|
||||||
|
if (!a.ok || !b.ok) throw new Error('setup fallito');
|
||||||
|
|
||||||
|
const contaScore = (id: number) => (db.prepare(`SELECT COUNT(*) n FROM score WHERE sessione_id = ?`).get(id) as { n: number }).n;
|
||||||
|
const primaA = contaScore(a.sessioneId);
|
||||||
|
const primaB = contaScore(b.sessioneId);
|
||||||
|
|
||||||
|
// Sposta A dopo B: la posizione minima (min fra vecchia e nuova data di A) resta
|
||||||
|
// 2026-09-01, quindi la ricalcolata copre sia A sia B.
|
||||||
|
const esito = correggiCheckup(db, {
|
||||||
|
sessioneId: a.sessioneId, data: '2026-09-20', eta: 38, operatore: 'n',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
|
||||||
|
expect(contaScore(a.sessioneId)).toBeGreaterThan(primaA);
|
||||||
|
expect(contaScore(b.sessioneId)).toBeGreaterThan(primaB);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rifiuti: sessione inesistente, sessione questionario, nessuna misura, test non f_* — nessuna scrittura', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
|
||||||
|
const inesistente = correggiCheckup(db, {
|
||||||
|
sessioneId: 999999, data: '2026-09-26', eta: 38, operatore: 'n', misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
expect(inesistente.ok).toBe(false);
|
||||||
|
|
||||||
|
const q = salvaDalForm(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-01', eta: 38,
|
||||||
|
risposte: { q_ore_sonno: 7.5, q_min_addorm: 15, q_risvegli: 0, q_riposato: 8, q_caffeina: 1, q_sonnolenza_diurna: 1 },
|
||||||
|
consensoSanitario: 'Si', liberi: {},
|
||||||
|
});
|
||||||
|
expect(q.ok).toBe(true);
|
||||||
|
if (!q.ok) throw new Error('setup questionario fallito');
|
||||||
|
const suQuestionario = correggiCheckup(db, {
|
||||||
|
sessioneId: q.sessioneId, data: '2026-09-01', eta: 38, operatore: 'n', misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
expect(suQuestionario.ok).toBe(false);
|
||||||
|
|
||||||
|
const checkup = salvaCheckup(db, {
|
||||||
|
client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'n',
|
||||||
|
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||||
|
});
|
||||||
|
expect(checkup.ok).toBe(true);
|
||||||
|
if (!checkup.ok) throw new Error('setup checkup fallito');
|
||||||
|
|
||||||
|
const nessunaMisura = correggiCheckup(db, {
|
||||||
|
sessioneId: checkup.sessioneId, data: '2026-09-26', eta: 38, operatore: 'n', misure: [],
|
||||||
|
});
|
||||||
|
expect(nessunaMisura.ok).toBe(false);
|
||||||
|
|
||||||
|
const nonFisico = correggiCheckup(db, {
|
||||||
|
sessioneId: checkup.sessioneId, data: '2026-09-26', eta: 38, operatore: 'n', misure: [{ test_id: 'q_ore_sonno', valore_num: 7 }],
|
||||||
|
});
|
||||||
|
expect(nonFisico.ok).toBe(false);
|
||||||
|
|
||||||
|
// Nessuna delle chiamate rifiutate ha scritto: la misura del checkup è ancora quella originale.
|
||||||
|
const rigaFinale = db.prepare(
|
||||||
|
`SELECT valore_num FROM misure WHERE sessione_id = ? AND test_id = 'f_peso_kg'`
|
||||||
|
).get(checkup.sessioneId) as { valore_num: number };
|
||||||
|
expect(rigaFinale.valore_num).toBe(80);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('anagrafica per il gestionale, versioni parametrizzate (testabili su :memory:, come codicePerUtente accanto a codicePerUtenteLoggato)', () => {
|
describe('anagrafica per il gestionale, versioni parametrizzate (testabili su :memory:, come codicePerUtente accanto a codicePerUtenteLoggato)', () => {
|
||||||
it('elencoClientiParam ordina per cognome poi nome, e dice chi ha già un accesso', () => {
|
it('elencoClientiParam ordina per cognome poi nome, e dice chi ha già un accesso', () => {
|
||||||
const id = createIdentityDb(':memory:');
|
const id = createIdentityDb(':memory:');
|
||||||
|
|||||||
Reference in New Issue
Block a user