@palladium-ethiopia/esm-clinical-workflow-app 5.4.2-pre.20
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/.turbo/turbo-build.log +14 -0
- package/README.md +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/159.js +1 -0
- package/dist/159.js.map +1 -0
- package/dist/208.js +1 -0
- package/dist/208.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/363.js +1 -0
- package/dist/363.js.map +1 -0
- package/dist/410.js +1 -0
- package/dist/410.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/484.js +11 -0
- package/dist/484.js.map +1 -0
- package/dist/540.js +1 -0
- package/dist/540.js.map +1 -0
- package/dist/545.js +43 -0
- package/dist/545.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/677.js +1 -0
- package/dist/677.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/697.js +1 -0
- package/dist/697.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/789.js +1 -0
- package/dist/789.js.map +1 -0
- package/dist/ethiopia-esm-clinical-workflow-app.js +6 -0
- package/dist/ethiopia-esm-clinical-workflow-app.js.buildmanifest.json +579 -0
- package/dist/ethiopia-esm-clinical-workflow-app.js.map +1 -0
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +59 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +69 -0
- package/src/constants.ts +2 -0
- package/src/createDashboardLink.tsx +10 -0
- package/src/dashboard.meta.ts +6 -0
- package/src/declarations.d.ts +3 -0
- package/src/helper.ts +115 -0
- package/src/index.ts +51 -0
- package/src/mru/billing-information/billing-information.resource.ts +139 -0
- package/src/mru/billing-information/billing-information.scss +55 -0
- package/src/mru/billing-information/billing-information.workspace.tsx +371 -0
- package/src/mru/dashboard.component.tsx +18 -0
- package/src/mru/mru.component.tsx +106 -0
- package/src/mru/mru.scss +28 -0
- package/src/patient-registration/patient-registration.resource.tsx +129 -0
- package/src/patient-registration/patient.registration.workspace.scss +47 -0
- package/src/patient-registration/patient.registration.workspace.tsx +443 -0
- package/src/patient-registration/useGenerateIdentifier.ts +26 -0
- package/src/patient-scoreboard/appointment-cards/checked-in-appointments.card.tsx +18 -0
- package/src/patient-scoreboard/appointment-cards/not-arrived-appointments.card.tsx +18 -0
- package/src/patient-scoreboard/appointment-cards/total-appointments.card.tsx +18 -0
- package/src/patient-scoreboard/hooks/useAppointmentList.ts +61 -0
- package/src/patient-scoreboard/hooks/useVisitList.ts +104 -0
- package/src/patient-scoreboard/metrics-card/metrics-card.component.scss +84 -0
- package/src/patient-scoreboard/metrics-card/metrics-card.component.tsx +40 -0
- package/src/patient-scoreboard/patient-scoreboard.component.scss +47 -0
- package/src/patient-scoreboard/patient-scoreboard.component.tsx +70 -0
- package/src/patient-scoreboard/visit-cards/active-visits.card.tsx +18 -0
- package/src/patient-scoreboard/visit-cards/scheduled-visits.card.tsx +18 -0
- package/src/patient-scoreboard/visit-cards/total-visits.card.tsx +18 -0
- package/src/patient-scoreboard/visits-table/visits-table.component.scss +31 -0
- package/src/patient-scoreboard/visits-table/visits-table.component.tsx +181 -0
- package/src/root.component.tsx +20 -0
- package/src/root.scss +10 -0
- package/src/routes.json +108 -0
- package/src/triage/patient-banner.component.tsx +59 -0
- package/src/triage/patient-banner.scss +14 -0
- package/src/triage/triage-dashboard.component.tsx +116 -0
- package/src/triage/triage-dashboard.scss +107 -0
- package/src/triage/triage.resource.tsx +44 -0
- package/src/triage/useStartVisitAndLaunchTriageForm.ts +156 -0
- package/src/types/index.ts +0 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ButtonSet, Checkbox, Dropdown, FormGroup, InlineLoading, NumberInput, TextInput } from '@carbon/react';
|
|
4
|
+
import {
|
|
5
|
+
OpenmrsDatePicker,
|
|
6
|
+
showSnackbar,
|
|
7
|
+
generateOfflineUuid,
|
|
8
|
+
useSession,
|
|
9
|
+
useConfig,
|
|
10
|
+
useLayoutType,
|
|
11
|
+
DefaultWorkspaceProps,
|
|
12
|
+
ResponsiveWrapper,
|
|
13
|
+
} from '@openmrs/esm-framework';
|
|
14
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
15
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
|
|
18
|
+
import type { ClinicalWorkflowConfig } from '../config-schema';
|
|
19
|
+
import { registerNewPatient, buildPatientRegistrationPayload } from './patient-registration.resource';
|
|
20
|
+
import { useStartVisitAndLaunchTriageForm } from '../triage/useStartVisitAndLaunchTriageForm';
|
|
21
|
+
import { getTriageFormForLocation } from '../triage/triage.resource';
|
|
22
|
+
import { useGenerateIdentifier } from './useGenerateIdentifier';
|
|
23
|
+
import styles from './patient.registration.workspace.scss';
|
|
24
|
+
import classNames from 'classnames';
|
|
25
|
+
|
|
26
|
+
const genderOptions = [
|
|
27
|
+
{
|
|
28
|
+
text: 'Male',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
text: 'Female',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const patientRegistrationSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
firstName: z.string().min(1, 'First name is required'),
|
|
38
|
+
middleName: z.string().min(1, 'Middle name is required'),
|
|
39
|
+
lastName: z.string().min(1, 'Last name is required'),
|
|
40
|
+
gender: z.enum(['Male', 'Female'], {
|
|
41
|
+
required_error: 'Gender is required',
|
|
42
|
+
}),
|
|
43
|
+
ageYears: z
|
|
44
|
+
.union([z.number().min(0).max(150), z.null()])
|
|
45
|
+
.optional()
|
|
46
|
+
.nullable(),
|
|
47
|
+
ageMonths: z
|
|
48
|
+
.union([z.number().min(0).max(11), z.null()])
|
|
49
|
+
.optional()
|
|
50
|
+
.nullable(),
|
|
51
|
+
ageDays: z
|
|
52
|
+
.union([z.number().min(0).max(31), z.null()])
|
|
53
|
+
.optional()
|
|
54
|
+
.nullable(),
|
|
55
|
+
isEstimatedDOB: z.boolean().optional().default(true),
|
|
56
|
+
dateOfBirth: z
|
|
57
|
+
.date({
|
|
58
|
+
required_error: 'Date of birth is required',
|
|
59
|
+
})
|
|
60
|
+
.refine((date) => date <= new Date(), {
|
|
61
|
+
message: 'Date of birth cannot be in the future',
|
|
62
|
+
})
|
|
63
|
+
.optional()
|
|
64
|
+
.nullable(),
|
|
65
|
+
isMedicoLegalCase: z.boolean().optional().default(false),
|
|
66
|
+
})
|
|
67
|
+
.refine(
|
|
68
|
+
(data) => {
|
|
69
|
+
const hasDateOfBirth = !!data.dateOfBirth;
|
|
70
|
+
const hasAgeFields =
|
|
71
|
+
(data.ageYears !== undefined && data.ageYears !== null && data.ageYears >= 0) ||
|
|
72
|
+
(data.ageMonths !== undefined && data.ageMonths !== null && data.ageMonths >= 0) ||
|
|
73
|
+
(data.ageDays !== undefined && data.ageDays !== null && data.ageDays >= 0);
|
|
74
|
+
return hasDateOfBirth || hasAgeFields;
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
message: 'Please provide either date of birth or age information',
|
|
78
|
+
path: ['dateOfBirth'],
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export type PatientRegistrationFormData = z.infer<typeof patientRegistrationSchema>;
|
|
83
|
+
|
|
84
|
+
const PatientRegistration: React.FC<DefaultWorkspaceProps> = ({
|
|
85
|
+
closeWorkspace,
|
|
86
|
+
closeWorkspaceWithSavedChanges,
|
|
87
|
+
promptBeforeClosing,
|
|
88
|
+
}) => {
|
|
89
|
+
const { t } = useTranslation();
|
|
90
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
91
|
+
const { handleStartVisitAndLaunchTriageForm } = useStartVisitAndLaunchTriageForm();
|
|
92
|
+
const {
|
|
93
|
+
visitTypeUuid,
|
|
94
|
+
identifierSourceUuid,
|
|
95
|
+
defaultIdentifierTypeUuid,
|
|
96
|
+
triageLocationForms,
|
|
97
|
+
medicoLegalCasesAttributeTypeUuid,
|
|
98
|
+
} = useConfig<ClinicalWorkflowConfig>();
|
|
99
|
+
const { sessionLocation } = useSession();
|
|
100
|
+
const { identifier } = useGenerateIdentifier(identifierSourceUuid);
|
|
101
|
+
|
|
102
|
+
const triageFormConfig = getTriageFormForLocation(sessionLocation?.uuid, triageLocationForms);
|
|
103
|
+
|
|
104
|
+
const {
|
|
105
|
+
control,
|
|
106
|
+
handleSubmit,
|
|
107
|
+
formState: { errors, isSubmitting, isDirty, isSubmitted },
|
|
108
|
+
} = useForm<PatientRegistrationFormData>({
|
|
109
|
+
resolver: zodResolver(patientRegistrationSchema),
|
|
110
|
+
mode: 'onSubmit',
|
|
111
|
+
reValidateMode: 'onSubmit',
|
|
112
|
+
shouldFocusError: false,
|
|
113
|
+
shouldUnregister: false,
|
|
114
|
+
defaultValues: {
|
|
115
|
+
firstName: '',
|
|
116
|
+
middleName: '',
|
|
117
|
+
lastName: '',
|
|
118
|
+
gender: null,
|
|
119
|
+
ageYears: null,
|
|
120
|
+
ageMonths: null,
|
|
121
|
+
ageDays: null,
|
|
122
|
+
isEstimatedDOB: true,
|
|
123
|
+
dateOfBirth: null,
|
|
124
|
+
isMedicoLegalCase: false,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
promptBeforeClosing(() => isDirty);
|
|
130
|
+
}, [promptBeforeClosing, isDirty]);
|
|
131
|
+
|
|
132
|
+
const onSubmit = async (data: PatientRegistrationFormData) => {
|
|
133
|
+
const uuid = generateOfflineUuid()?.replace('OFFLINE+', '');
|
|
134
|
+
try {
|
|
135
|
+
const registrationPayload = buildPatientRegistrationPayload(
|
|
136
|
+
data,
|
|
137
|
+
uuid,
|
|
138
|
+
identifier,
|
|
139
|
+
defaultIdentifierTypeUuid,
|
|
140
|
+
sessionLocation.uuid,
|
|
141
|
+
data.isMedicoLegalCase,
|
|
142
|
+
medicoLegalCasesAttributeTypeUuid,
|
|
143
|
+
triageFormConfig?.name,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const patient = await registerNewPatient(registrationPayload);
|
|
147
|
+
|
|
148
|
+
const patientData = patient?.data as any;
|
|
149
|
+
const patientUuid = patientData?.uuid || patientData?.id;
|
|
150
|
+
|
|
151
|
+
if (patientUuid) {
|
|
152
|
+
showSnackbar({
|
|
153
|
+
title: t('patientRegistrationSuccess', 'Patient registered successfully'),
|
|
154
|
+
kind: 'success',
|
|
155
|
+
isLowContrast: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (triageFormConfig) {
|
|
159
|
+
await handleStartVisitAndLaunchTriageForm(patientUuid, triageFormConfig.formUuid, triageFormConfig.name);
|
|
160
|
+
} else {
|
|
161
|
+
showSnackbar({
|
|
162
|
+
title: t('noTriageFormConfigured', 'No triage form configured'),
|
|
163
|
+
subtitle: t(
|
|
164
|
+
'noTriageFormConfiguredForLocation',
|
|
165
|
+
'No triage form is configured for the current location. Please configure a form for this location.',
|
|
166
|
+
),
|
|
167
|
+
kind: 'warning',
|
|
168
|
+
isLowContrast: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
closeWorkspaceWithSavedChanges();
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const errorMessage =
|
|
175
|
+
error instanceof Error ? error.message : t('patientRegistrationErrorSubtitle', 'Please try again.');
|
|
176
|
+
showSnackbar({
|
|
177
|
+
title: t('patientRegistrationError', 'Error registering patient'),
|
|
178
|
+
kind: 'error',
|
|
179
|
+
subtitle: errorMessage,
|
|
180
|
+
isLowContrast: true,
|
|
181
|
+
});
|
|
182
|
+
} finally {
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<form className={styles.form} onSubmit={handleSubmit(onSubmit)} noValidate>
|
|
188
|
+
<div className={styles.formContainer}>
|
|
189
|
+
<Controller
|
|
190
|
+
name="firstName"
|
|
191
|
+
control={control}
|
|
192
|
+
render={({ field: { onChange, value } }) => (
|
|
193
|
+
<ResponsiveWrapper>
|
|
194
|
+
<TextInput
|
|
195
|
+
id="first-name"
|
|
196
|
+
labelText={t('firstName', 'First Name')}
|
|
197
|
+
value={value || ''}
|
|
198
|
+
onChange={(e) => onChange(e.target.value)}
|
|
199
|
+
invalid={isSubmitted && !!errors.firstName}
|
|
200
|
+
invalidText={isSubmitted ? errors.firstName?.message : ''}
|
|
201
|
+
placeholder={t('enterFirstName', 'Enter Your First Name')}
|
|
202
|
+
size="md"
|
|
203
|
+
type="text"
|
|
204
|
+
disabled={isSubmitting}
|
|
205
|
+
/>
|
|
206
|
+
</ResponsiveWrapper>
|
|
207
|
+
)}
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<Controller
|
|
211
|
+
name="middleName"
|
|
212
|
+
control={control}
|
|
213
|
+
render={({ field: { onChange, value } }) => (
|
|
214
|
+
<ResponsiveWrapper>
|
|
215
|
+
<TextInput
|
|
216
|
+
id="middle-name"
|
|
217
|
+
labelText={t('middleName', 'Middle Name')}
|
|
218
|
+
value={value || ''}
|
|
219
|
+
onChange={(e) => onChange(e.target.value)}
|
|
220
|
+
invalid={isSubmitted && !!errors.middleName}
|
|
221
|
+
invalidText={isSubmitted ? errors.middleName?.message : ''}
|
|
222
|
+
placeholder={t('enterMiddleName', 'Enter Middle Name')}
|
|
223
|
+
size="md"
|
|
224
|
+
type="text"
|
|
225
|
+
disabled={isSubmitting}
|
|
226
|
+
/>
|
|
227
|
+
</ResponsiveWrapper>
|
|
228
|
+
)}
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
<Controller
|
|
232
|
+
name="lastName"
|
|
233
|
+
control={control}
|
|
234
|
+
render={({ field: { onChange, value } }) => (
|
|
235
|
+
<ResponsiveWrapper>
|
|
236
|
+
<TextInput
|
|
237
|
+
id="last-name"
|
|
238
|
+
labelText={t('lastName', 'Last Name')}
|
|
239
|
+
value={value || ''}
|
|
240
|
+
onChange={(e) => onChange(e.target.value)}
|
|
241
|
+
invalid={isSubmitted && !!errors.lastName}
|
|
242
|
+
invalidText={isSubmitted ? errors.lastName?.message : ''}
|
|
243
|
+
placeholder={t('enterLastName', 'Enter Last Name')}
|
|
244
|
+
size="md"
|
|
245
|
+
type="text"
|
|
246
|
+
disabled={isSubmitting}
|
|
247
|
+
/>
|
|
248
|
+
</ResponsiveWrapper>
|
|
249
|
+
)}
|
|
250
|
+
/>
|
|
251
|
+
|
|
252
|
+
<Controller
|
|
253
|
+
name="gender"
|
|
254
|
+
control={control}
|
|
255
|
+
render={({ field: { onChange, value } }) => (
|
|
256
|
+
<ResponsiveWrapper>
|
|
257
|
+
<Dropdown
|
|
258
|
+
id="gender"
|
|
259
|
+
invalid={isSubmitted && !!errors.gender}
|
|
260
|
+
invalidText={isSubmitted ? errors.gender?.message || t('invalidSelection', 'Invalid selection') : ''}
|
|
261
|
+
itemToString={(item) => (item ? item.text : '')}
|
|
262
|
+
items={genderOptions}
|
|
263
|
+
label={t('gender', 'Gender')}
|
|
264
|
+
titleText={t('selectGender', 'Select gender')}
|
|
265
|
+
type="default"
|
|
266
|
+
selectedItem={genderOptions.find((item) => item.text === value) || null}
|
|
267
|
+
onChange={({ selectedItem }) => onChange(selectedItem?.text)}
|
|
268
|
+
disabled={isSubmitting}
|
|
269
|
+
/>
|
|
270
|
+
</ResponsiveWrapper>
|
|
271
|
+
)}
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
<ResponsiveWrapper>
|
|
275
|
+
<FormGroup
|
|
276
|
+
legendText={t('age', 'Age')}
|
|
277
|
+
className={classNames(styles.ageFormGroup, {
|
|
278
|
+
[styles.ageFormGroupNotSubmitted]: !isSubmitted,
|
|
279
|
+
})}>
|
|
280
|
+
<Controller
|
|
281
|
+
name="ageYears"
|
|
282
|
+
control={control}
|
|
283
|
+
render={({ field: { onChange, value } }) => {
|
|
284
|
+
const invalidValue = isSubmitted && !!errors.ageYears;
|
|
285
|
+
const displayValue = value !== undefined && value !== null ? value : '';
|
|
286
|
+
const invalidTextValue = isSubmitted && errors.ageYears ? errors.ageYears.message : undefined;
|
|
287
|
+
const numberInputProps: any = {
|
|
288
|
+
id: 'age-years',
|
|
289
|
+
label: t('years', 'Years'),
|
|
290
|
+
value: displayValue === '' ? undefined : displayValue,
|
|
291
|
+
onChange: (e: any, { value: newValue }: any) => {
|
|
292
|
+
const numValue =
|
|
293
|
+
newValue === '' || newValue === null || newValue === undefined ? undefined : Number(newValue);
|
|
294
|
+
onChange(numValue);
|
|
295
|
+
},
|
|
296
|
+
invalid: invalidValue || false,
|
|
297
|
+
invalidText: invalidTextValue,
|
|
298
|
+
warn: false,
|
|
299
|
+
placeholder: t('enterYears', 'Enter years'),
|
|
300
|
+
size: 'md',
|
|
301
|
+
disabled: isSubmitting,
|
|
302
|
+
allowEmpty: true,
|
|
303
|
+
};
|
|
304
|
+
if (isSubmitted) {
|
|
305
|
+
numberInputProps.min = 0;
|
|
306
|
+
numberInputProps.max = 150;
|
|
307
|
+
}
|
|
308
|
+
return <NumberInput {...numberInputProps} />;
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
<Controller
|
|
312
|
+
name="ageMonths"
|
|
313
|
+
control={control}
|
|
314
|
+
render={({ field: { onChange, value } }) => {
|
|
315
|
+
const invalidValue = isSubmitted && !!errors.ageMonths;
|
|
316
|
+
const invalidTextValue = isSubmitted && errors.ageMonths ? errors.ageMonths.message : undefined;
|
|
317
|
+
const displayValue = value !== undefined && value !== null ? value : '';
|
|
318
|
+
const numberInputProps: any = {
|
|
319
|
+
id: 'age-months',
|
|
320
|
+
label: t('months', 'Months'),
|
|
321
|
+
value: displayValue === '' ? undefined : displayValue,
|
|
322
|
+
onChange: (e: any, { value: newValue }: any) => {
|
|
323
|
+
const numValue =
|
|
324
|
+
newValue === '' || newValue === null || newValue === undefined ? undefined : Number(newValue);
|
|
325
|
+
onChange(numValue);
|
|
326
|
+
},
|
|
327
|
+
invalid: invalidValue || false,
|
|
328
|
+
invalidText: invalidTextValue,
|
|
329
|
+
warn: false,
|
|
330
|
+
placeholder: t('enterMonths', 'Enter months'),
|
|
331
|
+
size: 'md',
|
|
332
|
+
disabled: isSubmitting,
|
|
333
|
+
allowEmpty: true,
|
|
334
|
+
};
|
|
335
|
+
if (isSubmitted) {
|
|
336
|
+
numberInputProps.min = 0;
|
|
337
|
+
numberInputProps.max = 11;
|
|
338
|
+
}
|
|
339
|
+
return <NumberInput {...numberInputProps} />;
|
|
340
|
+
}}
|
|
341
|
+
/>
|
|
342
|
+
<Controller
|
|
343
|
+
name="ageDays"
|
|
344
|
+
control={control}
|
|
345
|
+
render={({ field: { onChange, value } }) => {
|
|
346
|
+
const invalidValue = isSubmitted && !!errors.ageDays;
|
|
347
|
+
const invalidTextValue = isSubmitted && errors.ageDays ? errors.ageDays.message : undefined;
|
|
348
|
+
const displayValue = value !== undefined && value !== null ? value : '';
|
|
349
|
+
const numberInputProps: any = {
|
|
350
|
+
id: 'age-days',
|
|
351
|
+
label: t('days', 'Days'),
|
|
352
|
+
value: displayValue === '' ? undefined : displayValue,
|
|
353
|
+
onChange: (e: any, { value: newValue }: any) => {
|
|
354
|
+
const numValue =
|
|
355
|
+
newValue === '' || newValue === null || newValue === undefined ? undefined : Number(newValue);
|
|
356
|
+
onChange(numValue);
|
|
357
|
+
},
|
|
358
|
+
invalid: invalidValue || false,
|
|
359
|
+
invalidText: invalidTextValue,
|
|
360
|
+
warn: false,
|
|
361
|
+
placeholder: t('enterDays', 'Enter days'),
|
|
362
|
+
size: 'md',
|
|
363
|
+
disabled: isSubmitting,
|
|
364
|
+
allowEmpty: true,
|
|
365
|
+
};
|
|
366
|
+
if (isSubmitted) {
|
|
367
|
+
numberInputProps.min = 0;
|
|
368
|
+
numberInputProps.max = 31;
|
|
369
|
+
}
|
|
370
|
+
return <NumberInput {...numberInputProps} />;
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
</FormGroup>
|
|
374
|
+
</ResponsiveWrapper>
|
|
375
|
+
|
|
376
|
+
<Controller
|
|
377
|
+
name="isEstimatedDOB"
|
|
378
|
+
control={control}
|
|
379
|
+
render={({ field: { onChange, value } }) => (
|
|
380
|
+
<ResponsiveWrapper>
|
|
381
|
+
<Checkbox
|
|
382
|
+
id="estimated-dob"
|
|
383
|
+
labelText={t('estimated', 'Estimated')}
|
|
384
|
+
checked={value || false}
|
|
385
|
+
onChange={(event, { checked }) => onChange(checked)}
|
|
386
|
+
disabled={isSubmitting}
|
|
387
|
+
/>
|
|
388
|
+
</ResponsiveWrapper>
|
|
389
|
+
)}
|
|
390
|
+
/>
|
|
391
|
+
|
|
392
|
+
<Controller
|
|
393
|
+
name="dateOfBirth"
|
|
394
|
+
control={control}
|
|
395
|
+
render={({ field: { onChange, value } }) => (
|
|
396
|
+
<ResponsiveWrapper>
|
|
397
|
+
<OpenmrsDatePicker
|
|
398
|
+
labelText={t('selectDOB', 'Select Date of Birth')}
|
|
399
|
+
maxDate={new Date()}
|
|
400
|
+
value={value}
|
|
401
|
+
invalid={isSubmitted && !!errors.dateOfBirth}
|
|
402
|
+
invalidText={isSubmitted && errors.dateOfBirth ? errors.dateOfBirth.message : ''}
|
|
403
|
+
onChange={(date) => onChange(date)}
|
|
404
|
+
isDisabled={isSubmitting}
|
|
405
|
+
/>
|
|
406
|
+
</ResponsiveWrapper>
|
|
407
|
+
)}
|
|
408
|
+
/>
|
|
409
|
+
|
|
410
|
+
<Controller
|
|
411
|
+
name="isMedicoLegalCase"
|
|
412
|
+
control={control}
|
|
413
|
+
render={({ field: { onChange, value } }) => (
|
|
414
|
+
<ResponsiveWrapper>
|
|
415
|
+
<Checkbox
|
|
416
|
+
id="medico-legal-case"
|
|
417
|
+
labelText={t('medicoLegalCases', 'Medico Legal Cases')}
|
|
418
|
+
checked={value || false}
|
|
419
|
+
onChange={(event, { checked }) => onChange(checked)}
|
|
420
|
+
disabled={isSubmitting}
|
|
421
|
+
/>
|
|
422
|
+
</ResponsiveWrapper>
|
|
423
|
+
)}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
|
|
428
|
+
<Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
|
|
429
|
+
{t('cancel', 'Cancel')}
|
|
430
|
+
</Button>
|
|
431
|
+
<Button className={styles.button} disabled={isSubmitting || !isDirty} kind="primary" type="submit">
|
|
432
|
+
{isSubmitting ? (
|
|
433
|
+
<InlineLoading className={styles.spinner} description={t('saving', 'Saving') + '...'} />
|
|
434
|
+
) : (
|
|
435
|
+
<span>{t('saveAndClose', 'Save & close')}</span>
|
|
436
|
+
)}
|
|
437
|
+
</Button>
|
|
438
|
+
</ButtonSet>
|
|
439
|
+
</form>
|
|
440
|
+
);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export default PatientRegistration;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import { generateIdentifier } from './patient-registration.resource';
|
|
3
|
+
|
|
4
|
+
interface UseGenerateIdentifierReturn {
|
|
5
|
+
identifierData: any;
|
|
6
|
+
identifier: string | undefined;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useGenerateIdentifier = (identifierSourceUuid: string | undefined): UseGenerateIdentifierReturn => {
|
|
12
|
+
const {
|
|
13
|
+
data: identifierData,
|
|
14
|
+
error,
|
|
15
|
+
isLoading,
|
|
16
|
+
} = useSWR(identifierSourceUuid ? 'generateIdentifier' : null, () => generateIdentifier(identifierSourceUuid!));
|
|
17
|
+
|
|
18
|
+
const identifier = identifierData?.data ? (identifierData.data as any)?.identifier : undefined;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
identifierData,
|
|
22
|
+
identifier,
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
import MetricsCard from '../metrics-card/metrics-card.component';
|
|
5
|
+
import { useActiveVisits } from '../hooks/useVisitList';
|
|
6
|
+
|
|
7
|
+
export default function ActiveVisitsCard() {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const { count, isLoading } = useActiveVisits();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<MetricsCard
|
|
13
|
+
headerLabel={t('activeVisits', 'Active Visits')}
|
|
14
|
+
label={t('visits', 'Visits')}
|
|
15
|
+
value={isLoading ? '...' : count}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
import MetricsCard from '../metrics-card/metrics-card.component';
|
|
5
|
+
import { useScheduledVisits } from '../hooks/useVisitList';
|
|
6
|
+
|
|
7
|
+
export default function ScheduledVisitsCard() {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const { count, isLoading } = useScheduledVisits();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<MetricsCard
|
|
13
|
+
headerLabel={t('scheduledVisits', 'Scheduled Visits')}
|
|
14
|
+
label={t('visits', 'Visits')}
|
|
15
|
+
value={isLoading ? '...' : count}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
import MetricsCard from '../metrics-card/metrics-card.component';
|
|
5
|
+
import { useTotalVisits } from '../hooks/useVisitList';
|
|
6
|
+
|
|
7
|
+
export default function TotalVisitsCard() {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const { count, isLoading } = useTotalVisits();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<MetricsCard
|
|
13
|
+
headerLabel={t('totalVisits', 'Total Visits')}
|
|
14
|
+
label={t('visits', 'Visits')}
|
|
15
|
+
value={isLoading ? '...' : count}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
|
|
4
|
+
|
|
5
|
+
export interface Appointment {
|
|
6
|
+
uuid: string;
|
|
7
|
+
appointmentNumber: string;
|
|
8
|
+
patient: {
|
|
9
|
+
uuid: string;
|
|
10
|
+
name: string;
|
|
11
|
+
identifier: string;
|
|
12
|
+
};
|
|
13
|
+
service: {
|
|
14
|
+
uuid: string;
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
startDateTime: string;
|
|
18
|
+
endDateTime: string;
|
|
19
|
+
appointmentKind: string;
|
|
20
|
+
status: string;
|
|
21
|
+
comments?: string;
|
|
22
|
+
location?: {
|
|
23
|
+
uuid: string;
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
provider?: {
|
|
27
|
+
uuid: string;
|
|
28
|
+
display?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const useAppointmentList = (appointmentStatus: string, date?: string) => {
|
|
34
|
+
const selectedDate = date ? date : dayjs().format('YYYY-MM-DD');
|
|
35
|
+
const startDate = dayjs(selectedDate).startOf('day').format('YYYY-MM-DDTHH:mm:ss.SSSZZ');
|
|
36
|
+
const endDate = dayjs(selectedDate).endOf('day').format('YYYY-MM-DDTHH:mm:ss.SSSZZ');
|
|
37
|
+
const searchUrl = `${restBaseUrl}/appointments/search`;
|
|
38
|
+
|
|
39
|
+
const fetcher = async ([url, startDate, endDate, status]: [string, string, string, string]) => {
|
|
40
|
+
const response = await openmrsFetch<Array<Appointment>>(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
body: {
|
|
46
|
+
startDate: startDate,
|
|
47
|
+
endDate: endDate,
|
|
48
|
+
status: status,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return response.data;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const { data, error, isLoading, mutate } = useSWR<Array<Appointment>, Error>(
|
|
55
|
+
[searchUrl, startDate, endDate, appointmentStatus],
|
|
56
|
+
fetcher,
|
|
57
|
+
{ errorRetryCount: 2 },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { appointmentList: data ?? [], isLoading, error, mutate };
|
|
61
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { openmrsFetch, restBaseUrl, useSession, type Visit } from '@openmrs/esm-framework';
|
|
4
|
+
|
|
5
|
+
export interface VisitResponse {
|
|
6
|
+
results: Array<Visit>;
|
|
7
|
+
totalCount?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useActiveVisits = () => {
|
|
11
|
+
const session = useSession();
|
|
12
|
+
const sessionLocation = session?.sessionLocation?.uuid;
|
|
13
|
+
const customRepresentation =
|
|
14
|
+
'custom:(uuid,patient:(uuid,identifiers:(identifier,uuid),person:(age,display,gender,uuid)),visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,stopDatetime)';
|
|
15
|
+
|
|
16
|
+
const getUrl = () => {
|
|
17
|
+
if (!sessionLocation) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
let url = `${restBaseUrl}/visit?v=${customRepresentation}&`;
|
|
21
|
+
let urlSearchParams = new URLSearchParams();
|
|
22
|
+
|
|
23
|
+
urlSearchParams.append('includeParentLocations', 'true');
|
|
24
|
+
urlSearchParams.append('includeInactive', 'false');
|
|
25
|
+
urlSearchParams.append('totalCount', 'true');
|
|
26
|
+
urlSearchParams.append('location', `${sessionLocation}`);
|
|
27
|
+
|
|
28
|
+
return url + urlSearchParams.toString();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const { data, error, isLoading } = useSWR<{ data: VisitResponse }>(getUrl, openmrsFetch);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
visits: data?.data?.results ?? [],
|
|
35
|
+
count: data?.data?.totalCount ?? 0,
|
|
36
|
+
error,
|
|
37
|
+
isLoading,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const useScheduledVisits = () => {
|
|
42
|
+
const session = useSession();
|
|
43
|
+
const sessionLocation = session?.sessionLocation?.uuid;
|
|
44
|
+
const startDate = dayjs().format('YYYY-MM-DD');
|
|
45
|
+
const customRepresentation =
|
|
46
|
+
'custom:(uuid,patient:(uuid,identifiers:(identifier,uuid),person:(age,display,gender,uuid)),visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,stopDatetime)';
|
|
47
|
+
|
|
48
|
+
const getUrl = () => {
|
|
49
|
+
if (!sessionLocation) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
let url = `${restBaseUrl}/visit?v=${customRepresentation}&`;
|
|
53
|
+
let urlSearchParams = new URLSearchParams();
|
|
54
|
+
|
|
55
|
+
urlSearchParams.append('includeParentLocations', 'true');
|
|
56
|
+
urlSearchParams.append('includeInactive', 'true');
|
|
57
|
+
urlSearchParams.append('fromStartDate', startDate);
|
|
58
|
+
urlSearchParams.append('location', `${sessionLocation}`);
|
|
59
|
+
|
|
60
|
+
return url + urlSearchParams.toString();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const { data, error, isLoading } = useSWR<{ data: VisitResponse }>(getUrl, openmrsFetch);
|
|
64
|
+
|
|
65
|
+
// Filter visits that started today but are now inactive (stopped)
|
|
66
|
+
const scheduledVisits =
|
|
67
|
+
data?.data?.results?.filter((visit) => {
|
|
68
|
+
const startedToday = dayjs(visit.startDatetime).isSame(dayjs(), 'day');
|
|
69
|
+
const isStopped = !!visit.stopDatetime;
|
|
70
|
+
return startedToday && isStopped;
|
|
71
|
+
}) ?? [];
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
visits: scheduledVisits,
|
|
75
|
+
count: scheduledVisits.length,
|
|
76
|
+
error,
|
|
77
|
+
isLoading,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const useTotalVisits = () => {
|
|
82
|
+
const session = useSession();
|
|
83
|
+
const sessionLocation = session?.sessionLocation?.uuid;
|
|
84
|
+
const startDate = dayjs().format('YYYY-MM-DD');
|
|
85
|
+
const customRepresentation =
|
|
86
|
+
'custom:(uuid,patient:(uuid,identifiers:(identifier,uuid),person:(age,display,gender,uuid)),visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,stopDatetime)';
|
|
87
|
+
|
|
88
|
+
const getUrl = () => {
|
|
89
|
+
if (!sessionLocation) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const visitsUrl = `${restBaseUrl}/visit?includeInactive=true&includeParentLocations=true&v=${customRepresentation}&fromStartDate=${startDate}&location=${sessionLocation}`;
|
|
93
|
+
return visitsUrl;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const { data, error, isLoading } = useSWR<{ data: VisitResponse }>(getUrl, openmrsFetch);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
visits: data?.data?.results ?? [],
|
|
100
|
+
count: data?.data?.results?.length ?? 0,
|
|
101
|
+
error,
|
|
102
|
+
isLoading,
|
|
103
|
+
};
|
|
104
|
+
};
|