@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
package/src/routes.json
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
3
|
"backendDependencies": {
|
|
4
|
-
"webservices.rest": "^2.2.0"
|
|
4
|
+
"webservices.rest": "^2.2.0",
|
|
5
|
+
"emrapi": "^2.0.0 || 2.0.0-SNAPSHOT"
|
|
6
|
+
},
|
|
7
|
+
"optionalBackendDependencies":{
|
|
8
|
+
"bedmanagement":{
|
|
9
|
+
"version": "^6.0.0 || 6.0.0-SNAPSHOT",
|
|
10
|
+
"feature": {
|
|
11
|
+
"flagName": "bedmanagement-module",
|
|
12
|
+
"label":"Ward App Patient Service",
|
|
13
|
+
"description": "This module, if installed, provides services for managing patients admitted to the ward."
|
|
14
|
+
}
|
|
15
|
+
}
|
|
5
16
|
},
|
|
6
17
|
"extensions": [
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
{
|
|
19
|
+
"name": "ward-dashboard-link",
|
|
20
|
+
"component": "wardDashboardLink",
|
|
21
|
+
"slot": "homepage-dashboard-slot",
|
|
22
|
+
"meta": {
|
|
23
|
+
"name": "ward",
|
|
24
|
+
"slot": "ward-dashboard-slot",
|
|
25
|
+
"title": "Wards"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
9
28
|
{
|
|
10
29
|
"component": "root",
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
"name": "ward-dashboard",
|
|
31
|
+
"slot": "ward-dashboard-slot"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"component": "wardPatientActionButtonExtension",
|
|
35
|
+
"name": "ward-patient-action-button",
|
|
36
|
+
"slot": "action-menu-ward-patient-items-slot"
|
|
37
|
+
}],
|
|
38
|
+
"workspaces": [
|
|
39
|
+
{
|
|
40
|
+
"name":"admission-requests-workspace",
|
|
41
|
+
"component": "admissionRequestWorkspace",
|
|
42
|
+
"title":"admissionRequests",
|
|
43
|
+
"type":"admission-requests"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "ward-patient-workspace",
|
|
47
|
+
"component": "wardPatientWorkspace",
|
|
48
|
+
"type": "ward",
|
|
49
|
+
"title": "Ward Patient",
|
|
50
|
+
"width": "extra-wide",
|
|
51
|
+
"hasOwnSidebar": true,
|
|
52
|
+
"sidebarFamily": "ward-patient"
|
|
53
|
+
}]
|
|
14
54
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
import {
|
|
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
|
+
visit: Visit;
|
|
14
|
+
bed: Bed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WardPatientCardRow = React.FC<WardPatientCardProps>;
|
|
18
|
+
export type WardPatientCardElement = React.FC<WardPatientCardProps>;
|
|
19
|
+
|
|
20
|
+
export const patientCardElementTypes = [
|
|
21
|
+
'bed-number',
|
|
22
|
+
'patient-name',
|
|
23
|
+
'patient-age',
|
|
24
|
+
'patient-address',
|
|
25
|
+
'patient-obs',
|
|
26
|
+
'patient-coded-obs-tags',
|
|
27
|
+
'admission-time',
|
|
28
|
+
] as const;
|
|
29
|
+
export type PatientCardElementType = (typeof patientCardElementTypes)[number];
|
|
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 });
|
|
2
34
|
|
|
3
35
|
// server-side types defined in openmrs-module-bedmanagement:
|
|
4
36
|
|
|
@@ -27,7 +59,7 @@ export interface BedLayout {
|
|
|
27
59
|
status: BedStatus;
|
|
28
60
|
bedType: BedType;
|
|
29
61
|
location: string;
|
|
30
|
-
|
|
62
|
+
patients: Patient[];
|
|
31
63
|
bedTagMaps: BedTagMap[];
|
|
32
64
|
}
|
|
33
65
|
|
|
@@ -50,3 +82,82 @@ interface BedTagMap {
|
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
export type BedStatus = 'AVAILABLE' | 'OCCUPIED';
|
|
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
|
+
|
|
116
|
+
// TODO: Move these types to esm-core
|
|
117
|
+
export interface Observation extends OpenmrsResourceStrict {
|
|
118
|
+
concept: OpenmrsResource;
|
|
119
|
+
person: Person;
|
|
120
|
+
obsDatetime: string;
|
|
121
|
+
accessionNumber: string;
|
|
122
|
+
obsGroup: Observation;
|
|
123
|
+
value: number | string | boolean | OpenmrsResource;
|
|
124
|
+
valueCodedName: OpenmrsResource; // ConceptName
|
|
125
|
+
groupMembers: Array<Observation>;
|
|
126
|
+
comment: string;
|
|
127
|
+
location: Location;
|
|
128
|
+
order: OpenmrsResource; // Order
|
|
129
|
+
encounter: Encounter;
|
|
130
|
+
voided: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface Encounter extends OpenmrsResourceStrict {
|
|
134
|
+
encounterDatetime?: string;
|
|
135
|
+
patient?: Patient;
|
|
136
|
+
location?: Location;
|
|
137
|
+
form?: OpenmrsResource;
|
|
138
|
+
encounterType?: EncounterType;
|
|
139
|
+
obs?: Array<Observation>;
|
|
140
|
+
orders?: any;
|
|
141
|
+
voided?: boolean;
|
|
142
|
+
visit?: Visit;
|
|
143
|
+
encounterProviders?: Array<EncounterProvider>;
|
|
144
|
+
diagnoses?: any;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface EncounterProvider extends OpenmrsResourceStrict {
|
|
148
|
+
provider?: OpenmrsResource;
|
|
149
|
+
encounterRole?: EncounterRole;
|
|
150
|
+
voided?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface EncounterType extends OpenmrsResourceStrict {
|
|
154
|
+
name?: string;
|
|
155
|
+
description?: string;
|
|
156
|
+
retired?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface EncounterRole extends OpenmrsResourceStrict {
|
|
160
|
+
name?: string;
|
|
161
|
+
description?: string;
|
|
162
|
+
retired?: boolean;
|
|
163
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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 patient?.person?.age ? (
|
|
10
|
+
<div>
|
|
11
|
+
{t('yearsOld', '{{age}} yrs', {
|
|
12
|
+
age: patient?.person?.age,
|
|
13
|
+
})}
|
|
14
|
+
</div>
|
|
15
|
+
) : (
|
|
16
|
+
<></>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default WardPatientAge;
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
if (!bed) {
|
|
7
|
+
return <></>;
|
|
8
|
+
}
|
|
9
|
+
return (
|
|
10
|
+
<div className={styles.bedNumberBox}>
|
|
11
|
+
<span className={styles.wardPatientBedNumber}>{bed.bedNumber}</span>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default WardPatientBedNumber;
|
|
@@ -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;
|
|
@@ -0,0 +1,24 @@
|
|
|
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) =>
|
|
15
|
+
preferredAddress?.[field] ? <div key={i}>{preferredAddress?.[field] as string}</div> : <></>,
|
|
16
|
+
)}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return wardPatientAddress;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
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,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;
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
import wardPatientObs from './row-elements/ward-patient-obs';
|
|
17
|
+
import wardPatientCodedObsTags from './row-elements/ward-patient-coded-obs-tags';
|
|
18
|
+
|
|
19
|
+
export function usePatientCardRows(location: string) {
|
|
20
|
+
const { wardPatientCards } = useConfig<WardConfigObject>();
|
|
21
|
+
const patientCardRows = useMemo(() => {
|
|
22
|
+
const { cardDefinitions, patientCardElementDefinitions } = wardPatientCards;
|
|
23
|
+
|
|
24
|
+
// map of patientCardElementId to its corresponding React component
|
|
25
|
+
const patientCardElementsMap = new Map<string, WardPatientCardElement>();
|
|
26
|
+
for (const elementType of builtInPatientCardElements) {
|
|
27
|
+
patientCardElementsMap.set(
|
|
28
|
+
elementType,
|
|
29
|
+
getPatientCardElementFromDefinition({
|
|
30
|
+
id: elementType,
|
|
31
|
+
elementType,
|
|
32
|
+
config: defaultPatientCardElementConfig,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
for (const patientCardElementDef of patientCardElementDefinitions) {
|
|
37
|
+
patientCardElementsMap.set(patientCardElementDef.id, getPatientCardElementFromDefinition(patientCardElementDef));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cardDefinition = cardDefinitions.find((cardDef) => {
|
|
41
|
+
const appliedTo = cardDef.appliedTo;
|
|
42
|
+
|
|
43
|
+
return appliedTo == null || appliedTo.some((criteria) => criteria.location == location);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const ret = cardDefinition.rows.map((row) => {
|
|
47
|
+
const { rowType, elements } = row;
|
|
48
|
+
const patientCardElements = elements.map((patientCardElementId) => {
|
|
49
|
+
const slot = patientCardElementsMap.get(patientCardElementId);
|
|
50
|
+
return slot;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const WardPatientCardRow: React.FC<WardPatientCardProps> = (props) => {
|
|
54
|
+
return (
|
|
55
|
+
<div className={styles.wardPatientCardRow + ' ' + (rowType == 'header' ? styles.wardPatientCardHeader : '')}>
|
|
56
|
+
{patientCardElements.map((PatientCardElement, i) => (
|
|
57
|
+
<PatientCardElement {...props} key={i} />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return WardPatientCardRow;
|
|
64
|
+
});
|
|
65
|
+
return ret;
|
|
66
|
+
}, [location, wardPatientCards]);
|
|
67
|
+
|
|
68
|
+
return patientCardRows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getPatientCardElementFromDefinition(
|
|
72
|
+
patientCardElementDef: PatientCardElementDefinition,
|
|
73
|
+
): WardPatientCardElement {
|
|
74
|
+
const { elementType, config } = patientCardElementDef;
|
|
75
|
+
switch (elementType) {
|
|
76
|
+
case 'bed-number':
|
|
77
|
+
return WardPatientBedNumber;
|
|
78
|
+
case 'patient-name':
|
|
79
|
+
return WardPatientName;
|
|
80
|
+
case 'patient-age':
|
|
81
|
+
return WardPatientAge;
|
|
82
|
+
case 'patient-address': {
|
|
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);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
5
|
+
|
|
6
|
+
.wardPatientCard {
|
|
7
|
+
@include type.type-style('body-compact-01');
|
|
8
|
+
color: $text-02;
|
|
9
|
+
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: spacing.$spacing-02;
|
|
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%;
|
|
49
|
+
padding: spacing.$spacing-04;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.wardPatientCardRow:empty {
|
|
53
|
+
display: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.wardPatientCardHeader {
|
|
57
|
+
@extend .dotSeparatedChildren;
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.wardPatientName {
|
|
63
|
+
@include type.type-style('heading-compact-02');
|
|
64
|
+
color: $text-02;
|
|
65
|
+
&::before {
|
|
66
|
+
content: '' !important;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.wardPatientBedNumber {
|
|
71
|
+
@include type.type-style('heading-compact-01');
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
color: $ui-02;
|
|
74
|
+
background-color: $color-blue-60-2;
|
|
75
|
+
padding: spacing.$spacing-04;
|
|
76
|
+
width: spacing.$spacing-04;
|
|
77
|
+
height: spacing.$spacing-04;
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
align-items: center;
|
|
81
|
+
&.empty {
|
|
82
|
+
background-color: $color-blue-10;
|
|
83
|
+
color: $color-blue-60-2;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.bedNumberBox {
|
|
88
|
+
display: flex;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
align-items: center;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.wardPatientAddress {
|
|
94
|
+
@extend .dotSeparatedChildren;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-wrap: wrap;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: spacing.$spacing-02;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.wardPatientObsLabel {
|
|
102
|
+
padding-right: spacing.$spacing-02;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.dotSeparatedChildren {
|
|
106
|
+
> div:not(div:first-of-type) {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
&::before {
|
|
110
|
+
content: '·';
|
|
111
|
+
padding: 0 spacing.$spacing-02;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|