Siphonight f79a180e36 v1.9.4 (web) : presets ESE étendus à 6 esters + éditeur dynamique + portage v1.9.0 complété
- 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
2026-09-19 23:29:40 +02:00

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,
};
}