@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.
Files changed (90) hide show
  1. package/.turbo/turbo-build.log +28 -32
  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/{730.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/331.js +1 -0
  12. package/dist/331.js.map +1 -0
  13. package/dist/430.js +1 -0
  14. package/dist/430.js.map +1 -0
  15. package/dist/443.js +1 -0
  16. package/dist/443.js.map +1 -0
  17. package/dist/574.js +1 -1
  18. package/dist/729.js +1 -1
  19. package/dist/729.js.map +1 -1
  20. package/dist/747.js +1 -0
  21. package/dist/747.js.map +1 -0
  22. package/dist/836.js +2 -0
  23. package/dist/836.js.LICENSE.txt +5 -0
  24. package/dist/836.js.map +1 -0
  25. package/dist/main.js +1 -1
  26. package/dist/main.js.map +1 -1
  27. package/dist/openmrs-esm-ward-app.js +1 -1
  28. package/dist/openmrs-esm-ward-app.js.buildmanifest.json +194 -50
  29. package/dist/openmrs-esm-ward-app.js.map +1 -1
  30. package/dist/routes.json +1 -1
  31. package/package.json +2 -3
  32. package/src/{empty-beds → beds}/empty-bed-skeleton.tsx +1 -1
  33. package/src/beds/empty-bed.component.tsx +24 -0
  34. package/src/beds/occupied-bed.component.tsx +35 -0
  35. package/src/beds/occupied-bed.scss +24 -0
  36. package/src/beds/occupied-bed.test.tsx +52 -0
  37. package/src/config-schema.ts +300 -3
  38. package/src/constant.ts +1 -0
  39. package/src/createDashboardLink.component.tsx +42 -0
  40. package/src/hooks/useAdmissionLocation.ts +4 -5
  41. package/src/hooks/useAdmittedPatients.ts +13 -0
  42. package/src/hooks/useConcept.ts +11 -0
  43. package/src/hooks/useInpatientRequest.ts +15 -0
  44. package/src/hooks/useLocation.test.ts +38 -0
  45. package/src/hooks/useLocation.ts +9 -0
  46. package/src/hooks/useObs.ts +21 -0
  47. package/src/hooks/useWardLocation.test.ts +78 -0
  48. package/src/hooks/useWardLocation.ts +26 -0
  49. package/src/index.ts +30 -5
  50. package/src/root.component.tsx +2 -1
  51. package/src/routes.json +46 -6
  52. package/src/types/index.ts +113 -2
  53. package/src/ward-patient-card/row-elements/ward-patient-age.tsx +20 -0
  54. package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +16 -0
  55. package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +80 -0
  56. package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +24 -0
  57. package/src/ward-patient-card/row-elements/ward-patient-name.tsx +13 -0
  58. package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +52 -0
  59. package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +58 -0
  60. package/src/ward-patient-card/ward-patient-card-row.resources.tsx +92 -0
  61. package/src/ward-patient-card/ward-patient-card.scss +114 -0
  62. package/src/ward-patient-card/ward-patient-card.tsx +30 -0
  63. package/src/ward-patient-workspace/ward-patient-action-button.extension.tsx +18 -0
  64. package/src/ward-patient-workspace/ward-patient.style.scss +11 -0
  65. package/src/ward-patient-workspace/ward-patient.workspace.tsx +79 -0
  66. package/src/ward-view/ward-bed.component.tsx +8 -8
  67. package/src/ward-view/ward-view.component.tsx +86 -44
  68. package/src/ward-view/ward-view.resource.ts +11 -1
  69. package/src/ward-view/ward-view.scss +0 -4
  70. package/src/ward-view/ward-view.test.tsx +64 -20
  71. package/src/ward-view-header/admission-requests-bar.component.tsx +50 -0
  72. package/src/ward-view-header/admission-requests-bar.test.tsx +43 -0
  73. package/src/ward-view-header/admission-requests.scss +33 -0
  74. package/src/ward-view-header/ward-view-header.component.tsx +18 -0
  75. package/src/ward-view-header/ward-view-header.scss +8 -0
  76. package/src/ward-workspace/admission-request-card.component.tsx +23 -0
  77. package/src/ward-workspace/admission-request-card.scss +34 -0
  78. package/src/ward-workspace/admission-request-workspace.test.tsx +32 -0
  79. package/src/ward-workspace/admission-requests-workspace.component.tsx +21 -0
  80. package/src/ward-workspace/admission-requests-workspace.scss +13 -0
  81. package/translations/en.json +8 -1
  82. package/dist/588.js +0 -1
  83. package/dist/588.js.map +0 -1
  84. package/dist/669.js +0 -1
  85. package/dist/669.js.map +0 -1
  86. package/dist/730.js +0 -2
  87. package/dist/730.js.map +0 -1
  88. package/src/empty-beds/empty-bed.component.tsx +0 -19
  89. /package/src/{empty-beds → beds}/empty-bed.scss +0 -0
  90. /package/src/{admitted-patient/admitted-patient-header.scss → ward-patient-card/row-elements/row-elements.scss} +0 -0
@@ -1,5 +1,302 @@
1
- import { type ConfigSchema } from '@openmrs/esm-framework';
1
+ import { Type, validators, type ConfigSchema, type PersonAddress } from '@openmrs/esm-framework';
2
+ import { patientCardElementTypes, type PatientCardElementType } from './types';
2
3
 
3
- export const configSchema: ConfigSchema = {};
4
+ const defaultWardPatientCard: WardPatientCardDefinition = {
5
+ id: 'default-card',
6
+ rows: [
7
+ {
8
+ rowType: 'header',
9
+ elements: ['bed-number', 'patient-name', 'patient-age', 'patient-address'],
10
+ },
11
+ ],
12
+ appliedTo: null,
13
+ };
4
14
 
5
- export interface ConfigObject {}
15
+ const defaultPatientAddressFields: Array<keyof PersonAddress> = ['cityVillage', 'country'];
16
+
17
+ export const defaultPatientCardElementConfig: PatientCardElementConfig = {
18
+ address: {
19
+ addressFields: defaultPatientAddressFields,
20
+ },
21
+ obs: null,
22
+ codedObsTags: null,
23
+ };
24
+
25
+ export const builtInPatientCardElements: PatientCardElementType[] = [
26
+ 'bed-number',
27
+ 'patient-name',
28
+ 'patient-age',
29
+ 'patient-address',
30
+ ];
31
+
32
+ export const configSchema: ConfigSchema = {
33
+ wardPatientCards: {
34
+ _description: 'Configure the display of ward patient cards',
35
+ patientCardElementDefinitions: {
36
+ _type: Type.Array,
37
+ _default: [],
38
+ _elements: {
39
+ id: {
40
+ _type: Type.String,
41
+ _description: 'The unique identifier for this custom patient card element',
42
+ },
43
+ elementType: {
44
+ _type: Type.String,
45
+ _description: 'The patient card element type',
46
+ _validators: [validators.oneOf(patientCardElementTypes)],
47
+ },
48
+ config: {
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
+ },
135
+ },
136
+ },
137
+ },
138
+ },
139
+ cardDefinitions: {
140
+ _type: Type.Array,
141
+ _default: [defaultWardPatientCard],
142
+ _description: `An array of card configuration. A card configuration can be applied to different ward locations.
143
+ If multiple card configurations apply to a location, only the first one is chosen.`,
144
+ _elements: {
145
+ id: {
146
+ _type: Type.String,
147
+ _description: 'The unique identifier for this card definition. Currently unused, but that might change.',
148
+ },
149
+ rows: {
150
+ _type: Type.Array,
151
+ _elements: {
152
+ id: {
153
+ _type: Type.String,
154
+ _description: 'The unique identifier for this card row. Currently unused, but that might change.',
155
+ },
156
+ elements: {
157
+ _type: Type.Array,
158
+ _element: {
159
+ _type: Type.String,
160
+ _description: 'The ID of the (bulit-in or custom) patient card elements to appear in this card row',
161
+ _validators: [validators.oneOf(patientCardElementTypes)],
162
+ },
163
+ },
164
+ },
165
+ },
166
+ appliedTo: {
167
+ _type: Type.Array,
168
+ _elements: {
169
+ location: {
170
+ _type: Type.UUID,
171
+ _description: 'The UUID of the location. If not provided, applies to all queues.',
172
+ _default: null,
173
+ },
174
+ },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ };
180
+
181
+ export interface WardConfigObject {
182
+ wardPatientCards: WardPatientCardsConfig;
183
+ }
184
+
185
+ export interface WardPatientCardsConfig {
186
+ patientCardElementDefinitions: Array<PatientCardElementDefinition>;
187
+ cardDefinitions: Array<WardPatientCardDefinition>;
188
+ }
189
+
190
+ export interface WardPatientCardDefinition {
191
+ id: string;
192
+ rows: Array<{
193
+ /**
194
+ * The type of row. Currently, only "header" is supported
195
+ */
196
+ rowType: 'header';
197
+
198
+ /**
199
+ * an array of (either built-in or custom) patient card element ids
200
+ */
201
+ elements: Array<string>;
202
+ }>;
203
+ appliedTo?: Array<{
204
+ /**
205
+ * locationUuid. If given, only applies to patients at the specified ward locations. (If not provided, applies to all locations)
206
+ */
207
+ location: string;
208
+ }>;
209
+ }
210
+
211
+ export type PatientCardElementDefinition = {
212
+ id: string;
213
+ elementType: PatientCardElementType;
214
+ config?: PatientCardElementConfig;
215
+ };
216
+
217
+ export interface PatientAddressElementConfig {
218
+ addressFields: Array<keyof PersonAddress>;
219
+ }
220
+
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,42 @@
1
+ import React, { useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import { ConfigurableLink } from '@openmrs/esm-framework';
4
+ import { BrowserRouter, useLocation } from 'react-router-dom';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ export interface DashboardLinkConfig {
8
+ name: string;
9
+ title: string;
10
+ }
11
+
12
+ // TODO: extract this out into the esm-framework and all 4 copies of this file in this repo?
13
+
14
+ function DashboardExtension({ dashboardLinkConfig }: { dashboardLinkConfig: DashboardLinkConfig }) {
15
+ //
16
+ const { t } = useTranslation();
17
+ const { name, title } = dashboardLinkConfig;
18
+ const location = useLocation();
19
+ const spaBasePath = `${window.spaBase}/home`;
20
+
21
+ const navLink = useMemo(() => {
22
+ const pathArray = location.pathname.split('/home');
23
+ const lastElement = pathArray[pathArray.length - 1];
24
+ return decodeURIComponent(lastElement);
25
+ }, [location.pathname]);
26
+
27
+ return (
28
+ <ConfigurableLink
29
+ className={classNames('cds--side-nav__link', {
30
+ 'active-left-nav-link': navLink.match(name),
31
+ })}
32
+ to={`${spaBasePath}/${name}`}>
33
+ {t(title)}
34
+ </ConfigurableLink>
35
+ );
36
+ }
37
+
38
+ export const createDashboardLink = (dashboardLinkConfig: DashboardLinkConfig) => () => (
39
+ <BrowserRouter>
40
+ <DashboardExtension dashboardLinkConfig={dashboardLinkConfig} />
41
+ </BrowserRouter>
42
+ );
@@ -1,13 +1,12 @@
1
1
  import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
- import useSWR from 'swr';
3
2
  import { type AdmissionLocation } from '../types/index';
3
+ import useSWRImmutable from 'swr/immutable';
4
4
 
5
5
  export function useAdmissionLocation(locationUuid: string, rep: string = 'full') {
6
- const apiUrl = `${restBaseUrl}/admissionLocation/${locationUuid}` + (rep ? `?v=${rep}` : '');
7
- const { data, ...rest } = useSWR<{ data: AdmissionLocation }, Error>(apiUrl, openmrsFetch);
8
-
6
+ const apiUrl = locationUuid ? `${restBaseUrl}/admissionLocation/${locationUuid}?v=${rep}` : null;
7
+ const { data, ...rest } = useSWRImmutable<{ data: AdmissionLocation }, Error>(apiUrl, openmrsFetch);
9
8
  return {
10
- admissionLocation: data?.data ?? null,
9
+ admissionLocation: data?.data,
11
10
  ...rest,
12
11
  };
13
12
  }
@@ -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,15 @@
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import type { InpatientRequest } from '../types';
3
+ import useSWR from 'swr';
4
+
5
+ export function useInpatientRequest(locationUuid: string) {
6
+ const { data, ...rest } = useSWR<FetchResponse<Array<InpatientRequest>>, Error>(
7
+ locationUuid ? `${restBaseUrl}/emrapi/inpatient/admissionRequests?admissionLocation=${locationUuid}` : null,
8
+ openmrsFetch,
9
+ );
10
+
11
+ return {
12
+ inpatientRequests: data?.data,
13
+ ...rest,
14
+ };
15
+ }
@@ -0,0 +1,38 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import useLocation from './useLocation';
3
+ import useSWRImmutable from 'swr/immutable';
4
+ import { restBaseUrl } from '@openmrs/esm-framework';
5
+
6
+ jest.mock('swr/immutable', () =>
7
+ jest.fn().mockReturnValue({
8
+ data: {},
9
+ error: null,
10
+ isValidating: false,
11
+ mutate: jest.fn(),
12
+ }),
13
+ );
14
+
15
+ const useSWRImmutableMock = useSWRImmutable as jest.Mock;
16
+
17
+ describe('useLocation hook', () => {
18
+ it('should call useLocation', () => {
19
+ const { result } = renderHook(() => useLocation('testUUID'));
20
+ expect(useSWRImmutableMock).toHaveBeenCalledWith(
21
+ `${restBaseUrl}/location/testUUID?v=custom:(display,uuid)`,
22
+ expect.any(Function),
23
+ );
24
+ });
25
+
26
+ it('should call useLocation with given rep', () => {
27
+ const { result } = renderHook(() => useLocation('testUUID', 'custom:(display,uuid,links)'));
28
+ expect(useSWRImmutableMock).toHaveBeenCalledWith(
29
+ `${restBaseUrl}/location/testUUID?v=custom:(display,uuid,links)`,
30
+ expect.any(Function),
31
+ );
32
+ });
33
+
34
+ it('should call useSWR with key=null', () => {
35
+ const { result } = renderHook(() => useLocation(null, 'custom:(display,uuid,links)'));
36
+ expect(useSWRImmutableMock).toHaveBeenCalledWith(null, expect.any(Function));
37
+ });
38
+ });
@@ -0,0 +1,9 @@
1
+ import { type Location, openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
2
+ import useSWRImmutable from 'swr/immutable';
3
+
4
+ export default function useLocation(locationUuid: string, rep: string = 'custom:(display,uuid)') {
5
+ return useSWRImmutable<FetchResponse<Location>>(
6
+ locationUuid ? `${restBaseUrl}/location/${locationUuid}?v=${rep}` : null,
7
+ openmrsFetch,
8
+ );
9
+ }
@@ -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
+ }
@@ -0,0 +1,78 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { useSession } from '@openmrs/esm-framework';
3
+ import { useParams } from 'react-router-dom';
4
+ import useWardLocation from './useWardLocation';
5
+ import useLocation from './useLocation';
6
+
7
+ jest.mock('@openmrs/esm-framework', () => ({
8
+ useSession: jest.fn(),
9
+ }));
10
+ jest.mock('react-router-dom', () => ({
11
+ useParams: jest.fn(),
12
+ }));
13
+ jest.mock('./useLocation', () => jest.fn());
14
+
15
+ const mockedUseParams = useParams as jest.Mock;
16
+ const mockedUseSession = useSession as jest.Mock;
17
+ const mockedUseLocation = useLocation as jest.Mock;
18
+
19
+ describe('useWardLocation', () => {
20
+ it('returns session location when locationUuidFromUrl is not provided', async () => {
21
+ mockedUseParams.mockReturnValue({});
22
+ mockedUseSession.mockReturnValue({ sessionLocation: 'sessionLocation' });
23
+ mockedUseLocation.mockReturnValue({});
24
+
25
+ const { result } = renderHook(() => useWardLocation());
26
+
27
+ expect(result.current.location).toBe('sessionLocation');
28
+ });
29
+
30
+ it('returns location from useLocation when locationUuidFromUrl is provided', async () => {
31
+ mockedUseParams.mockReturnValue({ locationUuid: 'uuid' });
32
+ mockedUseLocation.mockReturnValue({
33
+ data: { data: 'locationData' },
34
+ isLoading: false,
35
+ error: null,
36
+ });
37
+
38
+ const { result } = renderHook(() => useWardLocation());
39
+
40
+ expect(result.current.location).toBe('locationData');
41
+ expect(result.current.invalidLocation).toBeFalsy();
42
+ });
43
+
44
+ it('handles loading state correctly', async () => {
45
+ mockedUseParams.mockReturnValue({ locationUuid: 'uuid' });
46
+ mockedUseLocation.mockReturnValue({
47
+ isLoading: true,
48
+ });
49
+
50
+ const { result } = renderHook(() => useWardLocation());
51
+
52
+ expect(result.current.isLoadingLocation).toBe(true);
53
+ });
54
+
55
+ it('handles error state correctly when fetching location fails', async () => {
56
+ const error = new Error('Error fetching location');
57
+ mockedUseParams.mockReturnValue({ locationUuid: 'uuid' });
58
+ mockedUseLocation.mockReturnValue({
59
+ error,
60
+ });
61
+
62
+ const { result } = renderHook(() => useWardLocation());
63
+
64
+ expect(result.current.errorFetchingLocation).toBe(error);
65
+ });
66
+
67
+ it('identifies invalid location correctly', async () => {
68
+ const error = new Error('Error fetching location');
69
+ mockedUseParams.mockReturnValue({ locationUuid: 'uuid' });
70
+ mockedUseLocation.mockReturnValue({
71
+ error,
72
+ });
73
+
74
+ const { result } = renderHook(() => useWardLocation());
75
+
76
+ expect(result.current.invalidLocation).toBeTruthy();
77
+ });
78
+ });
@@ -0,0 +1,26 @@
1
+ import { type Location, useLocations, useSession } from '@openmrs/esm-framework';
2
+ import { useParams } from 'react-router-dom';
3
+ import useLocation from './useLocation';
4
+
5
+ export default function useWardLocation(): {
6
+ location: Location;
7
+ isLoadingLocation: boolean;
8
+ errorFetchingLocation: Error;
9
+ invalidLocation: boolean;
10
+ } {
11
+ const { locationUuid: locationUuidFromUrl } = useParams();
12
+ const { sessionLocation } = useSession();
13
+ const {
14
+ data: locationResponse,
15
+ isLoading: isLoadingLocation,
16
+ error: errorFetchingLocation,
17
+ } = useLocation(locationUuidFromUrl ? locationUuidFromUrl : null);
18
+ const invalidLocation = locationUuidFromUrl && errorFetchingLocation;
19
+
20
+ return {
21
+ location: locationUuidFromUrl ? locationResponse?.data : sessionLocation,
22
+ isLoadingLocation,
23
+ errorFetchingLocation,
24
+ invalidLocation,
25
+ };
26
+ }
package/src/index.ts CHANGED
@@ -1,11 +1,18 @@
1
- import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
2
- import rootComponent from './root.component';
1
+ import {
2
+ defineConfigSchema,
3
+ getAsyncLifecycle,
4
+ getSyncLifecycle,
5
+ registerBreadcrumbs,
6
+ registerFeatureFlag,
7
+ } from '@openmrs/esm-framework';
3
8
  import { configSchema } from './config-schema';
9
+ import rootComponent from './root.component';
10
+ import { moduleName } from './constant';
11
+ import WardPatientActionButton from './ward-patient-workspace/ward-patient-action-button.extension';
12
+ import { createDashboardLink } from './createDashboardLink.component';
4
13
 
5
14
  export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
6
15
 
7
- const moduleName = '@openmrs/esm-ward-app';
8
-
9
16
  const options = {
10
17
  featureName: 'ward',
11
18
  moduleName,
@@ -13,8 +20,26 @@ const options = {
13
20
 
14
21
  export const root = getSyncLifecycle(rootComponent, options);
15
22
 
23
+ export const wardDashboardLink = getSyncLifecycle(createDashboardLink({ name: 'ward', title: 'wards' }), options);
24
+
25
+ export const admissionRequestWorkspace = getAsyncLifecycle(
26
+ () => import('./ward-workspace/admission-requests-workspace.component'),
27
+ options,
28
+ );
29
+
30
+ export const wardPatientWorkspace = getAsyncLifecycle(
31
+ () => import('./ward-patient-workspace/ward-patient.workspace'),
32
+ options,
33
+ );
34
+
35
+ export const wardPatientActionButtonExtension = getSyncLifecycle(WardPatientActionButton, options);
36
+
16
37
  export function startupApp() {
17
38
  registerBreadcrumbs([]);
18
-
19
39
  defineConfigSchema(moduleName, configSchema);
40
+ registerFeatureFlag(
41
+ 'bedmanagement-module',
42
+ 'Bed Management Module',
43
+ 'Enables features related to bed management / assignment. Requires the backend bed management module to be installed.',
44
+ );
20
45
  }
@@ -3,7 +3,8 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
3
  import WardView from './ward-view/ward-view.component';
4
4
 
5
5
  const Root: React.FC = () => {
6
- const wardViewBasename = window.getOpenmrsSpaBase() + 'ward';
6
+ // t('wards', 'Wards')
7
+ const wardViewBasename = window.getOpenmrsSpaBase() + 'home/ward';
7
8
 
8
9
  return (
9
10
  <main>