fix: dose tracking broken for per-intake takenBy and after medication edits (#100)
- Remove broken isDoseFromPreviousSchedule that falsely dismissed all past doses after any medication edit (compared dateOnlyMs < updatedAt incorrectly) - Fix takenBy normalization in AppContext: event.takenBy (string|null) was passed through as-is via || operator instead of being properly converted to string[] - Fix DashboardPage: 5 locations treated dose.takenBy as single string instead of iterating the array, causing per-person dose tracking to silently fail - Extract isDoseDismissed and computeMissedPastDoseIds as pure testable functions from AppContext.tsx into utils/schedule.ts - Update SharedSchedule.tsx to use shared isDoseDismissed from utils - Add 22 regression tests covering isDoseDismissed, computeMissedPastDoseIds, and full dose-tracking-survives-medication-edit workflows - Add 'fix bugs, don't test around them' rule to copilot instructions
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
- **Clean workspace**: Always clean up after yourself. If you create a file for a specific task, delete it once done.
|
||||
- **Remove old code when re-implementing**: When fixing a bug or re-implementing a feature that didn't work, ALWAYS remove the old/broken code completely. Never leave dead code, unused functions, or obsolete implementations in the codebase.
|
||||
- **Tests are mandatory**: Every new feature and every bug fix MUST have corresponding tests. When modifying existing features, update or add tests accordingly. If old tests become obsolete due to code changes, remove or update them.
|
||||
- **Fix bugs, don't test around them**: If you discover incorrect behavior in the code while writing tests, ALWAYS fix the buggy code first, then write tests that verify the correct behavior. NEVER write tests that mimic or assert broken behavior. The user's time is finite and irreplaceable — every bug left unfixed wastes it.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useParams } from "react-router-dom";
|
||||
import type { ExpiredLinkData, SharedScheduleData } from "../types";
|
||||
import { getMedTotal } from "../types";
|
||||
import { getSystemLocale } from "../utils/formatters";
|
||||
import { isDoseDismissed } from "../utils/schedule";
|
||||
import { loadCollapsedDaysFromStorage } from "../utils/storage";
|
||||
import { MedicationAvatar } from "./MedicationAvatar";
|
||||
|
||||
@@ -395,7 +396,7 @@ export function SharedSchedule() {
|
||||
}, [data]);
|
||||
|
||||
// Helper to check if a dose date is on or before the dismissedUntil date
|
||||
function isDoseDismissed(doseTimestamp: number, medName: string): boolean {
|
||||
function isDoseDismissedByName(doseTimestamp: number, medName: string): boolean {
|
||||
const dismissedUntilDate = dismissedUntilByMed.get(medName);
|
||||
if (!dismissedUntilDate) return false;
|
||||
// Compare date strings (YYYY-MM-DD format sorts correctly)
|
||||
@@ -404,39 +405,6 @@ export function SharedSchedule() {
|
||||
return doseDateStr <= dismissedUntilDate;
|
||||
}
|
||||
|
||||
// Build a map of medication name -> updatedAt timestamp
|
||||
// Used to filter out doses from previous schedule configurations
|
||||
const updatedAtByMed = useMemo(() => {
|
||||
if (!data) return new Map<string, number>();
|
||||
const map = new Map<string, number>();
|
||||
for (const med of data.medications) {
|
||||
if (med.updatedAt) {
|
||||
const ts = typeof med.updatedAt === "number" ? med.updatedAt : new Date(med.updatedAt).getTime();
|
||||
if (!Number.isNaN(ts)) {
|
||||
map.set(med.name, ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [data]);
|
||||
|
||||
// Helper to check if a dose was scheduled BEFORE the medication was last updated
|
||||
// If so, it's from a previous schedule configuration and shouldn't count as "missed"
|
||||
// This matches the main app's isDoseFromPreviousSchedule logic in AppContext.tsx
|
||||
function isDoseFromPreviousSchedule(doseId: string, medName: string): boolean {
|
||||
const updatedAtTimestamp = updatedAtByMed.get(medName);
|
||||
if (!updatedAtTimestamp) return false; // No updatedAt means it was never changed, all doses are valid
|
||||
|
||||
// Extract timestamp from dose ID (format: medId-blisterIdx-timestamp or medId-blisterIdx-timestamp-person)
|
||||
const parts = doseId.split("-");
|
||||
if (parts.length < 3) return false;
|
||||
const doseTimestamp = parseInt(parts[2], 10);
|
||||
if (Number.isNaN(doseTimestamp)) return false;
|
||||
|
||||
// If the dose was scheduled before the medication was updated, it's from a previous schedule
|
||||
return doseTimestamp < updatedAtTimestamp;
|
||||
}
|
||||
|
||||
// Calculate coverage for stock status colors (matches main app logic)
|
||||
// This needs to account for taken doses and calculate depletion time
|
||||
const { coverageByMed, depletionByMed } = useMemo(() => {
|
||||
@@ -595,17 +563,12 @@ export function SharedSchedule() {
|
||||
const medId = parts[0];
|
||||
const med = data?.medications.find((m) => String(m.id) === medId);
|
||||
if (med) {
|
||||
const timestamp = parseInt(parts[2], 10);
|
||||
if (isDoseDismissed(timestamp, med.name)) {
|
||||
if (isDoseDismissed(id, med.dismissedUntil ?? undefined)) {
|
||||
return false; // dismissed = not missed
|
||||
}
|
||||
// Check if this dose is from a previous schedule configuration
|
||||
if (isDoseFromPreviousSchedule(id, med.name)) {
|
||||
return false; // from previous schedule = not missed
|
||||
}
|
||||
}
|
||||
}
|
||||
return true; // not taken, not dismissed, not from previous schedule = missed
|
||||
return true; // not taken, not dismissed = missed
|
||||
}).length;
|
||||
return (
|
||||
<div
|
||||
@@ -637,24 +600,19 @@ export function SharedSchedule() {
|
||||
{/* Past days (when expanded) */}
|
||||
{showPastDays &&
|
||||
pastDays.map((day) => {
|
||||
// Helper to check if a dose ID is "done" (taken, dismissed, or from previous schedule)
|
||||
// Checks: per-dose dismissed flag, medication-level dismissedUntil, and updatedAt
|
||||
// Helper to check if a dose ID is "done" (taken or dismissed)
|
||||
// Checks: per-dose dismissed flag and medication-level dismissedUntil
|
||||
const isDoseIdDone = (doseId: string) => {
|
||||
if (takenDoses.has(doseId)) return true;
|
||||
// Check if this dose is dismissed via per-dose flag from API
|
||||
if (dismissedDoses.has(doseId)) return true;
|
||||
// Check if dismissed via medication-level dismissedUntil date or from previous schedule
|
||||
// Check if dismissed via medication-level dismissedUntil date
|
||||
const parts = doseId.split("-");
|
||||
if (parts.length >= 3) {
|
||||
const medId = parts[0];
|
||||
const med = data?.medications.find((m) => String(m.id) === medId);
|
||||
if (med) {
|
||||
const timestamp = parseInt(parts[2], 10);
|
||||
if (isDoseDismissed(timestamp, med.name)) {
|
||||
return true;
|
||||
}
|
||||
// Check if this dose is from a previous schedule configuration
|
||||
if (isDoseFromPreviousSchedule(doseId, med.name)) {
|
||||
if (isDoseDismissed(doseId, med.dismissedUntil ?? undefined)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -753,13 +711,11 @@ export function SharedSchedule() {
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
{item.doses.map((dose) => {
|
||||
// Check: medication-level dismissedUntil, per-dose dismissed flag, and previous schedule
|
||||
const isMedLevelDismissed = isDoseDismissed(dose.when, dose.medName);
|
||||
const isFromPreviousSchedule = isDoseFromPreviousSchedule(dose.id, dose.medName);
|
||||
// Check: medication-level dismissedUntil and per-dose dismissed flag
|
||||
const isMedLevelDismissed = isDoseDismissedByName(dose.when, dose.medName);
|
||||
const isTaken = takenDoses.has(dose.id);
|
||||
const isPerDoseDismissed = dismissedDoses.has(dose.id);
|
||||
const isDone =
|
||||
isTaken || isPerDoseDismissed || isMedLevelDismissed || isFromPreviousSchedule;
|
||||
const isDone = isTaken || isPerDoseDismissed || isMedLevelDismissed;
|
||||
return (
|
||||
<div key={dose.id} className={`dose-item past ${isDone ? "all-taken" : ""}`}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuth } from "../components/Auth";
|
||||
import { useCollapsedDays, useDoses, useMedications, useRefill, useSettings, useShare } from "../hooks";
|
||||
import type { Coverage, Medication, ScheduleEvent, StockThresholds } from "../types";
|
||||
import { getSystemLocale } from "../utils/formatters";
|
||||
import { buildSchedulePreview, calculateCoverage } from "../utils/schedule";
|
||||
import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds, isDoseDismissed } from "../utils/schedule";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
@@ -370,7 +370,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
timeStr: event.timeStr,
|
||||
when: event.when,
|
||||
usage: event.usage,
|
||||
takenBy: event.takenBy || [],
|
||||
takenBy: event.takenBy ? [event.takenBy] : [],
|
||||
});
|
||||
medEntry.lastWhen = Math.max(medEntry.lastWhen, event.when);
|
||||
day.meds.set(event.medName, medEntry);
|
||||
@@ -412,83 +412,11 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
.slice(0, scheduleDays);
|
||||
}, [groupedSchedule, scheduleDays]);
|
||||
|
||||
// Build a map of medId -> dismissedUntil date string from medication records
|
||||
// This is robust against timestamp changes from schedule updates or timezone fixes
|
||||
const _dismissedUntilByMed = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const med of medications.meds) {
|
||||
if (med.dismissedUntil) {
|
||||
map.set(String(med.id), med.dismissedUntil);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [medications.meds]);
|
||||
|
||||
// Helper to check if a dose date is on or before the dismissedUntil date
|
||||
const isDoseDismissed = useCallback((doseId: string, dismissedUntilDate: string | undefined): boolean => {
|
||||
if (!dismissedUntilDate) return false;
|
||||
// Extract timestamp from dose ID (format: medId-blisterIdx-timestamp or medId-blisterIdx-timestamp-person)
|
||||
const parts = doseId.split("-");
|
||||
if (parts.length < 3) return false;
|
||||
const timestamp = parseInt(parts[2], 10);
|
||||
if (Number.isNaN(timestamp)) return false;
|
||||
// Compare date strings (YYYY-MM-DD format sorts correctly)
|
||||
const doseDate = new Date(timestamp);
|
||||
const doseDateStr = `${doseDate.getFullYear()}-${String(doseDate.getMonth() + 1).padStart(2, "0")}-${String(doseDate.getDate()).padStart(2, "0")}`;
|
||||
return doseDateStr <= dismissedUntilDate;
|
||||
}, []);
|
||||
|
||||
// Helper to check if a dose was scheduled BEFORE the medication was last updated
|
||||
// If so, it's from a previous schedule configuration and shouldn't count as "missed"
|
||||
const isDoseFromPreviousSchedule = useCallback(
|
||||
(doseId: string, medUpdatedAt: string | number | null | undefined): boolean => {
|
||||
if (!medUpdatedAt) return false; // No updatedAt means it was never changed, all doses are valid
|
||||
|
||||
// Extract timestamp from dose ID (format: medId-blisterIdx-timestamp or medId-blisterIdx-timestamp-person)
|
||||
const parts = doseId.split("-");
|
||||
if (parts.length < 3) return false;
|
||||
const doseTimestamp = parseInt(parts[2], 10);
|
||||
if (Number.isNaN(doseTimestamp)) return false;
|
||||
|
||||
// Convert updatedAt to timestamp
|
||||
const updatedAtTimestamp = typeof medUpdatedAt === "number" ? medUpdatedAt : new Date(medUpdatedAt).getTime();
|
||||
if (Number.isNaN(updatedAtTimestamp)) return false;
|
||||
|
||||
// If the dose was scheduled before the medication was updated, it's from a previous schedule
|
||||
return doseTimestamp < updatedAtTimestamp;
|
||||
},
|
||||
[]
|
||||
const missedPastDoseIds = useMemo(
|
||||
() => computeMissedPastDoseIds(pastDays, medications.meds, doses.takenDoses, doses.dismissedDoses),
|
||||
[pastDays, medications.meds, doses.takenDoses, doses.dismissedDoses]
|
||||
);
|
||||
|
||||
const missedPastDoseIds = useMemo(() => {
|
||||
const totalPastDoses = pastDays.flatMap((d) =>
|
||||
d.meds.flatMap((m) => {
|
||||
// Find the medication to get its dismissedUntil and updatedAt
|
||||
const med = medications.meds.find((med) => med.name === m.medName);
|
||||
const dismissedUntilDate = med?.dismissedUntil ?? undefined;
|
||||
const medUpdatedAt = med?.updatedAt;
|
||||
|
||||
return m.doses.flatMap((dose) => {
|
||||
// Check if this dose is on or before the dismissed date for this medication
|
||||
if (isDoseDismissed(dose.id, dismissedUntilDate)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if this dose is from a previous schedule configuration
|
||||
// (scheduled before the medication was last updated)
|
||||
if (isDoseFromPreviousSchedule(dose.id, medUpdatedAt)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const takenByArray = Array.isArray(dose.takenBy) ? dose.takenBy : [];
|
||||
return takenByArray.length > 0 ? takenByArray.map((p: string) => `${dose.id}-${p}`) : [dose.id];
|
||||
});
|
||||
})
|
||||
);
|
||||
// Also filter out doses that are marked as taken or individually dismissed (legacy)
|
||||
return totalPastDoses.filter((id) => !doses.takenDoses.has(id) && !doses.dismissedDoses.has(id));
|
||||
}, [pastDays, medications.meds, doses.takenDoses, doses.dismissedDoses, isDoseDismissed, isDoseFromPreviousSchedule]);
|
||||
|
||||
// Modal helpers with browser history support
|
||||
const openMedDetail = useCallback(
|
||||
(med: Medication) => {
|
||||
|
||||
@@ -617,7 +617,7 @@ export function DashboardPage() {
|
||||
<div className="doses-col">
|
||||
{item.doses.map((dose) => {
|
||||
// If no takenBy, show single checkbox; otherwise show one per person
|
||||
const people = dose.takenBy ? [dose.takenBy] : [null];
|
||||
const people = dose.takenBy.length > 0 ? dose.takenBy : [null];
|
||||
return (
|
||||
<div key={dose.id} className="dose-item past">
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
@@ -833,7 +833,9 @@ export function DashboardPage() {
|
||||
(() => {
|
||||
const totalFutureDoses = futureDays.flatMap((d) =>
|
||||
d.meds.flatMap((m) =>
|
||||
m.doses.flatMap((dose) => (dose.takenBy ? [`${dose.id}-${dose.takenBy}`] : [dose.id]))
|
||||
m.doses.flatMap((dose) =>
|
||||
dose.takenBy.length > 0 ? dose.takenBy.map((p) => `${dose.id}-${p}`) : [dose.id]
|
||||
)
|
||||
)
|
||||
);
|
||||
const takenFutureDoses = totalFutureDoses.filter((id) => takenDoses.has(id)).length;
|
||||
@@ -955,7 +957,7 @@ export function DashboardPage() {
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
{item.doses.map((dose) => {
|
||||
const people = dose.takenBy ? [dose.takenBy] : [null];
|
||||
const people = dose.takenBy.length > 0 ? dose.takenBy : [null];
|
||||
const allTaken = people.every((person) => takenDoses.has(getDoseId(dose.id, person)));
|
||||
return (
|
||||
<div key={dose.id} className={`dose-item future ${allTaken ? "all-taken" : ""}`}>
|
||||
|
||||
@@ -3,9 +3,11 @@ import type { Coverage, Medication, StockThresholds } from "../../types";
|
||||
import {
|
||||
buildSchedulePreview,
|
||||
calculateCoverage,
|
||||
computeMissedPastDoseIds,
|
||||
getNextReminderForMed,
|
||||
getReminderStatusText,
|
||||
getStockStatus,
|
||||
isDoseDismissed,
|
||||
} from "../../utils/schedule";
|
||||
|
||||
describe("buildSchedulePreview", () => {
|
||||
@@ -151,6 +153,80 @@ describe("buildSchedulePreview", () => {
|
||||
expect(result.events[i].when).toBeGreaterThanOrEqual(result.events[i - 1].when);
|
||||
}
|
||||
});
|
||||
|
||||
it("dose IDs remain stable when only intake time changes (regression test for dose tracking)", () => {
|
||||
// This is a critical regression test: changing the time-of-day for an intake
|
||||
// must NOT change dose IDs for past days, so that taken-dose tracking survives edits.
|
||||
const medsWithMorningTime: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const medsWithEveningTime: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T21:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const resultBefore = buildSchedulePreview(medsWithMorningTime, "en", true);
|
||||
const resultAfter = buildSchedulePreview(medsWithEveningTime, "en", true);
|
||||
|
||||
// Get past dose IDs from both schedules
|
||||
const pastIdsBefore = resultBefore.events.filter((e) => e.isPast).map((e) => e.id);
|
||||
const pastIdsAfter = resultAfter.events.filter((e) => e.isPast).map((e) => e.id);
|
||||
|
||||
expect(pastIdsBefore.length).toBeGreaterThan(0);
|
||||
// All past dose IDs must match — changing time must not break dose tracking
|
||||
expect(pastIdsAfter).toEqual(pastIdsBefore);
|
||||
});
|
||||
|
||||
it("dose IDs use date-only timestamp, not full datetime", () => {
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-14T15:30:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(meds, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
expect(pastEvents.length).toBeGreaterThan(0);
|
||||
|
||||
// Each dose ID should use a midnight timestamp (date-only), not the intake time
|
||||
for (const event of pastEvents) {
|
||||
const parts = event.id.split("-");
|
||||
const timestampMs = parseInt(parts[2], 10);
|
||||
const date = new Date(timestampMs);
|
||||
expect(date.getHours()).toBe(0);
|
||||
expect(date.getMinutes()).toBe(0);
|
||||
expect(date.getSeconds()).toBe(0);
|
||||
expect(date.getMilliseconds()).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateCoverage", () => {
|
||||
@@ -573,3 +649,501 @@ describe("getReminderStatusText", () => {
|
||||
expect(result.lines.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// isDoseDismissed
|
||||
// =============================================================================
|
||||
|
||||
describe("isDoseDismissed", () => {
|
||||
it("returns false when dismissedUntilDate is undefined", () => {
|
||||
expect(isDoseDismissed("1-0-1710028800000", undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for dose IDs with fewer than 3 parts", () => {
|
||||
expect(isDoseDismissed("1-0", "2024-03-15")).toBe(false);
|
||||
expect(isDoseDismissed("1", "2024-03-15")).toBe(false);
|
||||
expect(isDoseDismissed("", "2024-03-15")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for dose IDs with non-numeric timestamp", () => {
|
||||
expect(isDoseDismissed("1-0-abc", "2024-03-15")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when dose date is before dismissedUntil", () => {
|
||||
// March 10 midnight UTC = 1710028800000
|
||||
const march10midnight = new Date("2024-03-10T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march10midnight}`, "2024-03-14")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when dose date equals dismissedUntil", () => {
|
||||
const march14midnight = new Date("2024-03-14T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march14midnight}`, "2024-03-14")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when dose date is after dismissedUntil", () => {
|
||||
const march15midnight = new Date("2024-03-15T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march15midnight}`, "2024-03-14")).toBe(false);
|
||||
});
|
||||
|
||||
it("works with person-suffixed dose IDs", () => {
|
||||
// Dose ID: medId-intakeIdx-timestamp-person
|
||||
const march10midnight = new Date("2024-03-10T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${march10midnight}-John`, "2024-03-14")).toBe(true);
|
||||
expect(isDoseDismissed(`1-0-${march10midnight}-John`, "2024-03-09")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles single-digit months and days correctly", () => {
|
||||
// January 5 = should produce "2024-01-05"
|
||||
const jan5 = new Date("2024-01-05T00:00:00").getTime();
|
||||
expect(isDoseDismissed(`1-0-${jan5}`, "2024-01-05")).toBe(true);
|
||||
expect(isDoseDismissed(`1-0-${jan5}`, "2024-01-04")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// computeMissedPastDoseIds
|
||||
// =============================================================================
|
||||
|
||||
describe("computeMissedPastDoseIds", () => {
|
||||
// Helper: create a past day with dose IDs
|
||||
function makePastDay(
|
||||
medName: string,
|
||||
doses: Array<{ id: string; takenBy?: string[] }>
|
||||
): { meds: Array<{ medName: string; doses: Array<{ id: string; takenBy: string[] }> }> } {
|
||||
return {
|
||||
meds: [
|
||||
{
|
||||
medName,
|
||||
doses: doses.map((d) => ({ id: d.id, takenBy: d.takenBy ?? [] })),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
it("returns all past dose IDs when none are taken or dismissed", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }, { id: `1-0-${march11}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
expect(result).toEqual([`1-0-${march10}`, `1-0-${march11}`]);
|
||||
});
|
||||
|
||||
it("excludes taken doses", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }, { id: `1-0-${march11}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
const taken = new Set([`1-0-${march10}`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, taken, new Set());
|
||||
expect(result).toEqual([`1-0-${march11}`]);
|
||||
});
|
||||
|
||||
it("excludes individually dismissed doses", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }, { id: `1-0-${march11}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
const dismissed = new Set([`1-0-${march10}`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), dismissed);
|
||||
expect(result).toEqual([`1-0-${march11}`]);
|
||||
});
|
||||
|
||||
it("excludes doses on or before medication dismissedUntil date", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const march11 = new Date("2024-03-11T00:00:00").getTime();
|
||||
const march13 = new Date("2024-03-13T00:00:00").getTime();
|
||||
const march14 = new Date("2024-03-14T00:00:00").getTime();
|
||||
const pastDays = [
|
||||
makePastDay("Aspirin", [
|
||||
{ id: `1-0-${march10}` },
|
||||
{ id: `1-0-${march11}` },
|
||||
{ id: `1-0-${march13}` },
|
||||
{ id: `1-0-${march14}` },
|
||||
]),
|
||||
];
|
||||
// Dismiss all doses up through March 12
|
||||
const meds = [{ name: "Aspirin", dismissedUntil: "2024-03-12" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
// March 10 & 11 are dismissed (≤ March 12), March 13 & 14 remain missed
|
||||
expect(result).toEqual([`1-0-${march13}`, `1-0-${march14}`]);
|
||||
});
|
||||
|
||||
it("expands takenBy people into separate dose IDs", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("SharedMed", [{ id: `1-0-${march10}`, takenBy: ["Alice", "Bob"] }])];
|
||||
const meds = [{ name: "SharedMed" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
expect(result).toEqual([`1-0-${march10}-Alice`, `1-0-${march10}-Bob`]);
|
||||
});
|
||||
|
||||
it("excludes expanded person dose IDs that are taken", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("SharedMed", [{ id: `1-0-${march10}`, takenBy: ["Alice", "Bob"] }])];
|
||||
const meds = [{ name: "SharedMed" }];
|
||||
// Alice took it, Bob didn't
|
||||
const taken = new Set([`1-0-${march10}-Alice`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, taken, new Set());
|
||||
expect(result).toEqual([`1-0-${march10}-Bob`]);
|
||||
});
|
||||
|
||||
it("returns empty array when all doses are taken", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [makePastDay("Aspirin", [{ id: `1-0-${march10}` }])];
|
||||
const meds = [{ name: "Aspirin" }];
|
||||
const taken = new Set([`1-0-${march10}`]);
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, taken, new Set());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles multiple medications independently", () => {
|
||||
const march10 = new Date("2024-03-10T00:00:00").getTime();
|
||||
const pastDays = [
|
||||
{
|
||||
meds: [
|
||||
{ medName: "Aspirin", doses: [{ id: `1-0-${march10}`, takenBy: [] as string[] }] },
|
||||
{ medName: "Vitamin D", doses: [{ id: `2-0-${march10}`, takenBy: [] as string[] }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
// Aspirin dismissed, Vitamin D not
|
||||
const meds = [{ name: "Aspirin", dismissedUntil: "2024-03-15" }, { name: "Vitamin D" }];
|
||||
|
||||
const result = computeMissedPastDoseIds(pastDays, meds, new Set(), new Set());
|
||||
// Only Vitamin D's dose remains missed
|
||||
expect(result).toEqual([`2-0-${march10}`]);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Dose Tracking Regression Tests
|
||||
// =============================================================================
|
||||
|
||||
describe("dose tracking survives medication edits (regression)", () => {
|
||||
beforeEach(() => {
|
||||
vi.setSystemTime(new Date("2024-03-15T12:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("taken dose remains taken after changing intake time", () => {
|
||||
// === BEFORE EDIT: medication with 09:00 morning intake ===
|
||||
const medBefore: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const resultBefore = buildSchedulePreview(medBefore, "en", true);
|
||||
const pastBefore = resultBefore.events.filter((e) => e.isPast);
|
||||
expect(pastBefore.length).toBe(5); // March 10, 11, 12, 13, 14
|
||||
|
||||
// User marks March 12 dose as taken
|
||||
const march12Dose = pastBefore.find((e) => {
|
||||
const d = new Date(e.when);
|
||||
return d.getDate() === 12;
|
||||
})!;
|
||||
expect(march12Dose).toBeDefined();
|
||||
const takenDoses = new Set([march12Dose.id]);
|
||||
|
||||
// Compute missed doses BEFORE edit
|
||||
const pastDaysBefore = groupEventsIntoPastDays(pastBefore);
|
||||
const missedBefore = computeMissedPastDoseIds(pastDaysBefore, medBefore, takenDoses, new Set());
|
||||
expect(missedBefore).not.toContain(march12Dose.id); // March 12 is taken
|
||||
expect(missedBefore).toHaveLength(4); // March 10, 11, 13, 14 are missed
|
||||
|
||||
// === AFTER EDIT: change intake time to 21:00 evening + updatedAt set ===
|
||||
const medAfter: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T21:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const resultAfter = buildSchedulePreview(medAfter, "en", true);
|
||||
const pastAfter = resultAfter.events.filter((e) => e.isPast);
|
||||
expect(pastAfter.length).toBe(5); // Same 5 past days
|
||||
|
||||
// Compute missed doses AFTER edit — with same takenDoses set
|
||||
const pastDaysAfter = groupEventsIntoPastDays(pastAfter);
|
||||
const missedAfter = computeMissedPastDoseIds(pastDaysAfter, medAfter, takenDoses, new Set());
|
||||
|
||||
// === CRITICAL ASSERTION: March 12 is STILL marked as taken ===
|
||||
expect(missedAfter).not.toContain(march12Dose.id);
|
||||
// The other 4 days remain missed
|
||||
expect(missedAfter).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("updatedAt does NOT filter out past doses as false dismissals", () => {
|
||||
// This is the core bug that was fixed: isDoseFromPreviousSchedule compared
|
||||
// dateOnlyMs < updatedAt, which falsely dismissed ALL past doses after ANY edit.
|
||||
const med: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(), // Just edited!
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(med, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
expect(pastEvents.length).toBe(5); // March 10-14
|
||||
|
||||
// No doses taken, no dismissals
|
||||
const pastDays = groupEventsIntoPastDays(pastEvents);
|
||||
const missed = computeMissedPastDoseIds(pastDays, med, new Set(), new Set());
|
||||
|
||||
// ALL 5 past days should be missed — updatedAt must NOT cause silent dismissals
|
||||
expect(missed).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("today's doses are not counted as past/missed", () => {
|
||||
const med: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-15T09:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(med, "en", true);
|
||||
const todayEvents = result.events.filter((e) => !e.isPast);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
|
||||
// Today's dose should NOT be past
|
||||
expect(todayEvents.length).toBeGreaterThan(0);
|
||||
// No past events (schedule starts today)
|
||||
expect(pastEvents.length).toBe(0);
|
||||
|
||||
// computeMissedPastDoseIds with empty pastDays => no missed
|
||||
const missed = computeMissedPastDoseIds([], med, new Set(), new Set());
|
||||
expect(missed).toEqual([]);
|
||||
});
|
||||
|
||||
it("dismissedUntil correctly clears missed doses after medication edit", () => {
|
||||
// Scenario: user edits medication, then clicks "Clear missed" which sets dismissedUntil
|
||||
const med: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "TestMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
dismissedUntil: "2024-03-14", // Dismissed through yesterday
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(med, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
expect(pastEvents.length).toBe(5); // March 10-14
|
||||
|
||||
const pastDays = groupEventsIntoPastDays(pastEvents);
|
||||
const missed = computeMissedPastDoseIds(pastDays, med, new Set(), new Set());
|
||||
|
||||
// All 5 past doses are on or before March 14 → all dismissed by dismissedUntil
|
||||
expect(missed).toEqual([]);
|
||||
});
|
||||
|
||||
it("multiple medications: edit one, other's tracking is unaffected", () => {
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aspirin",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T09:00:00" }],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(), // Just edited!
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Vitamin D",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-03-10T08:00:00" }],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSchedulePreview(meds, "en", true);
|
||||
const pastEvents = result.events.filter((e) => e.isPast);
|
||||
|
||||
// Mark one dose of each medication as taken on March 12
|
||||
const aspirinMarch12 = pastEvents.find((e) => e.medName === "Aspirin" && new Date(e.when).getDate() === 12)!;
|
||||
const vitDMarch12 = pastEvents.find((e) => e.medName === "Vitamin D" && new Date(e.when).getDate() === 12)!;
|
||||
expect(aspirinMarch12).toBeDefined();
|
||||
expect(vitDMarch12).toBeDefined();
|
||||
|
||||
const takenDoses = new Set([aspirinMarch12.id, vitDMarch12.id]);
|
||||
const pastDays = groupEventsIntoPastDays(pastEvents);
|
||||
const missed = computeMissedPastDoseIds(pastDays, meds, takenDoses, new Set());
|
||||
|
||||
// Each med has 5 past days, 1 taken = 4 missed each = 8 total
|
||||
expect(missed).toHaveLength(8);
|
||||
// Neither March 12 dose ID should be in missed
|
||||
expect(missed).not.toContain(aspirinMarch12.id);
|
||||
expect(missed).not.toContain(vitDMarch12.id);
|
||||
});
|
||||
|
||||
it("taken doses with per-intake takenBy survive time edit", () => {
|
||||
// Using the modern intakes format: each person gets their own intake entry
|
||||
// Alice = intake index 0, Bob = intake index 1
|
||||
const medBefore: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "SharedMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: ["Alice", "Bob"],
|
||||
blisters: [],
|
||||
intakes: [
|
||||
{ usage: 1, every: 1, start: "2024-03-10T09:00:00", takenBy: "Alice", intakeRemindersEnabled: false },
|
||||
{ usage: 1, every: 1, start: "2024-03-10T09:00:00", takenBy: "Bob", intakeRemindersEnabled: false },
|
||||
],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const resultBefore = buildSchedulePreview(medBefore, "en", true);
|
||||
const pastBefore = resultBefore.events.filter((e) => e.isPast);
|
||||
// 2 intakes × 5 days = 10 events
|
||||
expect(pastBefore.length).toBe(10);
|
||||
|
||||
// Alice's March 12 dose (intake index 0)
|
||||
const aliceMarch12 = pastBefore.find((e) => new Date(e.when).getDate() === 12 && e.id.startsWith("1-0-"))!;
|
||||
expect(aliceMarch12).toBeDefined();
|
||||
|
||||
// Bob's March 12 dose (intake index 1)
|
||||
const bobMarch12 = pastBefore.find((e) => new Date(e.when).getDate() === 12 && e.id.startsWith("1-1-"))!;
|
||||
expect(bobMarch12).toBeDefined();
|
||||
|
||||
// Alice takes her dose on March 12 — getDoseId adds person suffix
|
||||
// This matches how DashboardPage marks doses: getDoseId(dose.id, person)
|
||||
const aliceDoseId = `${aliceMarch12.id}-Alice`;
|
||||
const takenDoses = new Set([aliceDoseId]);
|
||||
|
||||
// Now edit: change both intakes to evening
|
||||
const medAfter: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "SharedMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: ["Alice", "Bob"],
|
||||
blisters: [],
|
||||
intakes: [
|
||||
{ usage: 1, every: 1, start: "2024-03-10T21:00:00", takenBy: "Alice", intakeRemindersEnabled: false },
|
||||
{ usage: 1, every: 1, start: "2024-03-10T21:00:00", takenBy: "Bob", intakeRemindersEnabled: false },
|
||||
],
|
||||
updatedAt: new Date("2024-03-15T10:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const resultAfter = buildSchedulePreview(medAfter, "en", true);
|
||||
const pastAfter = resultAfter.events.filter((e) => e.isPast);
|
||||
expect(pastAfter.length).toBe(10); // Still 10 events
|
||||
|
||||
const pastDays = groupEventsIntoPastDays(pastAfter);
|
||||
const missed = computeMissedPastDoseIds(pastDays, medAfter, takenDoses, new Set());
|
||||
|
||||
// 10 events, each expanded with person suffix = 10 dose IDs, minus 1 taken = 9 missed
|
||||
expect(missed).toHaveLength(9);
|
||||
// Alice's March 12 dose (with -Alice suffix) should NOT be in missed
|
||||
expect(missed).not.toContain(aliceDoseId);
|
||||
// Bob's March 12 dose (with -Bob suffix) should still be missed
|
||||
expect(missed).toContain(`${bobMarch12.id}-Bob`);
|
||||
// Dose IDs should be different between Alice and Bob (different intake index)
|
||||
expect(aliceMarch12.id).not.toBe(bobMarch12.id);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test Helpers
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Group flat schedule events into the pastDays structure expected by computeMissedPastDoseIds.
|
||||
* This mirrors the groupedSchedule logic in AppContext.tsx:
|
||||
* - event.takenBy is `string | null` (per-intake person)
|
||||
* - AppContext normalizes to `string[]`: `event.takenBy ? [event.takenBy] : []`
|
||||
*/
|
||||
function groupEventsIntoPastDays(
|
||||
events: Array<{
|
||||
id: string;
|
||||
medName: string;
|
||||
when: number;
|
||||
usage: number;
|
||||
isPast: boolean;
|
||||
takenBy?: string | string[] | null;
|
||||
}>
|
||||
): Array<{ meds: Array<{ medName: string; doses: Array<{ id: string; takenBy: string[] }> }> }> {
|
||||
const dayMap = new Map<string, Map<string, Array<{ id: string; takenBy: string[] }>>>();
|
||||
|
||||
for (const event of events) {
|
||||
if (!event.isPast) continue;
|
||||
const dateKey = new Date(event.when).toDateString();
|
||||
if (!dayMap.has(dateKey)) dayMap.set(dateKey, new Map());
|
||||
const medMap = dayMap.get(dateKey)!;
|
||||
if (!medMap.has(event.medName)) medMap.set(event.medName, []);
|
||||
// Mirror AppContext normalization: string|null → string[]
|
||||
const takenBy = Array.isArray(event.takenBy)
|
||||
? event.takenBy
|
||||
: typeof event.takenBy === "string"
|
||||
? [event.takenBy]
|
||||
: [];
|
||||
medMap.get(event.medName)!.push({ id: event.id, takenBy });
|
||||
}
|
||||
|
||||
return Array.from(dayMap.values()).map((medMap) => ({
|
||||
meds: Array.from(medMap.entries()).map(([medName, doses]) => ({ medName, doses })),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -411,3 +411,66 @@ export function getReminderStatusText(
|
||||
}
|
||||
return { lines };
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dose Dismissal & Missed Dose Computation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if a dose is dismissed based on its ID and a dismissedUntil date string.
|
||||
* Extracts the date-only timestamp from the dose ID and compares it with the dismissedUntil date.
|
||||
*
|
||||
* @param doseId - Dose ID in format "medId-intakeIdx-dateOnlyMs" or "medId-intakeIdx-dateOnlyMs-person"
|
||||
* @param dismissedUntilDate - YYYY-MM-DD formatted date string, or undefined if not dismissed
|
||||
* @returns true if the dose date is on or before the dismissedUntil date
|
||||
*/
|
||||
export function isDoseDismissed(doseId: string, dismissedUntilDate: string | undefined): boolean {
|
||||
if (!dismissedUntilDate) return false;
|
||||
const parts = doseId.split("-");
|
||||
if (parts.length < 3) return false;
|
||||
const timestamp = parseInt(parts[2], 10);
|
||||
if (Number.isNaN(timestamp)) return false;
|
||||
const doseDate = new Date(timestamp);
|
||||
const doseDateStr = `${doseDate.getFullYear()}-${String(doseDate.getMonth() + 1).padStart(2, "0")}-${String(doseDate.getDate()).padStart(2, "0")}`;
|
||||
return doseDateStr <= dismissedUntilDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the list of missed past dose IDs.
|
||||
* A dose is "missed" if it is in the past, not taken, not individually dismissed,
|
||||
* and not covered by the medication's dismissedUntil date.
|
||||
*
|
||||
* @param pastDays - Grouped schedule days that are in the past
|
||||
* @param medications - Full medication list (used to look up dismissedUntil)
|
||||
* @param takenDoses - Set of dose IDs marked as taken
|
||||
* @param dismissedDoses - Set of dose IDs individually dismissed
|
||||
* @returns Array of dose IDs that are missed
|
||||
*/
|
||||
export function computeMissedPastDoseIds(
|
||||
pastDays: ReadonlyArray<{
|
||||
meds: ReadonlyArray<{
|
||||
medName: string;
|
||||
doses: ReadonlyArray<{ id: string; takenBy: string[] }>;
|
||||
}>;
|
||||
}>,
|
||||
medications: ReadonlyArray<{ name: string; dismissedUntil?: string | null }>,
|
||||
takenDoses: Set<string>,
|
||||
dismissedDoses: Set<string>
|
||||
): string[] {
|
||||
const totalPastDoses = pastDays.flatMap((d) =>
|
||||
d.meds.flatMap((m) => {
|
||||
const med = medications.find((med) => med.name === m.medName);
|
||||
const dismissedUntilDate = med?.dismissedUntil ?? undefined;
|
||||
|
||||
return m.doses.flatMap((dose) => {
|
||||
if (isDoseDismissed(dose.id, dismissedUntilDate)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const takenByArray = Array.isArray(dose.takenBy) ? dose.takenBy : [];
|
||||
return takenByArray.length > 0 ? takenByArray.map((p: string) => `${dose.id}-${p}`) : [dose.id];
|
||||
});
|
||||
})
|
||||
);
|
||||
return totalPastDoses.filter((id) => !takenDoses.has(id) && !dismissedDoses.has(id));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user