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:
@@ -17,60 +17,156 @@
|
|||||||
* un valore di ripiego. È lo stesso principio di `applicaCurva`/`paramsValidi`, qui
|
* un valore di ripiego. È lo stesso principio di `applicaCurva`/`paramsValidi`, qui
|
||||||
* applicato a funzioni con più argomenti invece che a una curva coi parametri incompleti.
|
* applicato a funzioni con più argomenti invece che a una curva coi parametri incompleti.
|
||||||
*
|
*
|
||||||
|
* REGOLA DEI GRUPPI (issue #13, manuale di Nicola del 27/09/2026): alcune sotto-metriche
|
||||||
|
* hanno più test alternativi (es. handgrip = media dx/sx oggi, o il vecchio f_handgrip_kg
|
||||||
|
* storico). Per ognuna si considerano solo le alternative COMPLETE (tutti gli ingressi
|
||||||
|
* presenti, età/peso/altezza comprese quando la funzione le richiede — un'alternativa
|
||||||
|
* "solo registrata", senza tabella nel manuale, non entra mai nel gruppo), e fra quelle si
|
||||||
|
* sceglie la cui misura più recente vince (`recenza`, da `recenzaValideAlla` in
|
||||||
|
* `motore/index.ts`: 0 = più recente). A parità — o quando `recenza` non è passata, cioè il
|
||||||
|
* comportamento di oggi — vince l'ordine in cui le alternative sono elencate nel gruppo.
|
||||||
|
* Vedi `sceglieAlternativa` più sotto.
|
||||||
|
*
|
||||||
* FUORI DAL PERIMETRO (dichiarato, non dimenticato): spirometria (serve un calcolatore
|
* FUORI DAL PERIMETRO (dichiarato, non dimenticato): spirometria (serve un calcolatore
|
||||||
* GLI che non esiste ancora), milfit, il 2km walk, il sit-to-stand, la trazione isometrica.
|
* GLI che non esiste ancora), la trazione isometrica. L'oracolo li usa nella demo con
|
||||||
* L'oracolo li usa nella demo con segnaposto o non li usa affatto nei pesi correnti;
|
* segnaposto o non li usa affatto nei pesi correnti; restano fuori finché non arriva un
|
||||||
* restano fuori finché non arriva un test_id e una decisione su come raccoglierli.
|
* test_id e una decisione su come raccoglierli. (Milfit, 2km walk e sit-to-stand erano qui
|
||||||
|
* fino al #13: dal manuale hanno una tabella di riferimento e sono entrati nella regola dei
|
||||||
|
* gruppi qui sotto.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Sesso } from './test-fisici';
|
import type { Sesso } from './test-fisici';
|
||||||
import { clamp } from './curve';
|
import { clamp } from './curve';
|
||||||
import {
|
import {
|
||||||
scoreHandgrip, scorePushup, scoreSollevamentoSuPeso, TIERS_ROW_M, TIERS_ROW_F,
|
scoreHandgrip, scorePushup, scoreSollevamentoSuPeso, TIERS_ROW_M, TIERS_ROW_F,
|
||||||
TIERS_SQUAT_M, TIERS_SQUAT_F, scorePlank, scoreGrassoPercento, scoreMuscoloPercento,
|
TIERS_SQUAT_M, TIERS_SQUAT_F, TIERS_BENCH_M, TIERS_BENCH_F, scoreSitToStand, scorePlank,
|
||||||
scoreWhr, scoreVo2max, vo2maxDaMutt, vo2maxDaStepTest, scoreSpo2, scorePressione,
|
scoreGrassoPercento, scoreMuscoloPercento, scoreWhr, scoreVo2max, vo2maxDaMutt,
|
||||||
|
vo2maxDaMilfit, vo2maxDa2kmWalk, vo2maxDaStepTest, scoreSpo2, scorePressione,
|
||||||
scoreRecuperoCardiaco, scoreFlamingo, scoreSitAndReach, scoreBackScratch, scoreMobilitaSpalla,
|
scoreRecuperoCardiaco, scoreFlamingo, scoreSitAndReach, scoreBackScratch, scoreMobilitaSpalla,
|
||||||
} from './test-fisici';
|
} from './test-fisici';
|
||||||
|
|
||||||
export type { Sesso };
|
export type { Sesso };
|
||||||
|
|
||||||
|
/** Un'alternativa possibile per una sotto-metrica di gruppo: `valore` è `undefined` se
|
||||||
|
* mancava anche un solo ingresso (alternativa incompleta, non entra nel gruppo);
|
||||||
|
* `idRecenza` sono i test_id "propri" di questa alternativa — non le variabili di contesto
|
||||||
|
* condivise come peso/età/altezza — usati per confrontarne la freschezza. */
|
||||||
|
type Alternativa = { valore: number | undefined; idRecenza: string[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sceglie, fra le alternative COMPLETE di un gruppo, quella con la misura più recente:
|
||||||
|
* la posizione di un'alternativa è il MASSIMO (il più vecchio) fra le posizioni di
|
||||||
|
* `recenza` dei suoi `idRecenza` — un'alternativa è fresca quanto il suo ingresso più
|
||||||
|
* vecchio, non quanto il più nuovo. Senza `recenza` (firma opzionale di
|
||||||
|
* `sottoDominiFisici`, o nessuno dei suoi id in mappa) vale solo l'ordine di elencazione
|
||||||
|
* (indice più basso vince): è il comportamento di prima del #13, invariato per i
|
||||||
|
* chiamanti che non passano `recenza`.
|
||||||
|
*/
|
||||||
|
function sceglieAlternativa(alternative: Alternativa[], recenza?: Map<string, number>): number | undefined {
|
||||||
|
let scelto: { valore: number; posizione: number; indice: number } | undefined;
|
||||||
|
alternative.forEach((alt, indice) => {
|
||||||
|
if (alt.valore === undefined) return; // alternativa incompleta: esclusa dal gruppo
|
||||||
|
let posizione = indice;
|
||||||
|
if (recenza) {
|
||||||
|
let massimo = -1;
|
||||||
|
for (const id of alt.idRecenza) {
|
||||||
|
const p = recenza.get(id);
|
||||||
|
if (p !== undefined && p > massimo) massimo = p;
|
||||||
|
}
|
||||||
|
if (massimo !== -1) posizione = massimo;
|
||||||
|
}
|
||||||
|
if (!scelto || posizione < scelto.posizione || (posizione === scelto.posizione && indice < scelto.indice)) {
|
||||||
|
scelto = { valore: alt.valore, posizione, indice };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return scelto?.valore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Media di due misure, solo se ENTRAMBE sono presenti — un solo lato non produce mai un
|
||||||
|
* punteggio (regola dei gruppi, issue #13). */
|
||||||
|
function mediaCoppia(a: number | undefined, b: number | undefined): number | undefined {
|
||||||
|
return a !== undefined && b !== undefined ? (a + b) / 2 : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `m` è la mappa test_id -> valore grezzo più recente e valido (vedi `misureValideAlla` in
|
* `m` è la mappa test_id -> valore grezzo più recente e valido (vedi `misureValideAlla` in
|
||||||
* `motore/index.ts`), `eta` l'età alla data della sessione (nullable come in tutto il
|
* `motore/index.ts`), `eta` l'età alla data della sessione (nullable come in tutto il
|
||||||
* motore: senza età gli score che la richiedono restano assenti, mai calcolati a zero).
|
* motore: senza età gli score che la richiedono restano assenti, mai calcolati a zero).
|
||||||
|
* `recenza` (opzionale, `recenzaValideAlla`) alimenta la regola dei gruppi: senza, vale solo
|
||||||
|
* l'ordine di elencazione di ogni gruppo, cioè il comportamento di prima del #13.
|
||||||
*
|
*
|
||||||
* Ordine dei blocchi: uguale a `demo()` nell'oracolo (forza, composizione, cardio,
|
* Ordine dei blocchi: uguale a `demo()` nell'oracolo (forza, composizione, cardio,
|
||||||
* recupero, stabilità), così un confronto riga per riga fra questo file e quello resta
|
* recupero, stabilità), così un confronto riga per riga fra questo file e quello resta
|
||||||
* diretto.
|
* diretto.
|
||||||
*/
|
*/
|
||||||
export function sottoDominiFisici(
|
export function sottoDominiFisici(
|
||||||
m: Map<string, number>, eta: number | null, sesso: Sesso
|
m: Map<string, number>, eta: number | null, sesso: Sesso, recenza?: Map<string, number>
|
||||||
): Record<string, number> {
|
): Record<string, number> {
|
||||||
const out: Record<string, number> = {};
|
const out: Record<string, number> = {};
|
||||||
|
|
||||||
// --- Forza & Struttura ----------------------------------------------------
|
|
||||||
const handgrip = m.get('f_handgrip_kg');
|
|
||||||
if (handgrip !== undefined && eta !== null) out.handgrip = scoreHandgrip(handgrip, eta, sesso);
|
|
||||||
|
|
||||||
const pushup = m.get('f_pushup_rip');
|
|
||||||
if (pushup !== undefined && eta !== null) out.spinta = scorePushup(pushup, eta, sesso);
|
|
||||||
|
|
||||||
const pesoKg = m.get('f_peso_kg');
|
const pesoKg = m.get('f_peso_kg');
|
||||||
|
const altezzaCm = m.get('f_altezza_cm');
|
||||||
|
// Nessun BMI di ripiego (DECISIONI d nell'issue): senza peso E altezza, resta `undefined`
|
||||||
|
// e tutto ciò che lo richiede (sit-to-stand, 2km walk) non dà punteggio.
|
||||||
|
const bmi = (pesoKg !== undefined && altezzaCm !== undefined) ? pesoKg / (altezzaCm / 100) ** 2 : undefined;
|
||||||
|
|
||||||
|
// --- Forza & Struttura ----------------------------------------------------
|
||||||
|
// handgrip: media dx/sx (manuale) se entrambi presenti, altrimenti lo storico f_handgrip_kg.
|
||||||
|
const handgripMedia = mediaCoppia(m.get('f_handgrip_dx_kg'), m.get('f_handgrip_sx_kg'));
|
||||||
|
const handgripStorico = m.get('f_handgrip_kg');
|
||||||
|
if (eta !== null) {
|
||||||
|
const handgrip = sceglieAlternativa([
|
||||||
|
{ valore: handgripMedia !== undefined ? scoreHandgrip(handgripMedia, eta, sesso) : undefined,
|
||||||
|
idRecenza: ['f_handgrip_dx_kg', 'f_handgrip_sx_kg'] },
|
||||||
|
{ valore: handgripStorico !== undefined ? scoreHandgrip(handgripStorico, eta, sesso) : undefined,
|
||||||
|
idRecenza: ['f_handgrip_kg'] },
|
||||||
|
], recenza);
|
||||||
|
if (handgrip !== undefined) out.handgrip = handgrip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// spinta: panca con bilanciere (carico/rip/peso, tabella TIERS_BENCH) oppure push-up.
|
||||||
|
const pancaBilCarico = m.get('f_panca_bil_carico_kg');
|
||||||
|
const pancaBilRip = m.get('f_panca_bil_rip');
|
||||||
|
const pancaValore = (pancaBilCarico !== undefined && pesoKg !== undefined && pancaBilRip !== undefined)
|
||||||
|
? scoreSollevamentoSuPeso(pancaBilCarico, pesoKg, pancaBilRip, TIERS_BENCH_M, TIERS_BENCH_F, sesso)
|
||||||
|
: undefined;
|
||||||
|
const pushup = m.get('f_pushup_rip');
|
||||||
|
const pushupValore = (pushup !== undefined && eta !== null) ? scorePushup(pushup, eta, sesso) : undefined;
|
||||||
|
const spinta = sceglieAlternativa([
|
||||||
|
{ valore: pancaValore, idRecenza: ['f_panca_bil_carico_kg', 'f_panca_bil_rip'] },
|
||||||
|
{ valore: pushupValore, idRecenza: ['f_pushup_rip'] },
|
||||||
|
], recenza);
|
||||||
|
if (spinta !== undefined) out.spinta = spinta;
|
||||||
|
|
||||||
|
// trazione: invariata (solo f_row). f_trazioni_rip si registra ma non dà punteggio
|
||||||
|
// (nessuna tabella di riferimento nel manuale, DECISIONI a).
|
||||||
const rowCarico = m.get('f_row_carico_kg');
|
const rowCarico = m.get('f_row_carico_kg');
|
||||||
const rowRip = m.get('f_row_rip');
|
const rowRip = m.get('f_row_rip');
|
||||||
if (rowCarico !== undefined && pesoKg !== undefined && rowRip !== undefined) {
|
if (rowCarico !== undefined && pesoKg !== undefined && rowRip !== undefined) {
|
||||||
out.trazione = scoreSollevamentoSuPeso(rowCarico, pesoKg, rowRip, TIERS_ROW_M, TIERS_ROW_F, sesso);
|
out.trazione = scoreSollevamentoSuPeso(rowCarico, pesoKg, rowRip, TIERS_ROW_M, TIERS_ROW_F, sesso);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arti_inferiori: squat (invariato) oppure sit-to-stand (rip, età, sesso, bmi — SENZA
|
||||||
|
// ripiego: bmi assente = alternativa incompleta).
|
||||||
const squatCarico = m.get('f_squat_carico_kg');
|
const squatCarico = m.get('f_squat_carico_kg');
|
||||||
const squatRip = m.get('f_squat_rip');
|
const squatRip = m.get('f_squat_rip');
|
||||||
if (squatCarico !== undefined && pesoKg !== undefined && squatRip !== undefined) {
|
const squatValore = (squatCarico !== undefined && pesoKg !== undefined && squatRip !== undefined)
|
||||||
out.arti_inferiori = scoreSollevamentoSuPeso(squatCarico, pesoKg, squatRip, TIERS_SQUAT_M, TIERS_SQUAT_F, sesso);
|
? scoreSollevamentoSuPeso(squatCarico, pesoKg, squatRip, TIERS_SQUAT_M, TIERS_SQUAT_F, sesso)
|
||||||
}
|
: undefined;
|
||||||
|
const sitToStandRip = m.get('f_sit_to_stand_rip');
|
||||||
|
const sitToStandValore = (sitToStandRip !== undefined && eta !== null && bmi !== undefined)
|
||||||
|
? scoreSitToStand(sitToStandRip, eta, sesso, bmi)
|
||||||
|
: undefined;
|
||||||
|
const artiInferiori = sceglieAlternativa([
|
||||||
|
{ valore: squatValore, idRecenza: ['f_squat_carico_kg', 'f_squat_rip'] },
|
||||||
|
{ valore: sitToStandValore, idRecenza: ['f_sit_to_stand_rip'] },
|
||||||
|
], recenza);
|
||||||
|
if (artiInferiori !== undefined) out.arti_inferiori = artiInferiori;
|
||||||
|
|
||||||
// Il plank pesa su DUE assi (core in Forza & Struttura, plank in Stabilità & Mobilità):
|
// Il plank pesa su DUE assi (core in Forza & Struttura, plank in Stabilità & Mobilità):
|
||||||
// confermato da Nicola il 22/08, vedi il commento su PESI_ASSE in registro.ts. Stessa
|
// confermato da Nicola il 22/08, vedi il commento su PESI_ASSE in registro.ts. Stessa
|
||||||
// misura, stessa funzione, due chiavi nel risultato — non è una duplicazione per errore.
|
// misura, stessa funzione, due chiavi nel risultato — non è una duplicazione per errore.
|
||||||
|
// f_plank_ginocchia_sec (ginocchia) si registra ma non dà punteggio (DECISIONI a).
|
||||||
const plank = m.get('f_plank_sec');
|
const plank = m.get('f_plank_sec');
|
||||||
if (plank !== undefined) {
|
if (plank !== undefined) {
|
||||||
const punteggio = scorePlank(plank, sesso);
|
const punteggio = scorePlank(plank, sesso);
|
||||||
@@ -89,16 +185,29 @@ export function sottoDominiFisici(
|
|||||||
if (whr !== undefined) out.whr = scoreWhr(whr, sesso);
|
if (whr !== undefined) out.whr = scoreWhr(whr, sesso);
|
||||||
|
|
||||||
// --- Cardio-Respiratorio -----------------------------------------------------
|
// --- Cardio-Respiratorio -----------------------------------------------------
|
||||||
// VO2max: diretto se misurato, altrimenti stimato da MUTT, altrimenti dallo step test —
|
// vo2max: diretto, MUTT, MILFIT, 2km walk (UKK) o step test — in quest'ordine di
|
||||||
// il primo presente vince, come nella cascata di fonti dell'oracolo (che nella demo usa
|
// priorità, la regola dei gruppi sceglie fra le alternative complete quella più recente.
|
||||||
// solo MUTT, ma le tre vie coesistono nel resto del file Python).
|
|
||||||
const vo2Diretto = m.get('f_vo2max');
|
const vo2Diretto = m.get('f_vo2max');
|
||||||
const muttVam = m.get('f_mutt_vam');
|
const muttVam = m.get('f_mutt_vam');
|
||||||
|
const milfitWatt = m.get('f_milfit_watt');
|
||||||
|
const walk2kmMin = m.get('f_walk2km_min');
|
||||||
|
const walk2kmFc = m.get('f_walk2km_fc_bpm');
|
||||||
const stepHr = m.get('f_step_hr_recovery');
|
const stepHr = m.get('f_step_hr_recovery');
|
||||||
let vo2max: number | undefined;
|
|
||||||
if (vo2Diretto !== undefined) vo2max = vo2Diretto;
|
const vo2MuttValore = muttVam !== undefined ? vo2maxDaMutt(muttVam) : undefined;
|
||||||
else if (muttVam !== undefined) vo2max = vo2maxDaMutt(muttVam);
|
const vo2MilfitValore = (milfitWatt !== undefined && pesoKg !== undefined)
|
||||||
else if (stepHr !== undefined) vo2max = vo2maxDaStepTest(stepHr, sesso);
|
? vo2maxDaMilfit(milfitWatt, pesoKg) : undefined;
|
||||||
|
const vo2WalkValore = (walk2kmMin !== undefined && walk2kmFc !== undefined && eta !== null && bmi !== undefined)
|
||||||
|
? vo2maxDa2kmWalk(walk2kmMin, walk2kmFc, eta, bmi) : undefined;
|
||||||
|
const vo2StepValore = stepHr !== undefined ? vo2maxDaStepTest(stepHr, sesso) : undefined;
|
||||||
|
|
||||||
|
const vo2max = sceglieAlternativa([
|
||||||
|
{ valore: vo2Diretto, idRecenza: ['f_vo2max'] },
|
||||||
|
{ valore: vo2MuttValore, idRecenza: ['f_mutt_vam'] },
|
||||||
|
{ valore: vo2MilfitValore, idRecenza: ['f_milfit_watt'] },
|
||||||
|
{ valore: vo2WalkValore, idRecenza: ['f_walk2km_min', 'f_walk2km_fc_bpm'] },
|
||||||
|
{ valore: vo2StepValore, idRecenza: ['f_step_hr_recovery'] },
|
||||||
|
], recenza);
|
||||||
if (vo2max !== undefined && eta !== null) out.vo2max = scoreVo2max(vo2max, eta, sesso);
|
if (vo2max !== undefined && eta !== null) out.vo2max = scoreVo2max(vo2max, eta, sesso);
|
||||||
|
|
||||||
const spo2 = m.get('f_spo2_pct');
|
const spo2 = m.get('f_spo2_pct');
|
||||||
@@ -116,18 +225,47 @@ export function sottoDominiFisici(
|
|||||||
const diastolica = m.get('f_pa_diastolica');
|
const diastolica = m.get('f_pa_diastolica');
|
||||||
if (sistolica !== undefined && diastolica !== undefined) out.pressione = scorePressione(sistolica, diastolica);
|
if (sistolica !== undefined && diastolica !== undefined) out.pressione = scorePressione(sistolica, diastolica);
|
||||||
|
|
||||||
const hrr = m.get('f_hrr_1min_bpm');
|
// hrr: MUTT (FC max - FC dopo 1', manuale) oppure lo storico f_hrr_1min_bpm (già un calo).
|
||||||
if (hrr !== undefined) out.hrr = scoreRecuperoCardiaco(hrr);
|
const muttFcMax = m.get('f_mutt_fc_max_bpm');
|
||||||
|
const muttFc1min = m.get('f_mutt_fc_1min_bpm');
|
||||||
|
const hrrMuttValore = (muttFcMax !== undefined && muttFc1min !== undefined)
|
||||||
|
? scoreRecuperoCardiaco(muttFcMax - muttFc1min) : undefined;
|
||||||
|
const hrrStorico = m.get('f_hrr_1min_bpm');
|
||||||
|
const hrrStoricoValore = hrrStorico !== undefined ? scoreRecuperoCardiaco(hrrStorico) : undefined;
|
||||||
|
const hrr = sceglieAlternativa([
|
||||||
|
{ valore: hrrMuttValore, idRecenza: ['f_mutt_fc_max_bpm', 'f_mutt_fc_1min_bpm'] },
|
||||||
|
{ valore: hrrStoricoValore, idRecenza: ['f_hrr_1min_bpm'] },
|
||||||
|
], recenza);
|
||||||
|
if (hrr !== undefined) out.hrr = hrr;
|
||||||
|
|
||||||
// --- Stabilità & Mobilità Funzionale ------------------------------------------
|
// --- Stabilità & Mobilità Funzionale ------------------------------------------
|
||||||
const flamingo = m.get('f_flamingo_cadute');
|
// flamingo: media dx/sx a occhi APERTI (la tabella Eurofit è a occhi aperti, DECISIONI b;
|
||||||
if (flamingo !== undefined) out.flamingo = scoreFlamingo(flamingo);
|
// occhi chiusi si registra ma non dà punteggio) oppure lo storico f_flamingo_cadute.
|
||||||
|
const flamingoOaMedia = mediaCoppia(m.get('f_flamingo_oa_dx'), m.get('f_flamingo_oa_sx'));
|
||||||
|
const flamingoOaValore = flamingoOaMedia !== undefined ? scoreFlamingo(flamingoOaMedia) : undefined;
|
||||||
|
const flamingoCaduteStorico = m.get('f_flamingo_cadute');
|
||||||
|
const flamingoStoricoValore = flamingoCaduteStorico !== undefined ? scoreFlamingo(flamingoCaduteStorico) : undefined;
|
||||||
|
const flamingo = sceglieAlternativa([
|
||||||
|
{ valore: flamingoOaValore, idRecenza: ['f_flamingo_oa_dx', 'f_flamingo_oa_sx'] },
|
||||||
|
{ valore: flamingoStoricoValore, idRecenza: ['f_flamingo_cadute'] },
|
||||||
|
], recenza);
|
||||||
|
if (flamingo !== undefined) out.flamingo = flamingo;
|
||||||
|
|
||||||
const sitReach = m.get('f_sit_reach_cm');
|
const sitReach = m.get('f_sit_reach_cm');
|
||||||
if (sitReach !== undefined) out.sit_and_reach = scoreSitAndReach(sitReach, sesso);
|
if (sitReach !== undefined) out.sit_and_reach = scoreSitAndReach(sitReach, sesso);
|
||||||
|
|
||||||
const backScratch = m.get('f_back_scratch_cm');
|
// back_scratch: media dx/sx (manuale) oppure lo storico f_back_scratch_cm.
|
||||||
if (backScratch !== undefined && eta !== null) out.back_scratch = scoreBackScratch(backScratch, eta, sesso);
|
const backMedia = mediaCoppia(m.get('f_back_scratch_dx_cm'), m.get('f_back_scratch_sx_cm'));
|
||||||
|
const backStorico = m.get('f_back_scratch_cm');
|
||||||
|
if (eta !== null) {
|
||||||
|
const backScratch = sceglieAlternativa([
|
||||||
|
{ valore: backMedia !== undefined ? scoreBackScratch(backMedia, eta, sesso) : undefined,
|
||||||
|
idRecenza: ['f_back_scratch_dx_cm', 'f_back_scratch_sx_cm'] },
|
||||||
|
{ valore: backStorico !== undefined ? scoreBackScratch(backStorico, eta, sesso) : undefined,
|
||||||
|
idRecenza: ['f_back_scratch_cm'] },
|
||||||
|
], recenza);
|
||||||
|
if (backScratch !== undefined) out.back_scratch = backScratch;
|
||||||
|
}
|
||||||
|
|
||||||
const outreach = m.get('f_spalla_outreach_deg');
|
const outreach = m.get('f_spalla_outreach_deg');
|
||||||
const buckling = m.get('f_spalla_buckling_deg');
|
const buckling = m.get('f_spalla_buckling_deg');
|
||||||
|
|||||||
@@ -162,21 +162,37 @@ export const FINESTRA_GIORNI: number | null = null;
|
|||||||
* successiva che la eredita. `verifica = 'scartata'`, o mai verificata (NULL), resta
|
* successiva che la eredita. `verifica = 'scartata'`, o mai verificata (NULL), resta
|
||||||
* esclusa come prima.
|
* 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(
|
const sessione = db.prepare(
|
||||||
`SELECT client_code, data, id FROM sessioni WHERE id = ?`
|
`SELECT client_code, data, id FROM sessioni WHERE id = ?`
|
||||||
).get(sessioneId) as { client_code: string; data: string; id: number } | undefined;
|
).get(sessioneId) as { client_code: string; data: string; id: number } | undefined;
|
||||||
if (!sessione) throw new Error(`sessione ${sessioneId} inesistente`);
|
if (!sessione) throw new Error(`sessione ${sessioneId} inesistente`);
|
||||||
|
|
||||||
const righe = db.prepare(
|
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
|
FROM misure m
|
||||||
JOIN sessioni s2 ON s2.id = m.sessione_id
|
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
|
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 <= ?))
|
AND (s2.data < ? OR (s2.data = ? AND s2.id <= ?))
|
||||||
ORDER BY s2.data DESC, s2.id DESC, m.id DESC`
|
ORDER BY s2.data DESC, s2.id DESC, m.id DESC`
|
||||||
).all(sessione.client_code, sessione.data, sessione.data, sessione.id) as
|
).all(sessione.client_code, sessione.data, sessione.data, sessione.id) as RigaMisuraGrezza[];
|
||||||
{ test_id: string; valore_num: number; data: string }[];
|
|
||||||
|
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>();
|
const mappa = new Map<string, number>();
|
||||||
for (const r of righe) {
|
for (const r of righe) {
|
||||||
@@ -193,6 +209,39 @@ export function misureValideAlla(db: Database.Database, sessioneId: number): Map
|
|||||||
return mappa;
|
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
|
* 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
|
* (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`);
|
if (!sessione) throw new Error(`sessione ${sessioneId} inesistente`);
|
||||||
|
|
||||||
const misurate = misureValideAlla(db, sessioneId);
|
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
|
// Livello 1: normalizza ogni test attivo e misurato, poi raggruppa per
|
||||||
// sotto-dominio — più test nello stesso sotto-dominio contano come media.
|
// 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
|
// 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 {}
|
// in db.ts): il controllo resta comunque, perche' l'assenza di dato fisico e' un {}
|
||||||
// sicuro, mai un sesso indovinato.
|
// 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 };
|
return { ...daCurva, ...daFisici };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,8 +110,11 @@ const QUESTIONARIO: VoceRegistro[] = [
|
|||||||
const FISICI: VoceRegistro[] = [
|
const FISICI: VoceRegistro[] = [
|
||||||
{ test_id: 'f_peso_kg', etichetta: 'Peso corporeo', unita: 'kg', tipo_valore: 'num',
|
{ test_id: 'f_peso_kg', etichetta: 'Peso corporeo', unita: 'kg', tipo_valore: 'num',
|
||||||
asse: 'Composizione Corporea', range_min: 30, range_max: 250, attivo_da: DA },
|
asse: 'Composizione Corporea', range_min: 30, range_max: 250, attivo_da: DA },
|
||||||
|
// Dismesso il 28/09 (#13): sostituito da f_handgrip_dx_kg/f_handgrip_sx_kg (media dx/sx,
|
||||||
|
// manuale di Nicola). Mai cancellato: le sessioni passate lo usano ancora (limite #12);
|
||||||
|
// sottoDominiFisici lo legge come alternativa storica in fisici.ts.
|
||||||
{ test_id: 'f_handgrip_kg', etichetta: 'Forza di presa (handgrip, dinamometro)', unita: 'kg', tipo_valore: 'num',
|
{ test_id: 'f_handgrip_kg', etichetta: 'Forza di presa (handgrip, dinamometro)', unita: 'kg', tipo_valore: 'num',
|
||||||
asse: 'Forza & Struttura', range_min: 0, range_max: 100, attivo_da: DA },
|
asse: 'Forza & Struttura', range_min: 0, range_max: 100, attivo_da: DA, attivo_a: '2026-09-28' },
|
||||||
{ test_id: 'f_pushup_rip', etichetta: 'Push-up, ripetizioni massime', tipo_valore: 'num',
|
{ test_id: 'f_pushup_rip', etichetta: 'Push-up, ripetizioni massime', tipo_valore: 'num',
|
||||||
asse: 'Forza & Struttura', range_min: 0, range_max: 150, attivo_da: DA },
|
asse: 'Forza & Struttura', range_min: 0, range_max: 150, attivo_da: DA },
|
||||||
{ test_id: 'f_row_carico_kg', etichetta: 'Trazione/rematore, carico', unita: 'kg', tipo_valore: 'num',
|
{ test_id: 'f_row_carico_kg', etichetta: 'Trazione/rematore, carico', unita: 'kg', tipo_valore: 'num',
|
||||||
@@ -144,18 +147,86 @@ const FISICI: VoceRegistro[] = [
|
|||||||
asse: 'Recupero & Sistema Nervoso', range_min: 70, range_max: 250, attivo_da: DA },
|
asse: 'Recupero & Sistema Nervoso', range_min: 70, range_max: 250, attivo_da: DA },
|
||||||
{ test_id: 'f_pa_diastolica', etichetta: 'Pressione arteriosa diastolica', unita: 'mmHg', tipo_valore: 'num',
|
{ test_id: 'f_pa_diastolica', etichetta: 'Pressione arteriosa diastolica', unita: 'mmHg', tipo_valore: 'num',
|
||||||
asse: 'Recupero & Sistema Nervoso', range_min: 40, range_max: 150, attivo_da: DA },
|
asse: 'Recupero & Sistema Nervoso', range_min: 40, range_max: 150, attivo_da: DA },
|
||||||
|
// Dismesso il 28/09 (#13): sostituito da f_mutt_fc_max_bpm - f_mutt_fc_1min_bpm (manuale).
|
||||||
|
// Mai cancellato, resta come alternativa storica in fisici.ts (hrr).
|
||||||
{ test_id: 'f_hrr_1min_bpm', etichetta: 'Recupero cardiaco a 1 minuto post-sforzo', unita: 'bpm', tipo_valore: 'num',
|
{ test_id: 'f_hrr_1min_bpm', etichetta: 'Recupero cardiaco a 1 minuto post-sforzo', unita: 'bpm', tipo_valore: 'num',
|
||||||
asse: 'Recupero & Sistema Nervoso', range_min: 0, range_max: 100, attivo_da: DA },
|
asse: 'Recupero & Sistema Nervoso', range_min: 0, range_max: 100, attivo_da: DA, attivo_a: '2026-09-28' },
|
||||||
|
// Dismesso il 28/09 (#13): sostituito da media f_flamingo_oa_dx/f_flamingo_oa_sx (occhi
|
||||||
|
// aperti, manuale). Mai cancellato, resta come alternativa storica in fisici.ts (flamingo).
|
||||||
{ test_id: 'f_flamingo_cadute', etichetta: 'Test del fenicottero, numero di cadute', tipo_valore: 'num',
|
{ test_id: 'f_flamingo_cadute', etichetta: 'Test del fenicottero, numero di cadute', tipo_valore: 'num',
|
||||||
asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 50, attivo_da: DA },
|
asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 50, attivo_da: DA, attivo_a: '2026-09-28' },
|
||||||
{ test_id: 'f_sit_reach_cm', etichetta: 'Sit and reach', unita: 'cm', tipo_valore: 'num',
|
{ test_id: 'f_sit_reach_cm', etichetta: 'Sit and reach', unita: 'cm', tipo_valore: 'num',
|
||||||
asse: 'Stabilità & Mobilità Funzionale', range_min: -30, range_max: 50, attivo_da: DA },
|
asse: 'Stabilità & Mobilità Funzionale', range_min: -30, range_max: 50, attivo_da: DA },
|
||||||
|
// Dismesso il 28/09 (#13): sostituito da media f_back_scratch_dx_cm/f_back_scratch_sx_cm.
|
||||||
|
// Mai cancellato, resta come alternativa storica in fisici.ts (back_scratch).
|
||||||
{ test_id: 'f_back_scratch_cm', etichetta: 'Back scratch (mobilità spalle)', unita: 'cm', tipo_valore: 'num',
|
{ test_id: 'f_back_scratch_cm', etichetta: 'Back scratch (mobilità spalle)', unita: 'cm', tipo_valore: 'num',
|
||||||
asse: 'Stabilità & Mobilità Funzionale', range_min: -40, range_max: 20, attivo_da: DA },
|
asse: 'Stabilità & Mobilità Funzionale', range_min: -40, range_max: 20, attivo_da: DA, attivo_a: '2026-09-28' },
|
||||||
{ test_id: 'f_spalla_outreach_deg', etichetta: 'Wellness Tower spalla, outreach', unita: 'deg', tipo_valore: 'num',
|
{ test_id: 'f_spalla_outreach_deg', etichetta: 'Wellness Tower spalla, outreach', unita: 'deg', tipo_valore: 'num',
|
||||||
asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 180, attivo_da: DA },
|
asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 180, attivo_da: DA },
|
||||||
{ test_id: 'f_spalla_buckling_deg', etichetta: 'Wellness Tower spalla, buckling', unita: 'deg', tipo_valore: 'num',
|
{ test_id: 'f_spalla_buckling_deg', etichetta: 'Wellness Tower spalla, buckling', unita: 'deg', tipo_valore: 'num',
|
||||||
asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 180, attivo_da: DA },
|
asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 180, attivo_da: DA },
|
||||||
|
|
||||||
|
// Voci nuove del 28/09 (#13), dal manuale batteria test completo di Nicola (27/09/2026).
|
||||||
|
// Alcune alimentano sottoDominiFisici (vedi motore/fisici.ts), altre si registrano soltanto
|
||||||
|
// perche' il manuale non da' una tabella di riferimento per loro (DECISIONI a nell'issue):
|
||||||
|
// panca manubri, trazioni, plank sulle ginocchia, 6MWT, flamingo occhi chiusi, FC per step
|
||||||
|
// MUTT, FC a riposo, FC MUTT a 2'.
|
||||||
|
{ test_id: 'f_altezza_cm', etichetta: 'Altezza', unita: 'cm', tipo_valore: 'num',
|
||||||
|
asse: 'Composizione Corporea', range_min: 100, range_max: 230, attivo_da: DA },
|
||||||
|
{ test_id: 'f_fc_riposo_bpm', etichetta: 'Frequenza cardiaca a riposo', unita: 'bpm', tipo_valore: 'num',
|
||||||
|
asse: 'Cardio-Respiratorio', range_min: 30, range_max: 130, attivo_da: DA },
|
||||||
|
{ test_id: 'f_back_scratch_dx_cm', etichetta: 'Back scratch (mobilità spalle), lato destro', unita: 'cm',
|
||||||
|
tipo_valore: 'num', asse: 'Stabilità & Mobilità Funzionale', range_min: -40, range_max: 30, attivo_da: DA },
|
||||||
|
{ test_id: 'f_back_scratch_sx_cm', etichetta: 'Back scratch (mobilità spalle), lato sinistro', unita: 'cm',
|
||||||
|
tipo_valore: 'num', asse: 'Stabilità & Mobilità Funzionale', range_min: -40, range_max: 30, attivo_da: DA },
|
||||||
|
{ test_id: 'f_flamingo_oa_dx', etichetta: 'Test del fenicottero, cadute occhi aperti, lato destro',
|
||||||
|
tipo_valore: 'num', asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 60, attivo_da: DA },
|
||||||
|
{ test_id: 'f_flamingo_oa_sx', etichetta: 'Test del fenicottero, cadute occhi aperti, lato sinistro',
|
||||||
|
tipo_valore: 'num', asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 60, attivo_da: DA },
|
||||||
|
{ test_id: 'f_flamingo_oc_dx', etichetta: 'Test del fenicottero, cadute occhi chiusi, lato destro',
|
||||||
|
tipo_valore: 'num', asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 60, attivo_da: DA },
|
||||||
|
{ test_id: 'f_flamingo_oc_sx', etichetta: 'Test del fenicottero, cadute occhi chiusi, lato sinistro',
|
||||||
|
tipo_valore: 'num', asse: 'Stabilità & Mobilità Funzionale', range_min: 0, range_max: 60, attivo_da: DA },
|
||||||
|
{ test_id: 'f_handgrip_dx_kg', etichetta: 'Forza di presa (handgrip), lato destro', unita: 'kg',
|
||||||
|
tipo_valore: 'num', asse: 'Forza & Struttura', range_min: 5, range_max: 100, attivo_da: DA },
|
||||||
|
{ test_id: 'f_handgrip_sx_kg', etichetta: 'Forza di presa (handgrip), lato sinistro', unita: 'kg',
|
||||||
|
tipo_valore: 'num', asse: 'Forza & Struttura', range_min: 5, range_max: 100, attivo_da: DA },
|
||||||
|
{ test_id: 'f_panca_man_carico_kg', etichetta: 'Panca con manubri, carico (per manubrio)', unita: 'kg',
|
||||||
|
tipo_valore: 'num', asse: 'Forza & Struttura', range_min: 0, range_max: 100, attivo_da: DA },
|
||||||
|
{ test_id: 'f_panca_man_rip', etichetta: 'Panca con manubri, ripetizioni', tipo_valore: 'num',
|
||||||
|
asse: 'Forza & Struttura', range_min: 1, range_max: 30, attivo_da: DA },
|
||||||
|
{ test_id: 'f_panca_bil_carico_kg', etichetta: 'Panca con bilanciere, carico', unita: 'kg', tipo_valore: 'num',
|
||||||
|
asse: 'Forza & Struttura', range_min: 0, range_max: 300, attivo_da: DA },
|
||||||
|
{ test_id: 'f_panca_bil_rip', etichetta: 'Panca con bilanciere, ripetizioni', tipo_valore: 'num',
|
||||||
|
asse: 'Forza & Struttura', range_min: 1, range_max: 30, attivo_da: DA },
|
||||||
|
{ test_id: 'f_trazioni_rip', etichetta: 'Trazioni alla sbarra, ripetizioni massime', tipo_valore: 'num',
|
||||||
|
asse: 'Forza & Struttura', range_min: 0, range_max: 60, attivo_da: DA },
|
||||||
|
{ test_id: 'f_sit_to_stand_rip', etichetta: 'Sit-to-stand, ripetizioni in 30 secondi', tipo_valore: 'num',
|
||||||
|
asse: 'Forza & Struttura', range_min: 0, range_max: 80, attivo_da: DA },
|
||||||
|
{ test_id: 'f_plank_ginocchia_sec', etichetta: 'Plank sulle ginocchia, tenuta', unita: 'sec', tipo_valore: 'num',
|
||||||
|
asse: 'Forza & Struttura', range_min: 0, range_max: 600, attivo_da: DA },
|
||||||
|
{ test_id: 'f_6mwt_m', etichetta: '6 Minute Walk Test, distanza percorsa', unita: 'm', tipo_valore: 'num',
|
||||||
|
asse: 'Cardio-Respiratorio', range_min: 50, range_max: 1200, attivo_da: DA },
|
||||||
|
{ test_id: 'f_walk2km_min', etichetta: 'Camminata 2 km (UKK), tempo', unita: 'min', tipo_valore: 'num',
|
||||||
|
asse: 'Cardio-Respiratorio', range_min: 8, range_max: 40, attivo_da: DA },
|
||||||
|
{ test_id: 'f_walk2km_fc_bpm', etichetta: 'Camminata 2 km (UKK), frequenza cardiaca finale', unita: 'bpm',
|
||||||
|
tipo_valore: 'num', asse: 'Cardio-Respiratorio', range_min: 60, range_max: 220, attivo_da: DA },
|
||||||
|
{ test_id: 'f_milfit_watt', etichetta: 'MILFIT, potenza', unita: 'watt', tipo_valore: 'num',
|
||||||
|
asse: 'Cardio-Respiratorio', range_min: 20, range_max: 500, attivo_da: DA },
|
||||||
|
{ test_id: 'f_mutt_fc_max_bpm', etichetta: 'MUTT, frequenza cardiaca massima', unita: 'bpm', tipo_valore: 'num',
|
||||||
|
asse: 'Cardio-Respiratorio', range_min: 40, range_max: 230, attivo_da: DA },
|
||||||
|
{ test_id: 'f_mutt_fc_1min_bpm', etichetta: 'MUTT, frequenza cardiaca dopo 1\' di recupero', unita: 'bpm',
|
||||||
|
tipo_valore: 'num', asse: 'Cardio-Respiratorio', range_min: 40, range_max: 230, attivo_da: DA },
|
||||||
|
{ test_id: 'f_mutt_fc_2min_bpm', etichetta: 'MUTT, frequenza cardiaca dopo 2\' di recupero', unita: 'bpm',
|
||||||
|
tipo_valore: 'num', asse: 'Cardio-Respiratorio', range_min: 40, range_max: 230, attivo_da: DA },
|
||||||
|
// Gli otto step del MUTT, un km/h alla volta da 7,5 a 14,5: solo registrati (DECISIONI a),
|
||||||
|
// nessuna tabella di riferimento nel manuale per la FC punto per punto.
|
||||||
|
...([7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5, 14.5].map((kmh): VoceRegistro => ({
|
||||||
|
test_id: `f_mutt_fc_${String(kmh).replace('.', '_')}`,
|
||||||
|
etichetta: `MUTT, frequenza cardiaca allo step ${kmh} km/h`,
|
||||||
|
unita: 'bpm', tipo_valore: 'num', asse: 'Cardio-Respiratorio',
|
||||||
|
range_min: 40, range_max: 230, attivo_da: DA,
|
||||||
|
}))),
|
||||||
];
|
];
|
||||||
|
|
||||||
export function seedRegistro(db: Database.Database): void {
|
export function seedRegistro(db: Database.Database): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user