Skip to main content
Glama
BillingTab.tsx9.52 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Card, Flex, Group, Menu, Stack } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import type { ChargeItem, Claim, ClaimDiagnosis, Condition, Coverage, Encounter, EncounterDiagnosis, Patient, Practitioner, } from '@medplum/fhirtypes'; import { IconDownload, IconFileText, IconSend } from '@tabler/icons-react'; import { useCallback, useEffect, useState } from 'react'; import type { JSX } from 'react'; import { VisitDetailsPanel } from './VisitDetailsPanel'; import { getReferenceString, HTTP_HL7_ORG } from '@medplum/core'; import { showErrorNotification } from '../../utils/notifications'; import { useMedplum } from '@medplum/react'; import { createSelfPayCoverage } from '../../utils/coverage'; import { ConditionList } from '../Conditions/ConditionList'; import { useDebouncedUpdateResource } from '../../hooks/useDebouncedUpdateResource'; import { ChargeItemList } from '../ChargeItem/ChargeItemList'; import { createClaimFromEncounter, getCptChargeItems } from '../../utils/claims'; import { SAVE_TIMEOUT_MS } from '../../config/constants'; import { calculateTotalPrice } from '../../utils/chargeitems'; import { useDebouncedCallback } from '@mantine/hooks'; interface BillingTabProps { patient: Patient; encounter: Encounter; setEncounter: (encounter: Encounter) => void; practitioner: Practitioner | undefined; setPractitioner: (practitioner: Practitioner) => void; chargeItems: ChargeItem[] | undefined; setChargeItems: (chargeItems: ChargeItem[]) => void; claim: Claim | undefined; setClaim: (claim: Claim) => void; } export const BillingTab = (props: BillingTabProps): JSX.Element => { const { encounter, setEncounter, claim, patient, practitioner, setPractitioner, chargeItems, setChargeItems, setClaim, } = props; const medplum = useMedplum(); const [conditions, setConditions] = useState<Condition[]>([]); const [coverage, setCoverage] = useState<Coverage | undefined>(); const debouncedUpdateResource = useDebouncedUpdateResource(medplum); useEffect(() => { const fetchCoverage = async (): Promise<void> => { if (!patient) { return; } const coverageResult = await medplum.searchResources('Coverage', `patient=${getReferenceString(patient)}`); if (coverageResult.length > 0) { setCoverage(coverageResult[0] as Coverage); } }; fetchCoverage().catch((err) => showErrorNotification(err)); }, [medplum, patient]); const exportClaimAsCMS1500 = async (): Promise<void> => { if (!claim?.id || !patient?.id) { return; } let coverageForClaim = coverage; if (!coverageForClaim) { const coverageResults = await medplum.searchResources( 'Coverage', `patient=${getReferenceString(patient)}&status=active` ); if (coverageResults === undefined) { showErrorNotification('Failed to fetch coverage information'); return; } if (coverageResults.length > 0) { coverageForClaim = coverageResults[0]; } else { coverageForClaim = await createSelfPayCoverage(medplum, patient.id); } } const diagnosisArray = createDiagnosisArray(conditions || []); const claimToExport: Claim = { ...claim, insurance: [ { sequence: 1, focal: true, coverage: { reference: getReferenceString(coverageForClaim) }, }, ], diagnosis: diagnosisArray, }; const response = await medplum.post(medplum.fhirUrl('Claim', '$export'), { resourceType: 'Parameters', parameter: [{ name: 'resource', resource: claimToExport }], }); if (response.resourceType === 'Media' && response.content?.url) { const url = response.content.url; window.open(url, '_blank'); } else { showErrorNotification('Failed to download PDF'); } }; const handleDiagnosisChange = useCallback( async (diagnosis: EncounterDiagnosis[]): Promise<void> => { const updatedEncounter = { ...encounter, diagnosis }; setEncounter(updatedEncounter); await debouncedUpdateResource(updatedEncounter); }, [encounter, setEncounter, debouncedUpdateResource] ); const handleEncounterChange = useDebouncedCallback(async (updatedEncounter: Encounter): Promise<void> => { try { const savedEncounter = await medplum.updateResource(updatedEncounter); setEncounter(savedEncounter); if (savedEncounter?.participant?.[0]?.individual) { const practitionerResult = await medplum.readReference(savedEncounter.participant[0].individual); setPractitioner(practitionerResult as Practitioner); } if (!patient?.id || !encounter?.id || !practitioner?.id || !chargeItems?.length) { return; } if (!claim) { const newClaim = await createClaimFromEncounter( medplum, patient.id, encounter.id, practitioner.id, chargeItems ); if (newClaim) { setClaim(newClaim); } } else { const providerRefNeedsUpdate = claim.provider?.reference !== getReferenceString(practitioner); if (providerRefNeedsUpdate) { const updatedClaim: Claim = await medplum.updateResource({ ...claim, provider: { reference: getReferenceString(practitioner) }, }); setClaim(updatedClaim); } } } catch (err) { showErrorNotification(err); } }, SAVE_TIMEOUT_MS); const updateChargeItems = useCallback( async (updatedChargeItems: ChargeItem[]): Promise<void> => { setChargeItems(updatedChargeItems); if (claim?.id && updatedChargeItems.length > 0 && encounter) { const updatedClaim: Claim = { ...claim, item: getCptChargeItems(updatedChargeItems, { reference: getReferenceString(encounter) }), total: { value: calculateTotalPrice(updatedChargeItems) }, }; setClaim(updatedClaim); await debouncedUpdateResource(updatedClaim); } }, [setChargeItems, claim, encounter, setClaim, debouncedUpdateResource] ); return ( <Stack gap="md"> {claim && ( <Card withBorder shadow="sm"> <Flex justify="space-between"> <Menu shadow="md" width={200}> <Menu.Target> <Button variant="outline" leftSection={<IconDownload size={16} />}> Export Claim </Button> </Menu.Target> <Menu.Dropdown> <Menu.Label>Export Options</Menu.Label> <Menu.Item leftSection={<IconFileText size={14} />} onClick={async () => { await exportClaimAsCMS1500(); }} > CMS 1500 Form </Menu.Item> <Menu.Item leftSection={<IconFileText size={14} />} onClick={() => { showNotification({ title: 'EDI X12', message: 'Please contact sales to enable EDI X12 export', color: 'blue', }); }} > EDI X12 </Menu.Item> <Menu.Item leftSection={<IconFileText size={14} />} onClick={() => { showNotification({ title: 'NUCC Crosswalk', message: 'Please contact sales to enable NUCC Crosswalk export', color: 'blue', }); }} > NUCC Crosswalk CSV </Menu.Item> </Menu.Dropdown> </Menu> <Button variant="outline" leftSection={<IconSend size={16} />}> Request to connect a billing service </Button> </Flex> </Card> )} <Group grow align="flex-start"> <VisitDetailsPanel practitioner={practitioner} encounter={encounter} onEncounterChange={handleEncounterChange} /> </Group> {encounter && ( <ConditionList patient={patient} encounter={encounter} conditions={conditions} setConditions={setConditions} onDiagnosisChange={handleDiagnosisChange} /> )} {chargeItems && ( <ChargeItemList patient={patient} encounter={encounter} chargeItems={chargeItems} updateChargeItems={updateChargeItems} /> )} </Stack> ); }; const createDiagnosisArray = (conditions: Condition[]): ClaimDiagnosis[] => { return conditions.map((condition, index) => { const icd10Coding = condition.code?.coding?.find((c) => c.system === `${HTTP_HL7_ORG}/fhir/sid/icd-10-cm`); return { diagnosisCodeableConcept: { coding: icd10Coding ? [ { ...icd10Coding, system: `${HTTP_HL7_ORG}/fhir/sid/icd-10`, }, ] : [], }, sequence: index + 1, type: [{ coding: [{ code: index === 0 ? 'principal' : 'secondary' }] }], }; }); };

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