@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.
Files changed (89) hide show
  1. package/.turbo/turbo-build.log +14 -0
  2. package/README.md +1 -0
  3. package/dist/152.js +1 -0
  4. package/dist/152.js.map +1 -0
  5. package/dist/159.js +1 -0
  6. package/dist/159.js.map +1 -0
  7. package/dist/208.js +1 -0
  8. package/dist/208.js.map +1 -0
  9. package/dist/209.js +1 -0
  10. package/dist/209.js.map +1 -0
  11. package/dist/363.js +1 -0
  12. package/dist/363.js.map +1 -0
  13. package/dist/410.js +1 -0
  14. package/dist/410.js.map +1 -0
  15. package/dist/442.js +1 -0
  16. package/dist/442.js.map +1 -0
  17. package/dist/466.js +1 -0
  18. package/dist/466.js.map +1 -0
  19. package/dist/484.js +11 -0
  20. package/dist/484.js.map +1 -0
  21. package/dist/540.js +1 -0
  22. package/dist/540.js.map +1 -0
  23. package/dist/545.js +43 -0
  24. package/dist/545.js.map +1 -0
  25. package/dist/61.js +1 -0
  26. package/dist/61.js.map +1 -0
  27. package/dist/677.js +1 -0
  28. package/dist/677.js.map +1 -0
  29. package/dist/689.js +1 -0
  30. package/dist/689.js.map +1 -0
  31. package/dist/697.js +1 -0
  32. package/dist/697.js.map +1 -0
  33. package/dist/712.js +1 -0
  34. package/dist/712.js.map +1 -0
  35. package/dist/771.js +1 -0
  36. package/dist/771.js.map +1 -0
  37. package/dist/789.js +1 -0
  38. package/dist/789.js.map +1 -0
  39. package/dist/ethiopia-esm-clinical-workflow-app.js +6 -0
  40. package/dist/ethiopia-esm-clinical-workflow-app.js.buildmanifest.json +579 -0
  41. package/dist/ethiopia-esm-clinical-workflow-app.js.map +1 -0
  42. package/dist/main.js +16 -0
  43. package/dist/main.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/jest.config.js +3 -0
  46. package/package.json +59 -0
  47. package/rspack.config.js +1 -0
  48. package/src/config-schema.ts +69 -0
  49. package/src/constants.ts +2 -0
  50. package/src/createDashboardLink.tsx +10 -0
  51. package/src/dashboard.meta.ts +6 -0
  52. package/src/declarations.d.ts +3 -0
  53. package/src/helper.ts +115 -0
  54. package/src/index.ts +51 -0
  55. package/src/mru/billing-information/billing-information.resource.ts +139 -0
  56. package/src/mru/billing-information/billing-information.scss +55 -0
  57. package/src/mru/billing-information/billing-information.workspace.tsx +371 -0
  58. package/src/mru/dashboard.component.tsx +18 -0
  59. package/src/mru/mru.component.tsx +106 -0
  60. package/src/mru/mru.scss +28 -0
  61. package/src/patient-registration/patient-registration.resource.tsx +129 -0
  62. package/src/patient-registration/patient.registration.workspace.scss +47 -0
  63. package/src/patient-registration/patient.registration.workspace.tsx +443 -0
  64. package/src/patient-registration/useGenerateIdentifier.ts +26 -0
  65. package/src/patient-scoreboard/appointment-cards/checked-in-appointments.card.tsx +18 -0
  66. package/src/patient-scoreboard/appointment-cards/not-arrived-appointments.card.tsx +18 -0
  67. package/src/patient-scoreboard/appointment-cards/total-appointments.card.tsx +18 -0
  68. package/src/patient-scoreboard/hooks/useAppointmentList.ts +61 -0
  69. package/src/patient-scoreboard/hooks/useVisitList.ts +104 -0
  70. package/src/patient-scoreboard/metrics-card/metrics-card.component.scss +84 -0
  71. package/src/patient-scoreboard/metrics-card/metrics-card.component.tsx +40 -0
  72. package/src/patient-scoreboard/patient-scoreboard.component.scss +47 -0
  73. package/src/patient-scoreboard/patient-scoreboard.component.tsx +70 -0
  74. package/src/patient-scoreboard/visit-cards/active-visits.card.tsx +18 -0
  75. package/src/patient-scoreboard/visit-cards/scheduled-visits.card.tsx +18 -0
  76. package/src/patient-scoreboard/visit-cards/total-visits.card.tsx +18 -0
  77. package/src/patient-scoreboard/visits-table/visits-table.component.scss +31 -0
  78. package/src/patient-scoreboard/visits-table/visits-table.component.tsx +181 -0
  79. package/src/root.component.tsx +20 -0
  80. package/src/root.scss +10 -0
  81. package/src/routes.json +108 -0
  82. package/src/triage/patient-banner.component.tsx +59 -0
  83. package/src/triage/patient-banner.scss +14 -0
  84. package/src/triage/triage-dashboard.component.tsx +116 -0
  85. package/src/triage/triage-dashboard.scss +107 -0
  86. package/src/triage/triage.resource.tsx +44 -0
  87. package/src/triage/useStartVisitAndLaunchTriageForm.ts +156 -0
  88. package/src/types/index.ts +0 -0
  89. 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
+ };