mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(medications): advance the next dose past already-resolved slots
The card's next dose did not move on after a dose was logged: the server's next-due and the open-cycle descriptor both picked the next slot purely by clock time, ignoring whether that slot was already taken, skipped, or missed. For a twice-daily med whose remaining slots today were all logged, the card kept surfacing a present/past resolved slot instead of advancing to the next genuinely-open one. Skip resolved slots in both paths: computeNextDueAt and buildCurrentCycle now walk occurrences forward and return the first slot no intake has resolved (taken / skipped / auto-missed), so a med whose slots today are all logged advances to tomorrow's first open slot. The list and detail routes read the acted-on slots near now and thread them in; both walks are bounded so a logged-ahead history cannot spin. The card's compliance/next-dose queries already ride the ["medications"] invalidation prefix, so logging a dose refreshes the card promptly once the server value advances.
This commit is contained in:
1 parent
311ac65f81
commit
faeb770cbb
5 files changed
+271
-35
No files matched your search
@@ -64,6 +64,27 @@ export const GET = apiHandler(
|
||||
select: { takenAt: true },
|
||||
})
|
||||
: null;
|
||||
// v1.15.10 — slots the user has already acted on near now, so the
|
||||
// next-due search skips them and advances to the next genuinely-open
|
||||
// slot. Bound the read to [now-1d, now+2d] — the lookahead only needs the
|
||||
// slots adjacent to now.
|
||||
const now = new Date();
|
||||
const resolvedFrom = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const resolvedTo = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
const resolvedRows = await prisma.medicationIntakeEvent.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
medicationId: id,
|
||||
deletedAt: null,
|
||||
scheduledFor: { gte: resolvedFrom, lte: resolvedTo },
|
||||
OR: [
|
||||
{ takenAt: { not: null } },
|
||||
{ skipped: true },
|
||||
{ autoMissed: true },
|
||||
],
|
||||
},
|
||||
select: { scheduledFor: true },
|
||||
});
|
||||
const nextDue = computeNextDueAt({
|
||||
medication: {
|
||||
id: medication.id,
|
||||
@@ -73,9 +94,10 @@ export const GET = apiHandler(
|
||||
createdAt: medication.createdAt,
|
||||
},
|
||||
schedules: medication.schedules,
|
||||
now: new Date(),
|
||||
now,
|
||||
userTz: user.timezone || "Europe/Berlin",
|
||||
lastIntakeAt: lastIntake?.takenAt ?? null,
|
||||
resolvedSlots: resolvedRows.map((r) => r.scheduledFor),
|
||||
});
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -32,29 +32,63 @@ async function buildMedicationsList(
|
||||
userTz,
|
||||
);
|
||||
|
||||
const [medications, latestIntakes, todayEvents] = await Promise.all([
|
||||
prisma.medication.findMany({
|
||||
where: { userId },
|
||||
include: { schedules: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.medicationIntakeEvent.groupBy({
|
||||
by: ["medicationId"],
|
||||
// v1.7.0 sync — exclude tombstoned rows from the last-taken map.
|
||||
where: { userId, deletedAt: null, skipped: false, takenAt: { not: null } },
|
||||
_max: { takenAt: true },
|
||||
}),
|
||||
prisma.medicationIntakeEvent.groupBy({
|
||||
by: ["medicationId"],
|
||||
// v1.7.0 sync — exclude tombstoned rows from the today-count map.
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
scheduledFor: { gte: todayStartUtc, lte: todayEndUtc },
|
||||
},
|
||||
_count: { id: true },
|
||||
}),
|
||||
]);
|
||||
// v1.15.10 — the slots the user has already acted on near "now", so the
|
||||
// next-due search can skip them. A resolved slot is a taken, deliberately
|
||||
// skipped, or cron-auto-missed row. Bound the window to [today-start,
|
||||
// today-end + 2d] — the next-due lookahead only needs the slots adjacent to
|
||||
// now, and a tight window keeps the read cheap. The 60s list cache covers
|
||||
// the rest.
|
||||
const resolvedWindowEnd = new Date(todayEndUtc.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const [medications, latestIntakes, todayEvents, resolvedEvents] =
|
||||
await Promise.all([
|
||||
prisma.medication.findMany({
|
||||
where: { userId },
|
||||
include: { schedules: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.medicationIntakeEvent.groupBy({
|
||||
by: ["medicationId"],
|
||||
// v1.7.0 sync — exclude tombstoned rows from the last-taken map.
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
skipped: false,
|
||||
takenAt: { not: null },
|
||||
},
|
||||
_max: { takenAt: true },
|
||||
}),
|
||||
prisma.medicationIntakeEvent.groupBy({
|
||||
by: ["medicationId"],
|
||||
// v1.7.0 sync — exclude tombstoned rows from the today-count map.
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
scheduledFor: { gte: todayStartUtc, lte: todayEndUtc },
|
||||
},
|
||||
_count: { id: true },
|
||||
}),
|
||||
prisma.medicationIntakeEvent.findMany({
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
scheduledFor: { gte: todayStartUtc, lte: resolvedWindowEnd },
|
||||
OR: [
|
||||
{ takenAt: { not: null } },
|
||||
{ skipped: true },
|
||||
{ autoMissed: true },
|
||||
],
|
||||
},
|
||||
select: { medicationId: true, scheduledFor: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const resolvedSlotsByMedId = new Map<string, Date[]>();
|
||||
for (const e of resolvedEvents) {
|
||||
const list = resolvedSlotsByMedId.get(e.medicationId);
|
||||
if (list) list.push(e.scheduledFor);
|
||||
else resolvedSlotsByMedId.set(e.medicationId, [e.scheduledFor]);
|
||||
}
|
||||
|
||||
const lastTakenAtByMedicationId = Object.fromEntries(
|
||||
latestIntakes.map((entry) => [
|
||||
@@ -101,6 +135,7 @@ async function buildMedicationsList(
|
||||
now,
|
||||
userTz,
|
||||
lastIntakeAt: lastTakenAtDateByMedicationId[m.id] ?? null,
|
||||
resolvedSlots: resolvedSlotsByMedId.get(m.id) ?? [],
|
||||
});
|
||||
return {
|
||||
...m,
|
||||
|
||||
Reference in new issue
Block a user