@openmrs/esm-ward-app 7.0.1-pre.59 → 7.1.0
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 +28 -32
- package/dist/130.js +1 -1
- package/dist/130.js.map +1 -1
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/{730.js.LICENSE.txt → 255.js.LICENSE.txt} +0 -6
- package/dist/255.js.map +1 -0
- package/dist/303.js +1 -0
- package/dist/303.js.map +1 -0
- package/dist/331.js +1 -0
- package/dist/331.js.map +1 -0
- package/dist/430.js +1 -0
- package/dist/430.js.map +1 -0
- package/dist/443.js +1 -0
- package/dist/443.js.map +1 -0
- package/dist/574.js +1 -1
- package/dist/729.js +1 -1
- package/dist/729.js.map +1 -1
- package/dist/747.js +1 -0
- package/dist/747.js.map +1 -0
- package/dist/836.js +2 -0
- package/dist/836.js.LICENSE.txt +5 -0
- package/dist/836.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-ward-app.js +1 -1
- package/dist/openmrs-esm-ward-app.js.buildmanifest.json +194 -50
- package/dist/openmrs-esm-ward-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -3
- package/src/{empty-beds → beds}/empty-bed-skeleton.tsx +1 -1
- package/src/beds/empty-bed.component.tsx +24 -0
- package/src/beds/occupied-bed.component.tsx +35 -0
- package/src/beds/occupied-bed.scss +24 -0
- package/src/beds/occupied-bed.test.tsx +52 -0
- package/src/config-schema.ts +300 -3
- package/src/constant.ts +1 -0
- package/src/createDashboardLink.component.tsx +42 -0
- package/src/hooks/useAdmissionLocation.ts +4 -5
- package/src/hooks/useAdmittedPatients.ts +13 -0
- package/src/hooks/useConcept.ts +11 -0
- package/src/hooks/useInpatientRequest.ts +15 -0
- package/src/hooks/useLocation.test.ts +38 -0
- package/src/hooks/useLocation.ts +9 -0
- package/src/hooks/useObs.ts +21 -0
- package/src/hooks/useWardLocation.test.ts +78 -0
- package/src/hooks/useWardLocation.ts +26 -0
- package/src/index.ts +30 -5
- package/src/root.component.tsx +2 -1
- package/src/routes.json +46 -6
- package/src/types/index.ts +113 -2
- package/src/ward-patient-card/row-elements/ward-patient-age.tsx +20 -0
- package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +16 -0
- package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +80 -0
- package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +24 -0
- package/src/ward-patient-card/row-elements/ward-patient-name.tsx +13 -0
- package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +52 -0
- package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +58 -0
- package/src/ward-patient-card/ward-patient-card-row.resources.tsx +92 -0
- package/src/ward-patient-card/ward-patient-card.scss +114 -0
- package/src/ward-patient-card/ward-patient-card.tsx +30 -0
- package/src/ward-patient-workspace/ward-patient-action-button.extension.tsx +18 -0
- package/src/ward-patient-workspace/ward-patient.style.scss +11 -0
- package/src/ward-patient-workspace/ward-patient.workspace.tsx +79 -0
- package/src/ward-view/ward-bed.component.tsx +8 -8
- package/src/ward-view/ward-view.component.tsx +86 -44
- package/src/ward-view/ward-view.resource.ts +11 -1
- package/src/ward-view/ward-view.scss +0 -4
- package/src/ward-view/ward-view.test.tsx +64 -20
- package/src/ward-view-header/admission-requests-bar.component.tsx +50 -0
- package/src/ward-view-header/admission-requests-bar.test.tsx +43 -0
- package/src/ward-view-header/admission-requests.scss +33 -0
- package/src/ward-view-header/ward-view-header.component.tsx +18 -0
- package/src/ward-view-header/ward-view-header.scss +8 -0
- package/src/ward-workspace/admission-request-card.component.tsx +23 -0
- package/src/ward-workspace/admission-request-card.scss +34 -0
- package/src/ward-workspace/admission-request-workspace.test.tsx +32 -0
- package/src/ward-workspace/admission-requests-workspace.component.tsx +21 -0
- package/src/ward-workspace/admission-requests-workspace.scss +13 -0
- package/translations/en.json +8 -1
- package/dist/588.js +0 -1
- package/dist/588.js.map +0 -1
- package/dist/669.js +0 -1
- package/dist/669.js.map +0 -1
- package/dist/730.js +0 -2
- package/dist/730.js.map +0 -1
- package/src/empty-beds/empty-bed.component.tsx +0 -19
- /package/src/{empty-beds → beds}/empty-bed.scss +0 -0
- /package/src/{admitted-patient/admitted-patient-header.scss → ward-patient-card/row-elements/row-elements.scss} +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { type WardPatientCardProps } from '../types';
|
|
4
|
+
import { usePatientCardRows } from './ward-patient-card-row.resources';
|
|
5
|
+
import styles from './ward-patient-card.scss';
|
|
6
|
+
import { getPatientName, launchWorkspace } from '@openmrs/esm-framework';
|
|
7
|
+
import { type WardPatientWorkspaceProps } from '../ward-patient-workspace/ward-patient.workspace';
|
|
8
|
+
|
|
9
|
+
const WardPatientCard: React.FC<WardPatientCardProps> = (props) => {
|
|
10
|
+
const { locationUuid } = useParams();
|
|
11
|
+
const patientCardRows = usePatientCardRows(locationUuid);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={styles.wardPatientCard}>
|
|
15
|
+
{patientCardRows.map((WardPatientCardRow, i) => (
|
|
16
|
+
<WardPatientCardRow key={i} {...props} />
|
|
17
|
+
))}
|
|
18
|
+
<button
|
|
19
|
+
className={styles.wardPatientCardButton}
|
|
20
|
+
onClick={() =>
|
|
21
|
+
launchWorkspace<WardPatientWorkspaceProps>('ward-patient-workspace', { patientUuid: props.patient.uuid })
|
|
22
|
+
}>
|
|
23
|
+
{/* Name will not be displayed; just there for a11y */}
|
|
24
|
+
{getPatientName(props.patient.person)}
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default WardPatientCard;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { UserAvatarIcon } from '@openmrs/esm-framework';
|
|
4
|
+
import { ActionMenuButton, launchWorkspace } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
export default function WardPatientActionButton() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<ActionMenuButton
|
|
11
|
+
getIcon={(props) => <UserAvatarIcon {...props} />}
|
|
12
|
+
label={t('Patient', 'patient')}
|
|
13
|
+
iconDescription={t('Patient', 'patient')}
|
|
14
|
+
handler={() => launchWorkspace('ward-patient-workspace')}
|
|
15
|
+
type={'ward'}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
|
|
4
|
+
.workspaceContainer {
|
|
5
|
+
min-height: var(--desktop-workspace-window-height);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.headerPatientDetail {
|
|
9
|
+
@include type.type-style('body-compact-02');
|
|
10
|
+
margin: 0 spacing.$spacing-02;
|
|
11
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { InlineNotification } from '@carbon/react';
|
|
4
|
+
import { InlineLoading } from '@carbon/react';
|
|
5
|
+
import {
|
|
6
|
+
type DefaultWorkspaceProps,
|
|
7
|
+
ExtensionSlot,
|
|
8
|
+
attach,
|
|
9
|
+
getPatientName,
|
|
10
|
+
usePatient,
|
|
11
|
+
age,
|
|
12
|
+
} from '@openmrs/esm-framework';
|
|
13
|
+
import styles from './ward-patient.style.scss';
|
|
14
|
+
|
|
15
|
+
attach('ward-patient-workspace-header-slot', 'patient-vitals-info');
|
|
16
|
+
|
|
17
|
+
export interface WardPatientWorkspaceProps extends DefaultWorkspaceProps {
|
|
18
|
+
patientUuid: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function WardPatientWorkspace({ patientUuid, setTitle }: WardPatientWorkspaceProps) {
|
|
22
|
+
const { t } = useTranslation();
|
|
23
|
+
const { patient, isLoading, error } = usePatient(patientUuid);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isLoading) {
|
|
27
|
+
setTitle(t('wardPatientWorkspaceTitle', 'Ward Patient'), <InlineLoading />);
|
|
28
|
+
} else if (patient) {
|
|
29
|
+
setTitle(getPatientName(patient), <PatientWorkspaceTitle patient={patient} />);
|
|
30
|
+
} else if (error) {
|
|
31
|
+
setTitle(t('wardPatientWorkspaceTitle', 'Ward Patient'));
|
|
32
|
+
}
|
|
33
|
+
}, [patient]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={styles.workspaceContainer}>
|
|
37
|
+
{isLoading ? (
|
|
38
|
+
<InlineLoading />
|
|
39
|
+
) : patient ? (
|
|
40
|
+
<WardPatientWorkspaceView patient={patient} />
|
|
41
|
+
) : error ? (
|
|
42
|
+
<InlineNotification>{error.message}</InlineNotification>
|
|
43
|
+
) : (
|
|
44
|
+
<InlineNotification>
|
|
45
|
+
{t('failedToLoadPatientWorkspace', 'Ward patient workspace has failed to load.')}
|
|
46
|
+
</InlineNotification>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface WardPatientWorkspaceViewProps {
|
|
53
|
+
patient: fhir.Patient;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function WardPatientWorkspaceView({ patient }: WardPatientWorkspaceViewProps) {
|
|
57
|
+
const extensionSlotState = useMemo(() => ({ patient, patientUuid: patient.id }), [patient]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<div>
|
|
62
|
+
<ExtensionSlot name="ward-patient-workspace-header-slot" state={extensionSlotState} />
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<ExtensionSlot name="ward-patient-workspace-content-slot" state={extensionSlotState} />
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function PatientWorkspaceTitle({ patient }: { patient: fhir.Patient }) {
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<div>{getPatientName(patient)} </div>
|
|
75
|
+
<div className={styles.headerPatientDetail}>· {patient.gender}</div>
|
|
76
|
+
<div className={styles.headerPatientDetail}>· {age(patient.birthDate)}</div>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { type Patient } from '@openmrs/esm-framework';
|
|
1
|
+
import { type Visit, type Patient } from '@openmrs/esm-framework';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import EmptyBed from '../
|
|
4
|
-
import { type Bed } from '../types';
|
|
5
|
-
|
|
6
|
-
interface WardBedProps {
|
|
3
|
+
import EmptyBed from '../beds/empty-bed.component';
|
|
4
|
+
import { type WardPatient, type Bed } from '../types';
|
|
5
|
+
import OccupiedBed from '../beds/occupied-bed.component';
|
|
6
|
+
export interface WardBedProps {
|
|
7
|
+
wardPatients: Array<WardPatient>;
|
|
7
8
|
bed: Bed;
|
|
8
|
-
patients: Patient[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const WardBed = ({ bed,
|
|
12
|
-
return
|
|
11
|
+
const WardBed: React.FC<WardBedProps> = ({ bed, wardPatients }) => {
|
|
12
|
+
return wardPatients?.length > 0 ? <OccupiedBed bed={bed} wardPatients={wardPatients} /> : <EmptyBed bed={bed} />;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export default WardBed;
|
|
@@ -1,67 +1,100 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { InlineNotification } from '@carbon/react';
|
|
3
|
-
import { useLocations, useSession, type Location } from '@openmrs/esm-framework';
|
|
4
|
-
|
|
5
3
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import {
|
|
4
|
+
import { WorkspaceContainer, useFeatureFlag } from '@openmrs/esm-framework';
|
|
5
|
+
import EmptyBedSkeleton from '../beds/empty-bed-skeleton';
|
|
7
6
|
import { useAdmissionLocation } from '../hooks/useAdmissionLocation';
|
|
8
7
|
import WardBed from './ward-bed.component';
|
|
9
|
-
import { bedLayoutToBed } from './ward-view.resource';
|
|
8
|
+
import { bedLayoutToBed, filterBeds } from './ward-view.resource';
|
|
10
9
|
import styles from './ward-view.scss';
|
|
11
|
-
import
|
|
10
|
+
import WardViewHeader from '../ward-view-header/ward-view-header.component';
|
|
11
|
+
import { type AdmittedPatient, type WardPatient } from '../types';
|
|
12
|
+
import { useAdmittedPatients } from '../hooks/useAdmittedPatients';
|
|
13
|
+
import useWardLocation from '../hooks/useWardLocation';
|
|
12
14
|
|
|
13
15
|
const WardView = () => {
|
|
14
|
-
const
|
|
15
|
-
const {
|
|
16
|
-
|
|
16
|
+
const response = useWardLocation();
|
|
17
|
+
const { isLoadingLocation, errorFetchingLocation, invalidLocation } = response;
|
|
18
|
+
|
|
17
19
|
const { t } = useTranslation();
|
|
18
|
-
const
|
|
20
|
+
const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
|
|
21
|
+
|
|
22
|
+
//TODO:Display patients with admitted status (based on their observations) that have no beds assigned
|
|
23
|
+
if (!isBedManagementModuleInstalled || isLoadingLocation) {
|
|
24
|
+
return <></>;
|
|
25
|
+
}
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
if (invalidLocation) {
|
|
28
|
+
return <InlineNotification kind="error" title={t('invalidLocationSpecified', 'Invalid location specified')} />;
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
return (
|
|
24
32
|
<div className={styles.wardView}>
|
|
25
|
-
<
|
|
26
|
-
<h4>{location?.display}</h4>
|
|
27
|
-
</div>
|
|
33
|
+
<WardViewHeader />
|
|
28
34
|
<div className={styles.wardViewMain}>
|
|
29
|
-
|
|
30
|
-
<InlineNotification
|
|
31
|
-
kind="error"
|
|
32
|
-
lowContrast={true}
|
|
33
|
-
title={t('invalidLocationSpecified', 'Invalid location specified')}
|
|
34
|
-
subtitle={t('unknownLocationUuid', 'Unknown location uuid: {{locationUuidFromUrl}}', {
|
|
35
|
-
locationUuidFromUrl,
|
|
36
|
-
})}
|
|
37
|
-
/>
|
|
38
|
-
) : (
|
|
39
|
-
<WardViewByLocation location={location} />
|
|
40
|
-
)}
|
|
35
|
+
<WardViewByLocation />
|
|
41
36
|
</div>
|
|
37
|
+
<WorkspaceContainer overlay contextKey="ward" />
|
|
42
38
|
</div>
|
|
43
39
|
);
|
|
44
40
|
};
|
|
45
41
|
|
|
46
|
-
const WardViewByLocation = (
|
|
47
|
-
const {
|
|
42
|
+
const WardViewByLocation = () => {
|
|
43
|
+
const { location } = useWardLocation();
|
|
44
|
+
const {
|
|
45
|
+
admissionLocation,
|
|
46
|
+
isLoading: isLoadingLocation,
|
|
47
|
+
error: errorLoadingLocation,
|
|
48
|
+
} = useAdmissionLocation(location?.uuid);
|
|
49
|
+
const {
|
|
50
|
+
admittedPatients,
|
|
51
|
+
isLoading: isLoadingPatients,
|
|
52
|
+
error: errorLoadingPatients,
|
|
53
|
+
} = useAdmittedPatients(location.uuid);
|
|
48
54
|
const { t } = useTranslation();
|
|
55
|
+
const admittedPatientsByUuid = useMemo(() => {
|
|
56
|
+
const map = new Map<string, AdmittedPatient>();
|
|
57
|
+
for (const admittedPatient of admittedPatients ?? []) {
|
|
58
|
+
map.set(admittedPatient.patient.uuid, admittedPatient);
|
|
59
|
+
}
|
|
60
|
+
return map;
|
|
61
|
+
}, [admittedPatients]);
|
|
62
|
+
|
|
63
|
+
if (admissionLocation != null && admittedPatients != null) {
|
|
64
|
+
const bedLayouts = filterBeds(admissionLocation);
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
const wardBeds = bedLayouts.map((bedLayout, i) => {
|
|
67
|
+
const { patients } = bedLayout;
|
|
68
|
+
const bed = bedLayoutToBed(bedLayout);
|
|
69
|
+
const wardPatients: WardPatient[] = patients.map((patient) => {
|
|
70
|
+
const admittedPatient = admittedPatientsByUuid.get(patient.uuid);
|
|
71
|
+
|
|
72
|
+
if (admittedPatient) {
|
|
73
|
+
// ideally, we can just use the patient object within admittedPatient
|
|
74
|
+
// and not need the one from bedLayouts, however, the emr api
|
|
75
|
+
// does not respect custom representation right now and does not return
|
|
76
|
+
// all required fields for the patient object
|
|
77
|
+
// TODO: change after this is done. https://openmrs.atlassian.net/browse/EA-192
|
|
78
|
+
return { ...admittedPatient, patient, admitted: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// patient assigned a bed but *not* admitted
|
|
82
|
+
// TODO: get the patient's visit and current location
|
|
83
|
+
return {
|
|
84
|
+
patient,
|
|
85
|
+
visit: null,
|
|
86
|
+
admitted: true,
|
|
87
|
+
currentLocation: null,
|
|
88
|
+
timeSinceAdmissionInMinutes: null,
|
|
89
|
+
timeAtInpatientLocationInMinutes: null,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
return <WardBed key={bed.uuid} bed={bed} wardPatients={wardPatients} />;
|
|
93
|
+
});
|
|
57
94
|
|
|
58
95
|
return (
|
|
59
96
|
<>
|
|
60
|
-
{
|
|
61
|
-
const { patient } = bedLayout;
|
|
62
|
-
const bed = bedLayoutToBed(bedLayout);
|
|
63
|
-
return <WardBed key={bed.uuid} bed={bed} patients={patient ? [patient] : null} />;
|
|
64
|
-
})}
|
|
97
|
+
{wardBeds}
|
|
65
98
|
{bedLayouts.length == 0 && (
|
|
66
99
|
<InlineNotification
|
|
67
100
|
kind="warning"
|
|
@@ -71,7 +104,7 @@ const WardViewByLocation = ({ location }: { location: Location }) => {
|
|
|
71
104
|
)}
|
|
72
105
|
</>
|
|
73
106
|
);
|
|
74
|
-
} else if (
|
|
107
|
+
} else if (isLoadingLocation || isLoadingPatients) {
|
|
75
108
|
return (
|
|
76
109
|
<>
|
|
77
110
|
{Array(20)
|
|
@@ -81,18 +114,27 @@ const WardViewByLocation = ({ location }: { location: Location }) => {
|
|
|
81
114
|
))}
|
|
82
115
|
</>
|
|
83
116
|
);
|
|
84
|
-
} else {
|
|
117
|
+
} else if (errorLoadingLocation) {
|
|
85
118
|
return (
|
|
86
119
|
<InlineNotification
|
|
87
120
|
kind="error"
|
|
88
121
|
lowContrast={true}
|
|
89
122
|
title={t('errorLoadingWardLocation', 'Error loading ward location')}
|
|
90
123
|
subtitle={
|
|
91
|
-
|
|
124
|
+
errorLoadingLocation?.message ??
|
|
92
125
|
t('invalidWardLocation', 'Invalid ward location: {{location}}', { location: location.display })
|
|
93
126
|
}
|
|
94
127
|
/>
|
|
95
128
|
);
|
|
129
|
+
} else {
|
|
130
|
+
return (
|
|
131
|
+
<InlineNotification
|
|
132
|
+
kind="error"
|
|
133
|
+
lowContrast={true}
|
|
134
|
+
title={t('errorLoadingPatients', 'Error loading admitted patients')}
|
|
135
|
+
subtitle={errorLoadingPatients?.message}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
96
138
|
}
|
|
97
139
|
};
|
|
98
140
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AdmissionLocation, Bed, BedLayout } from '../types';
|
|
2
2
|
|
|
3
3
|
// the server side has 2 slightly incompatible types for Bed
|
|
4
4
|
export function bedLayoutToBed(bedLayout: BedLayout): Bed {
|
|
@@ -12,3 +12,13 @@ export function bedLayoutToBed(bedLayout: BedLayout): Bed {
|
|
|
12
12
|
status: bedLayout.status,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export function filterBeds(admissionLocation: AdmissionLocation): BedLayout[] {
|
|
17
|
+
// admissionLocation.bedLayouts can contain row+column positions with no bed,
|
|
18
|
+
// filter out layout positions with no real bed
|
|
19
|
+
let collator = new Intl.Collator([], { numeric: true });
|
|
20
|
+
const bedLayouts = admissionLocation.bedLayouts
|
|
21
|
+
.filter((bl) => bl.bedId)
|
|
22
|
+
.sort((bedA, bedB) => collator.compare(bedA.bedNumber, bedB.bedNumber));
|
|
23
|
+
return bedLayouts;
|
|
24
|
+
}
|
|
@@ -1,31 +1,44 @@
|
|
|
1
|
-
import { getDefaultsFromConfigSchema, useConfig, useSession } from '@openmrs/esm-framework';
|
|
2
|
-
import { screen } from '@testing-library/react';
|
|
3
1
|
import React from 'react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import {
|
|
4
|
+
type Person,
|
|
5
|
+
type ConfigSchema,
|
|
6
|
+
getDefaultsFromConfigSchema,
|
|
7
|
+
useConfig,
|
|
8
|
+
useFeatureFlag,
|
|
9
|
+
} from '@openmrs/esm-framework';
|
|
4
10
|
import { useParams } from 'react-router-dom';
|
|
5
|
-
import { mockLocations } from '../../../../__mocks__/locations.mock';
|
|
6
11
|
import { mockAdmissionLocation } from '../../../../__mocks__/wards.mock';
|
|
7
12
|
import { renderWithSwr } from '../../../../tools/test-utils';
|
|
8
13
|
import { configSchema } from '../config-schema';
|
|
9
14
|
import { useAdmissionLocation } from '../hooks/useAdmissionLocation';
|
|
15
|
+
import { mockPatientAlice } from '../../../../__mocks__/patient.mock';
|
|
16
|
+
import { useAdmittedPatients } from '../hooks/useAdmittedPatients';
|
|
17
|
+
import useWardLocation from '../hooks/useWardLocation';
|
|
10
18
|
import WardView from './ward-view.component';
|
|
11
19
|
|
|
12
|
-
jest.
|
|
13
|
-
|
|
20
|
+
jest.replaceProperty(mockPatientAlice.person as Person, 'preferredName', {
|
|
21
|
+
uuid: '',
|
|
22
|
+
givenName: 'Alice',
|
|
23
|
+
familyName: 'Johnson',
|
|
14
24
|
});
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
sessionLocation: mockedSessionLocation,
|
|
19
|
-
authenticated: true,
|
|
20
|
-
sessionId: 'sessionId',
|
|
26
|
+
jest.mocked(useConfig).mockReturnValue({
|
|
27
|
+
...getDefaultsFromConfigSchema<ConfigSchema>(configSchema),
|
|
21
28
|
});
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const mockedUseFeatureFlag = useFeatureFlag as jest.Mock;
|
|
31
|
+
|
|
32
|
+
jest.mock('../hooks/useWardLocation', () =>
|
|
33
|
+
jest.fn().mockReturnValue({
|
|
34
|
+
location: { uuid: 'abcd', display: 'mock location' },
|
|
35
|
+
isLoadingLocation: false,
|
|
36
|
+
errorFetchingLocation: null,
|
|
37
|
+
invalidLocation: false,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const mockedUseWardLocation = useWardLocation as jest.Mock;
|
|
29
42
|
|
|
30
43
|
jest.mock('react-router-dom', () => ({
|
|
31
44
|
...jest.requireActual('react-router-dom'),
|
|
@@ -36,6 +49,10 @@ const mockedUseParams = useParams as jest.Mock;
|
|
|
36
49
|
jest.mock('../hooks/useAdmissionLocation', () => ({
|
|
37
50
|
useAdmissionLocation: jest.fn(),
|
|
38
51
|
}));
|
|
52
|
+
jest.mock('../hooks/useAdmittedPatients', () => ({
|
|
53
|
+
useAdmittedPatients: jest.fn(),
|
|
54
|
+
}));
|
|
55
|
+
|
|
39
56
|
jest.mocked(useAdmissionLocation).mockReturnValue({
|
|
40
57
|
error: undefined,
|
|
41
58
|
mutate: jest.fn(),
|
|
@@ -43,19 +60,25 @@ jest.mocked(useAdmissionLocation).mockReturnValue({
|
|
|
43
60
|
isLoading: false,
|
|
44
61
|
admissionLocation: mockAdmissionLocation,
|
|
45
62
|
});
|
|
63
|
+
jest.mocked(useAdmittedPatients).mockReturnValue({
|
|
64
|
+
error: undefined,
|
|
65
|
+
mutate: jest.fn(),
|
|
66
|
+
isValidating: false,
|
|
67
|
+
isLoading: false,
|
|
68
|
+
admittedPatients: [],
|
|
69
|
+
});
|
|
46
70
|
|
|
47
71
|
describe('WardView:', () => {
|
|
48
72
|
it('renders the session location when no location provided in URL', () => {
|
|
49
73
|
renderWithSwr(<WardView />);
|
|
50
|
-
const header = screen.getByRole('heading', { name:
|
|
74
|
+
const header = screen.getByRole('heading', { name: 'mock location' });
|
|
51
75
|
expect(header).toBeInTheDocument();
|
|
52
76
|
});
|
|
53
77
|
|
|
54
78
|
it('renders the location provided in URL', () => {
|
|
55
|
-
|
|
56
|
-
mockedUseParams.mockReturnValueOnce({ locationUuid: locationToUse.uuid });
|
|
79
|
+
mockedUseParams.mockReturnValueOnce({ locationUuid: 'abcd' });
|
|
57
80
|
renderWithSwr(<WardView />);
|
|
58
|
-
const header = screen.getByRole('heading', { name:
|
|
81
|
+
const header = screen.getByRole('heading', { name: 'mock location' });
|
|
59
82
|
expect(header).toBeInTheDocument();
|
|
60
83
|
});
|
|
61
84
|
|
|
@@ -64,4 +87,25 @@ describe('WardView:', () => {
|
|
|
64
87
|
const emptyBedCards = await screen.findAllByText(/empty bed/i);
|
|
65
88
|
expect(emptyBedCards).toHaveLength(3);
|
|
66
89
|
});
|
|
90
|
+
|
|
91
|
+
it('renders notification for invalid location uuid', () => {
|
|
92
|
+
mockedUseWardLocation.mockReturnValueOnce({
|
|
93
|
+
location: null,
|
|
94
|
+
isLoadingLocation: false,
|
|
95
|
+
errorFetchingLocation: null,
|
|
96
|
+
invalidLocation: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
renderWithSwr(<WardView />);
|
|
100
|
+
const notification = screen.getByRole('status');
|
|
101
|
+
expect(notification).toBeInTheDocument();
|
|
102
|
+
const invalidText = screen.getByText('Invalid location specified');
|
|
103
|
+
expect(invalidText).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('screen should be empty if backend module is not installed', () => {
|
|
107
|
+
mockedUseFeatureFlag.mockReturnValueOnce(false);
|
|
108
|
+
const { container } = renderWithSwr(<WardView />);
|
|
109
|
+
expect(container).toBeEmptyDOMElement();
|
|
110
|
+
});
|
|
67
111
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Movement } from '@carbon/react/icons';
|
|
3
|
+
import { Button, InlineNotification } from '@carbon/react';
|
|
4
|
+
import { ArrowRightIcon, isDesktop, launchWorkspace, useLayoutType } from '@openmrs/esm-framework';
|
|
5
|
+
import { useInpatientRequest } from '../hooks/useInpatientRequest';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import useWardLocation from '../hooks/useWardLocation';
|
|
8
|
+
import styles from './admission-requests.scss';
|
|
9
|
+
|
|
10
|
+
const AdmissionRequestsBar = () => {
|
|
11
|
+
const { location } = useWardLocation();
|
|
12
|
+
const { inpatientRequests, isLoading, error } = useInpatientRequest(location?.uuid);
|
|
13
|
+
const admissionRequests = inpatientRequests?.filter((request) => request.type == 'ADMISSION');
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const layout = useLayoutType();
|
|
16
|
+
|
|
17
|
+
if (isLoading || !admissionRequests?.length) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
console.error(error);
|
|
23
|
+
return (
|
|
24
|
+
<InlineNotification
|
|
25
|
+
kind="error"
|
|
26
|
+
title={t('errorLoadingPatientAdmissionRequests', 'Error Loading patient admission requests')}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={styles.admissionRequestsContainer}>
|
|
33
|
+
<Movement className={styles.movementIcon} size="24" />
|
|
34
|
+
<span className={styles.content}>
|
|
35
|
+
{t('admissionRequestsCount', '{{count}} admission requests', {
|
|
36
|
+
count: admissionRequests.length,
|
|
37
|
+
})}
|
|
38
|
+
</span>
|
|
39
|
+
<Button
|
|
40
|
+
onClick={() => launchWorkspace('admission-requests-workspace', { admissionRequests })}
|
|
41
|
+
renderIcon={ArrowRightIcon}
|
|
42
|
+
kind="ghost"
|
|
43
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}>
|
|
44
|
+
{t('manage', 'Manage')}
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default AdmissionRequestsBar;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { renderWithSwr } from '../../../../tools/test-utils';
|
|
4
|
+
import { screen } from '@testing-library/react';
|
|
5
|
+
import { launchWorkspace } from '@openmrs/esm-framework';
|
|
6
|
+
import AdmissionRequestsBar from './admission-requests-bar.component';
|
|
7
|
+
import { mockInpatientRequest } from '../../../../__mocks__/ward-patient';
|
|
8
|
+
import { useInpatientRequest } from '../hooks/useInpatientRequest';
|
|
9
|
+
|
|
10
|
+
jest.mock('@openmrs/esm-framework', () => {
|
|
11
|
+
return {
|
|
12
|
+
...jest.requireActual('@openmrs/esm-framework'),
|
|
13
|
+
launchWorkspace: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
jest.mock('../hooks/useInpatientRequest', () => ({
|
|
18
|
+
useInpatientRequest: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
const mockInpatientRequestResponse = {
|
|
21
|
+
error: undefined,
|
|
22
|
+
mutate: jest.fn(),
|
|
23
|
+
isValidating: false,
|
|
24
|
+
isLoading: false,
|
|
25
|
+
inpatientRequests: [mockInpatientRequest],
|
|
26
|
+
};
|
|
27
|
+
jest.mocked(useInpatientRequest).mockReturnValue(mockInpatientRequestResponse);
|
|
28
|
+
|
|
29
|
+
describe('Admission Requests Button', () => {
|
|
30
|
+
it('call launch workspace when clicked on manage button', async () => {
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
renderWithSwr(<AdmissionRequestsBar />);
|
|
33
|
+
|
|
34
|
+
await user.click(screen.getByRole('button', { name: /manage/i }));
|
|
35
|
+
expect(launchWorkspace).toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('there should be one admission request', () => {
|
|
39
|
+
renderWithSwr(<AdmissionRequestsBar />);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText('1 admission requests')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/type';
|
|
2
|
+
@use '@carbon/styles/scss/spacing';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.admissionRequestsContainer {
|
|
6
|
+
background-color: $color-gray-70;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
padding: spacing.$spacing-02 0 spacing.$spacing-02 spacing.$spacing-04;
|
|
10
|
+
background-color: #393939;
|
|
11
|
+
|
|
12
|
+
& > button {
|
|
13
|
+
color: #78a9ff;
|
|
14
|
+
|
|
15
|
+
svg {
|
|
16
|
+
fill: #78a9ff !important;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.movementIcon {
|
|
22
|
+
padding: spacing.$spacing-02;
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
fill: $ui-03;
|
|
25
|
+
background-color: $color-blue-60-2;
|
|
26
|
+
margin-right: spacing.$spacing-03;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.content {
|
|
30
|
+
@include type.type-style('heading-compact-01');
|
|
31
|
+
color: $ui-02;
|
|
32
|
+
margin-right: spacing.$spacing-03;
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './ward-view-header.scss';
|
|
3
|
+
import AdmissionRequestsBar from './admission-requests-bar.component';
|
|
4
|
+
import useWardLocation from '../hooks/useWardLocation';
|
|
5
|
+
|
|
6
|
+
interface WardViewHeaderProps {}
|
|
7
|
+
|
|
8
|
+
const WardViewHeader: React.FC<WardViewHeaderProps> = () => {
|
|
9
|
+
const { location } = useWardLocation();
|
|
10
|
+
return (
|
|
11
|
+
<div className={styles.wardViewHeader}>
|
|
12
|
+
<h4>{location?.display}</h4>
|
|
13
|
+
<AdmissionRequestsBar />
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default WardViewHeader;
|