longevity: anagrafica del gestionale — elenco, dettaglio, età, apertura fascicolo

elencoClienti/clienteDaCodice/etaDelCliente/apriFascicolo (versione operativa,
sul singleton) affiancate alle rispettive versioni parametrizzate
(elencoClientiParam/clienteDaCodiceParam/etaDelClienteParam), sullo stesso
schema di codicePerUtente/codicePerUtenteLoggato: la parametrizzata resta
testabile su un DB :memory:, l'operativa è quella che pagine ed endpoint del
gestionale importano. apriFascicolo apre sempre senza user_id — l'accesso è #8,
non questa unità — per costruzione del tipo di `dati`, non per convenzione.

Refs #2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 12:53:20 +00:00
parent af8afebba8
commit 8e0b9e4647
+86 -1
View File
@@ -4,7 +4,7 @@
// risalga alla persona (richiesta del cliente, 21/08): un secondo punto di giunzione // risalga alla persona (richiesta del cliente, 21/08): un secondo punto di giunzione
// la annullerebbe in silenzio. Un test in tests/longevity/anagrafica.test.ts lo verifica. // la annullerebbe in silenzio. Un test in tests/longevity/anagrafica.test.ts lo verifica.
import type Database from 'better-sqlite3'; import type Database from 'better-sqlite3';
import { createIdentityDb } from './db'; import { createIdentityDb, getLongevityDb } from './db';
// Connessione singola a identity, privata di questo modulo: non è esportata, quindi non // Connessione singola a identity, privata di questo modulo: non è esportata, quindi non
// è raggiungibile da nessun'altra parte del codice, nemmeno con un re-export su più file // è raggiungibile da nessun'altra parte del codice, nemmeno con un re-export su più file
@@ -94,3 +94,88 @@ export function etaAllaData(dataNascita: string, alla: string): number {
if (m < 0 || (m === 0 && d.getDate() < n.getDate())) eta--; if (m < 0 || (m === 0 && d.getDate() < n.getDate())) eta--;
return eta; return eta;
} }
//
// Gestionale del trainer (#2, unità 2a): tre letture sull'anagrafica e l'apertura di un
// fascicolo, tutte sotto lo stesso vincolo di codicePerUtente/codicePerUtenteLoggato qui
// sopra — una versione PARAMETRIZZATA (identity esplicita, esportata solo per i test e per
// comporre da qui dentro) e una versione OPERATIVA (nessun argomento, apre da sola
// getIdentityDb) che è quella che pagine ed endpoint del gestionale importano davvero. La
// versione operativa non si può testare su un DB :memory: — il singleton è uno solo per
// processo — quindi il test vero gira sulla parametrizzata, esattamente come
// anagrafica.test.ts fa già con codicePerUtente invece di codicePerUtenteLoggato.
//
export type ClienteElenco = {
client_code: string;
nome: string;
cognome: string;
data_nascita: string | null;
/** True se il cliente ha già un account per vedere il proprio referto (user_id valorizzato
* in identity.clienti). Creare quell'account è #8, fuori da questa unità: qui si legge
* soltanto se esiste già. */
ha_accesso: boolean;
};
/** L'elenco dei clienti per la tabella del gestionale, ordinato per cognome poi nome —
* l'ordine in cui un trainer cerca una persona, non l'ordine di creazione del fascicolo. */
export function elencoClientiParam(identity: Database.Database): ClienteElenco[] {
const righe = identity.prepare(
`SELECT client_code, nome, cognome, data_nascita, user_id FROM clienti ORDER BY cognome, nome`
).all() as { client_code: string; nome: string; cognome: string; data_nascita: string | null; user_id: number | null }[];
return righe.map(({ user_id, ...resto }) => ({ ...resto, ha_accesso: user_id !== null }));
}
export function elencoClienti(): ClienteElenco[] {
return elencoClientiParam(getIdentityDb());
}
export type ClienteDettaglio = ClienteElenco & {
email: string | null;
telefono: string | null;
};
/** L'anagrafica di un cliente per la sua scheda nel gestionale, `null` se il codice non
* esiste — la pagina lo traduce in un 404, non in uno schermo vuoto. */
export function clienteDaCodiceParam(identity: Database.Database, code: string): ClienteDettaglio | null {
const r = identity.prepare(
`SELECT client_code, nome, cognome, data_nascita, email, telefono, user_id
FROM clienti WHERE client_code = ?`
).get(code) as
{ client_code: string; nome: string; cognome: string; data_nascita: string | null;
email: string | null; telefono: string | null; user_id: number | null } | undefined;
if (!r) return null;
const { user_id, ...resto } = r;
return { ...resto, ha_accesso: user_id !== null };
}
export function clienteDaCodice(code: string): ClienteDettaglio | null {
return clienteDaCodiceParam(getIdentityDb(), code);
}
/** L'età del cliente alla data indicata, `null` se non ha una data di nascita in anagrafica
* (compilazione facoltativa: il fascicolo può nascere senza). Non è un ripiego a zero —
* calcolaSessione tratta già `eta_alla_data: null` come "niente Fitness Age", stessa
* barriera del motore (vedi l'intestazione di motore/index.ts). */
export function etaDelClienteParam(identity: Database.Database, code: string, alla: string): number | null {
const r = identity.prepare(`SELECT data_nascita FROM clienti WHERE client_code = ?`).get(code) as
{ data_nascita: string | null } | undefined;
if (!r?.data_nascita) return null;
return etaAllaData(r.data_nascita, alla);
}
export function etaDelCliente(code: string, alla: string): number | null {
return etaDelClienteParam(getIdentityDb(), code, alla);
}
/** Apre un fascicolo nuovo dal gestionale: SEMPRE senza user_id — creare l'account che
* darebbe accesso al cliente è #8, non questa unità, e il tipo di `dati` qui sotto non
* prevede quel campo (stessa garanzia strutturale di CorpoQuestionario nell'endpoint del
* questionario: chi volesse passare uno user_id da qui non supererebbe `tsc --noEmit`).
* Delega tutto a `creaCliente`, che è già la versione testabile con DB propri. */
export function apriFascicolo(dati: {
nome: string; cognome: string; sesso: 'M' | 'F';
data_nascita?: string; email?: string; telefono?: string;
}): string {
return creaCliente(getIdentityDb(), getLongevityDb(), dati);
}