v1.12.0 : fix #70 (inactif à Posologie projetait encore) + Doses (icônes de forme, marqueurs prise de sang)
- FIX #70 : generateForecastDoses garde !isActive — un injectable archivé avec Posologie et de l'historique projetait encore ses injections (graphique Prévision, extension de fenêtre, horizon LabTiming) ; le chip Prévision s'activait sur des Posologies inactives. FIX : garde moteur + horizon/chip sur les ACTIFS. §6.bis inchangé : l'historique reste simulé, seul le FUTUR s'arrête (comme les rappels v1.4.0). Miroir web. 3 tests ForecastDosesGuardTest + 3 miroirs web. - Icônes de forme de prise dans Doses : routeIcon pur (pilule/seringue IM+SC teinte tertiary pour la SC/goutte/sparadrap/compte-gouttes ; ⚠️ MedicationLiquid absent d'icons-extended 1.7.8 → Colorize). - Marqueurs « prise de sang le même jour » : labMarkersForDose pur (jour calendaire LOCAL, lab à l'heure exacte = APRÈS) → encart 🧪↑/🧪↓ en bout de ligne. 7 tests DosesExtrasTest + 10 miroirs web. 274 tests JVM + 14 UI + lint verts ; web 194 tests + E2E verts (check.sh). Validé émulateur sur APK RELEASE avec données réelles seedées + scénario du bug (2 inactifs à Posologie 7 j) : Prévision on/off 0 crash, glyphes et marqueurs en place.
This commit is contained in:
parent
45ba0bdc62
commit
6a003fe8b9
18
README.en.md
18
README.en.md
@ -18,8 +18,8 @@ or Huawei Health) and JSON backup. **100% local, no account, no server.**
|
|||||||
> for informational purposes — they are not measurements. Always rely on your
|
> for informational purposes — they are not measurements. Always rely on your
|
||||||
> blood tests and on your endocrinologist's guidance.
|
> blood tests and on your endocrinologist's guidance.
|
||||||
|
|
||||||
- **Status**: v1.11.0 — Android build ✅, **lint clean** ✅, **264 unit tests** ✅
|
- **Status**: v1.12.0 — Android build ✅, **lint clean** ✅, **274 unit tests** ✅
|
||||||
(230 without the local test data; 6 regressions pinned on real data
|
(240 without the local test data; 6 regressions pinned on real data
|
||||||
**not committed to the repo**), **14 Compose UI tests** ✅ (emulator),
|
**not committed to the repo**), **14 Compose UI tests** ✅ (emulator),
|
||||||
smartwatch integration = notifications ✅, **private Gitea repo + releases
|
smartwatch integration = notifications ✅, **private Gitea repo + releases
|
||||||
with APK** ✅
|
with APK** ✅
|
||||||
@ -65,13 +65,17 @@ or Huawei Health) and JSON backup. **100% local, no account, no server.**
|
|||||||
upcoming doses are projected on the chart (never saved); **enabling forecast
|
upcoming doses are projected on the chart (never saved); **enabling forecast
|
||||||
does not move the chart start** but EXTENDS the window to the next dose
|
does not move the chart start** but EXTENDS the window to the next dose
|
||||||
(v1.4.4), and the projection is browsed by dragging left, up to **1 year**
|
(v1.4.4), and the projection is browsed by dragging left, up to **1 year**
|
||||||
- **Dose log** with exact date/time, dose in mg, **per-injection ester**
|
|
||||||
(EV↔EU↔EEn switch like in the spreadsheet), **editable** (tap a row in Doses),
|
|
||||||
**interval in days between doses** displayed
|
|
||||||
- **Inactive treatments**: an archived treatment disappears from input and
|
- **Inactive treatments**: an archived treatment disappears from input and
|
||||||
reminders, but **its whole history stays simulated and calibrated** — useful
|
reminders, but **its whole history stays simulated and calibrated** — useful
|
||||||
for a valerate → enanthate transition; **grouped at the very bottom** of the
|
for a valerate → enanthate transition; **grouped at the very bottom** of the
|
||||||
Treatments page under a dedicated header, dimmed cards (v1.11.0)
|
Treatments page under a dedicated header, dimmed cards (v1.11.0);
|
||||||
|
**no future projection anymore** (v1.12.0 — its slots disappear from the
|
||||||
|
Forecast chart)
|
||||||
|
- **Dose log** with exact date/time, dose in mg, **per-injection ester**
|
||||||
|
(EV↔EU↔EEn switch like in the spreadsheet), **editable** (tap a row in Doses),
|
||||||
|
**interval in days between doses** displayed; **route icon per dose row**
|
||||||
|
(oral / IM-SC injection / gel / patch, v1.12.0) and **same-day blood test
|
||||||
|
markers** (🧪↑ before the dose, 🧪↓ after)
|
||||||
- **Blood tests**: **E2 + T in a single entry** (each optional), displayed
|
- **Blood tests**: **E2 + T in a single entry** (each optional), displayed
|
||||||
**side by side** when they share the same date/time, editable (tap → pick the
|
**side by side** when they share the same date/time, editable (tap → pick the
|
||||||
entry); T units: ng/mL, ng/dL, ng/L, nmol/L
|
entry); T units: ng/mL, ng/dL, ng/L, nmol/L
|
||||||
@ -130,7 +134,7 @@ Gradle 9.7.1.
|
|||||||
git clone <repo> && cd HormoneTrack
|
git clone <repo> && cd HormoneTrack
|
||||||
echo "sdk.dir=/path/to/android-sdk" > local.properties # or ANDROID_HOME
|
echo "sdk.dir=/path/to/android-sdk" > local.properties # or ANDROID_HOME
|
||||||
./gradlew assembleDebug # APK: app/build/outputs/apk/debug/app-debug.apk
|
./gradlew assembleDebug # APK: app/build/outputs/apk/debug/app-debug.apk
|
||||||
./gradlew testDebugUnitTest # 264 tests (230 without local test data)
|
./gradlew testDebugUnitTest # 274 tests (240 without local test data)
|
||||||
./gradlew connectedDebugAndroidTest # 14 UI tests (emulator/device required)
|
./gradlew connectedDebugAndroidTest # 14 UI tests (emulator/device required)
|
||||||
./gradlew lint # clean lint required before a release
|
./gradlew lint # clean lint required before a release
|
||||||
```
|
```
|
||||||
|
|||||||
@ -18,7 +18,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
||||||
> sang et aux consignes de ton endocrinologue.
|
> sang et aux consignes de ton endocrinologue.
|
||||||
|
|
||||||
- **Statut** : v1.11.0 — build Android ✅, **lint vert** ✅, **264 tests unitaires** ✅ (230 sans les données de test locales ; 6 régressions épinglées sur données réelles **non versionnées**), **14 tests UI Compose** ✅ (émulateur), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
- **Statut** : v1.12.0 — build Android ✅, **lint vert** ✅, **274 tests unitaires** ✅ (240 sans les données de test locales ; 6 régressions épinglées sur données réelles **non versionnées**), **14 tests UI Compose** ✅ (émulateur), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
||||||
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
|
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
|
||||||
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
||||||
@ -67,7 +67,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
vers la gauche, jusqu'à **1 an**
|
vers la gauche, jusqu'à **1 an**
|
||||||
- **Log des doses** avec date/heure exacte, dose en mg, **ester par injection**
|
- **Log des doses** avec date/heure exacte, dose en mg, **ester par injection**
|
||||||
(switch EV↔EU↔EEn comme dans le tableur), **éditable** (tap sur une ligne dans Doses),
|
(switch EV↔EU↔EEn comme dans le tableur), **éditable** (tap sur une ligne dans Doses),
|
||||||
**intervalle en jours entre dosages** affiché
|
**intervalle en jours entre dosages** affiché ; **icône de forme de prise**
|
||||||
|
(oral / injection IM-SC / gel / patch, v1.12.0) et **encart « prise de sang
|
||||||
|
le même jour »** (🧪↑ avant la dose, 🧪↓ après)
|
||||||
- **Traitements inactifs** : un traitement archivé n'apparaît plus dans la saisie
|
- **Traitements inactifs** : un traitement archivé n'apparaît plus dans la saisie
|
||||||
ni dans les rappels, mais **tout son historique reste simulé et calibré** —
|
ni dans les rappels, mais **tout son historique reste simulé et calibré** —
|
||||||
utile pour une transition valerate → enanthate ; **regroupés tout en bas** de la
|
utile pour une transition valerate → enanthate ; **regroupés tout en bas** de la
|
||||||
@ -129,7 +131,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
|
|||||||
git clone <repo> && cd HormoneTrack
|
git clone <repo> && cd HormoneTrack
|
||||||
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
||||||
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
||||||
./gradlew testDebugUnitTest # 264 tests (230 sans les données locales)
|
./gradlew testDebugUnitTest # 274 tests (240 sans les données locales)
|
||||||
./gradlew connectedDebugAndroidTest # 14 tests UI (émulateur/appareil requis)
|
./gradlew connectedDebugAndroidTest # 14 tests UI (émulateur/appareil requis)
|
||||||
./gradlew lint # lint vert obligatoire avant release
|
./gradlew lint # lint vert obligatoire avant release
|
||||||
```
|
```
|
||||||
|
|||||||
@ -33,8 +33,8 @@ android {
|
|||||||
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
||||||
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
||||||
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
||||||
versionCode = 48
|
versionCode = 49
|
||||||
versionName = "1.11.0"
|
versionName = "1.12.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -416,6 +416,14 @@ object PharmacokineticEngine {
|
|||||||
): List<DoseLog> {
|
): List<DoseLog> {
|
||||||
val intervalDays = treatment.forecastIntervalDays ?: return emptyList()
|
val intervalDays = treatment.forecastIntervalDays ?: return emptyList()
|
||||||
if (intervalDays <= 0.0) return emptyList()
|
if (intervalDays <= 0.0) return emptyList()
|
||||||
|
// ⚠️ FIX #70 (v1.12.0, demande utilisatrice) : un traitement INACTIF ne
|
||||||
|
// projette PLUS de créneaux — l'archivage arrête les prédictions
|
||||||
|
// d'injections futures (graphique Prévision, extension de fenêtre,
|
||||||
|
// horizon LabTiming). Cohérent avec les rappels (nextReminderFireFor
|
||||||
|
// garde déjà !isActive). ⚠️ §6.bis inchangé : l'INACTIF reste simulé
|
||||||
|
// pour son HISTORIQUE (computeCurve ne passe pas ici) — seul le
|
||||||
|
// FUTUR s'arrête.
|
||||||
|
if (!treatment.isActive) return emptyList()
|
||||||
val intervalMs = (intervalDays * 24.0 * HOUR_MS).toLong()
|
val intervalMs = (intervalDays * 24.0 * HOUR_MS).toLong()
|
||||||
if (intervalMs <= 0L) return emptyList()
|
if (intervalMs <= 0L) return emptyList()
|
||||||
|
|
||||||
|
|||||||
@ -209,7 +209,14 @@ fun ChartScreen() {
|
|||||||
|
|
||||||
// Horizon de prévision (v1.4.1) : 12 × la plus grande Posologie, borné
|
// Horizon de prévision (v1.4.1) : 12 × la plus grande Posologie, borné
|
||||||
// [30 j ; 365 j] (cf forecastHorizonHours dans CurveChart.kt).
|
// [30 j ; 365 j] (cf forecastHorizonHours dans CurveChart.kt).
|
||||||
val maxIntervalDays = treatments.mapNotNull { it.forecastIntervalDays }.maxOrNull()
|
// ⚠️ FIX #70 (v1.12.0) : SEULS les traitements ACTIFS comptent — la
|
||||||
|
// Posologie d'un traitement archivé ne doit ni activer le chip
|
||||||
|
// Prévision ni étendre l'horizon (le moteur ne projette plus ses
|
||||||
|
// créneaux, cf generateForecastDoses).
|
||||||
|
val maxIntervalDays = treatments
|
||||||
|
.filter { it.isActive }
|
||||||
|
.mapNotNull { it.forecastIntervalDays }
|
||||||
|
.maxOrNull()
|
||||||
val forecastHorizonHours = forecastHorizonHours(maxIntervalDays)
|
val forecastHorizonHours = forecastHorizonHours(maxIntervalDays)
|
||||||
|
|
||||||
// ── Fenêtre temporelle (v1.4.1, extension v1.4.4, stabilisation v1.4.5) ──
|
// ── Fenêtre temporelle (v1.4.1, extension v1.4.4, stabilisation v1.4.5) ──
|
||||||
|
|||||||
@ -2,17 +2,24 @@ package com.hormonetrack.ui.screens
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Colorize
|
||||||
|
import androidx.compose.material.icons.filled.Healing
|
||||||
|
import androidx.compose.material.icons.filled.Medication
|
||||||
|
import androidx.compose.material.icons.filled.Science
|
||||||
|
import androidx.compose.material.icons.filled.Vaccines
|
||||||
|
import androidx.compose.material.icons.filled.WaterDrop
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AssistChip
|
import androidx.compose.material3.AssistChip
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@ -22,6 +29,7 @@ import androidx.compose.material3.FloatingActionButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -33,13 +41,20 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.hormonetrack.R
|
import com.hormonetrack.R
|
||||||
|
import com.hormonetrack.data.model.AdministrationRoute
|
||||||
import com.hormonetrack.data.model.DoseLog
|
import com.hormonetrack.data.model.DoseLog
|
||||||
|
import com.hormonetrack.data.model.LabResult
|
||||||
import com.hormonetrack.ui.LocalAppContainer
|
import com.hormonetrack.ui.LocalAppContainer
|
||||||
import com.hormonetrack.ui.components.DoseDialog
|
import com.hormonetrack.ui.components.DoseDialog
|
||||||
|
import com.hormonetrack.ui.theme.LabDot
|
||||||
import com.hormonetrack.util.HrtDuration
|
import com.hormonetrack.util.HrtDuration
|
||||||
import com.hormonetrack.ui.components.formatDose
|
import com.hormonetrack.ui.components.formatDose
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -49,6 +64,54 @@ import java.time.Instant
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icône de FORME DE PRISE (v1.12.0, demande utilisatrice : « des icônes pour
|
||||||
|
* bien séparer les différentes formes de prises des traitements »). Mapping :
|
||||||
|
* ORAL = pilule (Medication) ; IM/SC = seringue (Vaccines — le glyphe est
|
||||||
|
* partagé, la TEinte les distingue côté UI : IM = couleur courante, SC =
|
||||||
|
* tertiary) ; GEL = goutte (WaterDrop) ; PATCH = sparadrap (Healing) ;
|
||||||
|
* OTHER/inconnu = compte-gouttes (Colorize). PUR → testable en JVM
|
||||||
|
* (DosesExtrasTest) ; miroir web `routeGlyph` (js/data/models.js).
|
||||||
|
*/
|
||||||
|
fun routeIcon(route: AdministrationRoute?): ImageVector = when (route) {
|
||||||
|
AdministrationRoute.ORAL -> Icons.Filled.Medication
|
||||||
|
AdministrationRoute.INJECTION_IM -> Icons.Filled.Vaccines
|
||||||
|
AdministrationRoute.INJECTION_SUBCUT -> Icons.Filled.Vaccines
|
||||||
|
AdministrationRoute.TRANSDERMAL_GEL -> Icons.Filled.WaterDrop
|
||||||
|
AdministrationRoute.TRANSDERMAL_PATCH -> Icons.Filled.Healing
|
||||||
|
else -> Icons.Filled.Colorize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marqueurs d'ANALYSE DE SANG pour UNE dose (v1.12.0, demande utilisatrice :
|
||||||
|
* « une petite indication … dans le cas où un test sanguin a été effectué
|
||||||
|
* avant ou après une prise — peut-être un petit encart sur le côté »).
|
||||||
|
*
|
||||||
|
* Une analyse compte si elle tombe le MÊME JOUR calendaire LOCAL que la
|
||||||
|
* dose : au-delà, le lien clinique (labs au creux/pic autour d'une
|
||||||
|
* injection) devient ténu et chaque dose finirait marquée.
|
||||||
|
*
|
||||||
|
* @return Pair(AVANT la dose, APRÈS la dose) — une analyse exactement À
|
||||||
|
* l'heure de la dose compte comme APRÈS (convention arbitraire documentée).
|
||||||
|
* PUR → testable en JVM (DosesExtrasTest) ; miroir web `labMarkersForDose`
|
||||||
|
* (js/util/format.js).
|
||||||
|
*/
|
||||||
|
fun labMarkersForDose(
|
||||||
|
labs: List<LabResult>,
|
||||||
|
doseMs: Long,
|
||||||
|
zone: ZoneId = ZoneId.systemDefault()
|
||||||
|
): Pair<Boolean, Boolean> {
|
||||||
|
val day = Instant.ofEpochMilli(doseMs).atZone(zone).toLocalDate()
|
||||||
|
var before = false
|
||||||
|
var after = false
|
||||||
|
for (lab in labs) {
|
||||||
|
if (Instant.ofEpochMilli(lab.timestamp).atZone(zone).toLocalDate() == day) {
|
||||||
|
if (lab.timestamp < doseMs) before = true else after = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return before to after
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DosesScreen() {
|
fun DosesScreen() {
|
||||||
@ -56,6 +119,8 @@ fun DosesScreen() {
|
|||||||
val repo = container.repository
|
val repo = container.repository
|
||||||
val doseLogs by repo.allDoseLogs.collectAsState(initial = emptyList())
|
val doseLogs by repo.allDoseLogs.collectAsState(initial = emptyList())
|
||||||
val treatments by repo.allTreatments.collectAsState(initial = emptyList())
|
val treatments by repo.allTreatments.collectAsState(initial = emptyList())
|
||||||
|
// v1.12.0 : les labs alimentent les marqueurs « prise de sang avant/après »
|
||||||
|
val labResults by repo.allLabResults.collectAsState(initial = emptyList())
|
||||||
val treatmentMap = remember(treatments) { treatments.associateBy { it.id } }
|
val treatmentMap = remember(treatments) { treatments.associateBy { it.id } }
|
||||||
|
|
||||||
var showLogDialog by remember { mutableStateOf(false) }
|
var showLogDialog by remember { mutableStateOf(false) }
|
||||||
@ -154,6 +219,24 @@ fun DosesScreen() {
|
|||||||
val time = timeFmt.format(Instant.ofEpochMilli(log.timestamp).atZone(zone))
|
val time = timeFmt.format(Instant.ofEpochMilli(log.timestamp).atZone(zone))
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { toEdit = log },
|
modifier = Modifier.clickable { toEdit = log },
|
||||||
|
// v1.12.0 : icône de FORME DE PRISE à gauche —
|
||||||
|
// oral / injection IM / injection SC / gel /
|
||||||
|
// patch / autre se repèrent d'un coup d'œil.
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
routeIcon(tr?.route),
|
||||||
|
contentDescription = tr?.let {
|
||||||
|
stringResource(routeLabelRes(it.route))
|
||||||
|
},
|
||||||
|
tint = if (tr?.route == AdministrationRoute.INJECTION_SUBCUT) {
|
||||||
|
// IM/SC partagent le glyphe seringue :
|
||||||
|
// la teinte tertiary distingue la SC
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
} else {
|
||||||
|
LocalContentColor.current
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text("${tr?.name ?: "?"} · ${formatDose(log.doseAmount)} ${tr?.doseUnit ?: ""}")
|
Text("${tr?.name ?: "?"} · ${formatDose(log.doseAmount)} ${tr?.doseUnit ?: ""}")
|
||||||
},
|
},
|
||||||
@ -178,10 +261,18 @@ fun DosesScreen() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
// v1.12.0 : encart latéral « prise de sang
|
||||||
|
// le même jour » — ↑ = lab AVANT la dose,
|
||||||
|
// ↓ = lab APRÈS (cf labMarkersForDose)
|
||||||
|
val (labBefore, labAfter) = labMarkersForDose(labResults, log.timestamp, zone)
|
||||||
|
if (labBefore) LabSideMarker(up = true)
|
||||||
|
if (labAfter) LabSideMarker(up = false)
|
||||||
IconButton(onClick = { toDelete = log }) {
|
IconButton(onClick = { toDelete = log }) {
|
||||||
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
|
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,11 +325,37 @@ fun DosesScreen() {
|
|||||||
CoroutineScope(Dispatchers.IO).launch { repo.deleteDoseLog(log) }
|
CoroutineScope(Dispatchers.IO).launch { repo.deleteDoseLog(log) }
|
||||||
}) { Text(stringResource(R.string.delete)) }
|
}) { Text(stringResource(R.string.delete)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = { TextButton(onClick = { toDelete = null }) { Text(stringResource(R.string.cancel)) } },
|
||||||
TextButton(onClick = { toDelete = null }) { Text(stringResource(R.string.cancel)) }
|
|
||||||
},
|
|
||||||
title = { Text(stringResource(R.string.delete)) },
|
title = { Text(stringResource(R.string.delete)) },
|
||||||
text = { Text(stringResource(R.string.confirm_delete)) }
|
text = { Text(stringResource(R.string.confirm_delete)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encart latéral « prise de sang le même jour que cette dose » (v1.12.0,
|
||||||
|
* demande utilisatrice) : icône laboratoire + flèche — **↑** = analyse
|
||||||
|
* effectuée AVANT la dose, **↓** = APRÈS. La description sémantique porte
|
||||||
|
* la phrase complète (accessibilité TalkBack) ; le glyphe seul est décoratif
|
||||||
|
* (contentDescription = null sur l'icône, description portée par le Row).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun LabSideMarker(up: Boolean) {
|
||||||
|
val description = stringResource(if (up) R.string.lab_before_dose else R.string.lab_after_dose)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.semantics { contentDescription = description }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Science,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LabDot,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
if (up) "↑" else "↓",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = LabDot
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -147,6 +147,10 @@
|
|||||||
<!-- v1.11.0 : en-tête de la section des traitements inactifs (regroupés
|
<!-- v1.11.0 : en-tête de la section des traitements inactifs (regroupés
|
||||||
en bas de la page Traitements) -->
|
en bas de la page Traitements) -->
|
||||||
<string name="inactive_section">Traitements inactifs</string>
|
<string name="inactive_section">Traitements inactifs</string>
|
||||||
|
<!-- v1.12.0 : encart latéral « prise de sang le même jour » sur les
|
||||||
|
lignes de doses (↑ = lab avant la dose, ↓ = lab après) -->
|
||||||
|
<string name="lab_before_dose">Prise de sang plus tôt le même jour, avant cette dose</string>
|
||||||
|
<string name="lab_after_dose">Prise de sang plus tard le même jour, après cette dose</string>
|
||||||
<string name="no_treatment_hint">Crée d\'abord un traitement (onglet Traitements).</string>
|
<string name="no_treatment_hint">Crée d\'abord un traitement (onglet Traitements).</string>
|
||||||
|
|
||||||
<string name="type_estradiol">Œstradiol</string>
|
<string name="type_estradiol">Œstradiol</string>
|
||||||
|
|||||||
@ -147,6 +147,10 @@
|
|||||||
<!-- v1.11.0 : en-tête de la section des traitements inactifs (regroupés
|
<!-- v1.11.0 : en-tête de la section des traitements inactifs (regroupés
|
||||||
en bas de la page Traitements) -->
|
en bas de la page Traitements) -->
|
||||||
<string name="inactive_section">Inactive treatments</string>
|
<string name="inactive_section">Inactive treatments</string>
|
||||||
|
<!-- v1.12.0 : encart latéral « prise de sang le même jour » sur les
|
||||||
|
lignes de doses (↑ = lab avant la dose, ↓ = lab après) -->
|
||||||
|
<string name="lab_before_dose">Blood test earlier the same day, before this dose</string>
|
||||||
|
<string name="lab_after_dose">Blood test later the same day, after this dose</string>
|
||||||
<string name="no_treatment_hint">Create a treatment first (Treatments tab).</string>
|
<string name="no_treatment_hint">Create a treatment first (Treatments tab).</string>
|
||||||
|
|
||||||
<string name="type_estradiol">Estradiol</string>
|
<string name="type_estradiol">Estradiol</string>
|
||||||
|
|||||||
@ -0,0 +1,77 @@
|
|||||||
|
package com.hormonetrack.pk
|
||||||
|
|
||||||
|
import com.hormonetrack.data.model.AdministrationRoute
|
||||||
|
import com.hormonetrack.data.model.DoseLog
|
||||||
|
import com.hormonetrack.data.model.Treatment
|
||||||
|
import com.hormonetrack.data.model.TreatmentType
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **FIX #70 (v1.12.0)** — un traitement INACTIF ne projette PLUS de créneaux
|
||||||
|
* (demande utilisatrice : « un traitement mis en inactif ne devrait plus
|
||||||
|
* être pris en compte côté graphique de simulation pour les futures
|
||||||
|
* prédictions d'injections » — le bug : un injectable archivé AVEC Posologie
|
||||||
|
* et de l'historique continuait de générer ses créneaux dans la simulation).
|
||||||
|
*
|
||||||
|
* Sémantique §6.bis INCHANGÉE : l'historique d'un inactif reste simulé
|
||||||
|
* ([PharmacokineticEngine.computeCurve] ne passe pas par
|
||||||
|
* [PharmacokineticEngine.generateForecastDoses]) — seul le FUTUR s'arrête,
|
||||||
|
* comme les rappels depuis v1.4.0 (nextReminderFireFor garde déjà !isActive).
|
||||||
|
*/
|
||||||
|
class ForecastDosesGuardTest {
|
||||||
|
|
||||||
|
private val DAY_MS = 86_400_000L
|
||||||
|
private val NOW = 1_790_000_000_000L
|
||||||
|
|
||||||
|
private fun tr(id: Long, name: String, isActive: Boolean) = Treatment(
|
||||||
|
id = id, name = name, type = TreatmentType.ESTRADIOL,
|
||||||
|
route = AdministrationRoute.INJECTION_IM,
|
||||||
|
doseAmount = 5.0, doseUnit = "mg",
|
||||||
|
isActive = isActive,
|
||||||
|
forecastIntervalDays = 7.0,
|
||||||
|
createdAt = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun doses(treatmentId: Long) = listOf(
|
||||||
|
DoseLog(treatmentId = treatmentId, timestamp = NOW - 8 * DAY_MS, doseAmount = 5.0),
|
||||||
|
DoseLog(treatmentId = treatmentId, timestamp = NOW - DAY_MS, doseAmount = 5.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `inactive treatment with posology and history generates NO forecast slots`() {
|
||||||
|
val archived = tr(1, "EEn-old", isActive = false)
|
||||||
|
val slots = PharmacokineticEngine.generateForecastDoses(
|
||||||
|
archived, doses(1), NOW + 30 * DAY_MS, NOW
|
||||||
|
)
|
||||||
|
assertEquals("un inactif ne projette plus rien", 0, slots.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `active twin treatment still forecasts (behavior unchanged)`() {
|
||||||
|
val active = tr(2, "EEn", isActive = true)
|
||||||
|
val slots = PharmacokineticEngine.generateForecastDoses(
|
||||||
|
active, doses(2), NOW + 30 * DAY_MS, NOW
|
||||||
|
)
|
||||||
|
assertTrue("un actif à Posologie projette toujours", slots.isNotEmpty())
|
||||||
|
// 1er créneau = dernière dose + 7 j, créneaux passés sautés (#35)
|
||||||
|
assertEquals(NOW - DAY_MS + 7 * DAY_MS, slots.first().timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `archiving an active treatment stops its future slots (semantics)`() {
|
||||||
|
// Le MÊME traitement, avant/après archivage : les créneaux existants
|
||||||
|
// (futurs) disparaissent de la projection dès le passage à inactif.
|
||||||
|
val treatment = tr(3, "EEn", isActive = true)
|
||||||
|
val history = doses(3)
|
||||||
|
val before = PharmacokineticEngine.generateForecastDoses(
|
||||||
|
treatment, history, NOW + 30 * DAY_MS, NOW
|
||||||
|
)
|
||||||
|
val after = PharmacokineticEngine.generateForecastDoses(
|
||||||
|
treatment.copy(isActive = false), history, NOW + 30 * DAY_MS, NOW
|
||||||
|
)
|
||||||
|
assertTrue(before.isNotEmpty())
|
||||||
|
assertEquals(0, after.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
106
app/src/test/java/com/hormonetrack/ui/DosesExtrasTest.kt
Normal file
106
app/src/test/java/com/hormonetrack/ui/DosesExtrasTest.kt
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package com.hormonetrack.ui
|
||||||
|
|
||||||
|
import com.hormonetrack.data.model.AdministrationRoute
|
||||||
|
import com.hormonetrack.data.model.LabResult
|
||||||
|
import com.hormonetrack.ui.screens.labMarkersForDose
|
||||||
|
import com.hormonetrack.ui.screens.routeIcon
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests des ajouts v1.12.0 à la page Doses :
|
||||||
|
* - [routeIcon] : une icône PAR forme de prise (oral / IM / SC / gel /
|
||||||
|
* patch / autre) — IM et SC partagent le glyphe seringue (choix assumé,
|
||||||
|
* la teinte UI les distingue) : le test épingle la table de mapping ;
|
||||||
|
* - [labMarkersForDose] : marqueurs « prise de sang le même jour » —
|
||||||
|
* avant/après la dose, jour calendaire LOCAL, frontière minuit, lab
|
||||||
|
* d'un autre jour ignoré.
|
||||||
|
*
|
||||||
|
* Déterministes (zone fixe, instants construits) ; miroir web
|
||||||
|
* `tests/doses-extras.test.js`.
|
||||||
|
*/
|
||||||
|
class DosesExtrasTest {
|
||||||
|
|
||||||
|
private val ZONE = ZoneId.of("Europe/Paris")
|
||||||
|
|
||||||
|
/** 2026-09-27 12:00 Europe/Paris. */
|
||||||
|
private val noon = ZonedDateTime.of(2026, 9, 27, 12, 0, 0, 0, ZONE).toInstant().toEpochMilli()
|
||||||
|
|
||||||
|
private fun lab(atMs: Long) = LabResult(
|
||||||
|
marker = "E2", value = 100.0, unit = "pg/mL", timestamp = atMs
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── routeIcon ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `each route has its own glyph (except IM-SC sharing the syringe by design)`() {
|
||||||
|
val oral = routeIcon(AdministrationRoute.ORAL)
|
||||||
|
val im = routeIcon(AdministrationRoute.INJECTION_IM)
|
||||||
|
val sc = routeIcon(AdministrationRoute.INJECTION_SUBCUT)
|
||||||
|
val gel = routeIcon(AdministrationRoute.TRANSDERMAL_GEL)
|
||||||
|
val patch = routeIcon(AdministrationRoute.TRANSDERMAL_PATCH)
|
||||||
|
val other = routeIcon(AdministrationRoute.OTHER)
|
||||||
|
|
||||||
|
assertNotEquals(oral, im)
|
||||||
|
assertNotEquals(oral, gel)
|
||||||
|
assertNotEquals(oral, patch)
|
||||||
|
assertNotEquals(im, gel)
|
||||||
|
assertNotEquals(gel, patch)
|
||||||
|
assertNotEquals(patch, other)
|
||||||
|
assertEquals("IM/SC : même seringue (teinte UI distincte)", im, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `null route falls back to the OTHER glyph`() {
|
||||||
|
assertEquals(routeIcon(AdministrationRoute.OTHER), routeIcon(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── labMarkersForDose ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `same-day lab before and after the dose produce both markers`() {
|
||||||
|
val markers = labMarkersForDose(
|
||||||
|
listOf(lab(noon - 3_600_000L), lab(noon + 3_600_000L)), noon, ZONE
|
||||||
|
)
|
||||||
|
assertTrue(markers.first)
|
||||||
|
assertTrue(markers.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `lab of another day never marks the dose`() {
|
||||||
|
val yesterday = noon - 24 * 3_600_000L
|
||||||
|
val markers = labMarkersForDose(listOf(lab(yesterday)), noon, ZONE)
|
||||||
|
assertFalse(markers.first)
|
||||||
|
assertFalse(markers.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `midnight boundary is a calendar-day comparison`() {
|
||||||
|
// Lab à 23:50 la veille, dose à 00:10 : jours calendaires DIFFÉRENTS
|
||||||
|
// → pas de marqueur (le lien clinique ne traverse pas minuit).
|
||||||
|
val lateEvening = ZonedDateTime.of(2026, 9, 26, 23, 50, 0, 0, ZONE).toInstant().toEpochMilli()
|
||||||
|
val earlyMorning = ZonedDateTime.of(2026, 9, 27, 0, 10, 0, 0, ZONE).toInstant().toEpochMilli()
|
||||||
|
val markers = labMarkersForDose(listOf(lab(lateEvening)), earlyMorning, ZONE)
|
||||||
|
assertFalse(markers.first)
|
||||||
|
assertFalse(markers.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `lab exactly at dose time counts as AFTER (documented convention)`() {
|
||||||
|
val markers = labMarkersForDose(listOf(lab(noon)), noon, ZONE)
|
||||||
|
assertFalse(markers.first)
|
||||||
|
assertTrue(markers.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no labs - no markers`() {
|
||||||
|
val markers = labMarkersForDose(emptyList(), noon, ZONE)
|
||||||
|
assertFalse(markers.first)
|
||||||
|
assertFalse(markers.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,48 @@
|
|||||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
||||||
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
|
||||||
|
|
||||||
|
## [1.12.0] — 2026-09-29 (versionCode 49)
|
||||||
|
|
||||||
|
### Corrigé — un traitement inactif à Posologie projetait encore ses injections (#70)
|
||||||
|
|
||||||
|
- **Remontée** : « une injection qui n'est plus active est toujours prise en
|
||||||
|
compte dans le graphique de simulations si elle contient une posologie et
|
||||||
|
qu'elle a déjà été utilisée — un traitement mis en inactif ne devrait
|
||||||
|
plus être pris en compte pour les futures prédictions ».
|
||||||
|
- **Cause** : `generateForecastDoses` (source unique des créneaux
|
||||||
|
prévisionnels) ne vérifiait pas l'archivage ; de plus l'horizon du chip
|
||||||
|
Prévision du graphique incluait les Posologies INACTIVES.
|
||||||
|
- **Fix** : garde `!isActive` dans le moteur (l'historique reste simulé —
|
||||||
|
§6.bis inchangé — seul le FUTUR s'arrête, comme les rappels depuis
|
||||||
|
v1.4.0) + horizon/chip Prévision calculés sur les ACTIFS uniquement.
|
||||||
|
**Miroir web appliqué** (`js/pk/pk-engine.js` + `js/ui/chart.js`).
|
||||||
|
- Tests : 3 tests JVM (`ForecastDosesGuardTest`) + 3 miroirs web ;
|
||||||
|
émulateur avec données modifiées (2 inactifs à Posologie 7 j) :
|
||||||
|
Prévision on/off sans crash.
|
||||||
|
|
||||||
|
### Ajouté — icônes de forme de prise dans la page Doses
|
||||||
|
|
||||||
|
- Chaque ligne de dose porte désormais l'icône de sa forme : 💊-icône pilule
|
||||||
|
(oral), seringue (injection IM et SC — teinte distincte pour la SC),
|
||||||
|
goutte (gel), sparadrap (patch), compte-gouttes (autre). Miroir web :
|
||||||
|
émojis (`routeGlyph`, même mapping).
|
||||||
|
|
||||||
|
### Ajouté — marqueurs « prise de sang le même jour » sur les lignes de doses
|
||||||
|
|
||||||
|
- Une analyse de sang effectuée LE MÊME JOUR qu'une dose affiche un petit
|
||||||
|
encart latéral 🧪↑ (avant la dose) et/ou 🧪↓ (après) — pour visualiser
|
||||||
|
les prises de sang dans le tableau de dosages (demande utilisatrice).
|
||||||
|
Fenêtre = jour calendaire LOCAL (au-delà le lien clinique devient ténu).
|
||||||
|
Helper pur `labMarkersForDose` (Kotlin + miroir web) — 7 tests JVM
|
||||||
|
(`DosesExtrasTest` : mapping icônes + marqueurs, frontière minuit,
|
||||||
|
convention « à l'heure de la dose = après ») + 10 miroirs web.
|
||||||
|
- Vérifié émulateur avec données réelles : glyphes en tête de liste,
|
||||||
|
marqueur « Blood test later the same day » sur la dose du 10 sept
|
||||||
|
(analyse 35 min après).
|
||||||
|
|
||||||
|
Vérifié : **274 tests JVM** (240 sans données locales) + **14 tests UI** +
|
||||||
|
lint verts ; **web 194 tests + E2E verts** (`check.sh`).
|
||||||
|
|
||||||
## [1.11.0] — 2026-09-29 (versionCode 48)
|
## [1.11.0] — 2026-09-29 (versionCode 48)
|
||||||
|
|
||||||
### Ajouté — les traitements inactifs regroupés en bas de la page Traitements
|
### Ajouté — les traitements inactifs regroupés en bas de la page Traitements
|
||||||
|
|||||||
@ -63,6 +63,7 @@ Fonctionnalités v1 :
|
|||||||
|
|
||||||
| Date | Événement |
|
| Date | Événement |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| 29 sept. 2026 (session v1.12.0) | **Fix #70 + 2 améliorations Doses** (demandes). (a) **#70 — inactif à Posologie projetait encore** : `generateForecastDoses` (source unique des créneaux) ne vérifiait pas l'archivage, et l'horizon du chip Prévision incluait les Posologies INACTIVES. FIX : garde `!isActive` dans le moteur + horizon/chip sur les ACTIFS — §6.bis inchangé (historique toujours simulé, seul le FUTUR s'arrête, comme les rappels v1.4.0). **Miroir web** (pk-engine.js + chart.js). 3 tests JVM + 3 web. Émulateur : seed modifié (2 inactifs à Posologie 7 j), Prévision on/off 0 crash — leçon uiautomator : l'attribut `selected` des FilterChip ne reflète PAS la sélection Compose (vérifier par la LÉGENDE attendue, comme dit la recette §16.ter). (b) **Icônes de forme de prise** dans Doses (demande) : `routeIcon` pur — pilule/seringue (IM+SC, teinte tertiary pour SC)/goutte/sparadrap/compte-gouttes ; ⚠️ `MedicationLiquid` N'EXISTE PAS dans icons-extended 1.7.8 (résolu hors BOM) → `Colorize` pour OTHER (vérifié au jar). (c) **Marqueurs « prise de sang le même jour »** : `labMarkersForDose` pur (jour calendaire LOCAL, lab à l'heure exacte = APRÈS — convention documentée) → encart latéral 🧪↑/🧪↓ sur la ligne ; miroir web émojis + title. 7 tests JVM `DosesExtrasTest` + 10 web `doses-extras.test.js` (⚠️ helper web `makeDose(ts, amount, overrides)` — le 1ᵉʳ arg est le TIMESTAMP). Validé émulateur : content-desc des icônes (IM injection/Oral) + marqueur « Blood test later the same day » sur la dose du 10 sept. 274 tests JVM + 14 UI + lint ; web 194 tests + E2E (check.sh). |
|
||||||
| 29 sept. 2026 (session v1.11.0) | **Page Traitements : inactifs regroupés en bas + atténués** (demande : « peu discernables — les placer à la fin, et peut-être les rendre plus distincts »). Helper PUR `treatmentsForDisplay` (tri STABLE : actifs d'abord, inactifs après, ordre relatif conservé DANS chaque groupe) + LazyColumn en 3 sections (actifs / en-tête `inactive_section` SEULEMENT si des inactifs existent / inactifs `dimmed = true`) + cartes inactives **alpha 0,55** en plus du badge (lisibles et tappables — l'édition d'un archivé doit rester possible). **Miroir web** : `treatmentsForDisplay` dans `js/data/models.js` (node-safe, pas de DOM) + rendu sections + `opacity:0.55` + clés i18n `inactive_section` FR/EN. Tests : **5 JVM** (`TreatmentsDisplayTest` : regroupement, stabilité, tout-actif, tout-inactif, vide) + **4 web** (`tests/treatments-display.test.js`) — pas de test UI instrumenté pour l'en-tête (dépendrait de données inactives, contre la convention data-indépendance §8.ter) : couverture JVM + validation émulateur (5 actifs en haut, en-tête + EV-old/EEn-old tout en bas, dumps tr1/tr2). 264 tests JVM + lint ; web 184 tests + E2E verts (check.sh). **Publication** : commit a0f3ac5 + tag v1.11.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 80, farewell id 1588588) ; web v1.11.0 sync (commit 0dd5fa7 + tag, farewell = miroir serveur déjà répliqué). |
|
| 29 sept. 2026 (session v1.11.0) | **Page Traitements : inactifs regroupés en bas + atténués** (demande : « peu discernables — les placer à la fin, et peut-être les rendre plus distincts »). Helper PUR `treatmentsForDisplay` (tri STABLE : actifs d'abord, inactifs après, ordre relatif conservé DANS chaque groupe) + LazyColumn en 3 sections (actifs / en-tête `inactive_section` SEULEMENT si des inactifs existent / inactifs `dimmed = true`) + cartes inactives **alpha 0,55** en plus du badge (lisibles et tappables — l'édition d'un archivé doit rester possible). **Miroir web** : `treatmentsForDisplay` dans `js/data/models.js` (node-safe, pas de DOM) + rendu sections + `opacity:0.55` + clés i18n `inactive_section` FR/EN. Tests : **5 JVM** (`TreatmentsDisplayTest` : regroupement, stabilité, tout-actif, tout-inactif, vide) + **4 web** (`tests/treatments-display.test.js`) — pas de test UI instrumenté pour l'en-tête (dépendrait de données inactives, contre la convention data-indépendance §8.ter) : couverture JVM + validation émulateur (5 actifs en haut, en-tête + EV-old/EEn-old tout en bas, dumps tr1/tr2). 264 tests JVM + lint ; web 184 tests + E2E verts (check.sh). **Publication** : commit a0f3ac5 + tag v1.11.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 80, farewell id 1588588) ; web v1.11.0 sync (commit 0dd5fa7 + tag, farewell = miroir serveur déjà répliqué). |
|
||||||
| 28 sept. 2026 (session v1.10.1) | **Durcissement rappels — revue de code post-v1.10.0** (audit demandé : « optimisations ou problèmes, web et Android »). (a) **`launchMode="singleTop"`** : le combo `standard`+`CLEAR_TOP` recréait l'activité au tap notification app-ouverte et rendait le `onNewIntent` de la v1.10.0 inopérant (code mort) — désormais délivré SANS recreation (état UI préservé). (b) **`EXTRA_PLANNED_AT`** : le garde « créneau déjà honoré » évaluait le jour au moment du FEU — une alarme inexacte (`setWindow`/Doze) glissant 23:58→00:02 évaluait le mauvais jour ; l'instant planifié voyage dans l'intent (scheduleFor), le snooze le FORWARDE, le garde évalue le jour du CRÉNEAU ; vérifié émulateur (broadcast `planned_at` hier → notif sautée). (c) **`BootReceiver` consolidé** (dette §20.bis #2 FAIT) : `goAsync`+coroutine, `runBlocking` supprimés. (d) Style (import `mutableStateOf`) + audit doc (comptes 223 résiduels §14/#64 et §16.ter → 259 ; §9 et §20.bis à jour). Web NON concerné (miroir lab-timing laissé tel quel — boucle correcte et testée ; `importJson` par lots reste en dette). 259 tests JVM + 14 UI + lint verts. **Publication** : commit d531eca + tag v1.10.1 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 78, farewell id 1588586) ; validation §16.ter sur l'APK RELEASE AVANT tag (accueil + données, singleTop, planned_at A/B, 0 crash). |
|
| 28 sept. 2026 (session v1.10.1) | **Durcissement rappels — revue de code post-v1.10.0** (audit demandé : « optimisations ou problèmes, web et Android »). (a) **`launchMode="singleTop"`** : le combo `standard`+`CLEAR_TOP` recréait l'activité au tap notification app-ouverte et rendait le `onNewIntent` de la v1.10.0 inopérant (code mort) — désormais délivré SANS recreation (état UI préservé). (b) **`EXTRA_PLANNED_AT`** : le garde « créneau déjà honoré » évaluait le jour au moment du FEU — une alarme inexacte (`setWindow`/Doze) glissant 23:58→00:02 évaluait le mauvais jour ; l'instant planifié voyage dans l'intent (scheduleFor), le snooze le FORWARDE, le garde évalue le jour du CRÉNEAU ; vérifié émulateur (broadcast `planned_at` hier → notif sautée). (c) **`BootReceiver` consolidé** (dette §20.bis #2 FAIT) : `goAsync`+coroutine, `runBlocking` supprimés. (d) Style (import `mutableStateOf`) + audit doc (comptes 223 résiduels §14/#64 et §16.ter → 259 ; §9 et §20.bis à jour). Web NON concerné (miroir lab-timing laissé tel quel — boucle correcte et testée ; `importJson` par lots reste en dette). 259 tests JVM + 14 UI + lint verts. **Publication** : commit d531eca + tag v1.10.1 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 78, farewell id 1588586) ; validation §16.ter sur l'APK RELEASE AVANT tag (accueil + données, singleTop, planned_at A/B, 0 crash). |
|
||||||
| 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). |
|
| 27-28 sept. 2026 (session v1.10.0) | **2 fixes + 1 amélioration rappels** (remontées + nouvelles données réelles → 6ᵉ régression épinglée `backup-v1.9.8.json`). (a) **#68 — reco de prise de sang antérieure à la stabilisation** : le « creux » = minimum de la FENÊTRE ENTIÈRE tombait juste après l'injection PRÉCÉDENTE pour les esters à montée lente (EEn pic ~J+5 ≈ intervalle 7 j → creux d'absorption ~3 h post-injection) — date gouvernée par l'injection N−1 (antérieure à la stab) et « juste avant ton injection du … » faux de plusieurs jours (réel : creux 5 oct 02:32 < stab 7 oct 04:03). FIX : creux = niveau PRÉ-INJECTION du créneau (restaure le principe v1.8.0 documenté ; EV inchangé — le point pré-injection EST son min de fenêtre) ; **miroir web appliqué** (lab-timing.js). Tests : RegressionUserCase6Test data-driven dont balayage de `now` ±60 j (invariant « creux ≥ stab » à TOUT INSTANT) ; les 17 LabTimingTest passent inchangés. (b) **#69 — dialog fantôme** : tap notification → dialog ; le fermer + changer d'onglet + revenir sur l'accueil le ROUVRAIT à chaque fois (paramètre d'activité vs consommation dans les remember de Home, détruits à chaque sortie d'onglet — reproduit sur émulateur AVANT fix). FIX : `MutableState<Long?>` détenu par MainActivity, consommé UNE fois (extras retirés de l'intent après lecture ; bonus : tap notification app-ouverte ouvre aussi le dialog). Test UI `LogDoseRequestTest` + vérification émulateur. (c) **Rappel sauté si dose déjà loggée** (demande) : garde au déclenchement — `hasDoseLoggedOnDay` (pur, jour calendaire LOCAL) dans ReminderReceiver → notif sautée + re-programmation créneau suivant dans tous les cas ; 6 tests JVM + vérification A/B émulateur (EEn dose-du-jour → 0 notif, Fluoxetine sans dose → 1 notif). **259 tests JVM + 14 UI + lint** ; **web 180 tests + E2E verts** (check.sh). **Publication** : commit a4f859f + tag v1.10.0 poussés sur les 2 Gitea, APK release (2,7 Mo) + debug (21 Mo) publiés et vérifiés par téléchargement (cloudyfy id 77, farewell id 1588583). **Web miroir v1.10.0 sync** (fix #68 lab-timing, WEB_VERSION alignée, commit b865b16 + tag poussés ; #69/garde rappel = Android-only, notés dans le changelog web). |
|
||||||
@ -264,6 +265,7 @@ simulation de tout l'historique EV disparaissait) :
|
|||||||
| ✅ Retiré des chips « Log rapide » (accueil) | `HomeScreen` → `activeTreatments` |
|
| ✅ Retiré des chips « Log rapide » (accueil) | `HomeScreen` → `activeTreatments` |
|
||||||
| ✅ Retiré du dropdown des **nouvelles** doses | `DosesScreen` : création → actifs only ; **édition → tous** (une dose existante reste rattachée à son traitement inactif) |
|
| ✅ Retiré du dropdown des **nouvelles** doses | `DosesScreen` : création → actifs only ; **édition → tous** (une dose existante reste rattachée à son traitement inactif) |
|
||||||
| ✅ Rappels annulés (au save, au boot, au calcul de « prochaine dose ») | `rescheduleAll` + `nextReminderFireMs` (filtre `isActive`) ; `BootReceiver` lit `getAllOnce()` pour annuler les alarmes des inactifs |
|
| ✅ Rappels annulés (au save, au boot, au calcul de « prochaine dose ») | `rescheduleAll` + `nextReminderFireMs` (filtre `isActive`) ; `BootReceiver` lit `getAllOnce()` pour annuler les alarmes des inactifs |
|
||||||
|
| ✅ **Plus AUCUN créneau prévisionnel (v1.12.0, fix #70)** | `generateForecastDoses` garde `!isActive` — ni graphique Prévision, ni extension de fenêtre, ni horizon LabTiming ; le chip Prévision du graphique ne considère que les Posologies ACTIVES |
|
||||||
| ❌ JAMAIS retiré de la simulation | Home/Chart passent **`allTreatments`** au moteur — les écrans ne filtrent jamais |
|
| ❌ JAMAIS retiré de la simulation | Home/Chart passent **`allTreatments`** au moteur — les écrans ne filtrent jamais |
|
||||||
| ❌ JAMAIS retiré de la calibration (auto) | `autoCalibrated(allTreatments, …)` |
|
| ❌ JAMAIS retiré de la calibration (auto) | `autoCalibrated(allTreatments, …)` |
|
||||||
| ❌ JAMAIS supprimé de l'historique des doses | `DosesScreen` liste tout, suppression manuelle uniquement |
|
| ❌ JAMAIS supprimé de l'historique des doses | `DosesScreen` liste tout, suppression manuelle uniquement |
|
||||||
@ -755,7 +757,7 @@ devient à son tour**.
|
|||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**264 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **230 sans
|
**274 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **240 sans
|
||||||
les données de test locales** (cf §8.bis : les 6 classes de régression sont
|
les données de test locales** (cf §8.bis : les 6 classes de régression sont
|
||||||
skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||||||
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||||||
@ -988,9 +990,9 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
dans le code (le scanner refuserait, et ce serait une fuite).
|
dans le code (le scanner refuserait, et ce serait une fuite).
|
||||||
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
|
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
|
||||||
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
|
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
|
||||||
exécute 230 tests au lieu de 264 (v1.11.0 ; chiffres historiques : 44/87 à v1.3.x,
|
exécute 240 tests au lieu de 274 (v1.12.0 ; chiffres historiques : 44/87 à v1.3.x,
|
||||||
150/172 à v1.4.x, 160/183 à v1.5.0, 168/191 à v1.6.0, 177/200 à v1.7.0,
|
150/172 à v1.4.x, 160/183 à v1.5.0, 168/191 à v1.6.0, 177/200 à v1.7.0,
|
||||||
181/211 à v1.7.1, 190/220 à v1.8.0, 219/249 à v1.9.x, 225/259 à v1.10.x) ;
|
181/211 à v1.7.1, 190/220 à v1.8.0, 219/249 à v1.9.x, 225/259 à v1.10.x, 230/264 à v1.11.x) ;
|
||||||
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests
|
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests
|
||||||
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
|
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
|
||||||
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
|
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
|
||||||
@ -1190,7 +1192,11 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
|||||||
- `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même
|
- `DosesScreen` : LazyColumn par jour (desc), **Δ jours depuis la dose précédente du même
|
||||||
traitement** (`intervalsByDoseId`, colonne « Interval (d) » du `.ods`), suppression
|
traitement** (`intervalsByDoseId`, colonne « Interval (d) » du `.ods`), suppression
|
||||||
avec confirmation, FAB → `DoseDialog` (création), **tap sur la ligne → édition** ;
|
avec confirmation, FAB → `DoseDialog` (création), **tap sur la ligne → édition** ;
|
||||||
**en-tête « temps sous THS »** (v1.3.1) : carte depuis la 1re prise, jours
|
**v1.12.0** : **icône de forme de prise** en tête de ligne (`routeIcon` pur —
|
||||||
|
pilule/seringue IM+SC teinte tertiary pour SC/goutte/sparadrap/compte-gouttes)
|
||||||
|
et **encart latéral « prise de sang le même jour »** (`labMarkersForDose` pur —
|
||||||
|
🧪↑ avant la dose, 🧪↓ après ; jour calendaire LOCAL, lab à l'heure exacte =
|
||||||
|
APRÈS) ; **en-tête « temps sous THS »** (v1.3.1) : carte depuis la 1re prise, jours
|
||||||
totaux + décomposition mois/jours (`HrtDuration`, mois = 30 j estimés)
|
totaux + décomposition mois/jours (`HrtDuration`, mois = 30 j estimés)
|
||||||
- `LabsScreen` (v1.2.2) : **prise de sang E2 + T en une entrée** (LabDialog create :
|
- `LabsScreen` (v1.2.2) : **prise de sang E2 + T en une entrée** (LabDialog create :
|
||||||
deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage
|
deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage
|
||||||
@ -1870,6 +1876,19 @@ app-ouverte ouvre désormais aussi le dialog. Leçon : une demande à consommer
|
|||||||
fois doit vivre à la MÊME durée de vie que sa consommation — jamais « paramètre
|
fois doit vivre à la MÊME durée de vie que sa consommation — jamais « paramètre
|
||||||
longue-vie + effet courte-vie ». Épinglé par le test UI LogDoseRequestTest.
|
longue-vie + effet courte-vie ». Épinglé par le test UI LogDoseRequestTest.
|
||||||
|
|
||||||
|
**#70 (v1.12.0) — inactif à Posologie projetait encore ses injections** :
|
||||||
|
`generateForecastDoses` (source unique des créneaux prévisionnels) ne vérifiait
|
||||||
|
pas l'archivage — un injectable INACTIF avec Posologie et de l'historique
|
||||||
|
continuait d'alimenter le graphique Prévision, l'extension de fenêtre et
|
||||||
|
l'horizon LabTiming ; accessoirement le chip Prévision s'activait sur des
|
||||||
|
Posologies inactives. FIX : garde `!isActive` dans le moteur + horizon/chip sur
|
||||||
|
les ACTIFS. Cohérence d'ensemble (§6.bis) : l'INACTIF garde son historique
|
||||||
|
simulé/calibré mais ne projette plus rien — exactement comme ses rappels
|
||||||
|
(v1.4.0). Leçon : quand un état administratif gouverne DÉJÀ une consommation
|
||||||
|
d'une source (rappels), vérifier les AUTRES consommateurs de la même source.
|
||||||
|
Épinglé par ForecastDosesGuardTest (3 tests : inactif vide, actif inchangé,
|
||||||
|
avant/après archivage).
|
||||||
|
|
||||||
## 15.bis Comment régénérer l'asset mcmc_samples.json (v1.9.0)
|
## 15.bis Comment régénérer l'asset mcmc_samples.json (v1.9.0)
|
||||||
|
|
||||||
L'asset `src/main/assets/mcmc_samples.json` (posterior MCMC de
|
L'asset `src/main/assets/mcmc_samples.json` (posterior MCMC de
|
||||||
@ -2042,8 +2061,8 @@ de signature est requise — elle est désormais câblée :
|
|||||||
|
|
||||||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 264 au
|
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 274 au
|
||||||
total, 230 si `local-test-data/` est absent (les 6 classes de régression
|
total, 240 si `local-test-data/` est absent (les 6 classes de régression
|
||||||
réelles sont skippées via `Assume`) ; **lint vert
|
réelles sont skippées via `Assume`) ; **lint vert
|
||||||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||||||
attrapé les crashs #47/#48 dès v1.3.1).
|
attrapé les crashs #47/#48 dès v1.3.1).
|
||||||
@ -2343,6 +2362,11 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
- [ ] **v1.11.0** : page Traitements — les inactifs sont TOUT EN BAS sous
|
- [ ] **v1.11.0** : page Traitements — les inactifs sont TOUT EN BAS sous
|
||||||
l'en-tête « Traitements inactifs », cartes atténuées ; éditer un
|
l'en-tête « Traitements inactifs », cartes atténuées ; éditer un
|
||||||
inactif fonctionne toujours
|
inactif fonctionne toujours
|
||||||
|
- [ ] **v1.12.0** : archiver un injectable à Posologie → plus AUCUN créneau
|
||||||
|
projeté (chip Prévision : ses injections disparaissent de la projection)
|
||||||
|
- [ ] **v1.12.0** : page Doses — chaque ligne porte l'icône de sa forme
|
||||||
|
(pilule/seringue/goutte/sparadrap) ; une dose du jour d'une prise de
|
||||||
|
sang affiche l'encart 🧪↑ et/ou 🧪↓
|
||||||
- [ ] **v1.2.x** : graphique panoramique (glisser → passé, bouton « Revenir à maintenant »)
|
- [ ] **v1.2.x** : graphique panoramique (glisser → passé, bouton « Revenir à maintenant »)
|
||||||
- [ ] **v1.2.x** : toggles Estrannaise/TFS indépendants (les deux courbes superposées)
|
- [ ] **v1.2.x** : toggles Estrannaise/TFS indépendants (les deux courbes superposées)
|
||||||
- [ ] **v1.2.x** : chip « Prévision » (configurer la Posologie d'un traitement d'abord)
|
- [ ] **v1.2.x** : chip « Prévision » (configurer la Posologie d'un traitement d'abord)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user