From 68d5ccb80c978a76565fb3ea3cec338a53627298 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 17 Jun 2026 10:49:23 +0200 Subject: [PATCH] =?UTF-8?q?Today:=20"Ta=20alle=20n=C3=A5"=20bulk-take=20fo?= =?UTF-8?q?r=20a=20cluster=20of=20overdue=20doses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When two or more doses are due now (overdue and unresolved), show one button that takes them all in a single tap — the common case of a morning handful. It loops over DoseActions.ensureLog + take, the same path each cards "Ta nå" uses, so inventory decrement and escalation cancel stay consistent; take() is idempotent so a stale row cannot double-decrement. Scoped to due-now only, so a later dose cannot be taken early by accident. Co-Authored-By: Claude Opus 4.8 --- .../no/naiv/meddetsamme/ui/TodayScreen.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/no/naiv/meddetsamme/ui/TodayScreen.kt b/app/src/main/java/no/naiv/meddetsamme/ui/TodayScreen.kt index a1265e6..a58c9da 100644 --- a/app/src/main/java/no/naiv/meddetsamme/ui/TodayScreen.kt +++ b/app/src/main/java/no/naiv/meddetsamme/ui/TodayScreen.kt @@ -69,6 +69,10 @@ private data class TodayDose( ) { val status: DoseStatus? get() = log?.status val atMillis: Long get() = at.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + val resolved: Boolean get() = status == DoseStatus.TAKEN || status == DoseStatus.SKIPPED + + /** Due now = unresolved and its scheduled time has passed (what escalation nags about). */ + fun isDueNow(now: LocalDateTime): Boolean = !resolved && at.isBefore(now) } @OptIn(ExperimentalMaterial3Api::class) @@ -211,6 +215,25 @@ fun TodayScreen(nav: Nav) { } } } + // Bulk "take now" for a cluster of overdue doses (e.g. a morning + // handful). Same path as each card's "Ta nå", just looped — take() + // is idempotent so a stale entry can't double-decrement stock. + val dueNow = doses.filter { it.isDueNow(LocalDateTime.now()) } + if (dueNow.size >= 2) { + item { + FilledTonalButton( + onClick = { + scope.launch { + for (d in dueNow) { + val log = actions.ensureLog(d.doseTime.id, d.atMillis) + actions.take(log.id) + } + } + }, + modifier = Modifier.fillMaxWidth(), + ) { Text("Ta alle nå (${dueNow.size})") } + } + } items(doses, key = { "${it.doseTime.id}-${it.atMillis}" }) { dose -> DoseCard( dose = dose, @@ -248,8 +271,8 @@ private fun DoseCard( ) { val time = "%02d:%02d".format(dose.doseTime.minuteOfDay / 60, dose.doseTime.minuteOfDay % 60) val statusText = dose.status?.label - val resolved = dose.status == DoseStatus.TAKEN || dose.status == DoseStatus.SKIPPED - val overdue = !resolved && dose.at.isBefore(LocalDateTime.now()) + val resolved = dose.resolved + val overdue = dose.isDueNow(LocalDateTime.now()) val scheme = MaterialTheme.colorScheme // Visual state: overdue screams a little, resolved fades back, upcoming is calm.