@kenyaemr/esm-morgue-app 5.4.2-pre.2344 → 5.4.2-pre.2349

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 (122) hide show
  1. package/.turbo/turbo-build.log +21 -26
  2. package/dist/162.js +2 -0
  3. package/dist/162.js.map +1 -0
  4. package/dist/197.js +1 -1
  5. package/dist/221.js +1 -1
  6. package/dist/221.js.map +1 -1
  7. package/dist/293.js +1 -1
  8. package/dist/294.js +1 -1
  9. package/dist/300.js +1 -1
  10. package/dist/351.js +1 -0
  11. package/dist/351.js.map +1 -0
  12. package/dist/38.js +1 -1
  13. package/dist/38.js.map +1 -1
  14. package/dist/404.js +1 -0
  15. package/dist/404.js.map +1 -0
  16. package/dist/441.js +1 -0
  17. package/dist/441.js.map +1 -0
  18. package/dist/467.js +1 -1
  19. package/dist/467.js.map +1 -1
  20. package/dist/500.js +1 -0
  21. package/dist/500.js.map +1 -0
  22. package/dist/570.js +1 -0
  23. package/dist/570.js.map +1 -0
  24. package/dist/608.js +2 -0
  25. package/dist/608.js.map +1 -0
  26. package/dist/611.js +2 -0
  27. package/dist/611.js.map +1 -0
  28. package/dist/632.js +2 -1
  29. package/dist/632.js.map +1 -1
  30. package/dist/653.js +1 -1
  31. package/dist/653.js.map +1 -1
  32. package/dist/657.js +1 -0
  33. package/dist/657.js.map +1 -0
  34. package/dist/805.js +1 -1
  35. package/dist/805.js.map +1 -1
  36. package/dist/814.js +2 -0
  37. package/dist/814.js.LICENSE.txt +5 -0
  38. package/dist/814.js.map +1 -0
  39. package/dist/824.js +1 -1
  40. package/dist/824.js.map +1 -1
  41. package/dist/845.js +1 -0
  42. package/dist/845.js.map +1 -0
  43. package/dist/888.js +1 -0
  44. package/dist/888.js.map +1 -0
  45. package/dist/899.js +1 -0
  46. package/dist/899.js.map +1 -0
  47. package/dist/918.js +1 -1
  48. package/dist/918.js.map +1 -1
  49. package/dist/990.js +1 -0
  50. package/dist/990.js.map +1 -0
  51. package/dist/kenyaemr-esm-morgue-app.js +1 -1
  52. package/dist/kenyaemr-esm-morgue-app.js.buildmanifest.json +258 -184
  53. package/dist/kenyaemr-esm-morgue-app.js.map +1 -1
  54. package/dist/main.js +1 -1
  55. package/dist/main.js.LICENSE.txt +0 -6
  56. package/dist/main.js.map +1 -1
  57. package/dist/routes.json +1 -1
  58. package/package.json +1 -1
  59. package/src/bed/bed.component.tsx +63 -134
  60. package/src/bed/components/deceased-patient-card-header.component.tsx +73 -0
  61. package/src/bed/components/deceased-patient-info.component.tsx +47 -0
  62. package/src/bed/components/deceased-patient-status-footer.component.tsx +43 -0
  63. package/src/bed-layout/admitted/admitted-bed-layout.component.tsx +175 -96
  64. package/src/bed-layout/awaiting/awaiting-bed-layout.component.tsx +103 -36
  65. package/src/bed-layout/bed-layout.scss +4 -0
  66. package/src/bed-layout/discharged/discharged-bed-layout.component.tsx +131 -73
  67. package/src/bed-linelist-view/admitted/admitted-bed-linelist-view.component.tsx +182 -134
  68. package/src/bed-linelist-view/awaiting/awaiting-bed-linelist-view.component.tsx +115 -71
  69. package/src/bed-linelist-view/discharged/discharged-bed-line-view.component.tsx +181 -109
  70. package/src/config-schema.ts +140 -4
  71. package/src/context/deceased-person-context.tsx +33 -0
  72. package/src/extension/actionButton.component.tsx +1 -1
  73. package/src/forms/admit-deceased-person-workspace/admit-deceased-person.resource.ts +84 -166
  74. package/src/forms/admit-deceased-person-workspace/admit-deceased-person.scss +14 -0
  75. package/src/forms/admit-deceased-person-workspace/admit-deceased-person.workspace.tsx +504 -334
  76. package/src/forms/discharge-deceased-person-workspace/discharge-body.resource.ts +0 -1
  77. package/src/forms/discharge-deceased-person-workspace/discharge-body.scss +15 -0
  78. package/src/forms/discharge-deceased-person-workspace/discharge-body.workspace.tsx +303 -244
  79. package/src/helpers/expression-helper.ts +122 -0
  80. package/src/home/home.component.tsx +23 -4
  81. package/src/index.ts +0 -2
  82. package/src/metrics/metrics-card.component.tsx +2 -2
  83. package/src/routes.json +0 -6
  84. package/src/schemas/index.ts +243 -51
  85. package/src/summary/summary.component.tsx +16 -9
  86. package/src/switcher/content-switcher.component.tsx +61 -35
  87. package/src/switcher/content-switcher.scss +13 -0
  88. package/src/types/index.ts +43 -1
  89. package/translations/am.json +16 -6
  90. package/translations/en.json +16 -6
  91. package/translations/sw.json +16 -6
  92. package/dist/347.js +0 -1
  93. package/dist/347.js.map +0 -1
  94. package/dist/373.js +0 -2
  95. package/dist/373.js.map +0 -1
  96. package/dist/398.js +0 -1
  97. package/dist/398.js.map +0 -1
  98. package/dist/410.js +0 -1
  99. package/dist/410.js.map +0 -1
  100. package/dist/429.js +0 -2
  101. package/dist/429.js.map +0 -1
  102. package/dist/579.js +0 -2
  103. package/dist/579.js.map +0 -1
  104. package/dist/619.js +0 -1
  105. package/dist/619.js.map +0 -1
  106. package/dist/633.js +0 -1
  107. package/dist/633.js.map +0 -1
  108. package/dist/712.js +0 -1
  109. package/dist/712.js.map +0 -1
  110. package/dist/713.js +0 -1
  111. package/dist/713.js.map +0 -1
  112. package/dist/723.js +0 -1
  113. package/dist/723.js.map +0 -1
  114. package/dist/989.js +0 -2
  115. package/dist/989.js.map +0 -1
  116. package/src/forms/dispose-deceased-person-workspace/dispose-deceased-person.resource.ts +0 -18
  117. package/src/forms/dispose-deceased-person-workspace/dispose-deceased-person.scss +0 -84
  118. package/src/forms/dispose-deceased-person-workspace/dispose-deceased-person.workspace.tsx +0 -505
  119. /package/dist/{989.js.LICENSE.txt → 162.js.LICENSE.txt} +0 -0
  120. /package/dist/{373.js.LICENSE.txt → 608.js.LICENSE.txt} +0 -0
  121. /package/dist/{429.js.LICENSE.txt → 611.js.LICENSE.txt} +0 -0
  122. /package/dist/{579.js.LICENSE.txt → 632.js.LICENSE.txt} +0 -0
@@ -1,3 +1,5 @@
1
+ import { EnhancedPatient, MortuaryPatient, Patient } from '../types';
2
+
1
3
  /**
2
4
  * Converts a string to uppercase.
3
5
  * @param {string} str - The string to convert.
@@ -9,3 +11,123 @@ export const toUpperCase = (str) => {
9
11
  } // Handle empty or undefined input
10
12
  return str.toUpperCase();
11
13
  };
14
+
15
+ /**
16
+ * Transforms a MortuaryPatient object into an EnhancedPatient.
17
+ * @param {MortuaryPatient} mortuaryPatient - The MortuaryPatient object to transform.
18
+ * @return {EnhancedPatient} - The transformed EnhancedPatient.
19
+ */
20
+ export const transformMortuaryPatient = (mortuaryPatient: MortuaryPatient): EnhancedPatient => ({
21
+ uuid: mortuaryPatient.person?.person?.uuid || mortuaryPatient.person?.uuid,
22
+ person: {
23
+ display: mortuaryPatient.person?.person?.display || mortuaryPatient.person?.display,
24
+ gender: mortuaryPatient.person?.person?.gender || mortuaryPatient.person?.gender,
25
+ age: mortuaryPatient.person?.person?.age || mortuaryPatient.person?.age,
26
+ deathDate: mortuaryPatient.person?.person?.deathDate || mortuaryPatient.person?.deathDate,
27
+ causeOfDeath: mortuaryPatient.person?.person?.causeOfDeath || mortuaryPatient.person?.causeOfDeath,
28
+ },
29
+ originalMortuaryPatient: mortuaryPatient,
30
+ });
31
+
32
+ /**
33
+ * Transforms a Patient object into an EnhancedPatient with bed information.
34
+ * @param {Patient} patient - The Patient object to transform.
35
+ * @param {Object} bedInfo - The bed information associated with the patient.
36
+ * @param {string} bedInfo.bedNumber - The bed number assigned to the patient.
37
+ * @param {number} bedInfo.bedId - The unique identifier for the bed.
38
+ * @param {string} [bedInfo.bedType] - The type of bed assigned, if available.
39
+ * @return {EnhancedPatient} - The transformed EnhancedPatient with bed details.
40
+ */
41
+
42
+ export const transformAdmittedPatient = (
43
+ patient: Patient,
44
+ bedInfo: { bedNumber: string; bedId: number; bedType?: string },
45
+ ): EnhancedPatient => ({
46
+ uuid: patient.uuid,
47
+ person: {
48
+ display: patient.person?.display,
49
+ gender: patient.person?.gender,
50
+ age: patient.person?.age,
51
+ deathDate: patient.person?.deathDate,
52
+ causeOfDeath: patient.person?.causeOfDeath,
53
+ },
54
+ bedInfo,
55
+ originalPatient: patient,
56
+ });
57
+
58
+ /**
59
+ * Transforms a Patient object into an EnhancedPatient with a discharged status and optional encounter date.
60
+ * @param {Patient} patient - The Patient object to transform.
61
+ * @param {string} [encounterDate] - The date of the encounter for the patient's discharge.
62
+ * @return {EnhancedPatient} - The transformed EnhancedPatient with a discharged status.
63
+ */
64
+ export const transformDischargedPatient = (patient: Patient, encounterDate?: string): EnhancedPatient => ({
65
+ uuid: patient.uuid,
66
+ person: {
67
+ display: patient.person?.display,
68
+ gender: patient.person?.gender,
69
+ age: patient.person?.age,
70
+ deathDate: patient.person?.deathDate,
71
+ causeOfDeath: patient.person?.causeOfDeath,
72
+ },
73
+ isDischarged: true,
74
+ encounterDate,
75
+ originalPatient: patient,
76
+ });
77
+
78
+ /**
79
+ * Retrieves the original MortuaryPatient or Patient object from an EnhancedPatient,
80
+ * if present. If neither is available, returns null.
81
+ * @param {EnhancedPatient} enhancedPatient - The EnhancedPatient object to extract the original patient from.
82
+ * @return {MortuaryPatient | Patient | null} - The original MortuaryPatient or Patient if available, or null.
83
+ */
84
+ export const getOriginalPatient = (enhancedPatient: EnhancedPatient): MortuaryPatient | Patient | null => {
85
+ if (enhancedPatient.originalMortuaryPatient) {
86
+ return enhancedPatient.originalMortuaryPatient;
87
+ }
88
+ if (enhancedPatient.originalPatient) {
89
+ return enhancedPatient.originalPatient;
90
+ }
91
+ return null;
92
+ };
93
+
94
+ /**
95
+ * Extracts the original Patient object from an EnhancedPatient, if available.
96
+ * @param {EnhancedPatient} enhancedPatient - The EnhancedPatient object to extract the original patient from.
97
+ * @return {Patient | null} - The original Patient if available, or null.
98
+ * If the EnhancedPatient has an originalPatient property, that will be returned directly.
99
+ * If the EnhancedPatient has an originalMortuaryPatient property with a patient property, that will be returned.
100
+ * If neither of the above conditions are met, a partial Patient object will be created from the EnhancedPatient data.
101
+ * Note that this partial Patient object will not have the full set of properties a real Patient object would have.
102
+ */
103
+ export const extractPatientFromEnhanced = (enhancedPatient: EnhancedPatient): Patient | null => {
104
+ if (enhancedPatient.originalPatient) {
105
+ return enhancedPatient.originalPatient;
106
+ }
107
+
108
+ if (enhancedPatient.originalMortuaryPatient?.patient) {
109
+ return enhancedPatient.originalMortuaryPatient.patient;
110
+ }
111
+ return {
112
+ uuid: enhancedPatient.uuid,
113
+ display: enhancedPatient.person.display,
114
+ identifiers: [],
115
+ person: {
116
+ uuid: enhancedPatient.uuid,
117
+ display: enhancedPatient.person.display,
118
+ gender: enhancedPatient.person.gender,
119
+ age: enhancedPatient.person.age,
120
+ birthdate: '',
121
+ birthdateEstimated: false,
122
+ dead: true,
123
+ deathDate: enhancedPatient.person.deathDate,
124
+ causeOfDeath: enhancedPatient.person.causeOfDeath || { uuid: '', display: '' },
125
+ preferredAddress: { uuid: '', display: '' },
126
+ attributes: [],
127
+ voided: false,
128
+ birthtime: null,
129
+ deathdateEstimated: false,
130
+ identifiers: [],
131
+ },
132
+ } as Patient;
133
+ };
@@ -6,10 +6,10 @@ import Summary from '../summary/summary.component';
6
6
  import CustomContentSwitcher from '../switcher/content-switcher.component';
7
7
  import { useAwaitingPatients, useAwaitingQueuePatients } from './home.resource';
8
8
  import { useLocation, useMortuaryAdmissionLocation } from '../bed-layout/bed-layout.resource';
9
+ import { DataTableSkeleton } from '@carbon/react';
9
10
 
10
11
  const HomeViewComponent: React.FC = () => {
11
12
  const { t } = useTranslation();
12
-
13
13
  const [selectedLocation, setSelectedLocation] = React.useState<string>('');
14
14
 
15
15
  const { locations, isLoading: isLoadingLocation, error: locationError } = useLocation();
@@ -34,6 +34,24 @@ const HomeViewComponent: React.FC = () => {
34
34
  mutateAll,
35
35
  } = useAwaitingQueuePatients(admissionLocation);
36
36
 
37
+ const isInitialDataLoading = React.useMemo(() => {
38
+ return (
39
+ isLoadingLocation ||
40
+ isLoadingAdmission ||
41
+ isLoadingAwaitingQueuePatients ||
42
+ isLoadingDischarge ||
43
+ (!selectedLocation && locationItems?.length === 0)
44
+ );
45
+ }, [isLoadingLocation, isLoadingAdmission, isLoadingAwaitingQueuePatients, isLoadingDischarge, selectedLocation]);
46
+
47
+ const [hasInitialDataLoaded, setHasInitialDataLoaded] = React.useState(false);
48
+
49
+ React.useEffect(() => {
50
+ if (!isInitialDataLoading && selectedLocation) {
51
+ setHasInitialDataLoaded(true);
52
+ }
53
+ }, [isInitialDataLoading, selectedLocation]);
54
+
37
55
  const mutateAllData = React.useCallback(() => {
38
56
  mutateAdmissionLocation();
39
57
  mutateAll();
@@ -48,7 +66,7 @@ const HomeViewComponent: React.FC = () => {
48
66
  }, [locations]);
49
67
 
50
68
  React.useEffect(() => {
51
- if (locationItems.length === 1 && !selectedLocation) {
69
+ if (locationItems?.length === 1 && !selectedLocation) {
52
70
  setSelectedLocation(locationItems[0].id);
53
71
  }
54
72
  }, [locationItems, selectedLocation]);
@@ -58,6 +76,7 @@ const HomeViewComponent: React.FC = () => {
58
76
  const handleLocationChange = React.useCallback((data: { selectedItem: { id: string; text: string } }) => {
59
77
  if (data.selectedItem) {
60
78
  setSelectedLocation(data.selectedItem.id);
79
+ setHasInitialDataLoaded(false);
61
80
  }
62
81
  }, []);
63
82
 
@@ -68,11 +87,11 @@ const HomeViewComponent: React.FC = () => {
68
87
  awaitingQueueCount={trulyAwaitingPatients.length}
69
88
  admittedCount={admittedPatients.length}
70
89
  dischargedCount={dischargedPatientsCount}
71
- isLoading={isLoadingAll}
90
+ isLoading={isInitialDataLoading && !hasInitialDataLoaded}
72
91
  />
73
92
  <CustomContentSwitcher
74
93
  awaitingQueueDeceasedPatients={trulyAwaitingPatients}
75
- isLoading={isLoadingAwaitingQueuePatients}
94
+ isLoading={isInitialDataLoading && !hasInitialDataLoaded}
76
95
  locationItems={locationItems}
77
96
  selectedLocation={selectedLocation}
78
97
  admissionLocation={admissionLocation}
package/src/index.ts CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  import { configSchema } from './config-schema';
9
9
  import { createLeftPanelLink } from './left-panel/morgue-left-panel-link.component';
10
10
  import FormEntryWorkspace from './forms/form-entry-workspace/form-entry-workspace.workspace';
11
- import DisposeForm from './forms/dispose-deceased-person-workspace/dispose-deceased-person.workspace';
12
11
  import PrintPostMortemOverflowMenuItem from './extension/overflow-menu-item-postmortem/print-postmorterm-report.component';
13
12
  const moduleName = '@kenyaemr/esm-morgue-app';
14
13
 
@@ -57,7 +56,6 @@ export const dischargeBodyForm = getAsyncLifecycle(
57
56
  options,
58
57
  );
59
58
  export const mortuaryFormEntry = getSyncLifecycle(FormEntryWorkspace, options);
60
- export const disposeDeceasedPersonForm = getSyncLifecycle(DisposeForm, options);
61
59
  export const mortuaryChartView = getAsyncLifecycle(() => import('./view-details/main/main.component'), options);
62
60
  export const printConfirmationModal = getAsyncLifecycle(
63
61
  () => import('./modals/mortuary-gate-pass/print-preview-confirmation.modal'),
@@ -3,9 +3,9 @@ import { Layer, Tile } from '@carbon/react';
3
3
  import styles from './metrics-card.scss';
4
4
 
5
5
  interface MetricsCardProps {
6
- label: string;
6
+ label: string | React.ReactNode;
7
7
  value: number | string | React.ReactNode;
8
- headerLabel: string;
8
+ headerLabel: string | React.ReactNode;
9
9
  children?: React.ReactNode;
10
10
  }
11
11
 
package/src/routes.json CHANGED
@@ -87,12 +87,6 @@
87
87
  "width": "extra-wide",
88
88
  "canMaximize": true,
89
89
  "canHide": true
90
- },
91
- {
92
- "name": "dispose-deceased-person-form",
93
- "component": "disposeDeceasedPersonForm",
94
- "title": "Dispose Deceased Person",
95
- "type": "other-form"
96
90
  }
97
91
  ],
98
92
  "modals": [
@@ -1,66 +1,258 @@
1
1
  import { z } from 'zod';
2
+ import { ConfigObject } from '../config-schema';
2
3
 
3
- export const deceasedPatientAdmitSchema = z.object({
4
- dateOfAdmission: z
5
- .date({ coerce: true })
6
- .refine((date) => !!date, 'Date of admission is required')
7
- .refine((date) => date <= new Date(), 'Date of admission cannot be in the future'),
8
- timeOfDeath: z.string().nonempty('Time of death is required'),
9
- period: z
10
- .string()
11
- .nonempty('AM/PM is required')
12
- .regex(/^(AM|PM)$/i, 'Invalid period'),
13
- tagNumber: z.string().nonempty('Tag number is required'),
14
- obNumber: z.string().optional(),
15
- policeName: z.string().optional(),
16
- policeIDNo: z.string().optional(),
17
- dischargeArea: z.string().optional(),
18
- visitType: z.string().uuid('invalid visit type'),
19
- availableCompartment: z
20
- .union([z.number(), z.string()])
21
- .refine((val) => {
22
- if (typeof val === 'string') {
23
- return val.length > 0 && !isNaN(Number(val)) && Number(val) > 0;
4
+ const getConceptCodes = (config: ConfigObject) => ({
5
+ hospitalDeathCodes: [
6
+ config.locationOfDeathTypesList.find((item) => item.label === 'InPatient')?.concept,
7
+ config.locationOfDeathTypesList.find((item) => item.label === 'Outpatient')?.concept,
8
+ config.locationOfDeathTypesList.find((item) => item.label === 'Dead on arrival')?.concept,
9
+ ].filter(Boolean) as string[],
10
+ homeDeathCode: config.locationOfDeathTypesList.find((item) => item.label === 'Home')?.concept,
11
+ policeCaseCode: config.locationOfDeathTypesList.find((item) => item.label === 'Unknown (Police case)')?.concept,
12
+ yesConfirmationCode: config.deathConfirmationTypes.find((type) => type.label === 'Yes')?.concept,
13
+ noConfirmationCode: config.deathConfirmationTypes.find((type) => type.label === 'No')?.concept,
14
+ bodyEmbalmmentCode: config.deadBodyPreservationTypeUuid.find((type) => type.label === 'Body embalment')?.concept,
15
+ });
16
+
17
+ export const createDeceasedPatientAdmitSchema = (config?: ConfigObject) => {
18
+ const baseSchema = z.object({
19
+ dateOfAdmission: z
20
+ .date({ coerce: true })
21
+ .refine((date) => !!date, 'Date of admission is required')
22
+ .refine((date) => date <= new Date(), 'Date of admission cannot be in the future'),
23
+ placeOfDeath: z.string().min(1, 'Place of death is required'),
24
+ tagNumber: z.string().min(1, 'Tag number is required'),
25
+ visitType: z.string().uuid('Invalid visit type'),
26
+ availableCompartment: z
27
+ .union([z.number(), z.string()])
28
+ .refine((val) => {
29
+ if (typeof val === 'string') {
30
+ return val.length > 0 && !isNaN(Number(val)) && Number(val) > 0;
31
+ }
32
+ return typeof val === 'number' && !isNaN(val) && val > 0;
33
+ }, 'Please select a valid compartment')
34
+ .transform((val) => (typeof val === 'string' ? Number(val) : val)),
35
+ paymentMethod: z.string().uuid('Invalid payment method'),
36
+ services: z.array(z.string().uuid('Invalid service')).min(1, 'Must select at least one service'),
37
+
38
+ deathConfirmed: z.string().min(1, 'Death confirmation is required'),
39
+ autopsyPermission: z.string().min(1, 'Autopsy permission is required'),
40
+
41
+ dateOfDeath: z.date({ coerce: true }).optional(),
42
+ timeOfDeath: z.string().optional(),
43
+ period: z.string().optional(),
44
+ deathNotificationNumber: z.string().optional(),
45
+ attendingClinician: z.string().optional(),
46
+ doctorsRemarks: z.string().optional(),
47
+ causeOfDeath: z.string().optional(),
48
+ deadBodyPreservation: z.string().optional(),
49
+ bodyEmbalmentType: z.string().optional(),
50
+ obNumber: z.string().optional(),
51
+ policeName: z.string().optional(),
52
+ policeIDNo: z.string().optional(),
53
+ insuranceScheme: z.string().optional(),
54
+ policyNumber: z.string().optional(),
55
+ });
56
+
57
+ if (!config) {
58
+ return baseSchema;
59
+ }
60
+
61
+ return baseSchema.superRefine((data, ctx) => {
62
+ const conceptCodes = getConceptCodes(config);
63
+
64
+ const isHospitalDeath = conceptCodes.hospitalDeathCodes.includes(data.placeOfDeath);
65
+ const isHomeDeath = data.placeOfDeath === conceptCodes.homeDeathCode;
66
+ const isPoliceCaseDeath = data.placeOfDeath === conceptCodes.policeCaseCode;
67
+
68
+ if (data.visitType !== config.morgueVisitTypeUuid) {
69
+ ctx.addIssue({
70
+ code: z.ZodIssueCode.custom,
71
+ message: 'Invalid visit type for mortuary',
72
+ path: ['visitType'],
73
+ });
74
+ }
75
+
76
+ const validConfirmationCodes = [conceptCodes.yesConfirmationCode, conceptCodes.noConfirmationCode].filter(Boolean);
77
+ if (validConfirmationCodes.length > 0 && !validConfirmationCodes.includes(data.deathConfirmed)) {
78
+ ctx.addIssue({
79
+ code: z.ZodIssueCode.custom,
80
+ message: 'Invalid death confirmation value',
81
+ path: ['deathConfirmed'],
82
+ });
83
+ }
84
+
85
+ if (validConfirmationCodes.length > 0 && !validConfirmationCodes.includes(data.autopsyPermission)) {
86
+ ctx.addIssue({
87
+ code: z.ZodIssueCode.custom,
88
+ message: 'Invalid autopsy permission value',
89
+ path: ['autopsyPermission'],
90
+ });
91
+ }
92
+
93
+ if (!isHomeDeath && data.placeOfDeath !== '') {
94
+ if (!data.dateOfDeath) {
95
+ ctx.addIssue({
96
+ code: z.ZodIssueCode.custom,
97
+ message: 'Date of death is required for non-home deaths',
98
+ path: ['dateOfDeath'],
99
+ });
100
+ }
101
+ if (!data.timeOfDeath || data.timeOfDeath.trim() === '') {
102
+ ctx.addIssue({
103
+ code: z.ZodIssueCode.custom,
104
+ message: 'Time of death is required for non-home deaths',
105
+ path: ['timeOfDeath'],
106
+ });
107
+ }
108
+ if (!data.period || data.period.trim() === '') {
109
+ ctx.addIssue({
110
+ code: z.ZodIssueCode.custom,
111
+ message: 'AM/PM is required for non-home deaths',
112
+ path: ['period'],
113
+ });
114
+ }
115
+ }
116
+
117
+ if (isHospitalDeath) {
118
+ if (!data.attendingClinician || data.attendingClinician.trim() === '') {
119
+ ctx.addIssue({
120
+ code: z.ZodIssueCode.custom,
121
+ message: 'Attending clinician is required for hospital deaths',
122
+ path: ['attendingClinician'],
123
+ });
124
+ }
125
+ if (!data.doctorsRemarks || data.doctorsRemarks.trim() === '') {
126
+ ctx.addIssue({
127
+ code: z.ZodIssueCode.custom,
128
+ message: "Doctor's remarks are required for hospital deaths",
129
+ path: ['doctorsRemarks'],
130
+ });
131
+ }
132
+ }
133
+
134
+ if (isPoliceCaseDeath) {
135
+ if (!data.policeName || data.policeName.trim() === '') {
136
+ ctx.addIssue({
137
+ code: z.ZodIssueCode.custom,
138
+ message: 'Police name is required for police cases',
139
+ path: ['policeName'],
140
+ });
141
+ }
142
+ if (!data.policeIDNo || data.policeIDNo.trim() === '') {
143
+ ctx.addIssue({
144
+ code: z.ZodIssueCode.custom,
145
+ message: 'Police ID number is required for police cases',
146
+ path: ['policeIDNo'],
147
+ });
148
+ }
149
+ if (!data.obNumber || data.obNumber.trim() === '') {
150
+ ctx.addIssue({
151
+ code: z.ZodIssueCode.custom,
152
+ message: 'OB number is required for police cases',
153
+ path: ['obNumber'],
154
+ });
24
155
  }
25
- return typeof val === 'number' && !isNaN(val) && val > 0;
26
- }, 'Please select a valid compartment')
27
- .transform((val) => (typeof val === 'string' ? Number(val) : val)),
28
- services: z.array(z.string().uuid('invalid service')).nonempty('Must select one service'),
29
- paymentMethod: z.string().uuid('invalid payment method'),
30
- insuranceScheme: z.string().optional(),
31
- policyNumber: z.string().optional(),
156
+ }
157
+
158
+ if (data.deadBodyPreservation) {
159
+ const validPreservationTypes = config.deadBodyPreservationTypeUuid.map((type) => type.concept);
160
+ if (!validPreservationTypes.includes(data.deadBodyPreservation)) {
161
+ ctx.addIssue({
162
+ code: z.ZodIssueCode.custom,
163
+ message: 'Invalid body preservation type',
164
+ path: ['deadBodyPreservation'],
165
+ });
166
+ }
167
+
168
+ if (data.deadBodyPreservation === conceptCodes.bodyEmbalmmentCode) {
169
+ if (!data.bodyEmbalmentType || data.bodyEmbalmentType.trim() === '') {
170
+ ctx.addIssue({
171
+ code: z.ZodIssueCode.custom,
172
+ message: 'Body embalment type is required when body embalment is selected',
173
+ path: ['bodyEmbalmentType'],
174
+ });
175
+ } else {
176
+ const validEmbalmmentTypes = config.bodyEmbalmmentTypesUuid.map((type) => type.concept);
177
+ if (!validEmbalmmentTypes.includes(data.bodyEmbalmentType)) {
178
+ ctx.addIssue({
179
+ code: z.ZodIssueCode.custom,
180
+ message: 'Invalid body embalment type',
181
+ path: ['bodyEmbalmentType'],
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ if (data.paymentMethod === config.insurancepaymentModeUuid) {
189
+ if (!data.insuranceScheme || data.insuranceScheme.trim() === '') {
190
+ ctx.addIssue({
191
+ code: z.ZodIssueCode.custom,
192
+ message: 'Insurance scheme is required when insurance payment method is selected',
193
+ path: ['insuranceScheme'],
194
+ });
195
+ }
196
+ if (!data.policyNumber || data.policyNumber.trim() === '') {
197
+ ctx.addIssue({
198
+ code: z.ZodIssueCode.custom,
199
+ message: 'Policy number is required when insurance is selected',
200
+ path: ['policyNumber'],
201
+ });
202
+ }
203
+ }
204
+ });
205
+ };
206
+
207
+ export const deceasedPatientAdmitSchema = createDeceasedPatientAdmitSchema();
208
+
209
+ const baseDischargeSchema = z.object({
210
+ burialPermitNumber: z.string().min(1, 'Burial Permit Number is required'),
211
+ nextOfKinNames: z.string().min(1, 'Next of kin names is required'),
212
+ relationshipType: z.string().min(1, 'Next of kin relationship is required'),
213
+ nextOfKinNationalId: z.string().min(1, 'Next of kin national ID is required'),
214
+ nextOfKinContact: z
215
+ .string()
216
+ .regex(/^\d{10}$/, 'Phone number must be exactly 10 digits')
217
+ .min(1, 'Next of kin phone number is required'),
32
218
  });
33
- export const dischargeSchema = z.object({
219
+
220
+ const dischargeSchema = baseDischargeSchema.extend({
221
+ dischargeType: z.literal('discharge'),
34
222
  dateOfDischarge: z.date({ coerce: true }).refine((date) => !!date, 'Date of discharge is required'),
35
- timeOfDischarge: z.string().nonempty('Time of discharge is required'),
223
+ timeOfDischarge: z.string().min(1, 'Time of discharge is required'),
36
224
  period: z
37
225
  .string()
38
- .nonempty('AM/PM is required')
226
+ .min(1, 'AM/PM is required')
39
227
  .regex(/^(AM|PM)$/i, 'Invalid period'),
40
- burialPermitNumber: z.string().nonempty('Burial Permit Number is required'),
41
- nextOfKinNames: z.string().nonempty('Next of kin names is required'),
42
- relationshipType: z.string().nonempty('Next of kin relationship is required'),
43
- nextOfKinAddress: z.string().nonempty('Next of kin address is required'),
44
- nextOfKinContact: z
45
- .string()
46
- .regex(/^\d{10}$/, 'Phone number must be exactly 10 digits')
47
- .nonempty('Next of kin phone number is required'),
228
+ dischargeArea: z.string().min(1, 'Discharge Area is required'),
48
229
  });
49
230
 
50
- export const disposeSchema = z.object({
231
+ const transferSchema = baseDischargeSchema.extend({
232
+ dischargeType: z.literal('transfer'),
51
233
  dateOfDischarge: z.date({ coerce: true }).refine((date) => !!date, 'Date of discharge is required'),
52
- timeOfDischarge: z.string().nonempty('Time of discharge is required'),
234
+ timeOfDischarge: z.string().min(1, 'Time of discharge is required'),
53
235
  period: z
54
236
  .string()
55
- .nonempty('AM/PM is required')
237
+ .min(1, 'AM/PM is required')
56
238
  .regex(/^(AM|PM)$/i, 'Invalid period'),
57
- serialNumber: z.string().nonempty('Serial Number is required'),
58
- courtOrderCaseNumber: z.string().nonempty('Court Order Case Number is required'),
59
- nextOfKinNames: z.string().nonempty('Next of kin names is required'),
60
- relationshipType: z.string().nonempty('Next of kin relationship is required'),
61
- nextOfKinAddress: z.string().nonempty('Next of kin address is required'),
62
- nextOfKinContact: z
63
- .string()
64
- .regex(/^\d{10}$/, 'Phone number must be exactly 10 digits')
65
- .nonempty('Next of kin phone number is required'),
239
+ dischargeArea: z.string().min(1, 'Discharge Area is required'),
240
+ receivingArea: z.string().min(1, 'Receiving mortuary is required'),
241
+ reasonForTransfer: z.string().min(1, 'Reason for transfer is required'),
242
+ });
243
+
244
+ const disposeSchema = baseDischargeSchema.extend({
245
+ dischargeType: z.literal('dispose'),
246
+ dischargeArea: z.string().min(1, 'Discharge Area is required'),
247
+ serialNumber: z.string().min(1, 'Serial Number is required'),
248
+ courtOrderCaseNumber: z.string().min(1, 'Court Order Case Number is required'),
66
249
  });
250
+
251
+ export const dischargeFormSchema = z.discriminatedUnion('dischargeType', [
252
+ dischargeSchema,
253
+ transferSchema,
254
+ disposeSchema,
255
+ ]);
256
+
257
+ export type DischargeFormValues = z.infer<typeof dischargeFormSchema>;
258
+ export type DischargeType = 'discharge' | 'transfer' | 'dispose';
@@ -1,5 +1,12 @@
1
1
  import React from 'react';
2
- import { DataTableSkeleton, ContentSwitcher, Switch, InlineLoading } from '@carbon/react';
2
+ import {
3
+ DataTableSkeleton,
4
+ ContentSwitcher,
5
+ Switch,
6
+ InlineLoading,
7
+ SkeletonText,
8
+ RadioButtonSkeleton,
9
+ } from '@carbon/react';
3
10
  import { ArrowRight } from '@carbon/react/icons';
4
11
  import { useTranslation } from 'react-i18next';
5
12
  import { ConfigurableLink, ErrorState } from '@openmrs/esm-framework';
@@ -20,19 +27,19 @@ const Summary: React.FC<SummaryProps> = ({ awaitingQueueCount, admittedCount, di
20
27
  <>
21
28
  <div className={styles.cardContainer}>
22
29
  <MetricsCard
23
- headerLabel={t('awaitingAdmissionHeader', 'Awaiting Admission')}
24
- label={t('totalCount', 'total')}
30
+ headerLabel={isLoading ? <RadioButtonSkeleton /> : t('awaitingAdmissionHeader', 'Awaiting Admission')}
31
+ label={isLoading ? <SkeletonText /> : t('totalCount', 'total')}
25
32
  value={isLoading ? <InlineLoading /> : awaitingQueueCount.toString()}
26
33
  />
27
34
  <MetricsCard
28
- headerLabel={t('admittedHeader', 'Admitted')}
29
- label={t('totalCount', 'total')}
30
- value={admittedCount.toString()}
35
+ headerLabel={isLoading ? <RadioButtonSkeleton /> : t('admittedHeader', 'Admitted')}
36
+ label={isLoading ? <SkeletonText /> : t('totalCount', 'total')}
37
+ value={isLoading ? <InlineLoading /> : admittedCount.toString()}
31
38
  />
32
39
  <MetricsCard
33
- headerLabel={t('dischargedHeader', 'Discharged')}
34
- label={t('totalCount', 'total')}
35
- value={dischargedCount.toString()}
40
+ headerLabel={isLoading ? <RadioButtonSkeleton /> : t('dischargedHeader', 'Discharged')}
41
+ label={isLoading ? <SkeletonText /> : t('totalCount', 'total')}
42
+ value={isLoading ? <InlineLoading /> : dischargedCount.toString()}
36
43
  />
37
44
  </div>
38
45
  </>