Kroppsvikt: datumväljare i dialogen, statistiken laddar om, graf-fix samma dag
All checks were successful
release / build-release (push) Successful in 5m15s

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
This commit is contained in:
2026-07-25 18:14:14 +02:00
parent e2ffb4f011
commit 143abee871
4 changed files with 79 additions and 14 deletions

View File

@@ -183,11 +183,16 @@ class GymRepository(
} }
/** Ny mätning. Faller tillbaka på gamla updateBodyWeight mot äldre API. */ /** 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 { try {
client.execute( client.execute(
"mutation(\$input: AddBodyMeasurementInput!){addBodyMeasurement(input:\$input){id}}", "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(), auth.bearerToken(),
) )
} catch (e: GraphQlException) { } catch (e: GraphQlException) {

View File

@@ -66,10 +66,10 @@ class ProfileViewModel(
} }
} }
fun addWeight(weightKg: Double) { fun addWeight(weightKg: Double, dateIso: String?) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repo.addBodyMeasurement(weightKg) repo.addBodyMeasurement(weightKg, dateIso)
message.value = "Vikten sparad" message.value = "Vikten sparad"
load() load()
} catch (e: Exception) { } 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 { viewModelScope.launch {
try { try {
repo.updateBodyMeasurement(id, weightKg, null) repo.updateBodyMeasurement(id, weightKg, dateIso)
load() load()
} catch (e: Exception) { } catch (e: Exception) {
message.value = "Kunde inte uppdatera — offline?" message.value = "Kunde inte uppdatera — offline?"
@@ -129,8 +129,9 @@ fun ProfileScreen(
WeightDialog( WeightDialog(
title = "Uppdatera kroppsvikt", title = "Uppdatera kroppsvikt",
initial = profile?.bodyWeightKg, initial = profile?.bodyWeightKg,
onSave = { weight -> initialDate = java.time.LocalDate.now(),
viewModel.addWeight(weight) onSave = { weight, dateIso ->
viewModel.addWeight(weight, dateIso)
showAddWeight = false showAddWeight = false
}, },
onDismiss = { showAddWeight = false }, onDismiss = { showAddWeight = false },
@@ -139,10 +140,12 @@ fun ProfileScreen(
editTarget?.let { m -> editTarget?.let { m ->
WeightDialog( WeightDialog(
title = "Rätta mätning ${m.date.take(10)}", title = "Rätta mätning",
initial = m.weightKg, initial = m.weightKg,
onSave = { weight -> initialDate = runCatching { java.time.LocalDate.parse(m.date.take(10)) }
viewModel.updateMeasurement(m.id, weight) .getOrElse { java.time.LocalDate.now() },
onSave = { weight, dateIso ->
viewModel.updateMeasurement(m.id, weight, dateIso)
editTarget = null editTarget = null
}, },
onDelete = { onDelete = {
@@ -257,19 +260,47 @@ fun ProfileScreen(
} }
} }
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
@Composable @Composable
private fun WeightDialog( private fun WeightDialog(
title: String, title: String,
initial: Double?, initial: Double?,
onSave: (Double) -> Unit, initialDate: java.time.LocalDate,
onSave: (Double, String?) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onDelete: (() -> Unit)? = null, onDelete: (() -> Unit)? = null,
) { ) {
var text by remember { var text by remember {
mutableStateOf(initial?.let { if (it % 1.0 == 0.0) it.toInt().toString() else it.toString() } ?: "") 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() 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( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(title) }, title = { Text(title) },
@@ -283,6 +314,12 @@ private fun WeightDialog(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
androidx.compose.material3.FilledTonalButton(
onClick = { showDatePicker = true },
modifier = Modifier.fillMaxWidth(),
) {
Text("Datum: $date")
}
onDelete?.let { onDelete?.let {
TextButton(onClick = it) { TextButton(onClick = it) {
Text("Ta bort mätningen", color = MaterialTheme.colorScheme.error) Text("Ta bort mätningen", color = MaterialTheme.colorScheme.error)
@@ -292,7 +329,14 @@ private fun WeightDialog(
}, },
confirmButton = { confirmButton = {
Button( 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, enabled = parsed != null && parsed > 0,
) { Text("Spara") } ) { Text("Spara") }
}, },

View File

@@ -61,6 +61,10 @@ class StatsViewModel(private val repo: GymRepository) : ViewModel() {
init { init {
load() load()
refreshMeasurements()
}
fun refreshMeasurements() {
viewModelScope.launch { viewModelScope.launch {
runCatching { bodyMeasurements.value = repo.fetchBodyMeasurements() } runCatching { bodyMeasurements.value = repo.fetchBodyMeasurements() }
} }
@@ -95,6 +99,12 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val period by viewModel.period.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( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),

View File

@@ -26,13 +26,19 @@ fun WeightGraph(
pointColor: Color, pointColor: Color,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val points = measurements.mapNotNull { m -> var points = measurements.mapNotNull { m ->
runCatching { runCatching {
OffsetDateTime.parse(m.date).toInstant().toEpochMilli().toFloat() to m.weightKg.toFloat() OffsetDateTime.parse(m.date).toInstant().toEpochMilli().toFloat() to m.weightKg.toFloat()
}.getOrNull() }.getOrNull()
}.sortedBy { it.first } }.sortedBy { it.first }
if (points.size < 2) return 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 minX = points.first().first
val maxX = points.last().first val maxX = points.last().first
val minW = points.minOf { it.second } val minW = points.minOf { it.second }