@kenyaemr/esm-morgue-app 5.4.2-pre.2297 → 5.4.2-pre.2305

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 (38) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/dist/201.js +1 -1
  3. package/dist/201.js.map +1 -1
  4. package/dist/398.js +1 -1
  5. package/dist/398.js.map +1 -1
  6. package/dist/{826.js → 579.js} +2 -2
  7. package/dist/{826.js.map → 579.js.map} +1 -1
  8. package/dist/632.js +1 -1
  9. package/dist/632.js.map +1 -1
  10. package/dist/633.js +1 -1
  11. package/dist/633.js.map +1 -1
  12. package/dist/{146.js → 989.js} +2 -2
  13. package/dist/989.js.map +1 -0
  14. package/dist/kenyaemr-esm-morgue-app.js +1 -1
  15. package/dist/kenyaemr-esm-morgue-app.js.buildmanifest.json +73 -73
  16. package/dist/main.js +1 -1
  17. package/dist/main.js.map +1 -1
  18. package/dist/routes.json +1 -1
  19. package/package.json +1 -1
  20. package/src/bed-layout/discharged/discharged-bed-layout.component.tsx +32 -3
  21. package/src/bed-layout/discharged/discharged-bed-layout.resource.ts +22 -12
  22. package/src/bed-linelist-view/admitted/admitted-bed-linelist-view.component.tsx +1 -0
  23. package/src/bed-linelist-view/discharged/discharged-bed-line-view.component.tsx +0 -1
  24. package/src/config-schema.ts +0 -6
  25. package/src/forms/admit-deceased-person-workspace/admit-deceased-person.resource.ts +117 -75
  26. package/src/forms/admit-deceased-person-workspace/admit-deceased-person.scss +1 -1
  27. package/src/forms/admit-deceased-person-workspace/admit-deceased-person.workspace.tsx +15 -14
  28. package/src/forms/discharge-deceased-person-workspace/discharge-body.resource.ts +124 -26
  29. package/src/forms/discharge-deceased-person-workspace/discharge-body.workspace.tsx +188 -82
  30. package/src/forms/dispose-deceased-person-workspace/dispose-deceased-person.workspace.tsx +133 -48
  31. package/src/home/home.component.tsx +7 -2
  32. package/src/home/home.resource.ts +1 -1
  33. package/src/switcher/content-switcher.component.tsx +0 -1
  34. package/src/types/index.ts +1 -4
  35. package/src/utils/utils.ts +23 -39
  36. package/dist/146.js.map +0 -1
  37. /package/dist/{826.js.LICENSE.txt → 579.js.LICENSE.txt} +0 -0
  38. /package/dist/{146.js.LICENSE.txt → 989.js.LICENSE.txt} +0 -0
@@ -25,7 +25,7 @@ import {
25
25
  useLayoutType,
26
26
  useVisit,
27
27
  } from '@openmrs/esm-framework';
28
- import React, { useCallback, useEffect } from 'react';
28
+ import React, { useCallback, useEffect, useState } from 'react';
29
29
  import { Controller, useForm } from 'react-hook-form';
30
30
  import { useTranslation } from 'react-i18next';
31
31
  import { mutate as mutateSWR } from 'swr';
@@ -47,46 +47,66 @@ import { useMortuaryOperation } from '../admit-deceased-person-workspace/admit-d
47
47
  interface DisposeFormProps {
48
48
  closeWorkspace: () => void;
49
49
  patientUuid: string;
50
- personUuid: string;
51
50
  bedId: number;
52
51
  mutate: () => void;
53
52
  }
54
53
 
55
54
  type DisposeFormValues = z.infer<typeof disposeSchema>;
56
55
 
57
- const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid, bedId, personUuid, mutate }) => {
56
+ const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid, bedId, mutate }) => {
58
57
  const { t } = useTranslation();
59
58
  const isTablet = useLayoutType() === 'tablet';
59
+ const [submissionError, setSubmissionError] = useState<string | null>(null);
60
+
60
61
  const { activeVisit, currentVisitIsRetrospective } = useVisit(patientUuid);
61
62
  const { queueEntry } = useVisitQueueEntry(patientUuid, activeVisit?.uuid);
63
+
62
64
  const { disposeBody, isLoadingEmrConfiguration } = useMortuaryOperation();
63
- const { createOrUpdatePersonAttribute, personAttributes } = usePersonAttributes(personUuid);
64
- const { mutateAll, mutateAwaitingQueuePatients } = useAwaitingQueuePatients();
65
+
66
+ const {
67
+ createOrUpdatePersonAttribute,
68
+ personAttributes,
69
+ isLoading: isLoadingAttributes,
70
+ } = usePersonAttributes(patientUuid);
71
+
65
72
  const { isDischargeBlocked, blockingMessage, isLoadingBills } = useBlockDischargeWithPendingBills({
66
73
  patientUuid,
74
+ actionType: 'dispose',
67
75
  });
68
- const { time: defaultTime, period: defaultPeriod } = getCurrentTime();
69
76
 
70
77
  const { nextOfKinNameUuid, nextOfKinRelationshipUuid, nextOfKinPhoneUuid, nextOfKinAddressUuid } =
71
78
  useConfig<ConfigObject>();
72
79
 
80
+ const { time: defaultTime, period: defaultPeriod } = getCurrentTime();
81
+
73
82
  const getAttributeValue = useCallback(
74
83
  (attributeTypeUuid: string) => {
75
- if (!personAttributes) {
84
+ if (!personAttributes || !Array.isArray(personAttributes)) {
76
85
  return '';
77
86
  }
78
- const attributes = Array.isArray(personAttributes) ? personAttributes : [];
79
- const attribute = attributes.find((attr) => attr.attributeType.uuid === attributeTypeUuid);
87
+ const attribute = personAttributes.find((attr) => attr.attributeType.uuid === attributeTypeUuid);
80
88
  return attribute ? attribute.value : '';
81
89
  },
82
90
  [personAttributes],
83
91
  );
84
92
 
93
+ const getExistingAttributeUuid = useCallback(
94
+ (attributeTypeUuid: string) => {
95
+ if (!personAttributes || !Array.isArray(personAttributes)) {
96
+ return null;
97
+ }
98
+ const attribute = personAttributes.find((attr) => attr.attributeType.uuid === attributeTypeUuid);
99
+ return attribute ? attribute.uuid : null;
100
+ },
101
+ [personAttributes],
102
+ );
103
+
85
104
  const {
86
105
  control,
87
106
  setValue,
88
107
  handleSubmit,
89
108
  formState: { errors, isDirty, isSubmitting },
109
+ watch,
90
110
  } = useForm<DisposeFormValues>({
91
111
  resolver: zodResolver(disposeSchema),
92
112
  defaultValues: {
@@ -104,22 +124,32 @@ const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid,
104
124
 
105
125
  useEffect(() => {
106
126
  if (Array.isArray(personAttributes) && personAttributes.length > 0) {
107
- setValue('nextOfKinNames', getAttributeValue(nextOfKinNameUuid));
108
- setValue('relationshipType', getAttributeValue(nextOfKinRelationshipUuid));
109
- setValue('nextOfKinContact', getAttributeValue(nextOfKinPhoneUuid));
110
- setValue('nextOfKinAddress', getAttributeValue(nextOfKinAddressUuid));
127
+ const initialValues = {
128
+ nextOfKinNames: getAttributeValue(nextOfKinNameUuid),
129
+ relationshipType: getAttributeValue(nextOfKinRelationshipUuid),
130
+ nextOfKinContact: getAttributeValue(nextOfKinPhoneUuid),
131
+ nextOfKinAddress: getAttributeValue(nextOfKinAddressUuid),
132
+ };
133
+
134
+ Object.entries(initialValues).forEach(([field, value]) => {
135
+ if (value !== undefined && value !== null && value !== '') {
136
+ setValue(field as keyof DisposeFormValues, value);
137
+ }
138
+ });
111
139
  }
112
140
  }, [
113
141
  personAttributes,
142
+ setValue,
114
143
  getAttributeValue,
115
144
  nextOfKinNameUuid,
116
145
  nextOfKinRelationshipUuid,
117
146
  nextOfKinPhoneUuid,
118
147
  nextOfKinAddressUuid,
119
- setValue,
120
148
  ]);
121
149
 
122
150
  const onSubmit = async (data: DisposeFormValues) => {
151
+ setSubmissionError(null);
152
+
123
153
  if (currentVisitIsRetrospective) {
124
154
  setCurrentVisit(null, null);
125
155
  closeWorkspace();
@@ -127,35 +157,63 @@ const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid,
127
157
  }
128
158
 
129
159
  try {
130
- const nextOfKinAttributes = [
131
- { attributeType: nextOfKinNameUuid, value: data.nextOfKinNames },
132
- { attributeType: nextOfKinRelationshipUuid, value: data.relationshipType },
133
- { attributeType: nextOfKinPhoneUuid, value: data.nextOfKinContact },
134
- { attributeType: nextOfKinAddressUuid, value: data.nextOfKinAddress },
135
- ];
160
+ await disposeBody(activeVisit, queueEntry, bedId, data);
161
+
162
+ const attributeUpdates = [
163
+ {
164
+ uuid: nextOfKinNameUuid,
165
+ value: data.nextOfKinNames,
166
+ existingUuid: getExistingAttributeUuid(nextOfKinNameUuid),
167
+ },
168
+ {
169
+ uuid: nextOfKinRelationshipUuid,
170
+ value: data.relationshipType,
171
+ existingUuid: getExistingAttributeUuid(nextOfKinRelationshipUuid),
172
+ },
173
+ {
174
+ uuid: nextOfKinPhoneUuid,
175
+ value: data.nextOfKinContact,
176
+ existingUuid: getExistingAttributeUuid(nextOfKinPhoneUuid),
177
+ },
178
+ {
179
+ uuid: nextOfKinAddressUuid,
180
+ value: data.nextOfKinAddress,
181
+ existingUuid: getExistingAttributeUuid(nextOfKinAddressUuid),
182
+ },
183
+ ].filter((attr) => attr.value !== undefined && attr.value !== null && attr.value !== '');
136
184
 
137
185
  const patientInfo: PatientInfo = {
138
186
  uuid: activeVisit.patient.uuid,
139
- attributes: (activeVisit?.patient?.person?.attributes || []).map((attr) => ({
140
- uuid: attr.uuid,
141
- display: attr.display || '',
142
- })),
187
+ attributes: personAttributes || [],
143
188
  };
144
189
 
145
- for (const attribute of nextOfKinAttributes) {
190
+ for (const attr of attributeUpdates) {
146
191
  try {
147
- await createOrUpdatePersonAttribute(patientUuid, attribute, patientInfo);
148
- } catch (attributeError) {
192
+ const attributeData: any = {
193
+ attributeType: attr.uuid,
194
+ value: attr.value,
195
+ };
196
+
197
+ if (attr.existingUuid) {
198
+ attributeData.uuid = attr.existingUuid;
199
+ }
200
+
201
+ await createOrUpdatePersonAttribute(patientUuid, attributeData, patientInfo);
202
+ } catch (error) {
149
203
  showSnackbar({
150
- title: t('errorDisposingPatient', 'Error disposing patient'),
151
- subtitle: attributeError?.message || t('unknownError', 'Unknown error occurred'),
204
+ title: t('errorUpdatingAttribute', 'Error Updating Attribute'),
205
+ subtitle: t('errorUpdatingAttributeDescription', 'An error occurred while updating the attribute'),
152
206
  kind: 'error',
153
207
  isLowContrast: true,
154
208
  });
155
209
  }
156
210
  }
157
211
 
158
- await disposeBody(activeVisit, queueEntry, bedId, data);
212
+ await Promise.all([
213
+ mutateSWR((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/visit`)),
214
+ mutateSWR((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/patient`)),
215
+ mutateSWR((key) => typeof key === 'string' && key.startsWith(`${fhirBaseUrl}/Encounter`)),
216
+ ]);
159
217
 
160
218
  showSnackbar({
161
219
  title: t('disposedDeceasedPatient', 'Deceased patient disposed'),
@@ -164,41 +222,66 @@ const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid,
164
222
  isLowContrast: true,
165
223
  });
166
224
 
167
- mutateSWR((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/visit`));
168
- mutateSWR((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/patient`));
169
- mutateSWR((key) => typeof key === 'string' && key.startsWith(`${fhirBaseUrl}/Encounter`));
170
- mutateAwaitingQueuePatients();
171
- mutateAll();
172
225
  mutate();
173
226
  closeWorkspace();
174
227
  } catch (error) {
228
+ let errorMessage = t('disposeUnknownError', 'An unknown error occurred');
229
+ if (error?.message) {
230
+ errorMessage = error.message;
231
+ } else if (error?.responseBody?.error?.message) {
232
+ errorMessage = error.responseBody.error.message.replace(/\[|\]/g, '');
233
+ } else if (error?.responseBody?.error?.globalErrors) {
234
+ errorMessage = error.responseBody.error.globalErrors[0]?.message || errorMessage;
235
+ }
236
+
237
+ setSubmissionError(errorMessage);
175
238
  showSnackbar({
176
- title: t('errorDisposingPatient', 'Error disposing patient'),
177
- subtitle: error?.message || t('unknownError', 'Unknown error occurred'),
239
+ title: t('disposedError', 'Dispose Error'),
240
+ subtitle: errorMessage,
178
241
  kind: 'error',
179
242
  isLowContrast: true,
180
243
  });
181
244
  }
182
245
  };
183
246
 
184
- if (isLoadingEmrConfiguration || !personAttributes || !activeVisit) {
185
- return <InlineLoading status="active" iconDescription="Loading" description="Loading ..." />;
247
+ if (isLoadingEmrConfiguration || isLoadingAttributes || !activeVisit) {
248
+ return <InlineLoading status="active" iconDescription="Loading" description={t('loading', 'Loading...')} />;
186
249
  }
187
250
 
188
251
  return (
189
252
  <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
190
253
  <div className={styles.formContainer}>
254
+ {isLoadingBills && (
255
+ <InlineLoading
256
+ status="active"
257
+ iconDescription="Loading"
258
+ description={t('loadingBills', 'Loading bills...')}
259
+ />
260
+ )}
261
+
262
+ {isDischargeBlocked && (
263
+ <InlineNotification
264
+ kind="warning"
265
+ title={t('warningMsg', 'Warning')}
266
+ subtitle={blockingMessage}
267
+ lowContrast={true}
268
+ className={styles.blockingNotification}
269
+ />
270
+ )}
271
+
272
+ {submissionError && (
273
+ <InlineNotification
274
+ kind="error"
275
+ title={t('error', 'Error')}
276
+ subtitle={submissionError}
277
+ lowContrast={true}
278
+ className={styles.errorNotification}
279
+ />
280
+ )}
281
+
191
282
  <Stack gap={3}>
192
283
  <DeceasedInfo patientUuid={patientUuid} />
193
- {isDischargeBlocked && (
194
- <InlineNotification
195
- kind="error"
196
- title={t('disposeBlocked', 'Dispose Blocked')}
197
- subtitle={blockingMessage}
198
- lowContrast={true}
199
- className={styles.blockingNotification}
200
- />
201
- )}
284
+
202
285
  <ResponsiveWrapper>
203
286
  <div className={styles.dateTimePickerContainer}>
204
287
  <Column>
@@ -377,6 +460,7 @@ const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid,
377
460
  />
378
461
  </Column>
379
462
  </ResponsiveWrapper>
463
+
380
464
  <ResponsiveWrapper>
381
465
  <Column>
382
466
  <ExtensionSlot
@@ -390,6 +474,7 @@ const DisposeForm: React.FC<DisposeFormProps> = ({ closeWorkspace, patientUuid,
390
474
  </ResponsiveWrapper>
391
475
  </Stack>
392
476
  </div>
477
+
393
478
  <ButtonSet
394
479
  className={classNames({
395
480
  [styles.tablet]: isTablet,
@@ -18,7 +18,7 @@ const HomeViewComponent: React.FC = () => {
18
18
  admissionLocation,
19
19
  isLoading: isLoadingAdmission,
20
20
  error: admissionError,
21
- mutate,
21
+ mutate: mutateAdmissionLocation,
22
22
  } = useMortuaryAdmissionLocation(selectedLocation);
23
23
 
24
24
  const {
@@ -34,6 +34,11 @@ const HomeViewComponent: React.FC = () => {
34
34
  mutateAll,
35
35
  } = useAwaitingQueuePatients(admissionLocation);
36
36
 
37
+ const mutateAllData = React.useCallback(() => {
38
+ mutateAdmissionLocation();
39
+ mutateAll();
40
+ }, [mutateAdmissionLocation, mutateAll]);
41
+
37
42
  const locationItems = React.useMemo(() => {
38
43
  return locations.map((location) => ({
39
44
  id: location.ward.uuid,
@@ -76,7 +81,7 @@ const HomeViewComponent: React.FC = () => {
76
81
  locationError={locationError}
77
82
  admissionError={admissionError}
78
83
  onLocationChange={handleLocationChange}
79
- mutate={mutate}
84
+ mutate={mutateAllData}
80
85
  dischargedPatients={dischargedPatients}
81
86
  isLoadingDischarge={isLoadingDischarge}
82
87
  />
@@ -24,7 +24,7 @@ interface MortuaryApiResponse {
24
24
 
25
25
  export const useAwaitingQueuePatients = (admissionLocation?: MortuaryLocationResponse) => {
26
26
  const { morgueDischargeEncounterTypeUuid } = useConfig<ConfigObject>();
27
- const [currPageSize, setCurrPageSize] = useState(10);
27
+ const [currPageSize, setCurrPageSize] = useState(100);
28
28
 
29
29
  const customRepresentation =
30
30
  'custom:(uuid,display,identifiers:(identifier,uuid,preferred,location:(uuid,name)),person:(uuid,display,gender,birthdate,dead,age,deathDate,causeOfDeath:(uuid,display),preferredAddress:(uuid,stateProvince,countyDistrict,address4)))';
@@ -153,7 +153,6 @@ const CustomContentSwitcher: React.FC<CustomContentSwitcherProps> = ({
153
153
  },
154
154
  [
155
155
  selectedView,
156
- t,
157
156
  awaitingQueueDeceasedPatients,
158
157
  isLoading,
159
158
  admissionLocation,
@@ -358,10 +358,7 @@ export interface PatientInfo {
358
358
  };
359
359
  };
360
360
  uuid: string;
361
- attributes: {
362
- uuid: string;
363
- display: string;
364
- }[];
361
+ attributes: Attribute[];
365
362
  }
366
363
 
367
364
  export interface UseVisitQueueEntries {
@@ -87,42 +87,26 @@ export function parseDisplayText(displayText: string): { name: string; openmrsId
87
87
  }
88
88
  }
89
89
 
90
- export const patientInfoSchema = z.object({
91
- dateOfAdmission: z
92
- .date({ coerce: true })
93
- .refine((date) => !!date, 'Date of admission is required')
94
- .refine((date) => date <= new Date(), 'Date of admission cannot be in the future'),
95
- timeOfDeath: z.string().nonempty('Time of death is required'),
96
- period: z
97
- .string()
98
- .nonempty('AM/PM is required')
99
- .regex(/^(AM|PM)$/i, 'Invalid period'),
100
- tagNumber: z.string().nonempty('Tag number is required'),
101
- obNumber: z.string().optional(),
102
- policeName: z.string().optional(),
103
- policeIDNo: z.string().optional(),
104
- dischargeArea: z.string().optional(),
105
- visitType: z.string().uuid('invalid visit type'),
106
- availableCompartment: z.number({ coerce: true }),
107
- services: z.array(z.string().uuid('invalid service')).nonempty('Must select one service'),
108
- paymentMethod: z.string().uuid('invalid payment method'),
109
- insuranceScheme: z.string().optional(),
110
- policyNumber: z.string().optional(),
111
- });
112
-
113
- export const dischargeSchema = z.object({
114
- dateOfDischarge: z.date({ coerce: true }).refine((date) => !!date, 'Date of discharge is required'),
115
- timeOfDischarge: z.string().nonempty('Time of discharge is required'),
116
- period: z
117
- .string()
118
- .nonempty('AM/PM is required')
119
- .regex(/^(AM|PM)$/i, 'Invalid period'),
120
- burialPermitNumber: z.string().nonempty('Burial Permit Number is required'),
121
- nextOfKinNames: z.string().nonempty('Next of kin names is required'),
122
- relationshipType: z.string().nonempty('Next of kin relationship is required'),
123
- nextOfKinAddress: z.string().nonempty('Next of kin address is required'),
124
- nextOfKinContact: z
125
- .string()
126
- .regex(/^\d{10}$/, 'Phone number must be exactly 10 digits')
127
- .nonempty('Next of kin phone number is required'),
128
- });
90
+ export const parseDischargeDateTime = (data: {
91
+ dateOfDischarge: string | Date;
92
+ timeOfDischarge?: string;
93
+ period?: string;
94
+ }) => {
95
+ const dischargeDateTime =
96
+ typeof data.dateOfDischarge === 'string' ? new Date(data.dateOfDischarge) : new Date(data.dateOfDischarge);
97
+
98
+ if (data.timeOfDischarge && data.period) {
99
+ const [hours, minutes] = data.timeOfDischarge.split(':');
100
+ let hour = parseInt(hours, 10);
101
+
102
+ if (data.period === 'PM' && hour !== 12) {
103
+ hour += 12;
104
+ } else if (data.period === 'AM' && hour === 12) {
105
+ hour = 0;
106
+ }
107
+
108
+ dischargeDateTime.setHours(hour, parseInt(minutes, 10), 0, 0);
109
+ }
110
+
111
+ return dischargeDateTime;
112
+ };