From 8761fb7bf72a9f0ed709234e9dfa3c7678c5ff79 Mon Sep 17 00:00:00 2001 From: chibongho Date: Thu, 24 Oct 2024 15:30:42 -0400 Subject: [PATCH] (fix) O3-4016 Ward App - add way to unassign bed from patient without transferring / discharging (#1350) --- __mocks__/wards.mock.ts | 4 +- .../esm-ward-app/src/beds/ward-bed.test.tsx | 21 +- .../src/hooks/useWardPatientGrouping.ts | 5 + .../maternal-ward-patient-card.test.tsx | 11 + .../src/ward-view/ward-view.test.tsx | 8 + ...mission-request-card-actions.component.tsx | 65 +++- .../admission-requests-workspace.test.tsx | 46 ++- .../admission-requests.workspace.tsx | 31 +- .../admit-patient-form.test.tsx | 124 ++------ .../admit-patient-form.workspace.tsx | 290 ++++++------------ .../ward-workspace/bed-selector.component.tsx | 119 +++++++ .../src/ward-workspace/bed-selector.scss | 15 + .../patient-discharge.workspace.tsx | 15 +- .../patient-bed-swap-form.component.tsx | 133 ++++---- ...atient-transfer-request-form.component.tsx | 13 +- .../patient-transfer-swap.workspace.tsx | 16 +- packages/esm-ward-app/src/ward.resource.ts | 34 +- packages/esm-ward-app/translations/en.json | 11 +- 18 files changed, 548 insertions(+), 413 deletions(-) create mode 100644 packages/esm-ward-app/src/ward-workspace/bed-selector.component.tsx create mode 100644 packages/esm-ward-app/src/ward-workspace/bed-selector.scss diff --git a/__mocks__/wards.mock.ts b/__mocks__/wards.mock.ts index 365538548..66b29504d 100644 --- a/__mocks__/wards.mock.ts +++ b/__mocks__/wards.mock.ts @@ -36,7 +36,7 @@ export const mockAdmissionLocation: AdmissionLocationFetchResponse = { rowNumber: 1, columnNumber: 2, bedNumber: 'bed2', - bedId: 1, + bedId: 2, bedUuid: '0000-bed2', status: 'AVAILABLE', bedType: mockBedType, @@ -79,4 +79,4 @@ export const mockInpatientAdmissions: InpatientAdmission[] = [ currentInpatientRequest: null, firstAdmissionOrTransferEncounter: null, }, -]; \ No newline at end of file +]; diff --git a/packages/esm-ward-app/src/beds/ward-bed.test.tsx b/packages/esm-ward-app/src/beds/ward-bed.test.tsx index 5679bb119..fa5b8675e 100644 --- a/packages/esm-ward-app/src/beds/ward-bed.test.tsx +++ b/packages/esm-ward-app/src/beds/ward-bed.test.tsx @@ -1,26 +1,37 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework'; -import { configSchema, type WardConfigObject } from '../config-schema'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; import { mockAdmissionLocation, mockLocationInpatientWard, mockPatientAlice, mockPatientBrian, } from '../../../../__mocks__'; -import { bedLayoutToBed, filterBeds } from '../ward-view/ward-view.resource'; +import { configSchema, type WardConfigObject } from '../config-schema'; +import { useObs } from '../hooks/useObs'; import useWardLocation from '../hooks/useWardLocation'; -import WardBed from './ward-bed.component'; import { type WardPatient } from '../types'; import DefaultWardPatientCard from '../ward-view/default-ward/default-ward-patient-card.component'; +import { bedLayoutToBed, filterBeds } from '../ward-view/ward-view.resource'; +import WardBed from './ward-bed.component'; const defaultConfig: WardConfigObject = getDefaultsFromConfigSchema(configSchema); jest.mocked(useConfig).mockReturnValue(defaultConfig); +jest.mock('../hooks/useObs', () => ({ + useObs: jest.fn(), +})); +jest.mock('../ward-patient-card/row-elements/ward-patient-obs.resource', () => ({ + useConceptToTagColorMap: jest.fn(), +})); const mockBedLayouts = filterBeds(mockAdmissionLocation); jest.mock('../hooks/useWardLocation', () => jest.fn()); +//@ts-ignore +jest.mocked(useObs).mockReturnValue({ + data: [], +}); const mockedUseWardLocation = useWardLocation as jest.Mock; mockedUseWardLocation.mockReturnValue({ diff --git a/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts b/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts index f6c43b43f..cf2be55d3 100644 --- a/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts +++ b/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts @@ -23,5 +23,10 @@ export function useWardPatientGrouping() { inpatientRequestResponse, isLoading: admissionLocationResponse.isLoading || inpatientAdmissionResponse.isLoading || inpatientRequestResponse.isLoading, + mutate() { + admissionLocationResponse?.mutate(); + inpatientAdmissionResponse?.mutate(); + inpatientRequestResponse?.mutate(); + }, }; } diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.test.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.test.tsx index 131fd0d41..a01fa6b63 100644 --- a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.test.tsx +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.test.tsx @@ -7,14 +7,25 @@ import { mockInpatientAdmissionAlice } from '../../../../../__mocks__/inpatient- import { mockWardBeds } from '../../../../../__mocks__/wardBeds.mock'; import { mockWardViewContext } from '../../../mock'; import { configSchema, type WardConfigObject } from '../../config-schema'; +import { useObs } from '../../hooks/useObs'; import { type WardPatient, type WardViewContext } from '../../types'; import MaternalWardPatientCard from './maternal-ward-patient-card.component'; jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); +jest.mock('../../hooks/useObs', () => ({ + useObs: jest.fn(), +})); +jest.mock('../../ward-patient-card/row-elements/ward-patient-obs.resource', () => ({ + useConceptToTagColorMap: jest.fn(), +})); const defaultConfig: WardConfigObject = getDefaultsFromConfigSchema(configSchema); jest.mocked(useConfig).mockReturnValue(defaultConfig); +//@ts-ignore +jest.mocked(useObs).mockReturnValue({ + data: [], +}); describe('MaternalWardPatientCard', () => { it('renders a patient with no child', () => { 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 9c97eb943..d37ac8d53 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 @@ -11,6 +11,7 @@ import { useParams } from 'react-router-dom'; import { renderWithSwr } from 'tools'; import { mockWardPatientGroupDetails, mockWardViewContext } from '../../mock'; import { configSchema } from '../config-schema'; +import { useObs } from '../hooks/useObs'; import useWardLocation from '../hooks/useWardLocation'; import { type WardViewContext } from '../types'; import DefaultWardView from './default-ward/default-ward-view.component'; @@ -30,6 +31,9 @@ jest.mock('../hooks/useWardLocation', () => invalidLocation: false, }), ); +jest.mock('../hooks/useObs', () => ({ + useObs: jest.fn(), +})); const mockUseWardLocation = jest.mocked(useWardLocation); @@ -40,6 +44,10 @@ jest.mock('react-router-dom', () => ({ const mockUseParams = useParams as jest.Mock; jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); +//@ts-ignore +jest.mocked(useObs).mockReturnValue({ + data: [], +}); const intersectionObserverMock = () => ({ observe: () => null, diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx index fd7bfda05..f75193666 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx @@ -1,8 +1,18 @@ import { Button } from '@carbon/react'; -import { ArrowRightIcon, launchWorkspace, useAppContext, useLayoutType } from '@openmrs/esm-framework'; -import React, { useCallback } from 'react'; +import { + ArrowRightIcon, + launchWorkspace, + showSnackbar, + useAppContext, + useFeatureFlag, + useLayoutType, +} from '@openmrs/esm-framework'; +import React, { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import type { WardPatientWorkspaceProps, WardPatientCardType, WardViewContext } from '../../types'; +import useWardLocation from '../../hooks/useWardLocation'; +import type { WardPatientCardType, WardPatientWorkspaceProps, WardViewContext } from '../../types'; +import { useAdmitPatient } from '../../ward.resource'; +import { AdmissionRequestsWorkspaceContext } from '../admission-request-workspace/admission-requests.workspace'; import type { AdmitPatientFormWorkspaceProps } from '../admit-patient-form-workspace/types'; import styles from './admission-request-card.scss'; @@ -10,8 +20,10 @@ const AdmissionRequestCardActions: WardPatientCardType = (wardPatient) => { const { patient, inpatientRequest } = wardPatient; const { dispositionType } = inpatientRequest; const { t } = useTranslation(); + const { location } = useWardLocation(); const responsiveSize = useLayoutType() === 'tablet' ? 'lg' : 'md'; - const {WardPatientHeader} = useAppContext('ward-view-context') ?? {}; + const { WardPatientHeader, wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; + const { admitPatient, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useAdmitPatient(); const launchPatientAdmissionForm = useCallback( () => launchWorkspace('admit-patient-form-workspace', { patient, dispositionType }), @@ -21,16 +33,57 @@ const AdmissionRequestCardActions: WardPatientCardType = (wardPatient) => { const launchPatientTransferForm = useCallback(() => { launchWorkspace('patient-transfer-request-workspace', { wardPatient, - WardPatientHeader + WardPatientHeader, }); }, [wardPatient, WardPatientHeader]); + const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); + const { closeWorkspaceWithSavedChanges } = useContext(AdmissionRequestsWorkspaceContext); + + // If bed management module is installed, open the next form + // for bed selection. If not, admit patient directly + const onAdmit = () => { + if (isBedManagementModuleInstalled) { + launchPatientAdmissionForm(); + } else { + admitPatient(patient, dispositionType) + .then( + (response) => { + if (response && response?.ok) { + showSnackbar({ + kind: 'success', + title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), + subtitle: t('patientAdmittedWoBed', 'Patient admitted successfully to {{location}}', { + location: location?.display, + }), + }); + } + }, + (err: Error) => { + showSnackbar({ + kind: 'error', + title: t('errorCreatingEncounter', 'Failed to admit patient'), + subtitle: err.message, + }); + }, + ) + .finally(() => { + wardPatientGroupDetails?.mutate?.(); + closeWorkspaceWithSavedChanges(); + }); + } + }; return (
-
diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx index 59c89e035..29eeb0897 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx @@ -3,12 +3,16 @@ import { screen } from '@testing-library/react'; import React from 'react'; import { renderWithSwr } from '../../../../../tools'; import { mockWardViewContext } from '../../../mock'; +import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import { type WardViewContext } from '../../types'; import DefaultWardPendingPatients from '../../ward-view/default-ward/default-ward-pending-patients.component'; import AdmissionRequestsWorkspace, { type AdmissionRequestsWorkspaceProps } from './admission-requests.workspace'; jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); +jest.mock('../../hooks/useEmrConfiguration', () => jest.fn()); +const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration); + const workspaceProps: AdmissionRequestsWorkspaceProps = { closeWorkspace: jest.fn(), promptBeforeClosing: jest.fn(), @@ -18,9 +22,49 @@ const workspaceProps: AdmissionRequestsWorkspaceProps = { }; describe('Admission Requests Workspace', () => { - it('should render a admission request card', () => { + beforeEach(() => { + mockedUseEmrConfiguration.mockReturnValue({ + isLoadingEmrConfiguration: false, + errorFetchingEmrConfiguration: null, + // @ts-ignore - we only need these keys for now + emrConfiguration: { + admissionEncounterType: { + uuid: 'admission-encounter-type-uuid', + display: 'Admission Encounter', + }, + transferWithinHospitalEncounterType: { + uuid: 'transfer-within-hospital-encounter-type-uuid', + display: 'Transfer Within Hospital Encounter Type', + }, + clinicianEncounterRole: { + uuid: 'clinician-encounter-role-uuid', + }, + }, + mutateEmrConfiguration: jest.fn(), + }); + }); + + it('should render an admission request card', () => { renderWithSwr(); const alice = mockWardViewContext.wardPatientGroupDetails.inpatientRequestResponse.inpatientRequests[0].patient; expect(screen.getByText(alice.person?.preferredName?.display as string)).toBeInTheDocument(); }); + + it('should render an admission request card with disabled "admit patient" button when emr config fails to load', () => { + mockedUseEmrConfiguration.mockReturnValue({ + isLoadingEmrConfiguration: false, + errorFetchingEmrConfiguration: true, + emrConfiguration: null, + mutateEmrConfiguration: jest.fn(), + }); + + renderWithSwr(); + expect(screen.getByText("Some parts of the form didn't load")).toBeInTheDocument(); + expect( + screen.getByText( + 'Fetching EMR configuration failed. Try refreshing the page or contact your system administrator.', + ), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Admit patient/i })).toBeDisabled(); + }); }); diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx index 1be005eb3..88cc02fad 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx @@ -1,18 +1,24 @@ -import { Search } from '@carbon/react'; +import { InlineNotification, Search } from '@carbon/react'; import { type DefaultWorkspaceProps } from '@openmrs/esm-framework'; -import React, { type ReactNode } from 'react'; +import React, { createContext, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; +import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import styles from './admission-requests-workspace.scss'; export interface AdmissionRequestsWorkspaceProps extends DefaultWorkspaceProps { wardPendingPatients: ReactNode; } -const AdmissionRequestsWorkspace: React.FC = ({ wardPendingPatients }) => { + +export const AdmissionRequestsWorkspaceContext = createContext(null); + +const AdmissionRequestsWorkspace: React.FC = (props) => { + const { wardPendingPatients } = props; const { t } = useTranslation(); const [searchTerm, setSearchTerm] = React.useState(''); const handleSearch = (event: React.ChangeEvent) => { setSearchTerm(event.target.value); }; + const { errorFetchingEmrConfiguration } = useEmrConfiguration(); return (
@@ -24,8 +30,23 @@ const AdmissionRequestsWorkspace: React.FC = ({ placeholder={t('searchForPatient', 'Search for a patient')} disabled /> - -
{wardPendingPatients}
+ {errorFetchingEmrConfiguration && ( +
+ +
+ )} + +
{wardPendingPatients}
+
); }; 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 af4e23b77..d40219392 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 @@ -2,14 +2,13 @@ import { showSnackbar, useAppContext, useFeatureFlag, useSession } from '@openmr import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { mockAdmissionLocation, mockLocationInpatientWard, mockPatientAlice } from '../../../../../__mocks__'; +import { mockLocationInpatientWard, mockPatientAlice } from '../../../../../__mocks__'; import { renderWithSwr } from '../../../../../tools'; import { mockWardPatientGroupDetails, mockWardViewContext } from '../../../mock'; import { useAssignedBedByPatient } from '../../hooks/useAssignedBedByPatient'; -import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; import type { DispositionType, WardViewContext } from '../../types'; -import { assignPatientToBed, createEncounter, removePatientFromBed } from '../../ward.resource'; +import { assignPatientToBed, removePatientFromBed, useAdmitPatient } from '../../ward.resource'; import AdmitPatientFormWorkspace from './admit-patient-form.workspace'; import type { AdmitPatientFormWorkspaceProps } from './types'; @@ -19,8 +18,6 @@ jest.mock('../../hooks/useAdmissionLocation', () => ({ jest.mock('../../hooks/useWardLocation', () => jest.fn()); -jest.mock('../../hooks/useEmrConfiguration', () => jest.fn()); - jest.mock('../../hooks/useInpatientRequest', () => ({ useInpatientRequest: jest.fn(), })); @@ -39,22 +36,30 @@ jest.mock('../../hooks/useAssignedBedByPatient', () => ({ jest.mock('../../ward.resource', () => ({ createEncounter: jest.fn(), + useAdmitPatient: jest.fn(), assignPatientToBed: jest.fn(), removePatientFromBed: jest.fn(), })); -const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration); const mockedUseWardLocation = jest.mocked(useWardLocation); const mockedUseFeatureFlag = jest.mocked(useFeatureFlag); const mockedShowSnackbar = jest.mocked(showSnackbar); const mockedUseSession = jest.mocked(useSession); const mockedUseAssignedBedByPatient = jest.mocked(useAssignedBedByPatient); const mockedAssignPatientToBed = jest.mocked(assignPatientToBed); -const mockedCreateEncounter = jest.mocked(createEncounter); const mockedRemovePatientFromBed = jest.mocked(removePatientFromBed); +const mockedUseAdmitPatient = jest.mocked(useAdmitPatient); jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); +const mockUseAdmitPatientObj: ReturnType = { + admitPatient: jest.fn(), + isLoadingEmrConfiguration: false, + errorFetchingEmrConfiguration: false, +}; +jest.mocked(useAdmitPatient).mockReturnValue(mockUseAdmitPatientObj); +const mockedAdmitPatient = mockUseAdmitPatientObj.admitPatient; + const mockWorkspaceProps: AdmitPatientFormWorkspaceProps = { patient: mockPatientAlice, closeWorkspace: jest.fn(), @@ -81,31 +86,15 @@ describe('Testing AdmitPatientForm', () => { sessionId: 'session-id', }); mockedUseFeatureFlag.mockReturnValue(true); - mockedUseEmrConfiguration.mockReturnValue({ - isLoadingEmrConfiguration: false, - errorFetchingEmrConfiguration: null, - // @ts-ignore - we only need these two keys for now - emrConfiguration: { - admissionEncounterType: { - uuid: 'admission-encounter-type-uuid', - display: 'Admission Encounter', - }, - transferWithinHospitalEncounterType: { - uuid: 'transfer-within-hospital-encounter-type-uuid', - display: 'Transfer Within Hospital Encounter Type', - }, - clinicianEncounterRole: { - uuid: 'clinician-encounter-role-uuid', - }, - }, - mutateEmrConfiguration: jest.fn(), - }); + mockedUseWardLocation.mockReturnValue({ location: mockLocationInpatientWard, invalidLocation: false, isLoadingLocation: false, errorFetchingLocation: null, }); + + // @ts-ignore - we don't need to mock the entire object mockedUseAssignedBedByPatient.mockReturnValue({ data: { data: { @@ -120,10 +109,10 @@ describe('Testing AdmitPatientForm', () => { ], }, }, - } as ReturnType); + }); // @ts-ignore - we only need these two keys for now - mockedCreateEncounter.mockResolvedValue({ + mockedAdmitPatient.mockResolvedValue({ ok: true, data: { uuid: 'encounter-uuid', @@ -151,43 +140,24 @@ describe('Testing AdmitPatientForm', () => { }); screen.getByText('Admit'); expect(screen.getByText('Select a bed')).toBeInTheDocument(); - await user.click( - screen.getByRole('combobox', { - name: 'Choose an option', - }), - ); - expect(screen.getByText('bed1 · Alice Johnson')).toBeInTheDocument(); - expect(screen.getByText('bed2 · Empty')).toBeInTheDocument(); - expect(screen.getByText('bed3 · Empty')).toBeInTheDocument(); - expect(screen.getByText('bed4 · Empty')).toBeInTheDocument(); + + expect(screen.getByRole('radio', { name: 'bed1 · Alice Johnson' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'bed2 · Empty' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'bed3 · Empty' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'bed4 · Empty' })).toBeInTheDocument(); }); it('should block the form if emr configuration is not fetched properly', () => { - mockedUseEmrConfiguration.mockReturnValue({ + mockedUseAdmitPatient.mockReturnValueOnce({ + admitPatient: mockedAdmitPatient, isLoadingEmrConfiguration: false, errorFetchingEmrConfiguration: true, - emrConfiguration: null, - mutateEmrConfiguration: jest.fn(), }); renderAdmissionForm(); const admitButton = screen.getByText('Admit'); expect(admitButton).toBeDisabled(); - expect(screen.getByText("Some parts of the form didn't load")).toBeInTheDocument(); - expect( - screen.getByText( - 'Fetching EMR configuration failed. Try refreshing the page or contact your system administrator.', - ), - ).toBeInTheDocument(); - }); - - it('should render admit patient form if bed management module is not present', () => { - mockedUseFeatureFlag.mockReturnValue(false); - renderAdmissionForm(); - expect(screen.getByText('Select a bed')).toBeInTheDocument(); - expect(screen.getByText('Unable to select beds')).toBeInTheDocument(); - expect(screen.getByText('Bed management module is not present to allow bed selection')).toBeInTheDocument(); }); it('should render admit patient form if bed management module is present, but no beds are configured', () => { @@ -202,27 +172,12 @@ describe('Testing AdmitPatientForm', () => { it('should submit the form, create encounter and submit bed', async () => { const user = userEvent.setup(); renderAdmissionForm(); - const combobox = screen.getByRole('combobox', { - name: 'Choose an option', - }); - await user.click(combobox); - const bedOption = screen.getByText('bed3 · Empty'); + const bedOption = screen.getByRole('radio', { name: 'bed3 · Empty' }); await user.click(bedOption); const admitButton = screen.getByRole('button', { name: 'Admit' }); expect(admitButton).toBeEnabled(); await user.click(admitButton); - expect(mockedCreateEncounter).toHaveBeenCalledWith({ - patient: mockPatientAlice.uuid, - encounterType: 'admission-encounter-type-uuid', - location: mockAdmissionLocation.ward.uuid, - obs: [], - encounterProviders: [ - { - provider: 'current-provider-uuid', - encounterRole: 'clinician-encounter-role-uuid', - }, - ], - }); + expect(mockedAdmitPatient).toHaveBeenCalledWith(mockPatientAlice, 'ADMIT'); expect(mockedAssignPatientToBed).toHaveBeenCalledWith(3, mockPatientAlice.uuid, 'encounter-uuid'); expect(mockedShowSnackbar).toHaveBeenCalledWith({ kind: 'success', @@ -232,14 +187,10 @@ describe('Testing AdmitPatientForm', () => { }); it('should show snackbar if there was an issue creating an encounter', async () => { - mockedCreateEncounter.mockRejectedValue(new Error('Failed to create encounter')); + mockedAdmitPatient.mockRejectedValue(new Error('Failed to create encounter')); const user = userEvent.setup(); renderAdmissionForm(); - const combobox = screen.getByRole('combobox', { - name: 'Choose an option', - }); - await user.click(combobox); - const bedOption = screen.getByText('bed3 · Empty'); + const bedOption = screen.getByRole('radio', { name: 'bed3 · Empty' }); await user.click(bedOption); const admitButton = screen.getByRole('button', { name: 'Admit' }); expect(admitButton).toBeEnabled(); @@ -256,11 +207,7 @@ describe('Testing AdmitPatientForm', () => { const user = userEvent.setup(); renderAdmissionForm(); - const combobox = screen.getByRole('combobox', { - name: 'Choose an option', - }); - await user.click(combobox); - const bedOption = screen.getByText('bed3 · Empty'); + const bedOption = screen.getByRole('radio', { name: 'bed3 · Empty' }); await user.click(bedOption); const admitButton = screen.getByRole('button', { name: 'Admit' }); expect(admitButton).toBeEnabled(); @@ -278,18 +225,7 @@ describe('Testing AdmitPatientForm', () => { const admitButton = screen.getByRole('button', { name: 'Admit' }); expect(admitButton).toBeEnabled(); await user.click(admitButton); - expect(mockedCreateEncounter).toHaveBeenCalledWith({ - patient: mockPatientAlice.uuid, - encounterType: 'admission-encounter-type-uuid', - location: mockAdmissionLocation.ward.uuid, - obs: [], - encounterProviders: [ - { - provider: 'current-provider-uuid', - encounterRole: 'clinician-encounter-role-uuid', - }, - ], - }); + expect(mockedAdmitPatient).toHaveBeenCalledWith(mockPatientAlice, 'ADMIT'); expect(mockedRemovePatientFromBed).toHaveBeenCalledWith(1, mockPatientAlice.uuid); expect(mockedShowSnackbar).toHaveBeenCalledWith({ kind: 'success', 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 359756c87..1eebe85ee 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 @@ -1,18 +1,24 @@ -import { Button, ButtonSet, Column, Dropdown, DropdownSkeleton, Form, InlineNotification, Row } from '@carbon/react'; +import { Button, ButtonSet, Column, Form, InlineNotification, Row } from '@carbon/react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { showSnackbar, useAppContext, useFeatureFlag, useSession } from '@openmrs/esm-framework'; +import { showSnackbar, useAppContext } from '@openmrs/esm-framework'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import useEmrConfiguration from '../../hooks/useEmrConfiguration'; +import { useAssignedBedByPatient } from '../../hooks/useAssignedBedByPatient'; import useWardLocation from '../../hooks/useWardLocation'; -import type { BedLayout, WardViewContext } from '../../types'; -import { assignPatientToBed, createEncounter, removePatientFromBed } from '../../ward.resource'; +import type { WardViewContext } from '../../types'; +import { assignPatientToBed, removePatientFromBed, useAdmitPatient } from '../../ward.resource'; +import BedSelector from '../bed-selector.component'; import styles from './admit-patient-form.scss'; import type { AdmitPatientFormWorkspaceProps } from './types'; -import { useAssignedBedByPatient } from '../../hooks/useAssignedBedByPatient'; +/** + * This form gets rendered when the user clicks "admit patient" in + * the patient card in the admission requests workspace, but only when + * the bed management module is installed. It asks to (optionally) select + * a bed to assign to patient + */ const AdmitPatientFormWorkspace: React.FC = ({ patient, dispositionType, @@ -22,34 +28,23 @@ const AdmitPatientFormWorkspace: React.FC = ({ }) => { const { t } = useTranslation(); const { location } = useWardLocation(); - const { currentProvider } = useSession(); const [isSubmitting, setIsSubmitting] = useState(false); - const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); + const { admitPatient, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useAdmitPatient(); const [showErrorNotifications, setShowErrorNotifications] = useState(false); const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; - const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; + const { isLoading } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; + const { data: bedsAssignedToPatient, isLoading: isLoadingBedsAssignedToPatient } = useAssignedBedByPatient( patient.uuid, ); const beds = isLoading ? [] : wardPatientGroupDetails?.bedLayouts ?? []; - const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); - const getBedRepresentation = useCallback((bedLayout: BedLayout) => { - const bedNumber = bedLayout.bedNumber; - const patients = - bedLayout.patients.length === 0 - ? [t('emptyText', 'Empty')] - : bedLayout.patients.map((patient) => patient?.person?.preferredName?.display); - return [bedNumber, ...patients].join(' · '); - }, []); const zodSchema = useMemo( () => z.object({ bedId: z.number().optional(), }), - [isBedManagementModuleInstalled, beds], + [beds], ); type FormValues = z.infer; @@ -58,8 +53,6 @@ const AdmitPatientFormWorkspace: React.FC = ({ control, formState: { errors, isDirty }, handleSubmit, - getValues, - watch, } = useForm({ resolver: zodResolver(zodSchema), }); @@ -68,114 +61,77 @@ const AdmitPatientFormWorkspace: React.FC = ({ promptBeforeClosing(() => isDirty); }, [isDirty]); - const onSubmit = useCallback( - (values: FormValues) => { - setShowErrorNotifications(false); - setIsSubmitting(true); - const bedSelected = beds.find((bed) => bed.bedId === values.bedId); - createEncounter({ - patient: patient.uuid, - encounterType: - dispositionType === 'ADMIT' - ? emrConfiguration.admissionEncounterType.uuid - : dispositionType === 'TRANSFER' - ? emrConfiguration.transferWithinHospitalEncounterType.uuid - : null, - location: location?.uuid, - encounterProviders: [ - { - provider: currentProvider?.uuid, - encounterRole: emrConfiguration.clinicianEncounterRole.uuid, - }, - ], - obs: [], - }) - .then( - async (response) => { - if (response.ok) { - if (bedSelected) { - return assignPatientToBed(values.bedId, patient.uuid, response.data.uuid); - } else { - const assignedBedId = bedsAssignedToPatient?.data?.results?.[0]?.bedId; - if (assignedBedId) { - return removePatientFromBed(assignedBedId, patient.uuid); - } - return response; + const onSubmit = (values: FormValues) => { + setShowErrorNotifications(false); + setIsSubmitting(true); + const bedSelected = beds.find((bed) => bed.bedId === values.bedId); + admitPatient(patient, dispositionType) + .then( + async (response) => { + if (response.ok) { + if (bedSelected) { + return assignPatientToBed(values.bedId, patient.uuid, response.data.uuid); + } else { + const assignedBedId = bedsAssignedToPatient?.data?.results?.[0]?.bedId; + if (assignedBedId) { + return removePatientFromBed(assignedBedId, patient.uuid); } + return response; } - }, - (err: Error) => { - showSnackbar({ - kind: 'error', - title: t('errorCreatingEncounter', 'Failed to admit patient', { - encounterType: - dispositionType === 'ADMIT' - ? emrConfiguration.admissionEncounterType.display - : emrConfiguration.transferWithinHospitalEncounterType.display, - }), - subtitle: err.message, - }); - }, - ) - .then( - (response) => { - if (response && response?.ok) { - if (bedSelected) { - showSnackbar({ - kind: 'success', - title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), - subtitle: t( - 'patientAdmittedSuccessfullySubtitle', - '{{patientName}} has been successfully admitted and assigned to bed {{bedNumber}}', - { - patientName: patient.person.preferredName.display, - bedNumber: bedSelected.bedNumber, - }, - ), - }); - } else { - showSnackbar({ - kind: 'success', - title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), - subtitle: t('patientAdmittedWoBed', 'Patient admitted successfully to {{location}}', { - location: location?.display, - }), - }); - } + } + }, + (err: Error) => { + showSnackbar({ + kind: 'error', + title: t('errorCreatingEncounter', 'Failed to admit patient'), + subtitle: err.message, + }); + }, + ) + .then( + (response) => { + if (response && response?.ok) { + if (bedSelected) { + showSnackbar({ + kind: 'success', + title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), + subtitle: t( + 'patientAdmittedSuccessfullySubtitle', + '{{patientName}} has been successfully admitted and assigned to bed {{bedNumber}}', + { + patientName: patient.person.preferredName.display, + bedNumber: bedSelected.bedNumber, + }, + ), + }); + } else { + showSnackbar({ + kind: 'success', + title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), + subtitle: t('patientAdmittedWoBed', 'Patient admitted successfully to {{location}}', { + location: location?.display, + }), + }); } - }, - () => { - showSnackbar({ - kind: 'warning', - title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), - subtitle: t( - 'patientAdmittedButBedNotAssigned', - 'Patient admitted successfully but fail to assign bed to patient', - ), - }); - }, - ) - .finally(() => { - setIsSubmitting(false); - mutateAdmissionLocation(); - mutateInpatientRequest(); - mutateInpatientAdmission(); - closeWorkspaceWithSavedChanges(); - }); - }, - [ - beds, - patient, - emrConfiguration, - location, - closeWorkspaceWithSavedChanges, - dispositionType, - currentProvider, - mutateAdmissionLocation, - mutateInpatientRequest, - mutateInpatientAdmission, - ], - ); + } + }, + () => { + showSnackbar({ + kind: 'warning', + title: t('patientAdmittedSuccessfully', 'Patient admitted successfully'), + subtitle: t( + 'patientAdmittedButBedNotAssigned', + 'Patient admitted successfully but fail to assign bed to patient', + ), + }); + }, + ) + .finally(() => { + setIsSubmitting(false); + wardPatientGroupDetails?.mutate?.(); + closeWorkspaceWithSavedChanges(); + }); + }; const onError = useCallback((values) => { setShowErrorNotifications(true); @@ -186,71 +142,27 @@ const AdmitPatientFormWorkspace: React.FC = ({ return (
- {errorFetchingEmrConfiguration && ( -
- -
- )}

{t('selectABed', 'Select a bed')}

- {isBedManagementModuleInstalled ? ( - isLoading ? ( - - ) : beds.length ? ( - { - const selectedItem = beds.find((bed) => bed.bedId === getValues()?.bedId); - return ( - onChange(selectedItem.bedId)} - invalid={!!error} - invalidText={error?.message} - /> - ); - }} - /> - ) : ( - - ) - ) : ( - - )} + { + return ( + + ); + }} + />
diff --git a/packages/esm-ward-app/src/ward-workspace/bed-selector.component.tsx b/packages/esm-ward-app/src/ward-workspace/bed-selector.component.tsx new file mode 100644 index 000000000..2bdbcdd34 --- /dev/null +++ b/packages/esm-ward-app/src/ward-workspace/bed-selector.component.tsx @@ -0,0 +1,119 @@ +import { Dropdown, InlineNotification, RadioButton, RadioButtonGroup, RadioButtonSkeleton } from '@carbon/react'; +import { type Patient } from '@openmrs/esm-framework'; +import React from 'react'; +import { type Control, type FieldError } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import useWardLocation from '../hooks/useWardLocation'; +import { type BedLayout } from '../types'; +import styles from './bed-selector.scss'; + +interface BedSelectorProps { + beds: BedLayout[]; + isLoadingBeds: boolean; + currentPatient: Patient; + selectedBedId: number; + error: FieldError; + onChange(bedId: number); + control: Control<{ bedId?: number }, any, { bedId?: number }>; + minBedCountToUseDropdown?: number; +} + +interface BedDropdownItem { + bedId: number; + label: string; + disabled: boolean; +} + +const BedSelector: React.FC = ({ + selectedBedId, + beds, + isLoadingBeds, + error, + onChange, + currentPatient, + control, + minBedCountToUseDropdown = 16, +}) => { + const { location } = useWardLocation(); + const { t } = useTranslation(); + + const getBedRepresentation = (bedLayout: BedLayout) => { + const bedNumber = bedLayout.bedNumber; + const patients = + bedLayout.patients.length === 0 + ? [t('emptyText', 'Empty')] + : bedLayout.patients.map((patient) => patient?.person?.preferredName?.display); + return [bedNumber, ...patients].join(' · '); + }; + + const bedDropdownItems: BedDropdownItem[] = [ + { bedId: 0, label: t('noBed', 'No bed'), disabled: false }, + ...beds.map((bed) => { + const isPatientAssignedToBed = bed.patients.some((bedPatient) => bedPatient.uuid === currentPatient.uuid); + return { bedId: bed.bedId, label: getBedRepresentation(bed), disabled: isPatientAssignedToBed }; + }), + ]; + const selectedItem = bedDropdownItems.find((bed) => bed.bedId === selectedBedId); + + const useDropdown = beds.length >= minBedCountToUseDropdown; + + if (isLoadingBeds) { + return ( + + + + + + ); + } + if (!beds.length) { + return ( + + ); + } + if (useDropdown) { + return ( + bedDropdownItem.label} + selectedItem={selectedItem} + onChange={({ selectedItem }: { selectedItem: BedLayout }) => onChange(selectedItem.bedId)} + invalid={!!error} + invalidText={error?.message} + /> + ); + } else { + return ( + + {bedDropdownItems.map(({ bedId, label, disabled }) => ( + + ))} + + ); + } +}; + +export default BedSelector; diff --git a/packages/esm-ward-app/src/ward-workspace/bed-selector.scss b/packages/esm-ward-app/src/ward-workspace/bed-selector.scss new file mode 100644 index 000000000..68914f764 --- /dev/null +++ b/packages/esm-ward-app/src/ward-workspace/bed-selector.scss @@ -0,0 +1,15 @@ +@use '@carbon/type'; +@use '@carbon/layout'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.radioButtonGroup { + margin-top: layout.$spacing-03; + fieldset { + flex-direction: column; + align-items: flex-start; + + :global(.cds--radio-button-wrapper) { + margin-bottom: layout.$spacing-04; + } + } +} 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 bf5e00894..3094042bc 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 @@ -5,7 +5,7 @@ import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; -import { type WardViewContext, type WardPatientWorkspaceProps } from '../../types'; +import { type WardPatientWorkspaceProps, type WardViewContext } from '../../types'; import { createEncounter, removePatientFromBed } from '../../ward.resource'; import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; import styles from './patient-discharge.scss'; @@ -17,10 +17,7 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro const { currentProvider } = useSession(); const { location } = useWardLocation(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); - const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; - const { mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; const submitDischarge = useCallback(() => { setIsSubmitting(true); @@ -63,9 +60,7 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro .finally(() => { setIsSubmitting(false); closeWorkspaceWithSavedChanges(); - mutateAdmissionLocation(); - mutateInpatientRequest(); - mutateInpatientAdmission(); + wardPatientGroupDetails.mutate(); }); }, [ currentProvider, @@ -73,9 +68,7 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro emrConfiguration, wardPatient?.patient?.uuid, wardPatient?.bed?.uuid, - mutateAdmissionLocation, - mutateInpatientRequest, - mutateInpatientAdmission, + wardPatientGroupDetails, ]); if (!wardPatientGroupDetails) 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 13f1c4afc..1d4ac42ad 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,12 +1,4 @@ -import { - Button, - ButtonSet, - Form, - InlineNotification, - RadioButton, - RadioButtonGroup, - RadioButtonSkeleton, -} from '@carbon/react'; +import { Button, ButtonSet, Form, InlineNotification } from '@carbon/react'; import { zodResolver } from '@hookform/resolvers/zod'; import { showSnackbar, useAppContext, useSession } from '@openmrs/esm-framework'; import classNames from 'classnames'; @@ -16,8 +8,9 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; -import type { BedLayout, WardViewContext, WardPatientWorkspaceProps } from '../../types'; -import { assignPatientToBed, createEncounter } from '../../ward.resource'; +import type { BedLayout, WardPatientWorkspaceProps, WardViewContext } from '../../types'; +import { assignPatientToBed, createEncounter, removePatientFromBed } from '../../ward.resource'; +import BedSelector from '../bed-selector.component'; import styles from './patient-transfer-swap.scss'; export default function PatientBedSwapForm({ @@ -32,10 +25,8 @@ export default function PatientBedSwapForm({ const [isSubmitting, setIsSubmitting] = useState(false); const { currentProvider } = useSession(); const { location } = useWardLocation(); - const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; - const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; + const { isLoading } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; const zodSchema = useMemo( () => @@ -71,14 +62,6 @@ export default function PatientBedSwapForm({ ); const beds = wardPatientGroupDetails?.bedLayouts ?? []; - const bedDetails = useMemo( - () => - beds.map((bed) => { - const isPatientAssignedToBed = bed.patients.find((bedPatient) => bedPatient.uuid === patient.uuid); - return { id: bed.bedId, label: getBedInformation(bed), isPatientAssignedToBed }; - }), - [beds, getBedInformation], - ); const onSubmit = useCallback( (values: FormValues) => { @@ -98,47 +81,58 @@ export default function PatientBedSwapForm({ }) .then(async (response) => { if (response.ok) { - return assignPatientToBed(values.bedId, patient.uuid, response.data.uuid); + if (bedSelected) { + return assignPatientToBed(values.bedId, patient.uuid, response.data.uuid); + } else { + // get the bed that the patient is currently assigned to + const bedAssignedToPatient = beds.find((bed) => + bed.patients.some((bedPatient) => bedPatient.uuid == patient.uuid), + ); + if (bedAssignedToPatient) { + return removePatientFromBed(bedAssignedToPatient.bedId, patient.uuid); + } else { + // no-op + return Promise.resolve({ ok: true }); + } + } } }) .then((response) => { if (response && response?.ok) { - showSnackbar({ - kind: 'success', - title: t('patientAssignedNewbed', 'Patient assigned to new bed'), - subtitle: t('patientAssignedToBed', '{{patientName}} assigned to bed {{bedNumber}}', { - patientName: patient.person.preferredName.display, - bedNumber: bedSelected.bedNumber, - }), - }); + if (bedSelected) { + showSnackbar({ + kind: 'success', + title: t('patientAssignedNewBed', 'Patient assigned to new bed'), + subtitle: t('patientAssignedNewBedDetail', '{{patientName}} assigned to bed {{bedNumber}}', { + patientName: patient.person.preferredName.display, + bedNumber: bedSelected.bedNumber, + }), + }); + } else { + showSnackbar({ + kind: 'success', + title: t('patientUnassignedFromBed', 'Patient unassigned from bed'), + subtitle: t('patientUnassignedFromBedDetail', '{{patientName}} is now unassigned from bed', { + patientName: patient.person.preferredName.display, + }), + }); + } } }) .catch((error: Error) => { showSnackbar({ kind: 'error', - title: t('errorAssigningBedToPatient', 'Error assigning bed to patient'), + title: t('errorChangingPatientBedAssignment', 'Error changing patient bed assignment'), subtitle: error?.message, }); }) .finally(() => { setIsSubmitting(false); - mutateAdmissionLocation(); - mutateInpatientRequest(); - mutateInpatientAdmission(); + wardPatientGroupDetails.mutate(); closeWorkspaceWithSavedChanges(); }); }, - [ - setShowErrorNotifications, - patient, - emrConfiguration, - currentProvider, - location, - beds, - mutateAdmissionLocation, - mutateInpatientRequest, - mutateInpatientAdmission, - ], + [setShowErrorNotifications, patient, emrConfiguration, currentProvider, location, beds, wardPatientGroupDetails], ); const onError = useCallback(() => { @@ -166,36 +160,21 @@ export default function PatientBedSwapForm({
)}

{t('selectABed', 'Select a bed')}

- {isLoading ? ( - - - - - - ) : ( - ( - - {bedDetails.map(({ id, label, isPatientAssignedToBed }) => ( - - ))} - - )} - /> - )} + ( + + )} + /> {showErrorNotifications && (
{Object.values(errors).map((error) => ( diff --git a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx index 376f9a0ab..9228dd01d 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx @@ -9,7 +9,7 @@ import { z } from 'zod'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; import LocationSelector from '../../location-selector/location-selector.component'; -import type { ObsPayload, WardViewContext, WardPatientWorkspaceProps } from '../../types'; +import type { ObsPayload, WardPatientWorkspaceProps, WardViewContext } from '../../types'; import { createEncounter } from '../../ward.resource'; import styles from './patient-transfer-swap.scss'; @@ -30,9 +30,6 @@ export default function PatientTransferForm({ [emrConfiguration], ); const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; - const { mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; const zodSchema = useMemo( () => @@ -134,9 +131,7 @@ export default function PatientTransferForm({ .finally(() => { setIsSubmitting(false); closeWorkspaceWithSavedChanges(); - mutateAdmissionLocation(); - mutateInpatientAdmission(); - mutateInpatientRequest(); + wardPatientGroupDetails.mutate(); }); }, [ @@ -146,9 +141,7 @@ export default function PatientTransferForm({ emrConfiguration, patient?.uuid, dispositionsWithTypeTransfer, - mutateAdmissionLocation, - mutateInpatientAdmission, - mutateInpatientRequest, + wardPatientGroupDetails, ], ); diff --git a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx index c8d8de07e..5a82e6177 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react'; -import { useFeatureFlag } from '@openmrs/esm-framework'; import { ContentSwitcher, Switch } from '@carbon/react'; +import { useFeatureFlag } from '@openmrs/esm-framework'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import PatientTransferForm from './patient-transfer-request-form.component'; +import type { WardPatientWorkspaceProps } from '../../types'; +import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; import PatientBedSwapForm from './patient-bed-swap-form.component'; +import PatientTransferForm from './patient-transfer-request-form.component'; import styles from './patient-transfer-swap.scss'; -import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; -import type { WardPatientWorkspaceProps } from '../../types'; const TransferSection = { TRANSFER: 'transfer', @@ -37,7 +37,11 @@ export default function PatientTransferAndSwapWorkspace(props: WardPatientWorksp
)}
- {selectedSection === 'transfer' ? : } + {selectedSection === TransferSection.TRANSFER ? ( + + ) : ( + + )}
); diff --git a/packages/esm-ward-app/src/ward.resource.ts b/packages/esm-ward-app/src/ward.resource.ts index 434f48c11..766e3da1d 100644 --- a/packages/esm-ward-app/src/ward.resource.ts +++ b/packages/esm-ward-app/src/ward.resource.ts @@ -1,5 +1,7 @@ -import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; -import type { Encounter, EncounterPayload } from './types'; +import { openmrsFetch, type Patient, restBaseUrl, useSession } from '@openmrs/esm-framework'; +import type { DispositionType, Encounter, EncounterPayload } from './types'; +import useEmrConfiguration from './hooks/useEmrConfiguration'; +import useWardLocation from './hooks/useWardLocation'; export function createEncounter(encounterPayload: EncounterPayload) { return openmrsFetch(`${restBaseUrl}/encounter`, { @@ -11,6 +13,34 @@ export function createEncounter(encounterPayload: EncounterPayload) { }); } +export function useAdmitPatient() { + const { location } = useWardLocation(); + const { currentProvider } = useSession(); + const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); + + const admitPatient = (patient: Patient, dispositionType: DispositionType) => { + return createEncounter({ + patient: patient.uuid, + encounterType: + dispositionType === 'ADMIT' + ? emrConfiguration.admissionEncounterType.uuid + : dispositionType === 'TRANSFER' + ? emrConfiguration.transferWithinHospitalEncounterType.uuid + : null, + location: location?.uuid, + encounterProviders: [ + { + provider: currentProvider?.uuid, + encounterRole: emrConfiguration.clinicianEncounterRole.uuid, + }, + ], + obs: [], + }); + }; + + return { admitPatient, isLoadingEmrConfiguration, errorFetchingEmrConfiguration }; +} + export function assignPatientToBed(bedUuid: number, patientUuid: string, encounterUuid: string) { return openmrsFetch(`${restBaseUrl}/beds/${bedUuid}`, { method: 'POST', diff --git a/packages/esm-ward-app/translations/en.json b/packages/esm-ward-app/translations/en.json index a2b528f8f..93f7765ca 100644 --- a/packages/esm-ward-app/translations/en.json +++ b/packages/esm-ward-app/translations/en.json @@ -5,7 +5,6 @@ "admit": "Admit", "admitPatient": "Admit patient", "admitting": "Admitting...", - "bedManagementModuleNotInstalled": "Bed management module is not present to allow bed selection", "bedShare": "Bed share", "bedSwap": "Bed swap", "cancel": "Cancel", @@ -21,7 +20,7 @@ "emptyBed": "Empty bed", "emptyText": "Empty", "encounterDisplay": "{{encounterType}} {{encounterDate}}", - "errorAssigningBedToPatient": "Error assigning bed to patient", + "errorChangingPatientBedAssignment": "Error changing patient bed assignment", "errorConfiguringPatientCard": "Error configuring patient card", "errorConfiguringPatientCardMessage": "Unable to find configuration for {{elementType}}, id: {{id}}", "errorCreatingEncounter": "Failed to admit patient", @@ -45,6 +44,7 @@ "manage": "Manage", "motherChildBedShare": "Mother / Child", "nextPage": "Next page", + "noBed": "No bed", "noBedsConfigured": "No beds configured for this location", "noBedsConfiguredForLocation": "No beds configured for {{location}} location", "noLocationsFound": "No locations found", @@ -56,14 +56,16 @@ "patientAdmittedSuccessfully": "Patient admitted successfully", "patientAdmittedSuccessfullySubtitle": "{{patientName}} has been successfully admitted and assigned to bed {{bedNumber}}", "patientAdmittedWoBed": "Patient admitted successfully to {{location}}", - "patientAssignedNewbed": "Patient assigned to new bed", - "patientAssignedToBed": "{{patientName}} assigned to bed {{bedNumber}}", + "patientAssignedNewBed": "Patient assigned to new bed", + "patientAssignedNewBedDetail": "{{patientName}} assigned to bed {{bedNumber}}", "patientNoteNowVisible": "It should be now visible in the notes history", "patientNoteSaveError": "Error saving patient note", "patientNotesDidntLoad": "Patient notes didn't load", "patients": "Patients", "patientsMetricValue": "{{ metricValue }}", "patientTransferRequestCreated": "Patient transfer request created", + "patientUnassignedFromBed": "Patient unassigned from bed", + "patientUnassignedFromBedDetail": "{{patientName}} is now unassigned from bed", "patientWasDischarged": "Patient was discharged", "pendingOut": "Pending out", "pendingOutMetricValue": "{{ metricValue }}", @@ -87,7 +89,6 @@ "transfers": "Transfers", "transferType": "Transfer type", "typeOfTransfer": "Type of transfer", - "unableToSelectBeds": "Unable to select beds", "unknown": "Unknown", "visitNoteSaved": "Patient note saved", "wardClinicalNotePlaceholder": "Write any notes here",