mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(medications): record scheduled doses automatically
This commit is contained in:
1 parent
5919a93c29
commit
c94e109bf0
37 files changed
+1105
-15
No files matched your search
@@ -8,6 +8,7 @@ import { adminReminderCheckSchema } from "@/lib/validations/notifications";
|
||||
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
|
||||
import { dispatchLocalisedNotification } from "@/lib/notifications/dispatch-localised";
|
||||
import { getUserTodayBounds, getDayOfWeekInTz } from "@/lib/tz/local-day";
|
||||
import { sendsMedicationReminders } from "@/lib/medications/auto-mark-taken";
|
||||
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
|
||||
@@ -182,7 +183,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const isOverdue = dayMatch && minutesPastEnd > 0;
|
||||
const hasEvent = eventCount > schedulesProcessed;
|
||||
|
||||
if (isOverdue && !hasEvent && med.notificationsEnabled) {
|
||||
// v1.42.2 — a medication whose doses are recorded automatically
|
||||
// is never reminded, here or in the worker.
|
||||
if (isOverdue && !hasEvent && sendsMedicationReminders(med)) {
|
||||
const doseInfo = schedule.dose ?? med.dose;
|
||||
const timeWindow = `${schedule.windowStart}–${schedule.windowEnd}`;
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ interface SerializedDoseHistoryRow {
|
||||
takenAt: string | null;
|
||||
skipped: boolean;
|
||||
autoMissed: boolean;
|
||||
/** v1.42.2 — recorded automatically (`autoMarkTaken`). */
|
||||
autoRecorded: boolean;
|
||||
/** v1.16.4 — per-intake dose override; null = configured dose. */
|
||||
doseTaken: string | null;
|
||||
/** v1.32.8 (iOS #64) — how the dose was recorded; null on legacy rows. */
|
||||
@@ -179,6 +181,8 @@ export const GET = apiHandler(
|
||||
// v1.32.8 (iOS #64) — write provenance so the client can label how
|
||||
// each dose was recorded (Web / API / Reminder / Import / AppleHealth).
|
||||
source: true,
|
||||
// v1.42.2 — the history labels an automatically recorded dose.
|
||||
autoRecorded: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -191,6 +195,7 @@ export const GET = apiHandler(
|
||||
pinned: e.attributionSource === "USER_PIN",
|
||||
doseTaken: e.doseTaken,
|
||||
source: e.source,
|
||||
autoRecorded: e.autoRecorded,
|
||||
}));
|
||||
|
||||
const lastIntakeAt = lastNonSkippedTakenAt(mapped);
|
||||
@@ -296,6 +301,7 @@ export const GET = apiHandler(
|
||||
pinned: e.pinned,
|
||||
doseTaken: e.doseTaken,
|
||||
source: e.source,
|
||||
autoRecorded: e.autoRecorded,
|
||||
}));
|
||||
|
||||
const rows = reconstructDoseHistory(
|
||||
@@ -327,6 +333,7 @@ export const GET = apiHandler(
|
||||
autoMissed: row.intake.autoMissed ?? false,
|
||||
doseTaken: row.intake.doseTaken ?? null,
|
||||
source: row.intake.source ?? null,
|
||||
autoRecorded: row.intake.autoRecorded ?? false,
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
|
||||
@@ -252,6 +252,10 @@ export const PUT = apiHandler(
|
||||
// onto the slot the row already sits on, or an unpin whose band
|
||||
// re-attribution lands on the same anchor).
|
||||
...(attributionSource !== undefined && { attributionSource }),
|
||||
// v1.42.2 — an edit of the outcome is the person's own record.
|
||||
...((data.takenAt !== undefined || data.skipped !== undefined) && {
|
||||
autoRecorded: false,
|
||||
}),
|
||||
// v1.7.0 sync — bump the reconciliation counter on every
|
||||
// server-side mutation so the `/api/sync/changes` feed echoes a
|
||||
// monotonic value to paired clients.
|
||||
|
||||
@@ -310,6 +310,8 @@ export const PUT = apiHandler(
|
||||
asNeeded: true,
|
||||
// v1.39.1 (#1033) — the tracking transition below needs the before.
|
||||
trackIntake: true,
|
||||
// v1.42.2 — `autoMarkTakenSince` is stamped only on an off→on flip.
|
||||
autoMarkTaken: true,
|
||||
// v1.37.0 — the pre-image the audit row carries (C4).
|
||||
name: true,
|
||||
dose: true,
|
||||
@@ -407,6 +409,7 @@ export const PUT = apiHandler(
|
||||
oneShot,
|
||||
asNeeded,
|
||||
trackIntake,
|
||||
autoMarkTaken,
|
||||
reminderGraceMinutes: topLevelGraceMinutes,
|
||||
} = input;
|
||||
|
||||
@@ -962,6 +965,15 @@ export const PUT = apiHandler(
|
||||
...(asNeeded !== undefined && { asNeeded }),
|
||||
// v1.39.1 (#1033) — intake tracking, field-by-field.
|
||||
...(trackIntake !== undefined && { trackIntake }),
|
||||
// v1.42.2 — auto-mark-taken, field-by-field. Only an off→on flip
|
||||
// stamps `autoMarkTakenSince`, so re-saving the form with the
|
||||
// option already on never moves the floor; switching it off
|
||||
// clears the stamp.
|
||||
...(autoMarkTaken !== undefined &&
|
||||
autoMarkTaken !== existing.autoMarkTaken && {
|
||||
autoMarkTaken,
|
||||
autoMarkTakenSince: autoMarkTaken ? new Date() : null,
|
||||
}),
|
||||
...(normalisedSchedules && {
|
||||
schedules: {
|
||||
create: normalisedSchedules.map((n) => n.createData),
|
||||
|
||||
@@ -438,6 +438,8 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
data: {
|
||||
takenAt: resolvedTakenAt,
|
||||
skipped: false,
|
||||
// v1.42.2 — the person confirmed it; no longer automatic.
|
||||
autoRecorded: false,
|
||||
syncVersion: { increment: 1 },
|
||||
// v1.8.5 — persist the resolved site on the taken branch.
|
||||
...(resolvedInjectionSite !== null && {
|
||||
@@ -525,7 +527,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
updated = await prisma.medicationIntakeEvent.update({
|
||||
where: { id: intakeId },
|
||||
// v1.7.0 sync — bump the reconciliation counter on the skip toggle.
|
||||
data: { takenAt: null, skipped: true, syncVersion: { increment: 1 } },
|
||||
// v1.42.2 — a skip the person records clears the automatic marker.
|
||||
data: {
|
||||
takenAt: null,
|
||||
skipped: true,
|
||||
autoRecorded: false,
|
||||
syncVersion: { increment: 1 },
|
||||
},
|
||||
});
|
||||
// v1.16.10 — the row stopped being taken; refund whatever its
|
||||
// consumption stamp recorded (no-op for a never-consumed row).
|
||||
|
||||
@@ -189,6 +189,7 @@ async function postMedication(request: NextRequest): Promise<Response> {
|
||||
oneShot,
|
||||
asNeeded,
|
||||
trackIntake,
|
||||
autoMarkTaken,
|
||||
} = parsed.data;
|
||||
|
||||
// v1.40 (#1041) — a custom category must be one of this record's own.
|
||||
@@ -378,6 +379,12 @@ async function postMedication(request: NextRequest): Promise<Response> {
|
||||
// the schedule is stored as a record from the start; the column
|
||||
// default (true) covers every caller that does not send it.
|
||||
...(trackIntake !== undefined && { trackIntake }),
|
||||
// v1.42.2 — auto-mark-taken, field-by-field. Created on, the
|
||||
// auto-take job fills slots from this moment only.
|
||||
...(autoMarkTaken !== undefined && {
|
||||
autoMarkTaken,
|
||||
autoMarkTakenSince: autoMarkTaken ? new Date() : null,
|
||||
}),
|
||||
schedules: {
|
||||
create: scheduleInputs.map((s) => {
|
||||
// Invariant 2 — default to FREQ=DAILY when nothing else is set.
|
||||
|
||||
Reference in new issue
Block a user