- presets.js : EC huile (5 mg IM), EB (1 mg IM), EUCS suspension cristalline (10 mg SC) — 25 entrées, champs Bateman informatifs - pk-engine.js : Esters.EUCS manquant (portage v1.9.0 incomplet — le preset référençait undefined) - models.js : choicesForModel ESE = 6 esters (parité Android v1.9.0 ; était resté sur la couverture des tables ODS à 3) + KDoc à jour - treatment-editor.js : liste des esters = choicesForModel(model) — rebascule sur EV si l'ester sort de la couverture au changement de modèle (garde anti 0 pg/mL) + re-render du formulaire - tests/presets.test.js (nouveau, 6) : couverture 6 esters ESE, unicité, i18n EN/FR, TFS 7 / WHSAH 6 inchangés, choicesForModel aligné dispatchs - versions 1.9.4 (settings + e2e) + CHANGELOG/§4/§8/README
214 lines
7.1 KiB
JavaScript
214 lines
7.1 KiB
JavaScript
/**
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* Modèles de données (portage de `data/model/*.kt`).
|
|
*
|
|
* ⚠️ COMPATIBILITÉ BACKUP : les noms de champs sont CEUX DE GSON (noms de
|
|
* propriétés Kotlin) — identiques aux entités Room. NE JAMAIS renommer un
|
|
* champ ici sans vérifier BackupManager.kt (sinon les exports Android ne
|
|
* s'importent plus) :
|
|
* - Treatment : id, name, type, route, doseAmount, doseUnit, isActive,
|
|
* notes, esterType, pkModel, absorptionHours, eliminationHalfLifeHours,
|
|
* bioavailabilityFraction, scaleFactor, forecastIntervalDays,
|
|
* reminderHour, reminderMinute, reminderEnabled, calendarEventId, createdAt
|
|
* - DoseLog : id, treatmentId, timestamp, doseAmount, notes, esterType
|
|
* - LabResult : id, marker, value, unit, timestamp, notes
|
|
* - TConfig : base, floor, k
|
|
* - UserSettings : language, autoCalibrate, alertE2High, alertE2Low,
|
|
* alertTHigh, alertTLow (champs PLATS — cf BackupManager.kt)
|
|
*
|
|
* `calendarEventId` est conservé (fidelité du schéma) mais TOUJOURS null
|
|
* côté web : les événements d'agenda récurrents sont impossibles dans un
|
|
* navigateur (pas de CalendarProvider) — fonctionnalité Android non portée.
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
export const TreatmentType = {
|
|
ESTRADIOL: 'ESTRADIOL',
|
|
ANTI_ANDROGEN: 'ANTI_ANDROGEN',
|
|
PROGESTOGEN: 'PROGESTOGEN',
|
|
OTHER: 'OTHER',
|
|
};
|
|
|
|
export const AdministrationRoute = {
|
|
ORAL: 'ORAL',
|
|
TRANSDERMAL_GEL: 'TRANSDERMAL_GEL',
|
|
TRANSDERMAL_PATCH: 'TRANSDERMAL_PATCH',
|
|
INJECTION_IM: 'INJECTION_IM',
|
|
INJECTION_SUBCUT: 'INJECTION_SUBCUT',
|
|
OTHER: 'OTHER',
|
|
};
|
|
|
|
/**
|
|
* Esters proposables dans l'override par dose pour un MODÈLE PK donné
|
|
* (miroir de `Esters.choicesForModel` — la liste suit la couverture RÉELLE
|
|
* du modèle : TFS = 7 esters ; WHSAH = 6 (PEP non couvert) ; Estrannaise
|
|
* ANALYTIQUE (v1.9.0) = 6 esters injectables, dont EUCS exclusif à ce fit —
|
|
* un ester hors liste produirait 0 pg/mL : aucun fallback silencieux).
|
|
*
|
|
* @param {string} pkModel
|
|
* @returns {string[]}
|
|
*/
|
|
export function choicesForModel(pkModel) {
|
|
if (pkModel === 'TFS') return ['EV', 'EU', 'EEN', 'EB', 'EC', 'ECS', 'PEP'];
|
|
if (pkModel === 'WHS') return ['EV', 'EU', 'EEN', 'EB', 'EC', 'ECS'];
|
|
if (pkModel === 'ESE') return ['EV', 'EU', 'EEN', 'EC', 'EB', 'EUCS']; // v1.9.4 : analytique (parité Android v1.9.0)
|
|
return ['EV', 'EU', 'EEN'];
|
|
}
|
|
|
|
/**
|
|
* Libellé humain d'un ester (miroir de `esterLabel` du DoseDialog Android).
|
|
* @param {string} ester
|
|
* @returns {string}
|
|
*/
|
|
export function esterLabel(ester) {
|
|
switch (ester) {
|
|
case 'EV': return 'EV — valerate';
|
|
case 'EU': return 'EU — undecylate';
|
|
case 'EEN': return 'EEn — enanthate';
|
|
case 'EB': return 'EB — benzoate';
|
|
case 'EC': return 'EC — cypionate (oil)';
|
|
case 'ECS': return 'EC — cypionate (suspension)';
|
|
case 'PEP': return 'PEP — polyestradiol phosphate';
|
|
default: return ester;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clé i18n du label d'un modèle PK (miroir de `modelLabelRes`, fix #59 —
|
|
* UNIQUE source du label : TOUTE nouvelle utilisation doit passer par ce
|
|
* helper, pas par un ternaire à 2 branches qui oubliait WHSAH).
|
|
* @param {string} pkModel
|
|
* @returns {string} clé i18n
|
|
*/
|
|
export function modelLabelKey(pkModel) {
|
|
if (pkModel === 'TFS') return 'model_tfs';
|
|
if (pkModel === 'WHS') return 'model_whs';
|
|
return 'model_ese';
|
|
}
|
|
|
|
/** Clé i18n d'une voie d'administration. */
|
|
export function routeLabelKey(route) {
|
|
switch (route) {
|
|
case 'ORAL': return 'route_oral';
|
|
case 'TRANSDERMAL_GEL': return 'route_gel';
|
|
case 'TRANSDERMAL_PATCH': return 'route_patch';
|
|
case 'INJECTION_IM': return 'route_injection_im';
|
|
case 'INJECTION_SUBCUT': return 'route_injection_sc';
|
|
default: return 'route_other';
|
|
}
|
|
}
|
|
|
|
/** Clé i18n d'un type de traitement. */
|
|
export function typeLabelKey(type) {
|
|
switch (type) {
|
|
case 'ESTRADIOL': return 'type_estradiol';
|
|
case 'ANTI_ANDROGEN': return 'type_antiandrogen';
|
|
case 'PROGESTOGEN': return 'type_progestogen';
|
|
default: return 'type_other';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Est-ce une voie injectable (IM/SC) ?
|
|
* @param {string} route
|
|
* @returns {boolean}
|
|
*/
|
|
export function isInjectionRoute(route) {
|
|
return route === 'INJECTION_IM' || route === 'INJECTION_SUBCUT';
|
|
}
|
|
|
|
/**
|
|
* Suggère une clé i18n de route depuis un preset (utilisé par l'éditeur).
|
|
* (Aucune logique : lisibilité des écrans.)
|
|
*/
|
|
|
|
/**
|
|
* Crée un Treatment avec les valeurs par défaut de Room (`Treatment.kt`).
|
|
* Les traitements sont des objets PLATS (JSON-serialisables directement).
|
|
*
|
|
* @param {Partial<object>} [init={}] Champs à surcharger
|
|
* @returns {object}
|
|
*/
|
|
export function newTreatment(init = {}) {
|
|
return {
|
|
id: 0, // 0 = pas encore en base (auto-increment côté store)
|
|
name: '',
|
|
type: TreatmentType.ESTRADIOL,
|
|
route: AdministrationRoute.INJECTION_IM,
|
|
doseAmount: 0,
|
|
doseUnit: 'mg',
|
|
isActive: true,
|
|
notes: null,
|
|
esterType: 'NONE',
|
|
pkModel: 'ESE',
|
|
// PK Bateman (gel/patch/oral)
|
|
absorptionHours: 4,
|
|
eliminationHalfLifeHours: 24,
|
|
bioavailabilityFraction: 1.0,
|
|
// Calibration : ratio lab / prédiction (le « Scale factor » du .ods)
|
|
scaleFactor: 1.0,
|
|
// Simulation prévisionnelle : intervalle en JOURS (null = pas de prévision)
|
|
forecastIntervalDays: null,
|
|
// Rappel
|
|
reminderHour: null,
|
|
reminderMinute: null,
|
|
reminderEnabled: false,
|
|
// Agenda : TOUJOURS null côté web (feature Android non portable)
|
|
calendarEventId: null,
|
|
createdAt: Date.now(),
|
|
...init,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Propriétés DÉRIVÉES d'un treatment — recalculées à la demande (pas
|
|
* stockées) : miroir des getters Kotlin `isInjection` / `usesProfileModel`.
|
|
*
|
|
* @param {object} tr
|
|
* @returns {{isInjection:boolean, usesProfileModel:boolean}}
|
|
*/
|
|
export function treatmentFlags(tr) {
|
|
const isInjection = isInjectionRoute(tr.route);
|
|
return {
|
|
isInjection,
|
|
// Un traitement a un PROFIL PK s'il est injectable ET avec un ester
|
|
// (les traitements Bateman — gel/patch/oral — n'en ont pas)
|
|
usesProfileModel: isInjection && tr.esterType !== 'NONE',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Crée un DoseLog (valeurs par défaut = `DoseLog.kt`).
|
|
* @param {Partial<object>} [init={}]
|
|
* @returns {object}
|
|
*/
|
|
export function newDoseLog(init = {}) {
|
|
return {
|
|
id: 0,
|
|
treatmentId: 0,
|
|
timestamp: Date.now(),
|
|
doseAmount: 0,
|
|
notes: null,
|
|
// Override par injection (null = ester du traitement)
|
|
esterType: null,
|
|
...init,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Crée un LabResult (valeurs par défaut = `LabResult.kt`).
|
|
* @param {Partial<object>} [init={}]
|
|
* @returns {object}
|
|
*/
|
|
export function newLabResult(init = {}) {
|
|
return {
|
|
id: 0,
|
|
marker: 'E2',
|
|
value: 0,
|
|
unit: 'pg/mL',
|
|
timestamp: Date.now(),
|
|
notes: null,
|
|
...init,
|
|
};
|
|
}
|