@digihmis/esm-billing-app 1.0.0
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/README.md +4 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/16.js +1 -0
- package/dist/16.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/358.js +1 -0
- package/dist/358.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/496.js +12 -0
- package/dist/496.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/839.js +1 -0
- package/dist/839.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/965.js +15 -0
- package/dist/965.js.map +1 -0
- package/dist/digihmis-esm-billing-app.js +6 -0
- package/dist/digihmis-esm-billing-app.js.buildmanifest.json +827 -0
- package/dist/digihmis-esm-billing-app.js.map +1 -0
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/billing-checkin-form/billing-checkin-form-schema/index.ts +11 -0
- package/src/billing-checkin-form/billing-checkin-form.extension.tsx +179 -0
- package/src/billing-checkin-form/billing-checkin-form.resource.ts +42 -0
- package/src/billing-checkin-form/billing-checkin-form.scss +82 -0
- package/src/billing-checkin-form/billing-patient-type-tag/billing-patient-type-tag.component.tsx +39 -0
- package/src/billing-checkin-form/billing-patient-type-tag/billing-patient-type-tag.resource.ts +57 -0
- package/src/billing-checkin-form/billing-patient-type-tag/billing-patient-type-tag.scss +4 -0
- package/src/billing-checkin-form/helper/index.ts +2 -0
- package/src/billing-checkin-form/type/index.ts +42 -0
- package/src/billing-checkin-form/visit-attributes/visit-attributes-form.component.tsx +244 -0
- package/src/billing-checkin-form/visit-attributes/visit-attributes-form.resource.ts +23 -0
- package/src/billing-checkin-form/visit-attributes/visit-attributes-form.scss +186 -0
- package/src/billing-dashboard/billing-dashboard.component.tsx +224 -0
- package/src/billing-dashboard/billing-dashboard.resource.ts +101 -0
- package/src/billing-dashboard/billing-dashboard.scss +166 -0
- package/src/billing-dashboard/metrics/metrics.resource.ts +142 -0
- package/src/billing-dashboard/type/index.ts +176 -0
- package/src/billing-header/page-header/page-header.component.tsx +35 -0
- package/src/billing-header/page-header/page-header.scss +76 -0
- package/src/billing-header/page-header/page-icon/icon-wrapper.extension.tsx +17 -0
- package/src/billing-timesheet-modal/billing-timesheet-modal.scss +56 -0
- package/src/billing-timesheet-modal/billing-timesheet-modal.tsx +281 -0
- package/src/billing-timesheet-modal/billing-timesheet-resource-modal.ts +37 -0
- package/src/bills-table/bills-table.component.tsx +323 -0
- package/src/bills-table/bills-table.scss +252 -0
- package/src/config-schema.ts +148 -0
- package/src/constants/index.ts +2 -0
- package/src/dashboard.meta.ts +16 -0
- package/src/declarations.d.ts +6 -0
- package/src/index.ts +37 -0
- package/src/leftpanel/left-panel.component.tsx +98 -0
- package/src/leftpanel/left-panel.scss +13 -0
- package/src/routes.json +58 -0
- package/src/setup-tests.ts +1 -0
- package/src/utils/index.ts +270 -0
- package/translations/am.json +18 -0
- package/translations/ar.json +18 -0
- package/translations/en.json +18 -0
- package/translations/es.json +18 -0
- package/translations/fr.json +18 -0
- package/translations/he.json +18 -0
- package/translations/km.json +18 -0
- package/translations/pt.json +18 -0
- package/translations/sw.json +18 -0
- package/translations/vi.json +18 -0
- package/translations/zh.json +18 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, useSession } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { CashPoint, Timesheet } from './type';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get the current cashier's active timesheet (clocked-in session)
|
|
7
|
+
*/
|
|
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,
|
|
14
|
+
openmrsFetch,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const activeTimesheet = data?.data?.results?.[0] || null;
|
|
18
|
+
const isClockedIn = activeTimesheet !== null && activeTimesheet.clockOut === null;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
activeTimesheet,
|
|
22
|
+
isClockedIn,
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
mutate,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get all timesheets for a cashier within a date range
|
|
31
|
+
*/
|
|
32
|
+
export const useTimesheets = (
|
|
33
|
+
cashierUuid?: string,
|
|
34
|
+
cashPointUuid?: string,
|
|
35
|
+
startDatetime?: Date,
|
|
36
|
+
endDatetime?: Date,
|
|
37
|
+
) => {
|
|
38
|
+
const params = new URLSearchParams();
|
|
39
|
+
|
|
40
|
+
if (cashierUuid) {
|
|
41
|
+
params.append('cashier', cashierUuid);
|
|
42
|
+
}
|
|
43
|
+
if (cashPointUuid) {
|
|
44
|
+
params.append('cashPoint', cashPointUuid);
|
|
45
|
+
}
|
|
46
|
+
if (startDatetime) {
|
|
47
|
+
params.append('startDatetime', startDatetime.toISOString());
|
|
48
|
+
}
|
|
49
|
+
if (endDatetime) {
|
|
50
|
+
params.append('endDatetime', endDatetime.toISOString());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = `${restBaseUrl}/cashier/timesheet?${params.toString()}`;
|
|
54
|
+
|
|
55
|
+
const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<Timesheet> } }>(
|
|
56
|
+
cashierUuid || cashPointUuid || startDatetime || endDatetime ? url : null,
|
|
57
|
+
openmrsFetch,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
timesheets: data?.data?.results || [],
|
|
62
|
+
error,
|
|
63
|
+
isLoading,
|
|
64
|
+
mutate,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook to fetch all available cash points
|
|
70
|
+
*/
|
|
71
|
+
export const useCashPoints = () => {
|
|
72
|
+
const url = `${restBaseUrl}/cashier/cashPoint?v=default`;
|
|
73
|
+
|
|
74
|
+
const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<CashPoint> } }>(url, openmrsFetch);
|
|
75
|
+
|
|
76
|
+
// Filter out retired cash points
|
|
77
|
+
const activeCashPoints = data?.data?.results?.filter((cp) => !cp.retired) || [];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
cashPoints: activeCashPoints,
|
|
81
|
+
error,
|
|
82
|
+
isLoading,
|
|
83
|
+
mutate,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get a specific cash point by UUID
|
|
89
|
+
*/
|
|
90
|
+
export const useCashPoint = (cashPointUuid: string) => {
|
|
91
|
+
const url = cashPointUuid ? `${restBaseUrl}/cashier/cashPoint/${cashPointUuid}?v=default` : null;
|
|
92
|
+
|
|
93
|
+
const { data, error, isLoading, mutate } = useSWR<{ data: CashPoint }>(url, openmrsFetch);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
cashPoint: data?.data || null,
|
|
97
|
+
error,
|
|
98
|
+
isLoading,
|
|
99
|
+
mutate,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.tileContainer {
|
|
7
|
+
border: 1px solid colors.$cool-gray-30;
|
|
8
|
+
margin-bottom: layout.$spacing-04;
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
11
|
+
gap: 0;
|
|
12
|
+
height: 80px;
|
|
13
|
+
|
|
14
|
+
:global(.omrs-breakpoint-lt-desktop) & {
|
|
15
|
+
padding: layout.$spacing-04;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
19
|
+
padding: layout.$spacing-03;
|
|
20
|
+
grid-template-columns: 1fr;
|
|
21
|
+
grid-template-rows: auto auto auto;
|
|
22
|
+
gap: layout.$spacing-04;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tileHeader {
|
|
27
|
+
color: colors.$gray-70;
|
|
28
|
+
margin-bottom: layout.$spacing-03;
|
|
29
|
+
text-transform: none;
|
|
30
|
+
letter-spacing: 0;
|
|
31
|
+
|
|
32
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
33
|
+
margin-bottom: layout.$spacing-02;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.displayDetails {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
align-items: flex-start;
|
|
41
|
+
|
|
42
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
43
|
+
padding: layout.$spacing-03 0;
|
|
44
|
+
border-right: none;
|
|
45
|
+
border-bottom: 1px solid colors.$gray-20;
|
|
46
|
+
padding-bottom: layout.$spacing-04;
|
|
47
|
+
margin-bottom: layout.$spacing-03;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.countLabel {
|
|
52
|
+
color: colors.$gray-70;
|
|
53
|
+
margin-bottom: layout.$spacing-02;
|
|
54
|
+
@include type.type-style('caption-01');
|
|
55
|
+
font-weight: bold;
|
|
56
|
+
|
|
57
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
58
|
+
@include type.type-style('caption-01');
|
|
59
|
+
font-weight: bold;
|
|
60
|
+
margin-bottom: layout.$spacing-01;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.displayData {
|
|
65
|
+
color: colors.$gray-100;
|
|
66
|
+
font-weight: bold;
|
|
67
|
+
@include type.type-style('caption-01');
|
|
68
|
+
font-size: 25px;
|
|
69
|
+
|
|
70
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
71
|
+
font-size: 2.625rem;
|
|
72
|
+
line-height: 2.75rem;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.headerActions {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
padding-right: 1rem;
|
|
81
|
+
|
|
82
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
83
|
+
gap: 0.25rem;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dashboardContainer {
|
|
88
|
+
padding: layout.$spacing-05;
|
|
89
|
+
padding-bottom: 0;
|
|
90
|
+
background-color: var(--cds-field);
|
|
91
|
+
|
|
92
|
+
:global(.omrs-breakpoint-lt-desktop) & {
|
|
93
|
+
padding: layout.$spacing-02;
|
|
94
|
+
padding-bottom: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
98
|
+
padding: layout.$spacing-03;
|
|
99
|
+
padding-bottom: 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.clockInInfo {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: flex-end;
|
|
107
|
+
width: 100%;
|
|
108
|
+
gap: layout.$spacing-03;
|
|
109
|
+
|
|
110
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
111
|
+
flex-direction: row;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: layout.$spacing-02;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.clockInText {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
gap: 0.25rem;
|
|
122
|
+
|
|
123
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
124
|
+
font-size: 0.875rem;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.clockInTime {
|
|
129
|
+
color: colors.$gray-70;
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.alarmIcon {
|
|
134
|
+
flex-shrink: 0;
|
|
135
|
+
|
|
136
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
137
|
+
width: 18px;
|
|
138
|
+
height: 18px;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.emptyStateContainer {
|
|
143
|
+
border: 1px solid colors.$gray-20;
|
|
144
|
+
padding: 3rem 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.tile {
|
|
148
|
+
margin: auto;
|
|
149
|
+
width: fit-content;
|
|
150
|
+
background: colors.$gray-20;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.tileContent {
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
align-items: center;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.content {
|
|
160
|
+
@include type.type-style('heading-compact-02');
|
|
161
|
+
color: colors.$gray-70;
|
|
162
|
+
margin-bottom: layout.$spacing-03;
|
|
163
|
+
}
|
|
164
|
+
.loader {
|
|
165
|
+
min-height: fit-content;
|
|
166
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import { type MappedBill, type PatientInvoice, PaymentStatus } from '../type';
|
|
3
|
+
import { formatDate, openmrsFetch, parseDate, restBaseUrl } from '@openmrs/esm-framework';
|
|
4
|
+
import sortBy from 'lodash-es/sortBy';
|
|
5
|
+
import useSWR from 'swr';
|
|
6
|
+
import isEmpty from 'lodash-es/isEmpty';
|
|
7
|
+
import { extractString, useCurrencyFormatting } from '../../utils';
|
|
8
|
+
|
|
9
|
+
export const mapBillProperties = (bill: PatientInvoice): MappedBill => {
|
|
10
|
+
const mappedBill: MappedBill = {
|
|
11
|
+
id: bill?.id,
|
|
12
|
+
uuid: bill?.uuid,
|
|
13
|
+
patientName: bill?.patient?.display.split('-')?.[1],
|
|
14
|
+
identifier: bill?.patient?.display.split('-')?.[0],
|
|
15
|
+
patientUuid: bill?.patient?.uuid,
|
|
16
|
+
status: bill?.lineItems.some((item) => item?.paymentStatus === PaymentStatus.PENDING)
|
|
17
|
+
? PaymentStatus.PENDING
|
|
18
|
+
: PaymentStatus.PAID,
|
|
19
|
+
receiptNumber: bill?.receiptNumber,
|
|
20
|
+
cashier: bill?.cashier,
|
|
21
|
+
cashPointUuid: bill?.cashPoint?.uuid,
|
|
22
|
+
cashPointName: bill?.cashPoint?.name,
|
|
23
|
+
cashPointLocation: bill?.cashPoint?.location?.display,
|
|
24
|
+
dateCreated: bill?.dateCreated ? formatDate(parseDate(bill?.dateCreated), { mode: 'wide' }) : '--',
|
|
25
|
+
dateCreatedUnformatted: bill?.dateCreated,
|
|
26
|
+
lineItems: bill?.lineItems.filter((li) => !li?.voided),
|
|
27
|
+
billingService: extractString(
|
|
28
|
+
bill?.lineItems.map((bill) => bill?.item || bill?.billableService || '--').join(' '),
|
|
29
|
+
),
|
|
30
|
+
payments: bill?.payments,
|
|
31
|
+
display: bill?.display,
|
|
32
|
+
totalAmount: bill?.lineItems?.map((item) => item?.price * item?.quantity).reduce((prev, curr) => prev + curr, 0),
|
|
33
|
+
tenderedAmount: bill?.payments?.map((item) => item?.amountTendered).reduce((prev, curr) => prev + curr, 0),
|
|
34
|
+
referenceCodes: bill?.payments
|
|
35
|
+
.map((payment) =>
|
|
36
|
+
payment.attributes
|
|
37
|
+
.filter((attr) => attr.attributeType.description === 'Reference Number')
|
|
38
|
+
.map((attr) => {
|
|
39
|
+
return {
|
|
40
|
+
paymentMode: payment.instanceType.name,
|
|
41
|
+
value: attr.value,
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
.flat()
|
|
46
|
+
.map((ref) => `${ref.paymentMode}: ${ref.value}`)
|
|
47
|
+
.join(', '),
|
|
48
|
+
adjustmentReason: bill?.adjustmentReason,
|
|
49
|
+
balance: bill?.balance,
|
|
50
|
+
totalPayments: bill?.totalPayments,
|
|
51
|
+
totalDeposits: bill?.totalDeposits,
|
|
52
|
+
totalExempted: bill?.totalExempted,
|
|
53
|
+
closed: bill?.closed,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return mappedBill;
|
|
57
|
+
};
|
|
58
|
+
export const useBills = (
|
|
59
|
+
patientUuid: string = '',
|
|
60
|
+
billStatus: PaymentStatus.PENDING | '' | string = '',
|
|
61
|
+
startingDate: Date = dayjs().startOf('day').toDate(),
|
|
62
|
+
endDate: Date = dayjs().endOf('day').toDate(),
|
|
63
|
+
) => {
|
|
64
|
+
const startingDateISO = startingDate.toISOString();
|
|
65
|
+
const endDateISO = endDate.toISOString();
|
|
66
|
+
|
|
67
|
+
const url = `${restBaseUrl}/cashier/bill?status=${billStatus}&v=custom:(uuid,display,voided,voidReason,adjustedBy,cashPoint:(uuid,name),cashier:(uuid,display),dateCreated,lineItems,patient:(uuid,display))&createdOnOrAfter=${startingDateISO}&createdOnOrBefore=${endDateISO}`;
|
|
68
|
+
|
|
69
|
+
const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: { results: Array<PatientInvoice> } }>(
|
|
70
|
+
patientUuid ? `${url}&patientUuid=${patientUuid}` : url,
|
|
71
|
+
openmrsFetch,
|
|
72
|
+
{
|
|
73
|
+
errorRetryCount: 2,
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const sortBills = sortBy(data?.data?.results ?? [], ['dateCreated']).reverse();
|
|
78
|
+
const filteredBills = billStatus === '' ? sortBills : sortBills?.filter((bill) => bill?.status === billStatus);
|
|
79
|
+
const mappedResults = filteredBills?.map((bill) => mapBillProperties(bill));
|
|
80
|
+
const filteredResults = mappedResults?.filter((res) => res.patientUuid === patientUuid);
|
|
81
|
+
const formattedBills = isEmpty(patientUuid) ? mappedResults : filteredResults || [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
bills: formattedBills,
|
|
85
|
+
error,
|
|
86
|
+
isLoading,
|
|
87
|
+
isValidating,
|
|
88
|
+
mutate,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A custom hook for calculating bill metrics.
|
|
94
|
+
*
|
|
95
|
+
* This hook takes in an array of bills and calculates the total amount for different
|
|
96
|
+
* bill statuses (cumulative, pending, paid) using provided helper functions.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array<Object>} bills - An array of bill objects. Each bill object should have a `status` and `lineItems` properties.
|
|
99
|
+
*
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
export const useBillMetrics = (
|
|
103
|
+
bills: Array<MappedBill>,
|
|
104
|
+
): {
|
|
105
|
+
totalBills: string;
|
|
106
|
+
pendingBills: string;
|
|
107
|
+
paidBills: string;
|
|
108
|
+
exemptedBills: string;
|
|
109
|
+
} => {
|
|
110
|
+
const { paidTotal, pendingTotal, cumulativeTotal, exemptedTotal } = calculateBillTotals(bills);
|
|
111
|
+
const { format: formatCurrency } = useCurrencyFormatting();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
totalBills: formatCurrency(cumulativeTotal),
|
|
115
|
+
pendingBills: formatCurrency(pendingTotal),
|
|
116
|
+
paidBills: formatCurrency(paidTotal),
|
|
117
|
+
exemptedBills: formatCurrency(exemptedTotal),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const calculateBillTotals = (bills: Array<MappedBill>) => {
|
|
122
|
+
let paidTotal = 0;
|
|
123
|
+
let pendingTotal = 0;
|
|
124
|
+
let cumulativeTotal = 0;
|
|
125
|
+
let exemptedTotal = 0;
|
|
126
|
+
|
|
127
|
+
bills.forEach((bill) => {
|
|
128
|
+
const amount = bill.totalAmount || 0;
|
|
129
|
+
if (bill.status === PaymentStatus.PAID) {
|
|
130
|
+
paidTotal += amount;
|
|
131
|
+
} else if (bill.status === PaymentStatus.PENDING) {
|
|
132
|
+
pendingTotal += amount;
|
|
133
|
+
} else if (bill.status === PaymentStatus.EXEMPTED) {
|
|
134
|
+
exemptedTotal += amount;
|
|
135
|
+
}
|
|
136
|
+
cumulativeTotal += amount;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { paidTotal, pendingTotal, cumulativeTotal, exemptedTotal };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default calculateBillTotals;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { OpenmrsResource } from '@openmrs/esm-framework/';
|
|
2
|
+
|
|
3
|
+
export enum PaymentStatus {
|
|
4
|
+
POSTED = 'POSTED',
|
|
5
|
+
PENDING = 'PENDING',
|
|
6
|
+
PAID = 'PAID',
|
|
7
|
+
CREDITED = 'CREDITED',
|
|
8
|
+
CANCELLED = 'CANCELLED',
|
|
9
|
+
ADJUSTED = 'ADJUSTED',
|
|
10
|
+
EXEMPTED = 'EXEMPTED',
|
|
11
|
+
}
|
|
12
|
+
export interface Provider {
|
|
13
|
+
uuid: string;
|
|
14
|
+
display: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LineItem {
|
|
18
|
+
uuid: string;
|
|
19
|
+
display: string;
|
|
20
|
+
voided: boolean;
|
|
21
|
+
voidReason: string | null;
|
|
22
|
+
item: string;
|
|
23
|
+
billableService: string;
|
|
24
|
+
quantity: number;
|
|
25
|
+
price: number;
|
|
26
|
+
priceName: string;
|
|
27
|
+
priceUuid: string;
|
|
28
|
+
lineItemOrder: number;
|
|
29
|
+
resourceVersion: string;
|
|
30
|
+
paymentStatus: string;
|
|
31
|
+
itemOrServiceConceptUuid: string;
|
|
32
|
+
serviceTypeUuid: string;
|
|
33
|
+
order: OpenmrsResource;
|
|
34
|
+
}
|
|
35
|
+
interface AttributeType {
|
|
36
|
+
uuid?: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
retired: boolean;
|
|
40
|
+
attributeOrder?: number;
|
|
41
|
+
format?: string;
|
|
42
|
+
foreignKey?: string | null;
|
|
43
|
+
regExp?: string | null;
|
|
44
|
+
required: boolean;
|
|
45
|
+
value?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Attribute {
|
|
49
|
+
uuid: string;
|
|
50
|
+
display: string;
|
|
51
|
+
voided: boolean;
|
|
52
|
+
voidReason: string | null;
|
|
53
|
+
value: string;
|
|
54
|
+
attributeType: AttributeType;
|
|
55
|
+
order: number;
|
|
56
|
+
valueName: string;
|
|
57
|
+
resourceVersion: string;
|
|
58
|
+
}
|
|
59
|
+
interface PaymentInstanceType {
|
|
60
|
+
uuid: string;
|
|
61
|
+
name: string;
|
|
62
|
+
description: string;
|
|
63
|
+
retired: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface Payment {
|
|
66
|
+
uuid: string;
|
|
67
|
+
instanceType: PaymentInstanceType;
|
|
68
|
+
attributes: Array<Attribute>;
|
|
69
|
+
amount: number;
|
|
70
|
+
amountTendered: number;
|
|
71
|
+
dateCreated: number;
|
|
72
|
+
voided: boolean;
|
|
73
|
+
resourceVersion: string;
|
|
74
|
+
}
|
|
75
|
+
export interface MappedBill {
|
|
76
|
+
uuid: string;
|
|
77
|
+
id: number;
|
|
78
|
+
patientUuid: string;
|
|
79
|
+
patientName: string;
|
|
80
|
+
cashPointUuid: string;
|
|
81
|
+
cashPointName: string;
|
|
82
|
+
cashPointLocation: string;
|
|
83
|
+
cashier: Provider;
|
|
84
|
+
receiptNumber: string;
|
|
85
|
+
status: PaymentStatus;
|
|
86
|
+
identifier: string;
|
|
87
|
+
dateCreated: string;
|
|
88
|
+
dateCreatedUnformatted: string;
|
|
89
|
+
lineItems: Array<LineItem>;
|
|
90
|
+
billingService: string;
|
|
91
|
+
payments: Array<Payment>;
|
|
92
|
+
totalAmount?: number;
|
|
93
|
+
tenderedAmount?: number;
|
|
94
|
+
display?: string;
|
|
95
|
+
referenceCodes?: string;
|
|
96
|
+
adjustmentReason?: string;
|
|
97
|
+
totalPayments?: number;
|
|
98
|
+
totalDeposits?: number;
|
|
99
|
+
totalExempted?: number;
|
|
100
|
+
balance?: number;
|
|
101
|
+
closed?: boolean;
|
|
102
|
+
}
|
|
103
|
+
interface LocationLink {
|
|
104
|
+
rel: string;
|
|
105
|
+
uri: string;
|
|
106
|
+
resourceAlias: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface Location {
|
|
110
|
+
uuid: string;
|
|
111
|
+
display: string;
|
|
112
|
+
links: Array<LocationLink>;
|
|
113
|
+
}
|
|
114
|
+
export interface CashPoint {
|
|
115
|
+
uuid: string;
|
|
116
|
+
name: string;
|
|
117
|
+
description: string;
|
|
118
|
+
retired: boolean;
|
|
119
|
+
location: Location;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface PatientLink {
|
|
123
|
+
rel: string;
|
|
124
|
+
uri: string;
|
|
125
|
+
resourceAlias: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface Patient {
|
|
129
|
+
uuid: string;
|
|
130
|
+
display: string;
|
|
131
|
+
links: Array<PatientLink>;
|
|
132
|
+
identifiers: Array<{ uuid: string; display: string }>;
|
|
133
|
+
}
|
|
134
|
+
export interface PatientInvoice {
|
|
135
|
+
uuid: string;
|
|
136
|
+
display: string;
|
|
137
|
+
voided: boolean;
|
|
138
|
+
voidReason: string | null;
|
|
139
|
+
adjustedBy: any[];
|
|
140
|
+
billAdjusted: any;
|
|
141
|
+
cashPoint: CashPoint;
|
|
142
|
+
cashier: Provider;
|
|
143
|
+
dateCreated: string;
|
|
144
|
+
lineItems: Array<LineItem>;
|
|
145
|
+
patient: Patient;
|
|
146
|
+
payments: Payment[];
|
|
147
|
+
receiptNumber: string;
|
|
148
|
+
status: PaymentStatus;
|
|
149
|
+
adjustmentReason: any;
|
|
150
|
+
id: number;
|
|
151
|
+
resourceVersion: string;
|
|
152
|
+
totalPayments?: number;
|
|
153
|
+
totalDeposits?: number;
|
|
154
|
+
totalExempted?: number;
|
|
155
|
+
balance?: number;
|
|
156
|
+
closed?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface Timesheet {
|
|
160
|
+
uuid: string;
|
|
161
|
+
cashier: {
|
|
162
|
+
uuid: string;
|
|
163
|
+
display: string;
|
|
164
|
+
};
|
|
165
|
+
cashPoint: {
|
|
166
|
+
uuid: string;
|
|
167
|
+
name: string;
|
|
168
|
+
location?: {
|
|
169
|
+
uuid: string;
|
|
170
|
+
display: string;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
clockIn: string;
|
|
174
|
+
clockOut: string | null;
|
|
175
|
+
display: string;
|
|
176
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Calendar } from '@carbon/react/icons';
|
|
4
|
+
import { useSession, formatDate, ExtensionSlot, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './page-header.scss';
|
|
6
|
+
import { IconWrapper } from './page-icon/icon-wrapper.extension';
|
|
7
|
+
import { BillingConfig } from '../../config-schema';
|
|
8
|
+
|
|
9
|
+
export const PageHeader: React.FC = () => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const userSession = useSession();
|
|
12
|
+
const { billingIcon } = useConfig<BillingConfig>();
|
|
13
|
+
const userLocation = userSession?.sessionLocation?.display;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={styles.header}>
|
|
17
|
+
<div className={styles['left-justified-items']}>
|
|
18
|
+
<IconWrapper src={billingIcon.src} alt={billingIcon.alt} />
|
|
19
|
+
<div className={styles['page-labels']}>
|
|
20
|
+
<p className={styles['page-name']}>{userLocation}</p>
|
|
21
|
+
<p className={styles['page-subtitle']}>{t('billingHeader', 'Billing')}</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div className={styles['right-justified-items']}>
|
|
25
|
+
<div className={styles.extensionSlot}>
|
|
26
|
+
<ExtensionSlot name={'provider-banner-slot'} />
|
|
27
|
+
<div className={styles['date-and-location']}>
|
|
28
|
+
<Calendar size={16} />
|
|
29
|
+
<span className={styles.value}>{formatDate(new Date(), { mode: 'wide', noToday: true })}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
@include type.type-style('body-compact-02');
|
|
7
|
+
height: layout.$spacing-12;
|
|
8
|
+
border: 1px solid colors.$gray-20;
|
|
9
|
+
border-left: 0px;
|
|
10
|
+
display: flex;
|
|
11
|
+
background: var(--cds-field);
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
margin-top: 1.5rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.left-justified-items {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
align-items: center;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.right-justified-items {
|
|
23
|
+
@include type.type-style('body-01');
|
|
24
|
+
color: colors.$gray-80;
|
|
25
|
+
padding-top: layout.$spacing-05;
|
|
26
|
+
padding-bottom: layout.$spacing-07;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.page-subtitle {
|
|
30
|
+
@include type.type-style('caption-01');
|
|
31
|
+
color: colors.$gray-70;
|
|
32
|
+
font-size: layout.$spacing-06;
|
|
33
|
+
}
|
|
34
|
+
.page-name {
|
|
35
|
+
@include type.type-style('caption-01');
|
|
36
|
+
color: colors.$gray-80;
|
|
37
|
+
font-size: layout.$spacing-05;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.page-labels {
|
|
41
|
+
p:first-of-type {
|
|
42
|
+
margin-bottom: layout.$spacing-02;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.date-and-location {
|
|
47
|
+
margin-top: layout.$spacing-03;
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: flex-end;
|
|
50
|
+
align-items: center;
|
|
51
|
+
margin-right: layout.$spacing-05;
|
|
52
|
+
@include type.type-style('caption-01');
|
|
53
|
+
font-size: 0.83rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.value {
|
|
57
|
+
margin-left: layout.$spacing-02;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.middot {
|
|
61
|
+
margin: 0 layout.$spacing-03;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.view {
|
|
65
|
+
@include type.type-style('caption-01');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
svg.iconOverrides {
|
|
69
|
+
width: 72 !important;
|
|
70
|
+
height: 72 !important;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.svgContainer svg {
|
|
74
|
+
width: 72px;
|
|
75
|
+
height: 72px;
|
|
76
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
};
|