import type { Extension, Organization } from "fhir/r4"; import { ORGANIZATION_BOOKING_MODE_EXTENSION_URL, ORGANIZATION_DEFAULT_APPOINTMENT_DURATION_EXTENSION_URL, ORGANIZATION_REQUEST_STYLE_EXTENSION_URL, ORGANIZATION_REQUIRE_NEW_PATIENT_EXAM_FIRST_EXTENSION_URL, RETIRED_ORGANIZATION_NEW_PATIENT_EXAM_ROLES_EXTENSION_URL, getOrganizationPolicyFlags, type OrganizationBookingMode, type OrganizationRequestStyle, } from "../config/bookingPolicy"; import { resolveOrgAppointmentPolicy } from "../config/appointmentPolicy"; import { resolveOrgReminderPolicy, type QuietHours, type ReminderPolicy } from "../config/appointmentReminderPolicy"; // Everything the Booking rules panel edits, from its three homes: the organization's booking // extensions, the appointment-policy route and the reminder route. export type BookingRulesDraft = { telehealthEnabled: boolean; bookingMode: OrganizationBookingMode; requestStyle: OrganizationRequestStyle; requireNewPatientFirst: boolean; defaultDurationMinutes: number; reminderOffsets: number[]; maxReminders: number; quietHours: QuietHours | null; }; export type BookingRulesPart = "booking" | "policy" | "reminders"; export const readBookingRules = (org: Organization | null | undefined): BookingRulesDraft => { const flags = getOrganizationPolicyFlags(org ?? undefined); const reminders = resolveOrgReminderPolicy(org); return { telehealthEnabled: resolveOrgAppointmentPolicy(org ?? undefined).telehealthEnabled, bookingMode: flags.bookingMode, requestStyle: flags.requestStyle, requireNewPatientFirst: flags.requireNewPatientExamFirst, defaultDurationMinutes: flags.defaultAppointmentDurationMinutes, // Reminders are on exactly when a time is ticked, so switched-off reminders show none ticked. reminderOffsets: reminders.enabled ? [...reminders.offsetsMinutes].sort((a, b) => a - b) : [], maxReminders: reminders.maxPerAppointment, quietHours: reminders.quietHours, }; }; const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b); // The parts whose values differ from what is saved, in the order they are written. export const changedParts = (draft: BookingRulesDraft, saved: BookingRulesDraft): BookingRulesPart[] => { const parts: BookingRulesPart[] = []; if ( draft.bookingMode !== saved.bookingMode || draft.requestStyle !== saved.requestStyle || draft.requireNewPatientFirst !== saved.requireNewPatientFirst || draft.defaultDurationMinutes !== saved.defaultDurationMinutes ) parts.push("booking"); if (draft.telehealthEnabled !== saved.telehealthEnabled) parts.push("policy"); if ( !same(draft.reminderOffsets, saved.reminderOffsets) || draft.maxReminders !== saved.maxReminders || !same(draft.quietHours, saved.quietHours) ) parts.push("reminders"); return parts; }; export const bookingRulesProblem = (draft: BookingRulesDraft): string | undefined => { if (!Number.isInteger(draft.defaultDurationMinutes) || draft.defaultDurationMinutes <= 0) { return "Enter a default appointment length in minutes."; } return undefined; }; // The organization with its booking extensions replaced by the draft's; everything else is kept. export const withBookingRules = (org: Organization, draft: BookingRulesDraft): Organization => { const owned = new Set([ ORGANIZATION_REQUIRE_NEW_PATIENT_EXAM_FIRST_EXTENSION_URL, ORGANIZATION_BOOKING_MODE_EXTENSION_URL, ORGANIZATION_REQUEST_STYLE_EXTENSION_URL, RETIRED_ORGANIZATION_NEW_PATIENT_EXAM_ROLES_EXTENSION_URL, ORGANIZATION_DEFAULT_APPOINTMENT_DURATION_EXTENSION_URL, ]); const extension: Extension[] = [ ...(org.extension ?? []).filter((e) => !owned.has(e.url)), ...(draft.requireNewPatientFirst ? [{ url: ORGANIZATION_REQUIRE_NEW_PATIENT_EXAM_FIRST_EXTENSION_URL, valueBoolean: true }] : []), ...(draft.bookingMode !== "direct" ? [{ url: ORGANIZATION_BOOKING_MODE_EXTENSION_URL, valueCode: draft.bookingMode }] : []), ...(draft.bookingMode === "request-only" && draft.requestStyle === "time-window" ? [{ url: ORGANIZATION_REQUEST_STYLE_EXTENSION_URL, valueCode: "time-window" }] : []), { url: ORGANIZATION_DEFAULT_APPOINTMENT_DURATION_EXTENSION_URL, valueInteger: draft.defaultDurationMinutes }, ]; return { ...org, extension }; }; // Reminders switch off by leaving every time unticked; the saved times are kept for next time. export const toReminderPolicy = (draft: BookingRulesDraft, saved: ReminderPolicy): ReminderPolicy => ({ enabled: draft.reminderOffsets.length > 0, offsetsMinutes: draft.reminderOffsets.length > 0 ? draft.reminderOffsets : saved.offsetsMinutes, maxPerAppointment: draft.maxReminders, quietHours: draft.quietHours, });