Skip to content

Commit 2e8a12f

Browse files
[PRMP-1480] Implement add restriction patient search
1 parent 669d7e1 commit 2e8a12f

File tree

23 files changed

+547
-44
lines changed

23 files changed

+547
-44
lines changed

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsAddStage/UserPatientRestrictionsAddStage.test.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsAddStage/UserPatientRestrictionsAddStage.tsx

Lines changed: 0 additions & 5 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.user-patient-restrictions-existing-stage {
2+
.action-buttons {
3+
display: flex;
4+
align-items: center;
5+
6+
.continue-button {
7+
margin-bottom: 0;
8+
}
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import UserPatientRestrictionsExistingStage from './UserPatientRestrictionsExistingStage';
3+
import { Mock } from 'vitest';
4+
import getUserPatientRestrictions from '../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions';
5+
import usePatient from '../../../../helpers/hooks/usePatient';
6+
import { buildPatientDetails, buildUserRestrictions } from '../../../../helpers/test/testBuilders';
7+
import { routeChildren, routes } from '../../../../types/generic/routes';
8+
import { UserPatientRestriction } from '../../../../types/generic/userPatientRestriction';
9+
import userEvent from '@testing-library/user-event';
10+
11+
vi.mock('react-router-dom', async () => {
12+
const actual = await vi.importActual('react-router-dom');
13+
return {
14+
...actual,
15+
useNavigate: (): Mock => mockNavigate,
16+
Link: ({ children }: { children: React.ReactNode }): React.JSX.Element => (
17+
<div>{children}</div>
18+
),
19+
};
20+
});
21+
vi.mock('../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions');
22+
vi.mock('../../../../helpers/hooks/usePatient');
23+
vi.mock('../../../../helpers/hooks/useBaseAPIHeaders');
24+
vi.mock('../../../../helpers/hooks/useBaseAPIUrl');
25+
26+
const mockNavigate = vi.fn();
27+
const mockGetUserPatientRestrictions = getUserPatientRestrictions as Mock;
28+
const mockUsePatient = usePatient as Mock;
29+
const setExistingRestrictions = vi.fn();
30+
31+
describe('UserPatientRestrictionsExistingStage', () => {
32+
const mockRestrictions = buildUserRestrictions();
33+
const mockPatient = buildPatientDetails();
34+
35+
beforeEach(() => {
36+
vi.resetAllMocks();
37+
mockGetUserPatientRestrictions.mockResolvedValue({ restrictions: mockRestrictions });
38+
mockUsePatient.mockReturnValue(mockPatient);
39+
});
40+
41+
it('renders the page correctly', async () => {
42+
renderComponent(mockRestrictions);
43+
44+
await waitFor(() => {
45+
expect(mockGetUserPatientRestrictions).toHaveBeenCalledWith(
46+
expect.objectContaining({
47+
nhsNumber: mockPatient.nhsNumber,
48+
limit: 100,
49+
}),
50+
);
51+
expect(setExistingRestrictions).toHaveBeenCalledWith(mockRestrictions);
52+
expect(
53+
screen.getByText(
54+
`${mockRestrictions[0].restrictedUserFirstName} ${mockRestrictions[0].restrictedUserLastName}`,
55+
),
56+
).toBeInTheDocument();
57+
});
58+
});
59+
60+
it('navigates to search staff if no restrictions found', async () => {
61+
mockGetUserPatientRestrictions.mockResolvedValue({ restrictions: [] });
62+
63+
renderComponent();
64+
65+
await waitFor(() => {
66+
expect(mockNavigate).toHaveBeenCalledWith(
67+
routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_STAFF,
68+
{ replace: true },
69+
);
70+
});
71+
});
72+
73+
it('navigates to session expired if 403 error thrown', async () => {
74+
mockGetUserPatientRestrictions.mockRejectedValue({ response: { status: 403 } });
75+
76+
renderComponent();
77+
78+
await waitFor(() => {
79+
expect(mockNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED);
80+
});
81+
});
82+
83+
it('navigates to server error if non-403 error thrown', async () => {
84+
mockGetUserPatientRestrictions.mockRejectedValue({ response: { status: 500 } });
85+
86+
renderComponent();
87+
88+
await waitFor(() => {
89+
expect(mockNavigate).toHaveBeenCalledWith(expect.stringContaining(routes.SERVER_ERROR));
90+
});
91+
});
92+
93+
it('shows loading state while fetching restrictions', async () => {
94+
mockGetUserPatientRestrictions.mockReturnValue(new Promise(() => {}));
95+
96+
renderComponent();
97+
98+
expect(screen.getByText('Loading...')).toBeInTheDocument();
99+
});
100+
101+
it('navigates to verify patient on continue clicked', async () => {
102+
renderComponent(mockRestrictions);
103+
104+
let continueButton;
105+
await waitFor(() => {
106+
continueButton = screen.getByTestId('add-restriction-button');
107+
expect(continueButton).toBeInTheDocument();
108+
});
109+
await userEvent.click(continueButton!);
110+
111+
await waitFor(() => {
112+
expect(mockNavigate).toHaveBeenCalledWith(
113+
routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_STAFF,
114+
);
115+
});
116+
});
117+
});
118+
119+
const renderComponent = (existingRestrictions: UserPatientRestriction[] = []): void => {
120+
render(
121+
<UserPatientRestrictionsExistingStage
122+
existingRestrictions={existingRestrictions}
123+
setExistingRestrictions={setExistingRestrictions}
124+
/>,
125+
);
126+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Button, Table } from 'nhsuk-react-components';
2+
import useTitle from '../../../../helpers/hooks/useTitle';
3+
import BackButton from '../../../generic/backButton/BackButton';
4+
import PatientSummary from '../../../generic/patientSummary/PatientSummary';
5+
import { UserPatientRestriction } from '../../../../types/generic/userPatientRestriction';
6+
import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate';
7+
import { Link, useNavigate } from 'react-router-dom';
8+
import { routeChildren, routes } from '../../../../types/generic/routes';
9+
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
10+
import getUserPatientRestrictions from '../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions';
11+
import usePatient from '../../../../helpers/hooks/usePatient';
12+
import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders';
13+
import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl';
14+
import { AxiosError } from 'axios';
15+
import { isMock } from '../../../../helpers/utils/isLocal';
16+
import { buildUserRestrictions } from '../../../../helpers/test/testBuilders';
17+
import { errorToParams } from '../../../../helpers/utils/errorToParams';
18+
import Spinner from '../../../generic/spinner/Spinner';
19+
import formatSmartcardNumber from '../../../../helpers/utils/formatSmartcardNumber';
20+
21+
type Props = {
22+
existingRestrictions: UserPatientRestriction[];
23+
setExistingRestrictions: Dispatch<SetStateAction<UserPatientRestriction[]>>;
24+
};
25+
26+
const UserPatientRestrictionsExistingStage = ({
27+
existingRestrictions,
28+
setExistingRestrictions,
29+
}: Props): React.JSX.Element => {
30+
const navigate = useNavigate();
31+
const pageTitle = 'Existing restrictions on this patient record';
32+
useTitle({ pageTitle });
33+
const patient = usePatient();
34+
const baseAPIUrl = useBaseAPIUrl();
35+
const baseAPIHeaders = useBaseAPIHeaders();
36+
37+
const [isLoading, setIsLoading] = useState<boolean>(true);
38+
const mountedRef = useRef(false);
39+
40+
const loadRestrictions = async (): Promise<void> => {
41+
try {
42+
const { restrictions } = await getUserPatientRestrictions({
43+
nhsNumber: patient?.nhsNumber || '',
44+
baseAPIUrl,
45+
baseAPIHeaders,
46+
limit: 100,
47+
});
48+
49+
if (restrictions.length === 0) {
50+
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_STAFF, { replace: true });
51+
return;
52+
}
53+
54+
setExistingRestrictions(restrictions);
55+
} catch (e) {
56+
const error = e as AxiosError;
57+
if (isMock(error)) {
58+
setExistingRestrictions(buildUserRestrictions());
59+
} else if (error.response?.status === 403) {
60+
navigate(routes.SESSION_EXPIRED);
61+
} else {
62+
navigate(routes.SERVER_ERROR + errorToParams(error));
63+
}
64+
} finally {
65+
setIsLoading(false);
66+
}
67+
};
68+
69+
useEffect(() => {
70+
if (!mountedRef.current) {
71+
mountedRef.current = true;
72+
loadRestrictions();
73+
}
74+
}, []);
75+
76+
return (
77+
<>
78+
<BackButton />
79+
80+
{isLoading ? (
81+
<Spinner status="Loading..." />
82+
) : (
83+
<div className="user-patient-restrictions-existing-stage">
84+
<h1>{pageTitle}</h1>
85+
86+
<h3>This patient has existing restrictions on their record:</h3>
87+
88+
<PatientSummary oneLine />
89+
90+
<h3 className="mt-5 inline-block">
91+
Staff members restricted from accessing this patient record:
92+
</h3>
93+
94+
<Table responsive>
95+
<Table.Head>
96+
<Table.Row>
97+
<Table.Cell>Staff member</Table.Cell>
98+
<Table.Cell>NHS smartcard number</Table.Cell>
99+
<Table.Cell>Date restriction added</Table.Cell>
100+
</Table.Row>
101+
</Table.Head>
102+
<Table.Body>
103+
{existingRestrictions.map((restriction) => (
104+
<Table.Row key={restriction.id}>
105+
<Table.Cell>
106+
{`${restriction.restrictedUserFirstName} ${restriction.restrictedUserLastName}`}
107+
</Table.Cell>
108+
<Table.Cell>
109+
{formatSmartcardNumber(restriction.restrictedUser)}
110+
</Table.Cell>
111+
<Table.Cell>
112+
{getFormattedDateFromString(restriction.created)}
113+
</Table.Cell>
114+
</Table.Row>
115+
))}
116+
</Table.Body>
117+
</Table>
118+
119+
<div className="action-buttons">
120+
<Button
121+
className="continue-button"
122+
data-testid="add-restriction-button"
123+
onClick={(): void => {
124+
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_STAFF);
125+
}}
126+
>
127+
Continue to add a restriction
128+
</Button>
129+
<Link className="ml-4" to={routes.HOME}>
130+
Cancel without adding a restriction
131+
</Link>
132+
</div>
133+
</div>
134+
)}
135+
</>
136+
);
137+
};
138+
139+
export default UserPatientRestrictionsExistingStage;

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsIndex/UserPatientRestrictionsIndex.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ vi.mock('react-router-dom', async () => {
1818
const mockNavigate = vi.fn();
1919

2020
const renderComponent = (): void => {
21-
render(<UserPatientRestrictionsIndex />);
21+
render(<UserPatientRestrictionsIndex setSubRoute={vi.fn()} />);
2222
};
2323

2424
describe('UserRestrictionsPage', (): void => {
@@ -62,7 +62,9 @@ describe('UserRestrictionsPage', (): void => {
6262

6363
it('navigates to add restriction page when add restriction button is clicked', (): void => {
6464
screen.getByTestId('add-user-restriction-btn').click();
65-
expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
65+
expect(mockNavigate).toHaveBeenCalledWith(
66+
routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_PATIENT,
67+
);
6668
});
6769

6870
it('navigates to view restrictions page when view restrictions button is clicked', (): void => {

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsIndex/UserPatientRestrictionsIndex.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { Card } from 'nhsuk-react-components';
2-
import { JSX } from 'react';
2+
import { Dispatch, JSX, SetStateAction } from 'react';
33
import useTitle from '../../../../helpers/hooks/useTitle';
44
import { ReactComponent as RightCircleIcon } from '../../../../styles/right-chevron-circle.svg';
55
import BackButton from '../../../generic/backButton/BackButton';
66
import { routeChildren, routes } from '../../../../types/generic/routes';
77
import { useNavigate } from 'react-router-dom';
8+
import { UserPatientRestrictionsSubRoute } from '../../../../types/generic/userPatientRestriction';
89

9-
const UserPatientRestrictionsIndex = (): JSX.Element => {
10+
type Props = {
11+
setSubRoute: Dispatch<SetStateAction<UserPatientRestrictionsSubRoute | null>>;
12+
};
13+
14+
const UserPatientRestrictionsIndex = ({ setSubRoute }: Props): JSX.Element => {
1015
const navigate = useNavigate();
1116
useTitle({ pageTitle: 'Restrict staff from accessing patient records' });
1217

@@ -21,10 +26,13 @@ const UserPatientRestrictionsIndex = (): JSX.Element => {
2126
<Card.Heading className="nhsuk-heading-m">
2227
<Card.Link
2328
data-testid="add-user-restriction-btn"
24-
href="#"
29+
href={routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_PATIENT}
2530
onClick={(e): void => {
2631
e.preventDefault();
27-
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
32+
setSubRoute(UserPatientRestrictionsSubRoute.ADD);
33+
navigate(
34+
routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_PATIENT,
35+
);
2836
}}
2937
>
3038
Add a restriction
@@ -43,7 +51,7 @@ const UserPatientRestrictionsIndex = (): JSX.Element => {
4351
<Card.Heading className="nhsuk-heading-m">
4452
<Card.Link
4553
data-testid="view-user-restrictions-btn"
46-
href="#"
54+
href={routeChildren.USER_PATIENT_RESTRICTIONS_LIST}
4755
onClick={(e): void => {
4856
e.preventDefault();
4957
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_LIST);

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ describe('UserPatientRestrictionsListStage', () => {
7171

7272
await userEvent.click(addRestrictionButton);
7373

74-
expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
74+
expect(mockNavigate).toHaveBeenCalledWith(
75+
routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_PATIENT,
76+
);
7577
});
7678

7779
it('should navigate to view restrictions stage when view restriction button is clicked', async () => {

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsListStage/UserPatientRestrictionsListStage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ const UserPatientRestrictionsListStage = ({ setSubRoute }: Props): React.JSX.Ele
184184
<Button
185185
onClick={(e: React.MouseEvent<HTMLButtonElement>): void => {
186186
e.preventDefault();
187-
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
187+
setSubRoute(UserPatientRestrictionsSubRoute.ADD);
188+
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_SEARCH_PATIENT);
188189
}}
189190
>
190191
Add a restriction

0 commit comments

Comments
 (0)