diff --git a/app/lib/generators/mammogram-generator.js b/app/lib/generators/mammogram-generator.js index cd56e093..64e3675e 100644 --- a/app/lib/generators/mammogram-generator.js +++ b/app/lib/generators/mammogram-generator.js @@ -18,7 +18,7 @@ const INCOMPLETE_MAMMOGRAPHY_REASONS = [ // Follow-up appointment options const INCOMPLETE_MAMMOGRAPHY_FOLLOW_UP_OPTIONS = [ - "Yes, record as 'to be recalled'", + "Yes, record as 'more images due'", "No, record as 'partial mammography'" ] @@ -164,9 +164,9 @@ const generateMammogramImages = ({ notesForReaderChanceWithoutImperfect = 0.05 } = {}) => { // Use the provided accession number as base, or fall back to a random number - const accessionBase = accessionNumber || faker.number - .int({ min: 100000000, max: 999999999 }) - .toString() + const accessionBase = + accessionNumber || + faker.number.int({ min: 100000000, max: 999999999 }).toString() let currentIndex = 1 let currentTime = dayjs(startTime) const views = {} @@ -289,7 +289,7 @@ const generateMammogramImages = ({ if (isSeedData && hasMissingViews) { const reason = faker.helpers.arrayElement(INCOMPLETE_MAMMOGRAPHY_REASONS) const followUp = weighted.select({ - "Yes, record as 'to be recalled'": 0.4, + "Yes, record as 'more images due'": 0.4, "No, record as 'partial mammography'": 0.6 }) @@ -312,7 +312,7 @@ const generateMammogramImages = ({ } // Add details if follow-up is YES - if (followUp === "Yes, record as 'to be recalled'") { + if (followUp === "Yes, record as 'more images due'") { incompleteMammographyData.incompleteMammographyFollowUpAppointmentDetails = faker.helpers.arrayElement([ 'Participant has a shoulder injury that should heal in 4-6 weeks.', diff --git a/app/lib/utils/reading.js b/app/lib/utils/reading.js index c3cb2fa6..84ff0e03 100644 --- a/app/lib/utils/reading.js +++ b/app/lib/utils/reading.js @@ -1072,6 +1072,40 @@ const getPreviousCaseInSession = ( ) } +/** + * The case after the current one in a session, if there is one to open. + * + * The forward counterpart to getPreviousCaseInSession: steps through the + * session in order, finished cases included. For looking back over a session + * with nothing left to do - getNextCaseInSession skips finished cases, so it + * has nowhere to go once they all are. + * + * @param {object} data - Session data + * @param {object} session - The reading session + * @param {Array} sessionAppointments - The session's appointments, in order + * @param {string} currentAppointmentId - The case being looked at + * @param {string} userId - User ID + * @returns {object | undefined} The following appointment, or undefined if none + */ +const getFollowingCaseInSession = ( + data, + session, + sessionAppointments, + currentAppointmentId, + userId +) => { + const currentIndex = sessionAppointments.findIndex( + (appointment) => appointment.id === currentAppointmentId + ) + if (currentIndex === -1) return undefined + + return sessionAppointments + .slice(currentIndex + 1) + .find((appointment) => + canOpenCaseInSession(data, session, appointment, userId) + ) +} + /** * The first case still to work on in a session, wherever it sits. * @@ -1903,6 +1937,7 @@ module.exports = { getNextUserReadableAppointment, getNextCaseInSession, getPreviousCaseInSession, + getFollowingCaseInSession, canOpenCaseInSession, getFirstOutstandingCaseInSession, getResumeAppointmentForUser, diff --git a/app/routes/reading.js b/app/routes/reading.js index c6e8e65e..73aac975 100644 --- a/app/routes/reading.js +++ b/app/routes/reading.js @@ -30,6 +30,7 @@ const { getAppointmentReadingMetadata, appointmentHasBeenArbitrated, getNextCaseInSession, + getFollowingCaseInSession, getFirstOutstandingCaseInSession, filterAppointmentsByEligibleForReading, filterAppointmentsByNeedsAnyRead, @@ -1293,8 +1294,43 @@ module.exports = (router) => { (req, res) => { const data = req.session.data const { sessionId, appointmentId } = req.params + const currentUserId = data.currentUser?.id + + const onwardUrl = onwardFromCase(data, sessionId, appointmentId) + + // With nothing left to do, the reader is looking back over the session, + // so step through its cases in order as the "Previous case" link does - + // rather than landing on the end-of-session page every time + const session = getReadingSession(data, sessionId) + const sessionAppointments = session.appointmentIds + .map((id) => data.appointments.find((e) => e.id === id)) + .filter(Boolean) + const nothingLeftToDo = + isSessionEnded(session) || + !getFirstOutstandingCaseInSession( + data, + session, + sessionAppointments, + currentUserId + ) - res.redirect(onwardFromCase(data, sessionId, appointmentId)) + if (!nothingLeftToDo) { + return res.redirect(onwardUrl) + } + + const followingCase = getFollowingCaseInSession( + data, + session, + sessionAppointments, + appointmentId, + currentUserId + ) + + res.redirect( + followingCase + ? `/reading/session/${sessionId}/appointments/${followingCase.id}` + : sessionOverviewUrl(session) + ) } ) diff --git a/app/views/_includes/additional-image-details.njk b/app/views/_includes/additional-image-details.njk index 59befff9..804e54cf 100644 --- a/app/views/_includes/additional-image-details.njk +++ b/app/views/_includes/additional-image-details.njk @@ -94,8 +94,8 @@ }, items: [ { - value: "Yes, record as 'to be recalled'", - text: "Yes, record as 'to be recalled'", + value: "Yes, record as 'more images due'", + text: "Yes, record as 'more images due'", conditional: { html: yesRecallRescheduleDetailsHtml } diff --git a/tests/e2e/reading.spec.js b/tests/e2e/reading.spec.js index 26013975..db72ba9b 100644 --- a/tests/e2e/reading.spec.js +++ b/tests/e2e/reading.spec.js @@ -639,6 +639,45 @@ test.describe('Image reading', () => { ) }) + test('steps through a completed session with "Next case"', async ({ + page + }) => { + // Once every case is read there is nothing to move on to, so "Next case" + // walks the session in order rather than returning to the + // session-complete page each time + await pinSettings(page, readingSettings) + + await page.goto( + `/reading/create-session?type=all_reads&limit=${sessionSize}&lazy=false` + ) + await expect(page).toHaveURL(/\/reading\/session\/[^/]+\/appointments\//) + + const sessionId = page.url().split('/session/')[1].split('/')[0] + const readCases = [] + + for (let caseNumber = 0; caseNumber < sessionSize; caseNumber++) { + await expect(page).toHaveURL(/\/appointments\//) + readCases.push(page.url().split('/appointments/')[1].split('/')[0]) + await recordNormal(page) + } + + await expect(page).toHaveURL(/\/no-more-cases/) + + await page.goto(`/reading/session/${sessionId}/appointments/${readCases[0]}`) + await expect(page).toHaveURL(/\/existing-read/) + + for (const nextCaseId of readCases.slice(1)) { + await page.getByRole('link', { name: 'Next case' }).first().click() + await expect(page).toHaveURL( + new RegExp(`/appointments/${nextCaseId}/existing-read`) + ) + } + + // Past the last case, back to the session overview + await page.getByRole('link', { name: 'Next case' }).first().click() + await expect(page).toHaveURL(new RegExp(`/session/${sessionId}/your-reads`)) + }) + test('finalises reads from the session overview', async ({ page }) => { // With a finalisation delay, a fresh read sits unfinalised. Finalisation // deliberately lives on the session overview - behind a chance to review