@kenyaemr/esm-morgue-app 5.3.8-pre.1562 → 5.3.8-pre.1570

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 (54) hide show
  1. package/.turbo/turbo-build.log +19 -19
  2. package/dist/300.js +1 -1
  3. package/dist/38.js +1 -0
  4. package/dist/38.js.map +1 -0
  5. package/dist/553.js +1 -0
  6. package/dist/553.js.map +1 -0
  7. package/dist/570.js +1 -0
  8. package/dist/570.js.map +1 -0
  9. package/dist/632.js +1 -1
  10. package/dist/632.js.map +1 -1
  11. package/dist/687.js +2 -0
  12. package/dist/687.js.map +1 -0
  13. package/dist/759.js +1 -1
  14. package/dist/759.js.map +1 -1
  15. package/dist/864.js +1 -1
  16. package/dist/926.js +1 -1
  17. package/dist/926.js.map +1 -1
  18. package/dist/942.js +1 -1
  19. package/dist/942.js.map +1 -1
  20. package/dist/kenyaemr-esm-morgue-app.js +1 -1
  21. package/dist/kenyaemr-esm-morgue-app.js.buildmanifest.json +129 -108
  22. package/dist/kenyaemr-esm-morgue-app.js.map +1 -1
  23. package/dist/main.js +1 -1
  24. package/dist/main.js.map +1 -1
  25. package/dist/routes.json +1 -1
  26. package/package.json +1 -1
  27. package/src/card/compartment-view.compartment.tsx +1 -1
  28. package/src/component/main.component.tsx +7 -0
  29. package/src/config-schema.ts +7 -1
  30. package/src/empty-state/{empty-search-deceased.component.tsx → empty-morgue-admission.component.tsx} +3 -3
  31. package/src/header/morgue-header.component.tsx +11 -15
  32. package/src/header/morgue-header.scss +34 -40
  33. package/src/hook/useMorgue.resource.ts +107 -19
  34. package/src/tables/admitted-queue.component.tsx +11 -4
  35. package/src/tables/discharge-queue.component.tsx +73 -0
  36. package/src/tables/generic-table.component.tsx +1 -1
  37. package/src/tables/waiting-queue.component.tsx +10 -12
  38. package/src/tabs/tabs.component.tsx +44 -26
  39. package/src/tabs/tabs.scss +1 -2
  40. package/src/types/index.ts +120 -0
  41. package/src/utils/utils.ts +23 -0
  42. package/src/workspaces/discharge-body.scss +13 -0
  43. package/src/workspaces/discharge-body.workspace.tsx +224 -24
  44. package/translations/en.json +4 -5
  45. package/dist/268.js +0 -2
  46. package/dist/268.js.map +0 -1
  47. package/dist/340.js +0 -1
  48. package/dist/340.js.map +0 -1
  49. package/dist/701.js +0 -1
  50. package/dist/701.js.map +0 -1
  51. package/dist/809.js +0 -1
  52. package/dist/809.js.map +0 -1
  53. /package/dist/{268.js.LICENSE.txt → 687.js.LICENSE.txt} +0 -0
  54. /package/src/empty-state/{empty-deceased.scss → empty-morgue-admission.scss} +0 -0
@@ -59,7 +59,6 @@
59
59
  justify-content: space-between;
60
60
  align-items: center;
61
61
  padding: layout.$spacing-04 layout.$spacing-05;
62
- // background-color: $ui-02;
63
62
  }
64
63
 
65
64
  .backgroundDataFetchingIndicator {
@@ -183,7 +182,7 @@
183
182
  .actionBtn {
184
183
  display: flex;
185
184
  gap: 0.5rem;
186
- margin-right: layout.$spacing-05;
185
+ margin-right: layout.$spacing-07;
187
186
  }
188
187
 
189
188
  .mainSection {
@@ -1,3 +1,10 @@
1
+ import { OpenmrsResource } from '@openmrs/esm-framework';
2
+
3
+ export interface Concept extends OpenmrsResource {}
4
+ export type QueuePriority = 'Emergency' | 'Not Urgent' | 'Priority' | 'Urgent';
5
+ export type MappedQueuePriority = Omit<QueuePriority, 'Urgent'>;
6
+ export type QueueService = 'Clinical consultation' | 'Triage';
7
+ export type QueueStatus = 'Finished Service' | 'In Service' | 'Waiting';
1
8
  export interface Patient {
2
9
  uuid: string;
3
10
  display: string;
@@ -180,3 +187,116 @@ export interface EncounterList {
180
187
  name: string;
181
188
  };
182
189
  }
190
+
191
+ export interface VisitQueueEntry {
192
+ queueEntry: VisitQueueEntry;
193
+ uuid: string;
194
+ visit: Visit;
195
+ }
196
+
197
+ export interface VisitQueueEntry {
198
+ display: string;
199
+ endedAt: null;
200
+ locationWaitingFor: string | null;
201
+ patient: {
202
+ uuid: string;
203
+ person: {
204
+ age: string;
205
+ gender: string;
206
+ };
207
+ phoneNumber: string;
208
+ };
209
+ priority: {
210
+ display: QueuePriority;
211
+ uuid: string;
212
+ };
213
+ providerWaitingFor: null;
214
+ queue: Queue;
215
+ startedAt: string;
216
+ status: {
217
+ display: QueueStatus;
218
+ uuid: string;
219
+ };
220
+ uuid: string;
221
+ visit: Visit;
222
+ }
223
+
224
+ export interface MappedVisitQueueEntry {
225
+ id: string;
226
+ name: string;
227
+ patientUuid: string;
228
+ priority: MappedQueuePriority;
229
+ priorityUuid: string;
230
+ service: string;
231
+ status: QueueStatus;
232
+ statusUuid: string;
233
+ visitUuid: string;
234
+ visitType: string;
235
+ queue: Queue;
236
+ queueEntryUuid: string;
237
+ }
238
+
239
+ export interface UseVisitQueueEntries {
240
+ queueEntry: MappedVisitQueueEntry | null;
241
+ isLoading: boolean;
242
+ error: Error;
243
+ isValidating?: boolean;
244
+ mutate: () => void;
245
+ }
246
+ export interface Queue {
247
+ uuid: string;
248
+ display: string;
249
+ name: string;
250
+ description: string;
251
+ location: Location;
252
+ service: string;
253
+ allowedPriorities: Array<Concept>;
254
+ allowedStatuses: Array<Concept>;
255
+ }
256
+
257
+ export type UpdateVisitPayload = {
258
+ stopDatetime?: Date;
259
+ };
260
+
261
+ export interface PaginatedResponse {
262
+ uuid: string;
263
+ display: string;
264
+ identifiers: Identifier[];
265
+ person: Person;
266
+ }
267
+
268
+ export interface Identifier {
269
+ identifier: string;
270
+ uuid: string;
271
+ preferred: boolean;
272
+ location: Location;
273
+ }
274
+
275
+ export interface Location {
276
+ uuid: string;
277
+ name: string;
278
+ }
279
+
280
+ export interface Person {
281
+ uuid: string;
282
+ display: string;
283
+ gender: string;
284
+ birthdate: string;
285
+ dead: boolean;
286
+ age: number;
287
+ deathDate: string;
288
+ causeOfDeath: CauseOfDeath;
289
+ preferredAddress: PreferredAddress;
290
+ }
291
+
292
+ export interface CauseOfDeath {
293
+ uuid: string;
294
+ display: string;
295
+ }
296
+
297
+ export interface PreferredAddress {
298
+ uuid: string;
299
+ stateProvince: any;
300
+ countyDistrict: any;
301
+ address4: any;
302
+ }
@@ -34,3 +34,26 @@ export function convertDateToDays(startDate: string | Date): number {
34
34
  const start = dayjs(startDate);
35
35
  return today.diff(start, 'day');
36
36
  }
37
+
38
+ /**
39
+ * Formats a given date string into "DD-MMM-YYYY, hh:mm A" format.
40
+ *
41
+ * @param {string | Date | undefined} date - The date to format.
42
+ * @returns {string} - The formatted date or an empty string if no date is provided.
43
+ */
44
+ export const formatDateTime = (date) => {
45
+ if (!date) {
46
+ return '--';
47
+ }
48
+ return dayjs(date).format('DD-MMM-YYYY, hh:mm A');
49
+ };
50
+
51
+ // Utility functions to get the current date and time
52
+
53
+ export const getCurrentTime = () => {
54
+ const now = new Date();
55
+ const hours = now.getHours() % 12 || 12;
56
+ const minutes = String(now.getMinutes()).padStart(2, '0');
57
+ const period = now.getHours() >= 12 ? 'PM' : 'AM';
58
+ return { time: `${hours}:${minutes}`, period };
59
+ };
@@ -52,3 +52,16 @@
52
52
  .dateTimePickerContainer {
53
53
  margin-left: layout.$spacing-04;
54
54
  }
55
+
56
+ .sectionField {
57
+ min-width: 25rem;
58
+ }
59
+ .columnField {
60
+ padding: layout.$spacing-02;
61
+ }
62
+ .fieldColumn {
63
+ padding: layout.$spacing-04;
64
+ }
65
+ .fieldSection {
66
+ min-width: 22rem;
67
+ }
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import {
2
3
  Button,
3
4
  ButtonSet,
@@ -9,56 +10,255 @@ import {
9
10
  TimePickerSelect,
10
11
  SelectItem,
11
12
  Column,
13
+ TextInput,
12
14
  } from '@carbon/react';
13
- import { ExtensionSlot, ResponsiveWrapper, useLayoutType } from '@openmrs/esm-framework';
14
- import React from 'react';
15
+ import {
16
+ ResponsiveWrapper,
17
+ restBaseUrl,
18
+ setCurrentVisit,
19
+ showSnackbar,
20
+ useConfig,
21
+ useLayoutType,
22
+ useSession,
23
+ useVisit,
24
+ } from '@openmrs/esm-framework';
15
25
  import { useTranslation } from 'react-i18next';
16
26
  import styles from './discharge-body.scss';
17
27
  import DeceasedInfo from '../component/deceasedInfo/deceased-info.component';
28
+ import {
29
+ removeQueuedPatient,
30
+ startVisitWithEncounter,
31
+ updateVisit,
32
+ useVisitQueueEntry,
33
+ } from '../hook/useMorgue.resource';
34
+ import { z } from 'zod';
35
+ import { useForm, Controller } from 'react-hook-form';
36
+ import { zodResolver } from '@hookform/resolvers/zod';
37
+ import { getCurrentTime } from '../utils/utils';
38
+ import { ConfigObject } from '../config-schema';
39
+ import { mutate } from 'swr';
18
40
 
19
- interface PatientAdditionalInfoFormProps {
41
+ interface DischargeFormProps {
20
42
  closeWorkspace: () => void;
21
43
  patientUuid: string;
22
44
  }
23
45
 
24
- const PatientAdditionalInfoForm: React.FC<PatientAdditionalInfoFormProps> = ({ closeWorkspace, patientUuid }) => {
46
+ const dischargeSchema = z.object({
47
+ dateOfDischarge: z.date({ coerce: true }).refine((date) => !!date, 'Date of discharge is required'),
48
+ timeOfDischarge: z.string().nonempty('Time of discharge is required'),
49
+ period: z
50
+ .string()
51
+ .nonempty('AM/PM is required')
52
+ .regex(/^(AM|PM)$/i, 'Invalid period'),
53
+ burialPermitNumber: z.string().nonempty('Burial Permit Number is required'),
54
+ });
55
+
56
+ type DischargeFormValues = z.infer<typeof dischargeSchema>;
57
+
58
+ const DischargeForm: React.FC<DischargeFormProps> = ({ closeWorkspace, patientUuid }) => {
25
59
  const { t } = useTranslation();
26
60
  const layout = useLayoutType();
61
+ const { currentVisit, currentVisitIsRetrospective } = useVisit(patientUuid);
62
+ const { queueEntry } = useVisitQueueEntry(patientUuid, currentVisit?.uuid);
63
+
64
+ const { time: defaultTime, period: defaultPeriod } = getCurrentTime();
65
+
66
+ const {
67
+ currentProvider: { uuid: currentProviderUuid },
68
+ sessionLocation: { uuid: locationUuid },
69
+ } = useSession();
70
+
71
+ const {
72
+ burialPermitNumberUuid,
73
+ encounterProviderRoleUuid,
74
+ morgueAdmissionEncounterType,
75
+ morgueDischargeEncounterTypeUuid,
76
+ morgueVisitTypeUuid,
77
+ } = useConfig<ConfigObject>();
78
+
79
+ const {
80
+ control,
81
+ handleSubmit,
82
+ formState: { errors },
83
+ } = useForm<DischargeFormValues>({
84
+ resolver: zodResolver(dischargeSchema),
85
+ defaultValues: {
86
+ dateOfDischarge: new Date(),
87
+ timeOfDischarge: defaultTime,
88
+ period: defaultPeriod,
89
+ burialPermitNumber: '',
90
+ },
91
+ });
92
+
93
+ const onSubmit = async (data: DischargeFormValues) => {
94
+ if (currentVisitIsRetrospective) {
95
+ setCurrentVisit(null, null);
96
+ closeWorkspace();
97
+ } else {
98
+ const obs = [];
99
+ if (data.burialPermitNumber) {
100
+ obs.push({ concept: burialPermitNumberUuid, value: data.burialPermitNumber });
101
+ }
102
+
103
+ const encounterPayload = {
104
+ encounterDatetime: data?.dateOfDischarge,
105
+ patient: currentVisit?.patient?.uuid,
106
+ encounterType: morgueDischargeEncounterTypeUuid,
107
+ location: currentVisit?.location?.uuid,
108
+ encounterProviders: [
109
+ {
110
+ provider: currentProviderUuid,
111
+ encounterRole: encounterProviderRoleUuid,
112
+ },
113
+ ],
114
+ visit: currentVisit?.uuid,
115
+ obs: obs.length > 0 ? obs : undefined,
116
+ };
117
+
118
+ const endVisitPayload = {
119
+ stopDatetime: data.dateOfDischarge,
120
+ };
121
+
122
+ const abortController = new AbortController();
123
+
124
+ try {
125
+ // First, create the encounter
126
+ await startVisitWithEncounter(encounterPayload);
127
+
128
+ showSnackbar({
129
+ title: 'Discharge',
130
+ subtitle: 'The deceased has been discharged successfully',
131
+ kind: 'success',
132
+ });
133
+
134
+ // Then, end the visit
135
+ updateVisit(currentVisit.uuid, endVisitPayload, abortController).subscribe({
136
+ next: (response) => {
137
+ if (queueEntry) {
138
+ removeQueuedPatient(
139
+ queueEntry.queue.uuid,
140
+ queueEntry.queueEntryUuid,
141
+ abortController,
142
+ response?.data.stopDatetime,
143
+ );
144
+ }
145
+ closeWorkspace();
146
+ showSnackbar({
147
+ isLowContrast: true,
148
+ kind: 'success',
149
+ subtitle: t('visitEndSuccessfully', `${response?.data?.visitType?.display} ended successfully`),
150
+ title: t('visitEnded', 'Visit ended'),
151
+ });
152
+ mutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/visit`));
153
+ },
154
+ error: (error) => {
155
+ showSnackbar({
156
+ title: t('errorEndingVisit', 'Error ending visit'),
157
+ kind: 'error',
158
+ isLowContrast: false,
159
+ subtitle: error?.message,
160
+ });
161
+ },
162
+ });
163
+ } catch (error) {
164
+ const errorMessage = JSON.stringify(error?.responseBody?.error?.message?.replace(/\[/g, '').replace(/\]/g, ''));
165
+ showSnackbar({
166
+ title: 'Visit Error',
167
+ subtitle: `An error has occurred while starting visit, Contact system administrator quoting this error ${errorMessage}`,
168
+ kind: 'error',
169
+ isLowContrast: true,
170
+ });
171
+ }
172
+ }
173
+ };
27
174
 
28
175
  return (
29
- <Form className={styles.formContainer}>
176
+ <Form className={styles.formContainer} onSubmit={handleSubmit(onSubmit)}>
30
177
  <Stack gap={4} className={styles.formGrid}>
31
178
  <DeceasedInfo patientUuid={patientUuid} />
32
179
  <div className={styles.dateTimePickerContainer}>
33
180
  <Column>
34
- <DatePicker datePickerType="single" className={styles.formAdmissionDatepicker}>
35
- <DatePickerInput
36
- id="date-of-admission"
37
- placeholder="mm/dd/yyyy"
38
- labelText={t('dateOfAdmission', 'Date of admission*')}
39
- />
40
- </DatePicker>
181
+ <Controller
182
+ name="dateOfDischarge"
183
+ control={control}
184
+ render={({ field }) => (
185
+ <DatePicker
186
+ datePickerType="single"
187
+ className={styles.formAdmissionDatepicker}
188
+ onChange={(event) => {
189
+ if (event.length) {
190
+ field.onChange(event[0]);
191
+ }
192
+ }}
193
+ value={field.value ? new Date(field.value) : null}>
194
+ <DatePickerInput
195
+ {...field}
196
+ id="date-of-admission"
197
+ placeholder="yyyy-mm-dd"
198
+ labelText={t('dateOfAdmission', 'Date of discharge*')}
199
+ invalid={!!errors.dateOfDischarge}
200
+ invalidText={errors.dateOfDischarge?.message}
201
+ />
202
+ </DatePicker>
203
+ )}
204
+ />
41
205
  </Column>
42
206
 
43
207
  <Column>
44
208
  <div className={styles.dateTimeSection}>
45
209
  <ResponsiveWrapper>
46
- <TimePicker
47
- id="time-of-death-picker"
48
- labelText={t('timeofDischarge', 'Time of discharge*')}
49
- className={styles.formAdmissionTimepicker}
210
+ <Controller
211
+ name="timeOfDischarge"
212
+ control={control}
213
+ render={({ field }) => (
214
+ <TimePicker
215
+ {...field}
216
+ id="time-of-discharge-picker"
217
+ labelText={t('timeOfDischarge', 'Time of discharge*')}
218
+ className={styles.formAdmissionTimepicker}
219
+ invalid={!!errors.timeOfDischarge}
220
+ invalidText={errors.timeOfDischarge?.message}
221
+ />
222
+ )}
223
+ />
224
+ <Controller
225
+ name="period"
226
+ control={control}
227
+ render={({ field }) => (
228
+ <TimePickerSelect
229
+ {...field}
230
+ className={styles.formDeathTimepickerSelector}
231
+ id="time-picker-select"
232
+ labelText={t('selectPeriod', 'AM/PM')}
233
+ invalid={!!errors.period}
234
+ invalidText={errors.period?.message}>
235
+ <SelectItem value="AM" text="AM" />
236
+ <SelectItem value="PM" text="PM" />
237
+ </TimePickerSelect>
238
+ )}
50
239
  />
51
- <TimePickerSelect
52
- className={styles.formDeathTimepickerSelector}
53
- id="time-picker-select"
54
- labelText={t('selectPeriod', 'AM/PM')}>
55
- <SelectItem value="AM" text="AM" />
56
- <SelectItem value="PM" text="PM" />
57
- </TimePickerSelect>
58
240
  </ResponsiveWrapper>
59
241
  </div>
60
242
  </Column>
61
243
  </div>
244
+ <Column className={styles.fieldColumn}>
245
+ <Controller
246
+ name="burialPermitNumber"
247
+ control={control}
248
+ render={({ field }) => (
249
+ <TextInput
250
+ {...field}
251
+ id="burialPermitNumber"
252
+ type="text"
253
+ className={styles.fieldSection}
254
+ placeholder={t('burialPermitNumber', 'Burial permit number')}
255
+ labelText={t('burialPermitNumber', 'Burial permit number')}
256
+ invalid={!!errors.burialPermitNumber}
257
+ invalidText={errors.burialPermitNumber?.message}
258
+ />
259
+ )}
260
+ />
261
+ </Column>
62
262
 
63
263
  <ButtonSet className={styles.buttonSet}>
64
264
  <Button size="lg" kind="secondary" onClick={closeWorkspace}>
@@ -73,4 +273,4 @@ const PatientAdditionalInfoForm: React.FC<PatientAdditionalInfoFormProps> = ({ c
73
273
  );
74
274
  };
75
275
 
76
- export default PatientAdditionalInfoForm;
276
+ export default DischargeForm;
@@ -2,22 +2,21 @@
2
2
  "admissionForm": "Admission form",
3
3
  "admit": "Admit",
4
4
  "admitted": "Admitted Bodies",
5
- "admittedOnes": "Admitted",
6
5
  "allocation": "Allocation",
7
- "awaiting": "Awaiting",
6
+ "allocations": "Allocation",
8
7
  "causeOfDeath": "Cause of death: ",
8
+ "dischargeBodies": "Discharged bodies",
9
9
  "discharged": "Discharged",
10
- "dischargeForm": "Discharge form",
11
- "discharges": "Discharges",
12
10
  "empty": "Empty",
13
11
  "errorMessage": "Error",
14
12
  "mortuary": "Mortuary",
15
13
  "mortuaryManagement": "Mortuary management",
14
+ "noAdmittedBodies": "There are no admitted bodies",
16
15
  "noDeceasedPersons": "There are no deceased persons on the waiting list",
17
16
  "noResultFound": "Sorry, no results found",
18
17
  "noWaitingList": "Waiting List",
18
+ "nullDate": "--",
19
19
  "pullingCompartment": "Pulling compartments data.....",
20
- "release": "Release",
21
20
  "searchForADeceased": "Try to search again using the deceased patient's unique ID number",
22
21
  "waitingInLine": "Waiting In Line",
23
22
  "waitingQueue": "Waiting queue",