@openmrs/esm-billing-app 1.0.1-pre.100

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 (179) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +7 -0
  11. package/__mocks__/bills.mock.ts +394 -0
  12. package/__mocks__/delivery-summary.mock.ts +89 -0
  13. package/__mocks__/encounter-observation.mock.ts +10651 -0
  14. package/__mocks__/encounter-observations.mock.ts +6189 -0
  15. package/__mocks__/hiv-summary.mock.ts +22 -0
  16. package/__mocks__/patient-summary.mock.ts +32 -0
  17. package/__mocks__/patient.mock.ts +59 -0
  18. package/__mocks__/program-summary.mock.ts +43 -0
  19. package/__mocks__/react-i18next.js +57 -0
  20. package/dist/146.js +1 -0
  21. package/dist/146.js.map +1 -0
  22. package/dist/294.js +2 -0
  23. package/dist/294.js.LICENSE.txt +9 -0
  24. package/dist/294.js.map +1 -0
  25. package/dist/319.js +1 -0
  26. package/dist/384.js +1 -0
  27. package/dist/384.js.map +1 -0
  28. package/dist/421.js +1 -0
  29. package/dist/421.js.map +1 -0
  30. package/dist/533.js +1 -0
  31. package/dist/533.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/591.js +2 -0
  34. package/dist/591.js.LICENSE.txt +9 -0
  35. package/dist/591.js.map +1 -0
  36. package/dist/614.js +2 -0
  37. package/dist/614.js.LICENSE.txt +37 -0
  38. package/dist/614.js.map +1 -0
  39. package/dist/753.js +1 -0
  40. package/dist/753.js.map +1 -0
  41. package/dist/757.js +1 -0
  42. package/dist/770.js +1 -0
  43. package/dist/770.js.map +1 -0
  44. package/dist/783.js +1 -0
  45. package/dist/783.js.map +1 -0
  46. package/dist/788.js +1 -0
  47. package/dist/800.js +2 -0
  48. package/dist/800.js.LICENSE.txt +3 -0
  49. package/dist/800.js.map +1 -0
  50. package/dist/807.js +1 -0
  51. package/dist/833.js +1 -0
  52. package/dist/935.js +2 -0
  53. package/dist/935.js.LICENSE.txt +19 -0
  54. package/dist/935.js.map +1 -0
  55. package/dist/992.js +1 -0
  56. package/dist/992.js.map +1 -0
  57. package/dist/main.js +2 -0
  58. package/dist/main.js.LICENSE.txt +47 -0
  59. package/dist/main.js.map +1 -0
  60. package/dist/openmrs-esm-billing-app.js +1 -0
  61. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +609 -0
  62. package/dist/openmrs-esm-billing-app.js.map +1 -0
  63. package/dist/routes.json +1 -0
  64. package/e2e/README.md +115 -0
  65. package/e2e/core/global-setup.ts +32 -0
  66. package/e2e/core/index.ts +1 -0
  67. package/e2e/core/test.ts +20 -0
  68. package/e2e/fixtures/api.ts +27 -0
  69. package/e2e/fixtures/index.ts +1 -0
  70. package/e2e/pages/home-page.ts +9 -0
  71. package/e2e/pages/index.ts +1 -0
  72. package/e2e/specs/sample-test.spec.ts +11 -0
  73. package/e2e/support/github/Dockerfile +34 -0
  74. package/e2e/support/github/docker-compose.yml +24 -0
  75. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  76. package/example.env +6 -0
  77. package/i18next-parser.config.js +89 -0
  78. package/jest.config.js +34 -0
  79. package/package.json +124 -0
  80. package/playwright.config.ts +32 -0
  81. package/prettier.config.js +8 -0
  82. package/src/bill-history/bill-history.component.tsx +199 -0
  83. package/src/bill-history/bill-history.scss +151 -0
  84. package/src/bill-history/bill-history.test.tsx +122 -0
  85. package/src/billable-services/bill-waiver/bill-selection.component.tsx +76 -0
  86. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +110 -0
  87. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  88. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  89. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  90. package/src/billable-services/bill-waiver/patient-bills.component.tsx +137 -0
  91. package/src/billable-services/bill-waiver/utils.ts +41 -0
  92. package/src/billable-services/billable-service.resource.ts +72 -0
  93. package/src/billable-services/billable-services-home.component.tsx +51 -0
  94. package/src/billable-services/billable-services.component.tsx +255 -0
  95. package/src/billable-services/billable-services.scss +218 -0
  96. package/src/billable-services/billable-services.test.tsx +16 -0
  97. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  98. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  99. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  100. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  101. package/src/billable-services/dashboard/dashboard.scss +27 -0
  102. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  103. package/src/billable-services/dashboard/service-metrics.component.tsx +41 -0
  104. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  105. package/src/billable-services-admin-card-link.component.tsx +25 -0
  106. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  107. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  108. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  109. package/src/billing-form/billing-checkin-form.component.tsx +127 -0
  110. package/src/billing-form/billing-checkin-form.scss +13 -0
  111. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  112. package/src/billing-form/billing-form.component.tsx +347 -0
  113. package/src/billing-form/billing-form.resource.ts +32 -0
  114. package/src/billing-form/billing-form.scss +88 -0
  115. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  116. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  117. package/src/billing-header/billing-header.component.tsx +43 -0
  118. package/src/billing-header/billing-header.scss +83 -0
  119. package/src/billing-header/billing-illustration.component.tsx +30 -0
  120. package/src/billing.resource.ts +148 -0
  121. package/src/bills-table/bills-table.component.tsx +280 -0
  122. package/src/bills-table/bills-table.scss +181 -0
  123. package/src/bills-table/bills-table.test.tsx +154 -0
  124. package/src/config-schema.ts +50 -0
  125. package/src/constants.ts +3 -0
  126. package/src/dashboard.meta.ts +7 -0
  127. package/src/declarations.d.ts +4 -0
  128. package/src/helpers/functions.ts +66 -0
  129. package/src/helpers/index.ts +1 -0
  130. package/src/index.ts +72 -0
  131. package/src/invoice/invoice-table.component.tsx +189 -0
  132. package/src/invoice/invoice-table.scss +91 -0
  133. package/src/invoice/invoice.component.tsx +144 -0
  134. package/src/invoice/invoice.scss +93 -0
  135. package/src/invoice/invoice.test.tsx +242 -0
  136. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  137. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  138. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  139. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  140. package/src/invoice/payments/payment-history/payment-history.component.tsx +69 -0
  141. package/src/invoice/payments/payment.resource.ts +44 -0
  142. package/src/invoice/payments/payments.component.tsx +147 -0
  143. package/src/invoice/payments/payments.scss +46 -0
  144. package/src/invoice/payments/utils.ts +68 -0
  145. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  146. package/src/invoice/printable-invoice/print-receipt.component.tsx +29 -0
  147. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  148. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  149. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  150. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  151. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  152. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  153. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  154. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  155. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  156. package/src/left-panel-link.component.tsx +41 -0
  157. package/src/left-panel-link.test.tsx +38 -0
  158. package/src/metrics-cards/card.component.tsx +14 -0
  159. package/src/metrics-cards/card.scss +20 -0
  160. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  161. package/src/metrics-cards/metrics-cards.scss +12 -0
  162. package/src/metrics-cards/metrics-cards.test.tsx +44 -0
  163. package/src/metrics-cards/metrics.resource.ts +45 -0
  164. package/src/modal/require-payment-modal.component.tsx +85 -0
  165. package/src/modal/require-payment.scss +6 -0
  166. package/src/root.component.tsx +19 -0
  167. package/src/root.scss +30 -0
  168. package/src/routes.json +78 -0
  169. package/src/setup-tests.ts +13 -0
  170. package/src/types/index.ts +181 -0
  171. package/test-helpers.tsx +23 -0
  172. package/translations/am.json +117 -0
  173. package/translations/en.json +117 -0
  174. package/translations/es.json +117 -0
  175. package/translations/fr.json +117 -0
  176. package/translations/he.json +117 -0
  177. package/translations/km.json +117 -0
  178. package/tsconfig.json +16 -0
  179. package/webpack.config.js +1 -0
@@ -0,0 +1,218 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .container {
6
+ margin: 2rem 0;
7
+ }
8
+
9
+ .emptyStateContainer,
10
+ .loaderContainer {
11
+ @extend .container;
12
+ }
13
+
14
+ .serviceContainer {
15
+ background-color: $ui-02;
16
+ border: 1px solid $ui-03;
17
+ width: 100%;
18
+ margin: 0 auto;
19
+ max-width: 95vw;
20
+ padding-bottom: 0;
21
+
22
+ :has(.filterEmptyState) {
23
+ border-bottom: none;
24
+ }
25
+ }
26
+ .left-justified-items {
27
+ display: flex;
28
+ flex-direction: row;
29
+ align-items: center;
30
+ cursor: pointer;
31
+ align-items: center;
32
+ }
33
+
34
+ .filterContainer {
35
+ flex: 1;
36
+
37
+ :global(.cds--dropdown__wrapper--inline) {
38
+ gap: 0;
39
+ }
40
+
41
+ :global(.cds--list-box__menu-icon) {
42
+ height: 1rem;
43
+ }
44
+
45
+ :global(.cds--list-box__menu) {
46
+ min-width: max-content;
47
+ }
48
+
49
+ :global(.cds--list-box) {
50
+ margin-left: layout.$spacing-03;
51
+ }
52
+ }
53
+
54
+ .menu {
55
+ margin-left: layout.$spacing-03;
56
+ }
57
+
58
+ .headerContainer {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ padding: layout.$spacing-04 layout.$spacing-05;
63
+ background-color: $ui-02;
64
+ }
65
+
66
+ .backgroundDataFetchingIndicator {
67
+ align-items: center;
68
+ display: flex;
69
+ flex: 1;
70
+ justify-content: space-between;
71
+
72
+ &:global(.cds--inline-loading) {
73
+ max-height: 1rem;
74
+ }
75
+ }
76
+
77
+ .tableContainer section {
78
+ position: relative;
79
+ }
80
+
81
+ .tableContainer a {
82
+ text-decoration: none;
83
+ }
84
+
85
+ .pagination {
86
+ overflow: hidden;
87
+
88
+ &:global(.cds--pagination) {
89
+ border-top: none;
90
+ }
91
+ }
92
+
93
+ .hiddenRow {
94
+ display: none;
95
+ }
96
+
97
+ .emptyRow {
98
+ padding: 0 1rem;
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .visitSummaryContainer {
104
+ width: 100%;
105
+ max-width: 768px;
106
+ margin: 1rem auto;
107
+ }
108
+
109
+ .expandedActiveVisitRow > td > div {
110
+ max-height: max-content !important;
111
+ }
112
+
113
+ .expandedActiveVisitRow td {
114
+ padding: 0 2rem;
115
+ }
116
+
117
+ .expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
118
+ padding: 0 1rem;
119
+ }
120
+
121
+ .action {
122
+ margin-bottom: layout.$spacing-03;
123
+ }
124
+
125
+ .illo {
126
+ margin-top: layout.$spacing-05;
127
+ }
128
+
129
+ .content {
130
+ @include type.type-style('heading-compact-01');
131
+ color: $text-02;
132
+ margin-top: layout.$spacing-05;
133
+ margin-bottom: layout.$spacing-03;
134
+ }
135
+
136
+ .desktopHeading,
137
+ .tabletHeading {
138
+ text-align: left;
139
+ text-transform: capitalize;
140
+ flex: 1;
141
+
142
+ h4 {
143
+ @include type.type-style('heading-compact-02');
144
+ color: $text-02;
145
+
146
+ &:after {
147
+ content: '';
148
+ display: block;
149
+ width: 2rem;
150
+ padding-top: 3px;
151
+ border-bottom: 0.375rem solid;
152
+ @include brand-03(border-bottom-color);
153
+ }
154
+ }
155
+ }
156
+
157
+ .tile {
158
+ text-align: center;
159
+ border: 1px solid $ui-03;
160
+ }
161
+
162
+ .menuitem {
163
+ max-width: none;
164
+ }
165
+
166
+ .filterEmptyState {
167
+ display: flex;
168
+ justify-content: center;
169
+ align-items: center;
170
+ padding: layout.$spacing-05;
171
+ margin: layout.$spacing-09;
172
+ text-align: center;
173
+ }
174
+
175
+ .filterEmptyStateTile {
176
+ margin: auto;
177
+ }
178
+
179
+ .filterEmptyStateContent {
180
+ @include type.type-style('heading-compact-02');
181
+ color: $text-02;
182
+ margin-bottom: 0.5rem;
183
+ }
184
+
185
+ .filterEmptyStateHelper {
186
+ @include type.type-style('body-compact-01');
187
+ color: $text-02;
188
+ }
189
+
190
+ .metricsContainer {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ background-color: $ui-02;
194
+ height: layout.$spacing-10;
195
+ align-items: center;
196
+ padding: 0 layout.$spacing-05;
197
+ }
198
+
199
+ .metricsTitle {
200
+ @include type.type-style('heading-03');
201
+ color: $ui-05;
202
+ }
203
+
204
+ .actionsContainer {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ align-items: center;
208
+ background-color: $ui-02;
209
+ }
210
+ .actionBtn {
211
+ display: flex;
212
+ column-gap: 0.5rem;
213
+ }
214
+
215
+ .mainSection {
216
+ display: grid;
217
+ grid-template-columns: 16rem 1fr;
218
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import BillableServices from './billable-services.component';
4
+
5
+ describe('BillableService', () => {
6
+ test('renders an empty state when there are no billable services', () => {
7
+ renderBillableServices();
8
+
9
+ expect(screen.getByText(/Empty data illustration/i)).toBeInTheDocument();
10
+ expect(screen.getByText(/There are no services to display to display for this patient/i)).toBeInTheDocument();
11
+ });
12
+ });
13
+
14
+ function renderBillableServices() {
15
+ render(<BillableServices />);
16
+ }
@@ -0,0 +1,322 @@
1
+ /* eslint-disable curly */
2
+ import React, { useCallback, useRef, useState } from 'react';
3
+ import {
4
+ Button,
5
+ ComboBox,
6
+ Dropdown,
7
+ Form,
8
+ FormLabel,
9
+ InlineLoading,
10
+ Layer,
11
+ Search,
12
+ TextInput,
13
+ Tile,
14
+ } from '@carbon/react';
15
+ import { navigate, showSnackbar, useDebounce, useLayoutType, useSession } from '@openmrs/esm-framework';
16
+ import { Add, TrashCan, WarningFilled } from '@carbon/react/icons';
17
+ import { Controller, useFieldArray, useForm } from 'react-hook-form';
18
+ import { useTranslation } from 'react-i18next';
19
+ import { z } from 'zod';
20
+ import { zodResolver } from '@hookform/resolvers/zod';
21
+ import {
22
+ createBillableSerice,
23
+ useConceptsSearch,
24
+ usePaymentModes,
25
+ useServiceTypes,
26
+ } from '../billable-service.resource';
27
+ import { type ServiceConcept } from '../../types';
28
+ import styles from './add-billable-service.scss';
29
+
30
+ type PaymentMode = {
31
+ paymentMode: string;
32
+ price: string | number;
33
+ };
34
+
35
+ type PaymentModeFormValue = {
36
+ payment: Array<PaymentMode>;
37
+ };
38
+
39
+ const servicePriceSchema = z.object({
40
+ paymentMode: z.string().refine((value) => !!value, 'Payment method is required'),
41
+ price: z.union([
42
+ z.number().refine((value) => !!value, 'Price is required'),
43
+ z.string().refine((value) => !!value, 'Price is required'),
44
+ ]),
45
+ });
46
+
47
+ const paymentFormSchema = z.object({ payment: z.array(servicePriceSchema) });
48
+
49
+ const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: 0 };
50
+
51
+ const AddBillableService: React.FC = () => {
52
+ const { t } = useTranslation();
53
+
54
+ const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes();
55
+ const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes();
56
+ const [billableServicePayload, setBillableServicePayload] = useState<any>({});
57
+
58
+ const {
59
+ control,
60
+ handleSubmit,
61
+ formState: { errors },
62
+ } = useForm<any>({
63
+ mode: 'all',
64
+ defaultValues: {},
65
+ resolver: zodResolver(paymentFormSchema),
66
+ });
67
+ const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
68
+
69
+ const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]);
70
+ const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
71
+
72
+ const isTablet = useLayoutType() === 'tablet';
73
+ const searchInputRef = useRef(null);
74
+ const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(event.target.value);
75
+
76
+ const [selectedConcept, setSelectedConcept] = useState<ServiceConcept>(null);
77
+ const [searchTerm, setSearchTerm] = useState('');
78
+ const debouncedSearchTerm = useDebounce(searchTerm);
79
+ const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm);
80
+ const handleConceptChange = useCallback((selectedConcept: any) => {
81
+ setSelectedConcept(selectedConcept);
82
+ }, []);
83
+
84
+ const handleNavigateToServiceDashboard = () =>
85
+ navigate({
86
+ to: window.getOpenmrsSpaBase() + 'billable-services',
87
+ });
88
+
89
+ if (isLoadingPaymentModes && isLoadingServicesTypes) {
90
+ return (
91
+ <InlineLoading
92
+ status="active"
93
+ iconDescription={t('loadingDescription', 'Loading')}
94
+ description={t('loading', 'Loading data...')}
95
+ />
96
+ );
97
+ }
98
+
99
+ const onSubmit = (data) => {
100
+ const payload: any = {};
101
+
102
+ let servicePrices = [];
103
+ data.payment.forEach((element) => {
104
+ element.name = paymentModes.filter((p) => p.uuid === element.paymentMode)[0].name;
105
+ servicePrices.push(element);
106
+ });
107
+ payload.name = billableServicePayload.serviceName;
108
+ payload.shortName = billableServicePayload.shortName;
109
+ payload.serviceType = billableServicePayload.serviceType.uuid;
110
+ payload.servicePrices = servicePrices;
111
+ payload.serviceStatus = 'ENABLED';
112
+ payload.concept = selectedConcept?.concept?.uuid;
113
+
114
+ createBillableSerice(payload).then(
115
+ (resp) => {
116
+ showSnackbar({
117
+ title: t('billableService', 'Billable service'),
118
+ subtitle: 'Billable service created successfully',
119
+ kind: 'success',
120
+ timeoutInMs: 3000,
121
+ });
122
+ handleNavigateToServiceDashboard();
123
+ },
124
+ (error) => {
125
+ showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error?.message });
126
+ },
127
+ );
128
+ };
129
+
130
+ return (
131
+ <Form className={styles.form}>
132
+ <h4>{t('addBillableServices', 'Add Billable Services')}</h4>
133
+ <section className={styles.section}>
134
+ <Layer>
135
+ <TextInput
136
+ id="serviceName"
137
+ type="text"
138
+ labelText={t('serviceName', 'Service Name')}
139
+ size="md"
140
+ onChange={(e) =>
141
+ setBillableServicePayload({
142
+ ...billableServicePayload,
143
+ serviceName: e.target.value,
144
+ })
145
+ }
146
+ placeholder="Enter service name"
147
+ />
148
+ </Layer>
149
+ </section>
150
+ <section className={styles.section}>
151
+ <Layer>
152
+ <TextInput
153
+ id="serviceShortName"
154
+ type="text"
155
+ labelText={t('serviceShortName', 'Short Name')}
156
+ size="md"
157
+ onChange={(e) =>
158
+ setBillableServicePayload({
159
+ ...billableServicePayload,
160
+ shortName: e.target.value,
161
+ })
162
+ }
163
+ placeholder="Enter service short name"
164
+ />
165
+ </Layer>
166
+ </section>
167
+ <section>
168
+ <FormLabel className={styles.conceptLabel}>Associated Concept</FormLabel>
169
+ <Controller
170
+ name="search"
171
+ control={control}
172
+ render={({ field: { onChange, value, onBlur } }) => (
173
+ <ResponsiveWrapper isTablet={isTablet}>
174
+ <Search
175
+ ref={searchInputRef}
176
+ size="md"
177
+ id="conceptsSearch"
178
+ labelText={t('enterConcept', 'Associated concept')}
179
+ placeholder={t('searchConcepts', 'Search associated concept')}
180
+ className={errors?.search && styles.serviceError}
181
+ onChange={(e) => {
182
+ onChange(e);
183
+ handleSearchTermChange(e);
184
+ }}
185
+ renderIcon={errors?.search && <WarningFilled />}
186
+ onBlur={onBlur}
187
+ onClear={() => {
188
+ setSearchTerm('');
189
+ setSelectedConcept(null);
190
+ }}
191
+ value={(() => {
192
+ if (selectedConcept) {
193
+ return selectedConcept.display;
194
+ }
195
+ if (debouncedSearchTerm) {
196
+ return value;
197
+ }
198
+ })()}
199
+ />
200
+ </ResponsiveWrapper>
201
+ )}
202
+ />
203
+ {(() => {
204
+ if (!debouncedSearchTerm || selectedConcept) return null;
205
+ if (isSearching)
206
+ return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
207
+ if (searchResults && searchResults.length) {
208
+ return (
209
+ <ul className={styles.conceptsList}>
210
+ {/*TODO: use uuid instead of index as the key*/}
211
+ {searchResults?.map((searchResult, index) => (
212
+ <li
213
+ role="menuitem"
214
+ className={styles.service}
215
+ key={index}
216
+ onClick={() => handleConceptChange(searchResult)}>
217
+ {searchResult.display}
218
+ </li>
219
+ ))}
220
+ </ul>
221
+ );
222
+ }
223
+ return (
224
+ <Layer>
225
+ <Tile className={styles.emptyResults}>
226
+ <span>
227
+ {t('noResultsFor', 'No results for')} <strong>"{debouncedSearchTerm}"</strong>
228
+ </span>
229
+ </Tile>
230
+ </Layer>
231
+ );
232
+ })()}
233
+ </section>
234
+ <section className={styles.section}>
235
+ <Layer>
236
+ <ComboBox
237
+ id="serviceType"
238
+ items={serviceTypes ?? []}
239
+ titleText={t('serviceType', 'Service Type')}
240
+ itemToString={(item) => item?.display}
241
+ onChange={({ selectedItem }) => {
242
+ setBillableServicePayload({
243
+ ...billableServicePayload,
244
+ display: selectedItem?.display,
245
+ serviceType: selectedItem,
246
+ });
247
+ }}
248
+ placeholder="Select service type"
249
+ required
250
+ />
251
+ </Layer>
252
+ </section>
253
+
254
+ <section>
255
+ <div className={styles.container}>
256
+ {fields.map((field, index) => (
257
+ <div key={field.id} className={styles.paymentMethodContainer}>
258
+ <Controller
259
+ control={control}
260
+ name={`payment.${index}.paymentMode`}
261
+ render={({ field }) => (
262
+ <Layer>
263
+ <Dropdown
264
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
265
+ titleText={t('paymentMode', 'Payment Mode')}
266
+ label={t('selectPaymentMethod', 'Select payment method')}
267
+ items={paymentModes ?? []}
268
+ itemToString={(item) => (item ? item.name : '')}
269
+ invalid={!!errors?.payment?.[index]?.paymentMode}
270
+ invalidText={errors?.payment?.[index]?.paymentMode?.message}
271
+ />
272
+ </Layer>
273
+ )}
274
+ />
275
+ <Controller
276
+ control={control}
277
+ name={`payment.${index}.price`}
278
+ render={({ field }) => (
279
+ <Layer>
280
+ <TextInput
281
+ {...field}
282
+ invalid={!!errors?.payment?.[index]?.price}
283
+ invalidText={errors?.payment?.[index]?.price?.message}
284
+ labelText={t('sellingPrice', 'Selling Price')}
285
+ placeholder={t('sellingAmount', 'Enter selling price')}
286
+ />
287
+ </Layer>
288
+ )}
289
+ />
290
+ <div className={styles.removeButtonContainer}>
291
+ <TrashCan onClick={handleRemovePaymentMode} className={styles.removeButton} size={20} />
292
+ </div>
293
+ </div>
294
+ ))}
295
+ <Button
296
+ size="md"
297
+ onClick={handleAppendPaymentMode}
298
+ className={styles.paymentButtons}
299
+ renderIcon={(props) => <Add size={24} {...props} />}
300
+ iconDescription="Add">
301
+ {t('addPaymentOptions', 'Add payment option')}
302
+ </Button>
303
+ </div>
304
+ </section>
305
+
306
+ <section>
307
+ <Button kind="secondary" onClick={handleNavigateToServiceDashboard}>
308
+ {t('cancel', 'Cancel')}
309
+ </Button>
310
+ <Button type="submit" onClick={handleSubmit(onSubmit)}>
311
+ {t('save', 'Save')}
312
+ </Button>
313
+ </section>
314
+ </Form>
315
+ );
316
+ };
317
+
318
+ function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) {
319
+ return isTablet ? <Layer>{children} </Layer> : <>{children}</>;
320
+ }
321
+
322
+ export default AddBillableService;
@@ -0,0 +1,131 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .form {
7
+ display: flex;
8
+ flex-direction: column;
9
+ justify-content: space-between;
10
+ height: 100%;
11
+ }
12
+
13
+ .section {
14
+ margin: layout.$spacing-03;
15
+ }
16
+
17
+ .sectionTitle {
18
+ @include type.type-style('heading-compact-02');
19
+ color: $text-02;
20
+ margin-bottom: layout.$spacing-04;
21
+ }
22
+
23
+ .modalBody {
24
+ padding-bottom: layout.$spacing-05;
25
+ }
26
+
27
+ .container {
28
+ margin: 1rem;
29
+ }
30
+
31
+ .paymentContainer {
32
+ margin: layout.$layout-01;
33
+ padding: layout.$layout-01;
34
+ width: 70%;
35
+ border-right: 1px solid colors.$cool-gray-40;
36
+ }
37
+
38
+ .paymentButtons {
39
+ margin: layout.$layout-01 0;
40
+ }
41
+
42
+ .paymentMethodContainer {
43
+ display: grid;
44
+ grid-template-columns: repeat(4, minmax(auto, 1fr));
45
+ align-items: flex-start;
46
+ column-gap: 1rem;
47
+ margin: 0.625rem 0;
48
+ width: 100%;
49
+ }
50
+
51
+ .paymentTotals {
52
+ margin-top: layout.$spacing-01;
53
+ }
54
+
55
+ .processPayments {
56
+ display: flex;
57
+ justify-content: flex-end;
58
+ margin: layout.$spacing-05;
59
+ column-gap: layout.$spacing-04;
60
+ }
61
+
62
+ .errorPaymentContainer {
63
+ margin: layout.$spacing-04;
64
+ min-height: layout.$spacing-09;
65
+ }
66
+
67
+ .removeButtonContainer {
68
+ display: flex;
69
+ align-self: center;
70
+ cursor: pointer;
71
+ margin-left: layout.$spacing-07;
72
+ }
73
+
74
+ .removeButton {
75
+ color: colors.$red-60;
76
+ }
77
+
78
+ .service {
79
+ padding: 1rem 0.75rem;
80
+ }
81
+
82
+ .conceptsList {
83
+ background-color: $ui-02;
84
+ max-height: 14rem;
85
+ overflow-y: auto;
86
+ border: 1px solid $ui-03;
87
+
88
+ li:hover {
89
+ background-color: $ui-03;
90
+ }
91
+ }
92
+
93
+ .emptyResults {
94
+ @include type.type-style('body-compact-01');
95
+ color: $text-02;
96
+ min-height: 1rem;
97
+ border: 1px solid $ui-03;
98
+ }
99
+
100
+ .conceptLabel {
101
+ @include type.type-style('label-02');
102
+ margin: 1rem;
103
+ }
104
+
105
+ .errorContainer {
106
+ margin: 1rem;
107
+ }
108
+
109
+ .serviceError {
110
+ :global(.cds--search-input):focus {
111
+ outline: 2.5px solid $danger;
112
+ }
113
+
114
+ :global(.cds--search-magnifier) {
115
+ svg {
116
+ fill: $danger;
117
+ }
118
+ }
119
+ }
120
+
121
+ .errorMessage {
122
+ @include type.type-style('label-02');
123
+ color: $danger;
124
+ margin-top: 0.5rem;
125
+ }
126
+
127
+ .spinner {
128
+ &:global(.cds--inline-loading) {
129
+ min-height: 1rem;
130
+ }
131
+ }