Skip to content

Commit fa3f325

Browse files
committed
Merge branch 'develop' into plyshnykov/otr-1681-invoicing-and-invoiceable-patients-report-features-refactor
# Conflicts: # packages/zambdas/src/ehr/invoiceable-patients-report/index.ts
2 parents d3702ec + 6ed8495 commit fa3f325

File tree

201 files changed

+31834
-10973
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

201 files changed

+31834
-10973
lines changed

.vscode/Ottehr.code-workspace

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
{
2828
"name": "Test Utilities",
2929
"path": "../packages/test-utils"
30+
},
31+
{
32+
"name": "PACS Poll",
33+
"path": "../packages/pacs-poll"
3034
}
3135
],
3236
"settings": {
@@ -36,6 +40,11 @@
3640
}
3741
},
3842
"extensions": {
39-
"recommendations": ["dbaeumer.vscode-eslint", "streetsidesoftware.code-spell-checker", "eamodio.gitlens", "usernamehw.errorlens"]
43+
"recommendations": [
44+
"dbaeumer.vscode-eslint",
45+
"streetsidesoftware.code-spell-checker",
46+
"eamodio.gitlens",
47+
"usernamehw.errorlens"
48+
]
4049
}
4150
}

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,6 @@ npm run ehr:e2e:local:ui
137137

138138
Full E2E Documentation: [E2E_README.md](./E2E_README.md)
139139

140-
## Setting up Terminology Search
141-
142-
<!-- cSpell:disable-next umls -->
143-
144-
Ottehr uses UMLS Terminology Services for searching for ICD-10 and CPT codes.
145-
146-
To set up the terminology search service, please follow these instructions in the [Oystehr docs](https://docs.oystehr.com/oystehr/services/zambda/examples/terminology-search/#1-get-a-national-library-of-medicine-api-key), and then save the API key as `NLM_API_KEY` in the Zambdas secrets.
147-
148140
## Repository Structure
149141

150142
This repository uses a monorepo structure.

apps/ehr/package.json

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ehr-ui",
3-
"version": "1.27.9",
3+
"version": "1.27.25",
44
"private": true,
55
"type": "module",
66
"engines": {
@@ -45,12 +45,7 @@
4545
"e2e:specs:ui": "npm run e2e-skeleton -- ./tests/e2e/specs --ui $PLAYWRIGHT_EXTRA_ARGS",
4646
"e2e:manual-login": "node ./auth.setup.js",
4747
"e2e-skeleton": "env-cmd -f ./env/tests.${ENV}.json playwright test",
48-
"setup-test-deps": "node setup-test-deps.js",
49-
"ci-deploy:development": "VITE_APP_ENV=development VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') npm run build:development && aws s3 sync build/ s3://ottehr-dee6ce00-3e2f-45bc-bb3f-ab534e660109-ehr.ottehr.com --region us-east-1 --delete && aws cloudfront create-invalidation --distribution-id E32BZJ9BV9S9EY --paths '/*' --region us-east-1",
50-
"ci-deploy:testing": "VITE_APP_ENV=testing VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') npm run build:testing && aws s3 sync build/ s3://ottehr-aa616522-24b3-493b-b730-6cf48089c8bf-ehr.ottehr.com --region us-east-1 --delete && aws cloudfront create-invalidation --distribution-id EHWGHKXSEB2ZQ --paths '/*' --region us-east-1",
51-
"ci-deploy:staging": "VITE_APP_ENV=staging VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') npm run build:staging && aws s3 sync build/ s3://ottehr-4644976f-3ae4-490c-8c7a-f7ddb22e01e0-ehr.ottehr.com --region us-east-1 --delete && aws cloudfront create-invalidation --distribution-id EVMO86JIGAWU5 --paths '/*' --region us-east-1",
52-
"ci-deploy:demo": "VITE_APP_ENV=demo VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') npm run build:demo && aws s3 sync build/ s3://ehr.ottehr.com --region us-east-1 --delete && aws cloudfront create-invalidation --distribution-id E10TA6FN58D1OS --paths '/*' --region us-east-1",
53-
"ci-deploy-skeleton": "VITE_APP_ENV=${ENV} VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') npm run build:env --env=${ENV} && aws s3 sync build/ s3://ehr-${PREFIX}.ottehr.com --region us-east-1 --delete && aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths '/*' --region us-east-1"
48+
"setup-test-deps": "node setup-test-deps.js"
5449
},
5550
"dependencies": {
5651
"@fullcalendar/core": "^5.11.3",

apps/ehr/src/api/api.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import {
6060
GetLabOrdersParameters,
6161
GetNursingOrdersInput,
6262
GetOrUploadPatientProfilePhotoZambdaResponse,
63+
GetPatientBalancesZambdaInput,
64+
GetPatientBalancesZambdaOutput,
6365
GetPresignedFileURLInput,
6466
GetRadiologyOrderListZambdaInput,
6567
GetRadiologyOrderListZambdaOutput,
@@ -95,7 +97,11 @@ import {
9597
RecentPatientsReportZambdaOutput,
9698
SaveFollowupEncounterZambdaInput,
9799
SaveFollowupEncounterZambdaOutput,
100+
SavePreliminaryReportZambdaInput,
101+
SavePreliminaryReportZambdaOutput,
98102
ScheduleDTO,
103+
SendForFinalReadZambdaInput,
104+
SendForFinalReadZambdaOutput,
99105
SendReceiptByEmailZambdaInput,
100106
SendReceiptByEmailZambdaOutput,
101107
SubmitLabOrderInput,
@@ -180,6 +186,7 @@ const PENDING_SUPERVISOR_APPROVAL_ZAMBDA_ID = 'pending-supervisor-approval';
180186
const SEND_RECEIPT_BY_EMAIL_ZAMBDA_ID = 'send-receipt-by-email';
181187
const BULK_UPDATE_INSURANCE_STATUS_ZAMBDA_ID = 'bulk-update-insurance-status';
182188
const UPDATE_INVOICE_TASK_ZAMBDA_ID = 'update-invoice-task';
189+
const GET_PATIENT_BALANCES_ZAMBDA_ID = 'get-patient-balances';
183190

184191
export const getUser = async (token: string): Promise<User> => {
185192
const oystehr = new Oystehr({
@@ -972,6 +979,38 @@ export const radiologyLaunchViewer = async (
972979
}
973980
};
974981

982+
export const savePreliminaryReport = async (
983+
oystehr: Oystehr,
984+
parameters: SavePreliminaryReportZambdaInput
985+
): Promise<SavePreliminaryReportZambdaOutput> => {
986+
try {
987+
const response = await oystehr.zambda.execute({
988+
id: 'radiology-save-preliminary-report',
989+
...parameters,
990+
});
991+
return chooseJson(response);
992+
} catch (error: unknown) {
993+
console.log(error);
994+
throw error;
995+
}
996+
};
997+
998+
export const sendForFinalRead = async (
999+
oystehr: Oystehr,
1000+
parameters: SendForFinalReadZambdaInput
1001+
): Promise<SendForFinalReadZambdaOutput> => {
1002+
try {
1003+
const response = await oystehr.zambda.execute({
1004+
id: 'radiology-send-for-final-read',
1005+
...parameters,
1006+
});
1007+
return chooseJson(response);
1008+
} catch (error: unknown) {
1009+
console.log(error);
1010+
throw error;
1011+
}
1012+
};
1013+
9751014
export const getRadiologyOrders = async (
9761015
oystehr: Oystehr,
9771016
parameters: GetRadiologyOrderListZambdaInput
@@ -1466,3 +1505,23 @@ export const updateInvoiceTask = async (oystehr: Oystehr, parameters: UpdateInvo
14661505
throw apiErrorToThrow(error);
14671506
}
14681507
};
1508+
1509+
export const getPatientBalances = async (
1510+
oystehr: Oystehr,
1511+
parameters: GetPatientBalancesZambdaInput
1512+
): Promise<GetPatientBalancesZambdaOutput> => {
1513+
try {
1514+
if (GET_PATIENT_BALANCES_ZAMBDA_ID == null) {
1515+
throw new Error('get patient balances environment variable could not be loaded');
1516+
}
1517+
1518+
const response = await oystehr.zambda.execute({
1519+
id: GET_PATIENT_BALANCES_ZAMBDA_ID,
1520+
...parameters,
1521+
});
1522+
return chooseJson(response);
1523+
} catch (error: unknown) {
1524+
console.log(error);
1525+
throw apiErrorToThrow(error);
1526+
}
1527+
};
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { otherColors } from '@ehrTheme/colors';
2+
import { Alert, Box, CircularProgress, Paper, Snackbar, Typography } from '@mui/material';
3+
import { QueryObserverResult, RefetchOptions, useMutation } from '@tanstack/react-query';
4+
import { Patient } from 'fhir/r4b';
5+
import { DateTime } from 'luxon';
6+
import { Fragment, ReactElement, useState } from 'react';
7+
import { Link } from 'react-router-dom';
8+
import { RoundedButton } from 'src/components/RoundedButton';
9+
import { useApiClients } from 'src/hooks/useAppClients';
10+
import {
11+
APIError,
12+
CashOrCardPayment,
13+
GetPatientBalancesZambdaOutput,
14+
isApiError,
15+
PostPatientPaymentInput,
16+
} from 'utils';
17+
import PaymentDialog from './dialogs/PaymentDialog';
18+
19+
export interface PaymentBalancesProps {
20+
patient: Patient | undefined;
21+
patientBalances: GetPatientBalancesZambdaOutput | undefined;
22+
refetchPatientBalances: (
23+
options?: RefetchOptions | undefined
24+
) => Promise<QueryObserverResult<GetPatientBalancesZambdaOutput, Error>>;
25+
}
26+
27+
export default function PatientBalances({
28+
patient,
29+
patientBalances,
30+
refetchPatientBalances,
31+
}: PaymentBalancesProps): ReactElement {
32+
const { encounters } = patientBalances || { encounters: [] };
33+
34+
// for payment dialog
35+
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
36+
const { oystehrZambda } = useApiClients();
37+
const createNewPayment = useMutation({
38+
mutationFn: async (input: PostPatientPaymentInput) => {
39+
if (oystehrZambda && input) {
40+
return oystehrZambda.zambda
41+
.execute({
42+
id: 'patient-payments-post',
43+
...input,
44+
})
45+
.then(async () => {
46+
await refetchPatientBalances();
47+
setPaymentDialogOpen(false);
48+
});
49+
}
50+
},
51+
retry: 0,
52+
});
53+
const [selectedEncounter, setSelectedEncounter] = useState<{ appointmentId: string; encounterId: string }>({
54+
appointmentId: '',
55+
encounterId: '',
56+
});
57+
58+
// for snackbar
59+
const errorMessage = (() => {
60+
const networkError = createNewPayment.error;
61+
if (networkError) {
62+
if (isApiError(networkError)) {
63+
return (networkError as APIError).message;
64+
}
65+
return 'Something went wrong. Payment was not completed.';
66+
}
67+
return null;
68+
})();
69+
70+
return (
71+
<Paper
72+
sx={{
73+
marginTop: 2,
74+
padding: 3,
75+
}}
76+
>
77+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
78+
<Typography variant="h4" color="primary.dark">
79+
Outstanding Balance
80+
</Typography>
81+
<Typography variant="h4" color="error.dark">
82+
${((patientBalances?.totalBalanceCents ?? 0) / 100).toFixed(2)}
83+
</Typography>
84+
</Box>
85+
{patientBalances ? (
86+
<Box
87+
sx={{
88+
display: 'grid',
89+
gridTemplateColumns: '1fr 1fr 1fr 1fr',
90+
gap: 2,
91+
mt: 2,
92+
alignItems: 'center',
93+
backgroundColor: 'background.default',
94+
p: 2,
95+
borderRadius: 1,
96+
}}
97+
>
98+
{encounters.map((encounter, index) => (
99+
<Fragment key={encounter.encounterId}>
100+
<Box sx={{ display: 'contents' }}>
101+
<Box
102+
component={Link}
103+
to={`/visit/${encounter.appointmentId}`}
104+
target="_blank"
105+
sx={{ color: 'primary.main', textDecoration: 'none' }}
106+
>
107+
{encounter.appointmentId}
108+
</Box>
109+
<Box sx={{ color: 'text.primary' }}>
110+
{DateTime.fromISO(encounter.encounterDate).toFormat('MM/dd/yyyy')}
111+
</Box>
112+
<Box
113+
sx={{
114+
color: 'text.primary',
115+
fontWeight: 'bold',
116+
textAlign: 'right',
117+
}}
118+
>{`$${(encounter.patientBalanceCents / 100).toFixed(2)}`}</Box>
119+
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
120+
<RoundedButton
121+
onClick={() => {
122+
setPaymentDialogOpen(true);
123+
setSelectedEncounter({
124+
appointmentId: encounter.appointmentId,
125+
encounterId: encounter.encounterId,
126+
});
127+
}}
128+
>
129+
Pay for visit
130+
</RoundedButton>
131+
</Box>
132+
{index !== encounters.length - 1 && (
133+
<Box
134+
sx={{
135+
gridColumn: '1 / -1',
136+
borderBottom: `1px solid ${otherColors.dottedLine}`,
137+
}}
138+
/>
139+
)}
140+
</Box>
141+
</Fragment>
142+
))}
143+
</Box>
144+
) : (
145+
<CircularProgress />
146+
)}
147+
{patient && (
148+
<PaymentDialog
149+
open={paymentDialogOpen}
150+
patient={patient}
151+
appointmentId={selectedEncounter.appointmentId}
152+
handleClose={() => setPaymentDialogOpen(false)}
153+
isSubmitting={createNewPayment.isPending}
154+
submitPayment={async (data: CashOrCardPayment) => {
155+
const postInput: PostPatientPaymentInput = {
156+
patientId: patient.id ?? '',
157+
encounterId: selectedEncounter.encounterId,
158+
paymentDetails: data,
159+
};
160+
await createNewPayment.mutateAsync(postInput);
161+
}}
162+
/>
163+
)}
164+
<Snackbar open={errorMessage !== null} autoHideDuration={6000} onClose={() => createNewPayment.reset()}>
165+
<Alert severity="error" onClose={() => createNewPayment.reset()} sx={{ width: '100%' }}>
166+
{errorMessage}
167+
</Alert>
168+
</Snackbar>
169+
</Paper>
170+
);
171+
}

apps/ehr/src/components/PatientPaymentsList.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Appointment, DocumentReference, Encounter, Organization, Patient } from
2424
import { DateTime } from 'luxon';
2525
import { enqueueSnackbar } from 'notistack';
2626
import { FC, Fragment, ReactElement, useState } from 'react';
27+
import { getEligibilityCheckDetailsForCoverage } from 'src/features/visits/shared/components/patient/InsuranceSection';
2728
import { useOystehrAPIClient } from 'src/features/visits/shared/hooks/useOystehrAPIClient';
2829
import { useApiClients } from 'src/hooks/useAppClients';
2930
import { useEncounterReceipt, useGetEncounter } from 'src/hooks/useEncounter';
@@ -33,6 +34,7 @@ import {
3334
APIError,
3435
APIErrorCode,
3536
CashOrCardPayment,
37+
CoverageCheckWithDetails,
3638
FHIR_EXTENSION,
3739
getCoding,
3840
getPaymentVariantFromEncounter,
@@ -293,16 +295,24 @@ export default function PatientPaymentList({
293295
(extensionTemp) => extensionTemp.url === FHIR_EXTENSION.InsurancePlan.notes.url
294296
)?.valueString;
295297

298+
let coverageCheck: CoverageCheckWithDetails | undefined = undefined;
299+
if (insuranceCoverages?.coverages?.primary && insuranceData?.coverageChecks) {
300+
coverageCheck = getEligibilityCheckDetailsForCoverage(
301+
insuranceCoverages?.coverages?.primary,
302+
insuranceData?.coverageChecks
303+
);
304+
}
305+
296306
const copayAmount = getPaymentAmountFromPatientBenefit({
297-
coverage: insuranceData?.coverageChecks?.[0]?.copay || [],
307+
coverage: coverageCheck?.copay?.filter((item) => item.inNetwork === true) || [],
298308
code: 'UC',
299309
coverageCode: 'B',
300310
levelCode: 'IND',
301311
periodCode: undefined,
302312
});
303313

304314
const remainingDeductibleAmount = getPaymentAmountFromPatientBenefit({
305-
coverage: insuranceData?.coverageChecks?.[0]?.deductible || [],
315+
coverage: coverageCheck?.deductible?.filter((item) => item.inNetwork === true) || [],
306316
code: '30',
307317
coverageCode: 'C',
308318
levelCode: 'IND',

0 commit comments

Comments
 (0)