Skip to content

Commit

Permalink
O3-2890 Add ability to edit bill line item
Browse files Browse the repository at this point in the history
  • Loading branch information
CynthiaKamau committed Apr 8, 2024
1 parent aa9e124 commit 1c506d5
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 6 deletions.
4 changes: 3 additions & 1 deletion src/bill-history/bill-history.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
<React.Fragment key={row.id}>
<TableExpandRow {...getRowProps({ row })}>
{row.cells.map((cell) => (
<TableCell key={cell.id}>{cell.value}</TableCell>
<TableCell key={cell.id} className={styles.tableCells}>
{cell.value}
</TableCell>
))}
</TableExpandRow>
{row.isExpanded ? (
Expand Down
4 changes: 4 additions & 0 deletions src/bill-history/bill-history.scss
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,7 @@
padding-left: 2rem !important;
}
}

.tableCells {
white-space: wrap !important;
}
26 changes: 26 additions & 0 deletions src/bill-item-actions/bill-item-actions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
@use '@carbon/colors';

.section {
margin: spacing.$spacing-03;
}

.sectionTitle {
@include type.type-style('heading-compact-02');
color: colors.$gray-70;
margin-bottom: spacing.$spacing-04;
}

.modalBody {
padding-bottom: spacing.$spacing-05;
}

.label {
@include type.type-style('heading-compact-01');
margin-bottom: spacing.$spacing-05;
}

.controlField {
margin-bottom: spacing.$spacing-05;
}
214 changes: 214 additions & 0 deletions src/bill-item-actions/edit-bill-item.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Button, ModalBody, ModalFooter, ModalHeader, Form, InlineLoading } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { showSnackbar } from '@openmrs/esm-framework';
import { Controller, type FieldErrors, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { type LineItem, type MappedBill } from '../types';
import styles from './bill-item-actions.scss';
import { updateBillItems } from '../billing.resource';
import { mutate } from 'swr';
import { apiBasePath } from '../constants';
import { Column } from '@carbon/react';
import { InlineNotification } from '@carbon/react';
import { getBillableServiceUuid } from '../invoice/payments/utils';
import { useBillableServices } from '../billable-services/billable-service.resource';
import { NumberInput } from '@carbon/react';

interface BillLineItemProps {
bill: MappedBill;
item: LineItem;
closeModal: () => void;
}

const ChangeStatus: React.FC<BillLineItemProps> = ({ bill, item, closeModal }) => {
const { t } = useTranslation();
const [showErrorNotification, setShowErrorNotification] = useState(false);
const [total, setTotal] = useState(0);
const { billableServices } = useBillableServices();

const schema = useMemo(
() =>
z.object({
quantity: z.string({ required_error: t('quantityRequired', 'Quantity is required') }),
price: z.string({ required_error: t('priceIsRequired', 'Price is required') }),
}),
[],
);

type BillLineItemForm = z.infer<typeof schema>;

const onError = (errors: FieldErrors<LineItem>) => {
if (errors) {
setShowErrorNotification(true);
}
};

const {
control,
handleSubmit,
formState: { isSubmitting, errors, isDirty },
watch,
} = useForm<BillLineItemForm>({
defaultValues: {
quantity: item.quantity.toString(),
price: item.price.toString(),
},
resolver: zodResolver(schema),
});

const quantity = watch('quantity');
const price = watch('price');

useEffect(() => {
const newTotal = parseInt(quantity) * parseInt(price);
setTotal(newTotal);
}, [quantity, price]);

const onSubmit = (data: BillLineItemForm) => {
const url = `${apiBasePath}bill`;

const newItem = {
...item,
quantity: parseInt(data.quantity),
price: parseInt(data?.price),
billableService: getBillableServiceUuid(billableServices, item.billableService),
item: item?.item,
};

const previousLineitems = bill?.lineItems
.filter((currItem) => currItem.uuid !== item?.uuid)
.map((currItem) => ({
...currItem,
billableService: getBillableServiceUuid(billableServices, item.billableService),
}));
const updatedLineItems = previousLineitems.concat(newItem);

const payload = {
cashPoint: bill.cashPointUuid,
cashier: bill.cashier.uuid,
lineItems: updatedLineItems,
patient: bill.patientUuid,
status: bill.status,
uuid: bill.uuid,
};
updateBillItems(payload).then(
(res) => {
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
showSnackbar({
title: t('billItems', 'Save Bill'),
subtitle: 'Bill processing has been successful',
kind: 'success',
timeoutInMs: 3000,
});
closeModal();
},
(error) => {
showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: error?.message });
},
);
};

if (Object.keys(bill)?.length === 0) {
return <ModalHeader closeModal={closeModal} title={t('billLineItemEmpty', 'This bill has no line items')} />;
}

if (Object.keys(bill)?.length > 0) {
return (
<div>
<Form onSubmit={handleSubmit(onSubmit, onError)}>
<ModalHeader closeModal={closeModal} title={t('editBillLineItem', 'Edit bill line item?')} />
<ModalBody>
<div className={styles.modalBody}>
<h5>
{bill?.patientName} &nbsp; · &nbsp;{bill?.cashPointName} &nbsp; · &nbsp;{bill?.receiptNumber}&nbsp;
</h5>
</div>
<section className={styles.section}>
<p className={styles.label}>
{t('item', 'Item')} : {item?.billableService ? item?.billableService : item?.item}
</p>
<p className={styles.label}>
{t('currentPrice', 'Current price')} : {item?.price}
</p>
<p className={styles.label}>
{t('status', 'status')} : {item?.paymentStatus}
</p>
<Controller
name="quantity"
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<NumberInput
label={t('quantity', 'Quantity')}
id="quantityInput"
min={0}
max={100}
value={value}
onChange={onChange}
className={styles.controlField}
invalid={errors.quantity?.message}
invalidText={errors.quantity?.message}
/>
)}
/>

<Controller
name="price"
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<NumberInput
id="priceInput"
label={t('price', 'Price')}
value={value}
onChange={onChange}
className={styles.controlField}
invalid={errors.price?.message}
invalidText={errors.price?.message}
/>
)}
/>

<p className={styles.label}>
{t('total', 'Total')} : {total}{' '}
</p>

{showErrorNotification && (
<Column className={styles.errorContainer}>
<InlineNotification
lowContrast
title={t('error', 'Error')}
subtitle={t('pleaseRequiredFields', 'Please fill all required fields') + '.'}
onClose={() => setShowErrorNotification(false)}
/>
</Column>
)}
</section>
</ModalBody>
<ModalFooter>
<Button kind="secondary" onClick={closeModal}>
{t('cancel', 'Cancel')}
</Button>
<Button disabled={isSubmitting} type="submit">
<>
{isSubmitting ? (
<div className={styles.inline}>
<InlineLoading
status="active"
iconDescription={t('submitting', 'Submitting')}
description={t('submitting', 'Submitting...')}
/>
</div>
) : (
t('save', 'Save')
)}
</>
</Button>
</ModalFooter>
</Form>
</div>
);
}
};

export default ChangeStatus;
11 changes: 11 additions & 0 deletions src/billing.resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,14 @@ export const processBillItems = (payload) => {
},
});
};

export const updateBillItems = (payload) => {
const url = `${apiBasePath}bill/${payload.uuid}`;
return openmrsFetch(url, {
method: 'POST',
body: payload,
headers: {
'Content-Type': 'application/json',
},
});
};
7 changes: 7 additions & 0 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export const configSchema = {
_description: 'The default page size',
_default: 10,
},

showEditBillButton: {
_type: Type.Boolean,
_description: 'Whether to show the edit bill button or not.',
_default: false,
},
};

export interface ConfigObject {
Expand All @@ -47,4 +53,5 @@ export interface ConfigObject {
catergoryConcepts: Object;
pageSize;
object;
showEditBillButton;
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,8 @@ export const billingPatientSummary = getSyncLifecycle(BillHistory, options);
export const requirePaymentModal = getSyncLifecycle(RequirePaymentModal, options);
export const root = getSyncLifecycle(RootComponent, options);
export const visitAttributeTags = getSyncLifecycle(VisitAttributeTags, options);

export const editBillLineItemDialog = getAsyncLifecycle(() => import('./bill-item-actions/edit-bill-item.component'), {
featureName: 'edit bill line item',
moduleName,
});
33 changes: 30 additions & 3 deletions src/invoice/invoice-table.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import {
type DataTableHeader,
type DataTableRow,
} from '@carbon/react';
import { isDesktop, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
import { isDesktop, showModal, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
import { type LineItem, type MappedBill } from '../types';
import styles from './invoice-table.scss';
import { convertToCurrency } from '../helpers';
import { Edit } from '@carbon/react/icons';
import { Button } from '@carbon/react';

type InvoiceTableProps = {
bill: MappedBill;
Expand All @@ -41,7 +43,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
const [selectedLineItems, setSelectedLineItems] = useState(pendingLineItems ?? []);
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm);
const { defaultCurrency } = useConfig();
const { defaultCurrency, showEditBillButton } = useConfig();

const filteredLineItems = useMemo(() => {
if (!debouncedSearchTerm) {
Expand All @@ -66,8 +68,17 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
{ header: 'Quantity', key: 'quantity' },
{ header: 'Price', key: 'price' },
{ header: 'Total', key: 'total' },
{ header: t('actions', 'Actions'), key: 'actionButton' },
];

const handleSelectBillItem = (row) => {
const dispose = showModal('edit-bill-line-item-dialog', {
bill,
item: row,
closeModal: () => dispose(),
});
};

const tableRows: Array<typeof DataTableRow> = useMemo(
() =>
filteredLineItems?.map((item, index) => {
Expand All @@ -80,6 +91,22 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
quantity: item.quantity,
price: convertToCurrency(item.price, defaultCurrency),
total: convertToCurrency(item.price * item.quantity, defaultCurrency),
actionButton: {
content: (
<span>
{showEditBillButton ?? (
<Button
renderIcon={Edit}
hasIconOnly
kind="ghost"
iconDescription={t('editThisBillItem', 'Edit this bill item')}
tooltipPosition="left"
onClick={() => handleSelectBillItem(item)}
/>
)}
</span>
),
},
};
}) ?? [],
[bill?.receiptNumber, filteredLineItems],
Expand Down Expand Up @@ -155,7 +182,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
/>
)}
{row.cells.map((cell) => (
<TableCell key={cell.id}>{cell.value}</TableCell>
<TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
))}
</TableRow>
))}
Expand Down
1 change: 0 additions & 1 deletion src/invoice/invoice-table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
}

.invoiceContainer {
margin: layout.$spacing-09 layout.$spacing-05 0;
border: 1px solid $ui-03;
}

Expand Down
2 changes: 1 addition & 1 deletion src/invoice/payments/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ export const createPaymentPayload = (
return processedPayment;
};

const getBillableServiceUuid = (billableServices: Array<any>, serviceName: string) => {
export const getBillableServiceUuid = (billableServices: Array<any>, serviceName: string) => {
return billableServices.length ? billableServices.find((service) => service.name === serviceName).uuid : null;
};
Loading

0 comments on commit 1c506d5

Please sign in to comment.