68ab79c713
Closes #463
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
// =============================================================================
|
|
// ICS Calendar Generation
|
|
// =============================================================================
|
|
|
|
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)
|
|
*/
|
|
function formatICSDate(date: Date): string {
|
|
return date
|
|
.toISOString()
|
|
.replace(/[-:]/g, "")
|
|
.replace(/\.\d{3}/, "");
|
|
}
|
|
|
|
/**
|
|
* Generate and download an ICS calendar file for a medication's schedule
|
|
*/
|
|
export function generateICS(med: Medication): void {
|
|
const displayName = getMedDisplayName(med);
|
|
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 = intake.every;
|
|
|
|
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: ${frequencyText}`,
|
|
med.notes ? `Notes: ${med.notes}` : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\\n");
|
|
|
|
return `BEGIN:VEVENT
|
|
UID:medassist-ng-${med.id}-${idx}@medassist-ng
|
|
DTSTAMP:${formatICSDate(new Date())}
|
|
DTSTART:${formatICSDate(start)}
|
|
DTEND:${formatICSDate(end)}
|
|
${rrule}
|
|
SUMMARY:${summary}
|
|
DESCRIPTION:${description}
|
|
BEGIN:VALARM
|
|
TRIGGER:-PT5M
|
|
ACTION:DISPLAY
|
|
DESCRIPTION:Time to take ${displayName}
|
|
END:VALARM
|
|
END:VEVENT`;
|
|
})
|
|
.join("\n");
|
|
|
|
const ics = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//MedAssist-ng//Medication Schedule//EN
|
|
CALSCALE:GREGORIAN
|
|
METHOD:PUBLISH
|
|
X-WR-CALNAME:${displayName} Schedule
|
|
${events}
|
|
END:VCALENDAR`;
|
|
|
|
const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = `${displayName.replace(/[^a-zA-Z0-9]/g, "_")}_schedule.ics`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}
|