@kenyaemr/esm-ward-app 8.1.1-pre.111 → 8.1.1-pre.116

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 (160) hide show
  1. package/.turbo/turbo-build.log +19 -19
  2. package/dist/109.js +1 -0
  3. package/dist/109.js.map +1 -0
  4. package/dist/125.js +1 -0
  5. package/dist/125.js.map +1 -0
  6. package/dist/126.js +1 -0
  7. package/dist/126.js.map +1 -0
  8. package/dist/130.js +1 -1
  9. package/dist/130.js.LICENSE.txt +2 -0
  10. package/dist/130.js.map +1 -1
  11. package/dist/146.js +1 -0
  12. package/dist/146.js.map +1 -0
  13. package/dist/15.js +1 -0
  14. package/dist/15.js.map +1 -0
  15. package/dist/161.js +2 -0
  16. package/dist/161.js.LICENSE.txt +15 -0
  17. package/dist/161.js.map +1 -0
  18. package/dist/2.js +1 -0
  19. package/dist/2.js.map +1 -0
  20. package/dist/269.js +1 -1
  21. package/dist/269.js.map +1 -1
  22. package/dist/325.js +1 -0
  23. package/dist/325.js.map +1 -0
  24. package/dist/372.js +2 -0
  25. package/dist/372.js.map +1 -0
  26. package/dist/466.js +1 -1
  27. package/dist/466.js.map +1 -1
  28. package/dist/500.js +1 -0
  29. package/dist/500.js.map +1 -0
  30. package/dist/53.js +1 -0
  31. package/dist/53.js.map +1 -0
  32. package/dist/557.js +1 -0
  33. package/dist/557.js.map +1 -0
  34. package/dist/559.js +1 -0
  35. package/dist/559.js.map +1 -0
  36. package/dist/574.js +1 -1
  37. package/dist/577.js +1 -1
  38. package/dist/577.js.map +1 -1
  39. package/dist/659.js +1 -1
  40. package/dist/659.js.map +1 -1
  41. package/dist/701.js +1 -0
  42. package/dist/701.js.map +1 -0
  43. package/dist/749.js +1 -1
  44. package/dist/749.js.map +1 -1
  45. package/dist/908.js +1 -0
  46. package/dist/908.js.map +1 -0
  47. package/dist/922.js +1 -0
  48. package/dist/922.js.map +1 -0
  49. package/dist/969.js +1 -0
  50. package/dist/969.js.map +1 -0
  51. package/dist/kenyaemr-esm-ward-app.js +1 -1
  52. package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +300 -107
  53. package/dist/kenyaemr-esm-ward-app.js.map +1 -1
  54. package/dist/main.js +1 -1
  55. package/dist/main.js.map +1 -1
  56. package/dist/routes.json +1 -1
  57. package/mock.tsx +54 -0
  58. package/package.json +1 -1
  59. package/src/action-menu-buttons/clinical-forms-workspace-siderail.component.tsx +37 -0
  60. package/src/action-menu-buttons/discharge-workspace-siderail.component.tsx +20 -0
  61. package/src/beds/empty-bed-skeleton.tsx +2 -1
  62. package/src/beds/empty-bed.scss +0 -4
  63. package/src/beds/occupied-bed.scss +1 -0
  64. package/src/config-schema-mother-child-row.ts +26 -0
  65. package/src/config-schema-pending-items-extension.ts +29 -0
  66. package/src/config-schema.ts +12 -14
  67. package/src/hooks/useAdmissionLocation.ts +22 -4
  68. package/src/hooks/useBeds.ts +3 -4
  69. package/src/hooks/useConcept.ts +3 -4
  70. package/src/hooks/useEmrConfiguration.ts +5 -0
  71. package/src/hooks/useInpatientAdmission.ts +9 -14
  72. package/src/hooks/useInpatientRequest.ts +4 -15
  73. package/src/hooks/useLocations.ts +8 -51
  74. package/src/hooks/useMotherAndChildren.ts +46 -0
  75. package/src/hooks/useObs.ts +2 -6
  76. package/src/hooks/usePatientPendingOrders.ts +16 -0
  77. package/src/hooks/useWardPatientGrouping.ts +25 -0
  78. package/src/index.ts +50 -3
  79. package/src/location-selector/location-selector.component.tsx +18 -21
  80. package/src/routes.json +43 -0
  81. package/src/types/index.ts +34 -0
  82. package/src/ward-patient-card/card-rows/admission-request-note.extension.tsx +7 -2
  83. package/src/ward-patient-card/card-rows/mother-child-row.extension.tsx +110 -0
  84. package/src/ward-patient-card/card-rows/mother-child-row.scss +22 -0
  85. package/src/ward-patient-card/card-rows/pending-items-car-row.extension.tsx +50 -0
  86. package/src/ward-patient-card/row-elements/ward-pateint-skeleton-text.tsx +9 -0
  87. package/src/ward-patient-card/row-elements/ward-patient-age.tsx +1 -1
  88. package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +54 -36
  89. package/src/ward-patient-card/row-elements/ward-patient-identifier.tsx +2 -3
  90. package/src/ward-patient-card/row-elements/ward-patient-location.tsx +19 -0
  91. package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +36 -32
  92. package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +15 -9
  93. package/src/ward-patient-card/row-elements/ward-patient-pending-order.component.tsx +45 -0
  94. package/src/ward-patient-card/row-elements/ward-patient-pending-transfer.tsx +38 -0
  95. package/src/ward-patient-card/row-elements/ward-patient-responsive-tooltip.tsx +32 -0
  96. package/src/ward-patient-card/ward-patient-card-element.component.tsx +4 -0
  97. package/src/ward-patient-card/ward-patient-card.component.tsx +21 -14
  98. package/src/ward-patient-card/ward-patient-card.scss +61 -8
  99. package/src/ward-patient-card/ward-patient-resource.ts +15 -0
  100. package/src/ward-view/ward-view.component.tsx +124 -132
  101. package/src/ward-view/ward-view.resource.ts +121 -1
  102. package/src/ward-view/ward-view.scss +16 -6
  103. package/src/ward-view/ward-view.test.tsx +27 -42
  104. package/src/ward-view-header/admission-requests-bar.component.tsx +8 -7
  105. package/src/ward-view-header/admission-requests-bar.test.tsx +8 -21
  106. package/src/ward-view-header/admission-requests.scss +1 -1
  107. package/src/ward-view-header/ward-metric.component.tsx +24 -0
  108. package/src/ward-view-header/ward-metric.scss +25 -0
  109. package/src/ward-view-header/ward-metrics.component.tsx +77 -0
  110. package/src/ward-view-header/ward-metrics.scss +8 -0
  111. package/src/ward-view-header/ward-metrics.test.tsx +91 -0
  112. package/src/ward-view-header/ward-view-header.component.tsx +3 -0
  113. package/src/ward-view-header/ward-view-header.scss +0 -1
  114. package/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +11 -3
  115. package/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +4 -5
  116. package/src/ward-workspace/admission-request-card/admission-request-card.scss +8 -4
  117. package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +8 -3
  118. package/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +2 -0
  119. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +29 -61
  120. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +37 -21
  121. package/src/ward-workspace/patient-banner/patient-banner.component.tsx +3 -3
  122. package/src/ward-workspace/patient-clinical-forms-workspace/patient-clinical-forms.workspace.tsx +23 -0
  123. package/src/{ward-patient-workspace → ward-workspace/patient-details}/ward-patient-action-button.extension.tsx +2 -1
  124. package/src/{ward-patient-workspace → ward-workspace/patient-details}/ward-patient.workspace.tsx +7 -5
  125. package/src/ward-workspace/patient-discharge/patient-discharge.scss +41 -0
  126. package/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +120 -0
  127. package/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +40 -30
  128. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx +29 -22
  129. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.scss +12 -2
  130. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx +2 -2
  131. package/src/ward-workspace/patient-transfer-request-workspace/patient-transfer-request.workspace.tsx +11 -0
  132. package/src/ward-workspace/ward-patient-notes/form/notes-form.component.tsx +1 -1
  133. package/src/ward-workspace/ward-patient-notes/form/notes-form.test.tsx +1 -1
  134. package/src/ward-workspace/ward-patient-notes/history/notes-container.component.tsx +2 -2
  135. package/src/ward-workspace/ward-patient-notes/notes.resource.ts +5 -7
  136. package/src/ward-workspace/ward-patient-notes/notes.workspace.tsx +1 -1
  137. package/src/ward-workspace/ward-patient-notes/types.ts +0 -4
  138. package/src/ward.resource.ts +6 -0
  139. package/translations/en.json +18 -1
  140. package/dist/152.js +0 -1
  141. package/dist/152.js.map +0 -1
  142. package/dist/255.js +0 -2
  143. package/dist/255.js.map +0 -1
  144. package/dist/303.js +0 -1
  145. package/dist/303.js.map +0 -1
  146. package/dist/346.js +0 -1
  147. package/dist/346.js.map +0 -1
  148. package/dist/729.js +0 -1
  149. package/dist/729.js.map +0 -1
  150. package/dist/76.js +0 -1
  151. package/dist/76.js.map +0 -1
  152. package/dist/793.js +0 -2
  153. package/dist/793.js.LICENSE.txt +0 -5
  154. package/dist/793.js.map +0 -1
  155. package/dist/803.js +0 -1
  156. package/dist/803.js.map +0 -1
  157. package/dist/960.js +0 -1
  158. package/dist/960.js.map +0 -1
  159. /package/dist/{255.js.LICENSE.txt → 372.js.LICENSE.txt} +0 -0
  160. /package/src/{ward-patient-workspace → ward-workspace/patient-details}/ward-patient.style.scss +0 -0
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ import { coloredObsTagsCardRowConfigSchema } from './config-schema-extension-col
12
12
  import { moduleName } from './constant';
13
13
  import { createDashboardLink } from './createDashboardLink.component';
14
14
  import rootComponent from './root.component';
15
+ import { motherChildRowConfigSchema } from './config-schema-mother-child-row';
16
+ import { pendingItemsExtensionConfigSchema } from './config-schema-pending-items-extension';
15
17
 
16
18
  export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
17
19
 
@@ -38,7 +40,7 @@ export const admitPatientFormWorkspace = getAsyncLifecycle(
38
40
 
39
41
  // Title for this workspace is set dynamically
40
42
  export const wardPatientWorkspace = getAsyncLifecycle(
41
- () => import('./ward-patient-workspace/ward-patient.workspace'),
43
+ () => import('./ward-workspace/patient-details/ward-patient.workspace'),
42
44
  options,
43
45
  );
44
46
 
@@ -49,7 +51,7 @@ export const wardPatientNotesWorkspace = getAsyncLifecycle(
49
51
  );
50
52
 
51
53
  export const wardPatientActionButtonExtension = getAsyncLifecycle(
52
- () => import('./ward-patient-workspace/ward-patient-action-button.extension'),
54
+ () => import('./ward-workspace/patient-details/ward-patient-action-button.extension'),
53
55
  options,
54
56
  );
55
57
 
@@ -68,26 +70,71 @@ export const admissionRequestNoteRowExtension = getAsyncLifecycle(
68
70
  options,
69
71
  );
70
72
 
73
+ export const motherChildRowExtension = getAsyncLifecycle(
74
+ () => import('./ward-patient-card/card-rows/mother-child-row.extension'),
75
+ options,
76
+ );
77
+
78
+ export const pendingItemsCardRowExtension = getAsyncLifecycle(
79
+ () => import('./ward-patient-card/card-rows/pending-items-car-row.extension'),
80
+ options,
81
+ );
82
+
71
83
  // t('transfers', 'Transfers')
72
84
  export const patientTransferAndSwapWorkspace = getAsyncLifecycle(
73
85
  () => import('./ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace'),
74
86
  options,
75
87
  );
76
88
 
89
+ // t('discharge', 'Discharge')
90
+ export const patientDischargeWorkspace = getAsyncLifecycle(
91
+ () => import('./ward-workspace/patient-discharge/patient-discharge.workspace'),
92
+ options,
93
+ );
94
+
77
95
  export const patientTransferAndSwapWorkspaceSiderailIcon = getAsyncLifecycle(
78
96
  () => import('./action-menu-buttons/transfer-workspace-siderail.component'),
79
97
  options,
80
98
  );
81
99
 
100
+ // t('transferRequest', 'Transfer request')
101
+ export const patientTransferRequestWorkspace = getAsyncLifecycle(
102
+ () => import('./ward-workspace/patient-transfer-request-workspace/patient-transfer-request.workspace'),
103
+ options,
104
+ );
105
+
106
+ export const patientDischargeWorkspaceSideRailIcon = getAsyncLifecycle(
107
+ () => import('./action-menu-buttons/discharge-workspace-siderail.component'),
108
+ options,
109
+ );
110
+
111
+ export const patientClinicalFormsWorkspace = getAsyncLifecycle(
112
+ () => import('./ward-workspace/patient-clinical-forms-workspace/patient-clinical-forms.workspace'),
113
+ options,
114
+ );
115
+
116
+ export const clinicalFormWorkspaceSideRailIcon = getAsyncLifecycle(
117
+ () => import('./action-menu-buttons/clinical-forms-workspace-siderail.component'),
118
+ options,
119
+ );
120
+
82
121
  export function startupApp() {
83
122
  registerBreadcrumbs([]);
84
123
  defineConfigSchema(moduleName, configSchema);
85
124
  defineExtensionConfigSchema('colored-obs-tags-card-row', coloredObsTagsCardRowConfigSchema);
86
125
  defineExtensionConfigSchema('admission-request-note-card-row', admissionRequestNoteRowConfigSchema);
126
+ defineExtensionConfigSchema('mother-child-card-row', motherChildRowConfigSchema);
127
+ defineExtensionConfigSchema('ward-patient-pending-items-card-row', pendingItemsExtensionConfigSchema);
87
128
 
88
129
  registerFeatureFlag(
89
130
  'bedmanagement-module',
90
- 'Bed Management Module',
131
+ 'Bed management module',
91
132
  'Enables features related to bed management / assignment. Requires the backend bed management module to be installed.',
92
133
  );
134
+
135
+ registerFeatureFlag(
136
+ 'ward-view-vertical-tiling',
137
+ 'Ward view vertical tiling',
138
+ 'Enable tiling of bed cards vertically in the ward view.',
139
+ );
93
140
  }
@@ -24,29 +24,26 @@ export default function LocationSelector(props: LocationSelectorProps) {
24
24
  const isTablet = !isDesktop(useLayoutType());
25
25
  const [searchTerm, setSearchTerm] = useState('');
26
26
  const debouncedSearchTerm = useDebounce(searchTerm);
27
- const [page, setPage] = useState(1);
28
27
  const filterCriteria: Array<Array<string>> = useMemo(() => {
29
28
  const criteria = [];
30
29
  if (debouncedSearchTerm) {
31
30
  criteria.push(['name:contains', debouncedSearchTerm]);
32
31
  }
33
- criteria.push(['_count', size.toString()]);
34
32
  if (emrConfiguration) {
35
33
  criteria.push(['_tag', emrConfiguration.supportsTransferLocationTag.name]);
36
34
  }
37
- if (page > 1) {
38
- criteria.push(['_getpagesoffset', ((page - 1) * size).toString()]);
39
- }
40
35
  return criteria;
41
- }, [debouncedSearchTerm, page, emrConfiguration]);
42
- const { locations, isLoading, totalLocations } = useLocations(filterCriteria, !emrConfiguration);
36
+ }, [debouncedSearchTerm, emrConfiguration]);
37
+ const {
38
+ data: locations,
39
+ isLoading,
40
+ totalCount,
41
+ currentPage,
42
+ totalPages,
43
+ goToNext,
44
+ goToPrevious,
45
+ } = useLocations(filterCriteria, size, !emrConfiguration);
43
46
 
44
- const handlePageChange = useCallback(
45
- ({ page: newPage }) => {
46
- setPage(newPage);
47
- },
48
- [setPage, page],
49
- );
50
47
  const handleSearch = useCallback(
51
48
  (event: React.ChangeEvent<HTMLInputElement>) => {
52
49
  setSearchTerm(event.target.value);
@@ -84,30 +81,30 @@ export default function LocationSelector(props: LocationSelectorProps) {
84
81
  </RadioButtonGroup>
85
82
  </ResponsiveWrapper>
86
83
  )}
87
- {totalLocations > 5 && (
84
+ {totalCount > size && (
88
85
  <div className={styles.pagination}>
89
86
  <span className={styles.bodyShort01}>
90
87
  {t('showingLocations', '{{start}}-{{end}} of {{count}} locations', {
91
- start: (page - 1) * size + 1,
92
- end: Math.min(page * size, totalLocations),
93
- count: totalLocations,
88
+ start: (currentPage - 1) * size + 1,
89
+ end: Math.min(currentPage * size, totalCount),
90
+ count: totalCount,
94
91
  })}
95
92
  </span>
96
93
  <div>
97
94
  <IconButton
98
95
  className={classNames(styles.button, styles.buttonLeft)}
99
- disabled={page === 1}
96
+ disabled={currentPage === 1}
100
97
  kind="ghost"
101
98
  label={t('previousPage', 'Previous page')}
102
- onClick={() => handlePageChange({ page: page - 1 })}>
99
+ onClick={() => goToPrevious()}>
103
100
  <ChevronLeftIcon />
104
101
  </IconButton>
105
102
  <IconButton
106
103
  className={styles.button}
107
- disabled={page * size >= totalLocations}
104
+ disabled={currentPage >= totalPages}
108
105
  kind="ghost"
109
106
  label={t('nextPage', 'Next page')}
110
- onClick={() => handlePageChange({ page: page + 1 })}>
107
+ onClick={() => goToNext()}>
111
108
  <ChevronRightIcon />
112
109
  </IconButton>
113
110
  </div>
package/src/routes.json CHANGED
@@ -50,10 +50,30 @@
50
50
  "slot": "action-menu-ward-patient-items-slot",
51
51
  "component": "patientTransferAndSwapWorkspaceSiderailIcon"
52
52
  },
53
+ {
54
+ "name": "patient-discharge-siderail-button",
55
+ "slot": "action-menu-ward-patient-items-slot",
56
+ "component": "patientDischargeWorkspaceSideRailIcon"
57
+ },
58
+ {
59
+ "name": "clinical-forms-workspace-siderail-button",
60
+ "component": "clinicalFormWorkspaceSideRailIcon",
61
+ "slot": "action-menu-ward-patient-items-slot"
62
+ },
53
63
  {
54
64
  "component": "admissionRequestNoteRowExtension",
55
65
  "name": "admission-request-note-card-row",
56
66
  "slot": "ward-patient-card-slot"
67
+ },
68
+ {
69
+ "component": "motherChildRowExtension",
70
+ "name": "mother-child-card-row",
71
+ "slot": "ward-patient-card-slot"
72
+ },
73
+ {
74
+ "component": "pendingItemsCardRowExtension",
75
+ "name": "ward-patient-pending-items-card-row",
76
+ "slot": "ward-patient-card-pending-items-slot"
57
77
  }
58
78
  ],
59
79
  "workspaces": [
@@ -93,6 +113,29 @@
93
113
  "type": "transfer-swap-bed-form",
94
114
  "hasOwnSidebar": true,
95
115
  "sidebarFamily": "ward-patient"
116
+ },
117
+ {
118
+ "name": "patient-transfer-request-workspace",
119
+ "component": "patientTransferRequestWorkspace",
120
+ "title": "transferRequest",
121
+ "type": "transfer-request-form"
122
+ },
123
+ {
124
+ "name": "patient-discharge-workspace",
125
+ "component": "patientDischargeWorkspace",
126
+ "title": "discharge",
127
+ "type": "ward-patient-discharge",
128
+ "hasOwnSidebar": true,
129
+ "sidebarFamily": "ward-patient"
130
+ },
131
+ {
132
+ "name": "ward-patient-clinical-forms-workspace",
133
+ "component": "patientClinicalFormsWorkspace",
134
+ "title": "clinicalForms",
135
+ "type": "ward-patient-clinical-forms",
136
+ "hasOwnSidebar": true,
137
+ "sidebarFamily": "ward-patient",
138
+ "width": "wider"
96
139
  }
97
140
  ]
98
141
  }
@@ -9,6 +9,7 @@ import type {
9
9
  Visit,
10
10
  } from '@openmrs/esm-framework';
11
11
  import type React from 'react';
12
+ import type { useWardPatientGrouping } from '../hooks/useWardPatientGrouping';
12
13
 
13
14
  export type WardPatientCard = React.FC<WardPatient>;
14
15
 
@@ -40,9 +41,14 @@ export type WardPatient = {
40
41
  */
41
42
  inpatientRequest: InpatientRequest;
42
43
  };
44
+
43
45
  export interface WardPatientWorkspaceProps extends DefaultWorkspaceProps {
44
46
  wardPatient: WardPatient;
45
47
  }
48
+ export interface MotherAndChildrenRelationships {
49
+ motherByChildUuid: Map<string, Patient>;
50
+ childrenByMotherUuid: Map<string, Array<Patient>>;
51
+ }
46
52
 
47
53
  // server-side types defined in openmrs-module-bedmanagement:
48
54
 
@@ -134,6 +140,19 @@ export interface InpatientAdmission {
134
140
  // the first encounter of the visit that is of encounterType "Admission" or "Transfer",
135
141
  // regardless of the admission location
136
142
  firstAdmissionOrTransferEncounter: Encounter;
143
+
144
+ // the current in patient request
145
+ currentInpatientRequest: InpatientRequest;
146
+ }
147
+ export interface WardAppContext {
148
+ allPatientsByPatientUuid: Map<string, Patient>;
149
+ }
150
+
151
+ export interface MotherAndChild {
152
+ mother: Patient;
153
+ child: Patient;
154
+ motherAdmission: InpatientAdmission;
155
+ childAdmission: InpatientAdmission;
137
156
  }
138
157
 
139
158
  // TODO: Move these types to esm-core
@@ -185,6 +204,12 @@ export interface EncounterRole extends OpenmrsResourceStrict {
185
204
  retired?: boolean;
186
205
  }
187
206
 
207
+ export interface WardMetrics {
208
+ patients: string;
209
+ freeBeds: string;
210
+ capacity: string;
211
+ }
212
+
188
213
  export interface EncounterPayload {
189
214
  encounterDatetime?: string;
190
215
  encounterType: string;
@@ -202,3 +227,12 @@ export interface ObsPayload {
202
227
  value?: string;
203
228
  groupMembers?: Array<ObsPayload>;
204
229
  }
230
+
231
+ export interface MotherAndChildren {
232
+ childAdmission: InpatientAdmission;
233
+ child: Patient;
234
+ motherAdmission: InpatientAdmission;
235
+ mother: Patient;
236
+ }
237
+
238
+ export type WardPatientGroupDetails = ReturnType<typeof useWardPatientGrouping>;
@@ -3,6 +3,7 @@ import { type ObsElementDefinition } from '../../config-schema';
3
3
  import { type WardPatientCard } from '../../types';
4
4
  import WardPatientObs from '../row-elements/ward-patient-obs';
5
5
  import { useConfig } from '@openmrs/esm-framework';
6
+ import styles from '../ward-patient-card.scss';
6
7
 
7
8
  const AdmissionRequestNoteRowExtension: WardPatientCard = ({ patient, visit, inpatientAdmission }) => {
8
9
  const { conceptUuid } = useConfig<ObsElementDefinition>();
@@ -18,9 +19,13 @@ const AdmissionRequestNoteRowExtension: WardPatientCard = ({ patient, visit, inp
18
19
  // only show if the patient has not been admitted yet
19
20
  const admitted = inpatientAdmission != null;
20
21
  if (admitted) {
21
- return <></>;
22
+ return null;
22
23
  } else {
23
- return <WardPatientObs config={config} patient={patient} visit={visit} />;
24
+ return (
25
+ <div className={styles.wardPatientCardRow}>
26
+ <WardPatientObs config={config} patient={patient} visit={visit} />
27
+ </div>
28
+ );
24
29
  }
25
30
  };
26
31
 
@@ -0,0 +1,110 @@
1
+ import { InlineNotification } from '@carbon/react';
2
+ import { BabyIcon, MotherIcon } from '@openmrs/esm-framework';
3
+ import classNames from 'classnames';
4
+ import React from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { type MothersAndChildrenSearchCriteria, useMotherAndChildren } from '../../hooks/useMotherAndChildren';
7
+ import { type WardPatientCard } from '../../types';
8
+ import WardPatientSkeletonText from '../row-elements/ward-pateint-skeleton-text';
9
+ import WardPatientAge from '../row-elements/ward-patient-age';
10
+ import WardPatientIdentifier from '../row-elements/ward-patient-identifier';
11
+ import WardPatientLocation from '../row-elements/ward-patient-location';
12
+ import WardPatientName from '../row-elements/ward-patient-name';
13
+ import wardPatientCardStyles from '../ward-patient-card.scss';
14
+ import styles from './mother-child-row.scss';
15
+
16
+ const motherAndChildrenRep =
17
+ 'custom:(childAdmission,mother:(person,identifiers:full,uuid),child:(person,identifiers:full,uuid),motherAdmission)';
18
+
19
+ /**
20
+ * This extension displays the mother or children of the patient in the patient card.
21
+ *
22
+ * @param param0
23
+ * @returns
24
+ */
25
+ const MotherChildRowExtension: WardPatientCard = ({ patient }) => {
26
+ const { t } = useTranslation();
27
+
28
+ const getChildrenRequestParams: MothersAndChildrenSearchCriteria = {
29
+ mothers: [patient.uuid],
30
+ requireMotherHasActiveVisit: true,
31
+ requireChildHasActiveVisit: true,
32
+ requireChildBornDuringMothersActiveVisit: true,
33
+ };
34
+
35
+ const getMotherRequestParams: MothersAndChildrenSearchCriteria = {
36
+ children: [patient.uuid],
37
+ requireMotherHasActiveVisit: true,
38
+ requireChildHasActiveVisit: true,
39
+ requireChildBornDuringMothersActiveVisit: true,
40
+ };
41
+
42
+ const {
43
+ data: childrenData,
44
+ isLoading: isLoadingChildrenData,
45
+ error: childrenDataError,
46
+ } = useMotherAndChildren(getChildrenRequestParams, true, motherAndChildrenRep);
47
+ const {
48
+ data: motherData,
49
+ isLoading: isLoadingMotherData,
50
+ error: motherDataError,
51
+ } = useMotherAndChildren(getMotherRequestParams, true, motherAndChildrenRep);
52
+
53
+ if (isLoadingChildrenData || isLoadingMotherData) {
54
+ return (
55
+ <div className={classNames(styles.motherOrBabyRow, wardPatientCardStyles.wardPatientCardRow)}>
56
+ <WardPatientSkeletonText />
57
+ </div>
58
+ );
59
+ } else if (childrenDataError) {
60
+ return (
61
+ <InlineNotification
62
+ kind="warning"
63
+ lowContrast={true}
64
+ title={t('errorLoadingChildren', 'Error loading children info')}
65
+ />
66
+ );
67
+ } else if (motherDataError) {
68
+ return (
69
+ <InlineNotification
70
+ kind="warning"
71
+ lowContrast={true}
72
+ title={t('errorLoadingChildren', 'Error loading mother info')}
73
+ />
74
+ );
75
+ }
76
+
77
+ return (
78
+ <>
79
+ {[...childrenData, ...motherData]?.map(({ mother, motherAdmission, child, childAdmission }) => {
80
+ // patient A is the patient card's patient
81
+ const patientA = patient;
82
+ // patient B is either the mother or the child of patient A
83
+ const isPatientBTheMother = mother.uuid != patientA.uuid;
84
+ const patientB = isPatientBTheMother ? mother : child;
85
+
86
+ // we display patient B here
87
+ const Icon = isPatientBTheMother ? MotherIcon : BabyIcon;
88
+ const patientBAdmission = isPatientBTheMother ? motherAdmission : childAdmission;
89
+
90
+ return (
91
+ <div
92
+ key={patientB.uuid}
93
+ className={classNames(styles.motherOrBabyRow, wardPatientCardStyles.wardPatientCardRow)}>
94
+ <div className={styles.motherOrBabyIconDiv}>
95
+ <Icon className={styles.motherOrBabyIcon} size={24} />
96
+ </div>
97
+ <div className={classNames(styles.motherOrBabyRowElementsDiv, wardPatientCardStyles.dotSeparatedChildren)}>
98
+ <WardPatientName patient={patientB} />
99
+ <WardPatientIdentifier patient={patientB} />
100
+ <WardPatientAge patient={patientB} />
101
+ <WardPatientLocation inpatientAdmission={patientBAdmission} />
102
+ </div>
103
+ </div>
104
+ );
105
+ })}
106
+ </>
107
+ );
108
+ };
109
+
110
+ export default MotherChildRowExtension;
@@ -0,0 +1,22 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .motherOrBabyRow {
5
+ display: flex;
6
+ }
7
+
8
+ .motherOrBabyRowElementsDiv {
9
+ flex: 1;
10
+ }
11
+
12
+ .motherOrBabyIconDiv {
13
+ display: flex;
14
+ align-items: center;
15
+ }
16
+
17
+ .motherOrBabyIcon {
18
+ padding: layout.$spacing-02;
19
+ border-radius: 50%;
20
+ background-color: $grey-2;
21
+ margin-right: layout.$spacing-03;
22
+ }
@@ -0,0 +1,50 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import { type WardPatientCard } from '../../types';
3
+ import { Hourglass } from '@carbon/react/icons';
4
+
5
+ import { useConfig } from '@openmrs/esm-framework';
6
+ import type { PendingItemsDefinition } from '../../config-schema';
7
+ import { WardPatientPendingOrder } from '../row-elements/ward-patient-pending-order.component';
8
+ import styles from '../ward-patient-card.scss';
9
+ import WardPatientPendingTransfer from '../row-elements/ward-patient-pending-transfer';
10
+
11
+ const PendingItemsCarRowExtension: WardPatientCard = (wardPatient) => {
12
+ const { orders, showPendingItems } = useConfig<PendingItemsDefinition>();
13
+ const [hasPendingOrders, setHasPendingOrders] = React.useState(false);
14
+
15
+ const hasPendingItems = !!wardPatient?.inpatientRequest || hasPendingOrders;
16
+
17
+ const handlePendingOrderCount = useCallback((count: number) => {
18
+ if (count > 0) {
19
+ setHasPendingOrders(true);
20
+ }
21
+ }, []);
22
+
23
+ useEffect(() => {
24
+ if (!orders?.orderTypes?.length) {
25
+ setHasPendingOrders(false);
26
+ }
27
+ }, [orders]);
28
+
29
+ return (
30
+ <div className={styles.wardPatientCardPendingItemsRow}>
31
+ {showPendingItems && hasPendingItems ? (
32
+ <>
33
+ <Hourglass className={styles.hourGlassIcon} size="16" />:
34
+ </>
35
+ ) : null}
36
+ {orders?.orderTypes.map(({ uuid, label }) => (
37
+ <WardPatientPendingOrder
38
+ key={`pending-order-type-${uuid}`}
39
+ wardPatient={wardPatient}
40
+ orderUuid={uuid}
41
+ label={label}
42
+ onOrderCount={handlePendingOrderCount}
43
+ />
44
+ ))}
45
+ {wardPatient?.inpatientRequest ? <WardPatientPendingTransfer wardPatient={wardPatient} /> : null}
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default PendingItemsCarRowExtension;
@@ -0,0 +1,9 @@
1
+ import { SkeletonIcon } from '@carbon/react';
2
+ import React from 'react';
3
+ import styles from '../ward-patient-card.scss';
4
+
5
+ const WardPatientSkeletonText = () => {
6
+ return <SkeletonIcon className={styles.skeletonText} />;
7
+ };
8
+
9
+ export default WardPatientSkeletonText;
@@ -6,7 +6,7 @@ export interface WardPatientAgeProps {
6
6
  }
7
7
 
8
8
  const WardPatientAge: React.FC<WardPatientAgeProps> = ({ patient }) => {
9
- return <div>{age(patient.person?.birthdate)}</div>;
9
+ return patient.person?.birthdate ? <div>{age(patient.person.birthdate)}</div> : null;
10
10
  };
11
11
 
12
12
  export default WardPatientAge;
@@ -1,12 +1,13 @@
1
- import { SkeletonText, Tag } from '@carbon/react';
2
- import { type Patient, translateFrom, type Visit, type OpenmrsResource } from '@openmrs/esm-framework';
3
- import React from 'react';
1
+ import { Tag } from '@carbon/react';
2
+ import { type OpenmrsResource, type Patient, type Visit } from '@openmrs/esm-framework';
3
+ import React, { type ReactNode } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { moduleName } from '../../constant';
5
+ import { type ColoredObsTagsCardRowConfigObject } from '../../config-schema-extension-colored-obs-tags';
6
6
  import { useObs } from '../../hooks/useObs';
7
7
  import styles from '../ward-patient-card.scss';
8
- import { obsCustomRepresentation, useConceptToTagColorMap } from './ward-patient-obs.resource';
9
- import { type ColoredObsTagsCardRowConfigObject } from '../../config-schema-extension-colored-obs-tags';
8
+ import WardPatientSkeletonText from './ward-pateint-skeleton-text';
9
+ import { getObsEncounterString, obsCustomRepresentation, useConceptToTagColorMap } from './ward-patient-obs.resource';
10
+ import WardPatientResponsiveTooltip from './ward-patient-responsive-tooltip';
10
11
 
11
12
  interface WardPatientCodedObsTagsProps {
12
13
  config: ColoredObsTagsCardRowConfigObject;
@@ -25,49 +26,66 @@ interface WardPatientCodedObsTagsProps {
25
26
  * @returns
26
27
  */
27
28
  const WardPatientCodedObsTags: React.FC<WardPatientCodedObsTagsProps> = ({ config, patient, visit }) => {
28
- const { conceptUuid, summaryLabel, summaryLabelColor, summaryLabelI18nModule } = config;
29
+ const { conceptUuid, summaryLabel, summaryLabelColor } = config;
29
30
  const { data, isLoading } = useObs({ patient: patient.uuid, concept: conceptUuid }, obsCustomRepresentation);
30
31
  const { t } = useTranslation();
31
- const { data: conceptToTagColorMap } = useConceptToTagColorMap(config.tags);
32
+ const conceptToTagColorMap = useConceptToTagColorMap(config.tags);
32
33
 
33
34
  if (isLoading) {
34
- return <SkeletonText />;
35
+ return (
36
+ <div className={styles.wardPatientCardRow}>
37
+ <WardPatientSkeletonText />
38
+ </div>
39
+ );
35
40
  } else {
36
- const obsToDisplay = data?.data?.results?.filter((o) => {
41
+ const obsToDisplay = data?.filter((o) => {
37
42
  const matchVisit = o.encounter.visit?.uuid == visit?.uuid;
38
- return matchVisit || visit == null; // TODO: remove visit == null hack when server API supports returning visit
43
+ return matchVisit;
39
44
  });
40
45
 
41
- const summaryLabelToDisplay =
42
- summaryLabel != null
43
- ? translateFrom(summaryLabelI18nModule ?? moduleName, summaryLabel)
44
- : obsToDisplay?.[0]?.concept?.display;
46
+ const summaryLabelToDisplay = summaryLabel != null ? t(summaryLabel) : obsToDisplay?.[0]?.concept?.display;
45
47
 
46
- const obsNodes = obsToDisplay?.map((o) => {
47
- const { display, uuid } = o.value as OpenmrsResource;
48
+ // for each obs configured to be displayed with a color, we create a tag for it
49
+ // for other obs not configured, we create a single summary tag for all of them.
50
+ const summaryTagTooltipText: ReactNode[] = [];
51
+ const coloredOpsTags = obsToDisplay
52
+ ?.map((o) => {
53
+ const { display, uuid } = o.value as OpenmrsResource;
48
54
 
49
- const color = conceptToTagColorMap?.get(uuid);
50
- if (color) {
51
- return (
52
- <Tag type={color} key={`ward-coded-obs-tag-${o.uuid}`}>
53
- {display}
54
- </Tag>
55
- );
56
- } else {
57
- return null;
58
- }
59
- });
55
+ const color = conceptToTagColorMap?.get(uuid);
56
+ if (color) {
57
+ return (
58
+ <WardPatientResponsiveTooltip tooltipContent={getObsEncounterString(o, t)}>
59
+ <Tag type={color} key={`ward-coded-obs-tag-${o.uuid}`}>
60
+ {display}
61
+ </Tag>
62
+ </WardPatientResponsiveTooltip>
63
+ );
64
+ } else {
65
+ summaryTagTooltipText.push(
66
+ <div key={uuid}>
67
+ {display} ({getObsEncounterString(o, t)})
68
+ </div>,
69
+ );
70
+ return null;
71
+ }
72
+ })
73
+ .filter((o) => o != null);
60
74
 
61
- const obsWithNoTagCount = obsNodes.filter((o) => o == null).length;
62
- if (obsNodes?.length > 0 || obsWithNoTagCount > 0) {
75
+ if (coloredOpsTags?.length > 0 || summaryTagTooltipText.length > 0) {
63
76
  return (
64
- <div>
77
+ <div className={styles.wardPatientCardRow}>
65
78
  <span className={styles.wardPatientObsLabel}>
66
- {obsNodes}
67
- {obsWithNoTagCount > 0 ? (
68
- <Tag type={summaryLabelColor}>
69
- {t('countItems', '{{count}} {{item}}', { count: obsWithNoTagCount, item: summaryLabelToDisplay })}
70
- </Tag>
79
+ {coloredOpsTags}
80
+ {summaryTagTooltipText.length > 0 ? (
81
+ <WardPatientResponsiveTooltip tooltipContent={summaryTagTooltipText}>
82
+ <Tag type={summaryLabelColor}>
83
+ {t('countItems', '{{count}} {{item}}', {
84
+ count: summaryTagTooltipText.length,
85
+ item: summaryLabelToDisplay,
86
+ })}
87
+ </Tag>
88
+ </WardPatientResponsiveTooltip>
71
89
  ) : null}
72
90
  </span>
73
91
  </div>
@@ -33,15 +33,14 @@ const defaultConfig: IdentifierElementDefinition = {
33
33
  const WardPatientIdentifier: React.FC<WardPatientIdentifierProps> = ({ config: configProp, patient }) => {
34
34
  const { t } = useTranslation();
35
35
  const config = configProp ?? defaultConfig;
36
- const { identifierTypeUuid, labelI18nModule: labelModule, label } = config;
36
+ const { identifierTypeUuid, label } = config;
37
37
  const patientIdentifiers = patient.identifiers.filter(
38
38
  (patientIdentifier: PatientIdentifier) =>
39
39
  identifierTypeUuid == null || patientIdentifier.identifierType?.uuid === identifierTypeUuid,
40
40
  );
41
41
  patientIdentifiers.sort(identifierCompareFunction);
42
42
  const patientIdentifier = patientIdentifiers[0];
43
- const labelToDisplay =
44
- label != null ? translateFrom(labelModule ?? moduleName, label) : patientIdentifier?.identifierType?.name;
43
+ const labelToDisplay = label != null ? t(label) : patientIdentifier?.identifierType?.name;
45
44
  return (
46
45
  <div>
47
46
  {labelToDisplay ? <Tag>{t('identifierTypelabel', '{{label}}:', { label: labelToDisplay })}</Tag> : <></>}