@palladium-ethiopia/esm-clinical-workflow-app 5.4.2-pre.4 → 5.4.2-pre.6

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 (44) hide show
  1. package/.turbo/turbo-build.log +8 -2
  2. package/dist/112.js +1 -0
  3. package/dist/112.js.map +1 -0
  4. package/dist/127.js +1 -1
  5. package/dist/449.js +1 -1
  6. package/dist/449.js.map +1 -1
  7. package/dist/467.js +6 -0
  8. package/dist/467.js.map +1 -0
  9. package/dist/54.js +1 -0
  10. package/dist/54.js.map +1 -0
  11. package/dist/542.js +1 -1
  12. package/dist/542.js.map +1 -1
  13. package/dist/{532.js → 813.js} +18 -18
  14. package/dist/813.js.map +1 -0
  15. package/dist/834.js +1 -0
  16. package/dist/834.js.map +1 -0
  17. package/dist/916.js +1 -1
  18. package/dist/ethiopia-esm-clinical-workflow-app.js +3 -3
  19. package/dist/ethiopia-esm-clinical-workflow-app.js.buildmanifest.json +103 -122
  20. package/dist/ethiopia-esm-clinical-workflow-app.js.map +1 -1
  21. package/dist/main.js +10 -5
  22. package/dist/main.js.map +1 -1
  23. package/dist/routes.json +1 -1
  24. package/package.json +1 -1
  25. package/src/config-schema.ts +16 -0
  26. package/src/index.ts +15 -0
  27. package/src/mru/billing-information/billing-information.resource.ts +137 -0
  28. package/src/mru/billing-information/billing-information.scss +55 -0
  29. package/src/mru/billing-information/billing-information.workspace.tsx +369 -0
  30. package/src/mru/dashboard.component.tsx +18 -0
  31. package/src/mru/mru.component.tsx +106 -0
  32. package/src/mru/mru.scss +28 -0
  33. package/src/routes.json +25 -1
  34. package/translations/am.json +36 -1
  35. package/translations/en.json +36 -1
  36. package/dist/40.js +0 -1
  37. package/dist/532.js.map +0 -1
  38. package/dist/593.js +0 -6
  39. package/dist/593.js.map +0 -1
  40. package/dist/77.js +0 -1
  41. package/dist/77.js.map +0 -1
  42. package/dist/972.js +0 -1
  43. package/dist/972.js.map +0 -1
  44. package/translations/sw.json +0 -19
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"pages":[],"extensions":[{"name":"triage-dashboard-link","component":"triageDashboardLink","slot":"homepage-dashboard-slot","order":0,"meta":{"name":"triage","slot":"triage-dashboard-slot","title":"triage"}},{"name":"triage-dashboard","component":"triageDashboard","slot":"triage-dashboard-slot"}],"workspaces":[{"name":"triage-workspace","component":"triageWorkspace","title":"Triage Form","canMaximize":true,"type":"clinical-form"},{"name":"patient-registration-workspace","component":"patientRegistrationWorkspace","title":"Patient Registration","canMaximize":true,"type":"registration-form"}],"modals":[],"workspaceGroups":[],"version":"5.4.2-pre.4"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"pages":[],"extensions":[{"name":"triage-dashboard-link","component":"triageDashboardLink","slot":"homepage-dashboard-slot","order":0,"meta":{"name":"triage","slot":"triage-dashboard-slot","title":"triage"}},{"name":"triage-dashboard","component":"triageDashboard","slot":"triage-dashboard-slot"},{"name":"ewf-mru-dashboard-link","component":"mruLeftPanelLink","slot":"homepage-dashboard-slot","meta":{"name":"mru","title":"MRU","path":"mru","slot":"mru-dashboard-slot"}},{"name":"ewf-mru-dashboard","component":"mruDashboard","slot":"mru-dashboard-slot","online":true,"offline":false}],"workspaces":[{"name":"triage-workspace","component":"triageWorkspace","title":"Triage Form","canMaximize":true,"type":"clinical-form"},{"name":"patient-registration-workspace","component":"patientRegistrationWorkspace","title":"Patient Registration","canMaximize":true,"type":"registration-form"},{"name":"billing-information-workspace","component":"billingInformationWorkspace","title":"Billing Information Workspace","type":"clinical-form"}],"modals":[],"workspaceGroups":[],"version":"5.4.2-pre.6"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palladium-ethiopia/esm-clinical-workflow-app",
3
- "version": "5.4.2-pre.4",
3
+ "version": "5.4.2-pre.6",
4
4
  "description": "Express workflow app for OpenMRS 3",
5
5
  "keywords": [
6
6
  "openmrs",
@@ -7,8 +7,24 @@ export const configSchema = {
7
7
  _elements: { _type: Type.String },
8
8
  _default: ['Central Triage', 'Pediatrics Triage', 'Emergency Triage'],
9
9
  },
10
+ billingVisitAttributeTypes: {
11
+ _type: Type.Object,
12
+ _description: 'Visit attribute type UUIDs for billing information',
13
+ _default: {
14
+ paymentMethod: 'e6cb0c3b-04b0-4117-9bc6-ce24adbda802',
15
+ creditType: '5cd1eb62-e006-4146-bd22-80bc4d5bd2f7',
16
+ creditTypeDetails: 'd824aa96-d2c7-4a52-aa8d-03f60a516083',
17
+ freeType: '7523ecfe-b8f1-4e7f-80a7-1a495b15ace4',
18
+ },
19
+ },
10
20
  };
11
21
 
12
22
  export type ClinicalWorkflowConfig = {
13
23
  triageServices: Array<string>;
24
+ billingVisitAttributeTypes: {
25
+ paymentMethod: string;
26
+ creditType: string;
27
+ creditTypeDetails: string;
28
+ freeType: string;
29
+ };
14
30
  };
package/src/index.ts CHANGED
@@ -2,6 +2,9 @@ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmr
2
2
  import { createDashboardLink } from './createDashboardLink';
3
3
  import { configSchema } from './config-schema';
4
4
  import { dashboardMeta } from './dashboard.meta';
5
+ import MRUDashboard from './mru/dashboard.component';
6
+ import { spaBasePath } from './constants';
7
+ import BillingInformationWorkspace from './mru/billing-information/billing-information.workspace';
5
8
 
6
9
  const moduleName = '@ethiopia/esm-clinical-workflow-app';
7
10
 
@@ -24,3 +27,15 @@ export const patientRegistrationWorkspace = getAsyncLifecycle(
24
27
  () => import('./patient-registration/patient.registration.workspace'),
25
28
  options,
26
29
  );
30
+
31
+ export const mruDashboard = getSyncLifecycle(MRUDashboard, options);
32
+ export const mruLeftPanelLink = getSyncLifecycle(
33
+ createDashboardLink({
34
+ path: 'mru',
35
+ title: 'MRU',
36
+ basePath: spaBasePath,
37
+ }),
38
+ options,
39
+ );
40
+
41
+ export const billingInformationWorkspace = getSyncLifecycle(BillingInformationWorkspace, options);
@@ -0,0 +1,137 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { z } from 'zod';
3
+ // Create the billing form schema factory with conditional validation based on skip logic
4
+ export const createBillingFormSchema = (t: (key: string, defaultValue?: string) => string) => {
5
+ return z
6
+ .object({
7
+ billingType: z.enum(['credit', 'free', 'cash']).optional(),
8
+ creditType: z.string().optional(),
9
+ name: z.string().optional(),
10
+ code: z.string().optional(),
11
+ id: z.string().optional(),
12
+ expiryDate: z.string().optional(),
13
+ zone: z.string().optional(),
14
+ freeType: z.string().optional(),
15
+ })
16
+ .superRefine((data, ctx) => {
17
+ // Billing type is required on submit
18
+ if (!data.billingType) {
19
+ ctx.addIssue({
20
+ code: z.ZodIssueCode.custom,
21
+ message: t('billingTypeRequired', 'Billing type is required'),
22
+ path: ['billingType'],
23
+ });
24
+ return;
25
+ }
26
+
27
+ // Credit billing type validation
28
+ if (data.billingType === 'credit') {
29
+ // Credit type is required
30
+ if (!data.creditType || data.creditType.trim() === '') {
31
+ ctx.addIssue({
32
+ code: z.ZodIssueCode.custom,
33
+ message: t('creditTypeRequired', 'Credit type is required'),
34
+ path: ['creditType'],
35
+ });
36
+ }
37
+
38
+ // If creditType is insurance, require additional fields
39
+ if (data.creditType === 'insurance') {
40
+ if (!data.id || data.id.trim() === '') {
41
+ ctx.addIssue({
42
+ code: z.ZodIssueCode.custom,
43
+ message: t('idRequired', 'ID is required'),
44
+ path: ['id'],
45
+ });
46
+ }
47
+ }
48
+ }
49
+
50
+ // Free billing type validation
51
+ if (data.billingType === 'free') {
52
+ if (!data.freeType || data.freeType.trim() === '') {
53
+ ctx.addIssue({
54
+ code: z.ZodIssueCode.custom,
55
+ message: t('freeTypeRequired', 'Free type is required'),
56
+ path: ['freeType'],
57
+ });
58
+ }
59
+ }
60
+ });
61
+ };
62
+
63
+ export type BillingFormData = z.infer<ReturnType<typeof createBillingFormSchema>>;
64
+
65
+ const transformFormObjectToVisitAttributes = (
66
+ formObject: Record<string, any>,
67
+ visitAttributeTypeUuidsMap: Record<string, string>,
68
+ ) => {
69
+ return Object.entries(formObject)
70
+ .map(([key, value]) => ({
71
+ attributeType: visitAttributeTypeUuidsMap[key],
72
+ value: value,
73
+ }))
74
+ .filter((item) => {
75
+ // Filter out undefined, null, or empty string values
76
+ if (item.value === undefined || item.value === null || item.value === '') {
77
+ return false;
78
+ }
79
+ // Filter out empty creditTypeDetails object
80
+ if (item.value && typeof item.value === 'object' && !Array.isArray(item.value)) {
81
+ const creditDetails = item.value as Record<string, any>;
82
+ const hasAnyValue = Object.values(creditDetails).some((val) => val !== undefined && val !== null && val !== '');
83
+ if (!hasAnyValue) {
84
+ return false;
85
+ }
86
+ }
87
+ return true;
88
+ })
89
+ .map((item) => ({
90
+ attributeType: item.attributeType,
91
+ value: item.value && typeof item.value === 'object' ? JSON.stringify(item.value) : item.value,
92
+ }));
93
+ };
94
+
95
+ export const createBillingInformationVisitAttribute = (
96
+ billingFormData: BillingFormData,
97
+ visitAttributeTypeUuidsMap: {
98
+ paymentMethod: string;
99
+ creditType: string;
100
+ creditTypeDetails: string;
101
+ freeType: string;
102
+ },
103
+ ) => {
104
+ const { billingType, creditType, name, code, id, expiryDate, zone, freeType } = billingFormData;
105
+
106
+ const formObject = {
107
+ paymentMethod: billingType,
108
+ creditType: creditType,
109
+ creditTypeDetails: {
110
+ name: name,
111
+ code: code,
112
+ id: id,
113
+ expiryDate: expiryDate,
114
+ zone: zone,
115
+ },
116
+ freeType: freeType,
117
+ };
118
+
119
+ const visitAttributePayload = transformFormObjectToVisitAttributes(formObject, visitAttributeTypeUuidsMap);
120
+
121
+ return visitAttributePayload;
122
+ };
123
+
124
+ export const updateVisitWithBillingInformation = (
125
+ visitAttributePayload: Array<{ attributeType: string; value: string }>,
126
+ visitUuid: string,
127
+ ) => {
128
+ return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}`, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-type': 'application/json',
132
+ },
133
+ body: {
134
+ attributes: visitAttributePayload,
135
+ },
136
+ });
137
+ };
@@ -0,0 +1,55 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/type';
3
+ @use '@carbon/layout';
4
+ @use '@carbon/colors';
5
+
6
+ .sectionTitle {
7
+ @include type.type-style('label-01');
8
+ color: colors.$gray-70;
9
+ margin-bottom: spacing.$spacing-03;
10
+ }
11
+
12
+ .buttonSet {
13
+ width: 100%;
14
+ padding-top: spacing.$spacing-05;
15
+ background-color: colors.$white-0;
16
+ justify-content: space-between;
17
+ bottom: 0;
18
+ position: absolute;
19
+ & > button {
20
+ max-width: 50% !important;
21
+ width: 50%;
22
+ }
23
+ }
24
+
25
+ .billingInformationContainer {
26
+ margin: spacing.$spacing-05;
27
+ height: 100%;
28
+ overflow-y: auto;
29
+ }
30
+
31
+ .creditDetailsContainer {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: spacing.$spacing-05;
35
+ margin-top: spacing.$spacing-03;
36
+ min-height: 45rem;
37
+ border: 1px solid colors.$gray-20;
38
+ padding: spacing.$spacing-03;
39
+ }
40
+
41
+ .freeDetailsContainer {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: spacing.$spacing-05;
45
+ margin-top: spacing.$spacing-03;
46
+ min-height: 20rem;
47
+ border: 1px solid colors.$gray-20;
48
+ padding: spacing.$spacing-03;
49
+ }
50
+
51
+ .creditTypeDetailsContainer {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: spacing.$spacing-03;
55
+ }
@@ -0,0 +1,369 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import {
3
+ Button,
4
+ ButtonSet,
5
+ InlineLoading,
6
+ ContentSwitcher,
7
+ Dropdown,
8
+ Switch,
9
+ TextInput,
10
+ Form,
11
+ FormGroup,
12
+ } from '@carbon/react';
13
+ import {
14
+ DefaultWorkspaceProps,
15
+ ExtensionSlot,
16
+ usePatient,
17
+ OpenmrsDatePicker,
18
+ useConfig,
19
+ useVisit,
20
+ showSnackbar,
21
+ } from '@openmrs/esm-framework';
22
+ import { useTranslation } from 'react-i18next';
23
+ import { useForm, Controller, Control, FieldErrors, useWatch } from 'react-hook-form';
24
+ import { zodResolver } from '@hookform/resolvers/zod';
25
+
26
+ import {
27
+ type BillingFormData,
28
+ createBillingFormSchema,
29
+ createBillingInformationVisitAttribute,
30
+ updateVisitWithBillingInformation,
31
+ } from './billing-information.resource';
32
+ import type { ClinicalWorkflowConfig } from '../../config-schema';
33
+ import styles from './billing-information.scss';
34
+
35
+ type BillingInformationWorkspaceProps = DefaultWorkspaceProps & {
36
+ patientUuid: string;
37
+ };
38
+
39
+ const BillingInformationWorkspace: React.FC<BillingInformationWorkspaceProps> = ({
40
+ patientUuid,
41
+ closeWorkspace,
42
+ promptBeforeClosing,
43
+ closeWorkspaceWithSavedChanges,
44
+ }) => {
45
+ const { t } = useTranslation();
46
+ const { patient, isLoading } = usePatient(patientUuid);
47
+ const { activeVisit, mutate: mutateVisit } = useVisit(patientUuid);
48
+ const { billingVisitAttributeTypes } = useConfig<ClinicalWorkflowConfig>();
49
+ const billingFormSchema = useMemo(() => createBillingFormSchema(t), [t]);
50
+
51
+ const {
52
+ control,
53
+ handleSubmit,
54
+ watch,
55
+ setValue,
56
+ formState: { errors, isDirty },
57
+ } = useForm<BillingFormData>({
58
+ resolver: zodResolver(billingFormSchema),
59
+ mode: 'onTouched',
60
+ defaultValues: {
61
+ billingType: undefined,
62
+ creditType: '',
63
+ name: '',
64
+ code: '',
65
+ id: '',
66
+ expiryDate: '',
67
+ zone: '',
68
+ freeType: '',
69
+ },
70
+ });
71
+
72
+ const billingType = watch('billingType');
73
+
74
+ const onSubmit = async (data: BillingFormData) => {
75
+ try {
76
+ const visitAttributePayload = createBillingInformationVisitAttribute(data, billingVisitAttributeTypes);
77
+ const response = await updateVisitWithBillingInformation(visitAttributePayload, activeVisit?.uuid);
78
+ if (response.status === 200) {
79
+ showSnackbar({
80
+ title: t('updateVisitWithBillingInfo', 'Update Visit With Billing Information'),
81
+ subtitle: t('updateVisitWithBillingInfoSuccess', 'Update Visit With Billing Information Success'),
82
+ kind: 'success',
83
+ isLowContrast: true,
84
+ timeoutInMs: 5000,
85
+ });
86
+ mutateVisit();
87
+ closeWorkspaceWithSavedChanges();
88
+ }
89
+ } catch (error) {
90
+ showSnackbar({
91
+ title: t('error', 'Error'),
92
+ subtitle: t('errorUpdatingBillingInformation', 'Error updating billing information, {{error}}', {
93
+ error: error.message,
94
+ }),
95
+ kind: 'error',
96
+ isLowContrast: true,
97
+ timeoutInMs: 5000,
98
+ });
99
+ }
100
+ };
101
+
102
+ const handleBillingTypeChange = (type: 'credit' | 'free' | 'cash') => {
103
+ // Clear form fields to avoid carrying over values from previous billing type
104
+ setValue('billingType', type, { shouldDirty: true });
105
+ setValue('creditType', '', { shouldDirty: false });
106
+ setValue('name', '', { shouldDirty: false });
107
+ setValue('code', '', { shouldDirty: false });
108
+ setValue('id', '', { shouldDirty: false });
109
+ setValue('expiryDate', '', { shouldDirty: false });
110
+ setValue('zone', '', { shouldDirty: false });
111
+ setValue('freeType', '', { shouldDirty: false });
112
+ };
113
+
114
+ const getSelectedIndex = () => {
115
+ if (billingType === 'cash') {
116
+ return 0;
117
+ }
118
+ if (billingType === 'free') {
119
+ return 1;
120
+ }
121
+ if (billingType === 'credit') {
122
+ return 2;
123
+ }
124
+ return -1;
125
+ };
126
+
127
+ const handleContentSwitcherChange = ({ index }: { index: number }) => {
128
+ const types: ('cash' | 'free' | 'credit')[] = ['cash', 'free', 'credit'];
129
+ if (index >= 0 && index < types.length) {
130
+ handleBillingTypeChange(types[index]);
131
+ }
132
+ };
133
+
134
+ useEffect(() => {
135
+ promptBeforeClosing(() => isDirty);
136
+ }, [promptBeforeClosing, isDirty]);
137
+
138
+ if (isLoading) {
139
+ return <InlineLoading status="active" iconDescription="Loading" description="Loading billing information..." />;
140
+ }
141
+
142
+ return (
143
+ <>
144
+ {patient && (
145
+ <ExtensionSlot
146
+ name="patient-header-slot"
147
+ state={{
148
+ patient,
149
+ patientUuid: patientUuid,
150
+ hideActionsOverflow: true,
151
+ }}
152
+ />
153
+ )}
154
+ <Form onSubmit={handleSubmit(onSubmit)}>
155
+ <div className={styles.billingInformationContainer}>
156
+ <p className={styles.sectionTitle}>{t('paymentMethods', 'Payment Methods')}</p>
157
+ <ContentSwitcher onChange={handleContentSwitcherChange} selectedIndex={getSelectedIndex()} size="md">
158
+ <Switch name="cash" text={t('cash', 'Cash')} />
159
+ <Switch name="free" text={t('free', 'Free')} />
160
+ <Switch name="credit" text={t('credit', 'Credit')} />
161
+ </ContentSwitcher>
162
+
163
+ {billingType === 'credit' && <CreditDetails control={control} errors={errors} t={t} />}
164
+ {billingType === 'free' && <FreeDetails control={control} errors={errors} t={t} />}
165
+ </div>
166
+ <ButtonSet className={styles.buttonSet}>
167
+ <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
168
+ {t('discard', 'Discard')}
169
+ </Button>
170
+ <Button disabled={!isDirty} className={styles.button} kind="primary" type="submit">
171
+ {t('saveAndClose', 'Save & Close')}
172
+ </Button>
173
+ </ButtonSet>
174
+ </Form>
175
+ </>
176
+ );
177
+ };
178
+
179
+ export default BillingInformationWorkspace;
180
+
181
+ type CreditDetailsProps = {
182
+ control: Control<BillingFormData>;
183
+ errors: FieldErrors<BillingFormData>;
184
+ t: (key: string, defaultValue?: string) => string;
185
+ };
186
+
187
+ const CreditDetails: React.FC<CreditDetailsProps> = ({ control, errors, t }) => {
188
+ const creditType = useWatch({
189
+ control,
190
+ name: 'creditType',
191
+ });
192
+
193
+ const creditTypeOptions = [
194
+ {
195
+ id: 'chbi',
196
+ text: t('chbi', 'CHBI'),
197
+ },
198
+ {
199
+ id: 'shi',
200
+ text: t('shi', 'SHI'),
201
+ },
202
+ {
203
+ id: 'creditCompanies',
204
+ text: t('creditCompanies', 'Credit Companies'),
205
+ },
206
+ {
207
+ id: 'insurance',
208
+ text: t('insurance', 'Insurance'),
209
+ },
210
+ ];
211
+
212
+ const showCreditCompanyFields = creditType === 'creditCompanies' || creditType === 'insurance';
213
+
214
+ return (
215
+ <FormGroup className={styles.creditDetailsContainer} legendText={t('creditDetails', 'Credit Details')}>
216
+ <Controller
217
+ name="creditType"
218
+ control={control}
219
+ render={({ field: { onChange, value } }) => (
220
+ <Dropdown
221
+ id="credit-type"
222
+ invalid={!!errors.creditType}
223
+ invalidText={errors.creditType?.message || 'invalid selection'}
224
+ itemToString={(item) => item?.text ?? ''}
225
+ items={creditTypeOptions}
226
+ label="Credit"
227
+ titleText="Credit"
228
+ type="default"
229
+ selectedItem={creditTypeOptions.find((item) => item.id === value) || null}
230
+ onChange={({ selectedItem }) => onChange(selectedItem?.id || '')}
231
+ />
232
+ )}
233
+ />
234
+
235
+ {showCreditCompanyFields && (
236
+ <FormGroup
237
+ legendText={t('creditTypeDetails', 'Credit Type Details')}
238
+ className={styles.creditTypeDetailsContainer}>
239
+ <Controller
240
+ name="id"
241
+ control={control}
242
+ render={({ field: { onChange, value } }) => (
243
+ <TextInput
244
+ id="credit-id"
245
+ labelText={t('id', 'ID')}
246
+ value={value || ''}
247
+ onChange={(e) => onChange(e.target.value)}
248
+ placeholder={t('enterId', 'Enter ID')}
249
+ invalid={!!errors.id}
250
+ invalidText={errors.id?.message}
251
+ />
252
+ )}
253
+ />
254
+
255
+ <Controller
256
+ name="name"
257
+ control={control}
258
+ render={({ field: { onChange, value } }) => (
259
+ <TextInput
260
+ id="credit-name"
261
+ labelText={t('name', 'Name')}
262
+ value={value || ''}
263
+ onChange={(e) => onChange(e.target.value)}
264
+ placeholder={t('enterName', 'Enter name')}
265
+ invalid={!!errors.name}
266
+ invalidText={errors.name?.message}
267
+ />
268
+ )}
269
+ />
270
+
271
+ <Controller
272
+ name="code"
273
+ control={control}
274
+ render={({ field: { onChange, value } }) => (
275
+ <TextInput
276
+ id="credit-code"
277
+ labelText={t('code', 'Code')}
278
+ value={value || ''}
279
+ onChange={(e) => onChange(e.target.value)}
280
+ placeholder={t('enterCode', 'Enter code')}
281
+ invalid={!!errors.code}
282
+ invalidText={errors.code?.message}
283
+ />
284
+ )}
285
+ />
286
+
287
+ <Controller
288
+ name="expiryDate"
289
+ control={control}
290
+ render={({ field: { onChange, value } }) => (
291
+ <OpenmrsDatePicker
292
+ id="credit-expiry-date"
293
+ labelText={t('expiryDate', 'Expiry Date')}
294
+ minDate={new Date()}
295
+ value={value || ''}
296
+ onChange={(date) => {
297
+ const dateValue = typeof date === 'string' ? date : date.toISOString().split('T')[0];
298
+ onChange(dateValue);
299
+ }}
300
+ />
301
+ )}
302
+ />
303
+ <Controller
304
+ name="zone"
305
+ control={control}
306
+ render={({ field: { onChange, value } }) => (
307
+ <TextInput
308
+ id="credit-zone"
309
+ labelText={t('zone', 'Zone')}
310
+ value={value || ''}
311
+ onChange={(e) => onChange(e.target.value)}
312
+ placeholder={t('enterZone', 'Enter zone')}
313
+ invalid={!!errors.zone}
314
+ invalidText={errors.zone?.message}
315
+ />
316
+ )}
317
+ />
318
+ </FormGroup>
319
+ )}
320
+ </FormGroup>
321
+ );
322
+ };
323
+
324
+ type FreeDetailsProps = {
325
+ control: Control<BillingFormData>;
326
+ errors: FieldErrors<BillingFormData>;
327
+ t: (key: string, defaultValue?: string) => string;
328
+ };
329
+
330
+ const FreeDetails: React.FC<FreeDetailsProps> = ({ control, errors, t }) => {
331
+ const items = [
332
+ {
333
+ id: 'staff',
334
+ text: t('staff', 'Staff'),
335
+ },
336
+ {
337
+ id: '24Hours',
338
+ text: t('24Hours', '24 Hours'),
339
+ },
340
+ {
341
+ id: 'exempted',
342
+ text: t('exempted', 'Exempted'),
343
+ },
344
+ ];
345
+
346
+ return (
347
+ <FormGroup className={styles.freeDetailsContainer} legendText={t('freeDetails', 'Free Details')}>
348
+ <Controller
349
+ name="freeType"
350
+ control={control}
351
+ render={({ field: { onChange, value } }) => (
352
+ <Dropdown
353
+ id="free-type"
354
+ hideLabel={true}
355
+ invalid={!!errors.freeType}
356
+ invalidText={errors.freeType?.message || 'invalid selection'}
357
+ itemToString={(item) => item?.text ?? ''}
358
+ items={items}
359
+ label={t('selectOption', 'Select a free type')}
360
+ titleText={t('selectOption', 'Select a free type')}
361
+ type="default"
362
+ selectedItem={items.find((item) => item.id === value) || null}
363
+ onChange={({ selectedItem }) => onChange(selectedItem?.id || '')}
364
+ />
365
+ )}
366
+ />
367
+ </FormGroup>
368
+ );
369
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
+
4
+ import { spaBasePath } from '../constants';
5
+ import MRU from './mru.component';
6
+
7
+ const MRUDashboard: React.FC = () => {
8
+ return (
9
+ <BrowserRouter basename={`${spaBasePath}/mru`}>
10
+ <Routes>
11
+ <Route path="/" element={<MRU />} />
12
+ <Route path="/:patientUuid" element={<MRU />} />
13
+ </Routes>
14
+ </BrowserRouter>
15
+ );
16
+ };
17
+
18
+ export default MRUDashboard;