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.
+
+ {
+ navigate(routes.USER_PATIENT_RESTRICTIONS);
+ }}
+ >
+ Go to view restrictions
+
+ >
+ );
+};
+
+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 ? (
+
+ ) : (
+
+
+ Remove this restriction
+
+ {
+ 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.
+
+
+
+ Confirm patient details and continue
+
+ >
+ );
+};
+
+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}
+
+
+ Add a restriction to this patient record
+
+
+ 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}
+ ))}
+
+ {
+ navigate(routes.HOME);
+ }}
+ >
+ Go to home
+
+ >
+ );
+};
+
+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',
+}