longevity: registro dei test e pesi versionati come dato
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
|
||||
export type Curva = 'bell' | 'lin_dec' | 'inc_plateau' | 'x10' | 'x10_inv' | 'decstep' | 'incstep';
|
||||
|
||||
export type VoceRegistro = {
|
||||
test_id: string;
|
||||
etichetta: string;
|
||||
unita?: string;
|
||||
tipo_valore: 'num' | 'txt';
|
||||
curva?: Curva;
|
||||
params?: unknown;
|
||||
asse?: string;
|
||||
sotto_dominio?: string;
|
||||
range_min?: number;
|
||||
range_max?: number;
|
||||
attivo_da: string;
|
||||
attivo_a?: string;
|
||||
};
|
||||
|
||||
const DA = '2026-01-01';
|
||||
|
||||
// I 20 campi punteggiati, presi dal prototipo del cliente (questionario_longevity_score.html).
|
||||
// Gli id portano il prefisso q_ per distinguerli dai test fisici; i params sono nella forma
|
||||
// dichiarata per ciascuna curva, non nell'array posizionale del prototipo.
|
||||
const QUESTIONARIO: VoceRegistro[] = [
|
||||
{ test_id: 'q_ore_sonno', etichetta: 'Ore di sonno per notte (media)', unita: 'h', tipo_valore: 'num',
|
||||
curva: 'bell', params: { low: 4, peakLow: 7, peakHigh: 9, high: 12 },
|
||||
asse: 'Recupero & Sistema Nervoso', sotto_dominio: 'questionario_sonno', range_min: 0, range_max: 14, attivo_da: DA },
|
||||
{ test_id: 'q_min_addorm', etichetta: 'Minuti per addormentarti', unita: 'min', tipo_valore: 'num',
|
||||
curva: 'decstep', params: { steps: [[15, 100], [30, 75], [60, 50], [999, 25]] },
|
||||
asse: 'Recupero & Sistema Nervoso', sotto_dominio: 'questionario_sonno', range_min: 0, range_max: 180, attivo_da: DA },
|
||||
{ test_id: 'q_risvegli', etichetta: 'Risvegli notturni (numero)', tipo_valore: 'num',
|
||||
curva: 'decstep', params: { steps: [[0, 100], [1, 80], [2, 50], [999, 25]] },
|
||||
asse: 'Recupero & Sistema Nervoso', sotto_dominio: 'questionario_sonno', range_min: 0, range_max: 10, attivo_da: DA },
|
||||
{ test_id: 'q_riposato', etichetta: 'Quanto ti senti riposato al risveglio', tipo_valore: 'num',
|
||||
curva: 'x10', params: {}, asse: 'Recupero & Sistema Nervoso', sotto_dominio: 'questionario_sonno',
|
||||
range_min: 0, range_max: 10, attivo_da: DA },
|
||||
{ test_id: 'q_caffeina', etichetta: 'Caffeina dopo le 16:00 (volte/settimana)', tipo_valore: 'num',
|
||||
curva: 'lin_dec', params: { best: 0, worst: 7 },
|
||||
asse: 'Recupero & Sistema Nervoso', sotto_dominio: 'questionario_sonno', range_min: 0, range_max: 14, attivo_da: DA },
|
||||
{ test_id: 'q_sonnolenza_diurna', etichetta: 'Sonnolenza/fatica a concentrarti durante il giorno (volte/settimana)',
|
||||
tipo_valore: 'num', curva: 'lin_dec', params: { best: 0, worst: 7 },
|
||||
asse: 'Recupero & Sistema Nervoso', sotto_dominio: 'questionario_sonno', range_min: 0, range_max: 14, attivo_da: DA },
|
||||
|
||||
{ test_id: 'q_energia_media', etichetta: 'Energia media nella giornata, ultima settimana', tipo_valore: 'num',
|
||||
curva: 'x10', params: {}, asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress',
|
||||
range_min: 0, range_max: 10, attivo_da: DA },
|
||||
{ test_id: 'q_esaurimento', etichetta: 'Episodi di esaurimento senza motivo fisico (volte/sett.)', tipo_valore: 'num',
|
||||
curva: 'lin_dec', params: { best: 0, worst: 7 },
|
||||
asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress', range_min: 0, range_max: 14, attivo_da: DA },
|
||||
{ test_id: 'q_calo_pomeridiano', etichetta: 'Quanto forte e il calo di energia dal mattino al pomeriggio',
|
||||
tipo_valore: 'num', curva: 'x10_inv', params: {},
|
||||
asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress', range_min: 0, range_max: 10, attivo_da: DA },
|
||||
|
||||
{ test_id: 'q_sopraffatto', etichetta: 'Sopraffatto da imprevisti (volte/mese)', tipo_valore: 'num',
|
||||
curva: 'decstep', params: { steps: [[2, 100], [5, 70], [10, 40], [999, 20]] },
|
||||
asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress', range_min: 0, range_max: 30, attivo_da: DA },
|
||||
{ test_id: 'q_controllo', etichetta: 'Percezione di controllo sulla tua vita', tipo_valore: 'num',
|
||||
curva: 'x10', params: {}, asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress',
|
||||
range_min: 0, range_max: 10, attivo_da: DA },
|
||||
{ test_id: 'q_sicurezza_gestione', etichetta: 'Ti senti sicuro/a nella tua capacita di gestire i problemi personali',
|
||||
tipo_valore: 'num', curva: 'x10', params: {},
|
||||
asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress', range_min: 0, range_max: 10, attivo_da: DA },
|
||||
{ test_id: 'q_tensione', etichetta: 'Tensione/irritabilita (volte/settimana)', tipo_valore: 'num',
|
||||
curva: 'lin_dec', params: { best: 0, worst: 7 },
|
||||
asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress', range_min: 0, range_max: 14, attivo_da: DA },
|
||||
{ test_id: 'q_pensieri_lavoro', etichetta: 'Ore/giorno di pensieri di lavoro fuori orario', unita: 'h',
|
||||
tipo_valore: 'num', curva: 'lin_dec', params: { best: 0, worst: 4 },
|
||||
asse: 'Energia & Regolazione Stress', sotto_dominio: 'questionario_energia_stress', range_min: 0, range_max: 8, attivo_da: DA },
|
||||
|
||||
{ test_id: 'q_attivita', etichetta: 'Giorni/settimana attivita fisica extra ISL', tipo_valore: 'num',
|
||||
curva: 'incstep', params: { steps: [[0, 20], [2, 50], [4, 80], [999, 100]] },
|
||||
asse: 'Stile di Vita & Sonno', sotto_dominio: 'questionario_lifestyle', range_min: 0, range_max: 7, attivo_da: DA },
|
||||
{ test_id: 'q_alimentazione', etichetta: 'Qualita percepita alimentazione abituale', tipo_valore: 'num',
|
||||
curva: 'x10', params: {}, asse: 'Stile di Vita & Sonno', sotto_dominio: 'questionario_lifestyle',
|
||||
range_min: 0, range_max: 10, attivo_da: DA },
|
||||
{ test_id: 'q_sigarette', etichetta: 'Sigarette al giorno', tipo_valore: 'num',
|
||||
curva: 'decstep', params: { steps: [[0, 100], [5, 60], [10, 40], [20, 20], [999, 0]] },
|
||||
asse: 'Stile di Vita & Sonno', sotto_dominio: 'questionario_lifestyle', range_min: 0, range_max: 40, attivo_da: DA },
|
||||
// Soglia 0-7 = punteggio pieno: scelta deliberata del cliente (basso rischio NIAAA/WHO),
|
||||
// confermata il 21/08. Non e' un refuso per 0=100 -> 7=0.
|
||||
{ test_id: 'q_alcol_life', etichetta: 'Unita alcoliche/settimana', tipo_valore: 'num',
|
||||
curva: 'lin_dec', params: { best: 7, worst: 14 },
|
||||
asse: 'Stile di Vita & Sonno', sotto_dominio: 'questionario_lifestyle', range_min: 0, range_max: 30, attivo_da: DA },
|
||||
{ test_id: 'q_luce', etichetta: 'Ore/giorno luce naturale/esterno', unita: 'h', tipo_valore: 'num',
|
||||
curva: 'incstep', params: { steps: [[0.5, 60], [1, 90], [999, 100]], zeroVal: 20 },
|
||||
asse: 'Stile di Vita & Sonno', sotto_dominio: 'questionario_lifestyle', range_min: 0, range_max: 8, attivo_da: DA },
|
||||
{ test_id: 'q_schermi', etichetta: 'Minuti schermi nell\'ora pre-sonno', unita: 'min', tipo_valore: 'num',
|
||||
curva: 'decstep', params: { steps: [[15, 85], [30, 70], [60, 40], [999, 10]], zeroVal: 100 },
|
||||
asse: 'Stile di Vita & Sonno', sotto_dominio: 'questionario_lifestyle', range_min: 0, range_max: 90, attivo_da: DA },
|
||||
];
|
||||
|
||||
export function seedRegistro(db: Database.Database): void {
|
||||
const ins = db.prepare(
|
||||
`INSERT OR REPLACE INTO registro_test
|
||||
(test_id, etichetta, unita, tipo_valore, curva, params, asse, sotto_dominio,
|
||||
range_min, range_max, attivo_da, attivo_a, note)
|
||||
VALUES (@test_id, @etichetta, @unita, @tipo_valore, @curva, @params, @asse, @sotto_dominio,
|
||||
@range_min, @range_max, @attivo_da, @attivo_a, @note)`
|
||||
);
|
||||
const tx = db.transaction((voci: VoceRegistro[]) => {
|
||||
for (const v of voci) {
|
||||
ins.run({
|
||||
test_id: v.test_id, etichetta: v.etichetta, unita: v.unita ?? null,
|
||||
tipo_valore: v.tipo_valore, curva: v.curva ?? null,
|
||||
params: v.params === undefined ? null : JSON.stringify(v.params),
|
||||
asse: v.asse ?? null, sotto_dominio: v.sotto_dominio ?? null,
|
||||
range_min: v.range_min ?? null, range_max: v.range_max ?? null,
|
||||
attivo_da: v.attivo_da, attivo_a: v.attivo_a ?? null, note: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
tx(QUESTIONARIO);
|
||||
}
|
||||
|
||||
// Pesi presi dal motore del cliente (AXIS_SUBDOMAIN_WEIGHTS, MACRO_SCORE_WEIGHTS,
|
||||
// FITNESS_AGE_WEIGHTS). I macro con peso 0 non si inseriscono: assenza e zero sono
|
||||
// la stessa cosa per la rinormalizzazione, e una riga a zero confonde chi legge.
|
||||
const PESI_ASSE: Record<string, Record<string, number>> = {
|
||||
'Forza & Struttura': { handgrip: 0.25, spinta: 0.20, trazione: 0.20, arti_inferiori: 0.20, core: 0.15 },
|
||||
'Composizione Corporea': { grasso: 0.40, muscolo: 0.35, whr: 0.25 },
|
||||
'Cardio-Respiratorio': { vo2max: 0.60, spirometria: 0.20, wellness_tower_cardio: 0.20 },
|
||||
'Recupero & Sistema Nervoso': { hrv: 0.50, pressione: 0.15, hrr: 0.15, questionario_sonno: 0.20 },
|
||||
'Energia & Regolazione Stress': { hrv: 0.50, questionario_energia_stress: 0.50 },
|
||||
'Stabilità & Mobilità Funzionale': { flamingo: 0.35, sit_and_reach: 0.25, plank: 0.20, back_scratch: 0.14, wellness_tower_shoulder: 0.06 },
|
||||
'Stile di Vita & Sonno': { questionario_lifestyle: 1.00 },
|
||||
};
|
||||
|
||||
const PESI_MACRO: Record<string, Record<string, number>> = {
|
||||
PERFORMANCE: {
|
||||
'Forza & Struttura': 0.25, 'Cardio-Respiratorio': 0.25, 'Composizione Corporea': 0.15,
|
||||
'Stabilità & Mobilità Funzionale': 0.20, 'Recupero & Sistema Nervoso': 0.10,
|
||||
'Energia & Regolazione Stress': 0.05,
|
||||
},
|
||||
ENERGY: {
|
||||
'Energia & Regolazione Stress': 0.30, 'Stile di Vita & Sonno': 0.20,
|
||||
'Recupero & Sistema Nervoso': 0.25, 'Cardio-Respiratorio': 0.15, 'Composizione Corporea': 0.10,
|
||||
},
|
||||
RECOVERY: {
|
||||
'Recupero & Sistema Nervoso': 0.45, 'Energia & Regolazione Stress': 0.15,
|
||||
'Stile di Vita & Sonno': 0.20, 'Stabilità & Mobilità Funzionale': 0.15, 'Forza & Struttura': 0.05,
|
||||
},
|
||||
};
|
||||
|
||||
const PESI_FITNESS_AGE: Record<string, number> = {
|
||||
cardio: 0.30, handgrip_isolato: 0.20, hrv_isolato: 0.20,
|
||||
forza_resto: 0.15, composizione: 0.10, stabilita: 0.05,
|
||||
};
|
||||
|
||||
export function seedPesi(db: Database.Database, modelVersion: string): void {
|
||||
const ins = db.prepare(
|
||||
`INSERT OR REPLACE INTO pesi (model_version, livello, contenitore, elemento, peso)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
);
|
||||
const tx = db.transaction(() => {
|
||||
for (const [asse, sd] of Object.entries(PESI_ASSE))
|
||||
for (const [el, p] of Object.entries(sd)) ins.run(modelVersion, 'asse', asse, el, p);
|
||||
for (const [macro, assi] of Object.entries(PESI_MACRO))
|
||||
for (const [el, p] of Object.entries(assi)) ins.run(modelVersion, 'macro', macro, el, p);
|
||||
for (const [el, p] of Object.entries(PESI_FITNESS_AGE))
|
||||
ins.run(modelVersion, 'fitness_age', 'fitness_age', el, p);
|
||||
});
|
||||
tx();
|
||||
}
|
||||
|
||||
export function testAttivi(db: Database.Database, alla: string): VoceRegistro[] {
|
||||
const righe = db.prepare(
|
||||
`SELECT * FROM registro_test
|
||||
WHERE attivo_da <= ? AND (attivo_a IS NULL OR attivo_a > ?)
|
||||
ORDER BY test_id`
|
||||
).all(alla, alla) as Record<string, unknown>[];
|
||||
return righe.map((r) => ({
|
||||
...r,
|
||||
params: r.params ? JSON.parse(r.params as string) : undefined,
|
||||
})) as VoceRegistro[];
|
||||
}
|
||||
|
||||
export function esisteTest(db: Database.Database, testId: string): boolean {
|
||||
const r = db.prepare(`SELECT 1 FROM registro_test WHERE test_id = ?`).get(testId);
|
||||
return r !== undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user