@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.
- package/.turbo/turbo-build.log +16 -20
- package/dist/124.js +1 -0
- package/dist/124.js.map +1 -0
- package/dist/125.js +1 -1
- package/dist/125.js.map +1 -1
- package/dist/130.js +1 -1
- package/dist/130.js.map +1 -1
- package/dist/153.js +1 -0
- package/dist/153.js.map +1 -0
- package/dist/372.js +1 -1
- package/dist/372.js.map +1 -1
- package/dist/471.js +1 -0
- package/dist/471.js.map +1 -0
- package/dist/481.js +1 -0
- package/dist/481.js.map +1 -0
- package/dist/53.js +1 -1
- package/dist/53.js.map +1 -1
- package/dist/559.js +1 -1
- package/dist/559.js.map +1 -1
- package/dist/574.js +1 -1
- package/dist/{500.js → 576.js} +1 -1
- package/dist/576.js.map +1 -0
- package/dist/577.js +1 -1
- package/dist/577.js.map +1 -1
- package/dist/649.js +2 -0
- package/dist/{161.js.LICENSE.txt → 649.js.LICENSE.txt} +0 -6
- package/dist/649.js.map +1 -0
- package/dist/{659.js → 662.js} +1 -1
- package/dist/662.js.map +1 -0
- package/dist/67.js +2 -0
- package/dist/67.js.LICENSE.txt +5 -0
- package/dist/67.js.map +1 -0
- package/dist/920.js +1 -0
- package/dist/920.js.map +1 -0
- package/dist/921.js +1 -0
- package/dist/921.js.map +1 -0
- package/dist/922.js +1 -1
- package/dist/922.js.map +1 -1
- package/dist/kenyaemr-esm-ward-app.js +1 -1
- package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +148 -165
- package/dist/kenyaemr-esm-ward-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +0 -10
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/mock.tsx +8 -0
- package/package.json +1 -1
- package/src/beds/empty-bed-skeleton.tsx +3 -3
- package/src/beds/empty-bed.component.tsx +3 -3
- package/src/beds/ward-bed.component.tsx +41 -0
- package/src/beds/ward-bed.scss +45 -0
- package/src/beds/{occupied-bed.test.tsx → ward-bed.test.tsx} +27 -16
- package/src/config-schema.ts +196 -75
- package/src/hooks/useAssignedBedByPatient.ts +9 -0
- package/src/hooks/useInpatientAdmission.ts +1 -1
- package/src/hooks/useMotherAndChildren.ts +2 -2
- package/src/hooks/useObs.ts +2 -2
- package/src/hooks/useWardPatientGrouping.ts +2 -0
- package/src/index.ts +10 -29
- package/src/root.component.tsx +3 -0
- package/src/routes.json +6 -11
- package/src/types/index.ts +29 -14
- package/src/ward-patient-card/card-rows/admission-request-note-row.component.tsx +38 -0
- package/src/ward-patient-card/card-rows/coded-obs-tags-row.component.tsx +108 -0
- package/src/ward-patient-card/card-rows/mother-child-row.component.tsx +84 -0
- package/src/ward-patient-card/card-rows/{pending-items-car-row.extension.tsx → pending-items-row.component.tsx} +12 -8
- package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +12 -7
- package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +5 -5
- package/src/ward-patient-card/row-elements/ward-patient-identifier.tsx +18 -40
- package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +2 -2
- package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +15 -8
- package/src/ward-patient-card/ward-patient-card.component.tsx +16 -54
- package/src/ward-patient-card/ward-patient-card.scss +7 -1
- package/src/ward-view/default-ward/default-ward-beds.component.tsx +42 -0
- package/src/ward-view/default-ward/default-ward-patient-card-header.component.tsx +32 -0
- package/src/ward-view/default-ward/default-ward-patient-card.component.tsx +31 -0
- package/src/ward-view/default-ward/default-ward-pending-patients.component.tsx +52 -0
- package/src/ward-view/default-ward/default-ward-unassigned-patients.component.tsx +32 -0
- package/src/ward-view/default-ward/default-ward-view.component.tsx +31 -0
- package/src/ward-view/materal-ward/maternal-ward-beds.component.tsx +65 -0
- package/src/ward-view/materal-ward/maternal-ward-patient-card-header.component.tsx +30 -0
- package/src/ward-view/materal-ward/maternal-ward-patient-card.component.tsx +93 -0
- package/src/{beds/occupied-bed.scss → ward-view/materal-ward/maternal-ward-patient-card.scss} +4 -10
- package/src/ward-view/materal-ward/maternal-ward-patient-card.test.tsx +47 -0
- package/src/ward-view/materal-ward/maternal-ward-pending-patients.component.tsx +48 -0
- package/src/ward-view/materal-ward/maternal-ward-unassigned-patients.component.tsx +33 -0
- package/src/ward-view/materal-ward/maternal-ward-view.component.tsx +38 -0
- package/src/ward-view/materal-ward/maternal-ward-view.resource.ts +89 -0
- package/src/ward-view/ward-view.component.tsx +11 -151
- package/src/ward-view/ward-view.resource.ts +78 -6
- package/src/ward-view/ward-view.scss +1 -0
- package/src/ward-view/ward-view.test.tsx +10 -8
- package/src/ward-view/ward.component.tsx +106 -0
- package/src/ward-view-header/admission-requests-bar.component.tsx +10 -6
- package/src/ward-view-header/admission-requests-bar.test.tsx +5 -4
- package/src/ward-view-header/ward-metrics.component.tsx +12 -11
- package/src/ward-view-header/ward-metrics.test.tsx +4 -58
- package/src/ward-view-header/ward-view-header.component.tsx +6 -4
- package/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +7 -4
- package/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +9 -21
- package/src/ward-workspace/admission-request-card/admission-request-card.component.tsx +9 -3
- package/src/ward-workspace/admission-request-card/admission-request-card.scss +6 -1
- package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +11 -38
- package/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +8 -38
- package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +80 -89
- package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +21 -13
- package/src/ward-workspace/patient-banner/patient-banner.component.tsx +5 -12
- package/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx +2 -2
- package/src/ward-workspace/patient-details/ward-patient.workspace.tsx +13 -6
- package/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +6 -6
- package/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +7 -7
- package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx +6 -6
- package/translations/en.json +7 -1
- package/dist/126.js +0 -1
- package/dist/126.js.map +0 -1
- package/dist/161.js +0 -2
- package/dist/161.js.map +0 -1
- package/dist/2.js +0 -1
- package/dist/2.js.map +0 -1
- package/dist/269.js +0 -1
- package/dist/269.js.map +0 -1
- package/dist/466.js +0 -1
- package/dist/466.js.map +0 -1
- package/dist/500.js.map +0 -1
- package/dist/557.js +0 -1
- package/dist/557.js.map +0 -1
- package/dist/659.js.map +0 -1
- package/dist/701.js +0 -1
- package/dist/701.js.map +0 -1
- package/dist/749.js +0 -1
- package/dist/749.js.map +0 -1
- package/dist/908.js +0 -1
- package/dist/908.js.map +0 -1
- package/src/beds/empty-bed.scss +0 -24
- package/src/beds/occupied-bed.component.tsx +0 -35
- package/src/beds/unassigned-patient.component.tsx +0 -20
- package/src/beds/unassigned-patient.scss +0 -6
- package/src/config-schema-admission-request-note.ts +0 -9
- package/src/config-schema-extension-colored-obs-tags.ts +0 -91
- package/src/config-schema-mother-child-row.ts +0 -26
- package/src/config-schema-pending-items-extension.ts +0 -29
- package/src/hooks/useCurrentWardCardConfig.ts +0 -32
- package/src/ward-patient-card/card-rows/admission-request-note.extension.tsx +0 -32
- package/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx +0 -13
- package/src/ward-patient-card/card-rows/mother-child-row.extension.tsx +0 -110
- package/src/ward-patient-card/ward-patient-card-element.component.tsx +0 -69
- package/src/ward-patient-card/ward-patient-resource.ts +0 -15
- package/src/ward-view/ward-bed.component.tsx +0 -14
- /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 {
|
|
3
|
-
import
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
130
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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(
|
|
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(<
|
|
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(<
|
|
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(<
|
|
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(<
|
|
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(<
|
|
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(<
|
|
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;
|