From f38fdb8094f0090ef7ab714441e021868ed56da7 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 15:37:07 +0000 Subject: [PATCH 1/7] [PRMP-1422] Display the uploader ODS code on Review Document screens --- .../ReviewDetailsAssessmentStage.tsx | 27 ++++++- .../ReviewDetailsPatientSearchStage.tsx | 11 ++- .../ReviewsDetailsStage.tsx | 14 +++- .../DocumentUploadLloydGeorgePreview.tsx | 3 + .../generic/createdBy/createdBy.test.tsx | 79 +++++++++++++++++++ .../generic/createdBy/createdBy.tsx | 20 +++++ .../generic/recordLoader/RecordLoader.tsx | 16 ++-- app/src/helpers/utils/formatDate.ts | 21 +++++ 8 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 app/src/components/generic/createdBy/createdBy.test.tsx create mode 100644 app/src/components/generic/createdBy/createdBy.tsx diff --git a/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx b/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx index 340e70abbb..ac7816eb66 100644 --- a/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx @@ -12,7 +12,10 @@ import Spinner from '../../../generic/spinner/Spinner'; import ExistingRecordTable from './ExistingRecordTable'; import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl'; -import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate'; +import { + getFormattedDateFromString, + getFormattedDateTimeFromString, +} from '../../../../helpers/utils/formatDate'; import { GetDocumentReviewDto, ReviewDetails, @@ -28,6 +31,7 @@ import DocumentUploadLloydGeorgePreview from '../../_documentUpload/documentUplo import { AxiosError } from 'axios'; import { errorToParams } from '../../../../helpers/utils/errorToParams'; import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary'; +import { CreatedByText } from '../../../generic/createdBy/createdBy'; type FileAction = 'add-all' | 'choose-files' | 'duplicate' | 'accept' | 'reject' | ''; @@ -402,11 +406,20 @@ const ReviewDetailsAssessmentStage = ({

You are currently viewing: all files

- f.file.name.endsWith('.pdf'))} setMergedPdfBlob={(): void => {}} stitchedBlobLoaded={(): void => {}} + isReview={true} + createdElement={(): JSX.Element => ( + + )} documentConfig={reviewConfig} /> @@ -438,6 +451,16 @@ const ReviewDetailsAssessmentStage = ({ )} setMergedPdfBlob={(): void => {}} stitchedBlobLoaded={(): void => {}} + isReview={true} + createdElement={(): JSX.Element => ( + + )} documentConfig={reviewConfig} /> )} diff --git a/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx b/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx index ecd22965ab..70717f11ea 100644 --- a/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx @@ -29,8 +29,9 @@ import { RecordLayout } from '../../../generic/recordCard/RecordCard'; import { RecordLoader, RecordLoaderProps } from '../../../generic/recordLoader/RecordLoader'; import { getConfigForDocType } from '../../../../helpers/utils/documentType'; import { DOWNLOAD_STAGE } from '../../../../types/generic/downloadStage'; -import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate'; import { ReviewUploadDocument } from '../../../../types/pages/UploadDocumentsPage/types'; +import { getFormattedDateTimeFromString } from '../../../../helpers/utils/formatDate'; +import { CreatedByCard } from '../../../generic/createdBy/createdBy'; export const incorrectFormatMessage = "Enter patient's 10 digit NHS number"; @@ -137,7 +138,6 @@ const ReviewDetailsPatientSearchStage = ({ const recordDetailsProps: RecordLoaderProps = { downloadStage: DOWNLOAD_STAGE.SUCCEEDED, - lastUpdated: getFormattedDateFromString(reviewData.lastUpdated), childrenIfFailiure:

Failure: failed to load documents

, fileName: !reviewConfig.multifileReview && reviewData.files?.length === 1 @@ -236,6 +236,13 @@ const ReviewDetailsPatientSearchStage = ({ setMergedPdfBlob={(): void => {}} documentConfig={reviewConfig} isReview={true} + createdElement={(): JSX.Element => ( + + )} /> diff --git a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx index 6dbd256d43..07cf79a094 100644 --- a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx +++ b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx @@ -1,4 +1,4 @@ -import { Button, ErrorSummary, Fieldset, Radios } from 'nhsuk-react-components'; +import { Button, Card, ErrorSummary, Fieldset, Radios } from 'nhsuk-react-components'; import { Dispatch, JSX, SetStateAction, useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; @@ -7,7 +7,7 @@ import useConfig from '../../../../helpers/hooks/useConfig'; import useRole from '../../../../helpers/hooks/useRole'; import useTitle from '../../../../helpers/hooks/useTitle'; import { getConfigForDocType } from '../../../../helpers/utils/documentType'; -import { getFormattedDateFromString } from '../../../../helpers/utils/formatDate'; +import { getFormattedDateTimeFromString } from '../../../../helpers/utils/formatDate'; import { setFullScreen } from '../../../../helpers/utils/fullscreen'; import { handleSearch as handlePatientSearch } from '../../../../helpers/utils/handlePatientSearch'; import { usePatientDetailsContext } from '../../../../providers/patientProvider/PatientProvider'; @@ -108,7 +108,7 @@ const ReviewsDetailsStage = ({ }; const recordDetailsProps: RecordLoaderProps = { downloadStage, - lastUpdated: getFormattedDateFromString(reviewData.lastUpdated), + // lastUpdated: getFormattedDateFromString(reviewData.lastUpdated), childrenIfFailiure:

Failure: failed to load documents

, fileName: !reviewConfig.multifileReview && reviewData.files && reviewData.files.length === 1 @@ -269,6 +269,13 @@ const ReviewsDetailsStage = ({ } }; + const createdElement = (): JSX.Element => ( + + Created by practice {reviewData.uploader} on{' '} + {getFormattedDateTimeFromString(reviewData.dateUploaded)} + + ); + return ( <> {backButton} @@ -334,6 +341,7 @@ const ReviewsDetailsStage = ({ setMergedPdfBlob={(): void => {}} documentConfig={reviewConfig} isReview={true} + createdElement={createdElement} /> diff --git a/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx b/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx index 0f476dabc3..59dec86d17 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx @@ -12,6 +12,7 @@ type Props = { documentConfig: DOCUMENT_TYPE_CONFIG; isReview?: boolean; showCurrentlyViewingText?: boolean; + createdElement?: () => JSX.Element; }; const DocumentUploadLloydGeorgePreview = ({ @@ -21,6 +22,7 @@ const DocumentUploadLloydGeorgePreview = ({ documentConfig, isReview = false, showCurrentlyViewingText, + createdElement, }: Props): JSX.Element => { const [mergedPdfUrl, setMergedPdfUrl] = useState(''); const journey = getJourney(); @@ -106,6 +108,7 @@ const DocumentUploadLloydGeorgePreview = ({ )} )} + {isReview && createdElement && createdElement()} {documents && mergedPdfUrl && ( )} diff --git a/app/src/components/generic/createdBy/createdBy.test.tsx b/app/src/components/generic/createdBy/createdBy.test.tsx new file mode 100644 index 0000000000..b92f147efb --- /dev/null +++ b/app/src/components/generic/createdBy/createdBy.test.tsx @@ -0,0 +1,79 @@ +import { render, screen } from '@testing-library/react'; +import { CreatedByCard, CreatedByText } from './createdBy'; + +describe('createdBy.tsx', () => { + describe('CreatedByCard', () => { + const defaultProps = { + odsCode: 'Y12345', + dateUploaded: '2024-01-15', + }; + + it('renders the card with odsCode and dateUploaded', () => { + render(); + + expect( + screen.getByText( + `Created by practice ${defaultProps.odsCode} on ${defaultProps.dateUploaded}`, + ), + ).toBeInTheDocument(); + }); + + it('applies custom cssClass when provided', () => { + const customClass = 'custom-test-class'; + const { container } = render( + , + ); + + const cardContent = container.querySelector(`.${customClass}`); + expect(cardContent).toBeInTheDocument(); + }); + + it('renders without cssClass when not provided', () => { + const { container } = render(); + + const cardContent = container.firstChild; + expect(cardContent).not.toHaveClass('custom-test-class'); + }); + }); + + describe('CreatedByText', () => { + const defaultProps = { + odsCode: 'A98765', + dateUploaded: '2024-06-20', + }; + + it('renders the text with odsCode and dateUploaded', () => { + render(); + + expect( + screen.getByText( + `Created by practice ${defaultProps.odsCode} on ${defaultProps.dateUploaded}`, + ), + ).toBeInTheDocument(); + }); + + it('renders as a paragraph element', () => { + render(); + + const paragraph = screen.getByText(/Created by practice/); + expect(paragraph.tagName).toBe('P'); + }); + + it('applies custom cssClass when provided', () => { + const customClass = 'text-style-class'; + const { container } = render( + , + ); + + const paragraph = container.querySelector(`.${customClass}`); + expect(paragraph).toBeInTheDocument(); + }); + + it('renders without cssClass when not provided', () => { + const { container } = render(); + + const paragraph = container.firstChild; + expect(paragraph).not.toHaveClass('text-style-class'); + }); + }); +}); diff --git a/app/src/components/generic/createdBy/createdBy.tsx b/app/src/components/generic/createdBy/createdBy.tsx new file mode 100644 index 0000000000..3102ebd63c --- /dev/null +++ b/app/src/components/generic/createdBy/createdBy.tsx @@ -0,0 +1,20 @@ +import { Card } from 'nhsuk-react-components'; +import { JSX } from 'react'; + +export type CreatedByProps = { + odsCode: string; + dateUploaded: string; + cssClass?: string; +}; + +export const CreatedByCard = ({ odsCode, dateUploaded, cssClass }: CreatedByProps): JSX.Element => ( + + Created by practice {odsCode} on {dateUploaded} + +); + +export const CreatedByText = ({ odsCode, dateUploaded, cssClass }: CreatedByProps): JSX.Element => ( +

+ Created by practice {odsCode} on {dateUploaded} +

+); diff --git a/app/src/components/generic/recordLoader/RecordLoader.tsx b/app/src/components/generic/recordLoader/RecordLoader.tsx index 917c4dac49..3d8f5cfbcb 100644 --- a/app/src/components/generic/recordLoader/RecordLoader.tsx +++ b/app/src/components/generic/recordLoader/RecordLoader.tsx @@ -4,7 +4,7 @@ import ProgressBar from '../progressBar/ProgressBar'; export type RecordLoaderProps = { downloadStage: DOWNLOAD_STAGE; - lastUpdated: string; + lastUpdated?: string; childrenIfFailiure: React.JSX.Element; fileName: string; downloadAction?: (e: React.MouseEvent) => void; @@ -23,6 +23,10 @@ export const RecordLoader = ({ fileName, }; + if (!lastUpdated && !fileName) { + return <>; + } + switch (downloadStage) { case DOWNLOAD_STAGE.INITIAL: case DOWNLOAD_STAGE.PENDING: @@ -48,7 +52,7 @@ export const RecordLoader = ({ }; export type RecordDetailsProps = { - lastUpdated: string; + lastUpdated?: string; fileName: string; downloadAction?: (e: React.MouseEvent) => void; }; @@ -61,9 +65,11 @@ export const RecordDetails = ({ return (
-
-

Last updated: {lastUpdated}

-
+ {lastUpdated && ( +
+

Last updated: {lastUpdated}

+
+ )} {fileName && (

diff --git a/app/src/helpers/utils/formatDate.ts b/app/src/helpers/utils/formatDate.ts index 849ec91742..31d796ac2b 100644 --- a/app/src/helpers/utils/formatDate.ts +++ b/app/src/helpers/utils/formatDate.ts @@ -2,6 +2,17 @@ export const getFormattedDate = (date: Date): string => { return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); }; +export const getFormattedDateTime = (date: Date): string => { + return date.toLocaleDateString('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: 'numeric', + hour12: true, + }); +}; + export const formatDateWithDashes = (date: Date): string => { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); @@ -19,3 +30,13 @@ export const getFormattedDateFromString = (dateString: string | undefined): stri } return getFormattedDate(new Date(Number(dateString))); }; + +export const getFormattedDateTimeFromString = (dateString: string | undefined): string => { + if (!dateString) { + return ''; + } + if (Number.isNaN(Number(dateString))) { + return getFormattedDateTime(new Date(dateString)); + } + return getFormattedDateTime(new Date(Number(dateString))); +}; From 88211d1db20144a076bd4fef4d979975b049a8f8 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 15:41:57 +0000 Subject: [PATCH 2/7] Replace created element with CreatedByCard component in ReviewsDetailsStage --- .../ReviewsDetailsStage.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx index 07cf79a094..8aef93535d 100644 --- a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx +++ b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx @@ -35,6 +35,7 @@ import { errorToParams } from '../../../../helpers/utils/errorToParams'; import waitForSeconds from '../../../../helpers/utils/waitForSeconds'; import DocumentUploadLloydGeorgePreview from '../../_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview'; import { NHS_NUMBER_UNKNOWN } from '../../../../helpers/constants/numbers'; +import { CreatedByCard } from '../../../generic/createdBy/createdBy'; export type ReviewsDetailsStageProps = { reviewData: ReviewDetails; @@ -106,9 +107,9 @@ const ReviewsDetailsStage = ({ anchor.remove(); } }; + const recordDetailsProps: RecordLoaderProps = { downloadStage, - // lastUpdated: getFormattedDateFromString(reviewData.lastUpdated), childrenIfFailiure:

Failure: failed to load documents

, fileName: !reviewConfig.multifileReview && reviewData.files && reviewData.files.length === 1 @@ -269,13 +270,6 @@ const ReviewsDetailsStage = ({ } }; - const createdElement = (): JSX.Element => ( - - Created by practice {reviewData.uploader} on{' '} - {getFormattedDateTimeFromString(reviewData.dateUploaded)} - - ); - return ( <> {backButton} @@ -341,7 +335,15 @@ const ReviewsDetailsStage = ({ setMergedPdfBlob={(): void => {}} documentConfig={reviewConfig} isReview={true} - createdElement={createdElement} + createdElement={(): JSX.Element => ( + + )} />
From b70b925d2b7647e1fedad8dcd68e565d9f5492d0 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 15:56:14 +0000 Subject: [PATCH 3/7] Refactor date formatting tests to include getFormattedDateTime and improve structure --- app/src/helpers/utils/formatDate.test.ts | 350 +++-------------------- 1 file changed, 46 insertions(+), 304 deletions(-) diff --git a/app/src/helpers/utils/formatDate.test.ts b/app/src/helpers/utils/formatDate.test.ts index 2ff9ad24a6..942ef00589 100644 --- a/app/src/helpers/utils/formatDate.test.ts +++ b/app/src/helpers/utils/formatDate.test.ts @@ -1,329 +1,71 @@ import { describe, expect, it } from 'vitest'; -import { getFormattedDate, formatDateWithDashes, getFormattedDateFromString } from './formatDate'; +import { + getFormattedDate, + getFormattedDateTime, + formatDateWithDashes, + getFormattedDateFromString, + getFormattedDateTimeFromString, +} from './formatDate'; -describe('getFormattedDate', () => { - it('formats date in en-GB locale with full month name', () => { - const date = new Date('2025-12-18T10:30:00Z'); - const result = getFormattedDate(date); - expect(result).toBe('18 December 2025'); - }); - - it('formats date on 1st of month', () => { - const date = new Date('2025-01-01T00:00:00Z'); - const result = getFormattedDate(date); - expect(result).toBe('1 January 2025'); - }); - - it('formats date at end of month', () => { - const date = new Date('2025-12-31T23:59:59Z'); - const result = getFormattedDate(date); - expect(result).toBe('31 December 2025'); - }); - - it('formats date in February', () => { - const date = new Date('2025-02-14T12:00:00Z'); - const result = getFormattedDate(date); - expect(result).toBe('14 February 2025'); - }); - - it('formats date with single digit day', () => { - const date = new Date('2025-03-05T08:00:00Z'); - const result = getFormattedDate(date); - expect(result).toBe('5 March 2025'); - }); - - it('formats leap year date', () => { - const date = new Date('2024-02-29T00:00:00Z'); - const result = getFormattedDate(date); - expect(result).toBe('29 February 2024'); - }); - - it('formats different months correctly', () => { - const months = [ - { date: new Date('2025-01-15'), expected: '15 January 2025' }, - { date: new Date('2025-02-15'), expected: '15 February 2025' }, - { date: new Date('2025-03-15'), expected: '15 March 2025' }, - { date: new Date('2025-04-15'), expected: '15 April 2025' }, - { date: new Date('2025-05-15'), expected: '15 May 2025' }, - { date: new Date('2025-06-15'), expected: '15 June 2025' }, - { date: new Date('2025-07-15'), expected: '15 July 2025' }, - { date: new Date('2025-08-15'), expected: '15 August 2025' }, - { date: new Date('2025-09-15'), expected: '15 September 2025' }, - { date: new Date('2025-10-15'), expected: '15 October 2025' }, - { date: new Date('2025-11-15'), expected: '15 November 2025' }, - { date: new Date('2025-12-15'), expected: '15 December 2025' }, - ]; - - months.forEach(({ date, expected }) => { - expect(getFormattedDate(date)).toBe(expected); - }); - }); - - it('formats date in different years', () => { - const date1900 = new Date('1900-01-01'); - const date2000 = new Date('2000-06-15'); - const date2099 = new Date('2099-12-31'); - - expect(getFormattedDate(date1900)).toBe('1 January 1900'); - expect(getFormattedDate(date2000)).toBe('15 June 2000'); - expect(getFormattedDate(date2099)).toBe('31 December 2099'); - }); -}); - -describe('formatDateWithDashes', () => { - it('formats date with DD-MM-YYYY format', () => { - const date = new Date('2025-12-18T10:30:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('18-12-2025'); - }); - - it('pads single digit day with leading zero', () => { - const date = new Date('2025-01-05T00:00:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('05-01-2025'); - }); - - it('pads single digit month with leading zero', () => { - const date = new Date('2025-09-18T00:00:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('18-09-2025'); - }); - - it('formats date on 1st of month with leading zero', () => { - const date = new Date('2025-03-01T00:00:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('01-03-2025'); - }); - - it('formats date at end of month without leading zero', () => { - const date = new Date('2025-12-31T23:59:59Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('31-12-2025'); - }); - - it('formats February dates correctly', () => { - const date = new Date('2025-02-14T12:00:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('14-02-2025'); - }); - - it('formats leap year date', () => { - const date = new Date('2024-02-29T00:00:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('29-02-2024'); - }); - - it('formats all months correctly', () => { - const months = [ - { date: new Date('2025-01-15'), expected: '15-01-2025' }, - { date: new Date('2025-02-15'), expected: '15-02-2025' }, - { date: new Date('2025-03-15'), expected: '15-03-2025' }, - { date: new Date('2025-04-15'), expected: '15-04-2025' }, - { date: new Date('2025-05-15'), expected: '15-05-2025' }, - { date: new Date('2025-06-15'), expected: '15-06-2025' }, - { date: new Date('2025-07-15'), expected: '15-07-2025' }, - { date: new Date('2025-08-15'), expected: '15-08-2025' }, - { date: new Date('2025-09-15'), expected: '15-09-2025' }, - { date: new Date('2025-10-15'), expected: '15-10-2025' }, - { date: new Date('2025-11-15'), expected: '15-11-2025' }, - { date: new Date('2025-12-15'), expected: '15-12-2025' }, - ]; - - months.forEach(({ date, expected }) => { - expect(formatDateWithDashes(date)).toBe(expected); - }); - }); - - it('formats dates with single digit day and month', () => { - const date = new Date('2025-01-01T00:00:00Z'); - const result = formatDateWithDashes(date); - expect(result).toBe('01-01-2025'); - }); - - it('formats dates in different years', () => { - const date1900 = new Date('1900-01-01'); - const date2000 = new Date('2000-06-05'); - const date2099 = new Date('2099-12-09'); - - expect(formatDateWithDashes(date1900)).toBe('01-01-1900'); - expect(formatDateWithDashes(date2000)).toBe('05-06-2000'); - expect(formatDateWithDashes(date2099)).toBe('09-12-2099'); - }); - - it('handles dates with different times consistently', () => { - // Use local date construction to avoid timezone issues - const midnight = new Date(2025, 5, 15, 0, 0, 0); - const noon = new Date(2025, 5, 15, 12, 0, 0); - const endOfDay = new Date(2025, 5, 15, 23, 59, 59); - - expect(formatDateWithDashes(midnight)).toBe('15-06-2025'); - expect(formatDateWithDashes(noon)).toBe('15-06-2025'); - expect(formatDateWithDashes(endOfDay)).toBe('15-06-2025'); - }); -}); - -describe('getFormattedDateFromString', () => { - describe('empty or undefined input', () => { - it('returns empty string for undefined', () => { - const result = getFormattedDateFromString(undefined); - expect(result).toBe(''); - }); - - it('returns empty string for empty string', () => { - const result = getFormattedDateFromString(''); - expect(result).toBe(''); +describe('formatDate.ts', () => { + describe('getFormattedDate', () => { + it('formats date in en-GB locale', () => { + expect(getFormattedDate(new Date('2024-01-15T00:00:00Z'))).toBe('15 January 2024'); + expect(getFormattedDate(new Date('2024-02-29T00:00:00Z'))).toBe('29 February 2024'); + expect(getFormattedDate(new Date('2025-12-31T23:59:59Z'))).toBe('31 December 2025'); }); }); - describe('ISO date string format', () => { - it('formats ISO date string correctly', () => { - const result = getFormattedDateFromString('2025-12-18T10:30:00Z'); - expect(result).toBe('18 December 2025'); - }); - - it('formats ISO date without time', () => { - const result = getFormattedDateFromString('2025-01-15'); - expect(result).toBe('15 January 2025'); - }); - - it('formats ISO date with timezone offset', () => { - const result = getFormattedDateFromString('2025-06-15T14:30:00+01:00'); - expect(result).toBe('15 June 2025'); - }); - - it('formats ISO date string with milliseconds', () => { - const result = getFormattedDateFromString('2025-03-20T10:30:00.123Z'); - expect(result).toBe('20 March 2025'); - }); - }); - - describe('numeric timestamp format', () => { - it('formats numeric timestamp string (milliseconds)', () => { - const timestamp = '1734523800000'; // December 18, 2024 - const result = getFormattedDateFromString(timestamp); - expect(result).toContain('December'); - expect(result).toContain('2024'); - }); - - it('formats timestamp at epoch start', () => { - const result = getFormattedDateFromString('0'); - expect(result).toBe('1 January 1970'); - }); - - it('formats recent timestamp', () => { - // January 1, 2025 00:00:00 UTC - const timestamp = '1735689600000'; - const result = getFormattedDateFromString(timestamp); - expect(result).toBe('1 January 2025'); - }); - - it('formats future timestamp', () => { - // December 31, 2099 23:59:59 UTC - const timestamp = String(new Date('2099-12-31T23:59:59Z').getTime()); - const result = getFormattedDateFromString(timestamp); - expect(result).toBe('31 December 2099'); + describe('getFormattedDateTime', () => { + it('formats date and time in en-GB locale', () => { + const result = getFormattedDateTime(new Date('2024-06-20T13:05:00Z')); + expect(result).toContain('20 June 2024'); + expect(/\d{1,2}:[0-5][0-9]/.test(result)).toBe(true); }); }); - describe('various date string formats', () => { - it('formats US date format (MM/DD/YYYY)', () => { - const result = getFormattedDateFromString('12/18/2025'); - expect(result).toContain('December'); - expect(result).toContain('2025'); - }); - - it('formats date with full month name', () => { - const result = getFormattedDateFromString('December 18, 2025'); - expect(result).toBe('18 December 2025'); - }); - - it('formats short date format', () => { - const result = getFormattedDateFromString('2025-12-18'); - expect(result).toBe('18 December 2025'); + describe('formatDateWithDashes', () => { + it('formats date as DD-MM-YYYY with zero padding', () => { + expect(formatDateWithDashes(new Date('2025-01-05T00:00:00Z'))).toBe('05-01-2025'); + expect(formatDateWithDashes(new Date('2025-12-18T10:30:00Z'))).toBe('18-12-2025'); + expect(formatDateWithDashes(new Date('2024-02-29T00:00:00Z'))).toBe('29-02-2024'); }); }); - describe('edge cases', () => { - it('handles leap year date', () => { - const result = getFormattedDateFromString('2024-02-29'); - expect(result).toBe('29 February 2024'); + describe('getFormattedDateFromString', () => { + it('returns empty string for undefined or empty input', () => { + expect(getFormattedDateFromString(undefined)).toBe(''); + expect(getFormattedDateFromString('')).toBe(''); }); - it('handles date at start of year', () => { - const result = getFormattedDateFromString('2025-01-01T00:00:00Z'); - expect(result).toBe('1 January 2025'); + it('formats ISO date strings', () => { + expect(getFormattedDateFromString('2025-12-18T10:30:00Z')).toBe('18 December 2025'); + expect(getFormattedDateFromString('2025-01-15')).toBe('15 January 2025'); + expect(getFormattedDateFromString('2024-02-29')).toBe('29 February 2024'); }); - it('handles date at end of year', () => { - const result = getFormattedDateFromString('2025-12-31T23:59:59Z'); - expect(result).toBe('31 December 2025'); - }); - - it('formats timestamp string with spaces (treated as NaN)', () => { - const result = getFormattedDateFromString(' 12345 '); - // This will be treated as numeric timestamp - expect(result).toBeTruthy(); - }); - - it('handles various ISO formats', () => { - const formats = [ - { input: '2025-06-15T12:00:00Z', expected: '15 June 2025' }, - { input: '2025-06-15T12:00:00.000Z', expected: '15 June 2025' }, - { input: '2025-06-15', expected: '15 June 2025' }, - ]; - - formats.forEach(({ input, expected }) => { - expect(getFormattedDateFromString(input)).toBe(expected); - }); + it('formats numeric timestamp strings', () => { + expect(getFormattedDateFromString('0')).toBe('1 January 1970'); + expect(getFormattedDateFromString('1735689600000')).toBe('1 January 2025'); }); }); - describe('timestamp conversion logic', () => { - it('distinguishes between numeric string and ISO string', () => { - const numericTimestamp = '1735689600000'; - const isoString = '2025-01-01T00:00:00Z'; - - const numericResult = getFormattedDateFromString(numericTimestamp); - const isoResult = getFormattedDateFromString(isoString); - - expect(numericResult).toBe('1 January 2025'); - expect(isoResult).toBe('1 January 2025'); - }); - - it('handles very large timestamp', () => { - // Far future date - const timestamp = String(new Date('2099-12-31').getTime()); - const result = getFormattedDateFromString(timestamp); - expect(result).toContain('2099'); - }); - - it('handles small timestamp (early 1970s)', () => { - const timestamp = '86400000'; // 1 day after epoch - const result = getFormattedDateFromString(timestamp); - expect(result).toBe('2 January 1970'); + describe('getFormattedDateTimeFromString', () => { + it('returns empty string for undefined input', () => { + expect(getFormattedDateTimeFromString(undefined)).toBe(''); }); - }); - - describe('consistency with getFormattedDate', () => { - it('produces same output as getFormattedDate for ISO string', () => { - const dateString = '2025-06-15T10:30:00Z'; - const date = new Date(dateString); - - const fromString = getFormattedDateFromString(dateString); - const fromDate = getFormattedDate(date); - expect(fromString).toBe(fromDate); + it('formats ISO date strings with time', () => { + const result = getFormattedDateTimeFromString('2022-11-11T18:45:00'); + expect(result).toContain('11 November 2022'); + expect(/\d{1,2}:[0-5][0-9]/.test(result)).toBe(true); }); - it('produces same output as getFormattedDate for numeric timestamp', () => { - const timestamp = Date.now(); - const timestampString = String(timestamp); - const date = new Date(timestamp); - - const fromString = getFormattedDateFromString(timestampString); - const fromDate = getFormattedDate(date); - - expect(fromString).toBe(fromDate); + it('formats numeric timestamp strings with time', () => { + const ts = String(new Date('2024-07-21T09:30:00Z').getTime()); + const result = getFormattedDateTimeFromString(ts); + expect(result).toContain('21 July 2024'); + expect(/\d{1,2}:[0-5][0-9]/.test(result)).toBe(true); }); }); }); From 6c63e775e5308c5515b0acd746dedb70ce3601a4 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 16:02:32 +0000 Subject: [PATCH 4/7] minor fix --- .../blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx index 8aef93535d..5616ffa64c 100644 --- a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx +++ b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx @@ -1,4 +1,4 @@ -import { Button, Card, ErrorSummary, Fieldset, Radios } from 'nhsuk-react-components'; +import { Button, ErrorSummary, Fieldset, Radios } from 'nhsuk-react-components'; import { Dispatch, JSX, SetStateAction, useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders'; From dff7860d88c87c7137702348934649af86aea500 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 17:02:41 +0000 Subject: [PATCH 5/7] Remove test case for handling empty string in lastUpdated from RecordDetails tests --- .../components/generic/recordLoader/RecordLoader.test.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/components/generic/recordLoader/RecordLoader.test.tsx b/app/src/components/generic/recordLoader/RecordLoader.test.tsx index 586516627c..8cc2375b5d 100644 --- a/app/src/components/generic/recordLoader/RecordLoader.test.tsx +++ b/app/src/components/generic/recordLoader/RecordLoader.test.tsx @@ -343,12 +343,6 @@ describe('RecordDetails', () => { }); describe('Edge Cases', () => { - it('handles empty string for lastUpdated', () => { - render(); - - expect(screen.getByText('Last updated:')).toBeInTheDocument(); - }); - it('handles long date strings', () => { const longDate = 'Wednesday, 25th December 2024 at 12:30:45pm GMT'; render(); From 748794b8aaf40aa38ac7bf78f1964fe561ba9725 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 17:16:59 +0000 Subject: [PATCH 6/7] Refactor DocumentUploadLloydGeorgePreview to use children prop instead of createdElement for rendering CreatedByCard in ReviewDetailsAssessmentStage, ReviewDetailsPatientSearchStage, and ReviewsDetailsStage components. --- .../ReviewDetailsAssessmentStage.tsx | 36 +++++++++---------- .../ReviewDetailsPatientSearchStage.tsx | 15 ++++---- .../ReviewsDetailsStage.tsx | 19 +++++----- .../DocumentUploadLloydGeorgePreview.tsx | 6 ++-- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx b/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx index ac7816eb66..e211392cbd 100644 --- a/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.tsx @@ -411,17 +411,14 @@ const ReviewDetailsAssessmentStage = ({ setMergedPdfBlob={(): void => {}} stitchedBlobLoaded={(): void => {}} isReview={true} - createdElement={(): JSX.Element => ( - - )} documentConfig={reviewConfig} - /> + > + + )} @@ -452,17 +449,16 @@ const ReviewDetailsAssessmentStage = ({ setMergedPdfBlob={(): void => {}} stitchedBlobLoaded={(): void => {}} isReview={true} - createdElement={(): JSX.Element => ( - - )} documentConfig={reviewConfig} - /> + > + + )} )} diff --git a/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx b/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx index 70717f11ea..8bfe9e4440 100644 --- a/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx @@ -236,14 +236,13 @@ const ReviewDetailsPatientSearchStage = ({ setMergedPdfBlob={(): void => {}} documentConfig={reviewConfig} isReview={true} - createdElement={(): JSX.Element => ( - - )} - /> + > + + ); diff --git a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx index 5616ffa64c..619d31e5b5 100644 --- a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx +++ b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx @@ -335,16 +335,15 @@ const ReviewsDetailsStage = ({ setMergedPdfBlob={(): void => {}} documentConfig={reviewConfig} isReview={true} - createdElement={(): JSX.Element => ( - - )} - /> + > + +
diff --git a/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx b/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx index 59dec86d17..5e098d566d 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview.tsx @@ -12,7 +12,7 @@ type Props = { documentConfig: DOCUMENT_TYPE_CONFIG; isReview?: boolean; showCurrentlyViewingText?: boolean; - createdElement?: () => JSX.Element; + children?: React.ReactNode; }; const DocumentUploadLloydGeorgePreview = ({ @@ -22,7 +22,7 @@ const DocumentUploadLloydGeorgePreview = ({ documentConfig, isReview = false, showCurrentlyViewingText, - createdElement, + children, }: Props): JSX.Element => { const [mergedPdfUrl, setMergedPdfUrl] = useState(''); const journey = getJourney(); @@ -108,7 +108,7 @@ const DocumentUploadLloydGeorgePreview = ({ )} )} - {isReview && createdElement && createdElement()} + {isReview && <>{children}} {documents && mergedPdfUrl && ( )} From 03636f19f1e68f0fb63a4c79d9678c17b1dbb4f9 Mon Sep 17 00:00:00 2001 From: Lillie Dae Date: Wed, 11 Feb 2026 17:30:49 +0000 Subject: [PATCH 7/7] Add reviewData prop to DocumentSelectOrderStage and render CreatedByText in DocumentUploadLloydGeorgePreview for review context --- .../ReviewDetailsDocumentSelectOrderStage.tsx | 1 + .../DocumentSelectOrderStage.tsx | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectOrderStage/ReviewDetailsDocumentSelectOrderStage.tsx b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectOrderStage/ReviewDetailsDocumentSelectOrderStage.tsx index 2ca7a98cad..6bca6a24ab 100644 --- a/app/src/components/blocks/_admin/reviewDetailsDocumentSelectOrderStage/ReviewDetailsDocumentSelectOrderStage.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsDocumentSelectOrderStage/ReviewDetailsDocumentSelectOrderStage.tsx @@ -65,6 +65,7 @@ const ReviewDetailsDocumentSelectOrderStage = ({ confirmFiles={(): void => {}} onSuccess={onSuccess} isReview={true} + reviewData={reviewData} /> ); }; diff --git a/app/src/components/blocks/_documentUpload/documentSelectOrderStage/DocumentSelectOrderStage.tsx b/app/src/components/blocks/_documentUpload/documentSelectOrderStage/DocumentSelectOrderStage.tsx index e209f9be97..db09b81626 100644 --- a/app/src/components/blocks/_documentUpload/documentSelectOrderStage/DocumentSelectOrderStage.tsx +++ b/app/src/components/blocks/_documentUpload/documentSelectOrderStage/DocumentSelectOrderStage.tsx @@ -24,6 +24,9 @@ import ErrorBox from '../../../layout/errorBox/ErrorBox'; import DocumentUploadLloydGeorgePreview from '../documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview'; import SpinnerButton from '../../../generic/spinnerButton/SpinnerButton'; import { DOCUMENT_TYPE_CONFIG } from '../../../../helpers/utils/documentType'; +import { CreatedByText } from '../../../generic/createdBy/createdBy'; +import { getFormattedDateTimeFromString } from '../../../../helpers/utils/formatDate'; +import { ReviewDetails } from '../../../../types/generic/reviews'; type Props = { documents: UploadDocument[] | ReviewUploadDocument[]; @@ -34,6 +37,7 @@ type Props = { confirmFiles: () => void; onSuccess?: () => void; isReview?: boolean; + reviewData?: ReviewDetails; }; type FormData = { @@ -51,6 +55,7 @@ const DocumentSelectOrderStage = ({ confirmFiles, onSuccess, isReview = false, + reviewData, }: Readonly): JSX.Element => { const navigate = useEnhancedNavigate(); const journey = getJourney(); @@ -472,7 +477,18 @@ const DocumentSelectOrderStage = ({ setStitchedBlobLoaded(loaded); }} documentConfig={documentConfig} - /> + isReview={isReview} + > + {isReview && reviewData && ( + + )} +
{documents.length > 0 && stitchedBlobLoaded && (