f5ca841d92
Sostituisce il prototipo del cliente (salvava anche con "No" al consenso sanitario, perche' controllava solo che il campo non fosse vuoto). Qui il consenso si verifica PRIMA di scrivere qualunque dato: se non e' affermativo non si apre nessuna sessione, e la risposta lo dice. Il rifiuto si registra comunque fra le note di profilo, con esito e versione del questionario: va dimostrato tanto quanto un assenso. - src/lib/longevity/vista.ts: campiDelQuestionario (dal registro, non da una lista nel codice) e salvaDalForm (consenso prima di tutto, poi delega a salvaCompilazione). - src/pages/api/longevity/questionario.ts: il client_code arriva SEMPRE da codicePerUtente sulla sessione, mai dal corpo della richiesta. - src/lib/longevity/db.ts: getLongevityDb/getIdentityDb, connessioni singole per processo (come getDb() del sito), cosi' l'endpoint non apre una nuova connessione a ogni richiesta. - src/components/longevity/Questionario.tsx + pages/longevity/questionario.astro: isola React a quattro blocchi (le tre aree di domande + note/consenso), la pagina non interroga mai il database da sola.
152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { mkdirSync } from 'node:fs';
|
|
import { dirname } from 'node:path';
|
|
|
|
/**
|
|
* Sotto questa copertura di peso un elemento calcolato e' 'insufficiente' e non ha valore.
|
|
* Vale a ogni livello E dentro il questionario: soglia unica, confermata da Nicola il
|
|
* 22/08 — due criteri diversi sarebbero due cose da spiegare e da ricordare.
|
|
*/
|
|
export const COPERTURA_MINIMA = 0.4;
|
|
|
|
const SCHEMA_LONGEVITY = `
|
|
CREATE TABLE IF NOT EXISTS soggetti (
|
|
client_code TEXT PRIMARY KEY,
|
|
sesso TEXT NOT NULL CHECK (sesso IN ('M','F')),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS registro_test (
|
|
test_id TEXT PRIMARY KEY,
|
|
etichetta TEXT NOT NULL,
|
|
unita TEXT,
|
|
tipo_valore TEXT NOT NULL CHECK (tipo_valore IN ('num','txt')),
|
|
curva TEXT,
|
|
params TEXT,
|
|
asse TEXT,
|
|
sotto_dominio TEXT,
|
|
range_min REAL,
|
|
range_max REAL,
|
|
attivo_da TEXT NOT NULL,
|
|
attivo_a TEXT,
|
|
note TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sessioni (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
data TEXT NOT NULL,
|
|
tipo TEXT NOT NULL CHECK (tipo IN ('checkup','questionario')),
|
|
eta_alla_data INTEGER,
|
|
operatore TEXT,
|
|
note TEXT,
|
|
quest_version TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sessioni_cliente ON sessioni (client_code, data DESC);
|
|
CREATE TABLE IF NOT EXISTS misure (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sessione_id INTEGER NOT NULL REFERENCES sessioni(id) ON DELETE CASCADE,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
test_id TEXT NOT NULL REFERENCES registro_test(test_id),
|
|
valore_num REAL,
|
|
valore_txt TEXT,
|
|
unita TEXT,
|
|
fonte TEXT NOT NULL CHECK (fonte IN
|
|
('manuale','questionario','wellness_tower','vald','calibre','stress_index')),
|
|
fuori_range INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_misure_cliente_test ON misure (client_code, test_id);
|
|
CREATE INDEX IF NOT EXISTS idx_misure_sessione ON misure (sessione_id);
|
|
CREATE TABLE IF NOT EXISTS pesi (
|
|
model_version TEXT NOT NULL,
|
|
livello TEXT NOT NULL CHECK (livello IN ('asse','macro','fitness_age')),
|
|
contenitore TEXT NOT NULL,
|
|
elemento TEXT NOT NULL,
|
|
peso REAL NOT NULL,
|
|
PRIMARY KEY (model_version, livello, contenitore, elemento)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS profilo_note (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
sessione_id INTEGER REFERENCES sessioni(id),
|
|
campo_id TEXT NOT NULL,
|
|
testo TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS score (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
sessione_id INTEGER NOT NULL REFERENCES sessioni(id),
|
|
tipo TEXT NOT NULL CHECK (tipo IN ('asse','macro','fitness_age')),
|
|
elemento TEXT NOT NULL,
|
|
valore REAL,
|
|
copertura REAL NOT NULL,
|
|
stato TEXT NOT NULL CHECK (stato IN ('ok','insufficiente')),
|
|
quest_version TEXT,
|
|
model_version TEXT NOT NULL,
|
|
calcolato_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
-- Il tipo Punteggio protegge il codice ma non la tabella: l'interfaccia leggera'
|
|
-- 'score' direttamente, dove quel tipo non c'e' piu'. Questo vincolo lo riscrive
|
|
-- a livello di dato: o lo stato e' 'ok' e il valore c'e', o e' 'insufficiente' e
|
|
-- il valore e' NULL, mai un numero scritto sotto uno stato che lo nega.
|
|
-- ATTENZIONE al limite, che non e' quello che sembra: lo schema qui sopra gira con
|
|
-- CREATE TABLE IF NOT EXISTS, che su un file gia' esistente e' un'operazione a vuoto.
|
|
-- Quindi un longevity.db creato prima di questa modifica NON ha questo vincolo, e
|
|
-- continuera' a non averlo finche' qualcuno non lo migra a mano.
|
|
-- (Non e' un limite di SQLite: la versione imbarcata da better-sqlite3 e' la 3.53,
|
|
-- che ALTER TABLE ... ADD CONSTRAINT lo supporta. E' che qui una migrazione non c'e'.
|
|
-- Verificato il 22/08: la CLI di sistema e' la 3.37 e non lo supporta, quindi provarlo
|
|
-- da riga di comando da' un errore fuorviante.)
|
|
-- Oggi nessun longevity.db esiste ancora, quindi il caso e' teorico: lo diventa il
|
|
-- giorno del primo deploy, ed e' quello il momento di ricordarselo.
|
|
CHECK ((stato = 'ok' AND valore IS NOT NULL) OR (stato = 'insufficiente' AND valore IS NULL))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_score_cliente ON score (client_code, calcolato_at DESC);
|
|
`;
|
|
|
|
const SCHEMA_IDENTITY = `
|
|
CREATE TABLE IF NOT EXISTS clienti (
|
|
client_code TEXT PRIMARY KEY,
|
|
user_id INTEGER,
|
|
nome TEXT NOT NULL,
|
|
cognome TEXT NOT NULL,
|
|
data_nascita TEXT,
|
|
email TEXT,
|
|
telefono TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_clienti_user ON clienti (user_id);
|
|
`;
|
|
|
|
function apri(p: string, schema: string): Database.Database {
|
|
if (p !== ':memory:') mkdirSync(dirname(p), { recursive: true });
|
|
const db = new Database(p);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
db.exec(schema);
|
|
return db;
|
|
}
|
|
|
|
export function createLongevityDb(path?: string): Database.Database {
|
|
return apri(path ?? process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db', SCHEMA_LONGEVITY);
|
|
}
|
|
|
|
export function createIdentityDb(path?: string): Database.Database {
|
|
return apri(path ?? process.env.IDENTITY_DB_PATH ?? 'data/identity.db', SCHEMA_IDENTITY);
|
|
}
|
|
|
|
// Connessioni singole per processo, come getDb() in src/lib/db.ts: gli endpoint chiedono
|
|
// una connessione già aperta invece di aprirne una nuova a ogni richiesta.
|
|
let longevitySingleton: Database.Database | null = null;
|
|
let identitySingleton: Database.Database | null = null;
|
|
|
|
export function getLongevityDb(): Database.Database {
|
|
if (!longevitySingleton) longevitySingleton = createLongevityDb();
|
|
return longevitySingleton;
|
|
}
|
|
|
|
export function getIdentityDb(): Database.Database {
|
|
if (!identitySingleton) identitySingleton = createIdentityDb();
|
|
return identitySingleton;
|
|
}
|