Skip to main content
Glama
SchedulePage.tsx7.96 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Group, Title } from '@mantine/core'; import { useDisclosure, usePrevious } from '@mantine/hooks'; import { getReferenceString } from '@medplum/core'; import type { Appointment, Practitioner, Schedule, Slot } from '@medplum/fhirtypes'; import { Document, useMedplum, useMedplumProfile } from '@medplum/react'; import dayjs from 'dayjs'; import { useCallback, useContext, useEffect, useState } from 'react'; import type { JSX } from 'react'; import { Calendar, dayjsLocalizer } from 'react-big-calendar'; import type { Event } from 'react-big-calendar'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import { useNavigate } from 'react-router'; import { BlockAvailability } from '../components/actions/BlockAvailability'; import { CreateAppointment } from '../components/actions/CreateAppointment'; import { CreateUpdateSlot } from '../components/actions/CreateUpdateSlot'; import { SetAvailability } from '../components/actions/SetAvailability'; import { SlotDetails } from '../components/SlotDetails'; import { ScheduleContext } from '../Schedule.context'; /** * Schedule page that displays the practitioner's schedule. * Allows the practitioner to set availability, block availability, create/update slots, and create * appointments. * @returns A React component that displays the schedule page. */ export function SchedulePage(): JSX.Element { const navigate = useNavigate(); const medplum = useMedplum(); const [blockAvailabilityOpened, blockAvailabilityHandlers] = useDisclosure(false); const [setAvailabilityOpened, setAvailabilityHandlers] = useDisclosure(false); const [slotDetailsOpened, slotDetailsHandlers] = useDisclosure(false); const [createUpdateSlotOpened, createUpdateSlotHandlers] = useDisclosure(false); const [createAppointmentOpened, createAppointmentHandlers] = useDisclosure(false); const [selectedEvent, setSelectedEvent] = useState<Event>(); const { schedule } = useContext(ScheduleContext); const profile = useMedplumProfile() as Practitioner; const prevSchedule = usePrevious(schedule); const prevProfile = usePrevious(profile); const [shouldRefreshCalender, setShouldRefreshCalender] = useState(true); const [slots, setSlots] = useState<Slot[]>(); const [appointments, setAppointments] = useState<Appointment[]>(); useEffect(() => { if ((schedule && prevSchedule?.id !== schedule?.id) || (profile && prevProfile?.id !== profile?.id)) { setShouldRefreshCalender(true); } }, [schedule, profile, prevSchedule?.id, prevProfile?.id]); useEffect(() => { async function searchSlots(): Promise<void> { const slots = await medplum.searchResources( 'Slot', { schedule: getReferenceString(schedule as Schedule), _count: '100', }, { cache: 'no-cache' } ); setSlots(slots); } async function searchAppointments(): Promise<void> { const appointments = await medplum.searchResources( 'Appointment', { actor: getReferenceString(profile as Practitioner), }, { cache: 'no-cache' } ); setAppointments(appointments); } if (shouldRefreshCalender) { Promise.allSettled([searchSlots(), searchAppointments()]) .then(() => setShouldRefreshCalender(false)) .catch(console.error); } }, [medplum, schedule, profile, shouldRefreshCalender]); // Converts Slot resources to big-calendar Event objects // Only show free and busy-unavailable slots const slotEvents: Event[] = (slots ?? []) .filter((slot) => slot.status === 'free' || slot.status === 'busy-unavailable') .map((slot) => ({ title: slot.status === 'free' ? 'Available' : 'Blocked', start: new Date(slot.start), end: new Date(slot.end), resource: slot, })); // Converts Appointment resources to big-calendar Event objects // Exclude cancelled appointments to prevent them from overlapping free slots during rendering const appointmentEvents: Event[] = (appointments ?? []) .filter((appointment) => appointment.status !== 'cancelled') .map((appointment) => { // Find the patient among the participants to use as title const patientParticipant = appointment?.participant?.find((p) => p.actor?.reference?.startsWith('Patient/')); const status = !['booked', 'arrived', 'fulfilled'].includes(appointment.status as string) ? ` (${appointment.status})` : ''; return { title: `${patientParticipant?.actor?.display} ${status}`, start: new Date(appointment.start as string), end: new Date(appointment.end as string), resource: appointment, }; }); /** * When a date/time range is selected, set the event object and open the create slot modal */ const handleSelectSlot = useCallback( (event: Event & { action?: string }) => { if (event.action !== 'select') { return; } setSelectedEvent(event); createUpdateSlotHandlers.open(); }, [createUpdateSlotHandlers] ); /** * When an existing event (slot/appointment) is selected, set the event object and open the * appropriate modal. * - If the event is a free slot, open the create appointment modal. * - If the event is a busy-unavailable slot, open the slot details modal. * - If the event is an appointment, navigate to the appointment page. */ const handleSelectEvent = useCallback( (event: Event) => { const { resourceType, status, id } = event.resource; function handleSlot(): void { setSelectedEvent(event); if (status === 'free') { createAppointmentHandlers.open(); } else { slotDetailsHandlers.open(); } } function handleAppointment(): void { navigate(`/Appointment/${id}`)?.catch(console.error); } if (resourceType === 'Slot') { handleSlot(); return; } if (resourceType === 'Appointment') { handleAppointment(); } }, [slotDetailsHandlers, createAppointmentHandlers, navigate] ); return ( <Document width={1000}> <Title order={1} mb="lg"> My Schedule </Title> <Group mb="lg"> <Button size="sm" onClick={() => setAvailabilityHandlers.open()}> Set Availability </Button> <Button size="sm" onClick={() => blockAvailabilityHandlers.open()}> Block Availability </Button> </Group> <Calendar defaultView="week" views={['month', 'week', 'day', 'agenda']} localizer={dayjsLocalizer(dayjs)} events={appointmentEvents} backgroundEvents={slotEvents} // Background events don't show in the month view onSelectSlot={handleSelectSlot} onSelectEvent={handleSelectEvent} scrollToTime={new Date()} // Default scroll to current time style={{ height: 600 }} selectable /> {/* Modals */} <SetAvailability opened={setAvailabilityOpened} handlers={setAvailabilityHandlers} /> <BlockAvailability opened={blockAvailabilityOpened} handlers={blockAvailabilityHandlers} /> <CreateUpdateSlot event={selectedEvent} opened={createUpdateSlotOpened} handlers={createUpdateSlotHandlers} onSlotsUpdated={() => setShouldRefreshCalender(true)} /> <SlotDetails event={selectedEvent} opened={slotDetailsOpened} handlers={slotDetailsHandlers} onSlotsUpdated={() => setShouldRefreshCalender(true)} /> <CreateAppointment event={selectedEvent} opened={createAppointmentOpened} handlers={createAppointmentHandlers} onAppointmentsUpdated={() => setShouldRefreshCalender(true)} /> </Document> ); }

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