@kenyaemr/esm-ward-app 7.0.2-pre.66 → 7.0.2-pre.68
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 -29
- 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/574.js +1 -1
- package/dist/589.js +1 -0
- package/dist/589.js.map +1 -0
- package/dist/695.js +2 -0
- package/dist/695.js.LICENSE.txt +5 -0
- package/dist/695.js.map +1 -0
- package/dist/kenyaemr-esm-ward-app.js +1 -1
- package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +131 -35
- package/dist/kenyaemr-esm-ward-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- 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/hooks/useAdmittedPatients.ts +13 -0
- package/src/hooks/useConcept.ts +11 -0
- package/src/hooks/useInpatientRequest.ts +13 -0
- package/src/hooks/useObs.ts +21 -0
- package/src/index.ts +2 -0
- package/src/routes.json +10 -3
- package/src/types/index.ts +40 -2
- 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-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 +17 -0
- package/src/ward-patient-card/ward-patient-card.tsx +2 -2
- package/src/ward-view/ward-bed.component.tsx +6 -9
- package/src/ward-view/ward-view.component.tsx +77 -31
- package/src/ward-view/ward-view.scss +0 -15
- package/src/ward-view/ward-view.test.tsx +12 -0
- package/src/ward-view-header/admission-requests-bar.component.tsx +46 -0
- package/src/ward-view-header/admission-requests-bar.test.tsx +42 -0
- package/src/ward-view-header/admission-requests.scss +42 -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 +38 -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 +1 -0
- 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
|
@@ -5,6 +5,7 @@ import { mockAdmissionLocation } from '../../../../__mocks__/wards.mock';
|
|
|
5
5
|
import { bedLayoutToBed, filterBeds } from '../ward-view/ward-view.resource';
|
|
6
6
|
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
7
7
|
import { configSchema, defaultPatientCardElementConfig } from '../config-schema';
|
|
8
|
+
import { mockAdmittedPatient } from '../../../../__mocks__/ward-patient';
|
|
8
9
|
|
|
9
10
|
const defaultConfig = getDefaultsFromConfigSchema(configSchema);
|
|
10
11
|
|
|
@@ -13,22 +14,22 @@ jest.mocked(useConfig).mockReturnValue(defaultConfig);
|
|
|
13
14
|
const mockBedLayouts = filterBeds(mockAdmissionLocation);
|
|
14
15
|
|
|
15
16
|
const mockBedToUse = mockBedLayouts[0];
|
|
16
|
-
jest.replaceProperty(mockBedToUse.
|
|
17
|
+
jest.replaceProperty(mockBedToUse.patients[0].person, 'preferredName', {
|
|
17
18
|
uuid: '',
|
|
18
19
|
givenName: 'Alice',
|
|
19
20
|
familyName: 'Johnson',
|
|
20
21
|
});
|
|
21
|
-
const mockPatient = mockBedToUse.patient;
|
|
22
22
|
const mockBed = bedLayoutToBed(mockBedToUse);
|
|
23
23
|
|
|
24
24
|
describe('Occupied bed: ', () => {
|
|
25
25
|
it('renders a single bed with patient details', () => {
|
|
26
|
-
|
|
26
|
+
const mockPatient = mockAdmittedPatient.patient;
|
|
27
|
+
render(<OccupiedBed wardPatients={[{ ...mockAdmittedPatient, admitted: true }]} bed={mockBed} />);
|
|
27
28
|
const patientName = screen.getByText('Alice Johnson');
|
|
28
29
|
expect(patientName).toBeInTheDocument();
|
|
29
30
|
const patientAge = `${mockPatient.person.age} yrs`;
|
|
30
31
|
expect(screen.getByText(patientAge)).toBeInTheDocument();
|
|
31
|
-
const defaultAddressFields = defaultPatientCardElementConfig.addressFields;
|
|
32
|
+
const defaultAddressFields = defaultPatientCardElementConfig.address.addressFields;
|
|
32
33
|
defaultAddressFields.forEach((addressField) => {
|
|
33
34
|
const addressFieldValue = mockPatient.person.preferredAddress[addressField] as string;
|
|
34
35
|
expect(screen.getByText(addressFieldValue)).toBeInTheDocument();
|
|
@@ -36,7 +37,15 @@ describe('Occupied bed: ', () => {
|
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
it('renders a divider for shared patients', () => {
|
|
39
|
-
render(
|
|
40
|
+
render(
|
|
41
|
+
<OccupiedBed
|
|
42
|
+
wardPatients={[
|
|
43
|
+
{ ...mockAdmittedPatient, admitted: true },
|
|
44
|
+
{ ...mockAdmittedPatient, admitted: true },
|
|
45
|
+
]}
|
|
46
|
+
bed={mockBed}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
40
49
|
const bedShareText = screen.getByTitle('Bed share');
|
|
41
50
|
expect(bedShareText).toBeInTheDocument();
|
|
42
51
|
});
|
package/src/config-schema.ts
CHANGED
|
@@ -15,7 +15,11 @@ const defaultWardPatientCard: WardPatientCardDefinition = {
|
|
|
15
15
|
const defaultPatientAddressFields: Array<keyof PersonAddress> = ['cityVillage', 'country'];
|
|
16
16
|
|
|
17
17
|
export const defaultPatientCardElementConfig: PatientCardElementConfig = {
|
|
18
|
-
|
|
18
|
+
address: {
|
|
19
|
+
addressFields: defaultPatientAddressFields,
|
|
20
|
+
},
|
|
21
|
+
obs: null,
|
|
22
|
+
codedObsTags: null,
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
export const builtInPatientCardElements: PatientCardElementType[] = [
|
|
@@ -23,7 +27,6 @@ export const builtInPatientCardElements: PatientCardElementType[] = [
|
|
|
23
27
|
'patient-name',
|
|
24
28
|
'patient-age',
|
|
25
29
|
'patient-address',
|
|
26
|
-
'admission-time',
|
|
27
30
|
];
|
|
28
31
|
|
|
29
32
|
export const configSchema: ConfigSchema = {
|
|
@@ -43,10 +46,92 @@ export const configSchema: ConfigSchema = {
|
|
|
43
46
|
_validators: [validators.oneOf(patientCardElementTypes)],
|
|
44
47
|
},
|
|
45
48
|
config: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
address: {
|
|
50
|
+
_description: 'Config for the patientCardElementType "patient-address"',
|
|
51
|
+
addressFields: {
|
|
52
|
+
_type: Type.Array,
|
|
53
|
+
_description: 'defines which address fields to show',
|
|
54
|
+
_default: defaultPatientAddressFields,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
obs: {
|
|
58
|
+
_description: 'Config for the patientCardElementType "patient-obs"',
|
|
59
|
+
conceptUuid: {
|
|
60
|
+
_type: Type.UUID,
|
|
61
|
+
_description: 'Required. Identifies the concept to use to identify the desired observations.',
|
|
62
|
+
_default: null,
|
|
63
|
+
},
|
|
64
|
+
label: {
|
|
65
|
+
_type: Type.String,
|
|
66
|
+
_description:
|
|
67
|
+
"Optional. The custom label or i18n key to the translated label to display. If not provided, defaults to the concept's name. (Note that this can be set to an empty string to not show a label)",
|
|
68
|
+
_default: null,
|
|
69
|
+
},
|
|
70
|
+
labelI18nModule: {
|
|
71
|
+
_type: Type.String,
|
|
72
|
+
_description: 'Optional. The custom module to use for translation of the label',
|
|
73
|
+
_default: null,
|
|
74
|
+
},
|
|
75
|
+
orderBy: {
|
|
76
|
+
_type: Type.String,
|
|
77
|
+
_description:
|
|
78
|
+
"Optional. One of 'ascending' or 'descending', specifying whether to display the obs by obsDatetime ascendingly or descendingly. Defaults to ascending.",
|
|
79
|
+
_default: 'descending',
|
|
80
|
+
_validators: [validators.oneOf(['ascending', 'descending'])],
|
|
81
|
+
},
|
|
82
|
+
limit: {
|
|
83
|
+
_type: Type.Number,
|
|
84
|
+
_description: 'Optional. Limits the max number of obs to display. Unlimited by default.',
|
|
85
|
+
_default: null,
|
|
86
|
+
},
|
|
87
|
+
onlyWithinCurrentVisit: {
|
|
88
|
+
_type: Type.Boolean,
|
|
89
|
+
_description:
|
|
90
|
+
'Optional. If true, limits display to only observations within current visit. Defaults to false',
|
|
91
|
+
_default: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
codedObsTags: {
|
|
95
|
+
_description: 'Config for the patientCardElementType "patient-coded-obs-tags"',
|
|
96
|
+
conceptUuid: {
|
|
97
|
+
_type: Type.UUID,
|
|
98
|
+
_description: 'Required. Identifies the concept to use to identify the desired observations.',
|
|
99
|
+
_default: null,
|
|
100
|
+
},
|
|
101
|
+
summaryLabel: {
|
|
102
|
+
_type: Type.String,
|
|
103
|
+
_description: `Optional. The custom label or i18n key to the translated label to display for the summary tag. The summary tag shows the count of the number of answers that are present but not configured to show as their own tags. If not provided, defaults to the name of the concept.`,
|
|
104
|
+
_default: null,
|
|
105
|
+
},
|
|
106
|
+
summaryLabelI18nModule: {
|
|
107
|
+
_type: Type.String,
|
|
108
|
+
_description: 'Optional. The custom module to use for translation of the summary label',
|
|
109
|
+
_default: null,
|
|
110
|
+
},
|
|
111
|
+
summaryLabelColor: {
|
|
112
|
+
_type: Type.String,
|
|
113
|
+
_description:
|
|
114
|
+
'The color of the summary tag. See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors',
|
|
115
|
+
_default: null,
|
|
116
|
+
},
|
|
117
|
+
tags: {
|
|
118
|
+
_description: `An array specifying concept sets and color. Observations with coded values that are members of the specified concept sets will be displayed as their own tags with the specified color. Any observation with coded values not belonging to any concept sets specified will be summarized as a count in the summary tag. If a concept set is listed multiple times, the first matching applied-to rule takes precedence.`,
|
|
119
|
+
_type: Type.Array,
|
|
120
|
+
_elements: {
|
|
121
|
+
color: {
|
|
122
|
+
_type: Type.String,
|
|
123
|
+
_description:
|
|
124
|
+
'Color of the tag. See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors.',
|
|
125
|
+
},
|
|
126
|
+
appliedToConceptSets: {
|
|
127
|
+
_type: Type.Array,
|
|
128
|
+
_description: `The concept sets which the color applies to. Observations with coded values that are members of the specified concept sets will be displayed as their own tag with the specified color. If an observation's coded value belongs to multiple concept sets, the first matching applied-to rule takes precedence.`,
|
|
129
|
+
_elements: {
|
|
130
|
+
_type: Type.UUID,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
50
135
|
},
|
|
51
136
|
},
|
|
52
137
|
},
|
|
@@ -133,4 +218,85 @@ export interface PatientAddressElementConfig {
|
|
|
133
218
|
addressFields: Array<keyof PersonAddress>;
|
|
134
219
|
}
|
|
135
220
|
|
|
136
|
-
export
|
|
221
|
+
export interface PatientObsElementConfig {
|
|
222
|
+
/**
|
|
223
|
+
* Required. Identifies the concept to use to identify the desired observations.
|
|
224
|
+
*/
|
|
225
|
+
conceptUuid: string;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Optional. The custom label or i18n key to the translated label to display. If not provided, defaults to the concept's name.
|
|
229
|
+
* (Note that this can be set to an empty string to not show a label)
|
|
230
|
+
*/
|
|
231
|
+
label?: string;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Optional. The custom module to use for translation of the label
|
|
235
|
+
*/
|
|
236
|
+
labelI18nModule?: string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Optional. One of 'ascending' or 'descending', specifying whether to display the obs by obsDatetime ascendingly or descendingly. Defaults to descending.
|
|
240
|
+
*/
|
|
241
|
+
orderBy?: 'ascending' | 'descending';
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Optional. Limits the max number of obs to display. Unlimited by default.
|
|
245
|
+
*/
|
|
246
|
+
limit?: number;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Optional. If true, limits display to only observations within current visit
|
|
250
|
+
*/
|
|
251
|
+
onlyWithinCurrentVisit?: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface PatientCodedObsTagsElementConfig {
|
|
255
|
+
/**
|
|
256
|
+
* Required. Identifies the concept to use to identify the desired observations.
|
|
257
|
+
*/
|
|
258
|
+
conceptUuid: string;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Optional. The custom label or i18n key to the translated label to display for the summary tag. The summary tag
|
|
262
|
+
* shows the count of the number of answers that are present but not configured to show as their own tags. If not
|
|
263
|
+
* provided, defaults to the name of the concept.
|
|
264
|
+
*/
|
|
265
|
+
summaryLabel?: string;
|
|
266
|
+
/**
|
|
267
|
+
* Optional. The custom module to use for translation of the summary label
|
|
268
|
+
*/
|
|
269
|
+
summaryLabelI18nModule?: string;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The color of the summary tag.
|
|
273
|
+
* See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors
|
|
274
|
+
*/
|
|
275
|
+
summaryLabelColor?: string;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* An array specifying concept sets and color. Observations with coded values that are members of the specified concept sets
|
|
279
|
+
* will be displayed as their own tags with the specified color. Any observation with coded values not belonging to
|
|
280
|
+
* any concept sets specified will be summarized as a count in the summary tag. If a concept set is listed multiple times,
|
|
281
|
+
* the first matching applied-to rule takes precedence.
|
|
282
|
+
*/
|
|
283
|
+
tags: Array<{
|
|
284
|
+
/**
|
|
285
|
+
* Color of the tag. See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors.
|
|
286
|
+
*/
|
|
287
|
+
color: string;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The concept sets which the color applies to. Observations with coded values that are members of the specified concept sets
|
|
291
|
+
* will be displayed as their own tag with the specified color.
|
|
292
|
+
* If an observation's coded value belongs to multiple concept sets, the first matching applied-to rule takes precedence.
|
|
293
|
+
*/
|
|
294
|
+
appliedToConceptSets: Array<string>;
|
|
295
|
+
}>;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export type PatientCardElementConfig = {
|
|
299
|
+
address: PatientAddressElementConfig;
|
|
300
|
+
obs: PatientObsElementConfig;
|
|
301
|
+
codedObsTags: PatientCodedObsTagsElementConfig;
|
|
302
|
+
};
|
package/src/constant.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const moduleName = '@openmrs/esm-ward-app';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import { type AdmittedPatient } from '../types';
|
|
3
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
4
|
+
|
|
5
|
+
export function useAdmittedPatients(locationUuid: string) {
|
|
6
|
+
const apiUrl = `/ws/rest/emrapi/inpatient/visits?currentLocation=${locationUuid}`;
|
|
7
|
+
const { data, ...rest } = useSWR<{ data: AdmittedPatient[] }, Error>(apiUrl, openmrsFetch);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
admittedPatients: data?.data ?? null,
|
|
11
|
+
...rest,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Concept, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWRImmutable from 'swr/immutable';
|
|
3
|
+
|
|
4
|
+
export function useConcepts(uuids: string[], rep = 'default') {
|
|
5
|
+
const apiUrl = `${restBaseUrl}/concept?references=${uuids.join()}&v=${rep}`;
|
|
6
|
+
const { data, ...rest } = useSWRImmutable<{ data: { results: Array<Concept> } }, Error>(apiUrl, openmrsFetch);
|
|
7
|
+
return {
|
|
8
|
+
concepts: data?.data?.results,
|
|
9
|
+
...rest,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import type { InpatientRequest } from '../types';
|
|
4
|
+
|
|
5
|
+
export function useInpatientRequest(locationUuid: string) {
|
|
6
|
+
const apiUrl = `/ws/rest/emrapi/inpatient/admissionRequests?admissionLocation=${locationUuid}`;
|
|
7
|
+
const { data, ...rest } = useSWR<{ data: Array<InpatientRequest> }, Error>(apiUrl, openmrsFetch);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
inpatientRequests: data?.data || null,
|
|
11
|
+
...rest,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import { type Observation } from '../types';
|
|
3
|
+
import { type Link, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
4
|
+
|
|
5
|
+
interface ObsSearchCriteria {
|
|
6
|
+
patient: string;
|
|
7
|
+
concept: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useObs(criteria?: ObsSearchCriteria, representation = 'default') {
|
|
11
|
+
const params = new URLSearchParams({
|
|
12
|
+
...criteria,
|
|
13
|
+
v: representation,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const apiUrl = `${restBaseUrl}/obs?${params}`;
|
|
17
|
+
return useSWR<{ data: { results: Array<Observation>; totalCount: number; links: Array<Link> } }, Error>(
|
|
18
|
+
apiUrl,
|
|
19
|
+
openmrsFetch,
|
|
20
|
+
);
|
|
21
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs, registerFeatureFlag } from '@openmrs/esm-framework';
|
|
2
2
|
import { configSchema } from './config-schema';
|
|
3
3
|
import rootComponent from './root.component';
|
|
4
|
+
import admissionRequestsWorkspace from "./ward-workspace/admission-requests-workspace.component"
|
|
4
5
|
|
|
5
6
|
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ const options = {
|
|
|
13
14
|
|
|
14
15
|
export const root = getSyncLifecycle(rootComponent, options);
|
|
15
16
|
|
|
17
|
+
export const admissionRequestWorkspace = getSyncLifecycle(admissionRequestsWorkspace, options);
|
|
16
18
|
export function startupApp() {
|
|
17
19
|
registerBreadcrumbs([]);
|
|
18
20
|
defineConfigSchema(moduleName, configSchema);
|
package/src/routes.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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"
|
|
5
6
|
},
|
|
6
7
|
"optionalBackendDependencies":{
|
|
7
8
|
"bedmanagement":{
|
|
8
|
-
"version": "^
|
|
9
|
+
"version": "^6.0.0 || 6.0.0-SNAPSHOT",
|
|
9
10
|
"feature": {
|
|
10
11
|
"flagName": "bedmanagement-module",
|
|
11
12
|
"label":"Ward App Patient Service",
|
|
@@ -13,7 +14,13 @@
|
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
|
-
|
|
17
|
+
"workspaces": [
|
|
18
|
+
{
|
|
19
|
+
"name":"admission-requests-cards",
|
|
20
|
+
"component": "admissionRequestWorkspace",
|
|
21
|
+
"title":"admissionRequests",
|
|
22
|
+
"type":"admission-requests"
|
|
23
|
+
}
|
|
17
24
|
],
|
|
18
25
|
"pages": [
|
|
19
26
|
{
|
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;
|
|
@@ -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={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,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;
|