@kenyaemr/esm-ward-app 8.1.1-pre.118 → 8.1.1-pre.119

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 (149) hide show
  1. package/.turbo/turbo-build.log +16 -20
  2. package/dist/124.js +1 -0
  3. package/dist/124.js.map +1 -0
  4. package/dist/125.js +1 -1
  5. package/dist/125.js.map +1 -1
  6. package/dist/130.js +1 -1
  7. package/dist/130.js.map +1 -1
  8. package/dist/153.js +1 -0
  9. package/dist/153.js.map +1 -0
  10. package/dist/372.js +1 -1
  11. package/dist/372.js.map +1 -1
  12. package/dist/471.js +1 -0
  13. package/dist/471.js.map +1 -0
  14. package/dist/481.js +1 -0
  15. package/dist/481.js.map +1 -0
  16. package/dist/53.js +1 -1
  17. package/dist/53.js.map +1 -1
  18. package/dist/559.js +1 -1
  19. package/dist/559.js.map +1 -1
  20. package/dist/574.js +1 -1
  21. package/dist/{500.js → 576.js} +1 -1
  22. package/dist/576.js.map +1 -0
  23. package/dist/577.js +1 -1
  24. package/dist/577.js.map +1 -1
  25. package/dist/649.js +2 -0
  26. package/dist/{161.js.LICENSE.txt → 649.js.LICENSE.txt} +0 -6
  27. package/dist/649.js.map +1 -0
  28. package/dist/{659.js → 662.js} +1 -1
  29. package/dist/662.js.map +1 -0
  30. package/dist/67.js +2 -0
  31. package/dist/67.js.LICENSE.txt +5 -0
  32. package/dist/67.js.map +1 -0
  33. package/dist/920.js +1 -0
  34. package/dist/920.js.map +1 -0
  35. package/dist/921.js +1 -0
  36. package/dist/921.js.map +1 -0
  37. package/dist/922.js +1 -1
  38. package/dist/922.js.map +1 -1
  39. package/dist/kenyaemr-esm-ward-app.js +1 -1
  40. package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +148 -165
  41. package/dist/kenyaemr-esm-ward-app.js.map +1 -1
  42. package/dist/main.js +1 -1
  43. package/dist/main.js.LICENSE.txt +0 -10
  44. package/dist/main.js.map +1 -1
  45. package/dist/routes.json +1 -1
  46. package/mock.tsx +8 -0
  47. package/package.json +1 -1
  48. package/src/beds/empty-bed-skeleton.tsx +3 -3
  49. package/src/beds/empty-bed.component.tsx +3 -3
  50. package/src/beds/ward-bed.component.tsx +41 -0
  51. package/src/beds/ward-bed.scss +45 -0
  52. package/src/beds/{occupied-bed.test.tsx → ward-bed.test.tsx} +27 -16
  53. package/src/config-schema.ts +196 -75
  54. package/src/hooks/useAssignedBedByPatient.ts +9 -0
  55. package/src/hooks/useInpatientAdmission.ts +1 -1
  56. package/src/hooks/useMotherAndChildren.ts +2 -2
  57. package/src/hooks/useObs.ts +2 -2
  58. package/src/hooks/useWardPatientGrouping.ts +2 -0
  59. package/src/index.ts +10 -29
  60. package/src/root.component.tsx +3 -0
  61. package/src/routes.json +6 -11
  62. package/src/types/index.ts +29 -14
  63. package/src/ward-patient-card/card-rows/admission-request-note-row.component.tsx +38 -0
  64. package/src/ward-patient-card/card-rows/coded-obs-tags-row.component.tsx +108 -0
  65. package/src/ward-patient-card/card-rows/mother-child-row.component.tsx +84 -0
  66. package/src/ward-patient-card/card-rows/{pending-items-car-row.extension.tsx → pending-items-row.component.tsx} +12 -8
  67. package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +12 -7
  68. package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +5 -5
  69. package/src/ward-patient-card/row-elements/ward-patient-identifier.tsx +18 -40
  70. package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +2 -2
  71. package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +15 -8
  72. package/src/ward-patient-card/ward-patient-card.component.tsx +16 -54
  73. package/src/ward-patient-card/ward-patient-card.scss +7 -1
  74. package/src/ward-view/default-ward/default-ward-beds.component.tsx +42 -0
  75. package/src/ward-view/default-ward/default-ward-patient-card-header.component.tsx +32 -0
  76. package/src/ward-view/default-ward/default-ward-patient-card.component.tsx +31 -0
  77. package/src/ward-view/default-ward/default-ward-pending-patients.component.tsx +52 -0
  78. package/src/ward-view/default-ward/default-ward-unassigned-patients.component.tsx +32 -0
  79. package/src/ward-view/default-ward/default-ward-view.component.tsx +31 -0
  80. package/src/ward-view/materal-ward/maternal-ward-beds.component.tsx +65 -0
  81. package/src/ward-view/materal-ward/maternal-ward-patient-card-header.component.tsx +30 -0
  82. package/src/ward-view/materal-ward/maternal-ward-patient-card.component.tsx +93 -0
  83. package/src/{beds/occupied-bed.scss → ward-view/materal-ward/maternal-ward-patient-card.scss} +4 -10
  84. package/src/ward-view/materal-ward/maternal-ward-patient-card.test.tsx +47 -0
  85. package/src/ward-view/materal-ward/maternal-ward-pending-patients.component.tsx +48 -0
  86. package/src/ward-view/materal-ward/maternal-ward-unassigned-patients.component.tsx +33 -0
  87. package/src/ward-view/materal-ward/maternal-ward-view.component.tsx +38 -0
  88. package/src/ward-view/materal-ward/maternal-ward-view.resource.ts +89 -0
  89. package/src/ward-view/ward-view.component.tsx +11 -151
  90. package/src/ward-view/ward-view.resource.ts +78 -6
  91. package/src/ward-view/ward-view.scss +1 -0
  92. package/src/ward-view/ward-view.test.tsx +10 -8
  93. package/src/ward-view/ward.component.tsx +106 -0
  94. package/src/ward-view-header/admission-requests-bar.component.tsx +10 -6
  95. package/src/ward-view-header/admission-requests-bar.test.tsx +5 -4
  96. package/src/ward-view-header/ward-metrics.component.tsx +12 -11
  97. package/src/ward-view-header/ward-metrics.test.tsx +4 -58
  98. package/src/ward-view-header/ward-view-header.component.tsx +6 -4
  99. package/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +7 -4
  100. package/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +9 -21
  101. package/src/ward-workspace/admission-request-card/admission-request-card.component.tsx +9 -3
  102. package/src/ward-workspace/admission-request-card/admission-request-card.scss +6 -1
  103. package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +11 -38
  104. package/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +8 -38
  105. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +80 -89
  106. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +21 -13
  107. package/src/ward-workspace/patient-banner/patient-banner.component.tsx +5 -12
  108. package/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx +2 -2
  109. package/src/ward-workspace/patient-details/ward-patient.workspace.tsx +13 -6
  110. package/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +6 -6
  111. package/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +7 -7
  112. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx +6 -6
  113. package/translations/en.json +7 -1
  114. package/dist/126.js +0 -1
  115. package/dist/126.js.map +0 -1
  116. package/dist/161.js +0 -2
  117. package/dist/161.js.map +0 -1
  118. package/dist/2.js +0 -1
  119. package/dist/2.js.map +0 -1
  120. package/dist/269.js +0 -1
  121. package/dist/269.js.map +0 -1
  122. package/dist/466.js +0 -1
  123. package/dist/466.js.map +0 -1
  124. package/dist/500.js.map +0 -1
  125. package/dist/557.js +0 -1
  126. package/dist/557.js.map +0 -1
  127. package/dist/659.js.map +0 -1
  128. package/dist/701.js +0 -1
  129. package/dist/701.js.map +0 -1
  130. package/dist/749.js +0 -1
  131. package/dist/749.js.map +0 -1
  132. package/dist/908.js +0 -1
  133. package/dist/908.js.map +0 -1
  134. package/src/beds/empty-bed.scss +0 -24
  135. package/src/beds/occupied-bed.component.tsx +0 -35
  136. package/src/beds/unassigned-patient.component.tsx +0 -20
  137. package/src/beds/unassigned-patient.scss +0 -6
  138. package/src/config-schema-admission-request-note.ts +0 -9
  139. package/src/config-schema-extension-colored-obs-tags.ts +0 -91
  140. package/src/config-schema-mother-child-row.ts +0 -26
  141. package/src/config-schema-pending-items-extension.ts +0 -29
  142. package/src/hooks/useCurrentWardCardConfig.ts +0 -32
  143. package/src/ward-patient-card/card-rows/admission-request-note.extension.tsx +0 -32
  144. package/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx +0 -13
  145. package/src/ward-patient-card/card-rows/mother-child-row.extension.tsx +0 -110
  146. package/src/ward-patient-card/ward-patient-card-element.component.tsx +0 -69
  147. package/src/ward-patient-card/ward-patient-resource.ts +0 -15
  148. package/src/ward-view/ward-bed.component.tsx +0 -14
  149. /package/src/ward-patient-card/row-elements/{ward-pateint-skeleton-text.tsx → ward-patient-skeleton-text.tsx} +0 -0
@@ -0,0 +1,33 @@
1
+ import { useAppContext } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { type WardViewContext } from '../../types';
4
+ import MaternalWardPatientCard from './maternal-ward-patient-card.component';
5
+
6
+ /**
7
+ * Renders a list of patients in the ward that are admitted but not assigned a bed
8
+ * @returns
9
+ */
10
+ function MaternalWardUnassignedPatients() {
11
+ const { wardPatientGroupDetails } = useAppContext<WardViewContext>('ward-view-context') ?? {};
12
+ const { wardUnassignedPatientsList } = wardPatientGroupDetails ?? {};
13
+
14
+ const wardUnassignedPatients = wardUnassignedPatientsList?.map((inpatientAdmission) => {
15
+ return (
16
+ <MaternalWardPatientCard
17
+ wardPatient={{
18
+ patient: inpatientAdmission.patient,
19
+ visit: inpatientAdmission.visit,
20
+ bed: null,
21
+ inpatientAdmission,
22
+ inpatientRequest: inpatientAdmission.currentInpatientRequest,
23
+ }}
24
+ childrenOfWardPatientInSameBed={[]}
25
+ key={inpatientAdmission.patient.uuid}
26
+ />
27
+ );
28
+ });
29
+
30
+ return <>{wardUnassignedPatients}</>;
31
+ }
32
+
33
+ export default MaternalWardUnassignedPatients;
@@ -0,0 +1,38 @@
1
+ import { useDefineAppContext } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { useWardPatientGrouping } from '../../hooks/useWardPatientGrouping';
4
+ import { type MaternalWardViewContext, type WardViewContext } from '../../types';
5
+ import WardViewHeader from '../../ward-view-header/ward-view-header.component';
6
+ import Ward from '../ward.component';
7
+ import MaternalWardBeds from './maternal-ward-beds.component';
8
+ import MaternalWardPatientCardHeader from './maternal-ward-patient-card-header.component';
9
+ import MaternalWardPendingPatients from './maternal-ward-pending-patients.component';
10
+ import MaternalWardUnassignedPatients from './maternal-ward-unassigned-patients.component';
11
+ import { useMotherChildrenRelationshipsByPatient } from './maternal-ward-view.resource';
12
+
13
+ const MaternalWardView = () => {
14
+ const wardPatientGroupDetails = useWardPatientGrouping();
15
+ useDefineAppContext<WardViewContext>('ward-view-context', {
16
+ wardPatientGroupDetails,
17
+ WardPatientHeader: MaternalWardPatientCardHeader,
18
+ });
19
+ const { allWardPatientUuids, isLoading } = wardPatientGroupDetails;
20
+
21
+ const motherChildRelationships = useMotherChildrenRelationshipsByPatient(Array.from(allWardPatientUuids), !isLoading);
22
+ useDefineAppContext<MaternalWardViewContext>('maternal-ward-view-context', {
23
+ motherChildRelationships,
24
+ });
25
+
26
+ const wardBeds = <MaternalWardBeds {...motherChildRelationships} />;
27
+ const wardUnassignedPatients = <MaternalWardUnassignedPatients />;
28
+ const wardPendingPatients = <MaternalWardPendingPatients />;
29
+
30
+ return (
31
+ <>
32
+ <WardViewHeader {...{ wardPendingPatients }} />
33
+ <Ward {...{ wardBeds, wardUnassignedPatients }} />
34
+ </>
35
+ );
36
+ };
37
+
38
+ export default MaternalWardView;
@@ -0,0 +1,89 @@
1
+ import { showNotification } from '@openmrs/esm-framework';
2
+ import { useMemo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useMotherAndChildren, type MothersAndChildrenSearchCriteria } from '../../hooks/useMotherAndChildren';
5
+ import { type PatientAndAdmission } from '../../types';
6
+
7
+ const motherAndChildrenRep =
8
+ 'custom:(childAdmission,mother:(person,identifiers:full,uuid),child:(person,identifiers:full,uuid),motherAdmission)';
9
+
10
+ export function useMotherChildrenRelationshipsByPatient(allWardPatientUuids: string[], fetch: boolean) {
11
+ const { t } = useTranslation();
12
+
13
+ const getChildrenRequestParams: MothersAndChildrenSearchCriteria = {
14
+ mothers: allWardPatientUuids,
15
+ requireMotherHasActiveVisit: true,
16
+ requireChildHasActiveVisit: true,
17
+ requireChildBornDuringMothersActiveVisit: true,
18
+ };
19
+
20
+ const getMotherRequestParams: MothersAndChildrenSearchCriteria = {
21
+ children: allWardPatientUuids,
22
+ requireMotherHasActiveVisit: true,
23
+ requireChildHasActiveVisit: true,
24
+ requireChildBornDuringMothersActiveVisit: true,
25
+ };
26
+
27
+ const {
28
+ data: childrenData,
29
+ isLoading: isLoadingChildrenData,
30
+ error: childrenDataError,
31
+ } = useMotherAndChildren(getChildrenRequestParams, fetch && allWardPatientUuids.length > 0, motherAndChildrenRep);
32
+ const {
33
+ data: motherData,
34
+ isLoading: isLoadingMotherData,
35
+ error: motherDataError,
36
+ } = useMotherAndChildren(getMotherRequestParams, fetch && allWardPatientUuids.length > 0, motherAndChildrenRep);
37
+
38
+ if (childrenDataError) {
39
+ showNotification({
40
+ title: t('errorLoadingChildren', 'Error loading children info'),
41
+ kind: 'error',
42
+ critical: true,
43
+ description: childrenDataError?.message,
44
+ });
45
+ }
46
+ if (motherDataError) {
47
+ showNotification({
48
+ title: t('errorLoadingMother', 'Error loading mother info'),
49
+ kind: 'error',
50
+ critical: true,
51
+ description: motherDataError?.message,
52
+ });
53
+ }
54
+
55
+ const relationships = useMemo(() => {
56
+ if (isLoadingChildrenData || isLoadingMotherData) {
57
+ return null;
58
+ }
59
+
60
+ const motherByChildUuid = new Map<string, PatientAndAdmission>();
61
+ const childrenByMotherUuid = new Map<string, PatientAndAdmission[]>();
62
+
63
+ for (const { child, childAdmission, mother, motherAdmission } of motherData ?? []) {
64
+ motherByChildUuid.set(child.uuid, { patient: mother, currentAdmission: motherAdmission });
65
+ if (!childrenByMotherUuid.has(mother.uuid)) {
66
+ childrenByMotherUuid.set(mother.uuid, []);
67
+ }
68
+ childrenByMotherUuid.get(mother.uuid).push({ patient: child, currentAdmission: childAdmission });
69
+ }
70
+
71
+ for (const { child, childAdmission, mother, motherAdmission } of childrenData ?? []) {
72
+ motherByChildUuid.set(child.uuid, { patient: mother, currentAdmission: motherAdmission });
73
+ if (!childrenByMotherUuid.has(mother.uuid)) {
74
+ childrenByMotherUuid.set(mother.uuid, []);
75
+ }
76
+
77
+ // careful, we need to avoid duplicate entries if both mother and child as in same ward
78
+ const children = childrenByMotherUuid.get(mother.uuid);
79
+ const hasChildAlready = children.some(({ patient }) => patient.uuid == child.uuid);
80
+ if (!hasChildAlready) {
81
+ children.push({ patient: child, currentAdmission: childAdmission });
82
+ }
83
+ }
84
+
85
+ return { motherByChildUuid, childrenByMotherUuid };
86
+ }, [childrenData, motherData, isLoadingChildrenData, isLoadingMotherData]);
87
+
88
+ return relationships;
89
+ }
@@ -1,26 +1,20 @@
1
1
  import { InlineNotification } from '@carbon/react';
2
- import { useAppContext, useDefineAppContext, useFeatureFlag, WorkspaceContainer } from '@openmrs/esm-framework';
3
- import React, { useEffect, useRef } from 'react';
2
+ import { ExtensionSlot, useFeatureFlag } from '@openmrs/esm-framework';
3
+ import classNames from 'classnames';
4
+ import React from 'react';
4
5
  import { useTranslation } from 'react-i18next';
5
- import EmptyBedSkeleton from '../beds/empty-bed-skeleton';
6
- import UnassignedPatient from '../beds/unassigned-patient.component';
7
6
  import useWardLocation from '../hooks/useWardLocation';
8
- import { useWardPatientGrouping } from '../hooks/useWardPatientGrouping';
9
- import { type WardPatient, type WardPatientGroupDetails } from '../types';
10
- import WardViewHeader from '../ward-view-header/ward-view-header.component';
11
- import WardBed from './ward-bed.component';
12
- import { bedLayoutToBed } from './ward-view.resource';
7
+ import { useWardConfig } from './ward-view.resource';
13
8
  import styles from './ward-view.scss';
14
- import classNames from 'classnames';
15
9
 
16
- const WardView = () => {
10
+ const WardView: React.FC<{}> = () => {
17
11
  const response = useWardLocation();
18
- const { isLoadingLocation, invalidLocation } = response;
12
+ const { isLoadingLocation, invalidLocation, location } = response;
19
13
  const { t } = useTranslation();
20
14
 
21
- const wardPatientsGroupDetails = useWardPatientGrouping();
22
- useDefineAppContext<WardPatientGroupDetails>('ward-patients-group', wardPatientsGroupDetails);
15
+ const locationUuid = location?.uuid;
23
16
  const isVertical = useFeatureFlag('ward-view-vertical-tiling');
17
+ const wardConfig = useWardConfig(locationUuid);
24
18
 
25
19
  if (isLoadingLocation) {
26
20
  return <></>;
@@ -30,147 +24,13 @@ const WardView = () => {
30
24
  return <InlineNotification kind="error" title={t('invalidLocationSpecified', 'Invalid location specified')} />;
31
25
  }
32
26
 
33
- return (
34
- <>
35
- <div className={classNames(styles.wardView, { [styles.verticalTiling]: isVertical })}>
36
- <WardViewHeader />
37
- <WardViewMain />
38
- </div>
39
- <WorkspaceContainer overlay contextKey="ward" />
40
- </>
41
- );
42
- };
43
-
44
- const WardViewMain = () => {
45
- const { location } = useWardLocation();
46
- const { t } = useTranslation();
47
- const isVertical = useFeatureFlag('ward-view-vertical-tiling');
48
-
49
- const wardPatientsGrouping = useAppContext<WardPatientGroupDetails>('ward-patients-group');
50
- const { bedLayouts, wardAdmittedPatientsWithBed, wardUnassignedPatientsList } = wardPatientsGrouping ?? {};
51
- const { isLoading: isLoadingAdmissionLocation, error: errorLoadingAdmissionLocation } =
52
- wardPatientsGrouping?.admissionLocationResponse ?? {};
53
- const {
54
- isLoading: isLoadingInpatientAdmissions,
55
- error: errorLoadingInpatientAdmissions,
56
- hasMore: hasMoreInpatientAdmissions,
57
- loadMore: loadMoreInpatientAdmissions,
58
- } = wardPatientsGrouping?.inpatientAdmissionResponse ?? {};
59
- const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
60
-
61
- const scrollToLoadMoreTrigger = useRef<HTMLDivElement>(null);
62
- useEffect(
63
- function scrollToLoadMore() {
64
- const observer = new IntersectionObserver(
65
- (entries) => {
66
- entries.forEach((entry) => {
67
- if (entry.isIntersecting) {
68
- if (hasMoreInpatientAdmissions && !errorLoadingInpatientAdmissions && !isLoadingInpatientAdmissions) {
69
- loadMoreInpatientAdmissions();
70
- }
71
- }
72
- });
73
- },
74
- { threshold: 1 },
75
- );
76
-
77
- if (scrollToLoadMoreTrigger.current) {
78
- observer.observe(scrollToLoadMoreTrigger.current);
79
- }
80
- return () => {
81
- if (scrollToLoadMoreTrigger.current) {
82
- observer.unobserve(scrollToLoadMoreTrigger.current);
83
- }
84
- };
85
- },
86
- [scrollToLoadMoreTrigger, hasMoreInpatientAdmissions, errorLoadingInpatientAdmissions, loadMoreInpatientAdmissions],
87
- );
88
-
89
- if (!wardPatientsGrouping) return <></>;
90
-
91
- const wardBeds = bedLayouts?.map((bedLayout) => {
92
- const { patients } = bedLayout;
93
- const bed = bedLayoutToBed(bedLayout);
94
- const wardPatients: WardPatient[] = patients.map((patient): WardPatient => {
95
- const inpatientAdmission = wardAdmittedPatientsWithBed.get(patient.uuid);
96
- if (inpatientAdmission) {
97
- const { patient, visit, currentInpatientRequest } = inpatientAdmission;
98
- return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null };
99
- } else {
100
- // for some reason this patient is in a bed but not in the list of admitted patients, so we need to use the patient data from the bed endpoint
101
- return {
102
- patient: patient,
103
- visit: null,
104
- bed,
105
- inpatientAdmission: null, // populate after BED-13
106
- inpatientRequest: null,
107
- };
108
- }
109
- });
110
- return <WardBed key={bed.uuid} bed={bed} wardPatients={wardPatients} />;
111
- });
112
-
113
- const wardUnassignedPatients = wardUnassignedPatientsList?.map((inpatientAdmission) => {
114
- return (
115
- <UnassignedPatient
116
- wardPatient={{
117
- patient: inpatientAdmission.patient,
118
- visit: inpatientAdmission.visit,
119
- bed: null,
120
- inpatientAdmission,
121
- inpatientRequest: null,
122
- }}
123
- key={inpatientAdmission.patient.uuid}
124
- />
125
- );
126
- });
27
+ const wardId = wardConfig.id;
127
28
 
128
29
  return (
129
- <div className={classNames(styles.wardViewMain, { [styles.verticalTiling]: isVertical })}>
130
- {wardBeds}
131
- {bedLayouts?.length == 0 && isBedManagementModuleInstalled && (
132
- <InlineNotification
133
- kind="warning"
134
- lowContrast={true}
135
- title={t('noBedsConfigured', 'No beds configured for this location')}
136
- />
137
- )}
138
- {wardUnassignedPatients}
139
- {(isLoadingAdmissionLocation || isLoadingInpatientAdmissions) && <EmptyBeds />}
140
- {errorLoadingAdmissionLocation && (
141
- <InlineNotification
142
- kind="error"
143
- lowContrast={true}
144
- title={t('errorLoadingWardLocation', 'Error loading ward location')}
145
- subtitle={
146
- errorLoadingAdmissionLocation?.message ??
147
- t('invalidWardLocation', 'Invalid ward location: {{location}}', { location: location.display })
148
- }
149
- />
150
- )}
151
- {errorLoadingInpatientAdmissions && (
152
- <InlineNotification
153
- kind="error"
154
- lowContrast={true}
155
- title={t('errorLoadingPatients', 'Error loading admitted patients')}
156
- subtitle={errorLoadingInpatientAdmissions?.message}
157
- />
158
- )}
159
- <div ref={scrollToLoadMoreTrigger}></div>
30
+ <div className={classNames(styles.wardView, { [styles.verticalTiling]: isVertical })}>
31
+ <ExtensionSlot name={wardId} />
160
32
  </div>
161
33
  );
162
34
  };
163
35
 
164
- const EmptyBeds = () => {
165
- return (
166
- <>
167
- {Array(20)
168
- .fill(0)
169
- .map((_, i) => (
170
- <EmptyBedSkeleton key={i} />
171
- ))}
172
- </>
173
- );
174
- };
175
-
176
36
  export default WardView;
@@ -1,4 +1,16 @@
1
- import { type Patient } from '@openmrs/esm-framework';
1
+ import { showNotification, useConfig, type Patient } from '@openmrs/esm-framework';
2
+ import type { TFunction } from 'i18next';
3
+ import { useMemo } from 'react';
4
+ import {
5
+ type PendingItemsElementConfig,
6
+ type ColoredObsTagsElementConfig,
7
+ type IdentifierElementConfig,
8
+ type ObsElementConfig,
9
+ type PatientAddressElementConfig,
10
+ type WardConfigObject,
11
+ type WardDefinition,
12
+ type AdmissionRequestNoteElementConfig,
13
+ } from '../config-schema';
2
14
  import type {
3
15
  AdmissionLocationFetchResponse,
4
16
  Bed,
@@ -8,7 +20,7 @@ import type {
8
20
  WardMetrics,
9
21
  WardPatientGroupDetails,
10
22
  } from '../types';
11
- import type { TFunction } from 'i18next';
23
+ import { useTranslation } from 'react-i18next';
12
24
 
13
25
  // the server side has 2 slightly incompatible types for Bed
14
26
  export function bedLayoutToBed(bedLayout: BedLayout): Bed {
@@ -34,7 +46,6 @@ export function filterBeds(admissionLocation: AdmissionLocationFetchResponse): B
34
46
  return bedLayouts;
35
47
  }
36
48
 
37
- //TODO: This implementation will change when the api is ready
38
49
  export function getWardMetrics(bedLayouts: BedLayout[], wardPatientGroup: WardPatientGroupDetails): WardMetrics {
39
50
  const bedMetrics = {
40
51
  patients: '--',
@@ -43,7 +54,7 @@ export function getWardMetrics(bedLayouts: BedLayout[], wardPatientGroup: WardPa
43
54
  };
44
55
  if (bedLayouts == null || bedLayouts.length == 0) return bedMetrics;
45
56
  const total = bedLayouts.length;
46
- const occupiedBeds = bedLayouts.filter((bed) => bed.patients.length>0);
57
+ const occupiedBeds = bedLayouts.filter((bed) => bed.patients.length > 0);
47
58
  const patients = occupiedBeds.length;
48
59
  const freeBeds = total - patients;
49
60
  const capacity = total != 0 ? Math.trunc((wardPatientGroup.totalPatientsCount / total) * 100) : 0;
@@ -57,7 +68,10 @@ export function getWardMetrics(bedLayouts: BedLayout[], wardPatientGroup: WardPa
57
68
  export function getInpatientAdmissionsUuidMap(inpatientAdmissions: InpatientAdmission[]) {
58
69
  const map = new Map<string, InpatientAdmission>();
59
70
  for (const inpatientAdmission of inpatientAdmissions ?? []) {
60
- map.set(inpatientAdmission.patient.uuid, inpatientAdmission);
71
+ // TODO: inpatientAdmission is undefined sometimes, why?
72
+ if (inpatientAdmission) {
73
+ map.set(inpatientAdmission.patient.uuid, inpatientAdmission);
74
+ }
61
75
  }
62
76
  return map;
63
77
  }
@@ -103,7 +117,10 @@ export function createAndGetWardPatientGrouping(
103
117
  const totalPatientsCount = allWardPatientUuids.size;
104
118
 
105
119
  for (const inpatientRequest of inpatientRequests ?? []) {
106
- allWardPatientUuids.add(inpatientRequest.patient.uuid);
120
+ // TODO: inpatientRequest is undefined sometimes, why?
121
+ if (inpatientRequest) {
122
+ allWardPatientUuids.add(inpatientRequest.patient.uuid);
123
+ }
107
124
  }
108
125
 
109
126
  return {
@@ -142,3 +159,58 @@ export function getWardMetricValueTranslation(name: string, t: TFunction, value:
142
159
  return t('pendingOutMetricValue', '{{ metricValue }}', { metricValue: value });
143
160
  }
144
161
  }
162
+
163
+ export function useElementConfig(elementType: 'obs', id: string): ObsElementConfig;
164
+ export function useElementConfig(elementType: 'patientIdentifier', id: string): IdentifierElementConfig;
165
+ export function useElementConfig(elementType: 'patientAddress', id: string): PatientAddressElementConfig;
166
+ export function useElementConfig(elementType: 'coloredObsTags', id: string): ColoredObsTagsElementConfig;
167
+ export function useElementConfig(elementType: 'pendingItems', id: string): PendingItemsElementConfig;
168
+ export function useElementConfig(elementType: 'admissionRequestNote', id: string): AdmissionRequestNoteElementConfig;
169
+ export function useElementConfig(elementType, id: string): object {
170
+ const config = useConfig<WardConfigObject>();
171
+ const { t } = useTranslation();
172
+
173
+ try {
174
+ return config?.patientCardElements?.[elementType]?.find((elementConfig) => elementConfig?.id == id);
175
+ } catch (e) {
176
+ showNotification({
177
+ title: t('errorConfiguringPatientCard', 'Error configuring patient card'),
178
+ kind: 'error',
179
+ critical: true,
180
+ description: t(
181
+ 'errorConfiguringPatientCardMessage',
182
+ 'Unable to find configuration for {{elementType}}, id: {{id}}',
183
+ {
184
+ elementType,
185
+ id,
186
+ },
187
+ ),
188
+ });
189
+ return null;
190
+ }
191
+ }
192
+
193
+ export function useWardConfig(locationUuid: string): WardDefinition {
194
+ const { wards } = useConfig<WardConfigObject>();
195
+
196
+ const currentWardConfig = useMemo(() => {
197
+ const cardDefinition = wards?.find((wardDef) => {
198
+ return (
199
+ wardDef.appliedTo == null ||
200
+ wardDef.appliedTo?.length == 0 ||
201
+ wardDef.appliedTo.some((criteria) => criteria.location == locationUuid)
202
+ );
203
+ });
204
+
205
+ return cardDefinition;
206
+ }, [wards, locationUuid]);
207
+
208
+ if (!currentWardConfig) {
209
+ console.warn(
210
+ 'No ward card configuration has `appliedTo` criteria that matches the current location. Using the default configuration.',
211
+ );
212
+ return { id: 'default-ward' };
213
+ }
214
+
215
+ return currentWardConfig;
216
+ }
@@ -22,6 +22,7 @@
22
22
  display: block;
23
23
  column-width: 280px;
24
24
  overflow-x: auto;
25
+ height: calc(100% - 4rem);
25
26
 
26
27
  > div {
27
28
  break-inside: avoid;
@@ -9,9 +9,11 @@ import { screen } from '@testing-library/react';
9
9
  import React from 'react';
10
10
  import { useParams } from 'react-router-dom';
11
11
  import { renderWithSwr } from 'tools';
12
- import { mockWardPatientGroupDetails } from '../../mock';
12
+ import { mockWardPatientGroupDetails, mockWardViewContext } from '../../mock';
13
13
  import { configSchema } from '../config-schema';
14
14
  import useWardLocation from '../hooks/useWardLocation';
15
+ import { type WardViewContext } from '../types';
16
+ import DefaultWardView from './default-ward/default-ward-view.component';
15
17
  import WardView from './ward-view.component';
16
18
 
17
19
  jest.mocked(useConfig).mockReturnValue({
@@ -37,7 +39,7 @@ jest.mock('react-router-dom', () => ({
37
39
  }));
38
40
  const mockUseParams = useParams as jest.Mock;
39
41
 
40
- jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails());
42
+ jest.mocked(useAppContext<WardViewContext>).mockReturnValue(mockWardViewContext);
41
43
 
42
44
  const intersectionObserverMock = () => ({
43
45
  observe: () => null,
@@ -48,33 +50,33 @@ describe('WardView', () => {
48
50
  let replacedProperty: jest.ReplaceProperty<any> | null = null;
49
51
 
50
52
  it('renders the session location when no location provided in URL', () => {
51
- renderWithSwr(<WardView />);
53
+ renderWithSwr(<DefaultWardView />);
52
54
  const header = screen.getByRole('heading', { name: 'mock location' });
53
55
  expect(header).toBeInTheDocument();
54
56
  });
55
57
 
56
58
  it('renders the location provided in URL', () => {
57
59
  mockUseParams.mockReturnValueOnce({ locationUuid: 'abcd' });
58
- renderWithSwr(<WardView />);
60
+ renderWithSwr(<DefaultWardView />);
59
61
  const header = screen.getByRole('heading', { name: 'mock location' });
60
62
  expect(header).toBeInTheDocument();
61
63
  });
62
64
 
63
65
  it('renders the correct number of occupied and empty beds', async () => {
64
- renderWithSwr(<WardView />);
66
+ renderWithSwr(<DefaultWardView />);
65
67
  const emptyBedCards = await screen.findAllByText(/empty bed/i);
66
68
  expect(emptyBedCards).toHaveLength(3);
67
69
  });
68
70
 
69
71
  it('renders admitted patient without bed', async () => {
70
- renderWithSwr(<WardView />);
72
+ renderWithSwr(<DefaultWardView />);
71
73
  const admittedPatientWithoutBed = screen.queryByText('Brian Johnson');
72
74
  expect(admittedPatientWithoutBed).toBeInTheDocument();
73
75
  });
74
76
 
75
77
  it('renders all admitted patients even if bed management module not installed', async () => {
76
78
  mockUseFeatureFlag.mockReturnValueOnce(false);
77
- renderWithSwr(<WardView />);
79
+ renderWithSwr(<DefaultWardView />);
78
80
  const admittedPatientWithoutBed = screen.queryByText('Brian Johnson');
79
81
  expect(admittedPatientWithoutBed).toBeInTheDocument();
80
82
  });
@@ -100,7 +102,7 @@ describe('WardView', () => {
100
102
 
101
103
  mockUseFeatureFlag.mockReturnValue(true);
102
104
 
103
- renderWithSwr(<WardView />);
105
+ renderWithSwr(<DefaultWardView />);
104
106
  const noBedsConfiguredForThisLocation = screen.queryByText('No beds configured for this location');
105
107
  expect(noBedsConfiguredForThisLocation).toBeInTheDocument();
106
108
  });
@@ -0,0 +1,106 @@
1
+ import { InlineNotification } from '@carbon/react';
2
+ import { useAppContext, useFeatureFlag } from '@openmrs/esm-framework';
3
+ import classNames from 'classnames';
4
+ import React, { useEffect, useRef, type ReactNode } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import EmptyBedSkeleton from '../beds/empty-bed-skeleton';
7
+ import useWardLocation from '../hooks/useWardLocation';
8
+ import { type WardViewContext } from '../types';
9
+ import styles from './ward-view.scss';
10
+
11
+ const Ward = ({ wardBeds, wardUnassignedPatients }: { wardBeds: ReactNode; wardUnassignedPatients: ReactNode }) => {
12
+ const { location } = useWardLocation();
13
+ const { t } = useTranslation();
14
+ const isVertical = useFeatureFlag('ward-view-vertical-tiling');
15
+
16
+ const {wardPatientGroupDetails} = useAppContext<WardViewContext>('ward-view-context') ?? {};
17
+ const { bedLayouts } = wardPatientGroupDetails ?? {};
18
+ const { isLoading: isLoadingAdmissionLocation, error: errorLoadingAdmissionLocation } =
19
+ wardPatientGroupDetails?.admissionLocationResponse ?? {};
20
+ const {
21
+ isLoading: isLoadingInpatientAdmissions,
22
+ error: errorLoadingInpatientAdmissions,
23
+ hasMore: hasMoreInpatientAdmissions,
24
+ loadMore: loadMoreInpatientAdmissions,
25
+ } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {};
26
+ const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
27
+
28
+ const scrollToLoadMoreTrigger = useRef<HTMLDivElement>(null);
29
+ useEffect(
30
+ function scrollToLoadMore() {
31
+ const observer = new IntersectionObserver(
32
+ (entries) => {
33
+ entries.forEach((entry) => {
34
+ if (entry.isIntersecting) {
35
+ if (hasMoreInpatientAdmissions && !errorLoadingInpatientAdmissions && !isLoadingInpatientAdmissions) {
36
+ loadMoreInpatientAdmissions();
37
+ }
38
+ }
39
+ });
40
+ },
41
+ { threshold: 1 },
42
+ );
43
+
44
+ if (scrollToLoadMoreTrigger.current) {
45
+ observer.observe(scrollToLoadMoreTrigger.current);
46
+ }
47
+ return () => {
48
+ if (scrollToLoadMoreTrigger.current) {
49
+ observer.unobserve(scrollToLoadMoreTrigger.current);
50
+ }
51
+ };
52
+ },
53
+ [scrollToLoadMoreTrigger, hasMoreInpatientAdmissions, errorLoadingInpatientAdmissions, loadMoreInpatientAdmissions],
54
+ );
55
+
56
+ if (!wardPatientGroupDetails) return <></>;
57
+
58
+ return (
59
+ <div className={classNames(styles.wardViewMain, { [styles.verticalTiling]: isVertical })}>
60
+ {wardBeds}
61
+ {bedLayouts?.length == 0 && isBedManagementModuleInstalled && (
62
+ <InlineNotification
63
+ kind="warning"
64
+ lowContrast={true}
65
+ title={t('noBedsConfigured', 'No beds configured for this location')}
66
+ />
67
+ )}
68
+ {wardUnassignedPatients}
69
+ {(isLoadingAdmissionLocation || isLoadingInpatientAdmissions) && <EmptyBeds />}
70
+ {errorLoadingAdmissionLocation && (
71
+ <InlineNotification
72
+ kind="error"
73
+ lowContrast={true}
74
+ title={t('errorLoadingWardLocation', 'Error loading ward location')}
75
+ subtitle={
76
+ errorLoadingAdmissionLocation?.message ??
77
+ t('invalidWardLocation', 'Invalid ward location: {{location}}', { location: location.display })
78
+ }
79
+ />
80
+ )}
81
+ {errorLoadingInpatientAdmissions && (
82
+ <InlineNotification
83
+ kind="error"
84
+ lowContrast={true}
85
+ title={t('errorLoadingPatients', 'Error loading admitted patients')}
86
+ subtitle={errorLoadingInpatientAdmissions?.message}
87
+ />
88
+ )}
89
+ <div ref={scrollToLoadMoreTrigger}></div>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const EmptyBeds = () => {
95
+ return (
96
+ <>
97
+ {Array(20)
98
+ .fill(0)
99
+ .map((_, i) => (
100
+ <EmptyBedSkeleton key={i} />
101
+ ))}
102
+ </>
103
+ );
104
+ };
105
+
106
+ export default Ward;