@digihmis/esm-billing-app 1.0.0 → 1.0.1

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.
Files changed (75) hide show
  1. package/package.json +2 -1
  2. package/src/billing-checkin-form/billing-checkin-form.extension.tsx +90 -88
  3. package/src/billing-checkin-form/billing-checkin-form.resource.ts +92 -27
  4. package/src/billing-checkin-form/visit-attributes/visit-attributes-form.resource.ts +28 -16
  5. package/src/billing-dashboard/billing-dashboard.resource.ts +16 -7
  6. package/src/billing-header/page-header/page-icon/icon-wrapper.extension.tsx +2 -17
  7. package/src/billing-timesheet-modal/billing-timesheet-modal.tsx +4 -2
  8. package/src/billing-timesheet-modal/billing-timesheet-resource-modal.ts +7 -28
  9. package/src/config-schema.ts +8 -2
  10. package/src/leftpanel/left-panel.component.tsx +6 -98
  11. package/src/routes.json +12 -8
  12. package/dist/117.js +0 -1
  13. package/dist/117.js.map +0 -1
  14. package/dist/132.js +0 -1
  15. package/dist/132.js.map +0 -1
  16. package/dist/152.js +0 -1
  17. package/dist/152.js.map +0 -1
  18. package/dist/16.js +0 -1
  19. package/dist/16.js.map +0 -1
  20. package/dist/209.js +0 -1
  21. package/dist/209.js.map +0 -1
  22. package/dist/317.js +0 -1
  23. package/dist/317.js.map +0 -1
  24. package/dist/349.js +0 -1
  25. package/dist/349.js.map +0 -1
  26. package/dist/358.js +0 -1
  27. package/dist/358.js.map +0 -1
  28. package/dist/371.js +0 -1
  29. package/dist/371.js.map +0 -1
  30. package/dist/378.js +0 -1
  31. package/dist/378.js.map +0 -1
  32. package/dist/442.js +0 -1
  33. package/dist/442.js.map +0 -1
  34. package/dist/45.js +0 -1
  35. package/dist/45.js.map +0 -1
  36. package/dist/466.js +0 -1
  37. package/dist/466.js.map +0 -1
  38. package/dist/496.js +0 -12
  39. package/dist/496.js.map +0 -1
  40. package/dist/508.js +0 -1
  41. package/dist/508.js.map +0 -1
  42. package/dist/61.js +0 -1
  43. package/dist/61.js.map +0 -1
  44. package/dist/640.js +0 -1
  45. package/dist/640.js.map +0 -1
  46. package/dist/689.js +0 -1
  47. package/dist/689.js.map +0 -1
  48. package/dist/709.js +0 -1
  49. package/dist/709.js.map +0 -1
  50. package/dist/712.js +0 -1
  51. package/dist/712.js.map +0 -1
  52. package/dist/720.js +0 -1
  53. package/dist/720.js.map +0 -1
  54. package/dist/742.js +0 -1
  55. package/dist/742.js.map +0 -1
  56. package/dist/771.js +0 -1
  57. package/dist/771.js.map +0 -1
  58. package/dist/806.js +0 -1
  59. package/dist/806.js.map +0 -1
  60. package/dist/839.js +0 -1
  61. package/dist/839.js.map +0 -1
  62. package/dist/912.js +0 -1
  63. package/dist/912.js.map +0 -1
  64. package/dist/913.js +0 -1
  65. package/dist/913.js.map +0 -1
  66. package/dist/940.js +0 -1
  67. package/dist/940.js.map +0 -1
  68. package/dist/965.js +0 -15
  69. package/dist/965.js.map +0 -1
  70. package/dist/digihmis-esm-billing-app.js +0 -6
  71. package/dist/digihmis-esm-billing-app.js.buildmanifest.json +0 -827
  72. package/dist/digihmis-esm-billing-app.js.map +0 -1
  73. package/dist/main.js +0 -17
  74. package/dist/main.js.map +0 -1
  75. package/dist/routes.json +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digihmis/esm-billing-app",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Billing microfrontend for the OpenMRS SPA",
5
5
  "browser": "dist/digihmis-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -37,6 +37,7 @@
37
37
  "url": "https://github.com/its-kios09/DigiHMIS-apps/issues"
38
38
  },
39
39
  "dependencies": {
40
+ "@digihmis/esm-commons-lib": "1.0.0",
40
41
  "lodash-es": "^4.17.15",
41
42
  "react-to-print": "2.14.13"
42
43
  },
@@ -1,16 +1,22 @@
1
1
  import { FilterableMultiSelect, InlineLoading, InlineNotification, Tag } from '@carbon/react';
2
2
  import { zodResolver } from '@hookform/resolvers/zod';
3
- import { showSnackbar, useConfig, useFeatureFlag } from '@openmrs/esm-framework';
4
- import React, { useCallback, useEffect, useState, useMemo } from 'react';
3
+ import { showSnackbar, useConfig } from '@openmrs/esm-framework';
4
+ import React, { useEffect, useMemo, useState } from 'react';
5
5
  import { FormProvider, useForm } from 'react-hook-form';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { BillingConfig } from '../config-schema';
8
8
  import styles from './billing-checkin-form.scss';
9
9
  import VisitAttributesForm from './visit-attributes/visit-attributes-form.component';
10
- import { createPatientBill, useBillableItems, useCashPoint } from './billing-checkin-form.resource';
10
+ import {
11
+ addBillLinesForVisit,
12
+ priceForMode,
13
+ useBillableItems,
14
+ usePatientType,
15
+ type BillableItem,
16
+ type SelectedService,
17
+ } from './billing-checkin-form.resource';
11
18
  import { visitAttributesFormSchema, VisitAttributesFormValue } from './billing-checkin-form-schema';
12
19
  import { hasPatientBeenExempted } from './helper';
13
- import { EXEMPTED_PAYMENT_STATUS, PENDING_PAYMENT_STATUS } from '../constants';
14
20
  import { useCurrencyFormatting } from '../utils';
15
21
 
16
22
  type BillingCheckInFormProps = {
@@ -21,12 +27,12 @@ type BillingCheckInFormProps = {
21
27
  const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setExtraVisitInfo }) => {
22
28
  const { t } = useTranslation();
23
29
  const {
24
- visitAttributeTypes: { isPatientExempted },
30
+ visitAttributeTypes: { isPatientExempted, patientType: patientTypeAttrUuid },
25
31
  } = useConfig<BillingConfig>();
26
- const { cashPoints, isLoading: isLoadingCashPoints, error: cashError } = useCashPoint();
27
32
  const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
33
+ const { patientType } = usePatientType(patientUuid);
28
34
  const [attributes, setAttributes] = useState([]);
29
- const [selectedServices, setSelectedServices] = useState([]);
35
+ const [selectedServices, setSelectedServices] = useState<Array<SelectedService>>([]);
30
36
  const { formatSimple } = useCurrencyFormatting();
31
37
 
32
38
  const formMethods = useForm<VisitAttributesFormValue>({
@@ -41,70 +47,78 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
41
47
  resolver: zodResolver(visitAttributesFormSchema),
42
48
  });
43
49
 
44
- const isPatientExemptedValue = formMethods.watch('isPatientExempted');
45
50
  const paymentMethod = formMethods.watch('paymentMethods');
46
51
 
47
- const handleCreateBill = useCallback((createBillPayload) => {
48
- createPatientBill(createBillPayload).then(
49
- () => {
50
- showSnackbar({ title: 'Patient Bill', subtitle: 'Patient has been billed successfully', kind: 'success' });
51
- },
52
- (error) => {
53
- const errorMessage = JSON.stringify(error?.responseBody?.error?.message?.replace(/\[/g, '').replace(/\]/g, ''));
54
- showSnackbar({
55
- title: 'Patient Bill Error',
56
- subtitle: `An error has occurred while creating patient bill, Contact system administrator quoting this error ${errorMessage}`,
57
- kind: 'error',
58
- isLowContrast: true,
59
- });
60
- },
61
- );
62
- }, []);
63
-
64
- const handleBillingService = (selectedItems) => {
65
- setSelectedServices(selectedItems);
66
-
67
- const cashPointUuid = cashPoints?.[0]?.uuid ?? '';
68
- const billStatus = hasPatientBeenExempted(attributes, isPatientExempted)
69
- ? EXEMPTED_PAYMENT_STATUS
70
- : PENDING_PAYMENT_STATUS;
71
-
72
- const lineItems = selectedItems.map((item, index) => {
73
- const priceForPaymentMode =
74
- item.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || item?.servicePrices[0];
75
- return {
76
- billableService: item?.uuid ?? '',
77
- quantity: 1,
78
- price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
79
- priceName: 'Default',
80
- priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
81
- lineItemOrder: index,
82
- paymentStatus: billStatus,
83
- };
84
- });
52
+ // Only offer services actually priced for the chosen payment mode. A service with no price for this
53
+ // mode is not billable under it (it must not quote another mode's price), so it is left out entirely.
54
+ const availableItems = useMemo(
55
+ () => lineItems.filter((item) => priceForMode(item, paymentMethod) != null),
56
+ [lineItems, paymentMethod],
57
+ );
85
58
 
86
- const billPayload = {
87
- lineItems: lineItems,
88
- cashPoint: cashPointUuid,
89
- patient: patientUuid,
90
- status: billStatus,
91
- payments: [],
92
- };
59
+ // The price is per mode, so a service picked under one mode can't carry over to another. Clear the
60
+ // selection whenever the payment mode changes -- the clerk re-picks against the new mode's prices.
61
+ useEffect(() => {
62
+ setSelectedServices([]);
63
+ }, [paymentMethod]);
93
64
 
94
- setExtraVisitInfo({
95
- handleCreateExtraVisitInfo: () => handleCreateBill(billPayload),
96
- attributes,
97
- });
98
- };
65
+ const formatPrice = (price: number | string | undefined) =>
66
+ formatSimple(parseFloat(String(price ?? 0)), { minimumFractionDigits: 2, maximumFractionDigits: 2 });
99
67
 
68
+ // Hand the visit form a callback it runs *after* the visit is saved: DigiHMIS bills are
69
+ // visit-scoped, so the selected services can only be charged once the visit uuid exists. An
70
+ // exempted patient (or no selection) is a no-op. The captured payment/insurance/exemption details
71
+ // still ride along as visit attributes.
100
72
  useEffect(() => {
73
+ const exempt = hasPatientBeenExempted(attributes, isPatientExempted);
74
+ // New vs revisit is derived from the patient's visit history, not asked on the form.
75
+ const attributesWithType = patientTypeAttrUuid
76
+ ? [...attributes, { attributeType: patientTypeAttrUuid, value: patientType }]
77
+ : attributes;
101
78
  setExtraVisitInfo({
102
- handleCreateExtraVisitInfo: () => {},
103
- attributes,
79
+ handleCreateExtraVisitInfo: (visitUuid?: string) => {
80
+ if (exempt || !visitUuid || selectedServices.length === 0) {
81
+ return Promise.resolve();
82
+ }
83
+ return addBillLinesForVisit(visitUuid, selectedServices, paymentMethod).then(
84
+ () =>
85
+ showSnackbar({
86
+ title: t('patientBill', 'Patient bill'),
87
+ subtitle: t('patientBilledSuccessfully', 'Patient has been billed successfully'),
88
+ kind: 'success',
89
+ }),
90
+ (error) =>
91
+ showSnackbar({
92
+ title: t('patientBillError', 'Patient bill error'),
93
+ subtitle:
94
+ error?.responseBody?.error?.message ??
95
+ error?.message ??
96
+ t('billErrorGeneric', 'An error occurred while billing the patient'),
97
+ kind: 'error',
98
+ isLowContrast: true,
99
+ }),
100
+ );
101
+ },
102
+ attributes: attributesWithType,
104
103
  });
105
- }, [attributes, setExtraVisitInfo]);
104
+ }, [
105
+ attributes,
106
+ isPatientExempted,
107
+ patientType,
108
+ patientTypeAttrUuid,
109
+ paymentMethod,
110
+ selectedServices,
111
+ setExtraVisitInfo,
112
+ t,
113
+ ]);
114
+
115
+ const handleBillingService = (selectedItems: Array<BillableItem>) => {
116
+ setSelectedServices(
117
+ selectedItems.map((item) => ({ uuid: item.uuid, name: item.name, price: item.price, prices: item.prices })),
118
+ );
119
+ };
106
120
 
107
- if (isLoadingLineItems || isLoadingCashPoints) {
121
+ if (isLoadingLineItems) {
108
122
  return (
109
123
  <InlineLoading
110
124
  status="active"
@@ -114,7 +128,7 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
114
128
  );
115
129
  }
116
130
 
117
- if (cashError || lineError) {
131
+ if (lineError) {
118
132
  return (
119
133
  <InlineNotification
120
134
  kind="error"
@@ -131,34 +145,19 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
131
145
  {paymentMethod && (
132
146
  <section className={styles.sectionContainer}>
133
147
  <div className={styles.sectionField}>
134
- {lineItems && lineItems.length > 0 ? (
148
+ {availableItems && availableItems.length > 0 ? (
135
149
  <>
136
- {selectedServices.map((item, index) => {
137
- const priceForPaymentMode =
138
- item.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || item?.servicePrices[0];
139
- const price = priceForPaymentMode ? parseFloat(priceForPaymentMode.price) : 0;
140
- return (
141
- <Tag key={index} type="cyan" className={styles.selectedServicesTag}>
142
- {item.name} - {formatSimple(price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
143
- </Tag>
144
- );
145
- })}
150
+ {selectedServices.map((item, index) => (
151
+ <Tag key={index} type="cyan" className={styles.selectedServicesTag}>
152
+ {item.name} - {formatPrice(priceForMode(item, paymentMethod))}
153
+ </Tag>
154
+ ))}
146
155
  <FilterableMultiSelect
147
- key={isPatientExemptedValue}
148
156
  id="billing-service"
149
157
  titleText={t('searchServices', 'Search services')}
150
- items={lineItems}
151
- itemToString={(item) => {
152
- if (!item) {
153
- return '';
154
- }
155
- const priceForPaymentMode =
156
- item.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || item?.servicePrices[0];
157
- const price = priceForPaymentMode ? parseFloat(priceForPaymentMode.price) : 0;
158
- return `${item.name} - ${formatSimple(price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
159
- }}
160
- onChange={({ selectedItems }) => handleBillingService(selectedItems)}
161
- disabled={isPatientExemptedValue === ''}
158
+ items={availableItems}
159
+ itemToString={(item) => (item ? `${item.name} - ${formatPrice(priceForMode(item, paymentMethod))}` : '')}
160
+ onChange={({ selectedItems }) => handleBillingService(selectedItems as Array<BillableItem>)}
162
161
  />
163
162
  </>
164
163
  ) : (
@@ -166,7 +165,10 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
166
165
  kind="info"
167
166
  lowContrast
168
167
  title={t('noBillableServices', 'No billable services')}
169
- subtitle={t('noBillableServicesDescription', 'Please configure billable services to continue.')}
168
+ subtitle={t(
169
+ 'noServicesPricedForMode',
170
+ 'No services are priced for the selected payment method. Configure a price for this payment method to bill it here.',
171
+ )}
170
172
  />
171
173
  )}
172
174
  </div>
@@ -1,42 +1,107 @@
1
- import { openmrsFetch, type OpenmrsResource, restBaseUrl, useConfig } from '@openmrs/esm-framework';
1
+ import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
2
2
  import { useMemo, useState } from 'react';
3
3
  import useSWR from 'swr';
4
- import { BillingConfig } from '../config-schema';
4
+ import { type BillingConfig } from '../config-schema';
5
5
 
6
- export const useCashPoint = () => {
7
- const url = `${restBaseUrl}/cashier/cashPoint`;
8
- const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
6
+ export interface BillableItemPrice {
7
+ paymentMode: string;
8
+ price: number | string;
9
+ }
9
10
 
10
- return { isLoading, error, cashPoints: data?.data?.results ?? [] };
11
- };
12
- export const createPatientBill = (payload) => {
13
- const postUrl = `${restBaseUrl}/cashier/bill`;
14
- return openmrsFetch(postUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
11
+ export interface BillableItem {
12
+ uuid: string;
13
+ name: string;
14
+ price: number | string;
15
+ prices?: Array<BillableItemPrice>;
16
+ concept?: { uuid: string; display: string };
17
+ serviceType?: { uuid: string; display: string };
18
+ description?: string;
19
+ retired?: boolean;
20
+ }
21
+
22
+ export interface SelectedService {
23
+ uuid?: string;
24
+ name: string;
25
+ price: number | string;
26
+ prices?: Array<BillableItemPrice>;
27
+ }
28
+
29
+ /**
30
+ * The price to charge for a payment mode.
31
+ *
32
+ * Per-mode pricing is strict: once a service has any per-mode prices, it is only offered at the price
33
+ * set for the *selected* mode. A mode with no price of its own is not priced for that service -- it
34
+ * must never borrow another mode's price (that is what made a cash-only service quote the cash price
35
+ * under Mobile Money). Returns undefined in that case so the caller can hide/skip it. Only a service
36
+ * with no per-mode prices at all falls back to its single flat price.
37
+ */
38
+ export const priceForMode = (
39
+ item: { price: number | string; prices?: Array<BillableItemPrice> },
40
+ mode?: string,
41
+ ): number | string | undefined => {
42
+ const overrides = item.prices ?? [];
43
+ if (overrides.length > 0) {
44
+ const match = mode ? overrides.find((entry) => entry.paymentMode === mode) : undefined;
45
+ return match?.price;
46
+ }
47
+ return item.price;
15
48
  };
16
49
 
50
+ /**
51
+ * The facility's billable items (concept + price) from the DigiHMIS billing module.
52
+ */
17
53
  export const useBillableItems = () => {
18
54
  const { registrationServiceType } = useConfig<BillingConfig>();
19
- const url = `${restBaseUrl}/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(uuid,display),servicePrices:(uuid,name,price,paymentMode))`;
20
- const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
55
+ const url = `${restBaseUrl}/digihmisbilling/billableitem`;
56
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<BillableItem> } }>(url, openmrsFetch);
21
57
  const [searchTerm, setSearchTerm] = useState('');
22
58
 
23
- const filteredItems = useMemo(() => {
24
- if (!data?.data?.results) {
25
- return [];
59
+ const lineItems = useMemo(() => {
60
+ let items = (data?.data?.results ?? []).filter((item) => !item.retired);
61
+ // Check-in only offers the registration service(s) -- items in the configured registration
62
+ // service type. If none is configured, fall back to showing all so nothing silently disappears.
63
+ if (registrationServiceType) {
64
+ items = items.filter((item) => item.serviceType?.uuid === registrationServiceType);
26
65
  }
66
+ const query = searchTerm.trim().toLowerCase();
67
+ return query ? items.filter((item) => item.name?.toLowerCase().includes(query)) : items;
68
+ }, [data?.data?.results, searchTerm, registrationServiceType]);
27
69
 
28
- const lowerSearchTerm = searchTerm.toLowerCase();
70
+ return { lineItems, isLoading, error, searchTerm, setSearchTerm };
71
+ };
29
72
 
30
- return data.data.results.filter(
31
- (item) => item.serviceType?.uuid === registrationServiceType && item.name.toLowerCase().includes(lowerSearchTerm),
32
- );
33
- }, [data?.data?.results, searchTerm, registrationServiceType]);
73
+ /**
74
+ * New vs revisit is derived, never asked -- a manual choice only confuses the clerk. Checked at
75
+ * check-in before the new visit is saved: a patient with any existing visit is a REVISIT, otherwise
76
+ * NEW. The value is written to the patient-type visit attribute alongside the payment details.
77
+ */
78
+ export const usePatientType = (patientUuid: string) => {
79
+ const url = patientUuid
80
+ ? `${restBaseUrl}/visit?patient=${patientUuid}&includeInactive=true&limit=1&v=custom:(uuid)`
81
+ : null;
82
+ const { data, isLoading } = useSWR<{ data: { results: Array<{ uuid: string }> } }>(url, openmrsFetch);
83
+ const patientType = (data?.data?.results?.length ?? 0) > 0 ? 'REVISIT' : 'NEW';
84
+ return { patientType, isLoading };
85
+ };
34
86
 
35
- return {
36
- lineItems: filteredItems,
37
- isLoading,
38
- error,
39
- searchTerm,
40
- setSearchTerm,
41
- };
87
+ /**
88
+ * Charge the selected services to the visit's bill. DigiHMIS bills are visit-scoped, so this runs
89
+ * once the visit has been saved and its uuid is known; each service becomes a bill line.
90
+ */
91
+ export const addBillLinesForVisit = (
92
+ visitUuid: string,
93
+ services: Array<SelectedService>,
94
+ paymentMode?: string,
95
+ ) => {
96
+ return Promise.all(
97
+ services.map((service) => {
98
+ const params = new URLSearchParams({
99
+ visit: visitUuid,
100
+ description: service.name ?? '',
101
+ unitPrice: String(priceForMode(service, paymentMode) ?? '0'),
102
+ quantity: '1',
103
+ });
104
+ return openmrsFetch(`${restBaseUrl}/digihmisbilling/bill/line?${params.toString()}`, { method: 'POST' });
105
+ }),
106
+ );
42
107
  };
@@ -1,23 +1,35 @@
1
- import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
1
+ import { useConfig } from '@openmrs/esm-framework';
2
2
  import { BillingConfig } from '../../config-schema';
3
- import { PaymentMethod } from '../type';
4
- import useSWR from 'swr';
3
+
4
+ export interface PaymentModeOption {
5
+ uuid: string;
6
+ name: string;
7
+ description?: string;
8
+ }
9
+
10
+ /**
11
+ * DigiHMIS models the payment mode as a fixed enum
12
+ * (org.openmrs.module.digihmisbilling.model.PaymentMode), not a REST-listed entity, so the check-in
13
+ * payment-method options come from that closed set. The `uuid` carries the enum name, which is what
14
+ * the visit attribute stores and what the payment endpoint expects.
15
+ */
16
+ const PAYMENT_MODES: Array<PaymentModeOption> = [
17
+ { uuid: 'CASH', name: 'Cash', description: 'Physical cash at the till' },
18
+ { uuid: 'MOBILE_MONEY', name: 'Mobile money', description: 'M-Pesa and the like' },
19
+ { uuid: 'CARD', name: 'Card', description: 'Card payment' },
20
+ { uuid: 'INSURANCE', name: 'Insurance', description: 'Settled by an insurer or scheme' },
21
+ { uuid: 'WAIVER', name: 'Waiver', description: 'Written off (waiver / exemption)' },
22
+ ];
5
23
 
6
24
  export const usePaymentModes = (excludeWaiver: boolean = true) => {
7
25
  const { excludedPaymentMode } = useConfig<BillingConfig>();
8
- const url = `${restBaseUrl}/cashier/paymentMode?v=full`;
9
- const { data, isLoading, error, mutate } = useSWR<{ data: { results: Array<PaymentMethod> } }>(url, openmrsFetch, {
10
- errorRetryCount: 2,
11
- });
12
- const allowedPaymentModes =
13
- excludedPaymentMode?.length > 0
14
- ? (data?.data?.results.filter((mode) => !excludedPaymentMode.some((excluded) => excluded.uuid === mode.uuid)) ??
15
- [])
16
- : (data?.data?.results ?? []);
26
+ const excluded = new Set((excludedPaymentMode ?? []).map((mode) => mode.uuid));
27
+ const allowedPaymentModes = PAYMENT_MODES.filter((mode) => !excluded.has(mode.uuid));
28
+
17
29
  return {
18
- paymentModes: excludeWaiver ? allowedPaymentModes : data?.data?.results,
19
- isLoading,
20
- mutate,
21
- error,
30
+ paymentModes: excludeWaiver ? allowedPaymentModes.filter((mode) => mode.uuid !== 'WAIVER') : allowedPaymentModes,
31
+ isLoading: false,
32
+ error: undefined,
33
+ mutate: () => {},
22
34
  };
23
35
  };
@@ -6,15 +6,24 @@ import { CashPoint, Timesheet } from './type';
6
6
  * Hook to get the current cashier's active timesheet (clocked-in session)
7
7
  */
8
8
  export const useCurrentTimesheet = () => {
9
- const session = useSession();
10
- const providerUuid = session?.currentProvider?.uuid;
11
-
12
- const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<Timesheet> } }>(
13
- providerUuid ? `${restBaseUrl}/cashier/timesheet?cashier=${providerUuid}&getProviderOpenTimesheet=true` : null,
9
+ // The DigiHMIS shift is scoped to the logged-in user server-side; /shift/active returns it or {}.
10
+ const { data, error, isLoading, mutate } = useSWR<{ data: any }>(
11
+ `${restBaseUrl}/digihmisbilling/shift/active`,
14
12
  openmrsFetch,
15
13
  );
16
14
 
17
- const activeTimesheet = data?.data?.results?.[0] || null;
15
+ const shift = data?.data && data.data.uuid ? data.data : null;
16
+ const activeTimesheet = shift
17
+ ? {
18
+ uuid: shift.uuid,
19
+ cashPoint: { uuid: shift.paymentPoint?.uuid, name: shift.paymentPoint?.display },
20
+ cashier: shift.cashier,
21
+ clockIn: shift.clockIn,
22
+ clockOut: shift.clockOut ?? null,
23
+ totalCollected: shift.totalCollected,
24
+ collectedByMode: shift.collectedByMode,
25
+ }
26
+ : null;
18
27
  const isClockedIn = activeTimesheet !== null && activeTimesheet.clockOut === null;
19
28
 
20
29
  return {
@@ -69,7 +78,7 @@ export const useTimesheets = (
69
78
  * Hook to fetch all available cash points
70
79
  */
71
80
  export const useCashPoints = () => {
72
- const url = `${restBaseUrl}/cashier/cashPoint?v=default`;
81
+ const url = `${restBaseUrl}/digihmisbilling/paymentpoint`;
73
82
 
74
83
  const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<CashPoint> } }>(url, openmrsFetch);
75
84
 
@@ -1,17 +1,2 @@
1
- import React from 'react';
2
-
3
- interface IconWrapperProps {
4
- src: string;
5
- alt?: string;
6
- className?: string;
7
- style?: React.CSSProperties;
8
- }
9
-
10
- export const IconWrapper: React.FC<IconWrapperProps> = ({
11
- src,
12
- alt = 'Icon',
13
- className,
14
- style = { width: '100px', height: '148px' },
15
- }) => {
16
- return <img src={src} alt={alt} className={className} style={style} />;
17
- };
1
+ // Moved to the shared library so every app uses one implementation.
2
+ export { IconWrapper, type IconWrapperProps } from '@digihmis/esm-commons-lib';
@@ -59,7 +59,7 @@ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
59
59
  setIsSubmitting(true);
60
60
 
61
61
  try {
62
- const response = await clockIn(providerUuid, selectedCashPoint);
62
+ const response = await clockIn(selectedCashPoint);
63
63
  await mutateTimesheet();
64
64
 
65
65
  showSnackbar({
@@ -114,7 +114,7 @@ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
114
114
  await clockOut(activeTimesheet.uuid);
115
115
 
116
116
  // Clock in to new cash point
117
- const response = await clockIn(providerUuid, selectedCashPoint);
117
+ const response = await clockIn(selectedCashPoint);
118
118
  await mutateTimesheet();
119
119
 
120
120
  showSnackbar({
@@ -181,6 +181,8 @@ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
181
181
  {t('currentlyActiveAt', 'Currently active at {{location}}', {
182
182
  location: activeTimesheet.cashPoint?.name || t('unknown', 'Unknown'),
183
183
  })}
184
+ {' · '}
185
+ {t('collectedThisShift', 'Collected: {{amount}}', { amount: activeTimesheet.totalCollected ?? 0 })}
184
186
  </span>
185
187
  )}
186
188
  </div>
@@ -1,37 +1,16 @@
1
1
  import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
2
 
3
3
  /**
4
- * Clock in a cashier to a cash point
4
+ * Open a cashier shift at a payment point. The cashier is the logged-in user, resolved server-side,
5
+ * so only the payment point is passed.
5
6
  */
6
- export const clockIn = async (cashierUuid: string, cashPointUuid: string) => {
7
- const url = `${restBaseUrl}/cashier/timesheet`;
8
-
9
- return openmrsFetch(url, {
7
+ export const clockIn = async (paymentPointUuid: string) => {
8
+ return openmrsFetch(`${restBaseUrl}/digihmisbilling/shift/clockin?paymentPoint=${paymentPointUuid}`, {
10
9
  method: 'POST',
11
- headers: {
12
- 'Content-Type': 'application/json',
13
- },
14
- body: {
15
- cashier: cashierUuid,
16
- cashPoint: cashPointUuid,
17
- clockIn: new Date().toISOString(),
18
- },
19
10
  });
20
11
  };
21
12
 
22
- /**
23
- * Clock out the current cashier (close the timesheet)
24
- */
25
- export const clockOut = async (timesheetUuid: string) => {
26
- const url = `${restBaseUrl}/cashier/timesheet/${timesheetUuid}`;
27
-
28
- return openmrsFetch(url, {
29
- method: 'POST',
30
- headers: {
31
- 'Content-Type': 'application/json',
32
- },
33
- body: {
34
- clockOut: new Date().toISOString(),
35
- },
36
- });
13
+ /** Close an open shift. */
14
+ export const clockOut = async (shiftUuid: string) => {
15
+ return openmrsFetch(`${restBaseUrl}/digihmisbilling/shift/${shiftUuid}/clockout`, { method: 'POST' });
37
16
  };
@@ -8,6 +8,7 @@ export interface BillingConfig {
8
8
  exemptionCategory: string;
9
9
  billPaymentStatus: string;
10
10
  shaBenefitPackagesAndInterventions: string;
11
+ patientType: string;
11
12
  };
12
13
  insurancePaymentMethod: string;
13
14
  patientExemptionCategories: Array<{ value: string; label: string }>;
@@ -62,11 +63,16 @@ export const configSchema: ConfigSchema = {
62
63
  _description: 'The bill payment status visit attribute uuid',
63
64
  _default: '919b51c9-8e2e-468f-8354-181bf3e55786',
64
65
  },
66
+ patientType: {
67
+ _type: Type.String,
68
+ _description: 'The patient type (new vs revisit) visit attribute uuid',
69
+ _default: '6b9e1f7a-2c3d-4e5f-8a1b-9c0d1e2f3a4b',
70
+ },
65
71
  },
66
72
  insurancePaymentMethod: {
67
73
  _type: Type.String,
68
- _description: 'Insurance Payment method UUID',
69
- _default: 'beac329b-f1dc-4a33-9e7c-d95821a137a6',
74
+ _description: 'The payment-method value that reveals the insurance fields (DigiHMIS PaymentMode enum)',
75
+ _default: 'INSURANCE',
70
76
  },
71
77
  patientExemptionCategories: {
72
78
  _type: Type.Array,