@digihmis/esm-service-queue-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/157.js +1 -0
- package/dist/157.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/217.js +1 -0
- package/dist/217.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/390.js +1 -0
- package/dist/390.js.map +1 -0
- package/dist/393.js +12 -0
- package/dist/393.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/50.js +1 -0
- package/dist/50.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/716.js +1 -0
- package/dist/716.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/731.js +15 -0
- package/dist/731.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/989.js +1 -0
- package/dist/989.js.map +1 -0
- package/dist/digihmis-esm-service-queue-app.js +6 -0
- package/dist/digihmis-esm-service-queue-app.js.buildmanifest.json +873 -0
- package/dist/digihmis-esm-service-queue-app.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +181 -0
- package/src/dashboard.meta.ts +5 -0
- package/src/declarations.d.ts +6 -0
- package/src/index.ts +22 -0
- package/src/move-patient-new-room/component/queue-priority.component.tsx +44 -0
- package/src/move-patient-new-room/component/queue-priority.scss +16 -0
- package/src/move-patient-new-room/move-patient-new-room.extension.tsx +375 -0
- package/src/move-patient-new-room/move-patient-new-room.resource.ts +164 -0
- package/src/move-patient-new-room/move-patient-new-room.scss +50 -0
- package/src/move-patient-new-room/type/index.ts +156 -0
- package/src/routes.json +22 -0
- package/src/service-queue-checkin-form/queue-fields.component.tsx +316 -0
- package/src/service-queue-checkin-form/service-queue-checkin-form.extension.tsx +27 -0
- package/src/service-queue-checkin-form/service-queue-checkin-form.resource.ts +99 -0
- package/src/service-queue-checkin-form/service-queue-checkin-form.scss +18 -0
- package/src/setup-tests.ts +1 -0
- package/src/type/index.ts +10 -0
- package/translations/am.json +14 -0
- package/translations/ar.json +14 -0
- package/translations/en.json +14 -0
- package/translations/es.json +14 -0
- package/translations/fr.json +14 -0
- package/translations/he.json +14 -0
- package/translations/km.json +14 -0
- package/translations/pt.json +14 -0
- package/translations/sw.json +14 -0
- package/translations/vi.json +14 -0
- package/translations/zh.json +14 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FormGroup,
|
|
4
|
+
InlineNotification,
|
|
5
|
+
Select,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectSkeleton,
|
|
8
|
+
Stack,
|
|
9
|
+
InlineLoading,
|
|
10
|
+
ModalHeader,
|
|
11
|
+
ModalBody,
|
|
12
|
+
ModalFooter,
|
|
13
|
+
RadioButtonGroup,
|
|
14
|
+
RadioButton,
|
|
15
|
+
Button,
|
|
16
|
+
} from '@carbon/react';
|
|
17
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
18
|
+
import { useTranslation } from 'react-i18next';
|
|
19
|
+
import { type TFunction } from 'i18next';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
22
|
+
import { navigate, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
23
|
+
import QueuePriority from './component/queue-priority.component';
|
|
24
|
+
import styles from './move-patient-new-room.scss';
|
|
25
|
+
import { type ServiceQueueConfig } from '../config-schema';
|
|
26
|
+
import {
|
|
27
|
+
postQueueEntry,
|
|
28
|
+
updateQueueEntry,
|
|
29
|
+
endQueueEntry,
|
|
30
|
+
useCurrentQueueEntry,
|
|
31
|
+
usePerson,
|
|
32
|
+
useQueue,
|
|
33
|
+
} from './move-patient-new-room.resource';
|
|
34
|
+
import { useMutateQueueEntries } from '../service-queue-checkin-form/service-queue-checkin-form.resource';
|
|
35
|
+
import upperCase from 'lodash-es/upperCase';
|
|
36
|
+
import { QueueEntryPayload } from './type';
|
|
37
|
+
|
|
38
|
+
type MovePatientToNewRoomProps = {
|
|
39
|
+
patientUuid: string;
|
|
40
|
+
closeModal?: () => void;
|
|
41
|
+
context?: 'triage' | 'consultation' | 'default';
|
|
42
|
+
afterSubmitNavigateUrl?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createQueueRoomSchema = (t: TFunction) => {
|
|
46
|
+
return z.object({
|
|
47
|
+
queue: z.string().min(1, { message: t('queueRequired', 'Queue is required') }),
|
|
48
|
+
queueRoom: z.string().min(1, { message: t('roomRequired', 'Room is required') }),
|
|
49
|
+
priority: z.string().min(1, { message: t('priorityRequired', 'Priority is required') }),
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const MovePatientToNewRoom: React.FC<MovePatientToNewRoomProps> = ({
|
|
54
|
+
patientUuid,
|
|
55
|
+
closeModal,
|
|
56
|
+
afterSubmitNavigateUrl,
|
|
57
|
+
context = 'default',
|
|
58
|
+
}) => {
|
|
59
|
+
const { t } = useTranslation();
|
|
60
|
+
const { queues, isLoading, mutate: mutateQueues } = useQueue();
|
|
61
|
+
const config = useConfig<ServiceQueueConfig>();
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
currentQueueEntry,
|
|
65
|
+
isLoading: isLoadingCurrentEntry,
|
|
66
|
+
mutate: mutateCurrentEntry,
|
|
67
|
+
} = useCurrentQueueEntry(patientUuid);
|
|
68
|
+
|
|
69
|
+
const filteredQueues = useMemo(() => {
|
|
70
|
+
if (!queues) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const exclusionList = config.queueExclusions[context] || [];
|
|
75
|
+
return queues.filter((queue) => !exclusionList.includes(queue.uuid));
|
|
76
|
+
}, [queues, config.queueExclusions, context]);
|
|
77
|
+
|
|
78
|
+
const schema = useMemo(() => createQueueRoomSchema(t), [t]);
|
|
79
|
+
const { mutateQueueEntries } = useMutateQueueEntries();
|
|
80
|
+
const { person } = usePerson(patientUuid);
|
|
81
|
+
const openmrsId =
|
|
82
|
+
person?.identifiers?.find((id) => id.identifierType?.uuid === config.OpenmrsIdentifierTypeUuid)?.identifier || '';
|
|
83
|
+
|
|
84
|
+
const {
|
|
85
|
+
control,
|
|
86
|
+
formState: { errors, touchedFields },
|
|
87
|
+
setValue,
|
|
88
|
+
watch,
|
|
89
|
+
getValues,
|
|
90
|
+
clearErrors,
|
|
91
|
+
} = useForm({
|
|
92
|
+
defaultValues: {
|
|
93
|
+
queue: currentQueueEntry?.queue?.uuid || '',
|
|
94
|
+
queueRoom: '',
|
|
95
|
+
priority: currentQueueEntry?.priority?.uuid || '',
|
|
96
|
+
},
|
|
97
|
+
mode: 'onChange',
|
|
98
|
+
resolver: zodResolver(schema),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const selectedQueueUuid = watch('queue');
|
|
102
|
+
const selectedPriority = watch('priority');
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (currentQueueEntry) {
|
|
106
|
+
if (currentQueueEntry.priority?.uuid) {
|
|
107
|
+
setValue('priority', currentQueueEntry.priority.uuid);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, [currentQueueEntry, setValue]);
|
|
111
|
+
|
|
112
|
+
const selectedDestinationQueue = useMemo(() => {
|
|
113
|
+
if (!selectedQueueUuid || !queues) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return queues.find((q) => q.uuid === selectedQueueUuid);
|
|
117
|
+
}, [selectedQueueUuid, queues]);
|
|
118
|
+
|
|
119
|
+
const availableQueueRooms = useMemo(() => {
|
|
120
|
+
if (!selectedDestinationQueue?.queueRooms) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const exclusionList = config.queueExclusions[context] || [];
|
|
125
|
+
|
|
126
|
+
return selectedDestinationQueue.queueRooms.filter((room) => {
|
|
127
|
+
const isExcludedByContext = exclusionList.includes(room.uuid);
|
|
128
|
+
const isCurrentRoom = room.uuid === currentQueueEntry?.queueRoom?.uuid;
|
|
129
|
+
|
|
130
|
+
return !isExcludedByContext && !isCurrentRoom;
|
|
131
|
+
});
|
|
132
|
+
}, [selectedDestinationQueue, config.queueExclusions, context, currentQueueEntry?.queueRoom?.uuid]);
|
|
133
|
+
|
|
134
|
+
const allowedPriorities = useMemo(() => {
|
|
135
|
+
if (selectedDestinationQueue?.allowedPriorities && selectedDestinationQueue.allowedPriorities.length > 0) {
|
|
136
|
+
return selectedDestinationQueue.allowedPriorities;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
{ uuid: config.concepts.emergencyPriorityConceptUuid, display: 'Emergency' },
|
|
141
|
+
{ uuid: config.concepts.defaultPriorityConceptUuid, display: 'Not Urgent' },
|
|
142
|
+
{ uuid: config.concepts.defaultStatusConceptUuid, display: 'Waiting' },
|
|
143
|
+
];
|
|
144
|
+
}, [selectedDestinationQueue, config.concepts]);
|
|
145
|
+
|
|
146
|
+
const sortWeight = useMemo(() => {
|
|
147
|
+
return selectedPriority === config.concepts.emergencyPriorityConceptUuid ? 1 : 0;
|
|
148
|
+
}, [selectedPriority, config.concepts.emergencyPriorityConceptUuid]);
|
|
149
|
+
|
|
150
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
151
|
+
|
|
152
|
+
const handleSubmit = async () => {
|
|
153
|
+
const formValues = getValues();
|
|
154
|
+
|
|
155
|
+
if (!formValues.queue || !formValues.queueRoom || !formValues.priority) {
|
|
156
|
+
showSnackbar({
|
|
157
|
+
title: t('missingFields', 'Missing required fields'),
|
|
158
|
+
kind: 'error',
|
|
159
|
+
isLowContrast: true,
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setIsSubmitting(true);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const isSameQueue = formValues.queue === currentQueueEntry?.queue?.uuid;
|
|
168
|
+
|
|
169
|
+
if (isSameQueue) {
|
|
170
|
+
// Moving within the same queue - just update the room
|
|
171
|
+
await updateQueueEntry(currentQueueEntry.uuid, {
|
|
172
|
+
queueRoom: formValues.queueRoom,
|
|
173
|
+
priority: formValues.priority,
|
|
174
|
+
status: config.concepts.defaultStatusConceptUuid,
|
|
175
|
+
sortWeight,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
showSnackbar({
|
|
179
|
+
title: t('patientMoved', 'Patient moved successfully'),
|
|
180
|
+
subtitle: t('patientMovedDescription', '{{patientName}} has been moved to {{queueRoom}}', {
|
|
181
|
+
patientName: person?.display,
|
|
182
|
+
queueRoom: availableQueueRooms.find((r) => r.uuid === formValues.queueRoom)?.display || 'new room',
|
|
183
|
+
}),
|
|
184
|
+
kind: 'success',
|
|
185
|
+
isLowContrast: true,
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
// Transferring to a different queue - end old entry and create new one
|
|
189
|
+
if (currentQueueEntry?.uuid) {
|
|
190
|
+
await endQueueEntry(currentQueueEntry.uuid);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const payload: QueueEntryPayload = {
|
|
194
|
+
patientUuid,
|
|
195
|
+
queueUuid: formValues.queue,
|
|
196
|
+
queueRoomUuid: formValues.queueRoom,
|
|
197
|
+
priorityUuid: formValues.priority,
|
|
198
|
+
statusUuid: config.concepts.defaultStatusConceptUuid,
|
|
199
|
+
sortWeight,
|
|
200
|
+
queueComingFrom: currentQueueEntry?.queue?.uuid,
|
|
201
|
+
queueRoomComingFrom: currentQueueEntry?.queueRoom?.uuid,
|
|
202
|
+
visitUuid: currentQueueEntry?.visit?.uuid,
|
|
203
|
+
startedAt: new Date().toISOString(),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await postQueueEntry(payload);
|
|
207
|
+
|
|
208
|
+
showSnackbar({
|
|
209
|
+
title: t('patientTransferred', 'Patient transferred successfully'),
|
|
210
|
+
subtitle: t('patientTransferredDescription', '{{patientName}} has been transferred to {{queueName}}', {
|
|
211
|
+
patientName: person?.display,
|
|
212
|
+
queueName: selectedDestinationQueue?.name || 'new queue',
|
|
213
|
+
}),
|
|
214
|
+
kind: 'success',
|
|
215
|
+
isLowContrast: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Mutate all relevant data sources
|
|
220
|
+
await Promise.all([mutateQueueEntries(), mutateCurrentEntry(), mutateQueues()]);
|
|
221
|
+
|
|
222
|
+
closeModal?.();
|
|
223
|
+
|
|
224
|
+
if (afterSubmitNavigateUrl) {
|
|
225
|
+
navigate({ to: afterSubmitNavigateUrl });
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const errorMessage =
|
|
229
|
+
error?.responseBody?.error?.message ||
|
|
230
|
+
error?.responseBody?.error?.translatedMessage ||
|
|
231
|
+
error?.message ||
|
|
232
|
+
t('unknownError', 'An unknown error occurred');
|
|
233
|
+
|
|
234
|
+
showSnackbar({
|
|
235
|
+
title: t('errorTransferringPatient', 'Error transferring patient'),
|
|
236
|
+
subtitle: errorMessage,
|
|
237
|
+
kind: 'error',
|
|
238
|
+
isLowContrast: true,
|
|
239
|
+
});
|
|
240
|
+
} finally {
|
|
241
|
+
setIsSubmitting(false);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div>
|
|
247
|
+
<div>
|
|
248
|
+
<ModalHeader closeModal={closeModal}>
|
|
249
|
+
<div className={styles.patientHeader}>
|
|
250
|
+
<span className={styles.patientName}>{upperCase(person?.display)}</span>
|
|
251
|
+
<span className={styles.patientDetails}>· {openmrsId} </span>
|
|
252
|
+
<span className={styles.patientDetails}>
|
|
253
|
+
· {person?.gender} · {person?.age} {t('years', 'yrs')} ·
|
|
254
|
+
</span>
|
|
255
|
+
<span className={styles.currentLocation}>
|
|
256
|
+
{currentQueueEntry?.queueRoom?.display} - {new Date().toLocaleDateString()}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
</ModalHeader>
|
|
260
|
+
<ModalBody>
|
|
261
|
+
<Stack gap={6}>
|
|
262
|
+
<div>
|
|
263
|
+
<Controller
|
|
264
|
+
name="queue"
|
|
265
|
+
control={control}
|
|
266
|
+
render={({ field }) =>
|
|
267
|
+
isLoading ? (
|
|
268
|
+
<SelectSkeleton />
|
|
269
|
+
) : (
|
|
270
|
+
<Select
|
|
271
|
+
{...field}
|
|
272
|
+
labelText={t('queue', 'Queue')}
|
|
273
|
+
id="destinationQueue"
|
|
274
|
+
invalid={!!errors.queue && touchedFields.queue}
|
|
275
|
+
invalidText={errors.queue?.message}
|
|
276
|
+
onChange={(event) => {
|
|
277
|
+
field.onChange(event.target.value);
|
|
278
|
+
// Clear queue room when queue changes
|
|
279
|
+
setValue('queueRoom', '', { shouldValidate: false });
|
|
280
|
+
clearErrors('queueRoom');
|
|
281
|
+
}}>
|
|
282
|
+
<SelectItem text={t('selectQueue', 'Select a queue')} value="" />
|
|
283
|
+
{filteredQueues?.map((queue) => (
|
|
284
|
+
<SelectItem key={queue.uuid} text={queue.name} value={queue.uuid}>
|
|
285
|
+
{queue.name}
|
|
286
|
+
</SelectItem>
|
|
287
|
+
))}
|
|
288
|
+
</Select>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{selectedQueueUuid && (
|
|
295
|
+
<div>
|
|
296
|
+
<Controller
|
|
297
|
+
name="queueRoom"
|
|
298
|
+
control={control}
|
|
299
|
+
render={({ field }) =>
|
|
300
|
+
isLoading ? (
|
|
301
|
+
<SelectSkeleton />
|
|
302
|
+
) : (
|
|
303
|
+
<Select
|
|
304
|
+
{...field}
|
|
305
|
+
labelText={t('room', 'Room')}
|
|
306
|
+
id="queueRoom"
|
|
307
|
+
invalid={!!errors.queueRoom && touchedFields.queueRoom}
|
|
308
|
+
invalidText={errors.queueRoom?.message}
|
|
309
|
+
disabled={availableQueueRooms.length === 0}>
|
|
310
|
+
<SelectItem text={t('selectRoom', 'Select a room')} value="" />
|
|
311
|
+
{availableQueueRooms?.map((room) => (
|
|
312
|
+
<SelectItem key={room.uuid} text={room.display || room.name} value={room.uuid}>
|
|
313
|
+
{room.display || room.name}
|
|
314
|
+
</SelectItem>
|
|
315
|
+
))}
|
|
316
|
+
</Select>
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
<div>
|
|
324
|
+
{selectedQueueUuid && (
|
|
325
|
+
<Controller
|
|
326
|
+
name="priority"
|
|
327
|
+
control={control}
|
|
328
|
+
render={({ field }) => (
|
|
329
|
+
<>
|
|
330
|
+
<RadioButtonGroup
|
|
331
|
+
className={styles.radioButtonGroup}
|
|
332
|
+
name="priority"
|
|
333
|
+
valueSelected={field.value}
|
|
334
|
+
onChange={(uuid) => {
|
|
335
|
+
field.onChange(uuid);
|
|
336
|
+
}}>
|
|
337
|
+
{allowedPriorities.map(({ uuid, display }) => (
|
|
338
|
+
<RadioButton
|
|
339
|
+
key={uuid}
|
|
340
|
+
id={uuid}
|
|
341
|
+
labelText={
|
|
342
|
+
<QueuePriority priority={{ uuid, display }} priorityConfigs={config.priorityConfigs} />
|
|
343
|
+
}
|
|
344
|
+
value={uuid}
|
|
345
|
+
/>
|
|
346
|
+
))}
|
|
347
|
+
</RadioButtonGroup>
|
|
348
|
+
{errors.priority && touchedFields.priority && (
|
|
349
|
+
<div className={styles.errorText}>{errors.priority.message}</div>
|
|
350
|
+
)}
|
|
351
|
+
</>
|
|
352
|
+
)}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</Stack>
|
|
357
|
+
</ModalBody>
|
|
358
|
+
<ModalFooter>
|
|
359
|
+
<Button kind="secondary" onClick={closeModal} type="button" disabled={isSubmitting}>
|
|
360
|
+
{t('cancel', 'Cancel')}
|
|
361
|
+
</Button>
|
|
362
|
+
<Button onClick={handleSubmit} type="submit" disabled={isSubmitting || isLoadingCurrentEntry} kind="primary">
|
|
363
|
+
{isSubmitting ? (
|
|
364
|
+
<InlineLoading description={t('transferring', 'Transferring...')} />
|
|
365
|
+
) : (
|
|
366
|
+
t('confirmTransfer', 'Confirm Transfer')
|
|
367
|
+
)}
|
|
368
|
+
</Button>
|
|
369
|
+
</ModalFooter>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export default MovePatientToNewRoom;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
import { type Patient, type Queue, type QueueEntry, type QueueEntryPayload, type QueueEntryRequestBody } from './type';
|
|
5
|
+
|
|
6
|
+
export function useCurrentQueueEntry(patientUuid: string) {
|
|
7
|
+
const customRepresentation =
|
|
8
|
+
'custom:(uuid,queue:(uuid,display),queueRoom:(uuid,display),status:(uuid,display),priority:(uuid,display),patient:(uuid,display),startedAt,endedAt,queueComingFrom:(uuid,display),queueRoomComingFrom:(uuid,display),visit:(uuid,display))';
|
|
9
|
+
|
|
10
|
+
const apiUrl = `${restBaseUrl}/queue-entry?patient=${patientUuid}&isEnded=false&v=${customRepresentation}`;
|
|
11
|
+
|
|
12
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<{ results: Array<QueueEntry> }>>(
|
|
13
|
+
patientUuid ? apiUrl : null,
|
|
14
|
+
openmrsFetch,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const currentQueueEntry = useMemo(() => {
|
|
18
|
+
if (!data?.data?.results || data.data.results.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return data.data.results[0];
|
|
22
|
+
}, [data]);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
currentQueueEntry,
|
|
26
|
+
isLoading,
|
|
27
|
+
error,
|
|
28
|
+
mutate,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useQueue() {
|
|
33
|
+
const customRepresentation =
|
|
34
|
+
'custom:(uuid,display,name,description,location:(uuid,display),service:(uuid,display),' +
|
|
35
|
+
'allowedPriorities:(uuid,display),allowedStatuses:(uuid,display),' +
|
|
36
|
+
'queueRooms:(uuid,display,name,description))';
|
|
37
|
+
|
|
38
|
+
const apiUrl = `${restBaseUrl}/queue?v=${customRepresentation}`;
|
|
39
|
+
|
|
40
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<{ results: Array<Queue> }>>(apiUrl, openmrsFetch);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
queues: data?.data?.results || [],
|
|
44
|
+
isLoading,
|
|
45
|
+
error,
|
|
46
|
+
mutate,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function postQueueEntry(payload: QueueEntryPayload) {
|
|
51
|
+
const {
|
|
52
|
+
patientUuid,
|
|
53
|
+
queueUuid,
|
|
54
|
+
queueRoomUuid,
|
|
55
|
+
priorityUuid,
|
|
56
|
+
statusUuid,
|
|
57
|
+
sortWeight,
|
|
58
|
+
queueComingFrom,
|
|
59
|
+
queueRoomComingFrom,
|
|
60
|
+
visitUuid,
|
|
61
|
+
startedAt,
|
|
62
|
+
} = payload;
|
|
63
|
+
|
|
64
|
+
const abortController = new AbortController();
|
|
65
|
+
|
|
66
|
+
const requestBody: QueueEntryRequestBody = {
|
|
67
|
+
patient: patientUuid,
|
|
68
|
+
queue: queueUuid,
|
|
69
|
+
status: statusUuid,
|
|
70
|
+
priority: priorityUuid,
|
|
71
|
+
sortWeight,
|
|
72
|
+
startedAt: startedAt || new Date().toISOString(),
|
|
73
|
+
...(queueRoomUuid && { queueRoom: queueRoomUuid }),
|
|
74
|
+
...(queueComingFrom && { queueComingFrom }),
|
|
75
|
+
...(queueRoomComingFrom && { queueRoomComingFrom }),
|
|
76
|
+
...(visitUuid && { visit: visitUuid }),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await openmrsFetch(`${restBaseUrl}/queue-entry`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
signal: abortController.signal,
|
|
86
|
+
body: JSON.stringify(requestBody),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return response.data;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const usePerson = (uuid: string) => {
|
|
96
|
+
const customRepresentation = `custom:(uuid,display,gender,birthdate,dead,age,deathDate,causeOfDeath:(uuid,display),identifiers:(uuid,identifier,identifierType:(uuid,display),preferred),person:(uuid))`;
|
|
97
|
+
const url = `${restBaseUrl}/person/${uuid}?v=${customRepresentation}`;
|
|
98
|
+
const { isLoading, error, data } = useSWR<FetchResponse<Patient['person']>>(url, openmrsFetch);
|
|
99
|
+
const person = data?.data;
|
|
100
|
+
return { isLoading, error, person };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export async function updateQueueEntry(
|
|
104
|
+
queueEntryUuid: string,
|
|
105
|
+
updates: {
|
|
106
|
+
queueRoom?: string;
|
|
107
|
+
priority?: string;
|
|
108
|
+
status?: string;
|
|
109
|
+
sortWeight?: number;
|
|
110
|
+
},
|
|
111
|
+
) {
|
|
112
|
+
const abortController = new AbortController();
|
|
113
|
+
|
|
114
|
+
const requestBody: any = {};
|
|
115
|
+
|
|
116
|
+
if (updates.queueRoom) {
|
|
117
|
+
requestBody.queueRoom = updates.queueRoom;
|
|
118
|
+
}
|
|
119
|
+
if (updates.priority) {
|
|
120
|
+
requestBody.priority = updates.priority;
|
|
121
|
+
}
|
|
122
|
+
if (updates.status) {
|
|
123
|
+
requestBody.status = updates.status;
|
|
124
|
+
}
|
|
125
|
+
if (updates.sortWeight !== undefined) {
|
|
126
|
+
requestBody.sortWeight = updates.sortWeight;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const response = await openmrsFetch(`${restBaseUrl}/queue-entry/${queueEntryUuid}`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
},
|
|
135
|
+
signal: abortController.signal,
|
|
136
|
+
body: JSON.stringify(requestBody),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return response.data;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function endQueueEntry(queueEntryUuid: string, endedAt?: string) {
|
|
146
|
+
const abortController = new AbortController();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await openmrsFetch(`${restBaseUrl}/queue-entry/${queueEntryUuid}`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
signal: abortController.signal,
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
endedAt: endedAt || new Date().toISOString(),
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return response.data;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.inputRow {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: 1fr 1fr;
|
|
8
|
+
gap: layout.$spacing-05;
|
|
9
|
+
|
|
10
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
11
|
+
grid-template-columns: 1fr;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.patientHeader {
|
|
16
|
+
padding: layout.$spacing-05;
|
|
17
|
+
border-bottom: 1px solid colors.$gray-20;
|
|
18
|
+
background-color: colors.$gray-10;
|
|
19
|
+
|
|
20
|
+
.patientName {
|
|
21
|
+
@include type.type-style('heading-compact-01');
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
margin-right: layout.$spacing-03;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.patientId {
|
|
27
|
+
@include type.type-style('body-compact-01');
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
margin-right: layout.$spacing-02;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.patientDetails {
|
|
33
|
+
@include type.type-style('body-compact-01');
|
|
34
|
+
color: colors.$gray-70;
|
|
35
|
+
}
|
|
36
|
+
.currentLocation {
|
|
37
|
+
@include type.type-style('body-compact-01');
|
|
38
|
+
color: colors.$gray-90;
|
|
39
|
+
float: right;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
:global(.cds--select-input) {
|
|
43
|
+
background-color: colors.$gray-20;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (min-width: 82rem) {
|
|
47
|
+
:global(.cds--modal-container) {
|
|
48
|
+
width: 38%;
|
|
49
|
+
}
|
|
50
|
+
}
|