Siphonight 4bdcd52970 v1.12.0 (web) : fix #70 (inactifs sans projection) + page Doses (glyphes de forme, marqueurs prise de sang) — miroir Android
- FIX #70 : generateForecastDoses garde !isActive — un traitement archivé
  à Posologie ne projette plus de créneaux ; l'horizon du chip Prévision
  (chart.js) ne considère que les Posologies ACTIVES. §6.bis inchangé :
  l'historique reste simulé, seul le FUTUR s'arrête. 3 tests miroirs.
- Page Doses : routeGlyph (💊💉💧🩹🧴, miroir routeIcon) + encarts
  « prise de sang le même jour » 🧪↑/🧪↓ (labMarkersForDose, miroir) ;
  i18n FR/EN ; 10 tests doses-extras.test.js (⚠️ makeDose(ts, amount,
  overrides)).

194 tests + E2E verts (check.sh). WEB_VERSION alignée sur 1.12.0.
2026-09-29 23:24:18 +02:00

254 lines
8.6 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,
};
}
/**
* ORDRE D'AFFICHAGE de la page Traitements (v1.11.0, miroir fidèle de
* `ui/screens/TreatmentsScreen.kt` — même KDoc) : les ACTIFS d'abord, les
* INACTIFS regroupés TOUT EN BAS — l'ancien ordre brut du store mélangeait
* les deux groupes et le badge discret ne suffisait plus à les discerner
* (« peu discernables », remontée).
*
* Tri STABLE : l'ordre relatif À L'INTÉRIEUR de chaque groupe est conservé.
* PUR (aucune dépendance DOM) → testable en node
* (tests/treatments-display.test.js, miroir de TreatmentsDisplayTest.kt).
*
* @param {Array<object>} treatments
* @returns {Array<object>} nouvelle liste triée (l'entrée n'est pas mutée)
*/
export function treatmentsForDisplay(treatments) {
return [...treatments].sort((a, b) => Number(b.isActive) - Number(a.isActive));
}
/**
* GLYPHE de FORME DE PRISE (v1.12.0, miroir de `routeIcon` —
* `ui/screens/DosesScreen.kt`, même KDoc) : l'app web n'a pas d'icônes
* Material → émojis, même mapping : ORAL = pilule 💊 ; IM/SC = seringue 💉
* (glyphe partagé, la page Doses différencie par le texte de la ligne) ;
* GEL = goutte 💧 ; PATCH = sparadrap 🩹 ; OTHER/inconnu = compte-gouttes
* 🧴. PUR → testable en node (tests/doses-extras.test.js).
*
* @param {string|null} route
* @returns {string} émoji
*/
export function routeGlyph(route) {
switch (route) {
case 'ORAL': return '💊';
case 'INJECTION_IM':
case 'INJECTION_SUBCUT': return '💉';
case 'TRANSDERMAL_GEL': return '💧';
case 'TRANSDERMAL_PATCH': return '🩹';
default: return '🧴';
}
}