Skip to main content
Glama
Scheduler.tsx7.19 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Stack, Text } from '@mantine/core'; import type { WithId } from '@medplum/core'; import { getReferenceString, isReference } from '@medplum/core'; import type { Period, Practitioner, Questionnaire, QuestionnaireResponse, Reference, Schedule, Slot, } from '@medplum/fhirtypes'; import { useMedplum, useResource } from '@medplum/react-hooks'; import type { JSX } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { CalendarInput } from '../CalendarInput/CalendarInput'; import { getStartMonth } from '../CalendarInput/CalendarInput.utils'; import { QuestionnaireForm } from '../QuestionnaireForm/QuestionnaireForm'; import { ResourceAvatar } from '../ResourceAvatar/ResourceAvatar'; import { ResourceName } from '../ResourceName/ResourceName'; import classes from './Scheduler.module.css'; /** * Custom function to search for available slots within a given time period * @param period - The time period to search within * @returns Promise resolving to an array of available slots */ export type SlotSearchFunction = (period: Period) => Promise<Slot[]>; export interface SchedulerProps { readonly schedule: Schedule | Reference<Schedule> | Schedule[] | Reference<Schedule>[] | SlotSearchFunction; readonly questionnaire: Questionnaire | Reference<Questionnaire>; } export function Scheduler(props: SchedulerProps): JSX.Element | null { const medplum = useMedplum(); const questionnaire = useResource(props.questionnaire); const [month, setMonth] = useState<Date>(getStartMonth()); const [date, setDate] = useState<Date>(); const [response, setResponse] = useState<QuestionnaireResponse>(); const [actor, setActor] = useState<Reference<Practitioner> | undefined>(); const [slots, setSlots] = useState<Slot[]>(); const [selectedSlot, setSelectedSlot] = useState<Slot>(); useEffect(() => { if (!props.schedule) { setSlots([]); } // Function to fetch slots let fetchSlots: SlotSearchFunction; // If the user provides a function to fetch slots, use it if (typeof props.schedule === 'function') { fetchSlots = props.schedule; } else { // Otherwise, search based on the schedule(s) provided fetchSlots = async (period: Period): Promise<Slot[]> => { const scheduleArray: string[] = []; if (!Array.isArray(props.schedule)) { scheduleArray.push( isReference<Schedule>(props.schedule, 'Schedule') ? props.schedule.reference : getReferenceString(props.schedule as WithId<Schedule>) ); } else { for (const schedule of props.schedule) { if (isReference(schedule)) { scheduleArray.push(schedule.reference as string); } else { const scheduleRef = getReferenceString(schedule as WithId<Schedule>); scheduleArray.push(scheduleRef); } } } const slotSearchParams = new URLSearchParams([ ['_count', (30 * 24).toString()], ['schedule', scheduleArray.join(',')], ['start', 'gt' + period.start], ['start', 'lt' + period.end], ]); return medplum.searchResources('Slot', slotSearchParams); }; // If a single schedule is provided, set the actor if (props.schedule && !Array.isArray(props.schedule)) { if (isReference(props.schedule)) { medplum .readReference<Schedule>(props.schedule as Reference<Schedule>) .then((schedule) => { const actorRef = schedule.actor?.[0] as Reference<Practitioner>; setActor(actorRef); }) .catch(console.error); } else { setActor((props.schedule as Schedule).actor?.[0] as Reference<Practitioner>); } } } fetchSlots({ start: getStart(month), end: getEnd(month) }) .then(setSlots) .catch(console.error); }, [medplum, props.schedule, month]); // Create a map of start times to slots to handle duplicate start times const startTimeToSlotMap = useMemo(() => { if (!date) { return null; } const sortedSlots = (slots || []) // Filter slots to only include those that are within the date range .filter((slot) => { return ( new Date(slot.start as string).getTime() > date.getTime() && new Date(slot.start as string).getTime() < date.getTime() + 24 * 3600 * 1000 ); }) // Sort slots by start time .sort((a, b) => { return new Date(a.start as string).getTime() - new Date(b.start as string).getTime(); }); const startTimeToSlotMap = new Map<string, Slot>(); for (const slot of sortedSlots) { startTimeToSlotMap.set(formatTime(new Date(slot.start as string)), slot); } return startTimeToSlotMap; }, [slots, date]); if (!slots || !questionnaire) { return null; } return ( <div className={classes.container} data-testid="scheduler"> <div className={classes.info}> {actor && <ResourceAvatar value={actor} size="xl" />} {actor && ( <Text size="xl" fw={500}> <ResourceName value={actor} /> </Text> )} <p>1 hour</p> {date && <p>{date.toLocaleDateString()}</p>} {selectedSlot && <p>{formatTime(new Date(selectedSlot.start as string))}</p>} </div> <div className={classes.selection}> {!date && ( <div> <h3>Select date</h3> <CalendarInput slots={slots} onChangeMonth={setMonth} onClick={setDate} /> </div> )} {date && !selectedSlot && ( <div> <h3>Select time</h3> <Stack> {Array.from(startTimeToSlotMap?.entries() ?? []).map(([startTime, slot]) => { return ( <div key={slot.id}> <Button variant="outline" style={{ width: 150 }} onClick={() => setSelectedSlot(slot)}> {startTime} </Button> </div> ); })} </Stack> </div> )} {date && selectedSlot && !response && ( <QuestionnaireForm questionnaire={questionnaire} submitButtonText="Next" onSubmit={setResponse} /> )} {date && selectedSlot && response && ( <div> <h3>You're all set!</h3> <p>Check your email for a calendar invite.</p> </div> )} </div> </div> ); } function getStart(month: Date): string { return formatSlotInstant(month.getTime()); } function getEnd(month: Date): string { return formatSlotInstant(month.getTime() + 31 * 24 * 60 * 60 * 1000); } function formatSlotInstant(time: number): string { const date = new Date(Math.max(Date.now(), time)); date.setHours(0, 0, 0, 0); return date.toISOString(); } function formatTime(date: Date): string { return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server