@@ -4,6 +4,13 @@
|
||||
|
||||
import type { Medication } from "../types";
|
||||
import { getMedDisplayName } from "../types";
|
||||
import {
|
||||
getIntakeFrequencyText,
|
||||
getIntakeScheduleMode,
|
||||
getMedicationIntakes,
|
||||
getWeekdayIcsCode,
|
||||
normalizeWeekdays,
|
||||
} from "./intake-schedule";
|
||||
|
||||
/**
|
||||
* Format a Date for ICS format (YYYYMMDDTHHMMSSZ)
|
||||
@@ -20,20 +27,33 @@ function formatICSDate(date: Date): string {
|
||||
*/
|
||||
export function generateICS(med: Medication): void {
|
||||
const displayName = getMedDisplayName(med);
|
||||
const events = med.blisters
|
||||
.map((blister, idx) => {
|
||||
const start = new Date(blister.start);
|
||||
const events = getMedicationIntakes(med)
|
||||
.map((intake, idx) => {
|
||||
const start = new Date(intake.start);
|
||||
const end = new Date(start.getTime() + 15 * 60 * 1000); // 15 min duration
|
||||
const interval = blister.every;
|
||||
const interval = intake.every;
|
||||
|
||||
const pillInfo = `${blister.usage} pill${blister.usage !== 1 ? "s" : ""}${med.pillWeightMg ? ` (${blister.usage * med.pillWeightMg} mg)` : ""}`;
|
||||
const pillInfo = `${intake.usage} pill${intake.usage !== 1 ? "s" : ""}${med.pillWeightMg ? ` (${intake.usage * med.pillWeightMg} mg)` : ""}`;
|
||||
const summary = `💊 ${displayName} - ${pillInfo}`;
|
||||
const weekdayCodes = normalizeWeekdays(intake.weekdays);
|
||||
const frequencyText =
|
||||
getIntakeScheduleMode(intake) === "weekdays"
|
||||
? weekdayCodes.map(getWeekdayIcsCode).join(", ")
|
||||
: getIntakeFrequencyText(intake, (key, options) => {
|
||||
if (key === "common.daily") return "daily";
|
||||
if (key === "common.everyNDays") return `every ${options?.count ?? interval} days`;
|
||||
return key;
|
||||
});
|
||||
const rrule =
|
||||
getIntakeScheduleMode(intake) === "weekdays" && weekdayCodes.length > 0
|
||||
? `RRULE:FREQ=WEEKLY;BYDAY=${weekdayCodes.map(getWeekdayIcsCode).join(",")}`
|
||||
: `RRULE:FREQ=DAILY;INTERVAL=${interval}`;
|
||||
const description = [
|
||||
`Medication: ${displayName}`,
|
||||
med.genericName ? `Generic: ${med.genericName}` : "",
|
||||
med.takenBy && med.takenBy.length > 0 ? `For: ${med.takenBy.join(", ")}` : "",
|
||||
`Dosage: ${pillInfo}`,
|
||||
`Frequency: every ${interval} day${interval !== 1 ? "s" : ""}`,
|
||||
`Frequency: ${frequencyText}`,
|
||||
med.notes ? `Notes: ${med.notes}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -44,7 +64,7 @@ UID:medassist-ng-${med.id}-${idx}@medassist-ng
|
||||
DTSTAMP:${formatICSDate(new Date())}
|
||||
DTSTART:${formatICSDate(start)}
|
||||
DTEND:${formatICSDate(end)}
|
||||
RRULE:FREQ=DAILY;INTERVAL=${interval}
|
||||
${rrule}
|
||||
SUMMARY:${summary}
|
||||
DESCRIPTION:${description}
|
||||
BEGIN:VALARM
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Blister, Intake, ScheduleMode, WeekdayCode } from "../types";
|
||||
|
||||
type MedicationScheduleSource = {
|
||||
intakes?: Intake[] | null;
|
||||
blisters: Blister[];
|
||||
intakeRemindersEnabled?: boolean;
|
||||
};
|
||||
|
||||
type IntakeScheduleLike = {
|
||||
every?: number | string | null;
|
||||
scheduleMode?: ScheduleMode | null;
|
||||
weekdays?: ReadonlyArray<WeekdayCode> | null;
|
||||
};
|
||||
|
||||
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
||||
|
||||
export const WEEKDAY_CODES: WeekdayCode[] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
||||
|
||||
const WEEKDAY_LABELS: Record<WeekdayCode, { short: string; long: string; ics: string }> = {
|
||||
mon: { short: "form.blisters.weekdaysShort.mon", long: "form.blisters.weekdaysLong.mon", ics: "MO" },
|
||||
tue: { short: "form.blisters.weekdaysShort.tue", long: "form.blisters.weekdaysLong.tue", ics: "TU" },
|
||||
wed: { short: "form.blisters.weekdaysShort.wed", long: "form.blisters.weekdaysLong.wed", ics: "WE" },
|
||||
thu: { short: "form.blisters.weekdaysShort.thu", long: "form.blisters.weekdaysLong.thu", ics: "TH" },
|
||||
fri: { short: "form.blisters.weekdaysShort.fri", long: "form.blisters.weekdaysLong.fri", ics: "FR" },
|
||||
sat: { short: "form.blisters.weekdaysShort.sat", long: "form.blisters.weekdaysLong.sat", ics: "SA" },
|
||||
sun: { short: "form.blisters.weekdaysShort.sun", long: "form.blisters.weekdaysLong.sun", ics: "SU" },
|
||||
};
|
||||
|
||||
export function normalizeWeekdays(weekdays?: ReadonlyArray<WeekdayCode> | null): WeekdayCode[] {
|
||||
if (!Array.isArray(weekdays) || weekdays.length === 0) return [];
|
||||
const normalizedSet = new Set<WeekdayCode>();
|
||||
for (const day of weekdays) {
|
||||
if (WEEKDAY_CODES.includes(day)) {
|
||||
normalizedSet.add(day);
|
||||
}
|
||||
}
|
||||
return WEEKDAY_CODES.filter((day) => normalizedSet.has(day));
|
||||
}
|
||||
|
||||
export function hasSelectedWeekdays(weekdays?: ReadonlyArray<WeekdayCode> | null): boolean {
|
||||
return normalizeWeekdays(weekdays).length > 0;
|
||||
}
|
||||
|
||||
export function getIntakeScheduleMode(schedule: IntakeScheduleLike): ScheduleMode {
|
||||
return schedule.scheduleMode === "weekdays" ? "weekdays" : "interval";
|
||||
}
|
||||
|
||||
export function getNormalizedInterval(schedule: IntakeScheduleLike): number {
|
||||
const parsedEvery = Number(schedule.every);
|
||||
if (!Number.isFinite(parsedEvery) || parsedEvery <= 0) return 1;
|
||||
return Math.floor(parsedEvery);
|
||||
}
|
||||
|
||||
export function getWeekdayCode(date: Date): WeekdayCode {
|
||||
return WEEKDAY_CODES[(date.getDay() + 6) % 7];
|
||||
}
|
||||
|
||||
export function getWeekdayLabel(day: WeekdayCode, t: Translate, format: "short" | "long" = "short"): string {
|
||||
return t(WEEKDAY_LABELS[day][format]);
|
||||
}
|
||||
|
||||
export function getWeekdayIcsCode(day: WeekdayCode): string {
|
||||
return WEEKDAY_LABELS[day].ics;
|
||||
}
|
||||
|
||||
export function toggleWeekdaySelection(
|
||||
weekdays: ReadonlyArray<WeekdayCode> | null | undefined,
|
||||
day: WeekdayCode
|
||||
): WeekdayCode[] {
|
||||
const normalized = normalizeWeekdays(weekdays);
|
||||
if (normalized.includes(day)) {
|
||||
return normalized.filter((entry) => entry !== day);
|
||||
}
|
||||
return normalizeWeekdays([...normalized, day]);
|
||||
}
|
||||
|
||||
export function getMedicationIntakes(med: MedicationScheduleSource): Intake[] {
|
||||
if (med.intakes && med.intakes.length > 0) {
|
||||
return med.intakes;
|
||||
}
|
||||
return med.blisters.map((blister) => ({
|
||||
usage: blister.usage,
|
||||
every: blister.every,
|
||||
start: blister.start,
|
||||
scheduleMode: "interval",
|
||||
weekdays: [],
|
||||
intakeUnit: null,
|
||||
takenBy: null,
|
||||
intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
export function iterateIntakeOccurrences(
|
||||
intake: IntakeScheduleLike,
|
||||
start: Date,
|
||||
end: Date,
|
||||
callback: (occurrence: Date) => void
|
||||
): void {
|
||||
if (start > end) return;
|
||||
|
||||
if (getIntakeScheduleMode(intake) === "weekdays") {
|
||||
const weekdays = normalizeWeekdays(intake.weekdays);
|
||||
if (weekdays.length === 0) return;
|
||||
|
||||
const cursor = new Date(start);
|
||||
while (cursor <= end) {
|
||||
if (weekdays.includes(getWeekdayCode(cursor))) {
|
||||
callback(new Date(cursor));
|
||||
}
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = getNormalizedInterval(intake);
|
||||
const cursor = new Date(start);
|
||||
while (cursor <= end) {
|
||||
callback(new Date(cursor));
|
||||
cursor.setDate(cursor.getDate() + interval);
|
||||
}
|
||||
}
|
||||
|
||||
export function getIntakeDailyRate(schedule: IntakeScheduleLike): number {
|
||||
if (getIntakeScheduleMode(schedule) === "weekdays") {
|
||||
return normalizeWeekdays(schedule.weekdays).length / 7;
|
||||
}
|
||||
return 1 / getNormalizedInterval(schedule);
|
||||
}
|
||||
|
||||
export function getIntakeFrequencyText(schedule: IntakeScheduleLike, t: Translate): string {
|
||||
if (getIntakeScheduleMode(schedule) === "weekdays") {
|
||||
return normalizeWeekdays(schedule.weekdays)
|
||||
.map((day) => getWeekdayLabel(day, t, "short"))
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
const every = getNormalizedInterval(schedule);
|
||||
return every === 1 ? t("common.daily") : t("common.everyNDays", { count: every });
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { IntakeUnit } from "../types";
|
||||
|
||||
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
||||
|
||||
export function convertLiquidUsageToMl(usage: number, unit: IntakeUnit | null | undefined): number {
|
||||
if (unit === "tsp") return usage * 5;
|
||||
if (unit === "tbsp") return usage * 15;
|
||||
return usage;
|
||||
}
|
||||
|
||||
export function getLiquidCountUnitLabel(unit: IntakeUnit | null | undefined, usage: number, t: Translate): string {
|
||||
if (unit === "tsp") return t("form.blisters.teaspoons", { count: Math.abs(usage) });
|
||||
if (unit === "tbsp") return t("form.blisters.tablespoons", { count: Math.abs(usage) });
|
||||
return t("form.packageAmountUnitMl");
|
||||
}
|
||||
@@ -2,17 +2,10 @@
|
||||
// Schedule Building and Coverage Calculations
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
Blister,
|
||||
Coverage,
|
||||
Intake,
|
||||
Medication,
|
||||
PackageType,
|
||||
ScheduleEvent,
|
||||
StockStatus,
|
||||
StockThresholds,
|
||||
} from "../types";
|
||||
import type { Coverage, Intake, Medication, PackageType, ScheduleEvent, StockStatus, StockThresholds } from "../types";
|
||||
import { getMedDisplayName, getMedTotal, isLiquidContainerPackageType, isTubePackageType } from "../types";
|
||||
import { getIntakeDailyRate, getMedicationIntakes, iterateIntakeOccurrences } from "./intake-schedule";
|
||||
import { convertLiquidUsageToMl } from "./intake-units";
|
||||
|
||||
export function parseLocalDateTime(isoString: string): Date {
|
||||
const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):?(\d{2})?/);
|
||||
@@ -39,38 +32,7 @@ function normalizeIntakeUsageForStock(intake: Intake, med: Medication): number {
|
||||
const isLiquidStock = isLiquidContainerPackageType(med.packageType) || med.medicationForm === "liquid";
|
||||
if (!isLiquidStock) return usage;
|
||||
|
||||
if (intake.intakeUnit === "tsp") return usage * 5;
|
||||
if (intake.intakeUnit === "tbsp") return usage * 15;
|
||||
return usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intakes for a medication, preferring new intakes format over legacy blisters
|
||||
*/
|
||||
function getIntakesForMed(med: Medication): Intake[] {
|
||||
// Use new intakes array if available and non-empty
|
||||
if (med.intakes && med.intakes.length > 0) {
|
||||
return med.intakes;
|
||||
}
|
||||
// Fallback to legacy blisters (convert to Intake format)
|
||||
return med.blisters.map((b) => ({
|
||||
usage: b.usage,
|
||||
every: b.every,
|
||||
start: b.start,
|
||||
intakeUnit: null,
|
||||
takenBy: null, // Legacy format has no per-intake takenBy
|
||||
intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blisters for a medication (for backward compatibility with coverage calculations)
|
||||
*/
|
||||
function getBlistersForMed(med: Medication): Blister[] {
|
||||
if (med.intakes && med.intakes.length > 0) {
|
||||
return med.intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start }));
|
||||
}
|
||||
return med.blisters;
|
||||
return convertLiquidUsageToMl(usage, intake.intakeUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,13 +52,13 @@ export function buildSchedulePreview(
|
||||
end.setDate(end.getDate() + 180); // 6 months horizon
|
||||
|
||||
meds.forEach((med) => {
|
||||
const intakes = getIntakesForMed(med);
|
||||
const intakes = getMedicationIntakes(med);
|
||||
intakes.forEach((intake, idx) => {
|
||||
const start = parseLocalDateTime(intake.start);
|
||||
if (Number.isNaN(start.getTime())) return;
|
||||
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + intake.every)) {
|
||||
iterateIntakeOccurrences(intake, start, end, (d) => {
|
||||
const isPast = d < todayStart;
|
||||
if (isPast && !includePast) continue;
|
||||
if (isPast && !includePast) return;
|
||||
const whenMs = d.getTime();
|
||||
// Use date-only timestamp for stable ID (immune to time changes)
|
||||
// This ensures changing intake times doesn't invalidate past dose tracking
|
||||
@@ -113,7 +75,7 @@ export function buildSchedulePreview(
|
||||
dateStr: d.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "short" }),
|
||||
intakeRemindersEnabled: intake.intakeRemindersEnabled,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +91,7 @@ export function buildSchedulePreview(
|
||||
events,
|
||||
today: todayCount,
|
||||
nextThree: events.length,
|
||||
totalBlisters: meds.reduce((acc, m) => acc + getIntakesForMed(m).length, 0),
|
||||
totalBlisters: meds.reduce((acc, med) => acc + getMedicationIntakes(med).length, 0),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,10 +109,10 @@ export function calculateCoverage(
|
||||
): { low: Coverage[]; all: Coverage[] } {
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
const now = Date.now();
|
||||
const nowDate = new Date(now);
|
||||
|
||||
const coverage: Coverage[] = meds.map((m) => {
|
||||
const intakes = getIntakesForMed(m);
|
||||
const blisters = getBlistersForMed(m);
|
||||
const intakes = getMedicationIntakes(m);
|
||||
// Count unique people from all intakes (for per-intake takenBy)
|
||||
const uniquePeople = new Set<string>();
|
||||
intakes.forEach((intake) => {
|
||||
@@ -165,11 +127,9 @@ export function calculateCoverage(
|
||||
// one person's dose — do NOT multiply by personCount again.
|
||||
// For legacy intakes (no takenBy), the intake applies to ALL people.
|
||||
let dailyRate = 0;
|
||||
blisters.forEach((_s, idx) => {
|
||||
const intake = intakes[idx];
|
||||
if (!intake) return;
|
||||
intakes.forEach((intake) => {
|
||||
const usageForStock = normalizeIntakeUsageForStock(intake, m);
|
||||
const baseRate = intake.every > 0 ? usageForStock / intake.every : 0;
|
||||
const baseRate = usageForStock * getIntakeDailyRate(intake);
|
||||
if (intake?.takenBy) {
|
||||
// Per-intake takenBy: this intake is for exactly 1 person
|
||||
dailyRate += baseRate;
|
||||
@@ -189,29 +149,11 @@ export function calculateCoverage(
|
||||
// time (early intake), that dose is also counted as consumed immediately.
|
||||
// This prevents double-counting: once the scheduled time arrives, the dose
|
||||
// was already counted via the early-taken path, not again via time.
|
||||
blisters.forEach((s, blisterIdx) => {
|
||||
const blisterStart = parseLocalDateTime(s.start).getTime();
|
||||
const period = Math.max(1, s.every) * MS_PER_DAY;
|
||||
const intake = intakes[blisterIdx];
|
||||
if (!intake) return;
|
||||
intakes.forEach((intake, blisterIdx) => {
|
||||
const intakeStart = parseLocalDateTime(intake.start);
|
||||
if (Number.isNaN(intakeStart.getTime())) return;
|
||||
const usageForStock = normalizeIntakeUsageForStock(intake, m);
|
||||
|
||||
// After a stock correction, start counting consumption from the NEXT
|
||||
// scheduled dose on this blister's grid, because the user's pill count
|
||||
// already reflects all consumption up to the correction time.
|
||||
// We align to the schedule grid so that e.g. correction at 15:40 with
|
||||
// a daily 15:42 dose counts today's 15:42 dose (2 min later), not
|
||||
// tomorrow's dose (24h later as the old code did).
|
||||
let effectiveStart: number;
|
||||
if (stockCorrectionCutoff > 0 && stockCorrectionCutoff >= blisterStart) {
|
||||
const elapsedSinceStart = stockCorrectionCutoff - blisterStart;
|
||||
const periodsElapsed = Math.floor(elapsedSinceStart / period);
|
||||
effectiveStart = blisterStart + (periodsElapsed + 1) * period;
|
||||
} else {
|
||||
effectiveStart = blisterStart;
|
||||
}
|
||||
if (Number.isNaN(effectiveStart)) return;
|
||||
|
||||
const intakePerson = intake?.takenBy;
|
||||
|
||||
// For per-intake takenBy, only count for that person
|
||||
@@ -223,18 +165,15 @@ export function calculateCoverage(
|
||||
let timeBasedConsumed = 0;
|
||||
let lastAutoConsumedDateMs = 0;
|
||||
|
||||
if (effectiveStart <= now) {
|
||||
const occurrences = Math.floor((now - effectiveStart) / period) + 1;
|
||||
timeBasedConsumed = occurrences * usageForStock * peopleForThisIntake.length;
|
||||
|
||||
// Date-only timestamp of the last auto-consumed dose
|
||||
const lastDoseTime = new Date(effectiveStart + (occurrences - 1) * period);
|
||||
iterateIntakeOccurrences(intake, intakeStart, nowDate, (occurrence) => {
|
||||
if (occurrence.getTime() <= stockCorrectionCutoff) return;
|
||||
timeBasedConsumed += usageForStock * peopleForThisIntake.length;
|
||||
lastAutoConsumedDateMs = new Date(
|
||||
lastDoseTime.getFullYear(),
|
||||
lastDoseTime.getMonth(),
|
||||
lastDoseTime.getDate()
|
||||
occurrence.getFullYear(),
|
||||
occurrence.getMonth(),
|
||||
occurrence.getDate()
|
||||
).getTime();
|
||||
}
|
||||
});
|
||||
|
||||
// Early intakes: count future doses already marked as taken.
|
||||
// The cutoff is the later of: last auto-consumed date or stock correction date.
|
||||
@@ -276,16 +215,15 @@ export function calculateCoverage(
|
||||
const medId = parseInt(parts[0], 10);
|
||||
const blisterIdx = parseInt(parts[1], 10);
|
||||
const doseTimestamp = parseInt(parts[2], 10);
|
||||
if (medId === m.id && blisters[blisterIdx]) {
|
||||
const intake = intakes[blisterIdx];
|
||||
if (!intake) return;
|
||||
const intake = intakes[blisterIdx];
|
||||
if (medId === m.id && intake) {
|
||||
const usageForStock = normalizeIntakeUsageForStock(intake, m);
|
||||
// Convert blister start to date-only for comparison (dose timestamps are date-only)
|
||||
const blisterStartDate = new Date(blisters[blisterIdx].start);
|
||||
const blisterStartDateOnly = new Date(
|
||||
blisterStartDate.getFullYear(),
|
||||
blisterStartDate.getMonth(),
|
||||
blisterStartDate.getDate()
|
||||
const intakeStartDate = new Date(intake.start);
|
||||
const intakeStartDateOnly = new Date(
|
||||
intakeStartDate.getFullYear(),
|
||||
intakeStartDate.getMonth(),
|
||||
intakeStartDate.getDate()
|
||||
).getTime();
|
||||
|
||||
// Use actual takenAt timestamp for stock correction comparison.
|
||||
@@ -295,8 +233,8 @@ export function calculateCoverage(
|
||||
const afterCorrectionOrNoCorrectionMs = stockCorrectionCutoff === 0 || takenAt > stockCorrectionCutoff;
|
||||
|
||||
if (
|
||||
!Number.isNaN(blisterStartDateOnly) &&
|
||||
doseTimestamp >= blisterStartDateOnly &&
|
||||
!Number.isNaN(intakeStartDateOnly) &&
|
||||
doseTimestamp >= intakeStartDateOnly &&
|
||||
afterCorrectionOrNoCorrectionMs
|
||||
) {
|
||||
consumed += usageForStock;
|
||||
@@ -618,3 +556,48 @@ export function computeMissedPastDoseIds(
|
||||
);
|
||||
return totalPastDoses.filter((id) => !takenDoses.has(id) && !dismissedDoses.has(id));
|
||||
}
|
||||
|
||||
export function buildClearMissedPayload(
|
||||
pastDays: ReadonlyArray<{
|
||||
date: Date;
|
||||
meds: ReadonlyArray<{
|
||||
medName: string;
|
||||
doses: ReadonlyArray<{ id: string; takenBy: string[] }>;
|
||||
}>;
|
||||
}>,
|
||||
medications: ReadonlyArray<{ id: number; name: string; genericName?: string | null; dismissedUntil?: string | null }>,
|
||||
takenDoses: Set<string>,
|
||||
dismissedDoses: Set<string>
|
||||
): { medicationIds: number[]; until: string | null } {
|
||||
const medicationIds = new Set<number>();
|
||||
let latestMissedDate: string | null = null;
|
||||
|
||||
for (const day of pastDays) {
|
||||
for (const item of day.meds) {
|
||||
const med = medications.find((candidate) => getMedDisplayName(candidate as Medication) === item.medName);
|
||||
if (!med) continue;
|
||||
|
||||
const dismissedUntilDate = med.dismissedUntil ?? undefined;
|
||||
const hasMissedDose = item.doses.some((dose) => {
|
||||
if (isDoseDismissed(dose.id, dismissedUntilDate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expandDoseIds([dose]).some((doseId) => !takenDoses.has(doseId) && !dismissedDoses.has(doseId));
|
||||
});
|
||||
|
||||
if (!hasMissedDose) continue;
|
||||
|
||||
medicationIds.add(med.id);
|
||||
const dayDate = day.date.toISOString().slice(0, 10);
|
||||
if (!latestMissedDate || dayDate > latestMissedDate) {
|
||||
latestMissedDate = dayDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
medicationIds: [...medicationIds],
|
||||
until: latestMissedDate,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user