Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fix) O3-3211: Fix calculations for Patients and Capacity in Ward Metrics #1307

Merged
merged 23 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function useWardPatientGrouping() {
wardPatientPendingCount,
bedLayouts,
wardUnassignedPatientsList,
totalPatientsCount,
} = useMemo(() => {
return createAndGetWardPatientGrouping(inpatientAdmissions, admissionLocation, inpatientAdmissionsByPatientUuid);
}, [inpatientAdmissionsByPatientUuid, admissionLocation, inpatientAdmissions]);
Expand All @@ -31,5 +32,6 @@ export function useWardPatientGrouping() {
admissionLocationResponse,
inpatientAdmissionResponse,
bedLayouts,
totalPatientsCount,
};
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import React from 'react';
import styles from './ward-metric.scss';
import { SkeletonPlaceholder } from '@carbon/react';
import {useTranslation } from 'react-i18next';
import classNames from 'classnames';

interface WardMetricProps {
metricName: string;
metricValue: string;
isLoading: boolean;
}
const WardMetric: React.FC<WardMetricProps> = ({ metricName, metricValue, isLoading }) => {
const { i18n } = useTranslation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, this is done as a interpolated translation string, like this:

return t('encounterDisplay', '{{encounterType}} {{encounterDate}}', {
encounterType: obs.encounter.encounterType.display,
encounterDate: new Date(obs.encounter.encounterDatetime).toLocaleDateString(),
interpolation: { escapeValue: false },

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @chibongho , removed i18n here , seems the direction automatically handeled in the document. Thanks

const isLtr=i18n.dir()=="ltr";
const field1=isLtr?metricName:metricValue;
const field2=isLtr?metricValue:metricName;
return (
<div className={styles.metric}>
<span className={styles.metricName}>{metricName}</span>
<span className={
classNames({[styles.metricName]:isLtr,[styles.metricValue]:!isLtr})}>{field1}</span>
{isLoading ? (
<SkeletonPlaceholder className={styles.skeleton} />
) : (
<span className={styles.metricValue}>{metricValue}</span>
<span className={classNames({[styles.metricValue]:isLtr,[styles.metricName]:!isLtr})}>{field2}</span>
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ 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' },
{ name: 'patients', key: 'patients',defaultTranslation:"Patients" },
{ name: 'freeBeds', key: 'freeBeds',defaultTranslation:"Free Beds"},
{ name: 'capacity', key: 'capacity',defaultTranslation:"Capacity" },
];

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

Expand All @@ -28,26 +28,26 @@ const WardMetrics = () => {
description: error.message,
});
}
const wardMetricValues = getWardMetrics(beds);
const wardMetricValues = getWardMetrics(beds, wardPatientGroup);
return (
<div className={styles.metricsContainer}>
{isBedManagementModuleInstalled ? (
wardMetrics.map((wardMetric) => {
return (
<WardMetric
metricName={wardMetric.name}
metricValue={wardMetricValues[wardMetric.key]}
metricName={t(`metrics.names.${wardMetric.name}`,wardMetric.defaultTranslation)}
metricValue={t(`metrics.values.${wardMetric.name}`,wardMetricValues[wardMetric.key],{metricValue:wardMetricValues[wardMetric.key]})}
isLoading={!!isLoading}
key={wardMetric.key}
/>
);
})
) : (
<WardMetric metricName={'Patients'} metricValue={'--'} isLoading={false} key={'patients'} />
<WardMetric metricName={t(`metrics.names.patients`,"Patients")} metricValue={'--'} isLoading={false} key={'patients'} />
)}
{isBedManagementModuleInstalled && (
<WardMetric
metricName="Pending out"
metricName={t(`metrics.names.pendingOut`,"Pending Out")}
metricValue={error ? '--' : wardPatientGroup?.wardPatientPendingCount.toString() ?? '--'}
isLoading={!wardPatientGroup}
key="pending"
Expand Down
42 changes: 37 additions & 5 deletions packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,35 @@ 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 {
createAndGetWardPatientGrouping,
getInpatientAdmissionsUuidMap,
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';
import { useAppContext } from '@openmrs/esm-framework';
import { useTranslation } from 'react-i18next';
const wardMetrics = [
{ name: 'patients', key: 'patients',defaultTranslation:"Patients" },
{ name: 'freeBeds', key: 'freeBeds',defaultTranslation:"Free Beds"},
{ name: 'capacity', key: 'capacity',defaultTranslation:"Capacity" },
];

jest.mock('react-i18next', () => ({
useTranslation: () => {
return {
t: (key:string,defaultStr:string) => defaultStr,
i18n: {
dir:()=>"ltr",
changeLanguage: () => new Promise(() => {}),
},
};
},
}));

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Expand Down Expand Up @@ -45,21 +68,28 @@ jest.mocked(useBeds).mockReturnValue({
beds: mockWardBeds,
});

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

const inpatientAdmissionsUuidMap = getInpatientAdmissionsUuidMap(mockInpatientAdmissions);
const mockWardPatientGroupDetails = {
admissionLocationResponse: mockAdmissionLocationResponse(),
inpatientAdmissionResponse: mockInpatientAdmissionResponse(),
...createAndGetWardPatientGrouping(mockInpatientAdmissions, mockAdmissionLocation, inpatientAdmissionsUuidMap),
};
jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails);
describe('Ward Metrics', () => {
it('Should display metrics of in the ward ', () => {
mockUseWardLocation.mockReturnValueOnce({
Expand All @@ -68,10 +98,12 @@ describe('Ward Metrics', () => {
errorFetchingLocation: null,
invalidLocation: true,
});
const bedMetrics = getWardMetrics(mockWardBeds);
const bedMetrics = getWardMetrics(mockWardBeds, mockWardPatientGroupDetails);
renderWithSwr(<WardMetrics />);
for (let [key, value] of Object.entries(bedMetrics)) {
expect(screen.getByText(value)).toBeInTheDocument();
const fieldName=wardMetrics.find((metric)=>metric.name==key)?.defaultTranslation;
expect(screen.getByText(fieldName)).toBeInTheDocument();
}

});
});
26 changes: 21 additions & 5 deletions packages/esm-ward-app/src/ward-view/ward-view.resource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { type Patient } from '@openmrs/esm-framework';
import type { AdmissionLocationFetchResponse, Bed, BedLayout, InpatientAdmission, WardMetrics } from '../types';
import type {
AdmissionLocationFetchResponse,
Bed,
BedLayout,
InpatientAdmission,
WardMetrics,
WardPatientGroupDetails,
} from '../types';

// the server side has 2 slightly incompatible types for Bed
export function bedLayoutToBed(bedLayout: BedLayout): Bed {
Expand All @@ -25,19 +32,23 @@ export function filterBeds(admissionLocation: AdmissionLocationFetchResponse): B
}

//TODO: This implementation will change when the api is ready
export function getWardMetrics(beds: Bed[]): WardMetrics {
export function getWardMetrics(beds: Bed[], wardPatientGroup: WardPatientGroupDetails): WardMetrics {
const bedMetrics = {
patients: '--',
freeBeds: '--',
capacity: '--',
};
if (beds.length == 0) return bedMetrics;
if (beds.length == 0 || !wardPatientGroup) return bedMetrics;
const total = beds.length;
const occupiedBeds = beds.filter((bed) => bed.status === 'OCCUPIED');
const patients = occupiedBeds.length;
const freeBeds = total - patients;
const capacity = total != 0 ? Math.trunc((patients / total) * 100) : 0;
return { patients: patients.toString(), freeBeds: freeBeds.toString(), capacity: capacity.toString() + '%' };
const capacity = total != 0 ? Math.trunc((wardPatientGroup.totalPatientsCount / total) * 100) : 0;
return {
patients: wardPatientGroup.totalPatientsCount.toString(),
freeBeds: freeBeds.toString(),
capacity: capacity.toString(),
};
}

export function getInpatientAdmissionsUuidMap(inpatientAdmissions: InpatientAdmission[]) {
Expand All @@ -59,10 +70,12 @@ export function createAndGetWardPatientGrouping(
const bedLayouts = admissionLocation && filterBeds(admissionLocation);

let wardPatientPendingCount = 0;
const totalPatientsUuidSet=new Set<string>();
bedLayouts?.map((bedLayout) => {
const { patients } = bedLayout;
patients.map((patient) => {
const patientAdmittedWithBed = inpatientAdmissionsByPatientUuid.get(patient.uuid);
totalPatientsUuidSet.add(patient.uuid);
if (patientAdmittedWithBed) {
wardAdmittedPatientsWithBed.set(patient.uuid, patientAdmittedWithBed);
//count the pending metric
Expand All @@ -73,8 +86,10 @@ export function createAndGetWardPatientGrouping(
}
});
});

const wardUnassignedPatientsList =
inpatientAdmissions?.filter((inpatientAdmission) => {
totalPatientsUuidSet.add(inpatientAdmission.patient.uuid)
return (
!wardAdmittedPatientsWithBed.has(inpatientAdmission.patient.uuid) &&
!wardUnadmittedPatientsWithBed.has(inpatientAdmission.patient.uuid)
Expand All @@ -86,5 +101,6 @@ export function createAndGetWardPatientGrouping(
wardPatientPendingCount,
bedLayouts,
wardUnassignedPatientsList,
totalPatientsCount: totalPatientsUuidSet.size,
};
}
13 changes: 12 additions & 1 deletion packages/esm-ward-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,16 @@
"unknown": "Unknown",
"visitNoteSaved": "Patient note saved",
"wardClinicalNotePlaceholder": "Write any notes here",
"wards": "Wards"
"wards": "Wards",
"metrics":{
"names":{
"patients":"Patients",
"freeBeds":"Free Beds",
"capacity":"Capacity",
"pendingOut":"Pending Out"
},
"values":{
"capacity":"{{ metricValue }} %"
}
}
}
4 changes: 3 additions & 1 deletion react-i18next.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ useMock.t = (key, defaultValue, options = {}) => {

return translatedString ?? key;
};
useMock.i18n = {};
useMock.i18n = {
dir:()=>"ltr"
};

module.exports = {
// this mock makes sure any components using the translate HoC receive the t function as a prop
Expand Down