diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsCompleteStage/UserPatientRestrictionsCompleteStage.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsCompleteStage/UserPatientRestrictionsCompleteStage.tsx new file mode 100644 index 000000000..084c84aca --- /dev/null +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsCompleteStage/UserPatientRestrictionsCompleteStage.tsx @@ -0,0 +1,72 @@ +import { useNavigate } from 'react-router-dom'; +import useTitle from '../../../../helpers/hooks/useTitle'; +import { UserPatientRestrictionsSubRoute } from '../../../../types/generic/userPatientRestriction'; +import usePatient from '../../../../helpers/hooks/usePatient'; +import { getFormattedPatientFullName } from '../../../../helpers/utils/formatPatientFullName'; +import { formatNhsNumber } from '../../../../helpers/utils/formatNhsNumber'; +import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate'; +import { Button } from 'nhsuk-react-components'; +import { routes } from '../../../../types/generic/routes'; + +type Props = { + route: UserPatientRestrictionsSubRoute; +}; + +const UserPatientRestrictionsCompleteStage = ({ route }: Props): React.JSX.Element => { + const navigate = useNavigate(); + const patientDetails = usePatient(); + + const pageTitle = + route === UserPatientRestrictionsSubRoute.ADD + ? 'A restriction has been added to this patient record:' + : 'A restriction on accessing this patient record has been removed:'; + useTitle({ pageTitle }); + + return ( + <> +
+

+ {pageTitle} +

+
+
+ + Patient name: {getFormattedPatientFullName(patientDetails)} + +
+ + NHS number: {formatNhsNumber(patientDetails!.nhsNumber)} + +
+ + Date of birth: {getFormattedDateFromString(patientDetails!.birthDate)} + +
+
+ + {route === UserPatientRestrictionsSubRoute.ADD ? ( +

+ When you add a restriction, it will stay with this patient's record until it's + removed. If the patient moves practice, the new practice will see the name and + NHS smartcard number of the staff member you've restricted. +

+ ) : ( +

The staff member can now access this patient's record.

+ )} + +

What happens next

+

You can add, view and make changes to restrictions by going to the admin hub.

+ + + + ); +}; + +export default UserPatientRestrictionsCompleteStage; diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.test.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.test.tsx index 8ce7c7042..eb29dac10 100644 --- a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.test.tsx +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.test.tsx @@ -4,7 +4,10 @@ import userEvent from '@testing-library/user-event'; import { Mock } from 'vitest'; import { routeChildren } from '../../../../types/generic/routes'; import getUserPatientRestrictions from '../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions'; -import { buildUserRestrictions } from '../../../../helpers/test/testBuilders'; +import { buildPatientDetails, buildUserRestrictions } from '../../../../helpers/test/testBuilders'; +import { UserPatientRestrictionsSubRoute } from '../../../../types/generic/userPatientRestriction'; +import { useState } from 'react'; +import getPatientDetails from '../../../../helpers/requests/getPatientDetails'; vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); @@ -27,21 +30,32 @@ vi.mock('react-router-dom', async () => { }; }); vi.mock('../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions'); +vi.mock('../../../../helpers/requests/getPatientDetails'); vi.mock('../../../../helpers/hooks/useBaseAPIHeaders'); vi.mock('../../../../helpers/hooks/useBaseAPIUrl'); +vi.mock('../../../../providers/patientProvider/PatientProvider', () => ({ + usePatientDetailsContext: (): Mock => mockUsePatientDetailsContext(), +})); const mockNavigate = vi.fn(); const mockGetUserPatientRestrictions = getUserPatientRestrictions as Mock; +const mockGetPatientDetails = getPatientDetails as Mock; +const mockUsePatientDetailsContext = vi.fn(); +const mockSetPatientDetails = vi.fn(); describe('UserPatientRestrictionsListStage', () => { + const mockPatientDetails = buildPatientDetails(); + beforeEach(() => { mockGetUserPatientRestrictions.mockResolvedValue({ restrictions: buildUserRestrictions(), }); + mockUsePatientDetailsContext.mockReturnValue([mockPatientDetails, mockSetPatientDetails]); + mockGetPatientDetails.mockResolvedValue(mockPatientDetails); }); it('renders correctly', () => { - render(); + renderPage(); expect( screen.getByText('Manage restrictions on access to patient records'), @@ -49,7 +63,7 @@ describe('UserPatientRestrictionsListStage', () => { }); it('should navigate to add restrictions stage when add restriction button is clicked', async () => { - render(); + renderPage(); const addRestrictionButton = screen.getByRole('button', { name: 'Add a restriction' }); expect(addRestrictionButton).toBeInTheDocument(); @@ -65,7 +79,7 @@ describe('UserPatientRestrictionsListStage', () => { restrictions, }); - render(); + renderPage(); await waitFor(async () => { const viewRestrictionButton = screen.getByTestId( @@ -77,10 +91,7 @@ describe('UserPatientRestrictionsListStage', () => { }); expect(mockNavigate).toHaveBeenCalledWith( - routeChildren.USER_PATIENT_RESTRICTIONS_VIEW.replace( - ':restrictionId', - restrictions[0].id, - ), + routeChildren.USER_PATIENT_RESTRICTIONS_VERIFY_PATIENT, ); }); @@ -89,7 +100,7 @@ describe('UserPatientRestrictionsListStage', () => { new Error('Failed to load restrictions'), ); - render(); + renderPage(); await waitFor(() => { expect(screen.getByTestId('failed-to-load-error')).toBeInTheDocument(); @@ -103,7 +114,7 @@ describe('UserPatientRestrictionsListStage', () => { }), ); - render(); + renderPage(); await waitFor(() => { expect(screen.getByText('Searching...')).toBeInTheDocument(); @@ -116,7 +127,7 @@ describe('UserPatientRestrictionsListStage', () => { restrictions: [], }); - render(); + renderPage(); await waitFor(() => { expect(screen.getByText('No user patient restrictions found')).toBeInTheDocument(); @@ -130,7 +141,7 @@ describe('UserPatientRestrictionsListStage', () => { nextPageToken: 'next-page-token', }); - render(); + renderPage(); await waitFor(() => { expect(screen.getByText('Next')).toBeInTheDocument(); @@ -144,7 +155,7 @@ describe('UserPatientRestrictionsListStage', () => { nextPageToken: 'next-page-token', }); - render(); + renderPage(); await waitFor(async () => { const nextButton = screen.getByTestId('next-page-link'); @@ -168,7 +179,7 @@ describe('UserPatientRestrictionsListStage', () => { nextPageToken: 'next-page-token', }); - render(); + renderPage(); await waitFor(() => { expect(screen.getByText('Next')).toBeInTheDocument(); @@ -197,7 +208,7 @@ describe('UserPatientRestrictionsListStage', () => { nextPageToken: 'next-page-token', }); - render(); + renderPage(); await waitFor(async () => { const nextButton = screen.getByText('Next'); @@ -230,7 +241,7 @@ describe('UserPatientRestrictionsListStage', () => { nextPageToken: 'next-page-token', }); - render(); + renderPage(); await waitFor(async () => { const nextButton = screen.getByText('Next'); @@ -257,7 +268,7 @@ describe('UserPatientRestrictionsListStage', () => { }); it('should send nhsNumber parameter when searching by valid nhs number', async () => { - render(); + renderPage(); const nhsNumber = '2222222222'; await userEvent.click(screen.getByTestId('nhs-number-radio-button')); @@ -275,7 +286,7 @@ describe('UserPatientRestrictionsListStage', () => { }); it('should send smartcardNumber parameter when searching by valid smartcard number', async () => { - render(); + renderPage(); const smartcardNumber = '123456789012'; await userEvent.click(screen.getByTestId('smartcard-number-radio-button')); @@ -295,7 +306,7 @@ describe('UserPatientRestrictionsListStage', () => { it.each(['123', 'abc', '123456789', '12345678901', '1234567890'])( 'should show validation error when searching by invalid nhs number: %s', async (invalidNhsNumber) => { - render(); + renderPage(); await userEvent.click(screen.getByTestId('nhs-number-radio-button')); await userEvent.type(screen.getByTestId('search-input'), invalidNhsNumber); @@ -312,7 +323,7 @@ describe('UserPatientRestrictionsListStage', () => { it.each(['123', 'abc', '123456789', '12345678901', '1234567890', '12345678901a'])( 'should show validation error when searching by invalid smartcard number: %s', async (invalidSmartcardNumber) => { - render(); + renderPage(); await userEvent.click(screen.getByTestId('smartcard-number-radio-button')); await userEvent.type(screen.getByTestId('search-input'), invalidSmartcardNumber); @@ -326,3 +337,12 @@ describe('UserPatientRestrictionsListStage', () => { }, ); }); + +const TestApp = (): React.JSX.Element => { + const [, setSubRoute] = useState(null); + return ; +}; + +const renderPage = (): void => { + render(); +}; diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.tsx index 92654dc58..d14807325 100644 --- a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.tsx +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.tsx @@ -1,13 +1,16 @@ import { Link, useNavigate } from 'react-router-dom'; import useTitle from '../../../../helpers/hooks/useTitle'; import BackButton from '../../../generic/backButton/BackButton'; -import { routeChildren } from '../../../../types/generic/routes'; +import { routeChildren, routes } from '../../../../types/generic/routes'; import { Button, ErrorMessage, Fieldset, Radios, Table, TextInput } from 'nhsuk-react-components'; import { useForm } from 'react-hook-form'; import { InputRef } from '../../../../types/generic/inputRef'; -import { useEffect, useRef, useState } from 'react'; +import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import SpinnerButton from '../../../generic/spinnerButton/SpinnerButton'; -import { UserPatientRestriction } from '../../../../types/generic/userPatientRestriction'; +import { + UserPatientRestriction, + UserPatientRestrictionsSubRoute, +} from '../../../../types/generic/userPatientRestriction'; import { Pagination } from '../../../generic/paginationV2/Pagination'; import SpinnerV2 from '../../../generic/spinnerV2/SpinnerV2'; import { formatNhsNumber } from '../../../../helpers/utils/formatNhsNumber'; @@ -20,10 +23,14 @@ import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; import validateNhsNumber from '../../../../helpers/utils/nhsNumberValidator'; import { isMock } from '../../../../helpers/utils/isLocal'; import { AxiosError } from 'axios'; -import { buildUserRestrictions } from '../../../../helpers/test/testBuilders'; -import { getFormattedPatientFullName } from '../../../../helpers/utils/formatPatientFullName'; +import { buildPatientDetails, buildUserRestrictions } from '../../../../helpers/test/testBuilders'; +import getPatientDetails from '../../../../helpers/requests/getPatientDetails'; +import { usePatientDetailsContext } from '../../../../providers/patientProvider/PatientProvider'; import { PatientDetails } from '../../../../types/generic/patientDetails'; import formatSmartcardNumber from '../../../../helpers/utils/formatSmartcardNumber'; +import { getFormattedPatientFullName } from '../../../../helpers/utils/formatPatientFullName'; +import { ErrorResponse } from '../../../../types/generic/errorResponse'; +import { errorToParams } from '../../../../helpers/utils/errorToParams'; enum Fields { searchType = 'searchType', @@ -40,7 +47,11 @@ type FormData = { [Fields.searchText]: string; }; -const UserPatientRestrictionsListStage = (): React.JSX.Element => { +type Props = { + setSubRoute: Dispatch>; +}; + +const UserPatientRestrictionsListStage = ({ setSubRoute }: Props): React.JSX.Element => { const navigate = useNavigate(); const pageTitle = 'Manage restrictions on access to patient records'; useTitle({ pageTitle }); @@ -244,6 +255,7 @@ const UserPatientRestrictionsListStage = (): React.JSX.Element => { restrictions={restrictions} isLoading={isLoading} failedLoading={failedLoading} + setSubRoute={setSubRoute} /> @@ -299,13 +311,58 @@ type TableRowsProps = { restrictions: UserPatientRestriction[]; isLoading: boolean; failedLoading: boolean; + setSubRoute: Dispatch>; }; const TableRows = ({ restrictions, isLoading, failedLoading, + setSubRoute, }: TableRowsProps): React.JSX.Element => { + const [, setPatientDetails] = usePatientDetailsContext(); const navigate = useNavigate(); + const baseUrl = useBaseAPIUrl(); + const baseHeaders = useBaseAPIHeaders(); + + const [loadingPatient, setLoadingPatient] = useState(false); + + const onViewClicked = async (nhsNumber: string): Promise => { + setLoadingPatient(true); + try { + const patientDetails = await getPatientDetails({ + nhsNumber, + baseUrl, + baseHeaders, + }); + + handleSuccess(patientDetails); + } catch (e) { + const error = e as AxiosError; + const errorResponse = error.response?.data as ErrorResponse; + if (isMock(error)) { + handleSuccess( + buildPatientDetails({ + nhsNumber, + active: true, + }), + ); + } else if (errorResponse?.err_code === 'SP_4006') { + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_RESTRICTED); + } else if (error.response?.status === 403) { + navigate(routes.SESSION_EXPIRED); + } else { + navigate(routes.SERVER_ERROR + errorToParams(error)); + } + } finally { + setLoadingPatient(false); + } + }; + + const handleSuccess = (patientDetails: PatientDetails): void => { + setSubRoute(UserPatientRestrictionsSubRoute.VIEW); + setPatientDetails(patientDetails); + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_VERIFY_PATIENT); + }; if (failedLoading) { return ( @@ -340,21 +397,20 @@ const TableRows = ({ {getFormattedDateFromString(`${restriction.created}`)} - { - e.preventDefault(); - navigate( - routeChildren.USER_PATIENT_RESTRICTIONS_VIEW.replace( - ':restrictionId', - restriction.id, - ), - ); - }} - > - View - + {loadingPatient ? ( + + ) : ( + { + e.preventDefault(); + onViewClicked(restriction.nhsNumber); + }} + > + View + + )} ); diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage.test.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage.test.tsx new file mode 100644 index 000000000..2f1dc27e2 --- /dev/null +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage.test.tsx @@ -0,0 +1,104 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import UserPatientRestrictionsRemoveConfirmStage from './UserPatientRestrictionsRemoveConfirmStage'; +import { buildUserRestrictions } from '../../../../helpers/test/testBuilders'; +import { Mock } from 'vitest'; +import userEvent from '@testing-library/user-event'; +import { routeChildren, routes } from '../../../../types/generic/routes'; +import deleteUserPatientRestriction from '../../../../helpers/requests/userPatientRestrictions/deleteUserPatientRestriction'; +import { AxiosError } from 'axios'; + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + Link: ({ + children, + onClick, + 'data-testid': dataTestId, + }: { + children: React.ReactNode; + onClick?: () => void; + 'data-testid'?: string; + }): React.JSX.Element => ( +
+ {children} +
+ ), + useNavigate: (): Mock => mockNavigate, +})); +vi.mock('../../../../helpers/hooks/useBaseAPIHeaders'); +vi.mock('../../../../helpers/hooks/useBaseAPIUrl'); +vi.mock('../../../../helpers/requests/userPatientRestrictions/deleteUserPatientRestriction'); + +const mockNavigate = vi.fn(); +const mockDeleteUserPatientRestriction = deleteUserPatientRestriction as Mock; + +const mockRestriction = buildUserRestrictions()[0]; +describe('UserPatientRestrictionsRemoveConfirmStage', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockDeleteUserPatientRestriction.mockResolvedValue({}); + }); + + it('renders correctly', () => { + render(); + + expect( + screen.getByText( + `${mockRestriction.restrictedUserFirstName} ${mockRestriction.restrictedUserLastName}`, + ), + ).toBeInTheDocument(); + }); + + it('should call deleteUserPatientRestriction and navigate to complete on confirm', async () => { + render(); + + const confirmButton = screen.getByTestId('confirm-remove-restriction-button'); + await userEvent.click(confirmButton); + + await waitFor(() => { + expect(mockDeleteUserPatientRestriction).toHaveBeenCalledWith({ + restrictionId: mockRestriction.id, + nhsNumber: mockRestriction.nhsNumber, + }); + expect(mockNavigate).toHaveBeenCalledWith( + routeChildren.USER_PATIENT_RESTRICTIONS_REMOVE_COMPLETE, + ); + }); + }); + + it('should navigate to server error on delete failure', async () => { + mockDeleteUserPatientRestriction.mockRejectedValue({ response: { status: 500 } }); + + render(); + + const confirmButton = screen.getByTestId('confirm-remove-restriction-button'); + await userEvent.click(confirmButton); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(expect.stringContaining(routes.SERVER_ERROR)); + }); + }); + + it('should navigate to session expired on 403 error', async () => { + mockDeleteUserPatientRestriction.mockRejectedValue({ response: { status: 403 } }); + + render(); + + const confirmButton = screen.getByTestId('confirm-remove-restriction-button'); + await userEvent.click(confirmButton); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); + }); + }); + + it('should navigate to user patient restrictions list page on cancel', async () => { + render(); + + const cancelButton = screen.getByTestId('cancel-remove-button'); + await userEvent.click(cancelButton); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_LIST); + }); + }); +}); diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage.tsx new file mode 100644 index 000000000..057349b73 --- /dev/null +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage.tsx @@ -0,0 +1,115 @@ +import { Button, Table } from 'nhsuk-react-components'; +import useTitle from '../../../../helpers/hooks/useTitle'; +import { UserPatientRestriction } from '../../../../types/generic/userPatientRestriction'; +import BackButton from '../../../generic/backButton/BackButton'; +import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate'; +import { Link, useNavigate } from 'react-router-dom'; +import deleteUserPatientRestriction from '../../../../helpers/requests/userPatientRestrictions/deleteUserPatientRestriction'; +import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl'; +import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; +import { routeChildren, routes } from '../../../../types/generic/routes'; +import { useState } from 'react'; +import SpinnerButton from '../../../generic/spinnerButton/SpinnerButton'; +import { AxiosError } from 'axios'; +import { isMock } from '../../../../helpers/utils/isLocal'; +import { errorToParams } from '../../../../helpers/utils/errorToParams'; +import formatSmartcardNumber from '../../../../helpers/utils/formatSmartcardNumber'; + +type Props = { + restriction: UserPatientRestriction; +}; + +const UserPatientRestrictionsRemoveConfirmStage = ({ restriction }: Props): React.JSX.Element => { + const navigate = useNavigate(); + const baseUrl = useBaseAPIUrl(); + const baseAPIHeaders = useBaseAPIHeaders(); + + const [removing, setRemoving] = useState(false); + + const pageTitle = 'Are you sure you want to remove this restriction?'; + useTitle({ pageTitle }); + + const confirmRemove = async (): Promise => { + setRemoving(true); + + try { + await deleteUserPatientRestriction({ + restrictionId: restriction.id, + nhsNumber: restriction.nhsNumber, + baseUrl, + baseAPIHeaders, + }); + } catch (e) { + const error = e as AxiosError; + if (!isMock(error)) { + if (error.response?.status === 403) { + navigate(routes.SESSION_EXPIRED); + } else { + navigate(routes.SERVER_ERROR + errorToParams(error)); + } + return; + } + } + + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_REMOVE_COMPLETE); + }; + + return ( + <> + + +

{pageTitle}

+ +

+ If you remove this restriction, staff that were restricted will be able to access + this patient's record again. +

+ +

You are removing the restriction for this staff member:

+ + + + + Staff member + NHS smartcard number + Date restriction added + + + + + + + {restriction.restrictedUserFirstName}{' '} + {restriction.restrictedUserLastName} + + {formatSmartcardNumber(restriction.restrictedUser)} + {getFormattedDateFromString(restriction.created)} + + +
+ + {removing ? ( + + ) : ( +
+ + { + e.preventDefault(); + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_LIST); + }} + className="ml-4" + > + Cancel + +
+ )} + + ); +}; + +export default UserPatientRestrictionsRemoveConfirmStage; diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsVerifyPatientStage/UserPatientRestrictionsVerifyPatientStage.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsVerifyPatientStage/UserPatientRestrictionsVerifyPatientStage.tsx new file mode 100644 index 000000000..b1b006613 --- /dev/null +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsVerifyPatientStage/UserPatientRestrictionsVerifyPatientStage.tsx @@ -0,0 +1,52 @@ +import { Button } from 'nhsuk-react-components'; +import useTitle from '../../../../helpers/hooks/useTitle'; +import { UserPatientRestrictionsSubRoute } from '../../../../types/generic/userPatientRestriction'; +import BackButton from '../../../generic/backButton/BackButton'; +import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary'; + +type Props = { + route: UserPatientRestrictionsSubRoute; + confirmClicked: () => void; +}; + +const UserPatientRestrictionsVerifyPatientStage = ({ + route, + confirmClicked, +}: Props): React.JSX.Element => { + const pageTitle = + route === UserPatientRestrictionsSubRoute.ADD + ? 'Patient details' + : 'Verify patient details to view restrictions'; + useTitle({ pageTitle }); + + return ( + <> + + +

{pageTitle}

+ + + + + + {route === UserPatientRestrictionsSubRoute.ADD && ( + <> + + + + )} + + +

+ This page displays the current data recorded in the Personal Demographics Service + for this patient. +

+ + + + ); +}; + +export default UserPatientRestrictionsVerifyPatientStage; diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.test.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.test.tsx index f2aa2de3b..068be72a9 100644 --- a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.test.tsx +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.test.tsx @@ -1,10 +1,154 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import UserPatientRestrictionsViewStage from './UserPatientRestrictionsViewStage'; +import { UserPatientRestrictionsSubRoute } from '../../../../types/generic/userPatientRestriction'; +import usePatient from '../../../../helpers/hooks/usePatient'; +import { Mock } from 'vitest'; +import { buildPatientDetails, buildUserRestrictions } from '../../../../helpers/test/testBuilders'; +import getUserPatientRestrictions from '../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions'; +import { routeChildren, routes } from '../../../../types/generic/routes'; +import { UIErrorCode } from '../../../../types/generic/errors'; +import userEvent from '@testing-library/user-event'; +import useSmartcardNumber from '../../../../helpers/hooks/useSmartcardNumber'; + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: (): Mock => mockNavigate, + Link: ({ children }: { children: React.ReactNode }): React.JSX.Element => ( +
{children}
+ ), + }; +}); +vi.mock('../../../../helpers/hooks/usePatient'); +vi.mock('../../../../helpers/hooks/useSmartcardNumber'); +vi.mock('../../../../helpers/hooks/useBaseAPIUrl'); +vi.mock('../../../../helpers/hooks/useBaseAPIHeaders'); +vi.mock('../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions'); + +const mockNavigate = vi.fn(); +const mockGetUserPatientRestrictions = getUserPatientRestrictions as Mock; +const mockUsePatient = usePatient as Mock; +const mockUserSmartcardNumber = useSmartcardNumber as Mock; describe('UserPatientRestrictionsViewStage', () => { - it('renders correctly', () => { - render(); + const mockPatient = buildPatientDetails(); + const mockRestrictions = buildUserRestrictions(); + + beforeEach(() => { + vi.resetAllMocks(); + mockUsePatient.mockReturnValue(mockPatient); + mockGetUserPatientRestrictions.mockResolvedValue({ + restrictions: mockRestrictions, + }); + }); + + it('renders correctly', async () => { + renderComponent(); + + await waitFor(() => { + expect( + screen.getByText( + `${mockRestrictions[0].restrictedUserFirstName} ${mockRestrictions[0].restrictedUserLastName}`, + ), + ).toBeInTheDocument(); + }); + }); + + it('displays loading spinner while fetching restrictions', async () => { + mockGetUserPatientRestrictions.mockReturnValueOnce(new Promise(() => {})); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Loading restrictions...')).toBeInTheDocument(); + }); + }); + + it('should not display remove button for restrictions on current user', async () => { + mockUserSmartcardNumber.mockReturnValue(mockRestrictions[0].restrictedUser); + + renderComponent(); + + await waitFor(() => { + const removeButtons = screen.queryByTestId( + `remove-restriction-button-${mockRestrictions[0].id}`, + ); + expect(removeButtons).toBeNull(); + }); + }); - expect(screen.getByText('viewing user patient restrictions')).toBeInTheDocument(); + it('navigates to generic error page on patient access restricted error', async () => { + const mockError = { + response: { + data: { + err_code: 'SP_4006', + }, + }, + }; + mockGetUserPatientRestrictions.mockRejectedValueOnce(mockError); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith( + routes.GENERIC_ERROR + '?errorCode=' + UIErrorCode.PATIENT_ACCESS_RESTRICTED, + { replace: true }, + ); + }); + }); + + it('navigates to session expired page on 403', async () => { + const mockError = { + response: { + status: 403, + }, + }; + mockGetUserPatientRestrictions.mockRejectedValueOnce(mockError); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED); + }); + }); + + it('navigates to session expired page on 403', async () => { + const mockError = { + response: { + status: 500, + }, + }; + mockGetUserPatientRestrictions.mockRejectedValueOnce(mockError); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(expect.stringContaining(routes.SERVER_ERROR)); + }); + }); + + it('navigates to add restriction page on add restriction click', async () => { + renderComponent(); + + const addButton = screen.getByTestId('add-restriction-button'); + await userEvent.click(addButton); + + await waitFor(() => { + expect(mockSetSubRoute).toHaveBeenCalledWith(UserPatientRestrictionsSubRoute.ADD); + expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_ADD); + }); }); }); + +const onRemoveRestriction = vi.fn(); +const mockSetSubRoute = vi.fn(); + +const renderComponent = (): void => { + render( + , + ); +}; diff --git a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.tsx b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.tsx index 545664632..6a8eed276 100644 --- a/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.tsx +++ b/app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage.tsx @@ -1,5 +1,163 @@ -const UserPatientRestrictionsViewStage = (): React.JSX.Element => { - return
viewing user patient restrictions
; +import { AxiosError } from 'axios'; +import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; +import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl'; +import usePatient from '../../../../helpers/hooks/usePatient'; +import getUserPatientRestrictions from '../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions'; +import { isMock } from '../../../../helpers/utils/isLocal'; +import { + UserPatientRestriction, + UserPatientRestrictionsSubRoute, +} from '../../../../types/generic/userPatientRestriction'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; +import { buildUserRestrictions } from '../../../../helpers/test/testBuilders'; +import BackButton from '../../../generic/backButton/BackButton'; +import useTitle from '../../../../helpers/hooks/useTitle'; +import { Button, Table } from 'nhsuk-react-components'; +import PatientSummary from '../../../generic/patientSummary/PatientSummary'; +import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate'; +import useSmartcardNumber from '../../../../helpers/hooks/useSmartcardNumber'; +import { routeChildren, routes } from '../../../../types/generic/routes'; +import { Link, useNavigate } from 'react-router-dom'; +import { errorToParams } from '../../../../helpers/utils/errorToParams'; +import { ErrorResponse } from '../../../../types/generic/errorResponse'; +import { UIErrorCode } from '../../../../types/generic/errors'; +import formatSmartcardNumber from '../../../../helpers/utils/formatSmartcardNumber'; +import SpinnerV2 from '../../../generic/spinnerV2/SpinnerV2'; + +type Props = { + setSubRoute: Dispatch>; + onRemoveRestriction: (restriction: UserPatientRestriction) => void; +}; + +const UserPatientRestrictionsViewStage = ({ + setSubRoute, + onRemoveRestriction, +}: Props): React.JSX.Element => { + const navigate = useNavigate(); + const patientDetails = usePatient(); + const userSmartcardNumber = useSmartcardNumber(); + const baseAPIUrl = useBaseAPIUrl(); + const baseAPIHeaders = useBaseAPIHeaders(); + + const [restrictions, setRestrictions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const mountedRef = useRef(false); + + const pageTitle = 'Restrictions on accessing this patient record'; + useTitle({ pageTitle }); + + useEffect(() => { + if (!isLoading && !mountedRef.current) { + mountedRef.current = true; + loadPatientRestrictions(); + } + }, [isLoading]); + + const loadPatientRestrictions = async (): Promise => { + setIsLoading(true); + try { + const { restrictions } = await getUserPatientRestrictions({ + nhsNumber: patientDetails!.nhsNumber, + baseAPIUrl, + baseAPIHeaders, + limit: 100, + }); + + setRestrictions(restrictions); + } catch (e) { + const error = e as AxiosError; + const errorInfo = error.response?.data as ErrorResponse; + if (isMock(error)) { + setRestrictions(buildUserRestrictions()); + } else if (errorInfo?.err_code === 'SP_4006') { + navigate( + routes.GENERIC_ERROR + '?errorCode=' + UIErrorCode.PATIENT_ACCESS_RESTRICTED, + { replace: true }, + ); + } else if (error.response?.status === 403) { + navigate(routes.SESSION_EXPIRED); + } else { + navigate(routes.SERVER_ERROR + errorToParams(error)); + } + } finally { + setIsLoading(false); + } + }; + + const addRestrictionClicked = (): void => { + setSubRoute(UserPatientRestrictionsSubRoute.ADD); + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_ADD); + }; + + return ( + <> + + +

{pageTitle}

+ + + +

You cannot remove a restriction against your own NHS smartcard number.

+ +

+ You are viewing the restrictions on this patient record: +

+ + + +

+ Staff members restriction from accessing this patient record: +

+ + + + Staff member + NHS smartcard number + Date restriction added + Remove restriction + + + + {isLoading ? ( + + + + + + ) : ( + restrictions.map((restriction) => ( + + + {`${restriction.restrictedUserFirstName} ${restriction.restrictedUserLastName}`} + + + {formatSmartcardNumber(restriction.restrictedUser)} + + + {getFormattedDateFromString(restriction.created)} + + + {userSmartcardNumber !== restriction.restrictedUser ? ( + onRemoveRestriction(restriction)} + > + Remove + + ) : ( + '' + )} + + + )) + )} + +
+ + ); }; export default UserPatientRestrictionsViewStage; diff --git a/app/src/helpers/hooks/useSmartcardNumber.tsx b/app/src/helpers/hooks/useSmartcardNumber.tsx new file mode 100644 index 000000000..92edda8aa --- /dev/null +++ b/app/src/helpers/hooks/useSmartcardNumber.tsx @@ -0,0 +1,12 @@ +import { useSessionContext } from '../../providers/sessionProvider/SessionProvider'; +import { NdrTokenData } from '../../types/generic/ndrTokenData'; +import { decodeJwtToken } from '../utils/jwtDecoder'; + +const useSmartcardNumber = (): string | null => { + const [session] = useSessionContext(); + + const decodedToken = decodeJwtToken(session.auth!.authorisation_token); + return decodedToken ? decodedToken.nhs_user_id : null; +}; + +export default useSmartcardNumber; diff --git a/app/src/helpers/requests/userPatientRestrictions/deleteUserPatientRestriction.ts b/app/src/helpers/requests/userPatientRestrictions/deleteUserPatientRestriction.ts index e69de29bb..f2ba22224 100644 --- a/app/src/helpers/requests/userPatientRestrictions/deleteUserPatientRestriction.ts +++ b/app/src/helpers/requests/userPatientRestrictions/deleteUserPatientRestriction.ts @@ -0,0 +1,32 @@ +import axios, { AxiosError } from 'axios'; +import { AuthHeaders } from '../../../types/blocks/authHeaders'; +import { endpoints } from '../../../types/generic/endpoints'; + +type DeleteUserPatientRestrictionArgs = { + restrictionId: string; + nhsNumber: string; + baseUrl: string; + baseAPIHeaders: AuthHeaders; +}; + +const deleteUserPatientRestriction = async ({ + restrictionId, + nhsNumber, + baseUrl, + baseAPIHeaders, +}: DeleteUserPatientRestrictionArgs): Promise => { + try { + const url = `${baseUrl}/${endpoints.USER_PATIENT_RESTRICTIONS}/${restrictionId}`; + await axios.delete(url, { + headers: baseAPIHeaders, + params: { + nhsNumber, + }, + }); + } catch (e) { + const error = e as AxiosError; + throw error; + } +}; + +export default deleteUserPatientRestriction; diff --git a/app/src/helpers/test/testBuilders.ts b/app/src/helpers/test/testBuilders.ts index 91b81c404..cf33fca06 100644 --- a/app/src/helpers/test/testBuilders.ts +++ b/app/src/helpers/test/testBuilders.ts @@ -26,7 +26,8 @@ import { UserPatientRestriction } from '../../types/generic/userPatientRestricti const buildUserAuth = (userAuthOverride?: Partial): UserAuth => { const auth: UserAuth = { role: REPOSITORY_ROLE.GP_ADMIN, - authorisation_token: '111xxx222', + authorisation_token: + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzM3NDA1MjUsImlzcyI6Im5ocyByZXBvIiwic21hcnRfY2FyZF9yb2xlIjoiUjgwMTMiLCJzZWxlY3RlZF9vcmdhbmlzYXRpb24iOnsibmFtZSI6IkNBUEVMRklFTEQgU1VSR0VSWSIsIm9yZ19vZHNfY29kZSI6Ikg4MTEwOSIsInJvbGVfY29kZSI6IlJPNzYiLCJpY2Jfb2RzX2NvZGUiOiI5MkEifSwicmVwb3NpdG9yeV9yb2xlIjoiR1BfQURNSU4iLCJuZHJfc2Vzc2lvbl9pZCI6IjZjNmZjNWNlLTU1MzAtNGNmMS04MzcxLTM1M2E5OTZiYWM3NyIsIm5oc191c2VyX2lkIjoiMTIzNDU2Nzg5MDEyIn0.Nb0cIIFSNjL-zIAlYFnkFOWK3Ywh1X4XXfT8lcyWQGdFJ4x2_3K85u21Al-_xbO6xfTxHS29O6ggeaA_0nJ5EU2AE_xnIJnMs4E536avxDetHa3Hdg01ifsItzLgY8ET70I-C-7yn23GtcK8FSAYdz_1NN46m0Rg4ne_u6cI28GvzQRMZtQp2uANXcaOgB9yLMre5JC_su_oIylivmJGAQG3C7Akp-7w27thCRA1-OSMznC9LIQzMG4Ow-3c8QDrQeeqZiej-5yAlhquMe77S89oTCMcElREkChLqBpTgbzh9Ce84kR9RXFmeTNckL0_iRvU9XylMZnNKTho5Oiue0204DOrFMgAyRDxsxxUaUuIoh2XqeksNvjh5yNvimb7VBeDMYx4v77gfjYJIaHzRY-haHHDigR21na3DQeluiCYSRM-jSg1km3vTGmCyVRcZQTjvQ_lQ-XvKCG0VXzSHubKVbtZS_9UdkNM2gD4gnnxDxHPqe8EX917yE0pItFDNZYOq8NzKJrCV7QOa2zE9zo4dqnmacyNsqvdsF4_g46kGUIXi0jQQgcFtv3ttlLcwwAaR0EjsC6Hf56uVu4AfTyqq8PiOfjMrym-ENQV2AaiH4Pr_35SUdJUs5uywCSB5xVsfWZ-yC3W6nVWR9PIAiaSbZ5nlH19qWESc632N3A', ...userAuthOverride, }; return auth; @@ -261,7 +262,7 @@ const buildUserRestrictions = (patientCount?: number): UserPatientRestriction[] nhsNumber: '9000000009', patientGivenName: ['John'], patientFamilyName: 'Doe', - restrictedUser: '123456789012', + restrictedUser: '123456789013', restrictedUserFirstName: 'John', restrictedUserLastName: 'Smith', created: '2024-01-01T12:00:00Z', diff --git a/app/src/helpers/utils/errorCodes.ts b/app/src/helpers/utils/errorCodes.ts index 0f05528d4..7a9b4b942 100644 --- a/app/src/helpers/utils/errorCodes.ts +++ b/app/src/helpers/utils/errorCodes.ts @@ -40,6 +40,7 @@ const errorCodes: { [key: string]: string } = { SP_4002: 'The NHS number entered could not be found in the Personal Demographics Service', SP_4003: "You cannot access this patient's record because they are not registered at your practice. The patient's current practice can access this record if it's stored in this service.", + SP_4006: 'You are restricted from accessing this patient record.', UC_4002: 'There was an issue when attempting to virus scan your uploaded files', UC_4004: technicalIssueMsg, UC_4006: diff --git a/app/src/pages/genericErrorPage/GenericErrorPage.tsx b/app/src/pages/genericErrorPage/GenericErrorPage.tsx new file mode 100644 index 000000000..8d360d7fc --- /dev/null +++ b/app/src/pages/genericErrorPage/GenericErrorPage.tsx @@ -0,0 +1,45 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import BackButton from '../../components/generic/backButton/BackButton'; +import { Button } from 'nhsuk-react-components'; +import { routes } from '../../types/generic/routes'; +import { UIErrors, UIErrorCode } from '../../types/generic/errors'; +import useTitle from '../../helpers/hooks/useTitle'; + +const GenericErrorPage = (): React.JSX.Element => { + const navigate = useNavigate(); + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const errorCode = searchParams.get('errorCode') as UIErrorCode; + + const error = UIErrors[errorCode]; + const pageTitle = error?.title ?? 'An error has occurred'; + useTitle({ pageTitle }); + + if (!error) { + navigate(routes.HOME); + return <>; + } + + return ( + <> + + +

{error.title}

+ + {error.messageParagraphs.map((msg, index) => ( +

{msg}

+ ))} + + + + ); +}; + +export default GenericErrorPage; diff --git a/app/src/pages/userPatientRestrictionsPage/UserPatientRestrictionsPage.tsx b/app/src/pages/userPatientRestrictionsPage/UserPatientRestrictionsPage.tsx index fa087dd9b..476ec9fa6 100644 --- a/app/src/pages/userPatientRestrictionsPage/UserPatientRestrictionsPage.tsx +++ b/app/src/pages/userPatientRestrictionsPage/UserPatientRestrictionsPage.tsx @@ -1,4 +1,4 @@ -import { JSX, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { routeChildren, routes } from '../../types/generic/routes'; import useConfig from '../../helpers/hooks/useConfig'; import { Outlet, Route, Routes, useNavigate } from 'react-router-dom'; @@ -7,8 +7,15 @@ import { getLastURLPath } from '../../helpers/utils/urlManipulations'; import UserPatientRestrictionsListStage from '../../components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage'; import UserPatientRestrictionsViewStage from '../../components/blocks/_userPatientRestrictions/userPatientRestrictionsViewStage/UserPatientRestrictionsViewStage'; import UserPatientRestrictionsAddStage from '../../components/blocks/_userPatientRestrictions/userPatientRestrictionsAddStage/UserPatientRestrictionsAddStage'; +import { + UserPatientRestriction, + UserPatientRestrictionsSubRoute, +} from '../../types/generic/userPatientRestriction'; +import UserPatientRestrictionsRemoveConfirmStage from '../../components/blocks/_userPatientRestrictions/userPatientRestrictionsRemoveConfirmStage/UserPatientRestrictionsRemoveConfirmStage'; +import UserPatientRestrictionsVerifyPatientStage from '../../components/blocks/_userPatientRestrictions/userPatientRestrictionsVerifyPatientStage/UserPatientRestrictionsVerifyPatientStage'; +import UserPatientRestrictionsCompleteStage from '../../components/blocks/_userPatientRestrictions/userPatientRestrictionsCompleteStage/UserPatientRestrictionsCompleteStage'; -const UserPatientRestrictionsPage = (): JSX.Element => { +const UserPatientRestrictionsPage = (): React.JSX.Element => { const config = useConfig(); const navigate = useNavigate(); @@ -18,10 +25,31 @@ const UserPatientRestrictionsPage = (): JSX.Element => { } }, [config.featureFlags.userRestrictionEnabled, navigate]); + const [subRoute, setSubRoute] = useState(null); + const [restrictionToRemove, setRestrictionToRemove] = useState( + null, + ); + if (!config.featureFlags.userRestrictionEnabled) { return <>; } + const confirmVerifyPatientDetails = (): void => { + if (subRoute === UserPatientRestrictionsSubRoute.ADD) { + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_ADD); + } else if (subRoute === UserPatientRestrictionsSubRoute.VIEW) { + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_VIEW); + } + }; + + const onRemoveRestriction = (restriction: UserPatientRestriction): void => { + setRestrictionToRemove(restriction); + setSubRoute(UserPatientRestrictionsSubRoute.REMOVE); + setTimeout(() => { + navigate(routeChildren.USER_PATIENT_RESTRICTIONS_REMOVE_CONFIRM); + }, 2); + }; + return ( <> @@ -29,12 +57,41 @@ const UserPatientRestrictionsPage = (): JSX.Element => { } + element={} + /> + + + } /> } + element={ + + } + /> + + + } + /> + + } /> , type: ROUTE_TYPE.PUBLIC, }, + [GENERIC_ERROR]: { + page: , + type: ROUTE_TYPE.PUBLIC, + }, [SESSION_EXPIRED]: { page: , type: ROUTE_TYPE.PUBLIC, diff --git a/app/src/styles/App.scss b/app/src/styles/App.scss index 4cab5b1ef..8cfda2bbd 100644 --- a/app/src/styles/App.scss +++ b/app/src/styles/App.scss @@ -1292,6 +1292,19 @@ progress:not(.continuous-progress-bar) { color: #757575; } +.inline-block { + display: inline-block; +} + +.action-button-group { + display: flex; + align-items: center; + + .nhsuk-button { + margin-bottom: 0; + } + } + @import '../components/blocks/_patientDocuments/documentSearchResults/DocumentSearchResults.scss'; @import '../components/blocks/_documentManagement/documentUploadCompleteStage/DocumentUploadCompleteStage.scss'; @import '../components/blocks/_reviews/reviewsPageIndex/ReviewsPageIndex.scss'; diff --git a/app/src/types/generic/errors.ts b/app/src/types/generic/errors.ts index d96ea4cf0..7e6b6ff92 100644 --- a/app/src/types/generic/errors.ts +++ b/app/src/types/generic/errors.ts @@ -33,3 +33,39 @@ class StitchRecordError extends Error { } export { StitchRecordError }; + +export enum UIErrorCode { + USER_PATIENT_RESTRICTIONS_SELF_ADD = 'UPR01', + PATIENT_NOT_REGISTERED_AT_YOUR_PRACTICE = 'PA001', + PATIENT_ACCESS_RESTRICTED = 'PA002', +} + +export type UIErrorContent = { + title: string; + messageParagraphs: string[]; +}; + +export const UIErrors: Record = { + // self add restriction error + [UIErrorCode.USER_PATIENT_RESTRICTIONS_SELF_ADD]: { + title: 'You cannot add a restriction on your own NHS smartcard number', + messageParagraphs: [ + 'If you need to add a patient restriction on your own NHS smartcard number, another member of staff at your practice will need to do this.', + ], + }, + // patient not registered at your practice error + [UIErrorCode.PATIENT_NOT_REGISTERED_AT_YOUR_PRACTICE]: { + title: 'The patient is not registered at your practice', + messageParagraphs: [ + "You cannot perform this action because this patient is not registered at your practice. The patient's current practice can access and manage this record.", + ], + }, + // patient access restricted error + [UIErrorCode.PATIENT_ACCESS_RESTRICTED]: { + title: 'You cannot access this patient record', + messageParagraphs: [ + "A member of staff at your practice has restricted your access to this patient's record.", + 'You cannot remove this restriction. If you think this is a mistake, speak to your practice manager.', + ], + }, +}; diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index 1a93df797..d8f541390 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -10,6 +10,7 @@ export enum routes { AUTH_ERROR = '/auth-error', UNAUTHORISED_LOGIN = '/unauthorised-login', SERVER_ERROR = '/server-error', + GENERIC_ERROR = '/error', SESSION_EXPIRED = '/session-expired', PRIVACY_POLICY = '/privacy-policy', LOGOUT = '/logout', @@ -105,11 +106,12 @@ export enum routeChildren { USER_PATIENT_RESTRICTIONS_ADD = '/user-patient-restrictions/add', USER_PATIENT_RESTRICTIONS_CONFIRM_ADD = '/user-patient-restrictions/confirm-add', - USER_PATIENT_RESTRICTIONS_VIEW = '/user-patient-restrictions/:restrictionId', + USER_PATIENT_RESTRICTIONS_VIEW = '/user-patient-restrictions/view', USER_PATIENT_RESTRICTIONS_LIST = '/user-patient-restrictions/list', USER_PATIENT_RESTRICTIONS_VERIFY_PATIENT = '/user-patient-restrictions/verify-patient', USER_PATIENT_RESTRICTIONS_VERIFY_STAFF = '/user-patient-restrictions/verify-staff', USER_PATIENT_RESTRICTIONS_RESTRICTED = '/user-patient-restrictions/restricted', + USER_PATIENT_RESTRICTIONS_REMOVE_CONFIRM = '/user-patient-restrictions/remove-confirm', USER_PATIENT_RESTRICTIONS_REMOVE_COMPLETE = '/user-patient-restrictions/remove-complete', USER_PATIENT_RESTRICTIONS_CANCEL = '/user-patient-restrictions/add-cancel', } diff --git a/app/src/types/generic/userPatientRestriction.ts b/app/src/types/generic/userPatientRestriction.ts index 30c26482a..cfbb6f152 100644 --- a/app/src/types/generic/userPatientRestriction.ts +++ b/app/src/types/generic/userPatientRestriction.ts @@ -8,3 +8,9 @@ export type UserPatientRestriction = { restrictedUserLastName: string; created: string; }; + +export enum UserPatientRestrictionsSubRoute { + ADD = 'add', + VIEW = 'view', + REMOVE = 'remove', +}