From 143abee87133195a9ed61272c97ea99b7aea8336 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Jul 2026 18:14:14 +0200 Subject: [PATCH] =?UTF-8?q?Kroppsvikt:=20datumv=C3=A4ljare=20i=20dialogen,?= =?UTF-8?q?=20statistiken=20laddar=20om,=20graf-fix=20samma=20dag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vikt-dialogen har datumknapp (kalender) för både ny mätning och rättning — mätningar kan flyttas till rätt dag (skickas som 12:00 UTC så datumet inte glider över tidszoner) - Statistikfliken hämtar om mätningarna varje gång den öppnas - Grafen sprider mätningar inom samma dygn på index istället för att kollapsa till en punkt Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP --- .../fitnessdroid/data/GymRepository.kt | 9 ++- .../fitnessdroid/ui/profile/ProfileScreen.kt | 66 +++++++++++++++---- .../fitnessdroid/ui/stats/StatsScreen.kt | 10 +++ .../fitnessdroid/ui/stats/WeightGraph.kt | 8 ++- 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt index 916d08d..04972c6 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt @@ -183,11 +183,16 @@ class GymRepository( } /** Ny mätning. Faller tillbaka på gamla updateBodyWeight mot äldre API. */ - suspend fun addBodyMeasurement(weightKg: Double) { + suspend fun addBodyMeasurement(weightKg: Double, dateIso: String? = null) { try { client.execute( "mutation(\$input: AddBodyMeasurementInput!){addBodyMeasurement(input:\$input){id}}", - buildJsonObject { put("input", buildJsonObject { put("weightKg", weightKg) }) }, + buildJsonObject { + put("input", buildJsonObject { + put("weightKg", weightKg) + dateIso?.let { put("date", it) } + }) + }, auth.bearerToken(), ) } catch (e: GraphQlException) { diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt index 70ad2d0..b6d16fb 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt @@ -66,10 +66,10 @@ class ProfileViewModel( } } - fun addWeight(weightKg: Double) { + fun addWeight(weightKg: Double, dateIso: String?) { viewModelScope.launch { try { - repo.addBodyMeasurement(weightKg) + repo.addBodyMeasurement(weightKg, dateIso) message.value = "Vikten sparad" load() } catch (e: Exception) { @@ -78,10 +78,10 @@ class ProfileViewModel( } } - fun updateMeasurement(id: String, weightKg: Double) { + fun updateMeasurement(id: String, weightKg: Double, dateIso: String?) { viewModelScope.launch { try { - repo.updateBodyMeasurement(id, weightKg, null) + repo.updateBodyMeasurement(id, weightKg, dateIso) load() } catch (e: Exception) { message.value = "Kunde inte uppdatera — offline?" @@ -129,8 +129,9 @@ fun ProfileScreen( WeightDialog( title = "Uppdatera kroppsvikt", initial = profile?.bodyWeightKg, - onSave = { weight -> - viewModel.addWeight(weight) + initialDate = java.time.LocalDate.now(), + onSave = { weight, dateIso -> + viewModel.addWeight(weight, dateIso) showAddWeight = false }, onDismiss = { showAddWeight = false }, @@ -139,10 +140,12 @@ fun ProfileScreen( editTarget?.let { m -> WeightDialog( - title = "Rätta mätning ${m.date.take(10)}", + title = "Rätta mätning", initial = m.weightKg, - onSave = { weight -> - viewModel.updateMeasurement(m.id, weight) + initialDate = runCatching { java.time.LocalDate.parse(m.date.take(10)) } + .getOrElse { java.time.LocalDate.now() }, + onSave = { weight, dateIso -> + viewModel.updateMeasurement(m.id, weight, dateIso) editTarget = null }, onDelete = { @@ -257,19 +260,47 @@ fun ProfileScreen( } } +@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) @Composable private fun WeightDialog( title: String, initial: Double?, - onSave: (Double) -> Unit, + initialDate: java.time.LocalDate, + onSave: (Double, String?) -> Unit, onDismiss: () -> Unit, onDelete: (() -> Unit)? = null, ) { var text by remember { mutableStateOf(initial?.let { if (it % 1.0 == 0.0) it.toInt().toString() else it.toString() } ?: "") } + var date by remember { mutableStateOf(initialDate) } + var showDatePicker by remember { mutableStateOf(false) } val parsed = text.trim().replace(',', '.').toDoubleOrNull() + if (showDatePicker) { + val dateState = androidx.compose.material3.rememberDatePickerState( + initialSelectedDateMillis = date.atStartOfDay(java.time.ZoneOffset.UTC) + .toInstant().toEpochMilli(), + ) + androidx.compose.material3.DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + TextButton(onClick = { + dateState.selectedDateMillis?.let { picked -> + date = java.time.Instant.ofEpochMilli(picked) + .atZone(java.time.ZoneOffset.UTC).toLocalDate() + } + showDatePicker = false + }) { Text("OK") } + }, + dismissButton = { + TextButton(onClick = { showDatePicker = false }) { Text("Avbryt") } + }, + ) { + androidx.compose.material3.DatePicker(state = dateState) + } + } + AlertDialog( onDismissRequest = onDismiss, title = { Text(title) }, @@ -283,6 +314,12 @@ private fun WeightDialog( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), modifier = Modifier.fillMaxWidth(), ) + androidx.compose.material3.FilledTonalButton( + onClick = { showDatePicker = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Datum: $date") + } onDelete?.let { TextButton(onClick = it) { Text("Ta bort mätningen", color = MaterialTheme.colorScheme.error) @@ -292,7 +329,14 @@ private fun WeightDialog( }, confirmButton = { Button( - onClick = { parsed?.let(onSave) }, + onClick = { + parsed?.let { weight -> + // Mitt på dagen UTC så datumet inte glider en dag i någon tidszon + val iso = date.atTime(12, 0).atOffset(java.time.ZoneOffset.UTC) + .toInstant().toString() + onSave(weight, iso) + } + }, enabled = parsed != null && parsed > 0, ) { Text("Spara") } }, diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt index 4cf6926..d4cfd5b 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt @@ -61,6 +61,10 @@ class StatsViewModel(private val repo: GymRepository) : ViewModel() { init { load() + refreshMeasurements() + } + + fun refreshMeasurements() { viewModelScope.launch { runCatching { bodyMeasurements.value = repo.fetchBodyMeasurements() } } @@ -95,6 +99,12 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F val uiState by viewModel.uiState.collectAsStateWithLifecycle() val period by viewModel.period.collectAsStateWithLifecycle() + // Hämta om mätningarna varje gång fliken öppnas — ny vikt kan ha + // lagts in via profilen sedan sist. + androidx.compose.runtime.LaunchedEffect(Unit) { + viewModel.refreshMeasurements() + } + LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/WeightGraph.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/WeightGraph.kt index 7d4d9ce..fbe39b1 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/WeightGraph.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/WeightGraph.kt @@ -26,13 +26,19 @@ fun WeightGraph( pointColor: Color, modifier: Modifier = Modifier, ) { - val points = measurements.mapNotNull { m -> + var points = measurements.mapNotNull { m -> runCatching { OffsetDateTime.parse(m.date).toInstant().toEpochMilli().toFloat() to m.weightKg.toFloat() }.getOrNull() }.sortedBy { it.first } if (points.size < 2) return + // Alla mätningar inom samma dygn? Sprid dem jämnt på index istället, + // annars kollapsar hela grafen till en punkt. + if (points.last().first - points.first().first < 86_400_000f) { + points = points.mapIndexed { i, (_, w) -> i.toFloat() to w } + } + val minX = points.first().first val maxX = points.last().first val minW = points.minOf { it.second }