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 (