Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions apps/ehr/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
CreateDischargeSummaryResponse,
CreateInHouseLabOrderParameters,
CreateInHouseLabOrderResponse,
CreateInvoiceablePatientsReportZambdaInputType,
CreateLabOrderParameters,
CreateLabOrderZambdaOutput,
CreateNursingOrderInput,
Expand Down Expand Up @@ -185,7 +184,6 @@ const PAPERWORK_TO_PDF_ZAMBDA_ID = 'paperwork-to-pdf';
const VISIT_DETAILS_TO_PDF_ZAMBDA_ID = 'visit-details-to-pdf';
const PENDING_SUPERVISOR_APPROVAL_ZAMBDA_ID = 'pending-supervisor-approval';
const SEND_RECEIPT_BY_EMAIL_ZAMBDA_ID = 'send-receipt-by-email';
const INVOICEABLE_PATIENTS_REPORT_ZAMBDA_ID = 'invoiceable-patients-report';
const BULK_UPDATE_INSURANCE_STATUS_ZAMBDA_ID = 'bulk-update-insurance-status';
const UPDATE_INVOICE_TASK_ZAMBDA_ID = 'update-invoice-task';
const GET_PATIENT_BALANCES_ZAMBDA_ID = 'get-patient-balances';
Expand Down Expand Up @@ -1418,22 +1416,6 @@ export const updateVisitFiles = async (oystehr: Oystehr, parameters: UpdateVisit
}
};

export const invoiceablePatientsReport = async (
oystehr: Oystehr,
params: CreateInvoiceablePatientsReportZambdaInputType
): Promise<void> => {
try {
const response = await oystehr.zambda.execute({
id: INVOICEABLE_PATIENTS_REPORT_ZAMBDA_ID,
...params,
});
return chooseJson(response);
} catch (error: unknown) {
console.log(error);
throw error;
}
};

export const bulkUpdateInsuranceStatus = async (
oystehr: Oystehr,
parameters: BulkUpdateInsuranceStatusInput
Expand Down
124 changes: 2 additions & 122 deletions apps/ehr/src/components/PatientEncountersGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import SubdirectoryArrowRightIcon from '@mui/icons-material/SubdirectoryArrowRight';
import {
Box,
ButtonOwnProps,
capitalize,
Checkbox,
Chip,
Expand All @@ -21,13 +20,11 @@ import {
TablePagination,
TableRow,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { Patient, Task } from 'fhir/r4b';
import { Patient } from 'fhir/r4b';
import { DateTime } from 'luxon';
import { enqueueSnackbar } from 'notistack';
import React, { FC, ReactElement, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ROUTER_PATH } from 'src/features/visits/in-person/routing/routesInPerson';
Expand All @@ -40,18 +37,14 @@ import {
AppointmentType,
FollowUpVisitHistoryRow,
formatMinutes,
GetPatientAndResponsiblePartyInfoEndpointOutput,
PatientVisitListResponse,
PrefilledInvoiceInfo,
ServiceMode,
TelemedAppointmentStatus,
TelemedCallStatusesArr,
visitStatusArray,
} from 'utils';
import { updateInvoiceTask } from '../api/api';
import { formatISOStringToDateAndTime } from '../helpers/formatDateTime';
import { useApiClients } from '../hooks/useAppClients';
import { SendInvoiceToPatientDialog } from './dialogs';
import { RoundedButton } from './RoundedButton';
import { TelemedAppointmentStatusChip } from './TelemedAppointmentStatusChip';

Expand Down Expand Up @@ -136,7 +129,6 @@ const columns: TableColumn[] = [
{ id: 'los', label: 'LOS', width: 100 },
{ id: 'info', label: 'Visit Info', width: 120, align: 'center' },
{ id: 'note', label: 'Progress Note', width: 150 },
{ id: 'invoice', label: 'Invoice', width: 100, align: 'center' },
];

export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) => {
Expand All @@ -152,19 +144,11 @@ export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) =>
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [selectedInvoiceTask, setSelectedInvoiceTask] = useState<Task | undefined>(undefined);
const [additionalDataForInvoiceDialog, setAdditionalDataForInvoiceDialog] = useState<
{ visitDate?: string; location?: string } | undefined
>();

const { oystehrZambda } = useApiClients();
const navigate = useNavigate();

const {
data: visitHistory,
isLoading: visitHistoryIsLoading,
refetch: refetchVisitHistory,
} = useQuery({
const { data: visitHistory, isLoading: visitHistoryIsLoading } = useQuery({
queryKey: [`get-patient-visit-history`, { patientId, status, type, period }],
queryFn: async (): Promise<PatientVisitListResponse> => {
let from: string | undefined;
Expand Down Expand Up @@ -196,22 +180,6 @@ export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) =>
enabled: Boolean(patient?.id) && Boolean(oystehrZambda),
});

const { data: patientAndRpForInvoiceData, isLoading: patientAndRPLoading } = useQuery({
queryKey: [`get-patient-and-responsible-party-info`, { patientId }],
queryFn: async (): Promise<GetPatientAndResponsiblePartyInfoEndpointOutput> => {
if (oystehrZambda && patient?.id) {
const result = await oystehrZambda.zambda.execute({
id: 'get-patient-and-responsible-party-info',
patientId: patient.id,
});
return result.output as GetPatientAndResponsiblePartyInfoEndpointOutput;
}

throw new Error('api client not defined or patient id is not provided');
},
enabled: Boolean(patient?.id) && Boolean(oystehrZambda),
});

const filtered = useMemo(() => {
if (!visitHistory) return [];
const { visits, metadata } = visitHistory;
Expand Down Expand Up @@ -239,25 +207,6 @@ export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) =>
return filtered.slice(startIndex, startIndex + rowsPerPage);
}, [filtered, page, rowsPerPage]);

const sendInvoice = async (taskId: string, prefilledInvoiceInfo: PrefilledInvoiceInfo): Promise<void> => {
try {
if (oystehrZambda) {
await updateInvoiceTask(oystehrZambda, {
taskId,
status: 'requested',
prefilledInvoiceInfo,
userTimezone: DateTime.local().zoneName,
});
setSelectedInvoiceTask(undefined);
setAdditionalDataForInvoiceDialog(undefined);
void refetchVisitHistory();
enqueueSnackbar('Invoice created and sent successfully', { variant: 'success' });
}
} catch {
enqueueSnackbar('Error occurred during invoice creation, please try again', { variant: 'error' });
}
};

const handleSort = (field: SortField): void => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
Expand Down Expand Up @@ -356,62 +305,6 @@ export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) =>
Progress Note
</RoundedButton>
);
case 'invoice': {
const lastEncounterTask = row.sendInvoiceTask;

let buttonColor: ButtonOwnProps['color'] = 'secondary';
let tooltipText = '---';
let buttonDisabled = true;
if (lastEncounterTask !== undefined) {
switch (lastEncounterTask?.status) {
case 'ready':
buttonColor = 'primary';
tooltipText = 'Invoice can be sent for this visit.';
buttonDisabled = false;
break;
case 'completed':
buttonColor = 'success';
tooltipText =
'Invoice has been sent and processed for this visit successfully. You can resend it by pressing this button';
buttonDisabled = false;
break;
case 'failed':
buttonColor = 'error';
tooltipText = 'Invoice has been sent but failed to process for this visit.';
buttonDisabled = false;
break;
default:
buttonColor = 'secondary';
tooltipText = "Invoice can't be sent for this visit yet.";
buttonDisabled = true;
}
} else {
buttonColor = 'inherit';
tooltipText = "Invoice can't be sent for this visit yet.";
buttonDisabled = true;
}
return (
<Tooltip title={tooltipText} placement="top">
<Box>
<RoundedButton
disabled={buttonDisabled || patientAndRPLoading}
color={buttonColor}
onClick={() => {
const visitDateTime = row.dateTime ? DateTime.fromISO(row.dateTime) : null;
const visitDate = visitDateTime && visitDateTime.isValid ? visitDateTime.toFormat('MM/dd/yyyy') : '';
setAdditionalDataForInvoiceDialog({
visitDate,
location: row.office,
});
setSelectedInvoiceTask(lastEncounterTask);
}}
>
Send Invoice
</RoundedButton>
</Box>
</Tooltip>
);
}
default:
return '-';
}
Expand Down Expand Up @@ -622,19 +515,6 @@ export const PatientEncountersGrid: FC<PatientEncountersGridProps> = (props) =>
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
<SendInvoiceToPatientDialog
title="Send invoice"
modalOpen={selectedInvoiceTask !== undefined}
handleClose={() => {
setSelectedInvoiceTask(undefined);
setAdditionalDataForInvoiceDialog(undefined);
}}
submitButtonName="Send Invoice"
onSubmit={sendInvoice}
invoiceTask={selectedInvoiceTask}
patientAndRP={patientAndRpForInvoiceData}
additionalData={additionalDataForInvoiceDialog}
/>
</Paper>
);
};
47 changes: 21 additions & 26 deletions apps/ehr/src/components/dialogs/SendInvoiceToPatientDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ import {
Tooltip,
Typography,
} from '@mui/material';
import { Task } from 'fhir/r4b';
import { DateTime } from 'luxon';
import { enqueueSnackbar } from 'notistack';
import { ReactElement, useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import {
BRANDING_CONFIG,
GetPatientAndResponsiblePartyInfoEndpointOutput,
InvoiceablePatientReport,
InvoiceMessagesPlaceholders,
InvoiceTaskInput,
parseInvoiceTaskInput,
PrefilledInvoiceInfo,
replaceTemplateVariablesArrows,
REQUIRED_FIELD_ERROR_MESSAGE,
TEXTING_CONFIG,
Expand All @@ -43,11 +42,9 @@ interface SendInvoiceToPatientDialogProps {
title: string;
modalOpen: boolean;
handleClose: () => void;
onSubmit: (taskId: string, prefilledInvoiceInfo: PrefilledInvoiceInfo) => Promise<void>;
onSubmit: (taskId: string, prefilledInvoiceInfo: InvoiceTaskInput) => Promise<void>;
submitButtonName: string;
invoiceTask?: Task;
patientAndRP?: GetPatientAndResponsiblePartyInfoEndpointOutput;
additionalData?: { visitDate?: string; location?: string };
report?: InvoiceablePatientReport;
}

export default function SendInvoiceToPatientDialog({
Expand All @@ -56,9 +53,7 @@ export default function SendInvoiceToPatientDialog({
handleClose,
onSubmit,
submitButtonName,
invoiceTask,
patientAndRP,
additionalData,
report,
}: SendInvoiceToPatientDialogProps): ReactElement {
const [disableAllFields, setDisableAllFields] = useState(true);
const {
Expand All @@ -70,13 +65,13 @@ export default function SendInvoiceToPatientDialog({
} = useForm<SendInvoiceFormData>({
mode: 'onBlur',
});
const { visitDate, location } = additionalData ?? {};
const { visitDate, location, patient, task, responsibleParty } = report ?? {};
const invoiceMessagesPlaceholders: InvoiceMessagesPlaceholders = {
clinic: BRANDING_CONFIG.projectName,
amount: watch('amount')?.toString(),
'due-date': watch('dueDate'),
'invoice-link': 'https://example.com/invoice-link',
'patient-full-name': patientAndRP?.patient.fullName ?? '',
'patient-full-name': patient?.fullName ?? '',
'url-to-patient-portal': 'https://example.com/patient-portal',
'visit-date': visitDate,
location,
Expand All @@ -88,10 +83,10 @@ export default function SendInvoiceToPatientDialog({
const memoMessagePrefilledPreview = replaceTemplateVariablesArrows(watch('memo'), invoiceMessagesPlaceholders);

const handleSubmitWrapped = (data: SendInvoiceFormData): void => {
if (invoiceTask && invoiceTask?.id) {
if (task && task?.id) {
setDisableAllFields(true);

void onSubmit(invoiceTask?.id, {
void onSubmit(task?.id, {
dueDate: data.dueDate,
memo: data.memo,
smsTextMessage: data.smsTextMessage,
Expand All @@ -101,16 +96,16 @@ export default function SendInvoiceToPatientDialog({
};

useEffect(() => {
if (invoiceTask) {
if (task) {
try {
const invoiceTaskInput = parseInvoiceTaskInput(invoiceTask);
const invoiceTaskInput = parseInvoiceTaskInput(task);
if (invoiceTaskInput) {
const { amountCents } = invoiceTaskInput;
const dueDate = DateTime.now().plus({ days: TEXTING_CONFIG.invoicing.dueDateInDays }).toISODate();
const memo = TEXTING_CONFIG.invoicing.stripeMemoMessage;
const smsMessage = TEXTING_CONFIG.invoicing.smsMessage;
reset({
amount: amountCents / 100,
amount: (amountCents ?? 0) / 100,
dueDate: dueDate,
memo: memo,
smsTextMessage: smsMessage,
Expand All @@ -121,7 +116,7 @@ export default function SendInvoiceToPatientDialog({
/* empty */
}
}
}, [invoiceTask, reset]);
}, [task, reset]);

return (
<Dialog open={modalOpen}>
Expand All @@ -135,20 +130,20 @@ export default function SendInvoiceToPatientDialog({
</DialogTitle>

<DialogContent>
{patientAndRP !== undefined ? (
{report !== undefined ? (
<Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">
Patient
</Typography>
<Typography sx={{ fontWeight: 600, mb: 0.5 }}>{patientAndRP?.patient.fullName}</Typography>
<Typography sx={{ fontWeight: 600, mb: 0.5 }}>{patient?.fullName}</Typography>
<Box sx={{ flexDirection: 'row', display: 'flex' }}>
<Typography variant="body2">{patientAndRP?.patient.dob}</Typography>
<Typography variant="body2">{patient?.dob}</Typography>
<Typography variant="body2" sx={{ pl: 2 }}>
{patientAndRP?.patient.gender}
{patient?.gender}
</Typography>
<Typography variant="body2" sx={{ pl: 2 }}>
{patientAndRP?.patient.phoneNumber}
{patient?.phoneNumber}
</Typography>
</Box>
</Box>
Expand All @@ -157,11 +152,11 @@ export default function SendInvoiceToPatientDialog({
<Typography variant="subtitle2" color="text.secondary">
Responsible party name
</Typography>
<Typography sx={{ fontWeight: 600, mb: 0.5 }}>{patientAndRP?.responsibleParty.fullName}</Typography>
<Typography sx={{ fontWeight: 600, mb: 0.5 }}>{responsibleParty?.fullName}</Typography>
<Box sx={{ flexDirection: 'row', display: 'flex' }}>
<Typography variant="body2">{patientAndRP?.responsibleParty.email}</Typography>
<Typography variant="body2">{responsibleParty?.email}</Typography>
<Typography variant="body2" sx={{ pl: 2 }}>
{patientAndRP?.responsibleParty.phoneNumber}
{responsibleParty?.phoneNumber}
</Typography>
</Box>
</Box>
Expand Down
Loading
Loading