Seasonal (yearly) dosing for allergy/pollen meds
Add a recurring month-day season window to DoseTime (seasonStartMmdd/ seasonEndMmdd): a med rests outside its window and re-arms automatically every year. The pure ScheduleEngine gains a season filter plus one-year overrides -- startEpochDay/endEpochDay act as early-start/extend brackets for seasonal rows while staying hard bounds for tapers. - Daily SupplyCheck nudges before a season opens (Start naa) and before it closes (Forleng), handled by the new SeasonActionReceiver. - Med list lists resting seasonal meds in a "Sesong (hviler)" section. - Dose-time editor gains a season block and a "scroll for more" hint so cyclic/season below the time picker are discoverable. - Room migration v3->v4 (additive), backup DTO extended, engine/text tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b4c929eca2
commit
d81a4fd926
15 changed files with 1098 additions and 103 deletions
|
|
@ -4,11 +4,13 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import no.naiv.meddetsamme.data.MedDatabase
|
||||
import no.naiv.meddetsamme.domain.ScheduleEngine
|
||||
import no.naiv.meddetsamme.domain.ScheduleText
|
||||
import no.naiv.meddetsamme.notify.Notifications
|
||||
|
||||
/**
|
||||
|
|
@ -33,6 +35,12 @@ class SupplyCheckReceiver : BroadcastReceiver() {
|
|||
private companion object {
|
||||
/** "About a week in advance" for prescription renewal. */
|
||||
const val RX_LEAD_DAYS = 7
|
||||
|
||||
/** How early to nudge before a resting seasonal med's window opens. */
|
||||
const val SEASON_START_LEAD_DAYS = 7L
|
||||
|
||||
/** How early to offer "Forleng" before the current season closes. */
|
||||
const val SEASON_END_LEAD_DAYS = 5L
|
||||
}
|
||||
|
||||
private suspend fun check(context: Context) {
|
||||
|
|
@ -41,16 +49,19 @@ class SupplyCheckReceiver : BroadcastReceiver() {
|
|||
val lowStock = mutableListOf<String>()
|
||||
val rxRenewal = mutableListOf<String>()
|
||||
|
||||
val meds = db.medicationDao().getActive()
|
||||
// Dose-times feed both the supply rate and the season check; fetch once.
|
||||
val dtsByMed = meds.associate { it.med.id to db.doseTimeDao().getForMed(it.med.id) }
|
||||
|
||||
// Per inventory item: consumption is the SUM across all active regimens
|
||||
// drawing from it — one shared box drains faster than either alone.
|
||||
val byItem = db.medicationDao().getActive().groupBy { it.item.id }
|
||||
for ((_, meds) in byItem) {
|
||||
val item = meds.first().item
|
||||
for ((_, group) in meds.groupBy { it.item.id }) {
|
||||
val item = group.first().item
|
||||
var rate = 0.0
|
||||
for (med in meds) rate += ScheduleEngine.dailyConsumption(db.doseTimeDao().getForMed(med.med.id), today)
|
||||
for (med in group) rate += ScheduleEngine.dailyConsumption(dtsByMed[med.med.id].orEmpty(), today)
|
||||
if (ScheduleEngine.needsRefill(item.stockUnits, rate, item.lowStockLeadDays)) {
|
||||
val days = ScheduleEngine.daysOfSupply(item.stockUnits, rate)
|
||||
lowStock += "${item.name}: lager for ${no.naiv.meddetsamme.domain.ScheduleText.daysCount(days?.toInt() ?: 0)}"
|
||||
lowStock += "${item.name}: lager for ${ScheduleText.daysCount(days?.toInt() ?: 0)}"
|
||||
}
|
||||
if (ScheduleEngine.needsRenewal(item.rxExpiryEpochDay, today, RX_LEAD_DAYS)) {
|
||||
val expired = item.rxExpiryEpochDay!! < today.toEpochDay()
|
||||
|
|
@ -58,6 +69,8 @@ class SupplyCheckReceiver : BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
|
||||
for (med in meds) checkSeason(context, med.displayName, med.med.id, dtsByMed[med.med.id].orEmpty(), today)
|
||||
|
||||
// Post-or-cancel: cancelling when the condition clears is what lets the
|
||||
// ongoing rx notification disappear by itself after a renewal.
|
||||
if (lowStock.isNotEmpty()) {
|
||||
|
|
@ -79,4 +92,55 @@ class SupplyCheckReceiver : BroadcastReceiver() {
|
|||
Notifications.cancel(context, Notifications.RX_NOTIFICATION_ID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-med seasonal nudge, post-or-cancel like the digests above:
|
||||
* - resting and within [SEASON_START_LEAD_DAYS] of the next window → "Start nå";
|
||||
* - in season and within [SEASON_END_LEAD_DAYS] of its close → "Forleng".
|
||||
* Acting on either (or the season simply passing) clears it the next day.
|
||||
*/
|
||||
private fun checkSeason(
|
||||
context: Context,
|
||||
name: String,
|
||||
medId: Long,
|
||||
doseTimes: List<no.naiv.meddetsamme.data.DoseTime>,
|
||||
today: LocalDate,
|
||||
) {
|
||||
val notifId = Notifications.seasonNotificationId(medId)
|
||||
if (!ScheduleEngine.isSeasonal(doseTimes)) {
|
||||
Notifications.cancel(context, notifId)
|
||||
return
|
||||
}
|
||||
if (!ScheduleEngine.inSeasonNow(doseTimes, today)) {
|
||||
val start = ScheduleEngine.nextSeasonStart(doseTimes, today)
|
||||
val days = start?.let { ChronoUnit.DAYS.between(today, it) }
|
||||
if (start != null && days != null && days in 0..SEASON_START_LEAD_DAYS) {
|
||||
Notifications.post(
|
||||
context, notifId,
|
||||
Notifications.buildSeasonNotification(
|
||||
context, medId, name,
|
||||
"Sesongen nærmer seg (fra ${ScheduleText.dateLabel(start)}). Vil du starte nå?",
|
||||
starting = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Notifications.cancel(context, notifId)
|
||||
}
|
||||
} else {
|
||||
val end = ScheduleEngine.currentSeasonEnd(doseTimes, today)
|
||||
val days = end?.let { ChronoUnit.DAYS.between(today, it) }
|
||||
if (end != null && days != null && days in 0..SEASON_END_LEAD_DAYS) {
|
||||
Notifications.post(
|
||||
context, notifId,
|
||||
Notifications.buildSeasonNotification(
|
||||
context, medId, name,
|
||||
"Sesongen er snart over (${ScheduleText.dateLabel(end)}). Fortsatt plaget? Forleng to uker.",
|
||||
starting = false,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Notifications.cancel(context, notifId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue