Skip to main content
Glama
OrderLabsPage.tsx9.81 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Container, Group, Input, Radio, Stack, TextInput } from '@mantine/core'; import { ContentType, createReference } from '@medplum/core'; import type { MedplumClient } from '@medplum/core'; import type { Encounter, Patient, Practitioner, Reference, ServiceRequest, Task } from '@medplum/fhirtypes'; import { NPI_SYSTEM } from '@medplum/health-gorilla-core'; import type { BillingInformation, DiagnosisCodeableConcept, LabOrderInputErrors, LabOrganization, TestCoding, } from '@medplum/health-gorilla-core'; import { HealthGorillaLabOrderProvider, useHealthGorillaLabOrder } from '@medplum/health-gorilla-react'; import { AsyncAutocomplete, DateTimeInput, Panel, ResourceInput, useMedplum, useResource, ValueSetAutocomplete, } from '@medplum/react'; import type { AsyncAutocompleteOption } from '@medplum/react'; import { PerformingLabInput } from '../../components/PerformingLabInput'; import { TestMetadataCardInput } from '../../components/TestMetadataCardInput'; import { CoverageInput } from '../../components/CoverageInput'; import { useState, useEffect, useRef } from 'react'; import type { JSX } from 'react'; import { useParams } from 'react-router'; import { showErrorNotification } from '../../utils/notifications'; import { showNotification } from '@mantine/notifications'; async function sendLabOrderToHealthGorilla(medplum: MedplumClient, labOrder: ServiceRequest): Promise<void> { return medplum.executeBot( { system: 'https://www.medplum.com/integrations/bot-identifier', value: 'health-gorilla-labs/send-to-health-gorilla', }, labOrder, ContentType.FHIR_JSON ); } export interface OrderLabsPageProps { encounter?: Encounter | Reference<Encounter> | undefined; task?: Task | Reference<Task> | undefined; tests?: TestCoding[] | undefined; performingLab?: LabOrganization | undefined; onSubmitLabOrder: (serviceRequest?: ServiceRequest) => void; } export function OrderLabsPage(props: OrderLabsPageProps): JSX.Element { const { encounter, task, tests, performingLab, onSubmitLabOrder } = props; const medplum = useMedplum(); const { patientId } = useParams(); const [patient, setPatient] = useState<Patient | undefined>(); const [requester, setRequester] = useState<Practitioner | undefined>(medplum.getProfile() as Practitioner); const encounterResource = useResource(encounter); const taskResource = useResource(task); const labOrderReturn = useHealthGorillaLabOrder({ patient, requester, }); const { state, searchAvailableTests, setTests, setDiagnoses, updateBillingInformation, setSpecimenCollectedDateTime, setOrderNotes, createOrderBundle, } = labOrderReturn; const [isSubmitting, setIsSubmitting] = useState(false); const [createError, setCreateError] = useState<{ generic?: unknown; validation?: LabOrderInputErrors } | undefined>(); const prevTestsRef = useRef<TestCoding[] | undefined>(undefined); useEffect(() => { // Only set tests if they're provided and different from the previous value // Compare by checking if arrays have different lengths or different test codes const testsChanged = tests && (prevTestsRef.current === undefined || prevTestsRef.current.length !== tests.length || prevTestsRef.current.some((prevTest, index) => prevTest.code !== tests[index]?.code)); if (testsChanged) { setTests(tests); prevTestsRef.current = tests; } }, [tests, setTests]); useEffect(() => { if (patientId) { medplum .readResource('Patient', patientId) .then((patient) => { setPatient(patient); }) .catch((error) => { showErrorNotification(error); }); } }, [patientId, medplum]); const submitOrder = async (): Promise<void> => { try { setIsSubmitting(true); setCreateError(undefined); let { serviceRequest } = await createOrderBundle(); if (encounterResource) { serviceRequest = { ...serviceRequest, encounter: createReference(encounterResource), }; } if (taskResource) { await medplum.updateResource({ ...taskResource, output: [ ...(taskResource.output || []), { type: { text: 'ServiceRequest' }, valueReference: createReference(serviceRequest), }, ], focus: createReference(serviceRequest), }); } await sendLabOrderToHealthGorilla(medplum, serviceRequest); showNotification({ title: 'Lab Order Submitted', message: 'The lab order has been successfully submitted.', color: 'green', }); onSubmitLabOrder({ ...serviceRequest, status: 'active' }); } catch (error) { setCreateError({ generic: error }); showErrorNotification(error); } finally { setIsSubmitting(false); } }; return ( <HealthGorillaLabOrderProvider {...labOrderReturn}> <Container size="md"> <Panel> <Stack gap="md"> <Input.Wrapper label="Requester" required error={createError?.validation?.requester?.message}> <ResourceInput<Practitioner> resourceType="Practitioner" name="Requester" onChange={setRequester} defaultValue={requester} searchCriteria={{ identifier: `${NPI_SYSTEM}|` }} /> </Input.Wrapper> <Input.Wrapper label="Patient" required error={createError?.validation?.patient?.message}> <ResourceInput<Patient> resourceType="Patient" name="patient" defaultValue={patient} onChange={setPatient} /> </Input.Wrapper> <PerformingLabInput patient={patient} performingLab={performingLab} error={createError?.validation?.performingLab} /> <div> <AsyncAutocomplete<TestCoding> required error={createError?.validation?.selectedTests?.message} label="Selected tests" disabled={!state.performingLab} maxValues={10} defaultValue={tests} loadOptions={searchAvailableTests} toOption={TestCodingToOption} onChange={setTests} /> {state.selectedTests.length > 0 && ( <Group mt="md" gap="md" align="flex-start" wrap="wrap"> {state.selectedTests.map((test: TestCoding) => ( <TestMetadataCardInput key={test.code} test={test} metadata={state.testMetadata[test.code]} error={createError?.validation?.testMetadata?.[test.code]} /> ))} </Group> )} </div> <div> <ValueSetAutocomplete label="Diagnoses" binding="http://hl7.org/fhir/sid/icd-10-cm" name="diagnoses" maxValues={10} onChange={(items) => { const codeableConcepts = items.map((item) => ({ coding: [item] })) as DiagnosisCodeableConcept[]; setDiagnoses(codeableConcepts); }} /> </div> <Group align="flex-start" gap={48}> <div> <Radio.Group value={state.billingInformation.billTo} error={createError?.validation?.billingInformation?.billTo?.message} onChange={(newBillTo) => { updateBillingInformation({ billTo: newBillTo as BillingInformation['billTo'] }); }} label="Bill to" withAsterisk > <Stack gap={4}> <Radio value="patient" label="Patient" /> <Radio value="insurance" label="Insurance" /> <Radio value="customer-account" label="Customer" /> </Stack> </Radio.Group> </div> {patient && ( <CoverageInput patient={patient} error={createError?.validation?.billingInformation?.patientCoverage} /> )} </Group> <TextInput label="Order notes" onChange={(e) => setOrderNotes(e.currentTarget.value)} /> <DateTimeInput label="Specimen collection time" name="" onChange={(isoDateTimeString) => { setSpecimenCollectedDateTime(isoDateTimeString ? new Date(isoDateTimeString) : undefined); }} /> <Group> <Button onClick={submitOrder} loading={isSubmitting} disabled={isSubmitting}> Submit Order </Button> </Group> </Stack> </Panel> </Container> </HealthGorillaLabOrderProvider> ); } function TestCodingToOption(element: TestCoding): AsyncAutocompleteOption<TestCoding> { return { value: toKey(element), label: getDisplay(element), resource: element, }; } function toKey(element: TestCoding): string { if (typeof element.code === 'string') { return element.code; } return JSON.stringify(element); } function getDisplay(item: TestCoding): string { if (typeof item.display === 'string') { return item.display; } return toKey(item); }

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