From b17c19e664906b41068a02bbd677cba9cbdb69bb Mon Sep 17 00:00:00 2001 From: Bhargav kodali <115476530+kb019@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:07:59 +0530 Subject: [PATCH] (feat) O3-3211: Ward App - display metrics for admission status and bed 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 Co-authored-by: Lucy Jemutai <130601439+lucyjemutai@users.noreply.github.com> --- __mocks__/wardBeds.mock.ts | 31 +++ __mocks__/wards.mock.ts | 2 +- .../src/hooks/useWardPatientGrouping.ts | 35 +++ packages/esm-ward-app/src/types/index.ts | 9 + .../ward-view-header/admission-requests.scss | 2 +- .../ward-metric.component.tsx | 23 ++ .../src/ward-view-header/ward-metric.scss | 25 ++ .../ward-metrics.component.tsx | 60 +++++ .../src/ward-view-header/ward-metrics.scss | 8 + .../ward-view-header/ward-metrics.test.tsx | 77 ++++++ .../ward-view-header.component.tsx | 3 + .../ward-view-header/ward-view-header.scss | 1 - .../src/ward-view/ward-view.component.tsx | 221 +++++++----------- .../src/ward-view/ward-view.resource.ts | 68 +++++- .../src/ward-view/ward-view.test.tsx | 29 ++- .../admit-patient-form.test.tsx | 79 ++++--- .../admit-patient-form.workspace.tsx | 11 +- .../patient-discharge.workspace.tsx | 9 +- .../patient-bed-swap-form.component.tsx | 12 +- ...atient-transfer-request-form.component.tsx | 9 +- packages/esm-ward-app/translations/en.json | 2 + 21 files changed, 519 insertions(+), 197 deletions(-) create mode 100644 __mocks__/wardBeds.mock.ts create mode 100644 packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts create mode 100644 packages/esm-ward-app/src/ward-view-header/ward-metric.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view-header/ward-metric.scss create mode 100644 packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view-header/ward-metrics.scss create mode 100644 packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx diff --git a/__mocks__/wardBeds.mock.ts b/__mocks__/wardBeds.mock.ts new file mode 100644 index 000000000..4d758effe --- /dev/null +++ b/__mocks__/wardBeds.mock.ts @@ -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, + }, +]; diff --git a/__mocks__/wards.mock.ts b/__mocks__/wards.mock.ts index f64f0ee6d..f33ec41bc 100644 --- a/__mocks__/wards.mock.ts +++ b/__mocks__/wards.mock.ts @@ -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', diff --git a/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts b/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts new file mode 100644 index 000000000..eb818a3cd --- /dev/null +++ b/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts @@ -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, + }; +} diff --git a/packages/esm-ward-app/src/types/index.ts b/packages/esm-ward-app/src/types/index.ts index 99ca7b1ea..c302fe98e 100644 --- a/packages/esm-ward-app/src/types/index.ts +++ b/packages/esm-ward-app/src/types/index.ts @@ -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; @@ -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; @@ -206,3 +213,5 @@ export interface ObsPayload { value?: string; groupMembers?: Array; } + +export type WardPatientGroupDetails = ReturnType; diff --git a/packages/esm-ward-app/src/ward-view-header/admission-requests.scss b/packages/esm-ward-app/src/ward-view-header/admission-requests.scss index 15a71e421..f8fb13c37 100644 --- a/packages/esm-ward-app/src/ward-view-header/admission-requests.scss +++ b/packages/esm-ward-app/src/ward-view-header/admission-requests.scss @@ -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; diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metric.component.tsx b/packages/esm-ward-app/src/ward-view-header/ward-metric.component.tsx new file mode 100644 index 000000000..32568cb7d --- /dev/null +++ b/packages/esm-ward-app/src/ward-view-header/ward-metric.component.tsx @@ -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 = ({ metricName, metricValue, isLoading }) => { + return ( +
+ {metricName} + {isLoading ? ( + + ) : ( + {metricValue} + )} +
+ ); +}; + +export default WardMetric; diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metric.scss b/packages/esm-ward-app/src/ward-view-header/ward-metric.scss new file mode 100644 index 000000000..f50f0cf8a --- /dev/null +++ b/packages/esm-ward-app/src/ward-view-header/ward-metric.scss @@ -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; +} diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx b/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx new file mode 100644 index 000000000..d0ace903c --- /dev/null +++ b/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx @@ -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('ward-patients-group'); + + if (error) { + showNotification({ + kind: 'error', + title: t('errorLoadingBedDetails', 'Error Loading Bed Details'), + description: error.message, + }); + } + const wardMetricValues = getWardMetrics(beds); + return ( +
+ {isBedManagementModuleInstalled ? ( + wardMetrics.map((wardMetric) => { + return ( + + ); + }) + ) : ( + + )} + {isBedManagementModuleInstalled && ( + + )} +
+ ); +}; + +export default WardMetrics; diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metrics.scss b/packages/esm-ward-app/src/ward-view-header/ward-metrics.scss new file mode 100644 index 000000000..1888baa6e --- /dev/null +++ b/packages/esm-ward-app/src/ward-view-header/ward-metrics.scss @@ -0,0 +1,8 @@ +@use '@carbon/styles/scss/spacing'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.metricsContainer { + display: flex; + align-items: end; + margin-left: auto; +} diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx b/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx new file mode 100644 index 000000000..ef6ff8f86 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx @@ -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(); + for (let [key, value] of Object.entries(bedMetrics)) { + expect(screen.getByText(value)).toBeInTheDocument(); + } + }); +}); diff --git a/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx b/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx index 7932cfa92..74bd3f8a0 100644 --- a/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx +++ b/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx @@ -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 = () => { const { location } = useWardLocation(); + return (

{location?.display}

+
); diff --git a/packages/esm-ward-app/src/ward-view-header/ward-view-header.scss b/packages/esm-ward-app/src/ward-view-header/ward-view-header.scss index f8424f404..ca34e73cb 100644 --- a/packages/esm-ward-app/src/ward-view-header/ward-view-header.scss +++ b/packages/esm-ward-app/src/ward-view-header/ward-view-header.scss @@ -4,5 +4,4 @@ margin: layout.$spacing-05 0; display: flex; align-items: center; - justify-content: space-between; } diff --git a/packages/esm-ward-app/src/ward-view/ward-view.component.tsx b/packages/esm-ward-app/src/ward-view/ward-view.component.tsx index 4ca1906a5..808134b77 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.component.tsx +++ b/packages/esm-ward-app/src/ward-view/ward-view.component.tsx @@ -1,23 +1,24 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { InlineNotification } from '@carbon/react'; +import { useAppContext, useDefineAppContext, WorkspaceContainer } from '@openmrs/esm-framework'; import { useTranslation } from 'react-i18next'; -import { useFeatureFlag, WorkspaceContainer } from '@openmrs/esm-framework'; import EmptyBedSkeleton from '../beds/empty-bed-skeleton'; -import { useAdmissionLocation } from '../hooks/useAdmissionLocation'; +import UnassignedPatient from '../beds/unassigned-patient.component'; +import useWardLocation from '../hooks/useWardLocation'; +import { type WardPatientGroupDetails, type WardPatient } from '../types'; +import WardViewHeader from '../ward-view-header/ward-view-header.component'; import WardBed from './ward-bed.component'; -import { bedLayoutToBed, filterBeds } from './ward-view.resource'; +import { bedLayoutToBed } from './ward-view.resource'; import styles from './ward-view.scss'; -import WardViewHeader from '../ward-view-header/ward-view-header.component'; -import { type InpatientAdmission, type WardPatient } from '../types'; -import { useInpatientAdmission } from '../hooks/useInpatientAdmission'; -import useWardLocation from '../hooks/useWardLocation'; -import UnassignedPatient from '../beds/unassigned-patient.component'; +import { useWardPatientGrouping } from '../hooks/useWardPatientGrouping'; const WardView = () => { const response = useWardLocation(); const { isLoadingLocation, invalidLocation } = response; const { t } = useTranslation(); - const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); + + const wardPatientsGroupDetails = useWardPatientGrouping(); + useDefineAppContext('ward-patients-group', wardPatientsGroupDetails); if (isLoadingLocation) { return <>; @@ -30,143 +31,99 @@ const WardView = () => { return (
-
- {isBedManagementModuleInstalled ? : } -
+
); }; -// display to use if bed management is installed -const WardViewWithBedManagement = () => { +const WardViewMain = () => { const { location } = useWardLocation(); - const { admissionLocation, isLoading: isLoadingLocation, error: errorLoadingLocation } = useAdmissionLocation(); - const { inpatientAdmissions, isLoading: isLoadingPatients, error: errorLoadingPatients } = useInpatientAdmission(); + + const wardPatientsGrouping = useAppContext('ward-patients-group'); + const { + bedLayouts, + wardAdmittedPatientsWithBed = new Map(), + wardUnassignedPatientsList = [], + } = wardPatientsGrouping ?? {}; + const { isLoading: isLoadingAdmissionLocation, error: errorLoadingAdmissionLocation } = + wardPatientsGrouping?.admissionLocationResponse ?? {}; + const { isLoading: isLoadingInpatientAdmissions, error: errorLoadingInpatientAdmissions } = + wardPatientsGrouping?.inpatientAdmissionResponse ?? {}; + const { t } = useTranslation(); - const inpatientAdmissionsByPatientUuid = useMemo(() => { - const map = new Map(); - for (const inpatientAdmission of inpatientAdmissions ?? []) { - map.set(inpatientAdmission.patient.uuid, inpatientAdmission); - } - return map; - }, [inpatientAdmissions]); - if (admissionLocation != null || inpatientAdmissions != null) { - const bedLayouts = admissionLocation && filterBeds(admissionLocation); - // iterate over all beds - const wardBeds = bedLayouts?.map((bedLayout) => { - const { patients } = bedLayout; - const bed = bedLayoutToBed(bedLayout); - const wardPatients: WardPatient[] = patients.map((patient): WardPatient => { - const inpatientAdmission = inpatientAdmissionsByPatientUuid.get(patient.uuid); - if (inpatientAdmission) { - const { patient, visit, currentInpatientRequest } = inpatientAdmission; - return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null }; - } else { - // for some reason this patient is in a bed but not in the list of admitted patients, so we need to use the patient data from the bed endpoint - return { - patient: patient, - visit: null, - bed, - inpatientAdmission: null, // populate after BED-13 - inpatientRequest: null, - }; - } - }); - return ; + if (!wardPatientsGrouping) return <>; + const wardBeds = bedLayouts?.map((bedLayout) => { + const { patients } = bedLayout; + const bed = bedLayoutToBed(bedLayout); + const wardPatients: WardPatient[] = patients.map((patient): WardPatient => { + const inpatientAdmission = wardAdmittedPatientsWithBed.get(patient.uuid); + if (inpatientAdmission) { + const { patient, visit, currentInpatientRequest } = inpatientAdmission; + return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null }; + } else { + // for some reason this patient is in a bed but not in the list of admitted patients, so we need to use the patient data from the bed endpoint + return { + patient: patient, + visit: null, + bed, + inpatientAdmission: null, // populate after BED-13 + inpatientRequest: null, + }; + } }); + return ; + }); - const patientsInBedsUuids = bedLayouts?.flatMap((bedLayout) => bedLayout.patients.map((patient) => patient.uuid)); - const wardUnassignedPatients = - inpatientAdmissions && - inpatientAdmissions - .filter( - (inpatientAdmission) => - !patientsInBedsUuids || !patientsInBedsUuids.includes(inpatientAdmission.patient.uuid), - ) - .map((inpatientAdmission) => { - return ( - - ); - }); - - return ( - <> - {wardBeds} - {bedLayouts?.length == 0 && ( - - )} - {wardUnassignedPatients} - - ); - } else if (isLoadingLocation || isLoadingPatients) { - return ; - } else if (errorLoadingLocation) { - return ( - - ); - } else { + const wardUnassignedPatients = wardUnassignedPatientsList.map((inpatientAdmission) => { return ( - ); - } -}; - -// display to use if not using bed management -const WardViewWithoutBedManagement = () => { - const { inpatientAdmissions, isLoading: isLoadingPatients, error: errorLoadingPatients } = useInpatientAdmission(); - const { t } = useTranslation(); + }); - if (inpatientAdmissions) { - const wardPatients = inpatientAdmissions?.map((inpatientAdmission) => { - const { patient, visit } = inpatientAdmission; - return ( - + {wardBeds} + {bedLayouts?.length == 0 && ( + - ); - }); - return <>{wardPatients}; - } else if (isLoadingPatients) { - return ; - } else { - return ( - - ); - } + )} + {wardUnassignedPatients} + {(isLoadingAdmissionLocation || isLoadingInpatientAdmissions) && } + {errorLoadingAdmissionLocation && ( + + )} + {errorLoadingInpatientAdmissions && ( + + )} + + ); }; const EmptyBeds = () => { diff --git a/packages/esm-ward-app/src/ward-view/ward-view.resource.ts b/packages/esm-ward-app/src/ward-view/ward-view.resource.ts index a8f7facf1..1740135a9 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.resource.ts +++ b/packages/esm-ward-app/src/ward-view/ward-view.resource.ts @@ -1,4 +1,5 @@ -import type { AdmissionLocationFetchResponse, Bed, BedLayout } from '../types'; +import { type Patient } from '@openmrs/esm-framework'; +import type { AdmissionLocationFetchResponse, Bed, BedLayout, InpatientAdmission, WardMetrics } from '../types'; // the server side has 2 slightly incompatible types for Bed export function bedLayoutToBed(bedLayout: BedLayout): Bed { @@ -22,3 +23,68 @@ export function filterBeds(admissionLocation: AdmissionLocationFetchResponse): B .sort((bedA, bedB) => collator.compare(bedA.bedNumber, bedB.bedNumber)); return bedLayouts; } + +//TODO: This implementation will change when the api is ready +export function getWardMetrics(beds: Bed[]): WardMetrics { + const bedMetrics = { + patients: '--', + freeBeds: '--', + capacity: '--', + }; + if (beds.length == 0) 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() + '%' }; +} + +export function getInpatientAdmissionsUuidMap(inpatientAdmissions: InpatientAdmission[]) { + const map = new Map(); + for (const inpatientAdmission of inpatientAdmissions ?? []) { + map.set(inpatientAdmission.patient.uuid, inpatientAdmission); + } + return map; +} + +//catogorize and group patients with bed,without bed and unadmitted patients with bed +export function createAndGetWardPatientGrouping( + inpatientAdmissions: InpatientAdmission[], + admissionLocation: AdmissionLocationFetchResponse, + inpatientAdmissionsByPatientUuid: Map, +) { + const wardAdmittedPatientsWithBed = new Map(); + const wardUnadmittedPatientsWithBed = new Map(); + const bedLayouts = admissionLocation && filterBeds(admissionLocation); + + let wardPatientPendingCount = 0; + bedLayouts?.map((bedLayout) => { + const { patients } = bedLayout; + patients.map((patient) => { + const patientAdmittedWithBed = inpatientAdmissionsByPatientUuid.get(patient.uuid); + if (patientAdmittedWithBed) { + wardAdmittedPatientsWithBed.set(patient.uuid, patientAdmittedWithBed); + //count the pending metric + const dispositionType = patientAdmittedWithBed.currentInpatientRequest?.dispositionType; + if (dispositionType == 'TRANSFER' || dispositionType == 'DISCHARGE') wardPatientPendingCount++; + } else { + wardUnadmittedPatientsWithBed.set(patient.uuid, patient); + } + }); + }); + const wardUnassignedPatientsList = + inpatientAdmissions?.filter((inpatientAdmission) => { + return ( + !wardAdmittedPatientsWithBed.has(inpatientAdmission.patient.uuid) && + !wardUnadmittedPatientsWithBed.has(inpatientAdmission.patient.uuid) + ); + }) ?? []; + return { + wardAdmittedPatientsWithBed, + wardUnadmittedPatientsWithBed, + wardPatientPendingCount, + bedLayouts, + wardUnassignedPatientsList, + }; +} diff --git a/packages/esm-ward-app/src/ward-view/ward-view.test.tsx b/packages/esm-ward-app/src/ward-view/ward-view.test.tsx index 1dc2b0771..c471a2b36 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.test.tsx +++ b/packages/esm-ward-app/src/ward-view/ward-view.test.tsx @@ -1,14 +1,22 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { type ConfigSchema, getDefaultsFromConfigSchema, useConfig, useFeatureFlag } from '@openmrs/esm-framework'; +import { + type ConfigSchema, + getDefaultsFromConfigSchema, + useConfig, + useFeatureFlag, + useAppContext, +} from '@openmrs/esm-framework'; import { useParams } from 'react-router-dom'; import { mockAdmissionLocation, mockInpatientAdmissions } from '__mocks__'; import { renderWithSwr } from 'tools'; import { configSchema } from '../config-schema'; import { useAdmissionLocation } from '../hooks/useAdmissionLocation'; import { useInpatientAdmission } from '../hooks/useInpatientAdmission'; +import { useWardPatientGrouping } from '../hooks/useWardPatientGrouping'; import useWardLocation from '../hooks/useWardLocation'; import WardView from './ward-view.component'; +import { getInpatientAdmissionsUuidMap, createAndGetWardPatientGrouping } from './ward-view.resource'; jest.mocked(useConfig).mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), @@ -39,15 +47,17 @@ jest.mock('../hooks/useAdmissionLocation', () => ({ jest.mock('../hooks/useInpatientAdmission', () => ({ useInpatientAdmission: jest.fn(), })); - -jest.mocked(useAdmissionLocation).mockReturnValue({ +jest.mock('../hooks/useWardPatientGrouping', () => ({ + useWardPatientGrouping: jest.fn(), +})); +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, @@ -55,6 +65,14 @@ jest.mocked(useInpatientAdmission).mockReturnValue({ inpatientAdmissions: mockInpatientAdmissions, }); +const inpatientAdmissionsUuidMap = getInpatientAdmissionsUuidMap(mockInpatientAdmissions); +const mockWardPatientGroupDetails = jest.mocked(useWardPatientGrouping).mockReturnValue({ + admissionLocationResponse: mockAdmissionLocationResponse(), + inpatientAdmissionResponse: mockInpatientAdmissionResponse(), + ...createAndGetWardPatientGrouping(mockInpatientAdmissions, mockAdmissionLocation, inpatientAdmissionsUuidMap), +}); + +jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails()); describe('WardView', () => { it('renders the session location when no location provided in URL', () => { renderWithSwr(); @@ -112,11 +130,14 @@ describe('WardView', () => { isLoading: false, admissionLocation: { ...mockAdmissionLocation, bedLayouts: [] }, }); + const replacedProperty = jest.replaceProperty(mockWardPatientGroupDetails(), 'bedLayouts', []); + mockUseFeatureFlag.mockReturnValueOnce(true); renderWithSwr(); const noBedsConfiguredForThisLocation = screen.queryByText('No beds configured for this location'); expect(noBedsConfiguredForThisLocation).toBeInTheDocument(); + replacedProperty.restore(); }); it('screen not should render warning if backend module installed and no beds configured', () => { diff --git a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx index f8e767ff3..959017213 100644 --- a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx @@ -5,6 +5,7 @@ import { renderWithSwr } from '../../../../../tools'; import AdmitPatientFormWorkspace from './admit-patient-form.workspace'; import { mockAdmissionLocation, + mockInpatientAdmissions, mockInpatientRequest, mockLocationInpatientWard, mockPatientAlice, @@ -12,10 +13,13 @@ import { import type { DispositionType } from '../../types'; import type { AdmitPatientFormWorkspaceProps } from './types'; import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; -import { openmrsFetch, provide, showSnackbar, useFeatureFlag, useSession } from '@openmrs/esm-framework'; +import { openmrsFetch, provide, showSnackbar, useAppContext, useFeatureFlag, useSession } from '@openmrs/esm-framework'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; +import { useWardPatientGrouping } from '../../hooks/useWardPatientGrouping'; +import { getInpatientAdmissionsUuidMap, createAndGetWardPatientGrouping } from '../../ward-view/ward-view.resource'; +import { useInpatientAdmission } from '../../hooks/useInpatientAdmission'; jest.mock('../../hooks/useAdmissionLocation', () => ({ useAdmissionLocation: jest.fn(), @@ -29,14 +33,43 @@ jest.mock('../../hooks/useInpatientRequest', () => ({ useInpatientRequest: jest.fn(), })); +jest.mock('../../hooks/useWardPatientGrouping', () => ({ + useWardPatientGrouping: jest.fn(), +})); + +jest.mock('../../hooks/useInpatientAdmission', () => ({ + useInpatientAdmission: jest.fn(), +})); + +const inpatientAdmissionsUuidMap = getInpatientAdmissionsUuidMap(mockInpatientAdmissions); + const mockedUseInpatientRequest = jest.mocked(useInpatientRequest); const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration); const mockedUseWardLocation = jest.mocked(useWardLocation); const mockedOpenmrsFetch = jest.mocked(openmrsFetch); -const mockedUseAdmissionLocation = jest.mocked(useAdmissionLocation); +const mockedUseAdmissionLocation = jest.mocked(useAdmissionLocation).mockReturnValue({ + isLoading: false, + isValidating: false, + admissionLocation: mockAdmissionLocation, + mutate: jest.fn(), + error: undefined, +}); const mockedUseFeatureFlag = jest.mocked(useFeatureFlag); const mockedShowSnackbar = jest.mocked(showSnackbar); const mockedUseSession = jest.mocked(useSession); +const mockInpatientAdmissionResponse = jest.mocked(useInpatientAdmission).mockReturnValue({ + error: undefined, + mutate: jest.fn(), + isValidating: false, + isLoading: false, + inpatientAdmissions: mockInpatientAdmissions, +}); +const mockWardPatientGroupDetails = jest.mocked(useWardPatientGrouping).mockReturnValue({ + admissionLocationResponse: mockedUseAdmissionLocation(), + inpatientAdmissionResponse: mockInpatientAdmissionResponse(), + ...createAndGetWardPatientGrouping(mockInpatientAdmissions, mockAdmissionLocation, inpatientAdmissionsUuidMap), +}); +jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails()); const mockWorkspaceProps: AdmitPatientFormWorkspaceProps = { patient: mockPatientAlice, @@ -56,13 +89,7 @@ const mockedMutateInpatientRequest = jest.fn(); describe('Testing AdmitPatientForm', () => { beforeEach(() => { jest.clearAllMocks(); - mockedUseAdmissionLocation.mockReturnValue({ - isLoading: false, - isValidating: false, - admissionLocation: mockAdmissionLocation, - mutate: jest.fn(), - error: undefined, - }); + mockedUseSession.mockReturnValue({ currentProvider: { uuid: 'current-provider-uuid', @@ -157,30 +184,15 @@ describe('Testing AdmitPatientForm', () => { it('should render admit patient form if bed management module is present, but no beds are configured', () => { mockedUseFeatureFlag.mockReturnValue(true); - mockedUseAdmissionLocation.mockReturnValueOnce({ - isLoading: false, - isValidating: false, - admissionLocation: { - ...mockAdmissionLocation, - totalBeds: 0, - bedLayouts: [], - }, - mutate: jest.fn(), - error: null, - }); + const replacedProperty = jest.replaceProperty(mockWardPatientGroupDetails(), 'bedLayouts', []); + // @ts-i renderAdmissionForm(); expect(screen.getByText('Select a bed')).toBeInTheDocument(); expect(screen.getByText('No beds configured for Inpatient Ward location')).toBeInTheDocument(); + replacedProperty.restore(); }); it('should submit the form, create encounter and submit bed', async () => { - mockedUseAdmissionLocation.mockReturnValueOnce({ - isLoading: false, - isValidating: false, - admissionLocation: mockAdmissionLocation, - mutate: jest.fn(), - error: null, - }); // @ts-ignore - we only need these two keys for now mockedOpenmrsFetch.mockResolvedValue({ ok: true, @@ -290,17 +302,7 @@ describe('Testing AdmitPatientForm', () => { }); it('should admit patient if no beds are configured', async () => { - mockedUseAdmissionLocation.mockReturnValueOnce({ - isLoading: false, - isValidating: false, - admissionLocation: { - ...mockAdmissionLocation, - totalBeds: 0, - bedLayouts: [], - }, - mutate: jest.fn(), - error: null, - }); + const replacedProperty = jest.replaceProperty(mockWardPatientGroupDetails(), 'bedLayouts', []); // @ts-ignore - we only need these two keys for now mockedOpenmrsFetch.mockResolvedValue({ ok: true, @@ -337,5 +339,6 @@ describe('Testing AdmitPatientForm', () => { subtitle: 'Patient admitted successfully to Inpatient Ward', title: 'Patient admitted successfully', }); + replacedProperty.restore(); }); }); diff --git a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx index cc8d4845a..5799d0bde 100644 --- a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx @@ -4,11 +4,10 @@ import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'react-i18next'; import { Button, ButtonSet, Column, Dropdown, DropdownSkeleton, Form, InlineNotification, Row } from '@carbon/react'; -import { showSnackbar, useFeatureFlag, useSession } from '@openmrs/esm-framework'; +import { showSnackbar, useAppContext, useFeatureFlag, useSession } from '@openmrs/esm-framework'; import { filterBeds } from '../../ward-view/ward-view.resource'; -import type { BedLayout } from '../../types'; +import type { BedLayout, WardPatientGroupDetails } from '../../types'; import { assignPatientToBed, createEncounter } from '../../ward.resource'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; @@ -29,8 +28,9 @@ const AdmitPatientFormWorkspace: React.FC = ({ const { mutate: mutateInpatientRequest } = useInpatientRequest(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); const [showErrorNotifications, setShowErrorNotifications] = useState(false); - const { isLoading, admissionLocation, mutate: mutateAdmissionLocation } = useAdmissionLocation(); - const beds = useMemo(() => (isLoading ? [] : filterBeds(admissionLocation)), [admissionLocation]); + const wardPatientGrouping = useAppContext('ward-patients-group'); + const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGrouping?.admissionLocationResponse ?? {}; + const beds = isLoading ? [] : wardPatientGrouping?.bedLayouts ?? []; const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); const getBedRepresentation = useCallback((bedLayout: BedLayout) => { const bedNumber = bedLayout.bedNumber; @@ -175,6 +175,7 @@ const AdmitPatientFormWorkspace: React.FC = ({ setIsSubmitting(false); }, []); + if (!wardPatientGrouping) return <>; return (
diff --git a/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx index 9643e2370..23e68198f 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx @@ -1,14 +1,13 @@ import React, { useCallback, useState } from 'react'; -import { ExtensionSlot, showSnackbar, useSession } from '@openmrs/esm-framework'; +import { ExtensionSlot, showSnackbar, useAppContext, useSession } from '@openmrs/esm-framework'; import { Button, ButtonSet, InlineNotification } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import styles from './patient-discharge.scss'; import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; -import type { WardPatientWorkspaceProps } from '../../types'; +import {type WardPatientGroupDetails, type WardPatientWorkspaceProps } from '../../types'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import { createEncounter, removePatientFromBed } from '../../ward.resource'; import useWardLocation from '../../hooks/useWardLocation'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import { Exit } from '@carbon/react/icons'; @@ -19,7 +18,8 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro const { currentProvider } = useSession(); const { location } = useWardLocation(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); - const { mutate: mutateAdmissionLocation } = useAdmissionLocation(); + const wardGroupingDetails = useAppContext('ward-patients-group'); + const { mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; const { mutate: mutateInpatientRequest } = useInpatientRequest(); const submitDischarge = useCallback(() => { @@ -74,6 +74,7 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro mutateInpatientRequest, ]); + if (!wardGroupingDetails) return <>; return (
diff --git a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx index 728c84813..9e387c015 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx @@ -1,15 +1,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styles from './patient-transfer-swap.scss'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; import { z } from 'zod'; import { useTranslation } from 'react-i18next'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { filterBeds } from '../../ward-view/ward-view.resource'; -import type { BedLayout, WardPatientWorkspaceProps } from '../../types'; +import type { BedLayout, WardPatientGroupDetails, WardPatientWorkspaceProps } from '../../types'; import { assignPatientToBed, createEncounter } from '../../ward.resource'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; -import { showSnackbar, useSession } from '@openmrs/esm-framework'; +import { showSnackbar, useAppContext, useSession } from '@openmrs/esm-framework'; import useWardLocation from '../../hooks/useWardLocation'; import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import { @@ -31,12 +30,12 @@ export default function PatientBedSwapForm({ const { patient } = wardPatient; const { t } = useTranslation(); const [showErrorNotifications, setShowErrorNotifications] = useState(false); - const { isLoading, admissionLocation } = useAdmissionLocation(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); const [isSubmitting, setIsSubmitting] = useState(false); const { currentProvider } = useSession(); const { location } = useWardLocation(); - const { mutate: mutateAdmissionLocation } = useAdmissionLocation(); + const wardGroupingDetails = useAppContext('ward-patients-group'); + const { isLoading, mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; const { mutate: mutateInpatientRequest } = useInpatientRequest(); const zodSchema = useMemo( @@ -72,7 +71,7 @@ export default function PatientBedSwapForm({ [t], ); - const beds = useMemo(() => (admissionLocation ? filterBeds(admissionLocation) : []), [admissionLocation]); + const beds = wardGroupingDetails?.bedLayouts ?? []; const bedDetails = useMemo( () => beds.map((bed) => { @@ -148,6 +147,7 @@ export default function PatientBedSwapForm({ setShowErrorNotifications(true); }, []); + if (!wardGroupingDetails) return <>; return ( emrConfiguration?.dispositions.filter(({ type }) => type === 'TRANSFER'), [emrConfiguration], ); - const { mutate: mutateAdmissionLocation } = useAdmissionLocation(); + const wardGroupingDetails = useAppContext('ward-patients-group'); + const { mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; const { mutate: mutateInpatientRequest } = useInpatientRequest(); const zodSchema = useMemo( @@ -154,6 +154,7 @@ export default function PatientTransferForm({ setShowErrorNotifications(true); }, []); + if (!wardGroupingDetails) return <>; return (