Skip to content

Commit

Permalink
(feat) O3-3211: Ward App - display metrics for admission status and b…
Browse files Browse the repository at this point in the history
…ed occupancy (#1213)

* feat-metrics

* update metrics

* correct test

* refactor useAdmission

* fix e2e tests

* (refactor) Refactor registration form cancel modal to match conventions (#1294)

This PR refactors the registration form's cancel modal to match new modal naming and registration conventions. Modals
are now registered in the routes registry file under the `modals`. The naming convention has also changed - modals now use the `*.modal.tsx` suffix.
I've also amended the modal to use Carbon's ModalBody, ModalHeader, and ModalFooter components instead of using divs with
custom classes. Finally, I've amended the modal title and content to align with other confirmation modals in O3.

* (feat) 03-3404: follow-up -ensure the  dateAppointmentScheduled <= appointmentDate (#1295)

* correct yarn.lock

---------

Co-authored-by: Dennis Kigen <kigen.work@gmail.com>
Co-authored-by: Lucy Jemutai <130601439+lucyjemutai@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 30, 2024
1 parent 04fde53 commit b17c19e
Show file tree
Hide file tree
Showing 21 changed files with 519 additions and 197 deletions.
31 changes: 31 additions & 0 deletions __mocks__/wardBeds.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { mockBedType } from './wards.mock';

export const mockWardBeds = [
{
id: 1,
uuid: '0000-bed1',
bedNumber: 'bed1',
bedType: mockBedType,
row: 1,
column: 2,
status: 'OCCUPIED' as const,
},
{
id: 2,
uuid: '0000-bed2',
bedNumber: 'bed2',
bedType: mockBedType,
row: 1,
column: 2,
status: 'AVAILABLE' as const,
},
{
id: 1,
uuid: '0000-bed3',
bedNumber: 'bed3',
bedType: mockBedType,
row: 1,
column: 3,
status: 'AVAILABLE' as const,
},
];
2 changes: 1 addition & 1 deletion __mocks__/wards.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type AdmissionLocationFetchResponse, type BedType } from '../packages/e
import { mockLocationInpatientWard } from './locations.mock';
import { mockPatientAlice, mockPatientBrian } from './patient.mock';

const mockBedType: BedType = {
export const mockBedType: BedType = {
uuid: '0000-bed-type',
name: 'mockBedType',
displayName: 'Mock Bed Type',
Expand Down
35 changes: 35 additions & 0 deletions packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { createAndGetWardPatientGrouping, getInpatientAdmissionsUuidMap } from '../ward-view/ward-view.resource';
import { useAdmissionLocation } from './useAdmissionLocation';
import { useInpatientAdmission } from './useInpatientAdmission';

export function useWardPatientGrouping() {
const admissionLocationResponse = useAdmissionLocation();
const inpatientAdmissionResponse = useInpatientAdmission();

const { inpatientAdmissions } = inpatientAdmissionResponse;
const { admissionLocation } = admissionLocationResponse;
const inpatientAdmissionsByPatientUuid = useMemo(() => {
return getInpatientAdmissionsUuidMap(inpatientAdmissions);
}, [inpatientAdmissions]);

const {
wardAdmittedPatientsWithBed,
wardUnadmittedPatientsWithBed,
wardPatientPendingCount,
bedLayouts,
wardUnassignedPatientsList,
} = useMemo(() => {
return createAndGetWardPatientGrouping(inpatientAdmissions, admissionLocation, inpatientAdmissionsByPatientUuid);
}, [inpatientAdmissionsByPatientUuid, admissionLocation, inpatientAdmissions]);

return {
wardAdmittedPatientsWithBed,
wardUnadmittedPatientsWithBed,
wardUnassignedPatientsList,
wardPatientPendingCount,
admissionLocationResponse,
inpatientAdmissionResponse,
bedLayouts,
};
}
9 changes: 9 additions & 0 deletions packages/esm-ward-app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Visit,
} from '@openmrs/esm-framework';
import type React from 'react';
import type { useWardPatientGrouping } from '../hooks/useWardPatientGrouping';

export type WardPatientCard = React.FC<WardPatient>;

Expand Down Expand Up @@ -189,6 +190,12 @@ export interface EncounterRole extends OpenmrsResourceStrict {
retired?: boolean;
}

export interface WardMetrics {
patients: string;
freeBeds: string;
capacity: string;
}

export interface EncounterPayload {
encounterDatetime?: string;
encounterType: string;
Expand All @@ -206,3 +213,5 @@ export interface ObsPayload {
value?: string;
groupMembers?: Array<ObsPayload>;
}

export type WardPatientGroupDetails = ReturnType<typeof useWardPatientGrouping>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
align-items: center;
padding: layout.$spacing-02 0 layout.$spacing-02 layout.$spacing-04;
background-color: #393939;

margin-left: layout.$spacing-03;
& > button {
color: #78a9ff;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import styles from './ward-metric.scss';
import { SkeletonPlaceholder } from '@carbon/react';

interface WardMetricProps {
metricName: string;
metricValue: string;
isLoading: boolean;
}
const WardMetric: React.FC<WardMetricProps> = ({ metricName, metricValue, isLoading }) => {
return (
<div className={styles.metric}>
<span className={styles.metricName}>{metricName}</span>
{isLoading ? (
<SkeletonPlaceholder className={styles.skeleton} />
) : (
<span className={styles.metricValue}>{metricValue}</span>
)}
</div>
);
};

export default WardMetric;
25 changes: 25 additions & 0 deletions packages/esm-ward-app/src/ward-view-header/ward-metric.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/type';
@import '~@openmrs/esm-styleguide/src/vars';

.metric {
margin-left: spacing.$spacing-05;
display: flex;
align-items: end;
gap: 5px;
}

.metricName {
@include type.type-style('helper-text-01');
color: $color-gray-70;
}

.metricValue {
@include type.type-style('heading-03');
line-height: revert;
}

.skeleton {
height: 15px;
width: 15px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import styles from './ward-metrics.scss';
import { useBeds } from '../hooks/useBeds';
import { showNotification, useAppContext, useFeatureFlag } from '@openmrs/esm-framework';
import { useTranslation } from 'react-i18next';
import { getWardMetrics } from '../ward-view/ward-view.resource';
import WardMetric from './ward-metric.component';
import type { WardPatientGroupDetails } from '../types';
import useWardLocation from '../hooks/useWardLocation';

const wardMetrics = [
{ name: 'Patients', key: 'patients' },
{ name: 'Free beds', key: 'freeBeds' },
{ name: 'Capacity', key: 'capacity' },
];

const WardMetrics = () => {
const { location } = useWardLocation();
const { beds, isLoading, error } = useBeds({ locationUuid: location.uuid });
const { t } = useTranslation();
const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
const wardPatientGroup = useAppContext<WardPatientGroupDetails>('ward-patients-group');

if (error) {
showNotification({
kind: 'error',
title: t('errorLoadingBedDetails', 'Error Loading Bed Details'),
description: error.message,
});
}
const wardMetricValues = getWardMetrics(beds);
return (
<div className={styles.metricsContainer}>
{isBedManagementModuleInstalled ? (
wardMetrics.map((wardMetric) => {
return (
<WardMetric
metricName={wardMetric.name}
metricValue={wardMetricValues[wardMetric.key]}
isLoading={!!isLoading}
key={wardMetric.key}
/>
);
})
) : (
<WardMetric metricName={'Patients'} metricValue={'--'} isLoading={false} key={'patients'} />
)}
{isBedManagementModuleInstalled && (
<WardMetric
metricName="Pending out"
metricValue={error ? '--' : wardPatientGroup?.wardPatientPendingCount.toString() ?? '--'}
isLoading={!wardPatientGroup}
key="pending"
/>
)}
</div>
);
};

export default WardMetrics;
8 changes: 8 additions & 0 deletions packages/esm-ward-app/src/ward-view-header/ward-metrics.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@use '@carbon/styles/scss/spacing';
@import '~@openmrs/esm-styleguide/src/vars';

.metricsContainer {
display: flex;
align-items: end;
margin-left: auto;
}
77 changes: 77 additions & 0 deletions packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import WardMetrics from './ward-metrics.component';
import { renderWithSwr } from '../../../../tools/test-utils';
import { useBeds } from '../hooks/useBeds';
import { mockWardBeds } from '../../../../__mocks__/wardBeds.mock';
import { getWardMetrics } from '../ward-view/ward-view.resource';
import { useAdmissionLocation } from '../hooks/useAdmissionLocation';
import { mockAdmissionLocation, mockInpatientAdmissions } from '__mocks__';
import { useInpatientAdmission } from '../hooks/useInpatientAdmission';
import useWardLocation from '../hooks/useWardLocation';
import { screen } from '@testing-library/react';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockReturnValue({}),
}));

jest.mock('../hooks/useWardLocation', () =>
jest.fn().mockReturnValue({
location: { uuid: 'abcd', display: 'mock location' },
isLoadingLocation: false,
errorFetchingLocation: null,
invalidLocation: false,
}),
);

const mockUseWardLocation = jest.mocked(useWardLocation);

jest.mock('../hooks/useBeds', () => ({
useBeds: jest.fn(),
}));

jest.mock('../hooks/useAdmissionLocation', () => ({
useAdmissionLocation: jest.fn(),
}));
jest.mock('../hooks/useInpatientAdmission', () => ({
useInpatientAdmission: jest.fn(),
}));

jest.mocked(useBeds).mockReturnValue({
error: undefined,
mutate: jest.fn(),
isValidating: false,
isLoading: false,
beds: mockWardBeds,
});

jest.mocked(useAdmissionLocation).mockReturnValue({
error: undefined,
mutate: jest.fn(),
isValidating: false,
isLoading: false,
admissionLocation: mockAdmissionLocation,
});
jest.mocked(useInpatientAdmission).mockReturnValue({
error: undefined,
mutate: jest.fn(),
isValidating: false,
isLoading: false,
inpatientAdmissions: mockInpatientAdmissions,
});

describe('Ward Metrics', () => {
it('Should display metrics of in the ward ', () => {
mockUseWardLocation.mockReturnValueOnce({
location: null,
isLoadingLocation: false,
errorFetchingLocation: null,
invalidLocation: true,
});
const bedMetrics = getWardMetrics(mockWardBeds);
renderWithSwr(<WardMetrics />);
for (let [key, value] of Object.entries(bedMetrics)) {
expect(screen.getByText(value)).toBeInTheDocument();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import React from 'react';
import styles from './ward-view-header.scss';
import AdmissionRequestsBar from './admission-requests-bar.component';
import useWardLocation from '../hooks/useWardLocation';
import WardMetrics from './ward-metrics.component';

interface WardViewHeaderProps {}

const WardViewHeader: React.FC<WardViewHeaderProps> = () => {
const { location } = useWardLocation();

return (
<div className={styles.wardViewHeader}>
<h4>{location?.display}</h4>
<WardMetrics />
<AdmissionRequestsBar />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@
margin: layout.$spacing-05 0;
display: flex;
align-items: center;
justify-content: space-between;
}
Loading

0 comments on commit b17c19e

Please sign in to comment.