Kroppsvikt: datumväljare i dialogen, statistiken laddar om, graf-fix samma dag
All checks were successful
release / build-release (push) Successful in 5m15s
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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") }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user