Skip to content

Commit 2e2210d

Browse files
authored
Merge pull request #6193 from masslight/release/1.27
Release/1.27
2 parents 6052bf7 + 2ef3e97 commit 2e2210d

File tree

305 files changed

+31885
-4272
lines changed

Some content is hidden

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

305 files changed

+31885
-4272
lines changed

apps/ehr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ehr-ui",
3-
"version": "1.26.14",
3+
"version": "1.27.25",
44
"private": true,
55
"type": "module",
66
"engines": {

apps/ehr/src/components/CancelVisitDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const CancelVisitDialog = ({ onClose }: CancelVisitDialogProps): ReactElement =>
3232
const { id: appointmentID } = useParams();
3333
const navigate = useNavigate();
3434

35-
const cancellationReasons = VALUE_SETS.cancelReasonOptionsVirtualProviderSide;
35+
const cancellationReasons = VALUE_SETS.cancelReasonOptionsVirtualProvider;
3636

3737
const handleReasonChange = (event: SelectChangeEvent<string>): void => {
3838
setReason(event.target.value);
@@ -64,7 +64,7 @@ const CancelVisitDialog = ({ onClose }: CancelVisitDialogProps): ReactElement =>
6464
try {
6565
const response = await cancelTelemedAppointment(oystehrZambda, {
6666
appointmentID: appointmentID || '',
67-
cancellationReason: typedReason,
67+
cancellationReason: typedReason.value,
6868
cancellationReasonAdditional: otherReason,
6969
});
7070
console.log('Appointment cancelled successfully', response);

apps/ehr/src/components/LocationSelect.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ export default function LocationSelect({
104104

105105
const getLocationLabel = (location: LocationWithWalkinSchedule): string => {
106106
if (!location.name) {
107-
console.log('Location name is undefined', location);
108107
return 'Unknown Location';
109108
}
110109
return location.address?.state ? `${location.address.state.toUpperCase()} - ${location.name}` : location.name;

apps/ehr/src/components/PatientEncountersGrid.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,11 @@ export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) =>
545545
) : (
546546
paginatedData.map((row, index) => {
547547
const rowId = row.appointmentId || `row-${index}`;
548-
const followupEncountersForRow = row.followUps ?? [];
548+
const followupEncountersForRow = [...(row.followUps ?? [])].sort(
549+
(a, b) =>
550+
DateTime.fromISO(a.dateTime ?? '').diff(DateTime.fromISO(b.dateTime ?? ''), 'milliseconds')
551+
.milliseconds
552+
);
549553
const hasFollowups = followupEncountersForRow.length > 0;
550554

551555
return (

apps/ehr/src/components/PatientPaymentsList.tsx

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,18 @@ import { useMutation } from '@tanstack/react-query';
2323
import { Appointment, DocumentReference, Encounter, Organization, Patient } from 'fhir/r4b';
2424
import { DateTime } from 'luxon';
2525
import { enqueueSnackbar } from 'notistack';
26-
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
26+
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';
29-
import { useGetEncounter } from 'src/hooks/useEncounter';
30+
import { useEncounterReceipt, useGetEncounter } from 'src/hooks/useEncounter';
3031
import { useGetPatientAccount } from 'src/hooks/useGetPatient';
3132
import { useGetPatientPaymentsList } from 'src/hooks/useGetPatientPaymentsList';
3233
import {
3334
APIError,
3435
APIErrorCode,
3536
CashOrCardPayment,
37+
CoverageCheckWithDetails,
3638
FHIR_EXTENSION,
3739
getCoding,
3840
getPaymentVariantFromEncounter,
@@ -42,7 +44,6 @@ import {
4244
PatientPaymentDTO,
4345
PaymentVariant,
4446
PostPatientPaymentInput,
45-
RECEIPT_CODE,
4647
SendReceiptByEmailZambdaInput,
4748
SERVICE_CATEGORY_SYSTEM,
4849
updateEncounterPaymentVariantExtension,
@@ -88,13 +89,19 @@ export default function PatientPaymentList({
8889
const theme = useTheme();
8990
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
9091
const [sendReceiptByEmailDialogOpen, setSendReceiptByEmailDialogOpen] = useState(false);
91-
const [receiptDocRefId, setReceiptDocRefId] = useState<string | undefined>();
92+
9293
const {
9394
data: encounter,
9495
refetch: refetchEncounter,
9596
isRefetching: isEncounterRefetching,
9697
} = useGetEncounter({ encounterId });
9798

99+
const {
100+
data: receiptDocRef,
101+
refetch: refetchReceipt,
102+
isFetching: isReceiptFetching,
103+
} = useEncounterReceipt({ encounterId });
104+
98105
const {
99106
data: paymentData,
100107
refetch: refetchPaymentList,
@@ -121,15 +128,21 @@ export default function PatientPaymentList({
121128
code: string;
122129
coverageCode: string;
123130
levelCode: string;
124-
periodCode: string;
125-
}): number | undefined {
126-
return coverage?.find(
131+
periodCode: string | undefined;
132+
}): PatientPaymentBenefit | undefined {
133+
if (!periodCode) {
134+
return coverage.find(
135+
(item) => item.code === code && item.coverageCode === coverageCode && item.levelCode === levelCode
136+
);
137+
}
138+
139+
return coverage.find(
127140
(item) =>
128141
item.code === code &&
129142
item.coverageCode === coverageCode &&
130143
item.levelCode === levelCode &&
131144
item.periodCode === periodCode
132-
)?.amountInUSD;
145+
);
133146
}
134147

135148
const payments = paymentData?.payments ?? []; // Replace with actual payments when available
@@ -139,30 +152,7 @@ export default function PatientPaymentList({
139152
? (paymentListError as APIError).code === APIErrorCode.STRIPE_CUSTOMER_ID_DOES_NOT_EXIST
140153
: false;
141154

142-
useEffect(() => {
143-
if (oystehr && encounterId) {
144-
void oystehr.fhir
145-
.search<DocumentReference>({
146-
resourceType: 'DocumentReference',
147-
params: [
148-
{
149-
name: 'type',
150-
value: RECEIPT_CODE,
151-
},
152-
{
153-
name: 'encounter',
154-
value: 'Encounter/' + encounterId,
155-
},
156-
],
157-
})
158-
.then((response) => {
159-
const docRef = response.unbundle()[0];
160-
if (docRef) {
161-
setReceiptDocRefId(docRef.id);
162-
}
163-
});
164-
}
165-
}, [encounterId, oystehr, paymentData]);
155+
const receiptDocRefId = receiptDocRef?.id;
166156

167157
const sendReceipt = async (formData: SendReceiptFormData): Promise<void> => {
168158
if (!oystehr) return;
@@ -214,17 +204,34 @@ export default function PatientPaymentList({
214204

215205
const createNewPayment = useMutation({
216206
mutationFn: async (input: PostPatientPaymentInput) => {
217-
if (oystehrZambda && input) {
218-
return oystehrZambda.zambda
219-
.execute({
220-
id: 'patient-payments-post',
221-
...input,
222-
})
223-
.then(async () => {
224-
await refetchPaymentList();
225-
setPaymentDialogOpen(false);
226-
});
227-
}
207+
if (!oystehrZambda) return;
208+
209+
await oystehrZambda.zambda.execute({
210+
id: 'patient-payments-post',
211+
...input,
212+
});
213+
},
214+
onSuccess: async () => {
215+
await refetchPaymentList();
216+
const waitForReceipt = async (): Promise<void> => {
217+
let receipt: DocumentReference | null = null;
218+
const maxTries = 15;
219+
let tries = 0;
220+
221+
while (!receipt && tries < maxTries) {
222+
const result = await refetchReceipt();
223+
224+
receipt = result.data ?? null;
225+
if (!receipt) {
226+
await new Promise((res) => setTimeout(res, 2000));
227+
}
228+
tries += 1;
229+
}
230+
};
231+
232+
await waitForReceipt();
233+
234+
setPaymentDialogOpen(false);
228235
},
229236
retry: 0,
230237
});
@@ -288,16 +295,24 @@ export default function PatientPaymentList({
288295
(extensionTemp) => extensionTemp.url === FHIR_EXTENSION.InsurancePlan.notes.url
289296
)?.valueString;
290297

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+
291306
const copayAmount = getPaymentAmountFromPatientBenefit({
292-
coverage: insuranceData?.coverageChecks?.[0]?.copay || [],
307+
coverage: coverageCheck?.copay?.filter((item) => item.inNetwork === true) || [],
293308
code: 'UC',
294309
coverageCode: 'B',
295310
levelCode: 'IND',
296-
periodCode: '27',
311+
periodCode: undefined,
297312
});
298313

299314
const remainingDeductibleAmount = getPaymentAmountFromPatientBenefit({
300-
coverage: insuranceData?.coverageChecks?.[0]?.deductible || [],
315+
coverage: coverageCheck?.deductible?.filter((item) => item.inNetwork === true) || [],
301316
code: '30',
302317
coverageCode: 'C',
303318
levelCode: 'IND',
@@ -379,13 +394,13 @@ export default function PatientPaymentList({
379394
<TableRow>
380395
<TableCell style={{ fontSize: '16px' }}>Copay</TableCell>
381396
<TableCell style={{ fontSize: '16px', fontWeight: 'bold', textAlign: 'right' }}>
382-
{copayAmount ? `$${copayAmount}` : 'Unknown'}
397+
{copayAmount ? `$${copayAmount?.amountInUSD} / ${copayAmount?.periodDescription}` : 'Unknown'}
383398
</TableCell>
384399
</TableRow>
385400
<TableRow sx={{ '&:last-child td': { borderBottom: 'none' } }}>
386401
<TableCell style={{ fontSize: '16px' }}>Remaining Deductible</TableCell>
387402
<TableCell style={{ fontSize: '16px', fontWeight: 'bold', textAlign: 'right' }}>
388-
{remainingDeductibleAmount ? `$${remainingDeductibleAmount}` : 'Unknown'}
403+
{remainingDeductibleAmount ? `$${remainingDeductibleAmount?.amountInUSD}` : 'Unknown'}
389404
</TableCell>
390405
</TableRow>
391406
</TableBody>
@@ -494,7 +509,7 @@ export default function PatientPaymentList({
494509
<span>
495510
<Button
496511
sx={{ mt: 2, ml: 2 }}
497-
disabled={!receiptDocRefId}
512+
disabled={!receiptDocRefId || isReceiptFetching}
498513
onClick={() => setSendReceiptByEmailDialogOpen(true)}
499514
variant="contained"
500515
color="primary"

apps/ehr/src/components/dialogs/CancellationReasonDialog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { Appointment, Encounter } from 'fhir/r4b';
1717
import { enqueueSnackbar } from 'notistack';
1818
import React, { ReactElement, useState } from 'react';
19-
import { CancelAppointmentZambdaInput, VALUE_SETS } from 'utils';
19+
import { CancelAppointmentZambdaInput, isTelemedAppointment, VALUE_SETS } from 'utils';
2020
import { cancelAppointment } from '../../api/api';
2121
import { dataTestIds } from '../../constants/data-test-ids';
2222
import { useApiClients } from '../../hooks/useAppClients';
@@ -59,6 +59,10 @@ export default function CancellationReasonDialog({
5959
},
6060
};
6161

62+
const cancelOptions = isTelemedAppointment(appointment)
63+
? VALUE_SETS.cancelReasonOptionsVirtualProvider
64+
: VALUE_SETS.cancelReasonOptionsInPersonProvider;
65+
6266
const handleCancel = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
6367
event.preventDefault();
6468
setCancelLoading(true);
@@ -143,7 +147,7 @@ export default function CancellationReasonDialog({
143147
renderValue={(selected) => selected}
144148
MenuProps={MenuProps}
145149
>
146-
{VALUE_SETS.cancelReasonOptions.map((reason) => (
150+
{cancelOptions.map((reason) => (
147151
<MenuItem key={reason.value} value={reason.value}>
148152
<ListItemText primary={reason.label} />
149153
</MenuItem>

apps/ehr/src/components/form/DatePicker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Control, Controller, RegisterOptions } from 'react-hook-form';
1010
interface BasicDatePickerProps {
1111
name: string;
1212
control: Control<any>;
13-
rules: RegisterOptions;
13+
rules?: RegisterOptions;
1414
required?: boolean;
1515
defaultValue?: string;
1616
onChange?: (date: string) => void;
@@ -27,7 +27,7 @@ interface BasicDatePickerProps {
2727
export function BasicDatePicker({
2828
name,
2929
control,
30-
rules,
30+
rules = {},
3131
defaultValue,
3232
onChange,
3333
disabled,

apps/ehr/src/constants/data-test-ids.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const dataTestIds = {
6363
sexAtBirthDropdown: 'sex-at-birth-dropdown',
6464
reasonForVisitDropdown: 'reason-for-visit-dropdown',
6565
visitTypeDropdown: 'visit-type-dropdown',
66-
serviceCategoryDropdown: ' service-category-dropdown',
66+
serviceCategoryDropdown: 'service-category-dropdown',
6767
dateFormatValidationError: 'date-format-validation-error',
6868
prefillForButton: 'prefill-for-button',
6969
prefilledPatientName: 'prefilled-patient-name',

apps/ehr/src/constants/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const MOBILE_MODAL_STYLE = {
4141
p: 4,
4242
};
4343

44-
export const TYPE_WIDTH_MIN = '160px';
44+
export const TYPE_WIDTH_MIN = '195px';
4545
export const TIME_WIDTH_MIN = '140px';
4646
export const PATIENT_AND_REASON_WIDTH_MIN = '300px';
4747
export const ROOM_WIDTH_MIN = '120px';

apps/ehr/src/features/external-labs/components/labs-orders/usePatientLabOrders.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ interface UsePatientLabOrdersResult<SearchBy extends LabOrdersSearchBy> {
5858
}
5959

6060
export const usePatientLabOrders = <SearchBy extends LabOrdersSearchBy>(
61-
_searchBy: SearchBy
61+
_searchBy: SearchBy,
62+
refreshKey?: number
6263
): UsePatientLabOrdersResult<SearchBy> => {
6364
const { oystehrZambda } = useApiClients();
6465
const navigate = useNavigate();
@@ -198,7 +199,7 @@ export const usePatientLabOrders = <SearchBy extends LabOrdersSearchBy>(
198199
} else {
199200
console.error('searchParams are not valid', searchParams);
200201
}
201-
}, [fetchLabOrders, page, memoizedSearchBy]);
202+
}, [fetchLabOrders, page, memoizedSearchBy, refreshKey]);
202203

203204
const handleDeleteLabOrder = useCallback(
204205
async ({ serviceRequestId }: DeleteLabOrderZambdaInput): Promise<boolean> => {

0 commit comments

Comments
 (0)