Skip to content

Commit

Permalink
(feat) Add initial support for settling individual bill line items (#135
Browse files Browse the repository at this point in the history
)

* (feat) Add initial support for settling individual line items

* Fixup
  • Loading branch information
denniskigen authored Jan 19, 2024
1 parent 95ceec4 commit b832a5c
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

set -e # die on error

yarn verify
yarn verify
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"prettier": "prettier --config prettier.config.js --write \"packages/**/*.{ts,tsx,css,scss}\" \"e2e/**/*.ts\"",
"release": "lerna version --no-git-tag-version",
"start": "openmrs develop",
"verify": "turbo run lint && turbo run typescript && turbo run test"
"verify": "turbo run lint && turbo run typescript && turbo run test --color --concurrency=5"
},
"resolutions": {
"sass": "^1.54.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
DataTableSkeleton,
Expand Down Expand Up @@ -28,15 +28,15 @@ import {
import { useBills } from '../billing.resource';
import InvoiceTable from '../invoice/invoice-table.component';
import styles from './bill-history.scss';
import { MappedBill } from '../types';

interface BillHistoryProps {
patientUuid: string;
}

const PAGE_SIZE = 10;
const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
const { t } = useTranslation();
const { bills, isLoading, isValidating, error } = useBills(patientUuid);
const { bills, isLoading, error } = useBills(patientUuid);
const layout = useLayoutType();
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
const { paginated, goTo, results, currentPage } = usePagination(bills, PAGE_SIZE);
Expand Down Expand Up @@ -64,8 +64,8 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
const setBilledItems = (bill) =>
bill.lineItems.reduce((acc, item) => acc + (acc ? ' & ' : '') + (item.billableService || item.item || ''), '');

const rowData = results?.map((bill, index) => ({
id: `${index}`,
const rowData = results?.map((bill) => ({
id: bill.uuid,
uuid: bill.uuid,
billTotal: bill.totalAmount,
visitTime: bill.dateCreated,
Expand Down Expand Up @@ -120,7 +120,7 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
getRowProps,
}) => (
<TableContainer {...getTableContainerProps}>
<Table {...getTableProps()} aria-label="bill list">
<Table className={styles.table} {...getTableProps()} aria-label="Bill list">
<TableHead>
<TableRow>
<TableExpandHeader enableToggle {...getExpandHeaderProps()} />
Expand All @@ -136,24 +136,28 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, i) => (
<React.Fragment key={row.id}>
<TableExpandRow {...getRowProps({ row })}>
{row.cells.map((cell) => (
<TableCell key={cell.id}>{cell.value}</TableCell>
))}
</TableExpandRow>
{row.isExpanded ? (
<TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 1}>
<div className={styles.container} key={i}>
<InvoiceTable billUuid={rowData?.[i].uuid} />
</div>
</TableExpandedRow>
) : (
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
)}
</React.Fragment>
))}
{rows.map((row, i) => {
const currentBill = bills?.find((bill) => bill.uuid === row.id);

return (
<React.Fragment key={row.id}>
<TableExpandRow {...getRowProps({ row })}>
{row.cells.map((cell) => (
<TableCell key={cell.id}>{cell.value}</TableCell>
))}
</TableExpandRow>
{row.isExpanded ? (
<TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 1}>
<div className={styles.container} key={i}>
<InvoiceTable bill={currentBill} isSelectable={false} />
</div>
</TableExpandedRow>
) : (
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
)}
</React.Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
Expand Down
6 changes: 6 additions & 0 deletions packages/esm-billing-app/src/bill-history/bill-history.scss
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,9 @@
@include type.type-style('body-compact-01');
color: $text-02;
}

.table {
tr[data-child-row] td {
padding-left: 2rem !important;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,19 @@ import {
} from '@openmrs/esm-framework';
import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
import { useBills } from '../billing.resource';
import { PENDING } from '../constants';
import styles from './bills-table.scss';
import { MappedBill } from '../types';

const BillsTable = () => {
const { t } = useTranslation();
const id = useId();
const config = useConfig();
const layout = useLayoutType();
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
const [billPaymentStatus, setBillPaymentStatus] = useState('');
const pageSizes = config?.bills?.pageSizes ?? [10, 20, 30, 40, 50];
const [pageSize, setPageSize] = useState(config?.bills?.pageSize ?? 10);
const { bills, isLoading, isValidating, error } = useBills('', billPaymentStatus);
const [searchString, setSearchString] = useState('');
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';

const headerData = [
{
Expand Down Expand Up @@ -86,13 +84,16 @@ const BillsTable = () => {
const setBilledItems = (bill) =>
bill?.lineItems?.reduce((acc, item) => acc + (acc ? ' & ' : '') + (item.billableService || item.item || ''), '');

const billingUrl = '${openmrsSpaBase}/home/billing/patient/${patientUuid}/${uuid}';

const rowData = results?.map((bill, index) => ({
id: `${index}`,
uuid: bill.uuid,
patientName: (
<ConfigurableLink
style={{ textDecoration: 'none', maxWidth: '50%' }}
to={`${window.getOpenmrsSpaBase()}home/billing/patient/${bill.patientUuid}/${bill.uuid}`}>
to={billingUrl}
templateParams={{ patientUuid: bill.patientUuid, uuid: bill.uuid }}>
{bill.patientName}
</ConfigurableLink>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,6 @@ describe('BillsTable', () => {

const patientNameLink = screen.getByRole('link', { name: 'John Doe' });
expect(patientNameLink).toBeInTheDocument();
expect(patientNameLink).toHaveAttribute('href', '/openmrs/spa/home/billing/patient/uuid1/1');
expect(patientNameLink).toHaveAttribute('href', '/openmrs/spa/home/billing/patient/${patientUuid}/${uuid}');
});
});
47 changes: 37 additions & 10 deletions packages/esm-billing-app/src/invoice/invoice-table.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@ import {
TableToolbar,
TableToolbarContent,
TableToolbarSearch,
TableSelectRow,
Tile,
type DataTableHeader,
type DataTableRow,
} from '@carbon/react';
import { isDesktop, useDebounce, useLayoutType } from '@openmrs/esm-framework';
import { useBill } from '../billing.resource';
import { LineItem, MappedBill } from '../types';
import styles from './invoice-table.scss';

type InvoiceTableProps = {
billUuid: string;
bill: MappedBill;
isSelectable?: boolean;
isLoadingBill?: boolean;
onSelectItem?: (selectedLineItems: LineItem[]) => void;
};

const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
const { t } = useTranslation();
const { lineItems } = bill;
const layout = useLayoutType();
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
const { bill, isLoading } = useBill(billUuid);
const { lineItems } = bill;
const [selectedLineItems, setSelectedLineItems] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm);

Expand All @@ -42,14 +48,14 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
return debouncedSearchTerm
? fuzzy
.filter(debouncedSearchTerm, lineItems, {
extract: (lineItem) => `${lineItem.item}`,
extract: (lineItem: LineItem) => `${lineItem.item}`,
})
.sort((r1, r2) => r1.score - r2.score)
.map((result) => result.original)
: lineItems;
}, [debouncedSearchTerm, lineItems]);

const tableHeaders = [
const tableHeaders: Array<typeof DataTableHeader> = [
{ header: 'No', key: 'no' },
{ header: 'Bill item', key: 'billItem' },
{ header: 'Bill code', key: 'billCode' },
Expand All @@ -59,7 +65,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
{ header: 'Total', key: 'total' },
];

const tableRows = useMemo(
const tableRows: Array<typeof DataTableRow> = useMemo(
() =>
filteredLineItems?.map((item, index) => {
return {
Expand All @@ -76,7 +82,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
[bill.receiptNumber, bill.status, filteredLineItems],
);

if (isLoading) {
if (isLoadingBill) {
return (
<div className={styles.loaderContainer}>
<DataTableSkeleton
Expand All @@ -90,10 +96,23 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
);
}

const handleRowSelection = (row: typeof DataTableRow, checked: boolean) => {
const matchingRow = filteredLineItems.find((item) => item.uuid === row.id);
let newSelectedLineItems;

if (checked) {
newSelectedLineItems = [...selectedLineItems, matchingRow];
} else {
newSelectedLineItems = selectedLineItems.filter((item) => item.uuid !== row.id);
}
setSelectedLineItems(newSelectedLineItems);
onSelectItem(newSelectedLineItems);
};

return (
<div className={styles.invoiceContainer}>
<DataTable headers={tableHeaders} isSortable rows={tableRows} size={responsiveSize} useZebraStyles>
{({ rows, headers, getRowProps, getTableProps, getToolbarProps }) => (
{({ rows, headers, getRowProps, getSelectionProps, getTableProps, getToolbarProps }) => (
<TableContainer
description={
<span className={styles.tableDescription}>
Expand All @@ -117,6 +136,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
<Table {...getTableProps()} aria-label="Invoice line items" className={styles.table}>
<TableHead>
<TableRow>
{rows.length > 1 && isSelectable ? <TableHeader /> : null}
{headers.map((header) => (
<TableHeader key={header.key}>{header.header}</TableHeader>
))}
Expand All @@ -129,6 +149,13 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ billUuid }) => {
{...getRowProps({
row,
})}>
{rows.length > 1 && isSelectable && (
<TableSelectRow
aria-label="Select row"
{...getSelectionProps({ row })}
onChange={(checked: boolean) => handleRowSelection(row, checked)}
/>
)}
{row.cells.map((cell) => (
<TableCell key={cell.id}>{cell.value}</TableCell>
))}
Expand Down
39 changes: 21 additions & 18 deletions packages/esm-billing-app/src/invoice/invoice.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,35 @@ import { Printer } from '@carbon/react/icons';
import { useParams } from 'react-router-dom';
import { useReactToPrint } from 'react-to-print';
import { useTranslation } from 'react-i18next';
import { convertToCurrency } from '../helpers';
import { ExtensionSlot, usePatient } from '@openmrs/esm-framework';
import { ErrorState } from '@openmrs/esm-patient-common-lib';
import { convertToCurrency } from '../helpers';
import { LineItem } from '../types';
import { useBill } from '../billing.resource';
import InvoiceTable from './invoice-table.component';
import Payments from './payments/payments.component';
import PrintReceipt from './printable-invoice/print-receipt.component';
import PrintableInvoice from './printable-invoice/printable-invoice.component';
import styles from './invoice.scss';
import PrintReceipt from './printable-invoice/print-receipt.component';

type InvoiceProps = {};
interface InvoiceDetailsProps {
label: string;
value: string | number;
}

const Invoice: React.FC<InvoiceProps> = () => {
const params = useParams();
const Invoice: React.FC = () => {
const { t } = useTranslation();
const { patient, patientUuid, isLoading } = usePatient(params?.patientUuid);
const { bill, isLoading: isLoadingBilling, error } = useBill(params?.billUuid);
const { billUuid, patientUuid } = useParams();
const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid);
const { bill, isLoading: isLoadingBill, error } = useBill(billUuid);
const [isPrinting, setIsPrinting] = useState(false);
const componentRef = useRef(null);
const [selectedLineItems, setSelectedLineItems] = useState([]);
const componentRef = useRef<HTMLDivElement>(null);
const onBeforeGetContentResolve = useRef<(() => void) | null>(null);

const onBeforeGetContentResolve = useRef(null);
const handleSelectItem = (lineItems: Array<LineItem>) => {
setSelectedLineItems(lineItems);
};

const handleAfterPrint = useCallback(() => {
onBeforeGetContentResolve.current = null;
Expand Down Expand Up @@ -66,7 +74,7 @@ const Invoice: React.FC<InvoiceProps> = () => {
'Invoice Status': bill?.status,
};

if (isLoading && isLoadingBilling) {
if (isLoadingPatient && isLoadingBill) {
return (
<div className={styles.invoiceContainer}>
<InlineLoading
Expand Down Expand Up @@ -109,21 +117,16 @@ const Invoice: React.FC<InvoiceProps> = () => {
</div>
</div>

<InvoiceTable billUuid={bill?.uuid} />
{<Payments bill={bill} />}
<InvoiceTable bill={bill} isLoadingBill={isLoadingBill} onSelectItem={handleSelectItem} />
<Payments bill={bill} selectedLineItems={selectedLineItems} />

<div className={styles.printContainer} ref={componentRef}>
{isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoading} />}
{isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoadingPatient} />}
</div>
</div>
);
};

interface InvoiceDetailsProps {
label: string;
value: string | number;
}

function InvoiceDetails({ label, value }: InvoiceDetailsProps) {
return (
<div>
Expand Down
Loading

0 comments on commit b832a5c

Please sign in to comment.