Files
medassist-ng/frontend/src/utils/ics.ts
T
2026-03-20 14:58:25 +01:00

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);
}