mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(medications): read a pinned dose the same as the dose on its own
Automatic matching counts a dose taken up to an hour before a slot's on-time window as on time. A dose pinned to the slot was judged from the window's start instead, so the same dose read on time unpinned and late once pinned. A pinned dose now uses the same early grace, and the confirmation says so plainly: the dose counts as if it had been logged for the slot, on time or late depending on when it was taken. Refs #1028
This commit is contained in:
1 parent
eafef5adeb
commit
47c7c71802
9 files changed
+50
-35
No files matched your search
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "Zuordnung gelöst",
|
||||
"dueContext": "fällig gewesen: {time} ({delta})",
|
||||
"confirmTitle": "Slot {time} zuordnen?",
|
||||
"confirmBody": "Die Einnahme zählt für diesen Slot: pünktlich, wenn sie im Zeitfenster des Slots lag, sonst als verspätet. Die Quote wird also nicht geschönt. Die Zuordnung lässt sich jederzeit wieder lösen.",
|
||||
"confirmBody": "Die Einnahme zählt für diesen Slot genau so, als wäre sie dort erfasst worden: pünktlich oder verspätet, je nachdem, wann sie genommen wurde. Die Quote wird also nicht geschönt. Die Zuordnung lässt sich jederzeit wieder lösen.",
|
||||
"confirmCancel": "Abbrechen",
|
||||
"confirmAction": "Zuordnen"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "Attribution released",
|
||||
"dueContext": "was due: {time} ({delta})",
|
||||
"confirmTitle": "Attribute to slot {time}?",
|
||||
"confirmBody": "The intake counts for this slot: on time if it was taken inside the slot's on-time window, otherwise as late, so the rate is never flattered. You can release the attribution at any time.",
|
||||
"confirmBody": "The intake counts for this slot just as it would if it had been logged there: on time or late depending on when it was taken, so the rate is never flattered. You can release the attribution at any time.",
|
||||
"confirmCancel": "Cancel",
|
||||
"confirmAction": "Attribute"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "Asignación quitada",
|
||||
"dueContext": "prevista: {time} ({delta})",
|
||||
"confirmTitle": "¿Asignar al horario de las {time}?",
|
||||
"confirmBody": "La toma contará para este horario: a tiempo si se tomó dentro de su ventana puntual y, si no, con retraso, así que el porcentaje no se maquilla. Puedes quitar la asignación en cualquier momento.",
|
||||
"confirmBody": "La toma contará para este horario igual que si se hubiera registrado en él: a tiempo o con retraso según cuándo se tomó, así que el porcentaje no se maquilla. Puedes quitar la asignación en cualquier momento.",
|
||||
"confirmCancel": "Cancelar",
|
||||
"confirmAction": "Asignar"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "Prise détachée du créneau",
|
||||
"dueContext": "prévue à {time} ({delta})",
|
||||
"confirmTitle": "Rattacher au créneau de {time} ?",
|
||||
"confirmBody": "La prise comptera pour ce créneau : à l'heure si elle a eu lieu dans la fenêtre à l'heure du créneau, sinon en retard. Le taux n'est donc pas embelli. Le rattachement peut être retiré à tout moment.",
|
||||
"confirmBody": "La prise comptera pour ce créneau exactement comme si elle y avait été enregistrée : à l'heure ou en retard selon le moment où elle a été prise. Le taux n'est donc pas embelli. Le rattachement peut être retiré à tout moment.",
|
||||
"confirmCancel": "Annuler",
|
||||
"confirmAction": "Rattacher"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "Attribuzione rimossa",
|
||||
"dueContext": "prevista alle {time} ({delta})",
|
||||
"confirmTitle": "Attribuire allo slot delle {time}?",
|
||||
"confirmBody": "L'assunzione conterà per questo slot: puntuale se è avvenuta nella finestra puntuale dello slot, altrimenti in ritardo, quindi la percentuale non viene abbellita. L'attribuzione può essere rimossa in qualsiasi momento.",
|
||||
"confirmBody": "L'assunzione conterà per questo slot esattamente come se fosse stata registrata lì: puntuale o in ritardo a seconda di quando è stata presa, quindi la percentuale non viene abbellita. L'attribuzione può essere rimossa in qualsiasi momento.",
|
||||
"confirmCancel": "Annulla",
|
||||
"confirmAction": "Attribuisci"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "배정을 해제했어요",
|
||||
"dueContext": "예정 시각: {time} ({delta})",
|
||||
"confirmTitle": "{time} 시간대에 배정할까요?",
|
||||
"confirmBody": "이 복용은 해당 시간대로 계산돼요. 시간대의 정시 범위 안에 복용했다면 제시간, 아니면 늦게 복용한 것으로 계산되어 순응도가 부풀려지지 않아요. 배정은 언제든 해제할 수 있어요.",
|
||||
"confirmBody": "이 복용은 처음부터 이 시간대에 기록한 것과 똑같이 계산돼요. 복용한 시각에 따라 제시간 또는 늦게 복용한 것으로 계산되어 순응도가 부풀려지지 않아요. 배정은 언제든 해제할 수 있어요.",
|
||||
"confirmCancel": "취소",
|
||||
"confirmAction": "배정"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1795,7 +1795,7 @@
|
||||
"unpinnedToast": "Przypisanie usunięte",
|
||||
"dueContext": "termin: {time} ({delta})",
|
||||
"confirmTitle": "Przypisać do slotu {time}?",
|
||||
"confirmBody": "Dawka zostanie policzona dla tego slotu: jako przyjęta na czas, jeśli mieści się w oknie punktualności slotu, a w przeciwnym razie jako przyjęta z opóźnieniem, więc wskaźnik nie jest zawyżany. Przypisanie można w każdej chwili usunąć.",
|
||||
"confirmBody": "Dawka zostanie policzona dla tego slotu tak, jakby została w nim zapisana: jako przyjęta na czas lub z opóźnieniem, zależnie od tego, kiedy ją przyjęto, więc wskaźnik nie jest zawyżany. Przypisanie można w każdej chwili usunąć.",
|
||||
"confirmCancel": "Anuluj",
|
||||
"confirmAction": "Przypisz"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildSlotBands, type SlotWindowInput } from "../attribution";
|
||||
import {
|
||||
attributeIntakeToSlot,
|
||||
buildSlotBands,
|
||||
type SlotWindowInput,
|
||||
} from "../attribution";
|
||||
import {
|
||||
reconstructDoseHistory,
|
||||
suggestNearestSlot,
|
||||
@@ -284,28 +288,36 @@ describe("reconstructDoseHistory", () => {
|
||||
expect(morning?.pinned).toBe(true);
|
||||
});
|
||||
|
||||
// The pin confirm dialog promises exactly this split: on time inside the
|
||||
// slot's on-time window (also ahead of the slot's clock time), late
|
||||
// otherwise.
|
||||
it("an early pin inside the on-time window reads on time, one before it reads late", () => {
|
||||
const early = reconstructDoseHistory(
|
||||
bands,
|
||||
[intake({ takenAt: at(6, 15), scheduledFor: at(7, 0), pinned: true })],
|
||||
nowEvening,
|
||||
null,
|
||||
);
|
||||
expect(early.find((r) => r.timeOfDay === "07:00")?.status).toBe(
|
||||
"taken_on_time",
|
||||
);
|
||||
const tooEarly = reconstructDoseHistory(
|
||||
bands,
|
||||
[intake({ takenAt: at(5, 30), scheduledFor: at(7, 0), pinned: true })],
|
||||
nowEvening,
|
||||
null,
|
||||
);
|
||||
expect(tooEarly.find((r) => r.timeOfDay === "07:00")?.status).toBe(
|
||||
"taken_late",
|
||||
);
|
||||
// A pinned take reads exactly as the same take attributed on its own:
|
||||
// on time from the early grace before the on-time window through the
|
||||
// window's end, late otherwise. Pinning never changes the timing.
|
||||
it("a pinned early take reads the same as the unpinned take", () => {
|
||||
for (const [h, m] of [
|
||||
[6, 15],
|
||||
[5, 30],
|
||||
[4, 30],
|
||||
] as const) {
|
||||
const takenAt = at(h, m);
|
||||
const pinned = reconstructDoseHistory(
|
||||
bands,
|
||||
[intake({ takenAt, scheduledFor: at(7, 0), pinned: true })],
|
||||
nowEvening,
|
||||
null,
|
||||
).find((r) => r.timeOfDay === "07:00")?.status;
|
||||
const unpinned = attributeIntakeToSlot(takenAt, bands);
|
||||
const expected =
|
||||
unpinned?.band.timeOfDay === "07:00"
|
||||
? unpinned.status === "on_time"
|
||||
? "taken_on_time"
|
||||
: "taken_late"
|
||||
: // Outside every capture band on its own: a pin can only make
|
||||
// it late, never on time.
|
||||
"taken_late";
|
||||
expect(pinned, `${h}:${m}`).toBe(expected);
|
||||
}
|
||||
// 05:30 lies in the early grace (on time either way), 04:30 before it.
|
||||
expect(attributeIntakeToSlot(at(5, 30), bands)?.status).toBe("on_time");
|
||||
expect(attributeIntakeToSlot(at(4, 30), bands)).toBeNull();
|
||||
});
|
||||
|
||||
it("a pin inside the late tail reads taken_late, never flattered", () => {
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
* `attributionSource = USER_PIN`) binds by its `scheduledFor` anchor like
|
||||
* a skip, NOT by takenAt-band membership, so a pin outside the late tail
|
||||
* never degrades back to ad-hoc. Status is `taken_late` unless the
|
||||
* takenAt happens to sit inside the slot's on-time band — a pin can
|
||||
* never flatter the timing;
|
||||
* takenAt sits where the take would read on time on its own (the
|
||||
* on-time band plus its bounded early grace) — a pin never changes the
|
||||
* timing;
|
||||
* - a RELEASED pin (v1.16.0 — "Zuordnung lösen" persists `USER_PIN` with
|
||||
* `scheduledFor === takenAt`) is a deliberately ad-hoc take: it never
|
||||
* anchor-binds (not even when its instant sits within epsilon of a slot
|
||||
@@ -237,8 +238,9 @@ export function reconstructDoseHistory(
|
||||
// v1.15.20 — pinned takes bind by their stored slot anchor, NOT by
|
||||
// takenAt-band membership: the whole point of a pin is that the take sits
|
||||
// outside (or past the tail of) the band it belongs to. Status never
|
||||
// flatters: taken_late, unless the takenAt happens to sit inside the
|
||||
// slot's own on-time band anyway. A pin whose slot is gone (schedule
|
||||
// flatters: taken_late, unless the takenAt sits where band attribution
|
||||
// would read it on time anyway, the early grace included, so the same
|
||||
// take reads the same pinned or not. A pin whose slot is gone (schedule
|
||||
// changed) or already claimed falls through to ad-hoc so nothing vanishes.
|
||||
for (const i of pinnedTaken) {
|
||||
// v1.16.0 — a released pin ("Zuordnung lösen") persists USER_PIN with
|
||||
@@ -254,7 +256,8 @@ export function reconstructDoseHistory(
|
||||
if (band && !claim.has(band)) {
|
||||
const t = (i.takenAt as Date).getTime();
|
||||
const onTime =
|
||||
t >= band.onTimeStart.getTime() && t <= band.onTimeEnd.getTime();
|
||||
t >= (band.earlyStart ?? band.onTimeStart).getTime() &&
|
||||
t <= band.onTimeEnd.getTime();
|
||||
claim.set(band, {
|
||||
intake: i,
|
||||
status: onTime ? "taken_on_time" : "taken_late",
|
||||
|
||||
Reference in new issue
Block a user