Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions app/lib/generators/mammogram-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
]

Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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
})

Expand All @@ -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.',
Expand Down
35 changes: 35 additions & 0 deletions app/lib/utils/reading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -1903,6 +1937,7 @@ module.exports = {
getNextUserReadableAppointment,
getNextCaseInSession,
getPreviousCaseInSession,
getFollowingCaseInSession,
canOpenCaseInSession,
getFirstOutstandingCaseInSession,
getResumeAppointmentForUser,
Expand Down
38 changes: 37 additions & 1 deletion app/routes/reading.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
getAppointmentReadingMetadata,
appointmentHasBeenArbitrated,
getNextCaseInSession,
getFollowingCaseInSession,
getFirstOutstandingCaseInSession,
filterAppointmentsByEligibleForReading,
filterAppointmentsByNeedsAnyRead,
Expand Down Expand Up @@ -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)
)
}
)

Expand Down
4 changes: 2 additions & 2 deletions app/views/_includes/additional-image-details.njk
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
39 changes: 39 additions & 0 deletions tests/e2e/reading.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down