Today: "Ta alle nå" bulk-take for a cluster of overdue doses

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 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-06-17 10:49:23 +02:00
commit 68d5ccb80c

View file

@ -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.