Skip to main content
Glama
AddPlanDefinition.tsx8.43 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Box, Button, Card, Grid, Loader, Modal, Paper, ScrollArea, Stack, Text, TextInput, Title, } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { normalizeErrorString } from '@medplum/core'; import type { PlanDefinition } from '@medplum/fhirtypes'; import { useMedplum } from '@medplum/react'; import { IconCircleCheck, IconCircleOff } from '@tabler/icons-react'; import cx from 'clsx'; import { useCallback, useEffect, useState } from 'react'; import type { JSX } from 'react'; import classes from './AddPlanDefinition.module.css'; interface AddPlanDefinitionProps { encounterId: string; patientId: string; onApply: () => void; } export const AddPlanDefinition = ({ encounterId, patientId, onApply }: AddPlanDefinitionProps): JSX.Element => { const [opened, setOpened] = useState(false); const [planDefinitions, setPlanDefinitions] = useState<PlanDefinition[]>([]); const [selectedPlanDefinition, setSelectedPlanDefinition] = useState<PlanDefinition | undefined>(); const [searchQuery, setSearchQuery] = useState<string>(''); const [isLoading, setIsLoading] = useState(false); const medplum = useMedplum(); const handleApplyPlanDefinition = async (): Promise<void> => { if (!selectedPlanDefinition) { showNotification({ color: 'red', icon: <IconCircleOff />, title: 'Error', message: 'No plan definition selected', }); return; } try { await medplum.post(medplum.fhirUrl('PlanDefinition', selectedPlanDefinition.id as string, '$apply'), { resourceType: 'Parameters', parameter: [ { name: 'subject', valueString: `Patient/${patientId}`, }, { name: 'encounter', valueString: `Encounter/${encounterId}`, }, ], }); showNotification({ color: 'green', icon: <IconCircleCheck />, title: 'Success', message: 'Plan definition applied to the encounter', }); onApply(); handleClose(); } catch (error) { showNotification({ color: 'red', icon: <IconCircleOff />, title: 'Error', message: normalizeErrorString(error), }); } }; const fetchPlanDefinitions = useCallback( (query?: string) => { setIsLoading(true); const searchParam = query ? `name=${query}` : undefined; medplum .searchResources('PlanDefinition', searchParam) .then((result) => { const filteredResult = result.filter((pd) => pd.name?.trim()); setPlanDefinitions(filteredResult); }) .catch((err) => { showNotification({ color: 'red', icon: <IconCircleOff />, title: 'Error', message: normalizeErrorString(err), }); }) .finally(() => { setIsLoading(false); }); }, [medplum] ); useEffect(() => { if (opened) { fetchPlanDefinitions(); } }, [opened, fetchPlanDefinitions]); useEffect(() => { const debounceTimer = setTimeout(() => { if (opened) { fetchPlanDefinitions(searchQuery); } }, 300); return () => clearTimeout(debounceTimer); }, [searchQuery, opened, fetchPlanDefinitions]); const handleClose = (): void => { setOpened(false); setSelectedPlanDefinition(undefined); setSearchQuery(''); }; return ( <> <Stack gap="md"> <Button variant="outline" color="blue" fullWidth onClick={() => setOpened(true)}> Add care template </Button> <Text size="sm">Task groups predefined by care planner</Text> </Stack> <Modal opened={opened} onClose={handleClose} title="Add Care Template" size="75%" styles={{ title: { fontSize: '1.2rem', fontWeight: 600, }, body: { padding: '0', height: '80vh', }, }} > <Stack h="100%" justify="space-between" gap={0}> <Box flex={1} miw={0}> <Grid p="md"> <Grid.Col span={6} pr="md"> <Box> <Title order={5} mb="xs"> Select care template </Title> <Text size="sm" c="dimmed" mb="md"> Care templates are predefined sets of actions that can be applied to encounters. </Text> <TextInput placeholder="Search by name" mb="md" value={searchQuery} onChange={(event) => setSearchQuery(event.currentTarget.value)} rightSection={isLoading ? <Loader size={16} /> : null} styles={{ input: { '&:focus': { borderColor: '#228be6', }, }, }} /> <ScrollArea style={{ height: 'calc(80vh - 250px)' }} type="scroll"> {planDefinitions.length > 0 && planDefinitions.map((plan) => ( <Card key={plan.id} p="md" mb="xs" className={cx(classes.planDefinition, { [classes.selected]: selectedPlanDefinition?.id === plan.id, })} onClick={() => setSelectedPlanDefinition(plan)} > <Text fz="md" fw={500} c={selectedPlanDefinition?.id === plan.id ? 'white' : undefined}> {plan.name} </Text> <Text fw={500} c={selectedPlanDefinition?.id === plan.id ? 'white' : 'dimmed'}> {plan.subtitle} </Text> </Card> ))} {planDefinitions.length === 0 && !isLoading && ( <Paper className={classes.notFound} h={40}> <Text>Nothing found! Try searching for another name of the care plan.</Text> </Paper> )} </ScrollArea> </Box> </Grid.Col> <Grid.Col span={6}> <Paper withBorder className={classes.preview}> <ScrollArea style={{ height: 'calc(80vh - 110px)' }} type="scroll"> <Stack gap="sm" px="md" pt="md"> <Title order={5}>Preview</Title> {selectedPlanDefinition ? ( <> <Stack gap={0} p={0}> <Text fz="md" fw={500}> {selectedPlanDefinition.name} </Text> <Text fw={500} c="dimmed"> {selectedPlanDefinition.subtitle} </Text> </Stack> <Stack gap="xs" pb="md"> {selectedPlanDefinition.action?.map((action, index) => ( <Card key={`${action.id}-task-${index}`} withBorder shadow="sm"> <Text fw={500}>{action.title}</Text> {action.description && <Text c="dimmed">{action.description}</Text>} </Card> ))} </Stack> </> ) : ( <Text c="dimmed">Select a template to see preview.</Text> )} </Stack> </ScrollArea> </Paper> </Grid.Col> </Grid> </Box> <Box className={classes.footer} h={70} p="md"> <Button onClick={handleApplyPlanDefinition}>Add care template</Button> </Box> </Stack> </Modal> </> ); };

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