// 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>
</>
);
};