diff --git a/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx index 16db9f6b9..f89b88d45 100644 --- a/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx +++ b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.test.tsx @@ -349,11 +349,24 @@ describe('DocumentView', () => { it('calls removeDocument when remove action is triggered', () => { renderComponent(); - // Assuming the first record link is remove action + // assume first link is remove const removeRecordLink = screen.getByTestId(lloydGeorgeRecordLinks[0].key); fireEvent.click(removeRecordLink); expect(mockRemoveDocument).toHaveBeenCalled(); }); + + it('navigates to download success page when download action is triggered', () => { + vi.useFakeTimers(); + renderComponent(); + + // assume second link is download + const downloadRecordLink = screen.getByTestId(lloydGeorgeRecordLinks[1].key); + fireEvent.click(downloadRecordLink); + + vi.advanceTimersByTime(5000000); + expect(mockUseNavigate).toHaveBeenCalledWith(routes.DOWNLOAD_COMPLETE); + vi.useRealTimers(); + }); }); describe('Role-based rendering', () => { diff --git a/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx index 91e060a72..71eac227f 100644 --- a/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx +++ b/app/src/components/blocks/_patientDocuments/documentView/DocumentView.tsx @@ -70,12 +70,12 @@ const DocumentView = ({ const details = (): React.JSX.Element => { return ( -
-
-
+
+
+
Filename: {documentReference.fileName}
-
+
Last updated: {getFormattedDate(new Date(documentReference.created))}
@@ -85,12 +85,18 @@ const DocumentView = ({ const downloadClicked = (): void => { if (documentReference.url) { + const estimatedDownloadDuration = + Math.floor(documentReference.fileSize / 5000000 * 1000); // Estimate 5MB/s download speed const anchor = document.createElement('a'); anchor.href = documentReference.url; anchor.download = documentReference.fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove(); + + setTimeout(() => { + navigate(routes.DOWNLOAD_COMPLETE); + }, estimatedDownloadDuration); } }; @@ -182,10 +188,10 @@ const DocumentView = ({ return session.isFullscreen ? ( card ) : ( -
+
{card}
@@ -200,7 +206,7 @@ const DocumentView = ({ (role === REPOSITORY_ROLE.GP_ADMIN || role === REPOSITORY_ROLE.GP_CLINICAL); return ( -
+
{session.isFullscreen && (
diff --git a/app/src/pages/downloadCompletePage/DownloadCompletePage.test.tsx b/app/src/pages/downloadCompletePage/DownloadCompletePage.test.tsx new file mode 100644 index 000000000..5cafb7253 --- /dev/null +++ b/app/src/pages/downloadCompletePage/DownloadCompletePage.test.tsx @@ -0,0 +1,70 @@ +import { buildPatientDetails } from '../../helpers/test/testBuilders'; +import { render, screen } from '@testing-library/react'; +import { runAxeTest } from '../../helpers/test/axeTestHelper'; +import { afterEach, beforeEach, describe, expect, it, vi, Mock } from 'vitest'; +import DownloadCompletePage from './DownloadCompletePage'; +import userEvent from '@testing-library/user-event'; +import { routes } from '../../types/generic/routes'; +import usePatient from '../../helpers/hooks/usePatient'; + +vi.mock('../../helpers/hooks/usePatient'); + +const mockedUseNavigate = vi.fn(); +const mockUsePatient = usePatient as Mock; + +vi.mock('react-router-dom', () => ({ + useNavigate: () => mockedUseNavigate, +})); + +describe('DownloadCompletePage', () => { + const mockPatient = buildPatientDetails(); + + beforeEach(() => { + import.meta.env.VITE_ENVIRONMENT = 'vitest'; + mockUsePatient.mockReturnValue(mockPatient); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders the download complete screen', () => { + render(); + + expect(screen.getByTestId('page-title')).toBeInTheDocument(); + expect( + screen.getByText(`Patient name: ${mockPatient.familyName}, ${mockPatient.givenName}`), + ).toBeInTheDocument(); + expect(screen.getByText('Your responsibilities with this record')).toBeInTheDocument(); + expect( + screen.getByText('Follow the Record Management Code of Practice'), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { + name: 'Go to home', + }), + ).toBeInTheDocument(); + }); + + it('navigates to the home screen when go to home is clicked', async () => { + render(); + + await userEvent.click(screen.getByRole('button', {name: 'Go to home'})); + + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + }); + + it('navigates to the home screen if patient details are undefined', async () => { + mockUsePatient.mockReturnValue(undefined); + render(); + + expect(mockedUseNavigate).toHaveBeenCalledWith(routes.HOME); + }); + + describe('Accessibility', () => { + it('passes accessibility checks', async () => { + render(); + const results = await runAxeTest(document.body); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/app/src/pages/downloadCompletePage/DownloadCompletePage.tsx b/app/src/pages/downloadCompletePage/DownloadCompletePage.tsx new file mode 100644 index 000000000..a0f3668d5 --- /dev/null +++ b/app/src/pages/downloadCompletePage/DownloadCompletePage.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Button } from 'nhsuk-react-components'; +import useTitle from '../../helpers/hooks/useTitle'; +import { useNavigate } from 'react-router-dom'; +import { routes } from '../../types/generic/routes'; +import usePatient from '../../helpers/hooks/usePatient'; +import { formatNhsNumber } from '../../helpers/utils/formatNhsNumber'; +import { getFormattedDateFromString } from '../../helpers/utils/formatDate'; +import { getFormattedPatientFullName } from '../../helpers/utils/formatPatientFullName'; + +const DownloadCompletePage = (): React.JSX.Element => { + const navigate = useNavigate(); + const patient = usePatient(); + + const pageHeader = 'Download complete'; + useTitle({ pageTitle: pageHeader }); + + if (!patient) { + navigate(routes.HOME); + return <>; + } + + return ( +
+
+

+ Download complete +

+
+
+ Patient name: {getFormattedPatientFullName(patient)} +
+ NHS number: {formatNhsNumber(patient.nhsNumber)} +
+ Date of birth: {getFormattedDateFromString(patient.birthDate)} +
+
+ +

Your responsibilities with this record

+

+ Everyone in a health and care organisation is responsible for managing records + appropriately. It is important all general practice staff understand their + responsibilities for creating, maintaining, and disposing of records appropriately. +

+ +

Follow the Record Management Code of Practice

+

+ The{' '} + + Record Management Code of Practice + {' '} + provides a framework for consistent and effective records management, based on + established standards. +

+ + +
+ ); +}; + +export default DownloadCompletePage; diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx index dac5f9226..e2a9e7777 100644 --- a/app/src/router/AppRouter.tsx +++ b/app/src/router/AppRouter.tsx @@ -28,6 +28,7 @@ import PatientAccessAuditPage from '../pages/patientAccessAuditPage/PatientAcces import MockLoginPage from '../pages/mockLoginPage/MockLoginPage'; import DocumentUploadPage from '../pages/documentUploadPage/DocumentUploadPage'; import AdminRoutesPage from '../pages/adminRoutesPage/AdminRoutesPage'; +import DownloadCompletePage from '../pages/downloadCompletePage/DownloadCompletePage'; const { START, @@ -58,6 +59,7 @@ const { DOCUMENT_UPLOAD_WILDCARD, ADMIN_ROUTE, ADMIN_ROUTE_WILDCARD, + DOWNLOAD_COMPLETE, } = routes; type Routes = { @@ -247,7 +249,7 @@ export const routeMap: Routes = { type: ROUTE_TYPE.PRIVATE, }, - // App guard routes + // Patient guard routes [VERIFY_PATIENT]: { page: , type: ROUTE_TYPE.PATIENT, @@ -286,6 +288,10 @@ export const routeMap: Routes = { page: , type: ROUTE_TYPE.PATIENT, }, + [DOWNLOAD_COMPLETE]: { + page: , + type: ROUTE_TYPE.PATIENT, + }, }; const createRoutesFromType = (routeType: ROUTE_TYPE): Array => diff --git a/app/src/styles/App.scss b/app/src/styles/App.scss index 40524cb8a..2178ff38c 100644 --- a/app/src/styles/App.scss +++ b/app/src/styles/App.scss @@ -254,6 +254,7 @@ $hunit: '%'; } // Lloyd George +.document, .lloydgeorge { &_record-stage { &_links { @@ -571,6 +572,7 @@ $hunit: '%'; } .lloydgeorge, +.document, .report { &_download-complete { max-width: 711px; @@ -618,6 +620,7 @@ $hunit: '%'; @media (max-width: 685px) { .nhsuk-card__heading.report_download-complete_details-content_header, + .nhsuk-card__heading.document_download-complete_details-content_header, .nhsuk-card__heading.lloydgeorge_download-complete_details-content_header { font-size: 2rem; } @@ -625,11 +628,13 @@ $hunit: '%'; @media (max-width: 435px) { .nhsuk-card__heading.report_download-complete_details-content_header, + .nhsuk-card__heading.document_download-complete_details-content_header, .nhsuk-card__heading.lloydgeorge_download-complete_details-content_header { font-size: 1.5rem; } .nhsuk-card__description.report_download-complete_details-content_description, + .nhsuk-card__description.document_download-complete_details-content_description, .nhsuk-card__description.lloydgeorge_download-complete_details-content_description { font-size: 1.2rem !important; } @@ -929,6 +934,7 @@ $hunit: '%'; margin-bottom: 50px; } + .document_drag-and-drop, .lloydgeorge_drag-and-drop { border-color: $nhsuk-error-color; } @@ -975,6 +981,7 @@ $hunit: '%'; padding: 0; } + .document_record-stage, .lloydgeorge_record-stage { height: calc(100vh - 93px); position: relative; diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index 6c99e8c86..f24da5c2d 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -33,6 +33,8 @@ export enum routes { ADMIN_ROUTE = '/admin', ADMIN_ROUTE_WILDCARD = '/admin/*', + + DOWNLOAD_COMPLETE = '/download-complete', } export enum routeChildren {