diff --git a/app/lib/utils/reading.js b/app/lib/utils/reading.js index 491b3b23..41368bfd 100644 --- a/app/lib/utils/reading.js +++ b/app/lib/utils/reading.js @@ -1836,6 +1836,17 @@ const getSessionReadingProgress = ( const resolvedTargetSize = session.targetSize || sessionEvents.length + // Work out how large this session can actually become right now once we + // account for unclaimed eligible cases. This prevents showing "25 remaining" + // when only (for example) 20 cases are available to read. + const claimedEventIds = new Set( + Object.values(data.readingSessions || {}).flatMap((s) => s.eventIds || []) + ) + const availableTopUpCount = getEligibleCandidatesForSession(data, session) + .filter((event) => !claimedEventIds.has(event.id)).length + const reachableSessionSize = sessionEvents.length + availableTopUpCount + const effectiveTargetSize = Math.min(resolvedTargetSize, reachableSessionSize) + // Count deferred events so they count toward the session target const deferredCount = sessionEvents.filter(isDeferred).length @@ -1844,12 +1855,13 @@ const getSessionReadingProgress = ( // How many events are currently loaded vs the overall target populatedCount: sessionEvents.length, targetSize: resolvedTargetSize, + effectiveTargetSize, // Deferred events count as 'done' for session progress purposes deferredCount, // Remaining reads against the target (not just currently loaded events) targetRemaining: Math.max( 0, - resolvedTargetSize - + effectiveTargetSize - progress.userReadCount - progress.userAwaitingPriorsCount - deferredCount diff --git a/app/routes/reading.js b/app/routes/reading.js index 3631637b..7208516c 100644 --- a/app/routes/reading.js +++ b/app/routes/reading.js @@ -21,7 +21,10 @@ const { topUpSession, getReadingMetadata, getComparisonInfo, - shouldShowComparePage + shouldShowComparePage, + filterEventsByEligibleForReading, + filterEventsByNeedsAnyRead, + filterEventsByUserCanRead } = require('../lib/utils/reading') const { getShortName } = require('../lib/utils/participants') const { userRequestedPriors } = require('../lib/utils/prior-mammograms') @@ -39,10 +42,28 @@ module.exports = (router) => { // Reading index — choose layout based on setting router.get('/reading', (req, res) => { + const data = req.session.data + const currentUserId = data?.currentUser?.id const layout = req.session.data?.settings?.reading?.indexLayout || 'simple' const template = layout === 'complex' ? 'reading/index-complex' : 'reading/index-simple' - res.render(template) + + const sessionProgressById = {} + Object.values(data.readingSessions || {}).forEach((session) => { + const progress = getSessionReadingProgress( + data, + session.id, + null, + currentUserId + ) + if (progress) { + sessionProgressById[session.id] = progress + } + }) + + res.render(template, { + sessionProgressById + }) }) // Default clinics list to "mine" @@ -319,7 +340,16 @@ module.exports = (router) => { ) } - res.redirect(`/reading/session/${sessionId}`) + // Check if there are any readable cases left in the session + const firstReadable = getFirstUserReadableEvent( + sessionEvents, + data.currentUser.id + ) + if (firstReadable) { + res.redirect(`/reading/session/${sessionId}`) + } else { + res.redirect(`/reading/session/${sessionId}/no-more-cases`) + } }) // Route for skipped-review page (shown at end of batch when skipped cases remain) @@ -338,6 +368,19 @@ module.exports = (router) => { }) }) + // Route for no-more-cases page (shown when no more readable cases available in session) + router.get('/reading/session/:sessionId/no-more-cases', (req, res) => { + const data = req.session.data + const { sessionId } = req.params + const session = getReadingSession(data, sessionId) + if (!session) { + return res.redirect('/reading') + } + res.render('reading/no-more-cases', { + sessionId + }) + }) + // Route for viewing a session with specific view router.get('/reading/session/:sessionId/:view', (req, res) => { const data = req.session.data @@ -379,6 +422,13 @@ module.exports = (router) => { data.currentUser.id ) + const sessionProgress = getSessionReadingProgress( + data, + sessionId, + null, + data.currentUser.id + ) + // Find where the user should resume — first readable after the furthest // point they've reached (reads or skips), falling back to first readable const resumeEvent = getResumeEventForUser( @@ -410,13 +460,23 @@ module.exports = (router) => { clinic = data.clinics.find((c) => c.id === session.clinicId) } + // Overall backlog count — used to gate the 'Start a new session' button. + // Checks only cases the current user can actually read (not already read by them, + // not fully read by others, not deferred or awaiting priors). + const backlogTotal = filterEventsByUserCanRead( + filterEventsByEligibleForReading(data.events), + data.currentUser.id + ).length + res.render('reading/session', { session, events: enhancedEvents, readingStatus, + sessionProgress, resumeEvent, autoFinaliseAt, clinic, + backlogTotal, view: selectedView }) }) @@ -592,7 +652,16 @@ module.exports = (router) => { } else if (session.skippedEvents.length > 0) { res.redirect(`/reading/session/${sessionId}/skipped-review`) } else { - res.redirect(`/reading/session/${sessionId}`) + // Check if there are any readable cases left in the session + const firstReadable = getFirstUserReadableEvent( + sessionEvents, + currentUserId + ) + if (firstReadable) { + res.redirect(`/reading/session/${sessionId}`) + } else { + res.redirect(`/reading/session/${sessionId}/no-more-cases`) + } } }) @@ -669,7 +738,16 @@ module.exports = (router) => { modalBreakout(`/reading/session/${sessionId}/skipped-review`) ) } else { - res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + // Check if there are any readable cases left in the session + const firstReadable = getFirstUserReadableEvent( + sessionEvents, + currentUserId + ) + if (firstReadable) { + res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + } else { + res.redirect(modalBreakout(`/reading/session/${sessionId}/no-more-cases`)) + } } } ) @@ -783,7 +861,16 @@ module.exports = (router) => { modalBreakout(`/reading/session/${sessionId}/skipped-review`) ) } else { - res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + // Check if there are any readable cases left in the session + const firstReadable = getFirstUserReadableEvent( + sessionEvents, + currentUserId + ) + if (firstReadable) { + res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + } else { + res.redirect(modalBreakout(`/reading/session/${sessionId}/no-more-cases`)) + } } } ) @@ -1674,7 +1761,16 @@ module.exports = (router) => { modalBreakout(`/reading/session/${sessionId}/skipped-review`) ) } else { - res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + // Check if there are any readable cases left in the session + const firstReadable = getFirstUserReadableEvent( + sessionEvents, + currentUserId + ) + if (firstReadable) { + res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + } else { + res.redirect(modalBreakout(`/reading/session/${sessionId}/no-more-cases`)) + } } } ) diff --git a/app/views/reading/history.html b/app/views/reading/history.html index f3ef9a81..86c1b8ad 100644 --- a/app/views/reading/history.html +++ b/app/views/reading/history.html @@ -53,7 +53,8 @@
{{ backlogTotal }} cases require reading
+{{ userReadableTotal }} {{ "case" | pluralise(userReadableTotal) }} available to read
{% if inProgressSession %} @@ -117,10 +125,20 @@All cases have been read.
+ + {{ button({ + text: "Start now", + disabled: true + }) }} + {% else %}Give your opinion on the next {{ defaultSessionSize }} cases due for reading.
+Give your opinion on cases due for reading (up to {{ defaultSessionSize }}).
{{ button({ text: "Start now", diff --git a/app/views/reading/no-more-cases.html b/app/views/reading/no-more-cases.html new file mode 100644 index 00000000..41fbeacf --- /dev/null +++ b/app/views/reading/no-more-cases.html @@ -0,0 +1,32 @@ +{# /app/views/reading/no-more-cases.html #} + +{% extends 'layout-reading.html' %} + +{% set isReadingWorkflow = true %} +{% set hideStatusBar = true %} +{% set hideBackLink = true %} + +{% set pageHeading = "No more cases to read" %} + +{% block pageContent %} + +All eligible cases in this session have been read.
+ + + +An opinion has been provided for {{ readingStatus.userReadCount }} {{ "case" | pluralise(readingStatus.userReadCount) }}. The first opinion will be automatically finalised at {{ autoFinaliseAt | formatTime("h:mma") }}. Finalise opinions now
{% endset %} @@ -97,7 +99,8 @@Progress: {{ readingStatus.userReadCount }} read{%- if readingStatus.userAwaitingPriorsCount > 0 -%}, {{ readingStatus.userAwaitingPriorsCount }} awaiting priors{%- endif -%}{%- if deferredCount > 0 -%}, {{ deferredCount }} deferred{%- endif -%}, {{ userSessionRemainingCount }} remaining