@openmrs/esm-ward-app 7.0.1-pre.60 → 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 +23 -27
- 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/{697.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/574.js +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 +190 -46
- package/dist/openmrs-esm-ward-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -3
- package/src/beds/occupied-bed.component.tsx +7 -12
- package/src/beds/occupied-bed.scss +1 -1
- package/src/beds/occupied-bed.test.tsx +14 -5
- package/src/config-schema.ts +173 -7
- 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 +24 -3
- package/src/root.component.tsx +2 -1
- package/src/routes.json +37 -7
- package/src/types/index.ts +40 -2
- package/src/ward-patient-card/row-elements/ward-patient-age.tsx +3 -1
- package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +3 -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 +3 -1
- 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 +12 -5
- package/src/ward-patient-card/ward-patient-card.scss +43 -0
- package/src/ward-patient-card/ward-patient-card.tsx +12 -2
- 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 +6 -9
- package/src/ward-view/ward-view.component.tsx +80 -39
- package/src/ward-view/ward-view.scss +0 -15
- package/src/ward-view/ward-view.test.tsx +38 -25
- 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 +6 -1
- package/dist/49.js +0 -1
- package/dist/49.js.map +0 -1
- package/dist/697.js +0 -2
- package/dist/697.js.map +0 -1
package/src/types/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type React from 'react';
|
|
|
10
10
|
|
|
11
11
|
export interface WardPatientCardProps {
|
|
12
12
|
patient: Patient;
|
|
13
|
+
visit: Visit;
|
|
13
14
|
bed: Bed;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -21,10 +22,16 @@ export const patientCardElementTypes = [
|
|
|
21
22
|
'patient-name',
|
|
22
23
|
'patient-age',
|
|
23
24
|
'patient-address',
|
|
25
|
+
'patient-obs',
|
|
26
|
+
'patient-coded-obs-tags',
|
|
24
27
|
'admission-time',
|
|
25
28
|
] as const;
|
|
26
29
|
export type PatientCardElementType = (typeof patientCardElementTypes)[number];
|
|
27
30
|
|
|
31
|
+
// a Ward Patient can either be a patient that is already admitted or a
|
|
32
|
+
// patient that is awaiting admission
|
|
33
|
+
export type WardPatient = (AdmittedPatient & { admitted: true }) | (InpatientRequest & { admitted: false });
|
|
34
|
+
|
|
28
35
|
// server-side types defined in openmrs-module-bedmanagement:
|
|
29
36
|
|
|
30
37
|
export interface AdmissionLocation {
|
|
@@ -52,7 +59,7 @@ export interface BedLayout {
|
|
|
52
59
|
status: BedStatus;
|
|
53
60
|
bedType: BedType;
|
|
54
61
|
location: string;
|
|
55
|
-
|
|
62
|
+
patients: Patient[];
|
|
56
63
|
bedTagMaps: BedTagMap[];
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -76,6 +83,36 @@ interface BedTagMap {
|
|
|
76
83
|
|
|
77
84
|
export type BedStatus = 'AVAILABLE' | 'OCCUPIED';
|
|
78
85
|
|
|
86
|
+
// server-side types defined in openmrs-module-emrapi:
|
|
87
|
+
|
|
88
|
+
export type DispositionType = 'ADMISSION' | 'TRANSFER' | 'DISCHARGE';
|
|
89
|
+
|
|
90
|
+
// InpatientRequest[] returned by:
|
|
91
|
+
// GET /rest/emrapi/inpatient/admissionRequests
|
|
92
|
+
// GET /rest/emrapi/inpatient/transferRequests
|
|
93
|
+
// GET /rest/emrapi/inpatient/admissionAndTransferRequests
|
|
94
|
+
export interface InpatientRequest {
|
|
95
|
+
patient: Patient;
|
|
96
|
+
visit: Visit;
|
|
97
|
+
type: DispositionType;
|
|
98
|
+
|
|
99
|
+
// as of now, these fields are not included in the backend
|
|
100
|
+
encounter?: Encounter;
|
|
101
|
+
dispositionObs?: Observation;
|
|
102
|
+
dispositionLocation?: Location;
|
|
103
|
+
dispositionDate?: Date;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// AdmittedPatient[] returned by:
|
|
107
|
+
// GET /rest/emrapi/inpatient/visits
|
|
108
|
+
export interface AdmittedPatient {
|
|
109
|
+
patient: Patient;
|
|
110
|
+
visit: Visit;
|
|
111
|
+
currentLocation: Location;
|
|
112
|
+
timeSinceAdmissionInMinutes: number;
|
|
113
|
+
timeAtInpatientLocationInMinutes: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
79
116
|
// TODO: Move these types to esm-core
|
|
80
117
|
export interface Observation extends OpenmrsResourceStrict {
|
|
81
118
|
concept: OpenmrsResource;
|
|
@@ -83,6 +120,7 @@ export interface Observation extends OpenmrsResourceStrict {
|
|
|
83
120
|
obsDatetime: string;
|
|
84
121
|
accessionNumber: string;
|
|
85
122
|
obsGroup: Observation;
|
|
123
|
+
value: number | string | boolean | OpenmrsResource;
|
|
86
124
|
valueCodedName: OpenmrsResource; // ConceptName
|
|
87
125
|
groupMembers: Array<Observation>;
|
|
88
126
|
comment: string;
|
|
@@ -98,7 +136,7 @@ export interface Encounter extends OpenmrsResourceStrict {
|
|
|
98
136
|
location?: Location;
|
|
99
137
|
form?: OpenmrsResource;
|
|
100
138
|
encounterType?: EncounterType;
|
|
101
|
-
obs?: Observation
|
|
139
|
+
obs?: Array<Observation>;
|
|
102
140
|
orders?: any;
|
|
103
141
|
voided?: boolean;
|
|
104
142
|
visit?: Visit;
|
|
@@ -6,12 +6,14 @@ const WardPatientAge: WardPatientCardElement = ({ patient }) => {
|
|
|
6
6
|
const { t } = useTranslation();
|
|
7
7
|
// TODO: BED-10
|
|
8
8
|
// make the backend return patient.person.birthdate so we can use the age() function to calculate age
|
|
9
|
-
return (
|
|
9
|
+
return patient?.person?.age ? (
|
|
10
10
|
<div>
|
|
11
11
|
{t('yearsOld', '{{age}} yrs', {
|
|
12
12
|
age: patient?.person?.age,
|
|
13
13
|
})}
|
|
14
14
|
</div>
|
|
15
|
+
) : (
|
|
16
|
+
<></>
|
|
15
17
|
);
|
|
16
18
|
};
|
|
17
19
|
|
|
@@ -3,6 +3,9 @@ import styles from '../ward-patient-card.scss';
|
|
|
3
3
|
import { type WardPatientCardElement } from '../../types';
|
|
4
4
|
|
|
5
5
|
const WardPatientBedNumber: WardPatientCardElement = ({ bed }) => {
|
|
6
|
+
if (!bed) {
|
|
7
|
+
return <></>;
|
|
8
|
+
}
|
|
6
9
|
return (
|
|
7
10
|
<div className={styles.bedNumberBox}>
|
|
8
11
|
<span className={styles.wardPatientBedNumber}>{bed.bedNumber}</span>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SkeletonText, Tag } from '@carbon/react';
|
|
2
|
+
import { translateFrom, type OpenmrsResource } from '@openmrs/esm-framework';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { type PatientCodedObsTagsElementConfig } from '../../config-schema';
|
|
6
|
+
import { moduleName } from '../../constant';
|
|
7
|
+
import { useObs } from '../../hooks/useObs';
|
|
8
|
+
import { type WardPatientCardElement } from '../../types';
|
|
9
|
+
import styles from '../ward-patient-card.scss';
|
|
10
|
+
import { obsCustomRepresentation, useConceptToTagColorMap } from './ward-patient-obs.resource';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The WardPatientCodedObsTags displays observations of coded values of a particular concept in the active visit as tags.
|
|
14
|
+
* Typically, these are taken from checkbox fields from a form. Each answer value can either be configured
|
|
15
|
+
* to show as its own tag, or collapsed into a summary tag show the number of these values present.
|
|
16
|
+
*
|
|
17
|
+
* This is a rather specialized element;
|
|
18
|
+
* for a more general display of obs value, use WardPatientObs instead.
|
|
19
|
+
* @param config
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
const wardPatientCodedObsTags = (config: PatientCodedObsTagsElementConfig) => {
|
|
23
|
+
const WardPatientCodedObsTags: WardPatientCardElement = ({ patient, visit }) => {
|
|
24
|
+
const { conceptUuid, summaryLabel, summaryLabelColor, summaryLabelI18nModule } = config;
|
|
25
|
+
const { data, isLoading } = useObs({ patient: patient.uuid, concept: conceptUuid }, obsCustomRepresentation);
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const { data: conceptToTagColorMap } = useConceptToTagColorMap(config);
|
|
28
|
+
|
|
29
|
+
if (isLoading) {
|
|
30
|
+
return <SkeletonText />;
|
|
31
|
+
} else {
|
|
32
|
+
const obsToDisplay = data?.data?.results?.filter((o) => {
|
|
33
|
+
const matchVisit = o.encounter.visit?.uuid == visit?.uuid;
|
|
34
|
+
return matchVisit || visit == null; // TODO: remove visit == null hack when server API supports returning visit
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const summaryLabelToDisplay =
|
|
38
|
+
summaryLabel != null
|
|
39
|
+
? translateFrom(summaryLabelI18nModule ?? moduleName, summaryLabel)
|
|
40
|
+
: obsToDisplay?.[0]?.concept?.display;
|
|
41
|
+
|
|
42
|
+
const obsNodes = obsToDisplay?.map((o) => {
|
|
43
|
+
const { display, uuid } = o.value as OpenmrsResource;
|
|
44
|
+
|
|
45
|
+
const color = conceptToTagColorMap?.get(uuid);
|
|
46
|
+
if (color) {
|
|
47
|
+
return (
|
|
48
|
+
<Tag type={color} key={`ward-coded-obs-tag-${o.uuid}`}>
|
|
49
|
+
{display}
|
|
50
|
+
</Tag>
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const obsWithNoTagCount = obsNodes.filter((o) => o == null).length;
|
|
58
|
+
if (obsNodes?.length > 0 || obsWithNoTagCount > 0) {
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<span className={styles.wardPatientObsLabel}>
|
|
62
|
+
{obsNodes}
|
|
63
|
+
{obsWithNoTagCount > 0 ? (
|
|
64
|
+
<Tag type={summaryLabelColor}>
|
|
65
|
+
{t('countItems', '{{count}} {{item}}', { count: obsWithNoTagCount, item: summaryLabelToDisplay })}
|
|
66
|
+
</Tag>
|
|
67
|
+
) : null}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return WardPatientCodedObsTags;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default wardPatientCodedObsTags;
|
|
@@ -11,7 +11,9 @@ const wardPatientAddress = (config: PatientAddressElementConfig) => {
|
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
13
|
<div className={styles.wardPatientAddress}>
|
|
14
|
-
{addressFields?.map((field, i) =>
|
|
14
|
+
{addressFields?.map((field, i) =>
|
|
15
|
+
preferredAddress?.[field] ? <div key={i}>{preferredAddress?.[field] as string}</div> : <></>,
|
|
16
|
+
)}
|
|
15
17
|
</div>
|
|
16
18
|
);
|
|
17
19
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, type Concept } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWRImmutable from 'swr/immutable';
|
|
3
|
+
import { type PatientCodedObsTagsElementConfig } from '../../config-schema';
|
|
4
|
+
|
|
5
|
+
// prettier-ignore
|
|
6
|
+
export const obsCustomRepresentation =
|
|
7
|
+
'custom:(uuid,display,obsDatetime,value,' +
|
|
8
|
+
'concept:(uuid,display),' +
|
|
9
|
+
'encounter:(uuid,display,' +
|
|
10
|
+
'visit:(uuid,display)))';
|
|
11
|
+
|
|
12
|
+
// get the setMembers of a concept set
|
|
13
|
+
const conceptSetCustomRepresentation = 'custom:(uuid,setMembers:(uuid))';
|
|
14
|
+
|
|
15
|
+
export function useConceptToTagColorMap(codedObsTagsConfig: PatientCodedObsTagsElementConfig) {
|
|
16
|
+
// fetch the members of the concept sets and process the data
|
|
17
|
+
// to return conceptToTagColorMap (wrapped in a promise).
|
|
18
|
+
// Let swr cache the result of this function.
|
|
19
|
+
const fetchAndMap = (url: string) => {
|
|
20
|
+
const conceptSetToTagColorMap = new Map<string, string>();
|
|
21
|
+
for (const tag of codedObsTagsConfig.tags) {
|
|
22
|
+
const { color, appliedToConceptSets } = tag;
|
|
23
|
+
for (const answer of appliedToConceptSets ?? []) {
|
|
24
|
+
if (!conceptSetToTagColorMap.has(answer)) {
|
|
25
|
+
conceptSetToTagColorMap.set(answer, color);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return openmrsFetch<{ results: Array<Concept> }>(url).then((data) => {
|
|
31
|
+
const conceptSets = data.data.results;
|
|
32
|
+
const conceptToTagColorMap = new Map<string, string>();
|
|
33
|
+
if (conceptSets) {
|
|
34
|
+
for (const conceptSet of conceptSets) {
|
|
35
|
+
for (const concept of conceptSet.setMembers) {
|
|
36
|
+
if (!conceptToTagColorMap.has(concept.uuid)) {
|
|
37
|
+
conceptToTagColorMap.set(concept.uuid, conceptSetToTagColorMap.get(conceptSet.uuid));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return conceptToTagColorMap;
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const conceptSetUuids = codedObsTagsConfig.tags.flatMap((tag) => tag.appliedToConceptSets);
|
|
48
|
+
const apiUrl = `${restBaseUrl}/concept?references=${conceptSetUuids.join()}&v=${conceptSetCustomRepresentation}`;
|
|
49
|
+
const conceptToTagColorMap = useSWRImmutable(apiUrl, fetchAndMap);
|
|
50
|
+
|
|
51
|
+
return conceptToTagColorMap;
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SkeletonText, Tag } from '@carbon/react';
|
|
2
|
+
import { type OpenmrsResource, translateFrom } from '@openmrs/esm-framework';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { type PatientObsElementConfig } from '../../config-schema';
|
|
6
|
+
import { useObs } from '../../hooks/useObs';
|
|
7
|
+
import { type WardPatientCardElement } from '../../types';
|
|
8
|
+
import styles from '../ward-patient-card.scss';
|
|
9
|
+
import { moduleName } from '../../constant';
|
|
10
|
+
import { obsCustomRepresentation } from './ward-patient-obs.resource';
|
|
11
|
+
|
|
12
|
+
const wardPatientObs = (config: PatientObsElementConfig) => {
|
|
13
|
+
const WardPatientObs: WardPatientCardElement = ({ patient, visit }) => {
|
|
14
|
+
const { conceptUuid, onlyWithinCurrentVisit, orderBy, limit, label, labelI18nModule: labelModule } = config;
|
|
15
|
+
const { data, isLoading } = useObs({ patient: patient.uuid, concept: conceptUuid }, obsCustomRepresentation);
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
|
|
18
|
+
if (isLoading) {
|
|
19
|
+
return <SkeletonText />;
|
|
20
|
+
} else {
|
|
21
|
+
const obsToDisplay = data?.data?.results
|
|
22
|
+
?.filter((o) => {
|
|
23
|
+
const matchVisit = !onlyWithinCurrentVisit || o.encounter.visit?.uuid == visit?.uuid;
|
|
24
|
+
return matchVisit;
|
|
25
|
+
})
|
|
26
|
+
?.sort((obsA, obsB) => {
|
|
27
|
+
return (orderBy == 'descending' ? -1 : 1) * obsA.obsDatetime.localeCompare(obsB.obsDatetime);
|
|
28
|
+
})
|
|
29
|
+
?.slice(0, limit ?? Number.MAX_VALUE);
|
|
30
|
+
|
|
31
|
+
const labelToDisplay =
|
|
32
|
+
label != null ? translateFrom(labelModule ?? moduleName, label) : obsToDisplay?.[0]?.concept?.display;
|
|
33
|
+
|
|
34
|
+
const obsNodes = obsToDisplay?.map((o) => {
|
|
35
|
+
const { value } = o;
|
|
36
|
+
const display: any = (value as OpenmrsResource)?.display ?? o.value;
|
|
37
|
+
return <span key={o.uuid}> {display} </span>;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (obsNodes?.length > 0) {
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<span className={styles.wardPatientObsLabel}>
|
|
44
|
+
{labelToDisplay ? t('labelColon', '{{label}}:', { label: labelToDisplay }) : ''}
|
|
45
|
+
</span>
|
|
46
|
+
{obsNodes}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return WardPatientObs;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default wardPatientObs;
|
|
@@ -13,6 +13,8 @@ import wardPatientAddress from './row-elements/ward-patient-header-address';
|
|
|
13
13
|
import WardPatientName from './row-elements/ward-patient-name';
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import styles from './ward-patient-card.scss';
|
|
16
|
+
import wardPatientObs from './row-elements/ward-patient-obs';
|
|
17
|
+
import wardPatientCodedObsTags from './row-elements/ward-patient-coded-obs-tags';
|
|
16
18
|
|
|
17
19
|
export function usePatientCardRows(location: string) {
|
|
18
20
|
const { wardPatientCards } = useConfig<WardConfigObject>();
|
|
@@ -48,11 +50,11 @@ export function usePatientCardRows(location: string) {
|
|
|
48
50
|
return slot;
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
const WardPatientCardRow: React.FC<WardPatientCardProps> = (
|
|
53
|
+
const WardPatientCardRow: React.FC<WardPatientCardProps> = (props) => {
|
|
52
54
|
return (
|
|
53
|
-
<div className={rowType == 'header' ? styles.wardPatientCardHeader : ''}>
|
|
55
|
+
<div className={styles.wardPatientCardRow + ' ' + (rowType == 'header' ? styles.wardPatientCardHeader : '')}>
|
|
54
56
|
{patientCardElements.map((PatientCardElement, i) => (
|
|
55
|
-
<PatientCardElement
|
|
57
|
+
<PatientCardElement {...props} key={i} />
|
|
56
58
|
))}
|
|
57
59
|
</div>
|
|
58
60
|
);
|
|
@@ -78,8 +80,13 @@ function getPatientCardElementFromDefinition(
|
|
|
78
80
|
case 'patient-age':
|
|
79
81
|
return WardPatientAge;
|
|
80
82
|
case 'patient-address': {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
return wardPatientAddress(config.address);
|
|
84
|
+
}
|
|
85
|
+
case 'patient-obs': {
|
|
86
|
+
return wardPatientObs(config.obs);
|
|
87
|
+
}
|
|
88
|
+
case 'patient-coded-obs-tags': {
|
|
89
|
+
return wardPatientCodedObsTags(config.codedObsTags);
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
@use '@carbon/styles/scss/spacing';
|
|
2
2
|
@use '@carbon/styles/scss/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
3
4
|
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
5
|
|
|
5
6
|
.wardPatientCard {
|
|
@@ -11,9 +12,47 @@
|
|
|
11
12
|
align-items: center;
|
|
12
13
|
gap: spacing.$spacing-02;
|
|
13
14
|
background-color: $ui-02;
|
|
15
|
+
|
|
16
|
+
position: relative; // this allows positioning the button correctly
|
|
17
|
+
|
|
18
|
+
> .wardPatientCardRow:not(:first-child) {
|
|
19
|
+
border-top: 1px colors.$gray-20 solid;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.wardPatientCardButton {
|
|
24
|
+
border: none;
|
|
25
|
+
padding: 0;
|
|
26
|
+
|
|
27
|
+
&::before {
|
|
28
|
+
content: '';
|
|
29
|
+
position: absolute;
|
|
30
|
+
inset: 0;
|
|
31
|
+
z-index: 1;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
border: 2px solid transparent;
|
|
34
|
+
transition: border-color 200ms;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:hover::before,
|
|
38
|
+
&:focus::before {
|
|
39
|
+
border-color: $interactive-01;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:focus {
|
|
43
|
+
outline: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.wardPatientCardRow {
|
|
48
|
+
width: 100%;
|
|
14
49
|
padding: spacing.$spacing-04;
|
|
15
50
|
}
|
|
16
51
|
|
|
52
|
+
.wardPatientCardRow:empty {
|
|
53
|
+
display: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
17
56
|
.wardPatientCardHeader {
|
|
18
57
|
@extend .dotSeparatedChildren;
|
|
19
58
|
display: flex;
|
|
@@ -59,6 +98,10 @@
|
|
|
59
98
|
gap: spacing.$spacing-02;
|
|
60
99
|
}
|
|
61
100
|
|
|
101
|
+
.wardPatientObsLabel {
|
|
102
|
+
padding-right: spacing.$spacing-02;
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
.dotSeparatedChildren {
|
|
63
106
|
> div:not(div:first-of-type) {
|
|
64
107
|
display: flex;
|
|
@@ -3,6 +3,8 @@ import { useParams } from 'react-router-dom';
|
|
|
3
3
|
import { type WardPatientCardProps } from '../types';
|
|
4
4
|
import { usePatientCardRows } from './ward-patient-card-row.resources';
|
|
5
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';
|
|
6
8
|
|
|
7
9
|
const WardPatientCard: React.FC<WardPatientCardProps> = (props) => {
|
|
8
10
|
const { locationUuid } = useParams();
|
|
@@ -10,9 +12,17 @@ const WardPatientCard: React.FC<WardPatientCardProps> = (props) => {
|
|
|
10
12
|
|
|
11
13
|
return (
|
|
12
14
|
<div className={styles.wardPatientCard}>
|
|
13
|
-
{patientCardRows.map((WardPatientCardRow) => (
|
|
14
|
-
<WardPatientCardRow {...props} />
|
|
15
|
+
{patientCardRows.map((WardPatientCardRow, i) => (
|
|
16
|
+
<WardPatientCardRow key={i} {...props} />
|
|
15
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>
|
|
16
26
|
</div>
|
|
17
27
|
);
|
|
18
28
|
};
|
|
@@ -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,18 +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
3
|
import EmptyBed from '../beds/empty-bed.component';
|
|
4
|
-
import { type Bed } from '../types';
|
|
4
|
+
import { type WardPatient, type Bed } from '../types';
|
|
5
5
|
import OccupiedBed from '../beds/occupied-bed.component';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
export interface WardBedProps {
|
|
7
|
+
wardPatients: Array<WardPatient>;
|
|
8
8
|
bed: Bed;
|
|
9
|
-
patients: Patient[];
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
const WardBed: React.FC<WardBedProps> = ({ bed,
|
|
13
|
-
return
|
|
14
|
-
<OccupiedBed bed={bed} patients={patients} />
|
|
15
|
-
: <EmptyBed bed={bed} />;
|
|
11
|
+
const WardBed: React.FC<WardBedProps> = ({ bed, wardPatients }) => {
|
|
12
|
+
return wardPatients?.length > 0 ? <OccupiedBed bed={bed} wardPatients={wardPatients} /> : <EmptyBed bed={bed} />;
|
|
16
13
|
};
|
|
17
14
|
|
|
18
15
|
export default WardBed;
|