Skip to main content
Glama
EncounterChart.tsx8.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Box, Card, Stack, Textarea, Title } from '@mantine/core'; import { createReference, getReferenceString } from '@medplum/core'; import type { ClinicalImpression, Encounter, Patient, Practitioner, Provenance, Reference, Task, } from '@medplum/fhirtypes'; import { Loading, useMedplum, useResource } from '@medplum/react'; import { useCallback, useEffect, useState } from 'react'; import type { JSX } from 'react'; import { SAVE_TIMEOUT_MS } from '../../config/constants'; import { useEncounterChart } from '../../hooks/useEncounterChart'; import { useDebouncedUpdateResource } from '../../hooks/useDebouncedUpdateResource'; import { ChartNoteStatus } from '../../types/encounter'; import { updateEncounterStatus } from '../../utils/encounter'; import { showErrorNotification } from '../../utils/notifications'; import { EncounterHeader } from './EncounterHeader'; import { SignAddendum } from './SignAddendum'; import { TaskPanel } from '../tasks/encounter/TaskPanel'; import { BillingTab } from './BillingTab'; const FHIR_ACT_REASON_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v3-ActReason'; const FHIR_PROVENANCE_PARTICIPANT_TYPE_SYSTEM = 'http://terminology.hl7.org/CodeSystem/provenance-participant-type'; const FHIR_DOCUMENT_COMPLETION_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v3-DocumentCompletion'; const TASK_COMPLETED_STATUSES = new Set<Task['status']>([ 'completed', 'cancelled', 'failed', 'rejected', 'entered-in-error', ]); export interface EncounterChartProps { encounter: Encounter | Reference<Encounter>; } export const EncounterChart = (props: EncounterChartProps): JSX.Element => { const { encounter: encounterProp } = props; const medplum = useMedplum(); const encounterResource = useResource(encounterProp); const patientReference = encounterResource?.subject as Reference<Patient> | undefined; const patientResource = useResource(patientReference); const [activeTab, setActiveTab] = useState<string>('notes'); const { encounter, claim, practitioner, tasks, clinicalImpression, chargeItems, appointment, setEncounter, setClaim, setPractitioner, setTasks, setChargeItems, } = useEncounterChart(encounterProp, patientReference); const [chartNote, setChartNote] = useState<string | undefined>(clinicalImpression?.note?.[0]?.text); const debouncedUpdateResource = useDebouncedUpdateResource(medplum, SAVE_TIMEOUT_MS); const [provenances, setProvenances] = useState<Provenance[]>([]); const [chartNoteStatus, setChartNoteStatus] = useState<ChartNoteStatus>(ChartNoteStatus.Unsigned); useEffect(() => { if (!encounter) { return; } const fetchProvenance = async (): Promise<void> => { const provenance = await medplum.searchResources('Provenance', `target=${getReferenceString(encounter)}`); setProvenances(provenance); if (provenance.length > 0 && clinicalImpression?.status === 'completed') { setChartNoteStatus(ChartNoteStatus.SignedAndLocked); } else if (provenance.length > 0) { setChartNoteStatus(ChartNoteStatus.Signed); } else { setChartNoteStatus(ChartNoteStatus.Unsigned); } }; fetchProvenance().catch((err) => showErrorNotification(err)); }, [clinicalImpression, encounter, medplum]); const updateTaskList = useCallback( (updatedTask: Task): void => { setTasks((prevTasks) => prevTasks.map((task) => (task.id === updatedTask.id ? updatedTask : task))); }, [setTasks] ); const handleEncounterStatusChange = useCallback( async (newStatus: Encounter['status']): Promise<void> => { if (!encounter) { return; } try { const updatedEncounter = await updateEncounterStatus(medplum, encounter, appointment, newStatus); setEncounter(updatedEncounter); } catch (err) { showErrorNotification(err); } }, [encounter, medplum, setEncounter, appointment] ); const handleTabChange = (tab: string): void => { setActiveTab(tab); }; const handleChartNoteChange = async (e: React.ChangeEvent<HTMLTextAreaElement>): Promise<void> => { setChartNote(e.target.value); if (!clinicalImpression) { return; } try { if (!e.target.value || e.target.value === '') { const { note: _, ...restOfClinicalImpression } = clinicalImpression; const updatedClinicalImpression: ClinicalImpression = restOfClinicalImpression; await debouncedUpdateResource(updatedClinicalImpression); } else { const updatedClinicalImpression: ClinicalImpression = { ...clinicalImpression, note: [{ text: e.target.value }], }; await debouncedUpdateResource(updatedClinicalImpression); } } catch (err) { showErrorNotification(err); } }; const handleSign = async (practitioner: Reference<Practitioner>, lock: boolean): Promise<void> => { if (!encounter) { return; } if (lock) { // Complete all incomplete tasks const tasksToUpdate = tasks.filter((task) => !TASK_COMPLETED_STATUSES.has(task.status)); const updatedTasks = await Promise.all( tasksToUpdate.map((task) => medplum.updateResource({ ...task, status: 'completed', }) ) ); setTasks( tasks.map((task) => { const updated = updatedTasks.find((t) => t.id === task.id); return updated || task; }) ); } // Create provenance record with signature const newProvenance = await medplum.createResource<Provenance>({ resourceType: 'Provenance', target: [createReference(encounter)], recorded: new Date().toISOString(), reason: [ { coding: [ { system: FHIR_ACT_REASON_SYSTEM, code: 'SIGN', display: 'Signed', }, ], }, ], agent: [ { type: { coding: [ { system: FHIR_PROVENANCE_PARTICIPANT_TYPE_SYSTEM, code: 'author', }, ], }, who: practitioner, }, ], signature: [ { type: [ { system: FHIR_DOCUMENT_COMPLETION_SYSTEM, code: 'LA', display: 'legally authenticated', }, ], when: new Date().toISOString(), who: practitioner, }, ], }); setProvenances([...provenances, newProvenance]); if (lock) { setChartNoteStatus(ChartNoteStatus.SignedAndLocked); } else { setChartNoteStatus(ChartNoteStatus.Signed); } }; if (!patientResource || !encounter) { return <Loading />; } return ( <> <Stack justify="space-between" gap={0}> <EncounterHeader encounter={encounter} chartNoteStatus={chartNoteStatus} practitioner={practitioner} onStatusChange={handleEncounterStatusChange} onTabChange={handleTabChange} onSign={handleSign} /> <Box p="md"> {activeTab === 'notes' && ( <Stack gap="md"> <SignAddendum encounter={encounter} provenances={provenances} chartNoteStatus={chartNoteStatus} /> {clinicalImpression && ( <Card withBorder shadow="sm" mt="md"> <Title>Fill chart note</Title> <Textarea defaultValue={clinicalImpression.note?.[0]?.text} value={chartNote} onChange={handleChartNoteChange} autosize minRows={4} maxRows={8} disabled={chartNoteStatus === ChartNoteStatus.SignedAndLocked} /> </Card> )} {tasks.map((task: Task) => ( <TaskPanel key={task.id} task={task} onUpdateTask={updateTaskList} enabled={chartNoteStatus !== ChartNoteStatus.SignedAndLocked} /> ))} </Stack> )} {activeTab === 'details' && ( <BillingTab encounter={encounter} setEncounter={setEncounter} claim={claim} patient={patientResource} practitioner={practitioner} setPractitioner={setPractitioner} chargeItems={chargeItems} setChargeItems={setChargeItems} setClaim={setClaim} /> )} </Box> </Stack> </> ); };

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