@kenyaemr/esm-billing-app 5.4.1-pre.2103 → 5.4.1-pre.2108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-link-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-link-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-link-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-link-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-link-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-link-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-link-slot"},{"component":"claimsManagementSideNavGroup","name":"claims-management-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"claims-management","title":"Claims management Overview","slot":"case-management-slot"},"featureFlag":"healthInformationExchange"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"bill-deposit-workspace","component":"billDepositWorkspace","title":"Bill Deposit Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"}],"version":"5.4.1-pre.2103"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-link-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-link-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-link-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-link-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-link-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-link-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-link-slot"},{"component":"claimsManagementSideNavGroup","name":"claims-management-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"claims-management","title":"Claims management Overview","slot":"case-management-slot"},"featureFlag":"healthInformationExchange"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"bill-deposit-workspace","component":"billDepositWorkspace","title":"Bill Deposit Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"}],"version":"5.4.1-pre.2108"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.1-pre.2103",
3
+ "version": "5.4.1-pre.2108",
4
4
  "description": "Billing app for KenyaEMR",
5
5
  "browser": "dist/kenyaemr-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -92,6 +92,9 @@ const ClaimsTable: React.FC<TableProps> = ({ title, emptyStateText, emptyStateHe
92
92
  const layout = useLayoutType();
93
93
  const size = layout === 'tablet' ? 'lg' : 'md';
94
94
  const filteredClaimIds = filteredClaims.map((claim) => claim.responseUUID);
95
+ const responseUUIDs = filteredClaimIds
96
+ .map((claimId) => claims.find((c) => c.responseUUID === claimId)?.responseUUID)
97
+ .filter((uuid) => uuid);
95
98
 
96
99
  const getHeaders = (): Header[] => {
97
100
  let baseHeaders = [
@@ -208,7 +211,7 @@ const ClaimsTable: React.FC<TableProps> = ({ title, emptyStateText, emptyStateHe
208
211
  filters={filters}
209
212
  onFilterChanged={setFilters}
210
213
  statusOptions={status}
211
- filteredClaimIds={filteredClaimIds}
214
+ filteredClaimIds={responseUUIDs}
212
215
  />
213
216
  </div>
214
217
  <DataTable rows={results} headers={headers} isSortable useZebraStyles>
@@ -34,7 +34,6 @@ type ClaimsTableProps = {
34
34
 
35
35
  const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
36
36
  const { t } = useTranslation();
37
- const { lineItems } = bill;
38
37
  const layout = useLayoutType();
39
38
  const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
40
39
  const [selectedLineItems, setSelectedLineItems] = useState<LineItem[]>([]);
@@ -43,20 +42,33 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
43
42
  const [currentPage, setCurrentPage] = useState(1);
44
43
  const [pageSize, setPageSize] = useState(10);
45
44
 
46
- const paidLineItems = useMemo(() => (lineItems || []).filter((item) => item.paymentStatus === 'PAID'), [lineItems]);
45
+ // Filter line items that are paid and paid through Insurance
46
+ const insurancePaidLineItems = useMemo(() => {
47
+ return (bill.lineItems || []).filter((lineItem) => {
48
+ // Check if payment status is PAID
49
+ const isPaid = lineItem.paymentStatus === 'PAID';
50
+
51
+ // Check if there's an Insurance payment for this line item
52
+ const hasInsurancePayment = bill.payments?.some(
53
+ (payment) => payment.billLineItem?.uuid === lineItem.uuid && payment.instanceType?.name === 'Insurance',
54
+ );
55
+
56
+ return isPaid && hasInsurancePayment;
57
+ });
58
+ }, [bill.lineItems, bill.payments]);
47
59
 
48
60
  const filteredLineItems = useMemo(() => {
49
61
  if (!debouncedSearchTerm) {
50
- return paidLineItems;
62
+ return insurancePaidLineItems;
51
63
  }
52
64
 
53
65
  return fuzzy
54
- .filter(debouncedSearchTerm, paidLineItems, {
55
- extract: (lineItem: LineItem) => `${lineItem.item}`,
66
+ .filter(debouncedSearchTerm, insurancePaidLineItems, {
67
+ extract: (lineItem: LineItem) => `${lineItem.item || lineItem.billableService}`,
56
68
  })
57
69
  .sort((r1, r2) => r1.score - r2.score)
58
70
  .map((result) => result.original);
59
- }, [debouncedSearchTerm, paidLineItems]);
71
+ }, [debouncedSearchTerm, insurancePaidLineItems]);
60
72
 
61
73
  const paginatedLineItems = useMemo(() => {
62
74
  const startIndex = (currentPage - 1) * pageSize;
@@ -69,11 +81,20 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
69
81
  { header: t('serialNo', 'Serial No'), key: 'serialno' },
70
82
  { header: t('billItem', 'Bill Item'), key: 'inventoryname' },
71
83
  { header: t('status', 'Status'), key: 'status' },
84
+ { header: t('paymentMethod', 'Payment Method'), key: 'paymentMethod' },
72
85
  { header: t('totalAmount', 'Total amount'), key: 'total' },
73
86
  { header: t('billCreationDate', 'Bill creation date'), key: 'dateofbillcreation' },
74
87
  ];
75
88
 
76
- const processBillItem = (item) => (item?.item || item?.billableService)?.split(':')[1];
89
+ const processBillItem = (item) => {
90
+ const itemName = item?.item || item?.billableService;
91
+ return itemName?.split(':')[1]?.trim() || itemName;
92
+ };
93
+
94
+ const getPaymentMethod = (lineItemUuid: string) => {
95
+ const payment = bill.payments?.find((p) => p.billLineItem?.uuid === lineItemUuid);
96
+ return payment?.instanceType?.name || 'Unknown';
97
+ };
77
98
 
78
99
  const tableRows: Array<typeof DataTableRow> = useMemo(
79
100
  () =>
@@ -84,11 +105,12 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
84
105
  inventoryname: processBillItem(item),
85
106
  serialno: bill.receiptNumber,
86
107
  status: item.paymentStatus,
108
+ paymentMethod: getPaymentMethod(item.uuid),
87
109
  total: item.price * item.quantity,
88
110
  dateofbillcreation: formatDate(new Date(bill.dateCreated), { mode: 'standard' }),
89
111
  };
90
112
  }) ?? [],
91
- [bill.dateCreated, bill.receiptNumber, paginatedLineItems],
113
+ [bill.dateCreated, bill.receiptNumber, bill.payments, paginatedLineItems],
92
114
  );
93
115
 
94
116
  if (isLoadingBill) {
@@ -126,10 +148,10 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
126
148
  className={styles.tableContainer}
127
149
  description={
128
150
  <span className={styles.tableDescription}>
129
- <span>{t('selectitemstobeclaimed', 'Select items that are to be included in the claims')}</span>
151
+ <span>{t('insurancePaidItems', 'Items paid through Insurance')}</span>
130
152
  </span>
131
153
  }
132
- title={t('lineItems', 'Line items')}>
154
+ title={t('claimableItems', 'Claimable Items')}>
133
155
  <div className={styles.toolbarWrapper}>
134
156
  <TableToolbar {...getToolbarProps()} className={styles.tableToolbar} size={responsiveSize}>
135
157
  <TableToolbarContent className={styles.headerContainer}>
@@ -143,7 +165,7 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
143
165
  </TableToolbarContent>
144
166
  </TableToolbar>
145
167
  </div>
146
- <Table {...getTableProps()} aria-label="claim line items" className={styles.table}>
168
+ <Table {...getTableProps()} aria-label="insurance claim line items" className={styles.table}>
147
169
  <TableHead>
148
170
  <TableRow>
149
171
  {isSelectable ? <TableHeader /> : null}
@@ -184,14 +206,13 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
184
206
  <Layer>
185
207
  <Tile className={styles.filterEmptyStateTile}>
186
208
  <p className={styles.filterEmptyStateContent}>
187
- {t('noMatchingItemsToDisplay', 'No matching items to display')}
209
+ {t('noInsurancePaidItems', 'No items paid through insurance found')}
188
210
  </p>
189
- <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
190
211
  </Tile>
191
212
  </Layer>
192
213
  </div>
193
214
  )}
194
- {tableRows.length > pageSize && (
215
+ {filteredLineItems.length > pageSize && (
195
216
  <Pagination
196
217
  forwardText="Next page"
197
218
  backwardText="Previous page"
@@ -23,6 +23,7 @@ export function usePaymentSchema(bill: MappedBill) {
23
23
  return amountDue >= 0 && value > 0;
24
24
  }, 'Amount paid should not be greater than amount due'),
25
25
  referenceCode: z.string(),
26
+ lineItemUuid: z.string().uuid().nonempty({ message: 'Line item selection is required' }),
26
27
  })
27
28
  .refine(
28
29
  (data) => {
@@ -151,7 +151,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
151
151
  {...getRowProps({
152
152
  row,
153
153
  })}>
154
- {rows.length > 1 && isSelectable && (
154
+ {isSelectable && (
155
155
  <TableSelectRow
156
156
  aria-label="Select row"
157
157
  {...getSelectionProps({ row })}
@@ -6,23 +6,33 @@ import { Button, Dropdown, NumberInputSkeleton, TextInput, NumberInput } from '@
6
6
  import { ErrorState } from '@openmrs/esm-patient-common-lib';
7
7
  import styles from './payment-form.scss';
8
8
  import { usePaymentModes } from '../../../billing.resource';
9
- import { PaymentFormValue, PaymentMethod } from '../../../types';
9
+ import { type LineItem, PaymentFormValue, PaymentMethod } from '../../../types';
10
+ import { extractBillableName } from '../../../utils';
10
11
 
11
12
  type PaymentFormProps = {
12
13
  disablePayment: boolean;
13
14
  amountDue: number;
14
- append: (obj: { method: PaymentMethod; amount: number; referenceCode: string }) => void;
15
+ append: (obj: { method: PaymentMethod; amount: number; referenceCode: string; lineItemUuid: string }) => void;
15
16
  fields: FieldArrayWithId<PaymentFormValue, 'payment', 'id'>[];
16
17
  remove: UseFieldArrayRemove;
18
+ selectedLineItems: Array<LineItem>;
17
19
  };
18
20
 
19
- const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue, append, remove, fields }) => {
21
+ const PaymentForm: React.FC<PaymentFormProps> = ({
22
+ disablePayment,
23
+ amountDue,
24
+ append,
25
+ remove,
26
+ fields,
27
+ selectedLineItems,
28
+ }) => {
20
29
  const { t } = useTranslation();
21
30
  const {
22
31
  control,
23
32
  formState: { errors },
24
33
  setFocus,
25
34
  getValues,
35
+ watch,
26
36
  } = useFormContext<PaymentFormValue>();
27
37
  const { paymentModes, isLoading, error } = usePaymentModes();
28
38
 
@@ -31,9 +41,26 @@ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue, ap
31
41
  const attributes = formValues?.payment?.[index]?.method?.attributeTypes ?? [];
32
42
  return attributes.some((attribute) => attribute.required) || attributes?.length > 0;
33
43
  };
44
+ const paymentValues = watch('payment');
34
45
 
46
+ const getUsedItemUuids = (currentIndex: number) => {
47
+ return paymentValues
48
+ .map((payment, index) => (index !== currentIndex ? payment?.lineItemUuid : null))
49
+ .filter(Boolean);
50
+ };
51
+ const getAvailableLineItems = (currentIndex: number) => {
52
+ const usedUuids = getUsedItemUuids(currentIndex);
53
+ return selectedLineItems.filter((item) => item.paymentStatus === 'PENDING' && !usedUuids.includes(item.uuid));
54
+ };
35
55
  const handleAppendPaymentMode = useCallback(() => {
36
- append({ method: null, amount: null, referenceCode: '' });
56
+ const availableItems = getAvailableLineItems(fields.length);
57
+ const initialItemUuid = availableItems.length === 1 ? availableItems[0].uuid : '';
58
+ append({
59
+ method: null,
60
+ amount: null,
61
+ referenceCode: '',
62
+ lineItemUuid: initialItemUuid,
63
+ });
37
64
  setFocus(`payment.${fields.length}.method`);
38
65
  }, [append, fields.length, setFocus]);
39
66
 
@@ -53,67 +80,91 @@ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue, ap
53
80
 
54
81
  return (
55
82
  <div className={styles.container}>
56
- {fields.map((field, index) => (
57
- <div key={field.id} className={styles.paymentMethodContainer}>
58
- <Controller
59
- control={control}
60
- name={`payment.${index}.method`}
61
- render={({ field }) => (
62
- <Dropdown
63
- {...field}
64
- id="paymentMethod"
65
- onChange={({ selectedItem }) => {
66
- setFocus(`payment.${index}.amount`);
67
- field.onChange(selectedItem);
68
- }}
69
- titleText={t('paymentMethod', 'Payment method')}
70
- label={t('selectPaymentMethod', 'Select payment method')}
71
- items={paymentModes}
72
- itemToString={(item) => (item ? item.name : '')}
73
- invalid={!!errors?.payment?.[index]?.method}
74
- invalidText={errors?.payment?.[index]?.method?.message}
75
- />
76
- )}
77
- />
78
- <Controller
79
- control={control}
80
- name={`payment.${index}.amount`}
81
- render={({ field }) => (
82
- <NumberInput
83
- {...field}
84
- id="paymentAmount"
85
- onChange={(e) => field.onChange(Number(e.target.value))}
86
- invalid={!!errors?.payment?.[index]?.amount}
87
- invalidText={errors?.payment?.[index]?.amount?.message}
88
- label={t('amount', 'Amount')}
89
- placeholder={t('enterAmount', 'Enter amount')}
90
- />
91
- )}
92
- />
93
- {shouldShowReferenceCode(index) && (
83
+ {fields.map((field, index) => {
84
+ const availableLineItems = getAvailableLineItems(index);
85
+ return (
86
+ <div key={field.id} className={styles.paymentMethodContainer}>
87
+ <Controller
88
+ control={control}
89
+ name={`payment.${index}.lineItemUuid`}
90
+ render={({ field }) => (
91
+ <Dropdown
92
+ {...field}
93
+ id="paymentLineItem"
94
+ onChange={({ selectedItem }) => {
95
+ field.onChange(selectedItem?.uuid);
96
+ setFocus(`payment.${index}.method`);
97
+ }}
98
+ titleText={t('selectLineItemToPay', 'Select line item to pay')}
99
+ label={t('selectLineItemToPay', 'Select line item to pay')}
100
+ items={availableLineItems}
101
+ itemToString={(item) => extractBillableName(item)}
102
+ invalid={!!errors?.payment?.[index]?.lineItemUuid}
103
+ invalidText={errors?.payment?.[index]?.lineItemUuid?.message}
104
+ className={styles.lineItemDropdown}
105
+ />
106
+ )}
107
+ />
94
108
  <Controller
95
- name={`payment.${index}.referenceCode`}
96
109
  control={control}
110
+ name={`payment.${index}.method`}
97
111
  render={({ field }) => (
98
- <TextInput
112
+ <Dropdown
99
113
  {...field}
100
- id="paymentReferenceCode"
101
- labelText={t('referenceNumber', 'Reference number')}
102
- placeholder={t('enterReferenceNumber', 'Enter ref. number')}
103
- type="text"
104
- invalid={!!errors?.payment?.[index]?.referenceCode}
105
- invalidText={errors?.payment?.[index]?.referenceCode?.message}
114
+ id="paymentMethod"
115
+ onChange={({ selectedItem }) => {
116
+ setFocus(`payment.${index}.amount`);
117
+ field.onChange(selectedItem);
118
+ }}
119
+ titleText={t('paymentMethod', 'Payment method')}
120
+ label={t('selectPaymentMethod', 'Select payment method')}
121
+ items={paymentModes}
122
+ itemToString={(item) => (item ? item.name : '')}
123
+ invalid={!!errors?.payment?.[index]?.method}
124
+ invalidText={errors?.payment?.[index]?.method?.message}
106
125
  />
107
126
  )}
108
127
  />
109
- )}
110
- <div className={styles.removeButtonContainer}>
111
- <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
128
+ <Controller
129
+ control={control}
130
+ name={`payment.${index}.amount`}
131
+ render={({ field }) => (
132
+ <NumberInput
133
+ {...field}
134
+ id="paymentAmount"
135
+ onChange={(e) => field.onChange(Number(e.target.value))}
136
+ invalid={!!errors?.payment?.[index]?.amount}
137
+ invalidText={errors?.payment?.[index]?.amount?.message}
138
+ label={t('amount', 'Amount')}
139
+ placeholder={t('enterAmount', 'Enter amount')}
140
+ />
141
+ )}
142
+ />
143
+ {shouldShowReferenceCode(index) && (
144
+ <Controller
145
+ name={`payment.${index}.referenceCode`}
146
+ control={control}
147
+ render={({ field }) => (
148
+ <TextInput
149
+ {...field}
150
+ id="paymentReferenceCode"
151
+ labelText={t('referenceNumber', 'Reference number')}
152
+ placeholder={t('enterReferenceNumber', 'Enter ref. number')}
153
+ type="text"
154
+ invalid={!!errors?.payment?.[index]?.referenceCode}
155
+ invalidText={errors?.payment?.[index]?.referenceCode?.message}
156
+ />
157
+ )}
158
+ />
159
+ )}
160
+ <div className={styles.removeButtonContainer}>
161
+ <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
162
+ </div>
112
163
  </div>
113
- </div>
114
- ))}
164
+ );
165
+ })}
115
166
  <Button
116
- disabled={disablePayment}
167
+ disabled={disablePayment || !selectedLineItems.length}
117
168
  size="md"
118
169
  onClick={handleAppendPaymentMode}
119
170
  className={styles.paymentButtons}
@@ -19,9 +19,9 @@
19
19
 
20
20
  .paymentMethodContainer {
21
21
  display: grid;
22
- grid-template-columns: repeat(4, minmax(auto, 1fr));
22
+ grid-template-columns: repeat(5, minmax(auto, 1fr));
23
23
  align-items: flex-start;
24
- column-gap: 1rem;
24
+ column-gap: layout.$spacing-02;
25
25
  margin: 0.625rem 0;
26
26
  width: 100%;
27
27
  }
@@ -52,3 +52,6 @@
52
52
  .removeButton {
53
53
  color: colors.$red-60;
54
54
  }
55
+ .lineItemDropdown {
56
+ min-width: 10rem;
57
+ }
@@ -67,7 +67,6 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
67
67
  globalActiveSheet,
68
68
  );
69
69
  remove();
70
-
71
70
  processBillPayment(paymentPayload, bill.uuid).then(
72
71
  (resp) => {
73
72
  showSnackbar({
@@ -142,7 +141,12 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
142
141
  className={styles.paymentError}
143
142
  />
144
143
  )}
145
- <PaymentForm {...formArrayMethods} disablePayment={amountDue <= 0} amountDue={amountDue} />
144
+ <PaymentForm
145
+ {...formArrayMethods}
146
+ selectedLineItems={selectedLineItems}
147
+ disablePayment={amountDue <= 0}
148
+ amountDue={amountDue}
149
+ />
146
150
  </div>
147
151
  </div>
148
152
  <div className={styles.divider} />
@@ -6,6 +6,7 @@ import { mockBill, mockedActiveSheet, mockLineItems, mockPaymentModes } from '..
6
6
  import { processBillPayment, usePaymentModes } from '../../billing.resource';
7
7
  import { useClockInStatus } from '../../payment-points/use-clock-in-status';
8
8
  import Payments from './payments.component';
9
+
9
10
  const mockProcessBillPayment = processBillPayment as jest.MockedFunction<typeof processBillPayment>;
10
11
  const mockUsePaymentModes = usePaymentModes as jest.MockedFunction<typeof usePaymentModes>;
11
12
  const mockShowSnackbar = showSnackbar as jest.MockedFunction<typeof showSnackbar>;
@@ -31,6 +32,7 @@ describe('Payment', () => {
31
32
  },
32
33
  },
33
34
  };
35
+
34
36
  mockProcessBillPayment.mockRejectedValueOnce(mockFieldErrorResponse);
35
37
  mockUsePaymentModes.mockReturnValue({
36
38
  paymentModes: mockPaymentModes,
@@ -49,10 +51,19 @@ describe('Payment', () => {
49
51
  });
50
52
 
51
53
  render(<Payments bill={mockBill as any} selectedLineItems={mockLineItems} />);
54
+
52
55
  const addPaymentMethod = screen.getByRole('button', { name: /Add payment option/i });
53
56
  await user.click(addPaymentMethod);
54
- await user.click(screen.getByRole('combobox', { name: /Payment method/i }));
55
- const cashOption = screen.getByRole('option', { name: /Cash/i });
57
+
58
+ const lineItemDropdown = await screen.findByRole('combobox', { name: /Select line item to pay/i });
59
+ await user.click(lineItemDropdown);
60
+
61
+ const lineItemOptions = await screen.findAllByRole('option');
62
+ await user.click(lineItemOptions[0]);
63
+
64
+ const paymentMethodDropdown = await screen.findByRole('combobox', { name: /Payment method/i });
65
+ await user.click(paymentMethodDropdown);
66
+ const cashOption = await screen.findByRole('option', { name: /Cash/i });
56
67
  await user.click(cashOption);
57
68
 
58
69
  const amountInput = screen.getByRole('spinbutton', { name: /Amount/i });
@@ -71,6 +82,8 @@ describe('Payment', () => {
71
82
  billableService: 'c15d25b9-12bb-441d-9241-cae541dd4575',
72
83
  display: 'BillLineItem',
73
84
  item: 'c15d25b9-12bb-441d-9241-cae541dd4575',
85
+ itemOrServiceConceptUuid: 'c42525b9-12bb-441d-9241-cae541dd4575',
86
+ serviceTypeUuid: '915d25b9-12bb-441d-9241-cae541dd4575',
74
87
  lineItemOrder: 0,
75
88
  order: null,
76
89
  paymentStatus: 'PAID',
@@ -87,6 +100,8 @@ describe('Payment', () => {
87
100
  billableService: '04be5832-5440-44d0-83d2-5c0dfd0ac7de',
88
101
  display: 'BillLineItem',
89
102
  item: '04be5832-5440-44d0-83d2-5c0dfd0ac7de',
103
+ itemOrServiceConceptUuid: 'c42525b9-12bb-441d-9241-cae541dd4575',
104
+ serviceTypeUuid: '915d25b9-12bb-441d-9241-cae541dd4575',
90
105
  lineItemOrder: 1,
91
106
  order: null,
92
107
  paymentStatus: 'PAID',
@@ -103,6 +118,8 @@ describe('Payment', () => {
103
118
  billableService: '3f5d0684-a280-477e-a67b-2a956a1f6dca',
104
119
  display: 'BillLineItem',
105
120
  item: '3f5d0684-a280-477e-a67b-2a956a1f6dca',
121
+ itemOrServiceConceptUuid: 'c42525b9-12bb-441d-9241-cae541dd4575',
122
+ serviceTypeUuid: '915d25b9-12bb-441d-9241-cae541dd4575',
106
123
  lineItemOrder: 2,
107
124
  order: null,
108
125
  paymentStatus: 'PAID',
@@ -133,7 +150,13 @@ describe('Payment', () => {
133
150
  ],
134
151
  patient: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
135
152
  payments: [
136
- { amount: 100, amountTendered: 100, attributes: [], instanceType: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74' },
153
+ {
154
+ amount: 100,
155
+ amountTendered: 100,
156
+ attributes: [],
157
+ instanceType: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
158
+ billLineItem: '314c25fd-2c90-4a7f-9f98-c99cd3f153e8',
159
+ },
137
160
  ],
138
161
  status: 'PENDING',
139
162
  },
@@ -158,18 +181,27 @@ describe('Payment', () => {
158
181
  error: null,
159
182
  mutate: jest.fn(),
160
183
  });
184
+
161
185
  render(<Payments bill={mockBill as any} selectedLineItems={mockLineItems} />);
186
+
162
187
  const addPaymentMethod = screen.getByRole('button', { name: /Add payment option/i });
163
188
  await user.click(addPaymentMethod);
164
189
 
165
- // Check if the payment method field is focused
166
- expect(screen.getByRole('combobox', { name: /Payment method/i })).toHaveFocus();
167
- await user.click(screen.getByRole('combobox', { name: /Payment method/i }));
168
- const cashOption = screen.getByRole('option', { name: /Cash/i });
190
+ const lineItemDropdown = await screen.findByRole('combobox', { name: /Select line item to pay/i });
191
+ expect(lineItemDropdown).toHaveFocus();
192
+
193
+ await user.click(lineItemDropdown);
194
+ const lineItemOptions = await screen.findAllByRole('option');
195
+ await user.click(lineItemOptions[0]);
196
+
197
+ const paymentMethodField = await screen.findByRole('combobox', { name: /Payment method/i });
198
+ expect(paymentMethodField).toHaveFocus();
199
+
200
+ await user.click(paymentMethodField);
201
+ const cashOption = await screen.findByRole('option', { name: /Cash/i });
169
202
  await user.click(cashOption);
170
- // Check if the amount field is focused
171
- expect(screen.getByRole('spinbutton', { name: /Amount/i })).toHaveFocus();
203
+
172
204
  const amountInput = screen.getByRole('spinbutton', { name: /Amount/i });
173
- await user.type(amountInput, '100');
205
+ expect(amountInput).toHaveFocus();
174
206
  });
175
207
  });
@@ -126,6 +126,7 @@ export const createPaymentPayload = (
126
126
  value: attribute.value,
127
127
  })),
128
128
  instanceType: payment.instanceType.uuid,
129
+ billLineItem: payment.billLineItem?.uuid,
129
130
  }));
130
131
 
131
132
  // Transform new payments
@@ -137,6 +138,7 @@ export const createPaymentPayload = (
137
138
  value: formValue.referenceCode,
138
139
  })),
139
140
  instanceType: formValue.method?.uuid,
141
+ billLineItem: formValue.lineItemUuid,
140
142
  }));
141
143
 
142
144
  // Combine and calculate payments
@@ -285,12 +285,35 @@ export interface Payment {
285
285
  attributes: Attribute[];
286
286
  amount: number;
287
287
  amountTendered: number;
288
+ billLineItem: BillLineItem;
288
289
  dateCreated: number;
289
290
  voided: boolean;
290
291
  resourceVersion: string;
291
292
  }
293
+ export interface BillLineItem {
294
+ uuid: string;
295
+ display: string;
296
+ voided: boolean;
297
+ voidReason: any;
298
+ item: string;
299
+ billableService: string;
300
+ quantity: number;
301
+ price: number;
302
+ priceName: string;
303
+ priceUuid: string;
304
+ lineItemOrder: number;
305
+ paymentStatus: string;
306
+ itemOrServiceConceptUuid: string;
307
+ serviceTypeUuid: string;
308
+ resourceVersion: string;
309
+ }
292
310
 
293
- export type FormPayment = { method: PaymentMethod; amount: string | number; referenceCode?: number | string };
311
+ export type FormPayment = {
312
+ method: PaymentMethod;
313
+ amount: string | number;
314
+ referenceCode?: number | string;
315
+ lineItemUuid: string;
316
+ };
294
317
 
295
318
  export type PaymentFormValue = {
296
319
  payment: Array<FormPayment>;
package/src/utils.ts CHANGED
@@ -166,3 +166,9 @@ export const computeWaivedAmount = (bill: MappedBill) => {
166
166
  .filter((payment) => payment.instanceType.name.toLowerCase() === 'waiver')
167
167
  .reduce((curr: number, prev) => curr + Number(prev.amountTendered), 0);
168
168
  };
169
+
170
+ export const extractBillableName = (billableService: string | { billableService: string }) => {
171
+ const parts =
172
+ typeof billableService === 'string' ? billableService.split(':') : billableService.billableService.split(':');
173
+ return parts.length > 1 ? parts[1] : '';
174
+ };