v1.2.2 : prise de sang E2 + T en une entrée, affichage côte à côte, toggle T masque aussi les labs T
- LabDialog : mode création avec deux sections (E2 et T, chacune optionnelle, au moins une valeur requise) → insère 1 ou 2 LabResult au même timestamp ; mode édition inchangé (un seul marqueur, pré-rempli) - LabsScreen : regroupement par timestamp (groupLabsForDisplay : E2 avant T, autres ensuite, tri desc) → affichage « E2 306 pg/mL · T 45 ng/dL » côte à côte ; tap sur une paire → sélecteur « Quelle entrée modifier ? » puis édition pré-remplie ; suppression = la prise entière (confirm nommant les valeurs) ; formatLabValue préserve les décimales - ChartScreen : le toggle T masque désormais aussi les labs T (avant : seuls les points restaient visibles) - LabsGroupingTest : 4 tests de regroupement (48 tests verts au total) - versionCode 5, versionName 1.2.2 ; docs mises à jour
This commit is contained in:
parent
571e346b73
commit
4731e4c159
@ -9,7 +9,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.2.1 — build Android ✅, **44 tests unitaires** ✅ (2 régressions épinglées sur données réelles), intégration montre = notifications ✅, **repo git avec releases taguées** ✅
|
- **Statut** : v1.2.2 — build Android ✅, **48 tests unitaires** ✅ (2 régressions épinglées sur données réelles), intégration montre = notifications ✅, **repo git avec releases taguées** ✅
|
||||||
- **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)
|
||||||
@ -29,8 +29,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
- **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é
|
||||||
- **Analyses de sang** (E2, T, PRL…) tracées sur les courbes comme points de calibration,
|
- **Analyses de sang** : **E2 + T en une seule entrée** (chacune optionnelle), affichées
|
||||||
**éditables** (tap sur une ligne dans Analyses) ; unités T : ng/mL, ng/dL, ng/L, nmol/L
|
**côte à côte** quand elles partagent la même date/heure, éditables (tap → choix de
|
||||||
|
l'entrée) ; unités T : ng/mL, ng/dL, ng/L, nmol/L
|
||||||
- **Calibration** : facteur d'échelle par traitement = médiane(lab ÷ prédiction du modèle),
|
- **Calibration** : facteur d'échelle par traitement = médiane(lab ÷ prédiction du modèle),
|
||||||
calculé automatiquement (« Scale factor » du `.ods`, automatisé) — ou **calibration
|
calculé automatiquement (« Scale factor » du `.ods`, automatisé) — ou **calibration
|
||||||
automatique permanente** (option, désactivée par défaut) qui calibre **chaque ester
|
automatique permanente** (option, désactivée par défaut) qui calibre **chaque ester
|
||||||
|
|||||||
@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.hormonetrack"
|
applicationId = "com.hormonetrack"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 4
|
versionCode = 5
|
||||||
versionName = "1.2.1"
|
versionName = "1.2.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.hormonetrack.ui.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
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.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
@ -9,6 +10,7 @@ import androidx.compose.material3.DropdownMenuItem
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@ -23,20 +25,25 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.hormonetrack.R
|
import com.hormonetrack.R
|
||||||
import com.hormonetrack.data.model.LabResult
|
import com.hormonetrack.data.model.LabResult
|
||||||
|
|
||||||
|
/** Valeur de labo saisie pour un marqueur (valeur + unité). */
|
||||||
|
data class LabValue(val value: Double, val unit: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog create-OU-edit pour les résultats de labo (même mécanique que DoseDialog).
|
* Dialog de saisie de prise de sang.
|
||||||
*
|
*
|
||||||
* existing == null → création ;
|
* - **Création** (existing == null) : E2 et T **dans la même entrée**, chacune
|
||||||
* existing != null → édition : tout est pré-rempli (marqueur, valeur, unité,
|
* optionnelle (au moins une valeur requise), date/heure et notes partagées →
|
||||||
* date/heure, notes) et la confirmation appelle onConfirmEdit avec la copie mise à
|
* insère 1 ou 2 LabResult avec le MÊME timestamp (l'écran Analyses les affiche
|
||||||
* jour (id et timestamp préservés sauf modification explicite).
|
* alors côte à côte).
|
||||||
|
* - **Édition** (existing != null) : un seul marqueur, tout pré-rempli →
|
||||||
|
* onConfirmEdit avec la copie mise à jour.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LabDialog(
|
fun LabDialog(
|
||||||
existing: LabResult? = null,
|
existing: LabResult? = null,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirmCreate: (marker: String, value: Double, unit: String, timestampMs: Long, notes: String) -> Unit,
|
onConfirmCreate: (e2: LabValue?, t: LabValue?, timestampMs: Long, notes: String) -> Unit,
|
||||||
onConfirmEdit: (LabResult) -> Unit
|
onConfirmEdit: (LabResult) -> Unit
|
||||||
) {
|
) {
|
||||||
val isEdit = existing != null
|
val isEdit = existing != null
|
||||||
@ -50,6 +57,15 @@ fun LabDialog(
|
|||||||
}
|
}
|
||||||
var unit by remember(existing) { mutableStateOf(existing?.unit ?: "pg/mL") }
|
var unit by remember(existing) { mutableStateOf(existing?.unit ?: "pg/mL") }
|
||||||
var unitExpanded by remember { mutableStateOf(false) }
|
var unitExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Mode création : deux sections (E2 + T), chacune optionnelle
|
||||||
|
var e2Text by remember { mutableStateOf("") }
|
||||||
|
var e2Unit by remember { mutableStateOf("pg/mL") }
|
||||||
|
var e2UnitExpanded by remember { mutableStateOf(false) }
|
||||||
|
var tText by remember { mutableStateOf("") }
|
||||||
|
var tUnit by remember { mutableStateOf("ng/mL") }
|
||||||
|
var tUnitExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var dateTime by remember(existing) {
|
var dateTime by remember(existing) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
existing?.let { localDateTimeFromMs(it.timestamp) } ?: java.time.LocalDateTime.now()
|
existing?.let { localDateTimeFromMs(it.timestamp) } ?: java.time.LocalDateTime.now()
|
||||||
@ -60,22 +76,38 @@ fun LabDialog(
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(
|
||||||
val v = valueText.replace(',', '.').toDoubleOrNull() ?: return@TextButton
|
enabled = if (isEdit) {
|
||||||
if (isEdit && existing != null) {
|
valueText.replace(',', '.').toDoubleOrNull() != null
|
||||||
onConfirmEdit(
|
|
||||||
existing.copy(
|
|
||||||
marker = marker.trim().uppercase(),
|
|
||||||
value = v,
|
|
||||||
unit = unit.trim(),
|
|
||||||
timestamp = msFromLocalDateTime(dateTime),
|
|
||||||
notes = notes.ifBlank { null }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
onConfirmCreate(marker.trim().uppercase(), v, unit.trim(), msFromLocalDateTime(dateTime), notes)
|
e2Text.replace(',', '.').toDoubleOrNull() != null ||
|
||||||
|
tText.replace(',', '.').toDoubleOrNull() != null
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (isEdit && existing != null) {
|
||||||
|
val v = valueText.replace(',', '.').toDoubleOrNull()
|
||||||
|
?: return@TextButton
|
||||||
|
onConfirmEdit(
|
||||||
|
existing.copy(
|
||||||
|
marker = marker.trim().uppercase(),
|
||||||
|
value = v,
|
||||||
|
unit = unit.trim(),
|
||||||
|
timestamp = msFromLocalDateTime(dateTime),
|
||||||
|
notes = notes.ifBlank { null }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val e2Val = e2Text.replace(',', '.').toDoubleOrNull()
|
||||||
|
val tVal = tText.replace(',', '.').toDoubleOrNull()
|
||||||
|
onConfirmCreate(
|
||||||
|
e2Val?.let { LabValue(it, e2Unit.trim()) },
|
||||||
|
tVal?.let { LabValue(it, tUnit.trim()) },
|
||||||
|
msFromLocalDateTime(dateTime),
|
||||||
|
notes
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) { Text(stringResource(R.string.save)) }
|
) { Text(stringResource(R.string.save)) }
|
||||||
},
|
},
|
||||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
|
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
|
||||||
title = {
|
title = {
|
||||||
@ -83,37 +115,85 @@ fun LabDialog(
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
ExposedDropdownMenuBox(expanded = markerExpanded, onExpandedChange = { markerExpanded = it }) {
|
if (isEdit) {
|
||||||
OutlinedTextField(
|
// Édition : un seul marqueur (l'entrée existante)
|
||||||
value = marker,
|
ExposedDropdownMenuBox(expanded = markerExpanded, onExpandedChange = { markerExpanded = it }) {
|
||||||
onValueChange = { marker = it },
|
OutlinedTextField(
|
||||||
label = { Text(stringResource(R.string.lab_marker)) },
|
value = marker,
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = markerExpanded) },
|
onValueChange = { marker = it },
|
||||||
modifier = Modifier.menuAnchor().fillMaxWidth()
|
label = { Text(stringResource(R.string.lab_marker)) },
|
||||||
)
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = markerExpanded) },
|
||||||
ExposedDropdownMenu(expanded = markerExpanded, onDismissRequest = { markerExpanded = false }) {
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
markerSuggestions.forEach { s ->
|
)
|
||||||
DropdownMenuItem(text = { Text(s) }, onClick = { marker = s; markerExpanded = false })
|
ExposedDropdownMenu(expanded = markerExpanded, onDismissRequest = { markerExpanded = false }) {
|
||||||
|
markerSuggestions.forEach { s ->
|
||||||
|
DropdownMenuItem(text = { Text(s) }, onClick = { marker = s; markerExpanded = false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
value = valueText,
|
|
||||||
onValueChange = { valueText = it },
|
|
||||||
label = { Text(stringResource(R.string.lab_value)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
ExposedDropdownMenuBox(expanded = unitExpanded, onExpandedChange = { unitExpanded = it }) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = unit,
|
value = valueText,
|
||||||
onValueChange = { unit = it },
|
onValueChange = { valueText = it },
|
||||||
label = { Text(stringResource(R.string.lab_unit)) },
|
label = { Text(stringResource(R.string.lab_value)) },
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = unitExpanded) },
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.menuAnchor().fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(expanded = unitExpanded, onDismissRequest = { unitExpanded = false }) {
|
ExposedDropdownMenuBox(expanded = unitExpanded, onExpandedChange = { unitExpanded = it }) {
|
||||||
unitSuggestions.forEach { s ->
|
OutlinedTextField(
|
||||||
DropdownMenuItem(text = { Text(s) }, onClick = { unit = s; unitExpanded = false })
|
value = unit,
|
||||||
|
onValueChange = { unit = it },
|
||||||
|
label = { Text(stringResource(R.string.lab_unit)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = unitExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = unitExpanded, onDismissRequest = { unitExpanded = false }) {
|
||||||
|
unitSuggestions.forEach { s ->
|
||||||
|
DropdownMenuItem(text = { Text(s) }, onClick = { unit = s; unitExpanded = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Création : E2 et T ensemble, chacune optionnelle
|
||||||
|
OutlinedTextField(
|
||||||
|
value = e2Text,
|
||||||
|
onValueChange = { e2Text = it },
|
||||||
|
label = { Text(stringResource(R.string.lab_e2_value)) },
|
||||||
|
supportingText = { Text(stringResource(R.string.lab_optional)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenuBox(expanded = e2UnitExpanded, onExpandedChange = { e2UnitExpanded = it }) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = e2Unit,
|
||||||
|
onValueChange = { e2Unit = it },
|
||||||
|
label = { Text(stringResource(R.string.lab_e2_unit)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = e2UnitExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = e2UnitExpanded, onDismissRequest = { e2UnitExpanded = false }) {
|
||||||
|
unitSuggestions.forEach { s ->
|
||||||
|
DropdownMenuItem(text = { Text(s) }, onClick = { e2Unit = s; e2UnitExpanded = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
OutlinedTextField(
|
||||||
|
value = tText,
|
||||||
|
onValueChange = { tText = it },
|
||||||
|
label = { Text(stringResource(R.string.lab_t_value)) },
|
||||||
|
supportingText = { Text(stringResource(R.string.lab_optional)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenuBox(expanded = tUnitExpanded, onExpandedChange = { tUnitExpanded = it }) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = tUnit,
|
||||||
|
onValueChange = { tUnit = it },
|
||||||
|
label = { Text(stringResource(R.string.lab_t_unit)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = tUnitExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = tUnitExpanded, onDismissRequest = { tUnitExpanded = false }) {
|
||||||
|
unitSuggestions.forEach { s ->
|
||||||
|
DropdownMenuItem(text = { Text(s) }, onClick = { tUnit = s; tUnitExpanded = false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -168,7 +168,10 @@ fun ChartScreen() {
|
|||||||
allPoints.minOf { it.timestamp } to allPoints.maxOf { it.timestamp }
|
allPoints.minOf { it.timestamp } to allPoints.maxOf { it.timestamp }
|
||||||
} else 0L to 0L
|
} else 0L to 0L
|
||||||
val e2Labs = labResults.filter { it.marker.equals("E2", true) && it.timestamp in a..b }
|
val e2Labs = labResults.filter { it.marker.equals("E2", true) && it.timestamp in a..b }
|
||||||
val tLabs = labResults.filter { it.marker.equals("T", true) && it.timestamp in a..b }
|
// Le toggle T masque la courbe T ET les labs T (cohérence d'affichage)
|
||||||
|
val tLabs = if (showT) {
|
||||||
|
labResults.filter { it.marker.equals("T", true) && it.timestamp in a..b }
|
||||||
|
} else emptyList()
|
||||||
|
|
||||||
// Série visuelle : E2 Estrannaise = bleu plein, E2 TFS = turquoise plein ;
|
// Série visuelle : E2 Estrannaise = bleu plein, E2 TFS = turquoise plein ;
|
||||||
// T Estrannaise = rose plein, T TFS = rose pointillé.
|
// T Estrannaise = rose plein, T TFS = rose pointillé.
|
||||||
|
|||||||
@ -3,7 +3,6 @@ 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.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
|
||||||
@ -36,6 +35,7 @@ import com.hormonetrack.R
|
|||||||
import com.hormonetrack.data.model.LabResult
|
import com.hormonetrack.data.model.LabResult
|
||||||
import com.hormonetrack.ui.LocalAppContainer
|
import com.hormonetrack.ui.LocalAppContainer
|
||||||
import com.hormonetrack.ui.components.LabDialog
|
import com.hormonetrack.ui.components.LabDialog
|
||||||
|
import com.hormonetrack.ui.components.formatLabValue
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -43,6 +43,24 @@ import java.time.Instant
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regroupe les labs par timestamp (une « prise de sang » = E2 + T prises au même
|
||||||
|
* moment). Ordre d'affichage dans un groupe : E2 avant T, autres marqueurs ensuite.
|
||||||
|
* Retour : liste (timestamp, labs du groupe) triée par date décroissante.
|
||||||
|
*/
|
||||||
|
fun groupLabsForDisplay(labs: List<LabResult>): List<Pair<Long, List<LabResult>>> =
|
||||||
|
labs.groupBy { it.timestamp }
|
||||||
|
.map { (ts, group) ->
|
||||||
|
ts to group.sortedBy { lab ->
|
||||||
|
when (lab.marker.uppercase()) {
|
||||||
|
"E2" -> 0
|
||||||
|
"T" -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedByDescending { it.first }
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LabsScreen() {
|
fun LabsScreen() {
|
||||||
@ -52,11 +70,12 @@ fun LabsScreen() {
|
|||||||
|
|
||||||
var showLabDialog by remember { mutableStateOf(false) }
|
var showLabDialog by remember { mutableStateOf(false) }
|
||||||
var toEdit by remember { mutableStateOf<LabResult?>(null) }
|
var toEdit by remember { mutableStateOf<LabResult?>(null) }
|
||||||
var toDelete by remember { mutableStateOf<LabResult?>(null) }
|
// Groupe en cours de suppression (une prise de sang entière)
|
||||||
|
var toDeleteGroup by remember { mutableStateOf<List<LabResult>?>(null) }
|
||||||
|
// Choix du marqueur à éditer quand le groupe contient E2 + T
|
||||||
|
var chooserGroup by remember { mutableStateOf<List<LabResult>?>(null) }
|
||||||
|
|
||||||
val grouped = remember(labs) {
|
val grouped = remember(labs) { groupLabsForDisplay(labs) }
|
||||||
labs.sortedBy { it.marker.lowercase() }.groupBy { it.marker.uppercase() }
|
|
||||||
}
|
|
||||||
val fmt = remember {
|
val fmt = remember {
|
||||||
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(ZoneId.systemDefault())
|
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(ZoneId.systemDefault())
|
||||||
}
|
}
|
||||||
@ -72,92 +91,128 @@ fun LabsScreen() {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.nav_labs)) }
|
title = { Text(stringResource(R.string.nav_labs)) }
|
||||||
)
|
)
|
||||||
if (labs.isEmpty()) {
|
if (grouped.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.no_data),
|
stringResource(R.string.no_data),
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
grouped.forEach { (marker, results) ->
|
items(grouped, key = { it.first }) { (ts, group) ->
|
||||||
item(key = "header_$marker") {
|
// Affichage côte à côte : « E2 306 pg/mL · T 45 ng/dL »
|
||||||
Text(
|
val headline = group.joinToString(" · ") { lab ->
|
||||||
marker,
|
"${lab.marker.uppercase()} ${formatLabValue(lab.value)} ${lab.unit}"
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
items(results, key = { it.id }) { lab ->
|
ListItem(
|
||||||
ListItem(
|
modifier = Modifier.clickable {
|
||||||
modifier = Modifier.clickable { toEdit = lab },
|
if (group.size > 1) chooserGroup = group else toEdit = group.first()
|
||||||
headlineContent = {
|
},
|
||||||
Text("${lab.value} ${lab.unit}")
|
headlineContent = { Text(headline) },
|
||||||
},
|
supportingContent = {
|
||||||
supportingContent = {
|
Column {
|
||||||
Column {
|
Text(fmt.format(Instant.ofEpochMilli(ts)))
|
||||||
Text(fmt.format(Instant.ofEpochMilli(lab.timestamp)))
|
// Notes du groupe (toutes identiques en pratique)
|
||||||
lab.notes?.takeIf { it.isNotBlank() }?.let {
|
group.firstNotNullOfOrNull { it.notes }?.takeIf { it.isNotBlank() }?.let {
|
||||||
Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
IconButton(onClick = { toDelete = lab }) {
|
|
||||||
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
}
|
trailingContent = {
|
||||||
|
IconButton(onClick = { toDeleteGroup = group }) {
|
||||||
|
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showLabDialog || toEdit != null) {
|
if (showLabDialog) {
|
||||||
LabDialog(
|
LabDialog(
|
||||||
existing = toEdit,
|
existing = null,
|
||||||
onDismiss = {
|
onDismiss = { showLabDialog = false },
|
||||||
showLabDialog = false
|
onConfirmCreate = { e2, t, ts, notes ->
|
||||||
toEdit = null
|
|
||||||
},
|
|
||||||
onConfirmCreate = { marker, value, unit, ts, notes ->
|
|
||||||
showLabDialog = false
|
showLabDialog = false
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repo.insertLabResult(
|
e2?.let {
|
||||||
LabResult(
|
repo.insertLabResult(
|
||||||
marker = marker,
|
LabResult(marker = "E2", value = it.value, unit = it.unit, timestamp = ts, notes = notes.ifBlank { null })
|
||||||
value = value,
|
|
||||||
unit = unit,
|
|
||||||
timestamp = ts,
|
|
||||||
notes = notes.ifBlank { null }
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
t?.let {
|
||||||
|
repo.insertLabResult(
|
||||||
|
LabResult(marker = "T", value = it.value, unit = it.unit, timestamp = ts, notes = notes.ifBlank { null })
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onConfirmEdit = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Édition d'UNE entrée du groupe (choix E2 / T si paire)
|
||||||
|
toEdit?.let { lab ->
|
||||||
|
LabDialog(
|
||||||
|
existing = lab,
|
||||||
|
onDismiss = { toEdit = null },
|
||||||
|
onConfirmCreate = { _, _, _, _ -> },
|
||||||
onConfirmEdit = { updated ->
|
onConfirmEdit = { updated ->
|
||||||
toEdit = null
|
toEdit = null
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch { repo.updateLabResult(updated) }
|
||||||
repo.updateLabResult(updated)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélecteur quand la prise contient E2 + T
|
||||||
|
chooserGroup?.let { group ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { chooserGroup = null },
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { chooserGroup = null }) { Text(stringResource(R.string.cancel)) }
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.edit_which_lab)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
group.forEach { lab ->
|
||||||
|
TextButton(onClick = {
|
||||||
|
chooserGroup = null
|
||||||
|
toEdit = lab
|
||||||
|
}) {
|
||||||
|
Text("${lab.marker.uppercase()} · ${formatLabValue(lab.value)} ${lab.unit}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
toDelete?.let { lab ->
|
// Suppression : la prise de sang ENTIÈRE (les labs sont pris ensemble)
|
||||||
|
toDeleteGroup?.let { group ->
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { toDelete = null },
|
onDismissRequest = { toDeleteGroup = null },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
toDelete = null
|
toDeleteGroup = null
|
||||||
CoroutineScope(Dispatchers.IO).launch { repo.deleteLabResult(lab) }
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
group.forEach { repo.deleteLabResult(it) }
|
||||||
|
}
|
||||||
}) { Text(stringResource(R.string.delete)) }
|
}) { Text(stringResource(R.string.delete)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { toDelete = null }) { Text(stringResource(R.string.cancel)) }
|
TextButton(onClick = { toDeleteGroup = 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_blood_draw,
|
||||||
|
group.joinToString(" · ") { "${it.marker.uppercase()} ${formatLabValue(it.value)}" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,8 +51,15 @@
|
|||||||
<string name="confirm_delete_treatment">Supprimer ce traitement ? Son historique de doses sera aussi supprimé.</string>
|
<string name="confirm_delete_treatment">Supprimer ce traitement ? Son historique de doses sera aussi supprimé.</string>
|
||||||
|
|
||||||
<!-- Labs -->
|
<!-- Labs -->
|
||||||
<string name="add_lab">Ajouter un résultat</string>
|
<string name="add_lab">Ajouter une prise de sang</string>
|
||||||
<string name="edit_lab">Modifier le résultat</string>
|
<string name="edit_lab">Modifier le résultat</string>
|
||||||
|
<string name="lab_e2_value">E2 — valeur œstradiol</string>
|
||||||
|
<string name="lab_e2_unit">Unité E2</string>
|
||||||
|
<string name="lab_t_value">T — valeur testostérone</string>
|
||||||
|
<string name="lab_t_unit">Unité T</string>
|
||||||
|
<string name="lab_optional">optionnel</string>
|
||||||
|
<string name="edit_which_lab">Quelle entrée veux-tu modifier ?</string>
|
||||||
|
<string name="confirm_delete_blood_draw">Supprimer cette prise de sang (%1$s) ?</string>
|
||||||
<string name="lab_marker">Marqueur (ex : E2, T)</string>
|
<string name="lab_marker">Marqueur (ex : E2, T)</string>
|
||||||
<string name="lab_value">Valeur</string>
|
<string name="lab_value">Valeur</string>
|
||||||
<string name="lab_unit">Unité (ex : pg/mL, ng/mL)</string>
|
<string name="lab_unit">Unité (ex : pg/mL, ng/mL)</string>
|
||||||
|
|||||||
@ -51,8 +51,15 @@
|
|||||||
<string name="confirm_delete_treatment">Delete this treatment? Its dose history will also be deleted.</string>
|
<string name="confirm_delete_treatment">Delete this treatment? Its dose history will also be deleted.</string>
|
||||||
|
|
||||||
<!-- Labs -->
|
<!-- Labs -->
|
||||||
<string name="add_lab">Add lab result</string>
|
<string name="add_lab">Add a blood test</string>
|
||||||
<string name="edit_lab">Edit lab result</string>
|
<string name="edit_lab">Edit lab result</string>
|
||||||
|
<string name="lab_e2_value">E2 — estradiol value</string>
|
||||||
|
<string name="lab_e2_unit">E2 unit</string>
|
||||||
|
<string name="lab_t_value">T — testosterone value</string>
|
||||||
|
<string name="lab_t_unit">T unit</string>
|
||||||
|
<string name="lab_optional">optional</string>
|
||||||
|
<string name="edit_which_lab">Which entry do you want to edit?</string>
|
||||||
|
<string name="confirm_delete_blood_draw">Delete this blood draw (%1$s)?</string>
|
||||||
<string name="lab_marker">Marker (e.g. E2, T)</string>
|
<string name="lab_marker">Marker (e.g. E2, T)</string>
|
||||||
<string name="lab_value">Value</string>
|
<string name="lab_value">Value</string>
|
||||||
<string name="lab_unit">Unit (e.g. pg/mL, ng/mL)</string>
|
<string name="lab_unit">Unit (e.g. pg/mL, ng/mL)</string>
|
||||||
|
|||||||
56
app/src/test/java/com/hormonetrack/pk/LabsGroupingTest.kt
Normal file
56
app/src/test/java/com/hormonetrack/pk/LabsGroupingTest.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package com.hormonetrack.pk
|
||||||
|
|
||||||
|
import com.hormonetrack.data.model.LabResult
|
||||||
|
import com.hormonetrack.ui.screens.groupLabsForDisplay
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regroupement de l'écran Analyses (v1.2.2) : une prise de sang (E2 + T au même
|
||||||
|
* timestamp) s'affiche côte à côte ; E2 avant T ; tri chronologique décroissant.
|
||||||
|
*/
|
||||||
|
class LabsGroupingTest {
|
||||||
|
|
||||||
|
private fun lab(marker: String, ts: Long, value: Double) =
|
||||||
|
LabResult(marker = marker, value = value, unit = if (marker == "E2") "pg/mL" else "ng/mL", timestamp = ts)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `E2 and T at the same timestamp are grouped into one blood draw`() {
|
||||||
|
val ts = 1_788_621_894_346L
|
||||||
|
val labs = listOf(lab("E2", ts, 306.0), lab("T", ts, 45.0))
|
||||||
|
val groups = groupLabsForDisplay(labs)
|
||||||
|
assertEquals(1, groups.size)
|
||||||
|
assertEquals(ts, groups[0].first)
|
||||||
|
assertEquals(listOf("E2", "T"), groups[0].second.map { it.marker.uppercase() })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `different timestamps stay separate rows`() {
|
||||||
|
val labs = listOf(
|
||||||
|
lab("E2", 1000L, 306.0),
|
||||||
|
lab("T", 2000L, 45.0)
|
||||||
|
)
|
||||||
|
val groups = groupLabsForDisplay(labs)
|
||||||
|
assertEquals(2, groups.size)
|
||||||
|
// Tri décroissant : le plus récent (2000) d'abord
|
||||||
|
assertEquals(2000L, groups[0].first)
|
||||||
|
assertEquals(1000L, groups[1].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `labs entered alone also group correctly and E2 sorts before T`() {
|
||||||
|
val ts = 5000L
|
||||||
|
val labs = listOf(lab("T", ts, 45.0), lab("E2", ts, 306.0))
|
||||||
|
val groups = groupLabsForDisplay(labs)
|
||||||
|
assertEquals(1, groups.size)
|
||||||
|
assertEquals(listOf("E2", "T"), groups[0].second.map { it.marker.uppercase() })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `other markers sort after E2 and T`() {
|
||||||
|
val ts = 6000L
|
||||||
|
val labs = listOf(lab("PRL", ts, 12.0), lab("T", ts, 45.0), lab("E2", ts, 306.0))
|
||||||
|
val groups = groupLabsForDisplay(labs)
|
||||||
|
assertEquals(listOf("E2", "T", "PRL"), groups[0].second.map { it.marker.uppercase() })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,22 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
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.2.1`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.2`, …).
|
||||||
|
|
||||||
|
## [1.2.2] — 2026-09-05 (versionCode 5)
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Prise de sang E2 + T en UNE entrée** : le dialog d'ajout propose les deux
|
||||||
|
marqueurs (chacun **optionnel** — on peut ne saisir que E2 ou que T), avec
|
||||||
|
date/heure et notes partagées ; les entrées sont insérées au même timestamp et
|
||||||
|
s'affichent **côte à côte** dans l'écran Analyses (« E2 306 pg/mL · T 44 ng/dL »).
|
||||||
|
- **Tap sur une paire → sélecteur** (« Quelle entrée veux-tu modifier ? » E2 / T)
|
||||||
|
puis édition pré-remplie de l'entrée choisie.
|
||||||
|
- **Suppression par prise de sang** : la corbeille supprime la prise entière
|
||||||
|
(confirmation listant les valeurs).
|
||||||
|
- **Toggle T du graphique** : masque désormais aussi les **labs T** (avant : seule
|
||||||
|
la courbe T disparaissait, les points restaient).
|
||||||
|
- 4 tests de regroupement (48 au total).
|
||||||
|
|
||||||
## [1.2.1] — 2026-09-05 (versionCode 4)
|
## [1.2.1] — 2026-09-05 (versionCode 4)
|
||||||
|
|
||||||
|
|||||||
@ -297,7 +297,7 @@ Type de retour : `LevelPoint(timestamp, e2, t)`.
|
|||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**44 tests JVM, tous verts** (`./gradlew testDebugUnitTest`). Dépendance : JUnit 4.13.2.
|
**48 tests JVM, tous verts** (`./gradlew testDebugUnitTest`). 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 =
|
||||||
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
|
`app/` → l'asset est lu via `src/main/assets/pk_profiles.json` (fallback `app/src/…`).
|
||||||
|
|
||||||
@ -334,6 +334,9 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex
|
|||||||
non calibré ≈ 367 = les « 375 » rapportés), facteur unique EEN plausible, lab T en
|
non calibré ≈ 367 = les « 375 » rapportés), facteur unique EEN plausible, lab T en
|
||||||
unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente.
|
unité aberrante neutralisé, prévision 6 j exacte, auto-cal cohérente.
|
||||||
**Tout nouvel export utilisateur = un nouveau test de régression.**
|
**Tout nouvel export utilisateur = un nouveau test de régression.**
|
||||||
|
- **`LabsGroupingTest`** (4) : v1.2.2 — regroupement de l'écran Analyses (paire E2+T
|
||||||
|
même timestamp ; timestamps différents séparés ; tri E2 avant T avant autres ;
|
||||||
|
ordre chronologique décroissant).
|
||||||
|
|
||||||
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
||||||
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
||||||
@ -379,13 +382,18 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
|||||||
superposées (E2 ESE bleu plein, E2 TFS turquoise, T ESE rose plein, T TFS rose
|
superposées (E2 ESE bleu plein, E2 TFS turquoise, T ESE rose plein, T TFS rose
|
||||||
pointillé) ; **chip Prévision** (doses projetées via `generateForecastDoses`, horizon
|
pointillé) ; **chip Prévision** (doses projetées via `generateForecastDoses`, horizon
|
||||||
= 2× le plus grand intervalle configuré, borné 7–30 j) ; **auto-calibration** branchée
|
= 2× le plus grand intervalle configuré, borné 7–30 j) ; **auto-calibration** branchée
|
||||||
sur les Paramètres ; légende dynamique ; labs T normalisés en ng/mL
|
sur les Paramètres ; légende dynamique ; labs T normalisés en ng/mL ; **toggle T =
|
||||||
|
masque aussi les labs T** (v1.2.2)
|
||||||
- `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**
|
||||||
- `LabsScreen` : groupée par marqueur, FAB → `LabDialog` (création), **tap sur la ligne →
|
- `LabsScreen` (v1.2.2) : **prise de sang E2 + T en une entrée** (LabDialog create :
|
||||||
édition** (v1.2.1, même mécanique que les doses), suppression avec confirmation ;
|
deux sections optionnelles → 1 ou 2 LabResult au même timestamp) ; affichage
|
||||||
suggestions d'unités pg/mL, ng/mL, ng/dL, ng/L, nmol/L, mIU/L
|
**groupé par timestamp** (`groupLabsForDisplay` : E2 avant T, autres ensuite, tri
|
||||||
|
desc) → « E2 306 pg/mL · T 44 ng/dL » côte à côte ; tap → sélecteur E2/T si paire,
|
||||||
|
édition unitaire pré-remplie ; suppression = **la prise entière** (confirm nommant
|
||||||
|
les valeurs — choix de design : les labs sont prélevés ensemble) ; suggestions
|
||||||
|
d'unités pg/mL, ng/mL, ng/dL, ng/L, nmol/L, mIU/L
|
||||||
- `TreatmentsScreen` : cartes (nom, route, dose, chips ester·modèle / Tmax / ×scale / ⏰,
|
- `TreatmentsScreen` : cartes (nom, route, dose, chips ester·modèle / Tmax / ×scale / ⏰,
|
||||||
badge inactif), FAB → éditeur
|
badge inactif), FAB → éditeur
|
||||||
- `TreatmentEditorScreen` : 12 presets (`PKPresets`, cf `nameRes`) pré-remplissent tout ;
|
- `TreatmentEditorScreen` : 12 presets (`PKPresets`, cf `nameRes`) pré-remplissent tout ;
|
||||||
@ -632,4 +640,4 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
- [ ] Tester l'installation d'une watchface `.hwt` via Gadgetbridge (pour la Phase 2)
|
- [ ] Tester l'installation d'une watchface `.hwt` via Gadgetbridge (pour la Phase 2)
|
||||||
|
|
||||||
---
|
---
|
||||||
*Doc mise à jour le 5 sept. 2026 (v1.2.1) — build OK, 44/44 tests verts, repo git avec tags v1.1.0/v1.2.0/v1.2.1.*
|
*Doc mise à jour le 5 sept. 2026 (v1.2.2) — build OK, 48/48 tests verts, repo git avec tags v1.1.0→v1.2.2.*
|
||||||
|
|||||||
@ -65,10 +65,14 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
|
|||||||
- **Modifier une dose existante** : appuie simplement sur sa ligne dans l'écran Doses
|
- **Modifier une dose existante** : appuie simplement sur sa ligne dans l'écran Doses
|
||||||
(traitement, dose, date/heure, notes et ester tout ça éditable) — pas besoin de
|
(traitement, dose, date/heure, notes et ester tout ça éditable) — pas besoin de
|
||||||
supprimer/recréer
|
supprimer/recréer
|
||||||
3. **Analyses → +** → entre tes prises de sang (E2 en pg/mL, T en ng/mL ou ng/dL)
|
3. **Analyses → +** → une prise de sang complète en une entrée : **E2 et/ou T**
|
||||||
- **Modifier un résultat** : appuie simplement sur sa ligne (même principe que les doses)
|
(chacune optionnelle), date/heure commune, notes — les deux s'affichent côte à côte
|
||||||
|
dans la liste
|
||||||
|
- **Modifier** : appuie sur la ligne ; si la prise contient E2 **et** T, un sélecteur
|
||||||
|
te demande laquelle modifier
|
||||||
|
- **Supprimer** : la corbeille retire la prise entière (E2 + T ensemble)
|
||||||
- Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la
|
- Choisis bien l'unité : elle est convertie automatiquement pour l'affichage et la
|
||||||
calibration (ng/mL, ng/dL, ng/L, nmol/L)
|
calibration (E2 en pg/mL ; T en ng/mL, ng/dL, ng/L, nmol/L)
|
||||||
4. **Calibration** (dans l'édition d'un traitement E2) → « Calibrer avec les analyses »
|
4. **Calibration** (dans l'édition d'un traitement E2) → « Calibrer avec les analyses »
|
||||||
→ calcule le facteur d'échelle = médiane(lab ÷ prédiction), comme le « Scale factor » du `.ods`
|
→ calcule le facteur d'échelle = médiane(lab ÷ prédiction), comme le « Scale factor » du `.ods`
|
||||||
5. **Paramètres** :
|
5. **Paramètres** :
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user