Compare commits
3 Commits
880c928f43
...
6bbfcd8f3c
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bbfcd8f3c | |||
| 9e566ad7b1 | |||
| 19035e67b0 |
@@ -7,12 +7,23 @@
|
||||
// altro modulo, un'altra chiamata.
|
||||
import type Database from 'better-sqlite3';
|
||||
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 type { CampoQuestionario } from './vista';
|
||||
|
||||
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_`,
|
||||
* 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[] {
|
||||
const fisici = testAttivi(db, alla).filter((v) => v.test_id.startsWith('f_'));
|
||||
|
||||
const ordineAssi = (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);
|
||||
const ordineAssi = ordineAssiDaiPesi(db);
|
||||
|
||||
const perAsse = new Map<string, CampoQuestionario[]>();
|
||||
for (const v of fisici) {
|
||||
@@ -102,6 +110,163 @@ export function salvaCheckup(
|
||||
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 = {
|
||||
id: number;
|
||||
data: string;
|
||||
|
||||
@@ -25,6 +25,19 @@ export function apriSessione(
|
||||
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`.
|
||||
* 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(
|
||||
`INSERT INTO misure (sessione_id, client_code, test_id, valore_num, valore_txt, unita, fonte, fuori_range)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
@@ -56,17 +68,10 @@ export function registraMisure(
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
for (const m of misure) {
|
||||
const r = range.get(m.test_id) as { range_min: number | null; range_max: number | null };
|
||||
let fuori = 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
const fuori = fuoriRangeDi(db, m.test_id, m.valore_num);
|
||||
if (fuori) fuoriRange.push(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();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { APIRoute } from 'astro';
|
||||
import { getLongevityDb } from '../../../../lib/longevity/db';
|
||||
import { etaDelCliente } from '../../../../lib/longevity/anagrafica';
|
||||
import { testAttivi, type VoceRegistro } from '../../../../lib/longevity/registro';
|
||||
import { salvaCheckup } from '../../../../lib/longevity/gestionale';
|
||||
import { salvaCheckup, correggiCheckup } from '../../../../lib/longevity/gestionale';
|
||||
import type { MisuraIn } from '../../../../lib/longevity/misure';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -54,22 +54,52 @@ export function misureDalForm(
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const form = await request.formData();
|
||||
const clientCode = String(form.get('client_code') ?? '').trim();
|
||||
const clientCodeForm = String(form.get('client_code') ?? '').trim();
|
||||
const data = String(form.get('data') ?? '').trim();
|
||||
// Un sessione_id nel form (issue #12) è una correzione: stesso form, stessa API, solo
|
||||
// l'hidden field in più — checkup.astro lo mette quando arriva con ?sessione=<id>.
|
||||
const sessioneIdRaw = String(form.get('sessione_id') ?? '').trim();
|
||||
const sessioneId = sessioneIdRaw !== '' ? Number(sessioneIdRaw) : null;
|
||||
|
||||
// Senza un codice valido non c'è una scheda a cui tornare: l'unico ritorno sensato è
|
||||
// l'indice del gestionale, non /cliente/undefined/checkup.
|
||||
if (!REGEX_CODE.test(clientCode)) {
|
||||
if (!REGEX_CODE.test(clientCodeForm)) {
|
||||
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Codice cliente non valido.')}`);
|
||||
}
|
||||
const erroreAllaScheda = (testo: string) =>
|
||||
redirect(`/longevity/gestionale/cliente/${clientCode}/checkup?errore=${encodeURIComponent(testo)}`);
|
||||
|
||||
const db = getLongevityDb();
|
||||
|
||||
// In correzione il client_code per validazioni e redirect è SEMPRE quello letto dalla
|
||||
// sessione nel database, mai quello del form (stesso principio di verifica.ts): un
|
||||
// sessione_id non è un fascicolo che si possa dirottare scrivendo un altro codice cliente.
|
||||
let clientCode = clientCodeForm;
|
||||
if (sessioneId !== null) {
|
||||
if (!Number.isInteger(sessioneId)) {
|
||||
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Sessione non valida.')}`);
|
||||
}
|
||||
const sessione = db.prepare(`SELECT client_code FROM sessioni WHERE id = ?`).get(sessioneId) as
|
||||
{ client_code: string } | undefined;
|
||||
if (!sessione) {
|
||||
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Sessione inesistente.')}`);
|
||||
}
|
||||
clientCode = sessione.client_code;
|
||||
if (clientCodeForm !== clientCode) {
|
||||
return redirect(
|
||||
`/longevity/gestionale/cliente/${clientCode}?errore=${encodeURIComponent('Codice cliente non coerente con la sessione.')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const erroreAllaScheda = (testo: string) => {
|
||||
const query = new URLSearchParams({ errore: testo });
|
||||
if (sessioneId !== null) query.set('sessione', String(sessioneId));
|
||||
return redirect(`/longevity/gestionale/cliente/${clientCode}/checkup?${query.toString()}`);
|
||||
};
|
||||
|
||||
if (!REGEX_DATA.test(data)) return erroreAllaScheda('Data non valida (formato AAAA-MM-GG).');
|
||||
const oggi = new Date().toISOString().slice(0, 10);
|
||||
if (data > oggi) return erroreAllaScheda('La data del check-up non può essere nel futuro.');
|
||||
|
||||
const db = getLongevityDb();
|
||||
const campiAttivi = testAttivi(db, data).filter((v) => v.test_id.startsWith('f_'));
|
||||
|
||||
const parse = misureDalForm(form, campiAttivi);
|
||||
@@ -81,6 +111,14 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const operatore = locals.user!.username;
|
||||
const eta = etaDelCliente(clientCode, data);
|
||||
|
||||
if (sessioneId !== null) {
|
||||
const esito = correggiCheckup(db, { sessioneId, data, eta, operatore, misure: parse.misure });
|
||||
if (!esito.ok) return erroreAllaScheda(esito.errore);
|
||||
const params = new URLSearchParams({ corretto: String(esito.sessioneId) });
|
||||
if (esito.fuoriRange.length > 0) params.set('fuori', String(esito.fuoriRange.length));
|
||||
return redirect(`/longevity/gestionale/cliente/${clientCode}?${params.toString()}`);
|
||||
}
|
||||
|
||||
const esito = salvaCheckup(db, { client_code: clientCode, data, eta, operatore, misure: parse.misure });
|
||||
if (!esito.ok) return erroreAllaScheda(esito.errore);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ const formattaData = (iso: string) =>
|
||||
|
||||
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');
|
||||
@@ -69,6 +70,7 @@ const messaggioVerifica =
|
||||
|
||||
{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>}
|
||||
{fuoriCount && (
|
||||
<p class="lg-misura--fuori-range">
|
||||
@@ -131,7 +133,9 @@ const messaggioVerifica =
|
||||
<tbody>
|
||||
{sessioni.map((s) => (
|
||||
<tr>
|
||||
<td>{formattaData(s.data)}</td>
|
||||
<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>
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
// Il modulo del check-up manuale: i campi letti dal registro (campiDelCheckup),
|
||||
// raggruppati per asse come il modulo cartaceo. L'invio va all'API del gestionale, che fa
|
||||
// la validazione vera (pages/api/longevity/gestionale/checkup.ts) — qui c'è solo il form.
|
||||
//
|
||||
// Con ?sessione=<id> (#12) lo stesso form diventa una correzione: data e campi
|
||||
// precompilati dalla sessione, hidden sessione_id in più, campi attivi alla data DELLA
|
||||
// SESSIONE (non a oggi) — un test dismesso da allora resterebbe comunque compilabile.
|
||||
import Longevity from '../../../../../layouts/Longevity.astro';
|
||||
import { getLongevityDb } from '../../../../../lib/longevity/db';
|
||||
import { clienteDaCodice } from '../../../../../lib/longevity/anagrafica';
|
||||
import { campiDelCheckup } from '../../../../../lib/longevity/gestionale';
|
||||
import { campiDelCheckup, misureDellaSessione } from '../../../../../lib/longevity/gestionale';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -13,28 +17,42 @@ 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 gruppi = campiDelCheckup(getLongevityDb(), oggi);
|
||||
|
||||
const sessioneParam = Astro.url.searchParams.get('sessione');
|
||||
const sessioneId = sessioneParam !== null ? Number(sessioneParam) : null;
|
||||
const sessione = sessioneId !== null && Number.isInteger(sessioneId) ? misureDellaSessione(db, sessioneId) : null;
|
||||
// Una sessione di un altro cliente o non di tipo checkup non è un modulo modificabile da
|
||||
// questa scheda: stesso trattamento di un codice cliente inesistente.
|
||||
if (sessioneId !== null && (!sessione || sessione.client_code !== cliente.client_code || sessione.tipo !== 'checkup')) {
|
||||
return new Response('Sessione non trovata', { status: 404 });
|
||||
}
|
||||
|
||||
const dataForm = sessione?.data ?? oggi;
|
||||
const gruppi = campiDelCheckup(db, dataForm);
|
||||
const valori = new Map(sessione?.misure.map((m) => [m.test_id, m.valore_num]) ?? []);
|
||||
const errore = Astro.url.searchParams.get('errore');
|
||||
---
|
||||
<Longevity
|
||||
title={`Check-up — ${cliente.cognome} ${cliente.nome}`}
|
||||
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, 'Nuovo check-up']}
|
||||
title={`${sessione ? 'Modifica' : 'Nuovo'} check-up — ${cliente.cognome} ${cliente.nome}`}
|
||||
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, sessione ? 'Modifica check-up' : 'Nuovo check-up']}
|
||||
variante="gestionale"
|
||||
>
|
||||
<div class="lg-head">
|
||||
<h1>Nuovo check-up</h1>
|
||||
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code}. Lascia vuoto ciò che non hai misurato oggi.</p>
|
||||
<h1>{sessione ? 'Modifica check-up' : 'Nuovo check-up'}</h1>
|
||||
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code}. Lascia vuoto ciò che non hai misurato.</p>
|
||||
</div>
|
||||
|
||||
{errore && <p class="lg-form__errore">{errore}</p>}
|
||||
|
||||
<form class="lg-form" method="post" action="/api/longevity/gestionale/checkup">
|
||||
<input type="hidden" name="client_code" value={cliente.client_code} />
|
||||
{sessione && <input type="hidden" name="sessione_id" value={sessione.id} />}
|
||||
|
||||
<label class="lg-field">
|
||||
<span>Data</span>
|
||||
<input type="date" name="data" value={oggi} required />
|
||||
<input type="date" name="data" value={dataForm} required />
|
||||
</label>
|
||||
|
||||
{gruppi.map((g) => (
|
||||
@@ -43,7 +61,7 @@ const errore = Astro.url.searchParams.get('errore');
|
||||
{g.campi.map((c) => (
|
||||
<label class="lg-field">
|
||||
<span>{c.etichetta}{c.unita ? ` (${c.unita})` : ''}</span>
|
||||
<input type="text" inputmode="decimal" name={c.id} />
|
||||
<input type="text" inputmode="decimal" name={c.id} value={valori.get(c.id) ?? ''} />
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
@@ -51,7 +69,7 @@ const errore = Astro.url.searchParams.get('errore');
|
||||
|
||||
<div class="lg-form__nav">
|
||||
<a href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Annulla</a>
|
||||
<button type="submit">Salva check-up</button>
|
||||
<button type="submit">{sessione ? 'Salva correzione' : 'Salva check-up'}</button>
|
||||
</div>
|
||||
</form>
|
||||
</Longevity>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
// Il dettaglio di una sessione (#12): data, tipo, operatore, misure con valore/unità e
|
||||
// stato di fuori-range/verifica. Da qui si apre la correzione di un check-up
|
||||
// (checkup.astro?sessione=<id>) — un questionario resta di sola lettura, la sua correzione
|
||||
// non è questa issue. Un id che non esiste, o di un altro cliente, è un 404: non c'è niente
|
||||
// da mostrare per una sessione che non è la sua.
|
||||
import Longevity from '../../../../../../layouts/Longevity.astro';
|
||||
import { getLongevityDb } from '../../../../../../lib/longevity/db';
|
||||
import { clienteDaCodice } from '../../../../../../lib/longevity/anagrafica';
|
||||
import { misureDellaSessione, campiDelCheckup, type MisuraDiSessione } from '../../../../../../lib/longevity/gestionale';
|
||||
import { formattaNumero } from '../../../../../../lib/longevity/formato';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const { code, id } = Astro.params;
|
||||
const cliente = code ? clienteDaCodice(code) : null;
|
||||
if (!cliente) return new Response('Cliente non trovato', { status: 404 });
|
||||
|
||||
const sessioneId = id ? Number(id) : NaN;
|
||||
const db = getLongevityDb();
|
||||
const sessione = Number.isInteger(sessioneId) ? misureDellaSessione(db, sessioneId) : null;
|
||||
if (!sessione || sessione.client_code !== cliente.client_code) {
|
||||
return new Response('Sessione non trovata', { status: 404 });
|
||||
}
|
||||
|
||||
const ETICHETTA_TIPO: Record<'checkup' | 'questionario', string> = {
|
||||
checkup: 'Check-up', questionario: 'Questionario',
|
||||
};
|
||||
const ETICHETTA_VERIFICA: Record<'confermata' | 'scartata', string> = {
|
||||
confermata: 'Confermata', scartata: 'Scartata',
|
||||
};
|
||||
const formattaData = (iso: string) =>
|
||||
new Date(`${iso}T00:00:00`).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
|
||||
// Un check-up si raggruppa per asse come il modulo che lo ha compilato (campiDelCheckup);
|
||||
// un questionario non ha un raggruppamento del genere nel codice — resta un'unica sezione,
|
||||
// già ordinata per asse da misureDellaSessione.
|
||||
type Sezione = { asse: string | null; misure: MisuraDiSessione[] };
|
||||
let sezioni: Sezione[];
|
||||
if (sessione.tipo === 'checkup') {
|
||||
const perTestId = new Map(sessione.misure.map((m) => [m.test_id, m]));
|
||||
sezioni = campiDelCheckup(db, sessione.data)
|
||||
.map((g) => ({
|
||||
asse: g.asse,
|
||||
misure: g.campi.map((c) => perTestId.get(c.id)).filter((m): m is MisuraDiSessione => m !== undefined),
|
||||
}))
|
||||
.filter((s) => s.misure.length > 0);
|
||||
} else {
|
||||
sezioni = [{ asse: null, misure: sessione.misure }];
|
||||
}
|
||||
---
|
||||
<Longevity
|
||||
title={`Sessione del ${formattaData(sessione.data)} — ${cliente.cognome} ${cliente.nome}`}
|
||||
crumbs={['Clienti', `${cliente.cognome} ${cliente.nome}`, `Sessione del ${formattaData(sessione.data)}`]}
|
||||
variante="gestionale"
|
||||
>
|
||||
<div class="lg-head lg-head--azioni">
|
||||
<div>
|
||||
<h1>{ETICHETTA_TIPO[sessione.tipo]} del {formattaData(sessione.data)}</h1>
|
||||
<p>{cliente.cognome} {cliente.nome} — {cliente.client_code} · operatore {sessione.operatore ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
{sessione.tipo === 'checkup' && (
|
||||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}/checkup?sessione=${sessione.id}`}>Modifica</a>
|
||||
)}
|
||||
<a class="lg-btn" href={`/longevity/gestionale/cliente/${cliente.client_code}`}>Torna alla scheda</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sezioni.map((s) => (
|
||||
<div class="lg-card">
|
||||
{s.asse && <p class="lg-score__label">{s.asse}</p>}
|
||||
<table class="lg-table">
|
||||
<thead><tr><th>Test</th><th>Valore</th><th>Stato</th></tr></thead>
|
||||
<tbody>
|
||||
{s.misure.map((m) => (
|
||||
<tr>
|
||||
<td>{m.etichetta}</td>
|
||||
<td class:list={{ 'lg-misura--fuori-range': m.fuori_range === 1 }}>
|
||||
{m.valore_num !== null ? formattaNumero(m.valore_num) : (m.valore_txt ?? '—')}
|
||||
{m.unita ? ` ${m.unita}` : ''}
|
||||
</td>
|
||||
<td>
|
||||
{m.fuori_range === 0
|
||||
? '—'
|
||||
: m.verifica
|
||||
? ETICHETTA_VERIFICA[m.verifica]
|
||||
: 'Da verificare'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</Longevity>
|
||||
@@ -0,0 +1,62 @@
|
||||
// Issue #12: la correzione di un check-up passa dalla stessa API di salvaCheckup, distinta
|
||||
// da un sessione_id nel form. File separato da gestionale.test.ts apposta: l'endpoint usa il
|
||||
// singleton getLongevityDb() (db.ts), e gestionale.test.ts ha già un test (apriFascicolo)
|
||||
// che lo inizializza — nello stesso file la variabile d'ambiente impostata qui arriverebbe
|
||||
// troppo tardi. Stesso trucco di questionario-pagina.test.ts: LONGEVITY_DB_PATH va impostata
|
||||
// PRIMA che il singleton apra il suo longevity.db. Il setup usa lo STESSO getLongevityDb()
|
||||
// che poi chiamerà POST (una sola connessione, non due sullo stesso file): così si può
|
||||
// chiuderla nel finally, altrimenti rmSync fallisce con EPERM su Windows a handle aperto.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { getLongevityDb } from '../../src/lib/longevity/db';
|
||||
import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro';
|
||||
import { salvaCheckup } from '../../src/lib/longevity/gestionale';
|
||||
|
||||
describe('API checkup.ts, correzione (#12): il client_code del redirect e delle validazioni viene sempre dalla sessione', () => {
|
||||
it('un sessione_id di un altro cliente rispetto al client_code del form: errore, niente scritto', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'lg-api-checkup-test-'));
|
||||
const lgPath = join(dir, 'longevity.db');
|
||||
const primaLg = process.env.LONGEVITY_DB_PATH;
|
||||
let lg: ReturnType<typeof getLongevityDb> | undefined;
|
||||
try {
|
||||
process.env.LONGEVITY_DB_PATH = lgPath;
|
||||
|
||||
lg = getLongevityDb();
|
||||
seedRegistro(lg);
|
||||
seedPesi(lg, MODEL_VERSION);
|
||||
lg.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run();
|
||||
lg.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0002', 'M')`).run();
|
||||
|
||||
const esito = salvaCheckup(lg, {
|
||||
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 { POST } = await import('../../src/pages/api/longevity/gestionale/checkup');
|
||||
const form = new FormData();
|
||||
form.set('sessione_id', String(esito.sessioneId));
|
||||
form.set('client_code', 'ISL-0002'); // la sessione è di ISL-0001: incoerente
|
||||
form.set('data', '2026-09-26');
|
||||
form.set('f_peso_kg', '999'); // se scrivesse, sarebbe questo il valore
|
||||
const richiesta = new Request('http://localhost/api/longevity/gestionale/checkup', { method: 'POST', body: form });
|
||||
|
||||
const res = await POST({ request: richiesta, locals: { user: { username: 'donata' } } } as any);
|
||||
expect(res.status).toBe(303);
|
||||
expect(res.headers.get('Location')).toMatch(/^\/longevity\/gestionale\/cliente\/ISL-0001\?/); // redirect sul cliente VERO, non su quello del form
|
||||
expect(res.headers.get('Location')).toMatch(/errore=/);
|
||||
|
||||
const riga = lg.prepare(
|
||||
`SELECT valore_num FROM misure WHERE sessione_id = ? AND test_id = 'f_peso_kg'`
|
||||
).get(esito.sessioneId) as { valore_num: number };
|
||||
expect(riga.valore_num).toBe(80); // invariato: nessuna scrittura
|
||||
} finally {
|
||||
lg?.close(); // libera l'handle sul file: senza, rmSync fallisce con EPERM su Windows
|
||||
if (primaLg === undefined) delete process.env.LONGEVITY_DB_PATH; else process.env.LONGEVITY_DB_PATH = primaLg;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,9 @@ import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db';
|
||||
import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro';
|
||||
import { salvaCheckup, sessioniDi } from '../../src/lib/longevity/gestionale';
|
||||
import { misureDaVerificare } from '../../src/lib/longevity/misure';
|
||||
import { salvaCheckup, sessioniDi, correggiCheckup, misureDellaSessione } from '../../src/lib/longevity/gestionale';
|
||||
import { misureDaVerificare, verificaMisura } from '../../src/lib/longevity/misure';
|
||||
import { leggiScore } from '../../src/lib/longevity/motore';
|
||||
import { salvaDalForm, refertoDi } from '../../src/lib/longevity/vista';
|
||||
import {
|
||||
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)', () => {
|
||||
it('elencoClientiParam ordina per cognome poi nome, e dice chi ha già un accesso', () => {
|
||||
const id = createIdentityDb(':memory:');
|
||||
|
||||
Reference in New Issue
Block a user