Portage navigateur complet de l'app Android HormoneTrack v1.4.10 : - moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C, WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés - calibration par période d'ester et par modèle affiché (#60/#61) - UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire, prévision, pics/creux, fuseau configurable), rappels web (Notification API), seuils d'alerte, i18n FR/EN - sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1) - 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie - processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur, smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG) - dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions alignées sur l'Android porté, changelogs indépendants Non porté (impossible dans un navigateur, documenté §12) : agenda récurrent, notifications onglet fermé, montre.
213 lines
6.9 KiB
JavaScript
213 lines
6.9 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`, v1.4.6 — la liste suit la couverture
|
|
* du modèle : TFS = 7 esters ; WHSAH = 6 (PEP non couvert) ; Estrannaise
|
|
* (tables ODS) = EV/EU/EEn seulement — un traitement ESE + EB produirait
|
|
* 0 pg/mL).
|
|
*
|
|
* @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'];
|
|
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,
|
|
};
|
|
}
|