@kenyaemr/esm-ward-app 7.0.2-pre.65
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 +37 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/443.js +1 -0
- package/dist/443.js.map +1 -0
- package/dist/49.js +1 -0
- package/dist/49.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/697.js +2 -0
- package/dist/697.js.LICENSE.txt +15 -0
- package/dist/697.js.map +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/kenyaemr-esm-ward-app.js +1 -0
- package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +284 -0
- package/dist/kenyaemr-esm-ward-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +15 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +54 -0
- package/src/beds/empty-bed-skeleton.tsx +14 -0
- package/src/beds/empty-bed.component.tsx +24 -0
- package/src/beds/empty-bed.scss +28 -0
- package/src/beds/occupied-bed.component.tsx +40 -0
- package/src/beds/occupied-bed.scss +24 -0
- package/src/beds/occupied-bed.test.tsx +43 -0
- package/src/config-schema.ts +136 -0
- package/src/hooks/useAdmissionLocation.ts +13 -0
- package/src/hooks/useBeds.ts +26 -0
- package/src/index.ts +24 -0
- package/src/root.component.tsx +20 -0
- package/src/routes.json +24 -0
- package/src/types/index.ts +125 -0
- package/src/ward-patient-card/row-elements/row-elements.scss +16 -0
- package/src/ward-patient-card/row-elements/ward-patient-age.tsx +18 -0
- package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +13 -0
- package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +22 -0
- package/src/ward-patient-card/row-elements/ward-patient-name.tsx +13 -0
- package/src/ward-patient-card/ward-patient-card-row.resources.tsx +85 -0
- package/src/ward-patient-card/ward-patient-card.scss +71 -0
- package/src/ward-patient-card/ward-patient-card.tsx +20 -0
- package/src/ward-view/ward-bed.component.tsx +18 -0
- package/src/ward-view/ward-view.component.tsx +100 -0
- package/src/ward-view/ward-view.resource.ts +24 -0
- package/src/ward-view/ward-view.scss +36 -0
- package/src/ward-view/ward-view.test.tsx +98 -0
- package/translations/en.json +9 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { type Bed, type BedStatus } from '../types/index';
|
|
4
|
+
|
|
5
|
+
interface BedSearchCriteria {
|
|
6
|
+
locationUuid?: string;
|
|
7
|
+
status?: BedStatus;
|
|
8
|
+
bedType?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useBeds(searchCriteria?: BedSearchCriteria) {
|
|
12
|
+
const searchParam = new URLSearchParams();
|
|
13
|
+
for (let [key, value] of Object.entries(searchCriteria)) {
|
|
14
|
+
if (value != null) {
|
|
15
|
+
searchParam.append(key, value?.toString());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const apiUrl = `${restBaseUrl}/bed?${searchParam}`;
|
|
20
|
+
const { data, ...rest } = useSWR<{ data: { results: Array<Bed> } }, Error>(apiUrl, openmrsFetch);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
beds: data?.data?.results ?? [],
|
|
24
|
+
...rest,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs, registerFeatureFlag } from '@openmrs/esm-framework';
|
|
2
|
+
import { configSchema } from './config-schema';
|
|
3
|
+
import rootComponent from './root.component';
|
|
4
|
+
|
|
5
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
6
|
+
|
|
7
|
+
const moduleName = '@kenyaemr/esm-ward-app';
|
|
8
|
+
|
|
9
|
+
const options = {
|
|
10
|
+
featureName: 'ward',
|
|
11
|
+
moduleName,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const root = getSyncLifecycle(rootComponent, options);
|
|
15
|
+
|
|
16
|
+
export function startupApp() {
|
|
17
|
+
registerBreadcrumbs([]);
|
|
18
|
+
defineConfigSchema(moduleName, configSchema);
|
|
19
|
+
registerFeatureFlag(
|
|
20
|
+
'bedmanagement-module',
|
|
21
|
+
'Bed Management Module',
|
|
22
|
+
'Enables features related to bed management / assignment. Requires the backend bed management module to be installed.',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
3
|
+
import WardView from './ward-view/ward-view.component';
|
|
4
|
+
|
|
5
|
+
const Root: React.FC = () => {
|
|
6
|
+
const wardViewBasename = window.getOpenmrsSpaBase() + 'ward';
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<main>
|
|
10
|
+
<BrowserRouter basename={wardViewBasename}>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/" element={<WardView />} />
|
|
13
|
+
<Route path="/:locationUuid" element={<WardView />} />
|
|
14
|
+
</Routes>
|
|
15
|
+
</BrowserRouter>
|
|
16
|
+
</main>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default Root;
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"webservices.rest": "^2.2.0"
|
|
5
|
+
},
|
|
6
|
+
"optionalBackendDependencies":{
|
|
7
|
+
"bedmanagement":{
|
|
8
|
+
"version": "^5.14.0 || 5.14.0-SNAPSHOT",
|
|
9
|
+
"feature": {
|
|
10
|
+
"flagName": "bedmanagement-module",
|
|
11
|
+
"label":"Ward App Patient Service",
|
|
12
|
+
"description": "This module, if installed, provides services for managing patients admitted to the ward."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"extensions": [
|
|
17
|
+
],
|
|
18
|
+
"pages": [
|
|
19
|
+
{
|
|
20
|
+
"component": "root",
|
|
21
|
+
"route": "ward"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OpenmrsResource,
|
|
3
|
+
type OpenmrsResourceStrict,
|
|
4
|
+
type Person,
|
|
5
|
+
type Visit,
|
|
6
|
+
type Location,
|
|
7
|
+
type Patient,
|
|
8
|
+
} from '@openmrs/esm-framework';
|
|
9
|
+
import type React from 'react';
|
|
10
|
+
|
|
11
|
+
export interface WardPatientCardProps {
|
|
12
|
+
patient: Patient;
|
|
13
|
+
bed: Bed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type WardPatientCardRow = React.FC<WardPatientCardProps>;
|
|
17
|
+
export type WardPatientCardElement = React.FC<WardPatientCardProps>;
|
|
18
|
+
|
|
19
|
+
export const patientCardElementTypes = [
|
|
20
|
+
'bed-number',
|
|
21
|
+
'patient-name',
|
|
22
|
+
'patient-age',
|
|
23
|
+
'patient-address',
|
|
24
|
+
'admission-time',
|
|
25
|
+
] as const;
|
|
26
|
+
export type PatientCardElementType = (typeof patientCardElementTypes)[number];
|
|
27
|
+
|
|
28
|
+
// server-side types defined in openmrs-module-bedmanagement:
|
|
29
|
+
|
|
30
|
+
export interface AdmissionLocation {
|
|
31
|
+
totalBeds: number;
|
|
32
|
+
occupiedBeds: number;
|
|
33
|
+
ward: Location;
|
|
34
|
+
bedLayouts: Array<BedLayout>;
|
|
35
|
+
}
|
|
36
|
+
export interface Bed {
|
|
37
|
+
id: number;
|
|
38
|
+
uuid: string;
|
|
39
|
+
bedNumber: string;
|
|
40
|
+
bedType: BedType;
|
|
41
|
+
row: number;
|
|
42
|
+
column: number;
|
|
43
|
+
status: BedStatus;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BedLayout {
|
|
47
|
+
rowNumber: number;
|
|
48
|
+
columnNumber: number;
|
|
49
|
+
bedNumber: string;
|
|
50
|
+
bedId: number;
|
|
51
|
+
bedUuid: string;
|
|
52
|
+
status: BedStatus;
|
|
53
|
+
bedType: BedType;
|
|
54
|
+
location: string;
|
|
55
|
+
patient: Patient;
|
|
56
|
+
bedTagMaps: BedTagMap[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BedType {
|
|
60
|
+
uuid: string;
|
|
61
|
+
name: string;
|
|
62
|
+
displayName: string;
|
|
63
|
+
description: string;
|
|
64
|
+
resourceVersion: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface BedTagMap {
|
|
68
|
+
uuid: string;
|
|
69
|
+
bedTag: {
|
|
70
|
+
id: number;
|
|
71
|
+
name: string;
|
|
72
|
+
uuid: string;
|
|
73
|
+
resourceVersion: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type BedStatus = 'AVAILABLE' | 'OCCUPIED';
|
|
78
|
+
|
|
79
|
+
// TODO: Move these types to esm-core
|
|
80
|
+
export interface Observation extends OpenmrsResourceStrict {
|
|
81
|
+
concept: OpenmrsResource;
|
|
82
|
+
person: Person;
|
|
83
|
+
obsDatetime: string;
|
|
84
|
+
accessionNumber: string;
|
|
85
|
+
obsGroup: Observation;
|
|
86
|
+
valueCodedName: OpenmrsResource; // ConceptName
|
|
87
|
+
groupMembers: Array<Observation>;
|
|
88
|
+
comment: string;
|
|
89
|
+
location: Location;
|
|
90
|
+
order: OpenmrsResource; // Order
|
|
91
|
+
encounter: Encounter;
|
|
92
|
+
voided: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Encounter extends OpenmrsResourceStrict {
|
|
96
|
+
encounterDatetime?: string;
|
|
97
|
+
patient?: Patient;
|
|
98
|
+
location?: Location;
|
|
99
|
+
form?: OpenmrsResource;
|
|
100
|
+
encounterType?: EncounterType;
|
|
101
|
+
obs?: Observation;
|
|
102
|
+
orders?: any;
|
|
103
|
+
voided?: boolean;
|
|
104
|
+
visit?: Visit;
|
|
105
|
+
encounterProviders?: Array<EncounterProvider>;
|
|
106
|
+
diagnoses?: any;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface EncounterProvider extends OpenmrsResourceStrict {
|
|
110
|
+
provider?: OpenmrsResource;
|
|
111
|
+
encounterRole?: EncounterRole;
|
|
112
|
+
voided?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface EncounterType extends OpenmrsResourceStrict {
|
|
116
|
+
name?: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
retired?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface EncounterRole extends OpenmrsResourceStrict {
|
|
122
|
+
name?: string;
|
|
123
|
+
description?: string;
|
|
124
|
+
retired?: boolean;
|
|
125
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.bedNumber {
|
|
6
|
+
@include type.type-style('heading-compact-01');
|
|
7
|
+
border-radius: 50%;
|
|
8
|
+
background-color: $color-blue-10;
|
|
9
|
+
color: $color-blue-60-2;
|
|
10
|
+
padding: spacing.$spacing-04;
|
|
11
|
+
width: spacing.$spacing-03;
|
|
12
|
+
height: spacing.$spacing-03;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
align-items: center;
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { type WardPatientCardElement } from '../../types';
|
|
4
|
+
|
|
5
|
+
const WardPatientAge: WardPatientCardElement = ({ patient }) => {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
// TODO: BED-10
|
|
8
|
+
// make the backend return patient.person.birthdate so we can use the age() function to calculate age
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
{t('yearsOld', '{{age}} yrs', {
|
|
12
|
+
age: patient?.person?.age,
|
|
13
|
+
})}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default WardPatientAge;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from '../ward-patient-card.scss';
|
|
3
|
+
import { type WardPatientCardElement } from '../../types';
|
|
4
|
+
|
|
5
|
+
const WardPatientBedNumber: WardPatientCardElement = ({ bed }) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className={styles.bedNumberBox}>
|
|
8
|
+
<span className={styles.wardPatientBedNumber}>{bed.bedNumber}</span>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default WardPatientBedNumber;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type WardPatientCardElement } from '../../types';
|
|
3
|
+
import styles from '../ward-patient-card.scss';
|
|
4
|
+
import { type PatientAddressElementConfig } from '../../config-schema';
|
|
5
|
+
|
|
6
|
+
const wardPatientAddress = (config: PatientAddressElementConfig) => {
|
|
7
|
+
const wardPatientAddress: WardPatientCardElement = ({ patient }) => {
|
|
8
|
+
const { addressFields } = config;
|
|
9
|
+
|
|
10
|
+
const preferredAddress = patient?.person?.preferredAddress;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.wardPatientAddress}>
|
|
14
|
+
{addressFields?.map((field, i) => <div key={i}>{preferredAddress?.[field] as string}</div>)}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return wardPatientAddress;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default wardPatientAddress;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type WardPatientCardElement } from '../../types';
|
|
3
|
+
import styles from '../ward-patient-card.scss';
|
|
4
|
+
|
|
5
|
+
const WardPatientName: WardPatientCardElement = ({ patient }) => {
|
|
6
|
+
// TODO: BED-10
|
|
7
|
+
// make server return patient.display and use that instead
|
|
8
|
+
const { givenName, familyName } = patient?.person?.preferredName;
|
|
9
|
+
const name = `${givenName} ${familyName}`;
|
|
10
|
+
return <div className={styles.wardPatientName}>{name}</div>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default WardPatientName;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
builtInPatientCardElements,
|
|
5
|
+
defaultPatientCardElementConfig,
|
|
6
|
+
type PatientCardElementDefinition,
|
|
7
|
+
type WardConfigObject,
|
|
8
|
+
} from '../config-schema';
|
|
9
|
+
import type { WardPatientCardElement, WardPatientCardProps } from '../types';
|
|
10
|
+
import WardPatientAge from './row-elements/ward-patient-age';
|
|
11
|
+
import WardPatientBedNumber from './row-elements/ward-patient-bed-number';
|
|
12
|
+
import wardPatientAddress from './row-elements/ward-patient-header-address';
|
|
13
|
+
import WardPatientName from './row-elements/ward-patient-name';
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import styles from './ward-patient-card.scss';
|
|
16
|
+
|
|
17
|
+
export function usePatientCardRows(location: string) {
|
|
18
|
+
const { wardPatientCards } = useConfig<WardConfigObject>();
|
|
19
|
+
const patientCardRows = useMemo(() => {
|
|
20
|
+
const { cardDefinitions, patientCardElementDefinitions } = wardPatientCards;
|
|
21
|
+
|
|
22
|
+
// map of patientCardElementId to its corresponding React component
|
|
23
|
+
const patientCardElementsMap = new Map<string, WardPatientCardElement>();
|
|
24
|
+
for (const elementType of builtInPatientCardElements) {
|
|
25
|
+
patientCardElementsMap.set(
|
|
26
|
+
elementType,
|
|
27
|
+
getPatientCardElementFromDefinition({
|
|
28
|
+
id: elementType,
|
|
29
|
+
elementType,
|
|
30
|
+
config: defaultPatientCardElementConfig,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
for (const patientCardElementDef of patientCardElementDefinitions) {
|
|
35
|
+
patientCardElementsMap.set(patientCardElementDef.id, getPatientCardElementFromDefinition(patientCardElementDef));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cardDefinition = cardDefinitions.find((cardDef) => {
|
|
39
|
+
const appliedTo = cardDef.appliedTo;
|
|
40
|
+
|
|
41
|
+
return appliedTo == null || appliedTo.some((criteria) => criteria.location == location);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const ret = cardDefinition.rows.map((row) => {
|
|
45
|
+
const { rowType, elements } = row;
|
|
46
|
+
const patientCardElements = elements.map((patientCardElementId) => {
|
|
47
|
+
const slot = patientCardElementsMap.get(patientCardElementId);
|
|
48
|
+
return slot;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const WardPatientCardRow: React.FC<WardPatientCardProps> = ({ patient, bed }) => {
|
|
52
|
+
return (
|
|
53
|
+
<div className={rowType == 'header' ? styles.wardPatientCardHeader : ''}>
|
|
54
|
+
{patientCardElements.map((PatientCardElement, i) => (
|
|
55
|
+
<PatientCardElement patient={patient} bed={bed} key={i} />
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return WardPatientCardRow;
|
|
62
|
+
});
|
|
63
|
+
return ret;
|
|
64
|
+
}, [location, wardPatientCards]);
|
|
65
|
+
|
|
66
|
+
return patientCardRows;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getPatientCardElementFromDefinition(
|
|
70
|
+
patientCardElementDef: PatientCardElementDefinition,
|
|
71
|
+
): WardPatientCardElement {
|
|
72
|
+
const { elementType, config } = patientCardElementDef;
|
|
73
|
+
switch (elementType) {
|
|
74
|
+
case 'bed-number':
|
|
75
|
+
return WardPatientBedNumber;
|
|
76
|
+
case 'patient-name':
|
|
77
|
+
return WardPatientName;
|
|
78
|
+
case 'patient-age':
|
|
79
|
+
return WardPatientAge;
|
|
80
|
+
case 'patient-address': {
|
|
81
|
+
// TODO: configure address fields to pass in
|
|
82
|
+
return wardPatientAddress(config);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.wardPatientCard {
|
|
6
|
+
@include type.type-style('body-compact-01');
|
|
7
|
+
color: $text-02;
|
|
8
|
+
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: spacing.$spacing-02;
|
|
13
|
+
background-color: $ui-02;
|
|
14
|
+
padding: spacing.$spacing-04;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.wardPatientCardHeader {
|
|
18
|
+
@extend .dotSeparatedChildren;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-wrap: wrap;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.wardPatientName {
|
|
24
|
+
@include type.type-style('heading-compact-02');
|
|
25
|
+
color: $text-02;
|
|
26
|
+
&::before {
|
|
27
|
+
content: '' !important;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wardPatientBedNumber {
|
|
32
|
+
@include type.type-style('heading-compact-01');
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
color: $ui-02;
|
|
35
|
+
background-color: $color-blue-60-2;
|
|
36
|
+
padding: spacing.$spacing-04;
|
|
37
|
+
width: spacing.$spacing-04;
|
|
38
|
+
height: spacing.$spacing-04;
|
|
39
|
+
display: flex;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
align-items: center;
|
|
42
|
+
&.empty {
|
|
43
|
+
background-color: $color-blue-10;
|
|
44
|
+
color: $color-blue-60-2;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.bedNumberBox {
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
align-items: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.wardPatientAddress {
|
|
55
|
+
@extend .dotSeparatedChildren;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: spacing.$spacing-02;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.dotSeparatedChildren {
|
|
63
|
+
> div:not(div:first-of-type) {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
&::before {
|
|
67
|
+
content: '·';
|
|
68
|
+
padding: 0 spacing.$spacing-02;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
|
|
7
|
+
const WardPatientCard: React.FC<WardPatientCardProps> = (props) => {
|
|
8
|
+
const { locationUuid } = useParams();
|
|
9
|
+
const patientCardRows = usePatientCardRows(locationUuid);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className={styles.wardPatientCard}>
|
|
13
|
+
{patientCardRows.map((WardPatientCardRow) => (
|
|
14
|
+
<WardPatientCardRow {...props} />
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default WardPatientCard;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Patient } from '@openmrs/esm-framework';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import EmptyBed from '../beds/empty-bed.component';
|
|
4
|
+
import { type Bed } from '../types';
|
|
5
|
+
import OccupiedBed from '../beds/occupied-bed.component';
|
|
6
|
+
|
|
7
|
+
interface WardBedProps {
|
|
8
|
+
bed: Bed;
|
|
9
|
+
patients: Patient[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const WardBed: React.FC<WardBedProps> = ({ bed, patients }) => {
|
|
13
|
+
return patients?.length > 0 ?
|
|
14
|
+
<OccupiedBed bed={bed} patients={patients} />
|
|
15
|
+
: <EmptyBed bed={bed} />;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default WardBed;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { InlineNotification } from '@carbon/react';
|
|
2
|
+
import { useFeatureFlag, useLocations, useSession, type Location } from '@openmrs/esm-framework';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useParams } from 'react-router-dom';
|
|
6
|
+
import EmptyBedSkeleton from '../beds/empty-bed-skeleton';
|
|
7
|
+
import { useAdmissionLocation } from '../hooks/useAdmissionLocation';
|
|
8
|
+
import WardBed from './ward-bed.component';
|
|
9
|
+
import { bedLayoutToBed, filterBeds } from './ward-view.resource';
|
|
10
|
+
import styles from './ward-view.scss';
|
|
11
|
+
|
|
12
|
+
const WardView = () => {
|
|
13
|
+
const { locationUuid: locationUuidFromUrl } = useParams();
|
|
14
|
+
const { sessionLocation } = useSession();
|
|
15
|
+
const allLocations = useLocations();
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
|
|
18
|
+
const locationFromUrl = allLocations.find((l) => l.uuid === locationUuidFromUrl);
|
|
19
|
+
const invalidLocation = Boolean(locationUuidFromUrl && !locationFromUrl);
|
|
20
|
+
const location = (locationFromUrl ?? sessionLocation) as any as Location;
|
|
21
|
+
//TODO:Display patients with admitted status (based on their observations) that have no beds assigned
|
|
22
|
+
if (!isBedManagementModuleInstalled) {
|
|
23
|
+
return <></>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={styles.wardView}>
|
|
28
|
+
<div className={styles.wardViewHeader}>
|
|
29
|
+
<div className={styles.wardViewHeaderLocationDisplay}>
|
|
30
|
+
<h4>{location?.display}</h4>
|
|
31
|
+
</div>
|
|
32
|
+
<div className={styles.wardViewHeaderAdmissionRequestMenuBar}>{/* TODO: Admission Request bar */}</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className={styles.wardViewMain}>
|
|
35
|
+
{invalidLocation ? (
|
|
36
|
+
<InlineNotification
|
|
37
|
+
kind="error"
|
|
38
|
+
lowContrast={true}
|
|
39
|
+
title={t('invalidLocationSpecified', 'Invalid location specified')}
|
|
40
|
+
subtitle={t('unknownLocationUuid', 'Unknown location uuid: {{locationUuidFromUrl}}', {
|
|
41
|
+
locationUuidFromUrl,
|
|
42
|
+
})}
|
|
43
|
+
/>
|
|
44
|
+
) : (
|
|
45
|
+
<WardViewByLocation location={location} />
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const WardViewByLocation = ({ location }: { location: Location }) => {
|
|
53
|
+
const { admissionLocation, isLoading, error } = useAdmissionLocation(location.uuid);
|
|
54
|
+
const { t } = useTranslation();
|
|
55
|
+
|
|
56
|
+
if (admissionLocation) {
|
|
57
|
+
const bedLayouts = filterBeds(admissionLocation);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{bedLayouts.map((bedLayout, i) => {
|
|
62
|
+
const { patient } = bedLayout;
|
|
63
|
+
const bed = bedLayoutToBed(bedLayout);
|
|
64
|
+
return <WardBed key={bed.uuid} bed={bed} patients={patient ? [patient] : null} />;
|
|
65
|
+
})}
|
|
66
|
+
{bedLayouts.length == 0 && (
|
|
67
|
+
<InlineNotification
|
|
68
|
+
kind="warning"
|
|
69
|
+
lowContrast={true}
|
|
70
|
+
title={t('noBedsConfigured', 'No beds configured for this location')}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
} else if (isLoading) {
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
{Array(20)
|
|
79
|
+
.fill(0)
|
|
80
|
+
.map((_, i) => (
|
|
81
|
+
<EmptyBedSkeleton key={i} />
|
|
82
|
+
))}
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
return (
|
|
87
|
+
<InlineNotification
|
|
88
|
+
kind="error"
|
|
89
|
+
lowContrast={true}
|
|
90
|
+
title={t('errorLoadingWardLocation', 'Error loading ward location')}
|
|
91
|
+
subtitle={
|
|
92
|
+
error?.message ??
|
|
93
|
+
t('invalidWardLocation', 'Invalid ward location: {{location}}', { location: location.display })
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default WardView;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AdmissionLocation, Bed, BedLayout } from '../types';
|
|
2
|
+
|
|
3
|
+
// the server side has 2 slightly incompatible types for Bed
|
|
4
|
+
export function bedLayoutToBed(bedLayout: BedLayout): Bed {
|
|
5
|
+
return {
|
|
6
|
+
id: bedLayout.bedId,
|
|
7
|
+
uuid: bedLayout.bedUuid,
|
|
8
|
+
bedNumber: bedLayout.bedNumber,
|
|
9
|
+
bedType: bedLayout.bedType,
|
|
10
|
+
row: bedLayout.rowNumber,
|
|
11
|
+
column: bedLayout.columnNumber,
|
|
12
|
+
status: bedLayout.status,
|
|
13
|
+
};
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
|
|
3
|
+
.wardView {
|
|
4
|
+
background-color: #e4e4e4;
|
|
5
|
+
position: absolute;
|
|
6
|
+
top: var(--omrs-topnav-height);
|
|
7
|
+
bottom: 0;
|
|
8
|
+
left: 0;
|
|
9
|
+
right: 0;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
padding: 0 spacing.$spacing-05;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.wardViewHeader {
|
|
16
|
+
display: flex;
|
|
17
|
+
margin: spacing.$spacing-05 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.wardViewHeaderLocationDisplay {
|
|
21
|
+
flex: 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.wardViewHeaderAdmissionRequestMenuBar {
|
|
25
|
+
background-color: black;
|
|
26
|
+
color: white;
|
|
27
|
+
padding: 4px 0 4px 12px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.wardViewMain {
|
|
31
|
+
background-color: #e4e4e4;
|
|
32
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
33
|
+
display: grid;
|
|
34
|
+
gap: spacing.$spacing-05;
|
|
35
|
+
padding: spacing.$spacing-05;
|
|
36
|
+
}
|