@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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +23 -29
  2. package/dist/130.js +1 -1
  3. package/dist/130.js.map +1 -1
  4. package/dist/152.js +1 -0
  5. package/dist/152.js.map +1 -0
  6. package/dist/255.js +2 -0
  7. package/dist/{697.js.LICENSE.txt → 255.js.LICENSE.txt} +0 -6
  8. package/dist/255.js.map +1 -0
  9. package/dist/303.js +1 -0
  10. package/dist/303.js.map +1 -0
  11. package/dist/574.js +1 -1
  12. package/dist/589.js +1 -0
  13. package/dist/589.js.map +1 -0
  14. package/dist/695.js +2 -0
  15. package/dist/695.js.LICENSE.txt +5 -0
  16. package/dist/695.js.map +1 -0
  17. package/dist/kenyaemr-esm-ward-app.js +1 -1
  18. package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +131 -35
  19. package/dist/kenyaemr-esm-ward-app.js.map +1 -1
  20. package/dist/main.js +1 -1
  21. package/dist/main.js.map +1 -1
  22. package/dist/routes.json +1 -1
  23. package/package.json +1 -1
  24. package/src/beds/occupied-bed.component.tsx +7 -12
  25. package/src/beds/occupied-bed.scss +1 -1
  26. package/src/beds/occupied-bed.test.tsx +14 -5
  27. package/src/config-schema.ts +173 -7
  28. package/src/constant.ts +1 -0
  29. package/src/hooks/useAdmittedPatients.ts +13 -0
  30. package/src/hooks/useConcept.ts +11 -0
  31. package/src/hooks/useInpatientRequest.ts +13 -0
  32. package/src/hooks/useObs.ts +21 -0
  33. package/src/index.ts +2 -0
  34. package/src/routes.json +10 -3
  35. package/src/types/index.ts +40 -2
  36. package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +3 -0
  37. package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +80 -0
  38. package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +52 -0
  39. package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +58 -0
  40. package/src/ward-patient-card/ward-patient-card-row.resources.tsx +12 -5
  41. package/src/ward-patient-card/ward-patient-card.scss +17 -0
  42. package/src/ward-patient-card/ward-patient-card.tsx +2 -2
  43. package/src/ward-view/ward-bed.component.tsx +6 -9
  44. package/src/ward-view/ward-view.component.tsx +77 -31
  45. package/src/ward-view/ward-view.scss +0 -15
  46. package/src/ward-view/ward-view.test.tsx +12 -0
  47. package/src/ward-view-header/admission-requests-bar.component.tsx +46 -0
  48. package/src/ward-view-header/admission-requests-bar.test.tsx +42 -0
  49. package/src/ward-view-header/admission-requests.scss +42 -0
  50. package/src/ward-view-header/ward-view-header.component.tsx +18 -0
  51. package/src/ward-view-header/ward-view-header.scss +8 -0
  52. package/src/ward-workspace/admission-request-card.component.tsx +23 -0
  53. package/src/ward-workspace/admission-request-card.scss +34 -0
  54. package/src/ward-workspace/admission-request-workspace.test.tsx +38 -0
  55. package/src/ward-workspace/admission-requests-workspace.component.tsx +21 -0
  56. package/src/ward-workspace/admission-requests-workspace.scss +13 -0
  57. package/translations/en.json +1 -0
  58. package/dist/49.js +0 -1
  59. package/dist/49.js.map +0 -1
  60. package/dist/697.js +0 -2
  61. 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.patient.person, 'preferredName', {
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
- render(<OccupiedBed patients={[mockPatient]} bed={mockBed} />);
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(<OccupiedBed patients={[mockPatient, mockPatient]} bed={mockBed} />);
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
  });
@@ -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
- addressFields: defaultPatientAddressFields,
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
- addressFields: {
47
- _type: Type.Array,
48
- _description: 'For patientCardElementType "patient-address", defining which address fields to show',
49
- _default: defaultPatientAddressFields,
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 type PatientCardElementConfig = {} & PatientAddressElementConfig;
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
+ };
@@ -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": "^5.14.0 || 5.14.0-SNAPSHOT",
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
- "extensions": [
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
  {
@@ -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
- patient: Patient;
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;