longevity: registro e motore per la nuova batteria test (manuale Nicola 27/09)
Nuovi test_id nel registro (altezza, FC riposo, handgrip/back scratch/flamingo dx-sx, panca manubri/bilanciere, trazioni, sit-to-stand, plank ginocchia, 6MWT, 2km walk UKK, MILFIT, MUTT FC max/1'/2'/step) con attivo_da, e i quattro test_id storici sostituiti (handgrip_kg, back_scratch_cm, flamingo_cadute, hrr_1min_bpm) marcati attivo_a 2026-09-28 senza cancellarli. motore/index.ts: nuova recenzaValideAlla (posizione di recenza per test_id, stesso ORDER BY di misureValideAlla, fattorizzata la query condivisa). motore/fisici.ts: regola dei gruppi in sottoDominiFisici (recenza opzionale) - per ogni sotto-metrica con piu' test alternativi si sceglie l'alternativa COMPLETA la cui misura piu' recente vince; senza recenza resta l'ordine di priorita' di oggi. Handgrip/back_scratch/flamingo ora mediano dx/sx; spinta e arti_inferiori aggiungono panca bilanciere e sit-to-stand; vo2max aggiunge MILFIT e 2km walk alla cascata esistente. Refs #13 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,21 +162,37 @@ export const FINESTRA_GIORNI: number | null = null;
|
||||
* successiva che la eredita. `verifica = 'scartata'`, o mai verificata (NULL), resta
|
||||
* esclusa come prima.
|
||||
*/
|
||||
export function misureValideAlla(db: Database.Database, sessioneId: number): Map<string, number> {
|
||||
/** Una riga grezza di `misure`, con la sessione di provenienza: usata sia da
|
||||
* `misureValideAlla` che da `recenzaValideAlla` (stessa query, stesso ORDER BY). */
|
||||
type RigaMisuraGrezza = { test_id: string; valore_num: number; data: string; sessione_id: number };
|
||||
|
||||
/** Fattorizza la query condivisa fra `misureValideAlla` e `recenzaValideAlla`: stessa
|
||||
* sessione, stesse righe, stesso ordine. Aggiunto `s2.id AS sessione_id` (assente
|
||||
* nella query originale) perche' `recenzaValideAlla` deve sapere DA QUALE sessione viene
|
||||
* ogni riga, non solo la data — due sessioni nello stesso giorno sono comunque due
|
||||
* posizioni di recenza diverse (lo stesso criterio di `ORDER BY s2.id DESC`). */
|
||||
function righeMisureGrezze(
|
||||
db: Database.Database, sessioneId: number
|
||||
): { sessione: { client_code: string; data: string; id: number }; righe: RigaMisuraGrezza[] } {
|
||||
const sessione = db.prepare(
|
||||
`SELECT client_code, data, id FROM sessioni WHERE id = ?`
|
||||
).get(sessioneId) as { client_code: string; data: string; id: number } | undefined;
|
||||
if (!sessione) throw new Error(`sessione ${sessioneId} inesistente`);
|
||||
|
||||
const righe = db.prepare(
|
||||
`SELECT m.test_id AS test_id, m.valore_num AS valore_num, s2.data AS data
|
||||
`SELECT m.test_id AS test_id, m.valore_num AS valore_num, s2.data AS data, s2.id AS sessione_id
|
||||
FROM misure m
|
||||
JOIN sessioni s2 ON s2.id = m.sessione_id
|
||||
WHERE m.client_code = ? AND (m.fuori_range = 0 OR m.verifica = 'confermata') AND m.valore_num IS NOT NULL
|
||||
AND (s2.data < ? OR (s2.data = ? AND s2.id <= ?))
|
||||
ORDER BY s2.data DESC, s2.id DESC, m.id DESC`
|
||||
).all(sessione.client_code, sessione.data, sessione.data, sessione.id) as
|
||||
{ test_id: string; valore_num: number; data: string }[];
|
||||
).all(sessione.client_code, sessione.data, sessione.data, sessione.id) as RigaMisuraGrezza[];
|
||||
|
||||
return { sessione, righe };
|
||||
}
|
||||
|
||||
export function misureValideAlla(db: Database.Database, sessioneId: number): Map<string, number> {
|
||||
const { sessione, righe } = righeMisureGrezze(db, sessioneId);
|
||||
|
||||
const mappa = new Map<string, number>();
|
||||
for (const r of righe) {
|
||||
@@ -193,6 +209,39 @@ export function misureValideAlla(db: Database.Database, sessioneId: number): Map
|
||||
return mappa;
|
||||
}
|
||||
|
||||
/**
|
||||
* La posizione di recenza di ogni test_id (0 = il piu' recente), sullo STESSO ordine di
|
||||
* `misureValideAlla` (`righeMisureGrezze`: data di sessione DESC, poi id di sessione DESC).
|
||||
* Non e' una data: e' un rango relativo fra le SESSIONI (data, sessione_id) di questo
|
||||
* cliente fino a questa sessione compresa — due test_id misurati nella stessa sessione
|
||||
* hanno la STESSA posizione (sono ugualmente "recenti": la regola dei gruppi di
|
||||
* `sottoDominiFisici`, issue #13, rompe quel pareggio con l'ordine di priorita' del
|
||||
* gruppo, non con l'id di riga in `misure`). Serve a scegliere, fra piu' alternative
|
||||
* complete per una stessa sotto-metrica (es. panca vs push-up), quella la cui misura piu'
|
||||
* recente vince.
|
||||
*/
|
||||
export function recenzaValideAlla(db: Database.Database, sessioneId: number): Map<string, number> {
|
||||
const { sessione, righe } = righeMisureGrezze(db, sessioneId);
|
||||
|
||||
const mappa = new Map<string, number>();
|
||||
let posizione = -1;
|
||||
let chiaveSessionePrecedente: string | null = null;
|
||||
for (const r of righe) {
|
||||
if (FINESTRA_GIORNI !== null) {
|
||||
const giorni = (Date.parse(sessione.data) - Date.parse(r.data)) / 86_400_000;
|
||||
if (giorni > FINESTRA_GIORNI) continue;
|
||||
}
|
||||
if (mappa.has(r.test_id)) continue; // gia' assegnata la sua posizione (la piu' recente)
|
||||
const chiaveSessione = `${r.data}|${r.sessione_id}`;
|
||||
if (chiaveSessione !== chiaveSessionePrecedente) {
|
||||
posizione++;
|
||||
chiaveSessionePrecedente = chiaveSessione;
|
||||
}
|
||||
mappa.set(r.test_id, posizione);
|
||||
}
|
||||
return mappa;
|
||||
}
|
||||
|
||||
/**
|
||||
* I sotto-domini di una sessione, dalle due fonti che oggi esistono: il questionario
|
||||
* (livello 1, `applicaCurva` sulle misure di `misureValideAlla`) e i test fisici
|
||||
@@ -209,6 +258,9 @@ export function sottoDominiDellaSessione(db: Database.Database, sessioneId: numb
|
||||
if (!sessione) throw new Error(`sessione ${sessioneId} inesistente`);
|
||||
|
||||
const misurate = misureValideAlla(db, sessioneId);
|
||||
// Recenza per test_id (issue #13): serve alla regola dei gruppi di sottoDominiFisici,
|
||||
// per scegliere fra alternative COMPLETE quella con la misura piu' recente.
|
||||
const recenza = recenzaValideAlla(db, sessioneId);
|
||||
|
||||
// Livello 1: normalizza ogni test attivo e misurato, poi raggruppa per
|
||||
// sotto-dominio — più test nello stesso sotto-dominio contano come media.
|
||||
@@ -234,7 +286,7 @@ export function sottoDominiDellaSessione(db: Database.Database, sessioneId: numb
|
||||
// soggetto dovrebbe sempre esistere (sessioni.client_code referenzia soggetti, FK attive
|
||||
// in db.ts): il controllo resta comunque, perche' l'assenza di dato fisico e' un {}
|
||||
// sicuro, mai un sesso indovinato.
|
||||
const daFisici = soggetto ? sottoDominiFisici(misurate, sessione.eta_alla_data, soggetto.sesso) : {};
|
||||
const daFisici = soggetto ? sottoDominiFisici(misurate, sessione.eta_alla_data, soggetto.sesso, recenza) : {};
|
||||
|
||||
return { ...daCurva, ...daFisici };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user