@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
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useParams } from 'react-router-dom';
3
+ import { type WardPatientCardProps } from '../types';
4
+ import { usePatientCardRows } from './ward-patient-card-row.resources';
5
+ import styles from './ward-patient-card.scss';
6
+ import { getPatientName, launchWorkspace } from '@openmrs/esm-framework';
7
+ import { type WardPatientWorkspaceProps } from '../ward-patient-workspace/ward-patient.workspace';
8
+
9
+ const WardPatientCard: React.FC<WardPatientCardProps> = (props) => {
10
+ const { locationUuid } = useParams();
11
+ const patientCardRows = usePatientCardRows(locationUuid);
12
+
13
+ return (
14
+ <div className={styles.wardPatientCard}>
15
+ {patientCardRows.map((WardPatientCardRow, i) => (
16
+ <WardPatientCardRow key={i} {...props} />
17
+ ))}
18
+ <button
19
+ className={styles.wardPatientCardButton}
20
+ onClick={() =>
21
+ launchWorkspace<WardPatientWorkspaceProps>('ward-patient-workspace', { patientUuid: props.patient.uuid })
22
+ }>
23
+ {/* Name will not be displayed; just there for a11y */}
24
+ {getPatientName(props.patient.person)}
25
+ </button>
26
+ </div>
27
+ );
28
+ };
29
+
30
+ export default WardPatientCard;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { UserAvatarIcon } from '@openmrs/esm-framework';
4
+ import { ActionMenuButton, launchWorkspace } from '@openmrs/esm-framework';
5
+
6
+ export default function WardPatientActionButton() {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <ActionMenuButton
11
+ getIcon={(props) => <UserAvatarIcon {...props} />}
12
+ label={t('Patient', 'patient')}
13
+ iconDescription={t('Patient', 'patient')}
14
+ handler={() => launchWorkspace('ward-patient-workspace')}
15
+ type={'ward'}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,11 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+
4
+ .workspaceContainer {
5
+ min-height: var(--desktop-workspace-window-height);
6
+ }
7
+
8
+ .headerPatientDetail {
9
+ @include type.type-style('body-compact-02');
10
+ margin: 0 spacing.$spacing-02;
11
+ }
@@ -0,0 +1,79 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { InlineNotification } from '@carbon/react';
4
+ import { InlineLoading } from '@carbon/react';
5
+ import {
6
+ type DefaultWorkspaceProps,
7
+ ExtensionSlot,
8
+ attach,
9
+ getPatientName,
10
+ usePatient,
11
+ age,
12
+ } from '@openmrs/esm-framework';
13
+ import styles from './ward-patient.style.scss';
14
+
15
+ attach('ward-patient-workspace-header-slot', 'patient-vitals-info');
16
+
17
+ export interface WardPatientWorkspaceProps extends DefaultWorkspaceProps {
18
+ patientUuid: string;
19
+ }
20
+
21
+ export default function WardPatientWorkspace({ patientUuid, setTitle }: WardPatientWorkspaceProps) {
22
+ const { t } = useTranslation();
23
+ const { patient, isLoading, error } = usePatient(patientUuid);
24
+
25
+ useEffect(() => {
26
+ if (isLoading) {
27
+ setTitle(t('wardPatientWorkspaceTitle', 'Ward Patient'), <InlineLoading />);
28
+ } else if (patient) {
29
+ setTitle(getPatientName(patient), <PatientWorkspaceTitle patient={patient} />);
30
+ } else if (error) {
31
+ setTitle(t('wardPatientWorkspaceTitle', 'Ward Patient'));
32
+ }
33
+ }, [patient]);
34
+
35
+ return (
36
+ <div className={styles.workspaceContainer}>
37
+ {isLoading ? (
38
+ <InlineLoading />
39
+ ) : patient ? (
40
+ <WardPatientWorkspaceView patient={patient} />
41
+ ) : error ? (
42
+ <InlineNotification>{error.message}</InlineNotification>
43
+ ) : (
44
+ <InlineNotification>
45
+ {t('failedToLoadPatientWorkspace', 'Ward patient workspace has failed to load.')}
46
+ </InlineNotification>
47
+ )}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ interface WardPatientWorkspaceViewProps {
53
+ patient: fhir.Patient;
54
+ }
55
+
56
+ function WardPatientWorkspaceView({ patient }: WardPatientWorkspaceViewProps) {
57
+ const extensionSlotState = useMemo(() => ({ patient, patientUuid: patient.id }), [patient]);
58
+
59
+ return (
60
+ <>
61
+ <div>
62
+ <ExtensionSlot name="ward-patient-workspace-header-slot" state={extensionSlotState} />
63
+ </div>
64
+ <div>
65
+ <ExtensionSlot name="ward-patient-workspace-content-slot" state={extensionSlotState} />
66
+ </div>
67
+ </>
68
+ );
69
+ }
70
+
71
+ function PatientWorkspaceTitle({ patient }: { patient: fhir.Patient }) {
72
+ return (
73
+ <>
74
+ <div>{getPatientName(patient)} &nbsp;</div>
75
+ <div className={styles.headerPatientDetail}>&middot; &nbsp; {patient.gender}</div>
76
+ <div className={styles.headerPatientDetail}>&middot; &nbsp; {age(patient.birthDate)}</div>
77
+ </>
78
+ );
79
+ }
@@ -1,15 +1,15 @@
1
- import { type Patient } from '@openmrs/esm-framework';
1
+ import { type Visit, type Patient } from '@openmrs/esm-framework';
2
2
  import React from 'react';
3
- import EmptyBed from '../empty-beds/empty-bed.component';
4
- import { type Bed } from '../types';
5
-
6
- interface WardBedProps {
3
+ import EmptyBed from '../beds/empty-bed.component';
4
+ import { type WardPatient, type Bed } from '../types';
5
+ import OccupiedBed from '../beds/occupied-bed.component';
6
+ export interface WardBedProps {
7
+ wardPatients: Array<WardPatient>;
7
8
  bed: Bed;
8
- patients: Patient[];
9
9
  }
10
10
 
11
- const WardBed = ({ bed, patients }: WardBedProps) => {
12
- return <>{patients?.length > 0 ? <div></div> : <EmptyBed bed={bed} />}</>;
11
+ const WardBed: React.FC<WardBedProps> = ({ bed, wardPatients }) => {
12
+ return wardPatients?.length > 0 ? <OccupiedBed bed={bed} wardPatients={wardPatients} /> : <EmptyBed bed={bed} />;
13
13
  };
14
14
 
15
15
  export default WardBed;
@@ -1,67 +1,100 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { InlineNotification } from '@carbon/react';
3
- import { useLocations, useSession, type Location } from '@openmrs/esm-framework';
4
-
5
3
  import { useTranslation } from 'react-i18next';
6
- import { useParams } from 'react-router-dom';
4
+ import { WorkspaceContainer, useFeatureFlag } from '@openmrs/esm-framework';
5
+ import EmptyBedSkeleton from '../beds/empty-bed-skeleton';
7
6
  import { useAdmissionLocation } from '../hooks/useAdmissionLocation';
8
7
  import WardBed from './ward-bed.component';
9
- import { bedLayoutToBed } from './ward-view.resource';
8
+ import { bedLayoutToBed, filterBeds } from './ward-view.resource';
10
9
  import styles from './ward-view.scss';
11
- import EmptyBedSkeleton from '../empty-beds/empty-bed-skeleton';
10
+ import WardViewHeader from '../ward-view-header/ward-view-header.component';
11
+ import { type AdmittedPatient, type WardPatient } from '../types';
12
+ import { useAdmittedPatients } from '../hooks/useAdmittedPatients';
13
+ import useWardLocation from '../hooks/useWardLocation';
12
14
 
13
15
  const WardView = () => {
14
- const { locationUuid: locationUuidFromUrl } = useParams();
15
- const { sessionLocation } = useSession();
16
- const allLocations = useLocations();
16
+ const response = useWardLocation();
17
+ const { isLoadingLocation, errorFetchingLocation, invalidLocation } = response;
18
+
17
19
  const { t } = useTranslation();
18
- const locationFromUrl = allLocations.find((l) => l.uuid === locationUuidFromUrl);
20
+ const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
21
+
22
+ //TODO:Display patients with admitted status (based on their observations) that have no beds assigned
23
+ if (!isBedManagementModuleInstalled || isLoadingLocation) {
24
+ return <></>;
25
+ }
19
26
 
20
- const invalidLocation = locationUuidFromUrl && !locationFromUrl;
21
- const location = (locationFromUrl ?? sessionLocation) as any as Location;
27
+ if (invalidLocation) {
28
+ return <InlineNotification kind="error" title={t('invalidLocationSpecified', 'Invalid location specified')} />;
29
+ }
22
30
 
23
31
  return (
24
32
  <div className={styles.wardView}>
25
- <div className={styles.wardViewHeader}>
26
- <h4>{location?.display}</h4>
27
- </div>
33
+ <WardViewHeader />
28
34
  <div className={styles.wardViewMain}>
29
- {invalidLocation ? (
30
- <InlineNotification
31
- kind="error"
32
- lowContrast={true}
33
- title={t('invalidLocationSpecified', 'Invalid location specified')}
34
- subtitle={t('unknownLocationUuid', 'Unknown location uuid: {{locationUuidFromUrl}}', {
35
- locationUuidFromUrl,
36
- })}
37
- />
38
- ) : (
39
- <WardViewByLocation location={location} />
40
- )}
35
+ <WardViewByLocation />
41
36
  </div>
37
+ <WorkspaceContainer overlay contextKey="ward" />
42
38
  </div>
43
39
  );
44
40
  };
45
41
 
46
- const WardViewByLocation = ({ location }: { location: Location }) => {
47
- const { admissionLocation, isLoading, error } = useAdmissionLocation(location.uuid);
42
+ const WardViewByLocation = () => {
43
+ const { location } = useWardLocation();
44
+ const {
45
+ admissionLocation,
46
+ isLoading: isLoadingLocation,
47
+ error: errorLoadingLocation,
48
+ } = useAdmissionLocation(location?.uuid);
49
+ const {
50
+ admittedPatients,
51
+ isLoading: isLoadingPatients,
52
+ error: errorLoadingPatients,
53
+ } = useAdmittedPatients(location.uuid);
48
54
  const { t } = useTranslation();
55
+ const admittedPatientsByUuid = useMemo(() => {
56
+ const map = new Map<string, AdmittedPatient>();
57
+ for (const admittedPatient of admittedPatients ?? []) {
58
+ map.set(admittedPatient.patient.uuid, admittedPatient);
59
+ }
60
+ return map;
61
+ }, [admittedPatients]);
62
+
63
+ if (admissionLocation != null && admittedPatients != null) {
64
+ const bedLayouts = filterBeds(admissionLocation);
49
65
 
50
- if (admissionLocation) {
51
- // admissionLocation.bedLayouts can contain row+column positions with no bed,
52
- // filter out layout positions with no real bed
53
- let collator = new Intl.Collator([], { numeric: true });
54
- const bedLayouts = admissionLocation.bedLayouts
55
- .filter((bl) => bl.bedId)
56
- .sort((bedA, bedB) => collator.compare(bedA.bedNumber, bedB.bedNumber));
66
+ const wardBeds = bedLayouts.map((bedLayout, i) => {
67
+ const { patients } = bedLayout;
68
+ const bed = bedLayoutToBed(bedLayout);
69
+ const wardPatients: WardPatient[] = patients.map((patient) => {
70
+ const admittedPatient = admittedPatientsByUuid.get(patient.uuid);
71
+
72
+ if (admittedPatient) {
73
+ // ideally, we can just use the patient object within admittedPatient
74
+ // and not need the one from bedLayouts, however, the emr api
75
+ // does not respect custom representation right now and does not return
76
+ // all required fields for the patient object
77
+ // TODO: change after this is done. https://openmrs.atlassian.net/browse/EA-192
78
+ return { ...admittedPatient, patient, admitted: true };
79
+ }
80
+
81
+ // patient assigned a bed but *not* admitted
82
+ // TODO: get the patient's visit and current location
83
+ return {
84
+ patient,
85
+ visit: null,
86
+ admitted: true,
87
+ currentLocation: null,
88
+ timeSinceAdmissionInMinutes: null,
89
+ timeAtInpatientLocationInMinutes: null,
90
+ };
91
+ });
92
+ return <WardBed key={bed.uuid} bed={bed} wardPatients={wardPatients} />;
93
+ });
57
94
 
58
95
  return (
59
96
  <>
60
- {bedLayouts.map((bedLayout, i) => {
61
- const { patient } = bedLayout;
62
- const bed = bedLayoutToBed(bedLayout);
63
- return <WardBed key={bed.uuid} bed={bed} patients={patient ? [patient] : null} />;
64
- })}
97
+ {wardBeds}
65
98
  {bedLayouts.length == 0 && (
66
99
  <InlineNotification
67
100
  kind="warning"
@@ -71,7 +104,7 @@ const WardViewByLocation = ({ location }: { location: Location }) => {
71
104
  )}
72
105
  </>
73
106
  );
74
- } else if (isLoading) {
107
+ } else if (isLoadingLocation || isLoadingPatients) {
75
108
  return (
76
109
  <>
77
110
  {Array(20)
@@ -81,18 +114,27 @@ const WardViewByLocation = ({ location }: { location: Location }) => {
81
114
  ))}
82
115
  </>
83
116
  );
84
- } else {
117
+ } else if (errorLoadingLocation) {
85
118
  return (
86
119
  <InlineNotification
87
120
  kind="error"
88
121
  lowContrast={true}
89
122
  title={t('errorLoadingWardLocation', 'Error loading ward location')}
90
123
  subtitle={
91
- error?.message ??
124
+ errorLoadingLocation?.message ??
92
125
  t('invalidWardLocation', 'Invalid ward location: {{location}}', { location: location.display })
93
126
  }
94
127
  />
95
128
  );
129
+ } else {
130
+ return (
131
+ <InlineNotification
132
+ kind="error"
133
+ lowContrast={true}
134
+ title={t('errorLoadingPatients', 'Error loading admitted patients')}
135
+ subtitle={errorLoadingPatients?.message}
136
+ />
137
+ );
96
138
  }
97
139
  };
98
140
 
@@ -1,4 +1,4 @@
1
- import { type Bed, type BedLayout } from '../types';
1
+ import type { AdmissionLocation, Bed, BedLayout } from '../types';
2
2
 
3
3
  // the server side has 2 slightly incompatible types for Bed
4
4
  export function bedLayoutToBed(bedLayout: BedLayout): Bed {
@@ -12,3 +12,13 @@ export function bedLayoutToBed(bedLayout: BedLayout): Bed {
12
12
  status: bedLayout.status,
13
13
  };
14
14
  }
15
+
16
+ export function filterBeds(admissionLocation: AdmissionLocation): BedLayout[] {
17
+ // admissionLocation.bedLayouts can contain row+column positions with no bed,
18
+ // filter out layout positions with no real bed
19
+ let collator = new Intl.Collator([], { numeric: true });
20
+ const bedLayouts = admissionLocation.bedLayouts
21
+ .filter((bl) => bl.bedId)
22
+ .sort((bedA, bedB) => collator.compare(bedA.bedNumber, bedB.bedNumber));
23
+ return bedLayouts;
24
+ }
@@ -12,10 +12,6 @@
12
12
  padding: 0 spacing.$spacing-05;
13
13
  }
14
14
 
15
- .wardViewHeader {
16
- margin: spacing.$spacing-05 0;
17
- }
18
-
19
15
  .wardViewMain {
20
16
  background-color: #e4e4e4;
21
17
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -1,31 +1,44 @@
1
- import { getDefaultsFromConfigSchema, useConfig, useSession } from '@openmrs/esm-framework';
2
- import { screen } from '@testing-library/react';
3
1
  import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import {
4
+ type Person,
5
+ type ConfigSchema,
6
+ getDefaultsFromConfigSchema,
7
+ useConfig,
8
+ useFeatureFlag,
9
+ } from '@openmrs/esm-framework';
4
10
  import { useParams } from 'react-router-dom';
5
- import { mockLocations } from '../../../../__mocks__/locations.mock';
6
11
  import { mockAdmissionLocation } from '../../../../__mocks__/wards.mock';
7
12
  import { renderWithSwr } from '../../../../tools/test-utils';
8
13
  import { configSchema } from '../config-schema';
9
14
  import { useAdmissionLocation } from '../hooks/useAdmissionLocation';
15
+ import { mockPatientAlice } from '../../../../__mocks__/patient.mock';
16
+ import { useAdmittedPatients } from '../hooks/useAdmittedPatients';
17
+ import useWardLocation from '../hooks/useWardLocation';
10
18
  import WardView from './ward-view.component';
11
19
 
12
- jest.mocked(useConfig).mockReturnValue({
13
- ...getDefaultsFromConfigSchema(configSchema),
20
+ jest.replaceProperty(mockPatientAlice.person as Person, 'preferredName', {
21
+ uuid: '',
22
+ givenName: 'Alice',
23
+ familyName: 'Johnson',
14
24
  });
15
25
 
16
- const mockedSessionLocation = { uuid: 'abcd', display: 'mock location', links: [] };
17
- jest.mocked(useSession).mockReturnValue({
18
- sessionLocation: mockedSessionLocation,
19
- authenticated: true,
20
- sessionId: 'sessionId',
26
+ jest.mocked(useConfig).mockReturnValue({
27
+ ...getDefaultsFromConfigSchema<ConfigSchema>(configSchema),
21
28
  });
22
29
 
23
- jest.mock('@openmrs/esm-framework', () => {
24
- return {
25
- ...jest.requireActual('@openmrs/esm-framework'),
26
- useLocations: jest.fn().mockImplementation(() => mockLocations.data.results),
27
- };
28
- });
30
+ const mockedUseFeatureFlag = useFeatureFlag as jest.Mock;
31
+
32
+ jest.mock('../hooks/useWardLocation', () =>
33
+ jest.fn().mockReturnValue({
34
+ location: { uuid: 'abcd', display: 'mock location' },
35
+ isLoadingLocation: false,
36
+ errorFetchingLocation: null,
37
+ invalidLocation: false,
38
+ }),
39
+ );
40
+
41
+ const mockedUseWardLocation = useWardLocation as jest.Mock;
29
42
 
30
43
  jest.mock('react-router-dom', () => ({
31
44
  ...jest.requireActual('react-router-dom'),
@@ -36,6 +49,10 @@ const mockedUseParams = useParams as jest.Mock;
36
49
  jest.mock('../hooks/useAdmissionLocation', () => ({
37
50
  useAdmissionLocation: jest.fn(),
38
51
  }));
52
+ jest.mock('../hooks/useAdmittedPatients', () => ({
53
+ useAdmittedPatients: jest.fn(),
54
+ }));
55
+
39
56
  jest.mocked(useAdmissionLocation).mockReturnValue({
40
57
  error: undefined,
41
58
  mutate: jest.fn(),
@@ -43,19 +60,25 @@ jest.mocked(useAdmissionLocation).mockReturnValue({
43
60
  isLoading: false,
44
61
  admissionLocation: mockAdmissionLocation,
45
62
  });
63
+ jest.mocked(useAdmittedPatients).mockReturnValue({
64
+ error: undefined,
65
+ mutate: jest.fn(),
66
+ isValidating: false,
67
+ isLoading: false,
68
+ admittedPatients: [],
69
+ });
46
70
 
47
71
  describe('WardView:', () => {
48
72
  it('renders the session location when no location provided in URL', () => {
49
73
  renderWithSwr(<WardView />);
50
- const header = screen.getByRole('heading', { name: mockedSessionLocation.display });
74
+ const header = screen.getByRole('heading', { name: 'mock location' });
51
75
  expect(header).toBeInTheDocument();
52
76
  });
53
77
 
54
78
  it('renders the location provided in URL', () => {
55
- const locationToUse = mockLocations.data.results[0];
56
- mockedUseParams.mockReturnValueOnce({ locationUuid: locationToUse.uuid });
79
+ mockedUseParams.mockReturnValueOnce({ locationUuid: 'abcd' });
57
80
  renderWithSwr(<WardView />);
58
- const header = screen.getByRole('heading', { name: locationToUse.display });
81
+ const header = screen.getByRole('heading', { name: 'mock location' });
59
82
  expect(header).toBeInTheDocument();
60
83
  });
61
84
 
@@ -64,4 +87,25 @@ describe('WardView:', () => {
64
87
  const emptyBedCards = await screen.findAllByText(/empty bed/i);
65
88
  expect(emptyBedCards).toHaveLength(3);
66
89
  });
90
+
91
+ it('renders notification for invalid location uuid', () => {
92
+ mockedUseWardLocation.mockReturnValueOnce({
93
+ location: null,
94
+ isLoadingLocation: false,
95
+ errorFetchingLocation: null,
96
+ invalidLocation: true,
97
+ });
98
+
99
+ renderWithSwr(<WardView />);
100
+ const notification = screen.getByRole('status');
101
+ expect(notification).toBeInTheDocument();
102
+ const invalidText = screen.getByText('Invalid location specified');
103
+ expect(invalidText).toBeInTheDocument();
104
+ });
105
+
106
+ it('screen should be empty if backend module is not installed', () => {
107
+ mockedUseFeatureFlag.mockReturnValueOnce(false);
108
+ const { container } = renderWithSwr(<WardView />);
109
+ expect(container).toBeEmptyDOMElement();
110
+ });
67
111
  });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { Movement } from '@carbon/react/icons';
3
+ import { Button, InlineNotification } from '@carbon/react';
4
+ import { ArrowRightIcon, isDesktop, launchWorkspace, useLayoutType } from '@openmrs/esm-framework';
5
+ import { useInpatientRequest } from '../hooks/useInpatientRequest';
6
+ import { useTranslation } from 'react-i18next';
7
+ import useWardLocation from '../hooks/useWardLocation';
8
+ import styles from './admission-requests.scss';
9
+
10
+ const AdmissionRequestsBar = () => {
11
+ const { location } = useWardLocation();
12
+ const { inpatientRequests, isLoading, error } = useInpatientRequest(location?.uuid);
13
+ const admissionRequests = inpatientRequests?.filter((request) => request.type == 'ADMISSION');
14
+ const { t } = useTranslation();
15
+ const layout = useLayoutType();
16
+
17
+ if (isLoading || !admissionRequests?.length) {
18
+ return null;
19
+ }
20
+
21
+ if (error) {
22
+ console.error(error);
23
+ return (
24
+ <InlineNotification
25
+ kind="error"
26
+ title={t('errorLoadingPatientAdmissionRequests', 'Error Loading patient admission requests')}
27
+ />
28
+ );
29
+ }
30
+
31
+ return (
32
+ <div className={styles.admissionRequestsContainer}>
33
+ <Movement className={styles.movementIcon} size="24" />
34
+ <span className={styles.content}>
35
+ {t('admissionRequestsCount', '{{count}} admission requests', {
36
+ count: admissionRequests.length,
37
+ })}
38
+ </span>
39
+ <Button
40
+ onClick={() => launchWorkspace('admission-requests-workspace', { admissionRequests })}
41
+ renderIcon={ArrowRightIcon}
42
+ kind="ghost"
43
+ size={isDesktop(layout) ? 'sm' : 'lg'}>
44
+ {t('manage', 'Manage')}
45
+ </Button>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default AdmissionRequestsBar;
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { renderWithSwr } from '../../../../tools/test-utils';
4
+ import { screen } from '@testing-library/react';
5
+ import { launchWorkspace } from '@openmrs/esm-framework';
6
+ import AdmissionRequestsBar from './admission-requests-bar.component';
7
+ import { mockInpatientRequest } from '../../../../__mocks__/ward-patient';
8
+ import { useInpatientRequest } from '../hooks/useInpatientRequest';
9
+
10
+ jest.mock('@openmrs/esm-framework', () => {
11
+ return {
12
+ ...jest.requireActual('@openmrs/esm-framework'),
13
+ launchWorkspace: jest.fn(),
14
+ };
15
+ });
16
+
17
+ jest.mock('../hooks/useInpatientRequest', () => ({
18
+ useInpatientRequest: jest.fn(),
19
+ }));
20
+ const mockInpatientRequestResponse = {
21
+ error: undefined,
22
+ mutate: jest.fn(),
23
+ isValidating: false,
24
+ isLoading: false,
25
+ inpatientRequests: [mockInpatientRequest],
26
+ };
27
+ jest.mocked(useInpatientRequest).mockReturnValue(mockInpatientRequestResponse);
28
+
29
+ describe('Admission Requests Button', () => {
30
+ it('call launch workspace when clicked on manage button', async () => {
31
+ const user = userEvent.setup();
32
+ renderWithSwr(<AdmissionRequestsBar />);
33
+
34
+ await user.click(screen.getByRole('button', { name: /manage/i }));
35
+ expect(launchWorkspace).toHaveBeenCalled();
36
+ });
37
+
38
+ it('there should be one admission request', () => {
39
+ renderWithSwr(<AdmissionRequestsBar />);
40
+
41
+ expect(screen.getByText('1 admission requests')).toBeInTheDocument();
42
+ });
43
+ });
@@ -0,0 +1,33 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .admissionRequestsContainer {
6
+ background-color: $color-gray-70;
7
+ display: flex;
8
+ align-items: center;
9
+ padding: spacing.$spacing-02 0 spacing.$spacing-02 spacing.$spacing-04;
10
+ background-color: #393939;
11
+
12
+ & > button {
13
+ color: #78a9ff;
14
+
15
+ svg {
16
+ fill: #78a9ff !important;
17
+ }
18
+ }
19
+ }
20
+
21
+ .movementIcon {
22
+ padding: spacing.$spacing-02;
23
+ border-radius: 50%;
24
+ fill: $ui-03;
25
+ background-color: $color-blue-60-2;
26
+ margin-right: spacing.$spacing-03;
27
+ }
28
+
29
+ .content {
30
+ @include type.type-style('heading-compact-01');
31
+ color: $ui-02;
32
+ margin-right: spacing.$spacing-03;
33
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import styles from './ward-view-header.scss';
3
+ import AdmissionRequestsBar from './admission-requests-bar.component';
4
+ import useWardLocation from '../hooks/useWardLocation';
5
+
6
+ interface WardViewHeaderProps {}
7
+
8
+ const WardViewHeader: React.FC<WardViewHeaderProps> = () => {
9
+ const { location } = useWardLocation();
10
+ return (
11
+ <div className={styles.wardViewHeader}>
12
+ <h4>{location?.display}</h4>
13
+ <AdmissionRequestsBar />
14
+ </div>
15
+ );
16
+ };
17
+
18
+ export default WardViewHeader;
@@ -0,0 +1,8 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+
3
+ .wardViewHeader {
4
+ margin: spacing.$spacing-05 0;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ }