From a696f3a13b78c4904a3d92669f6bb8b18a72c5f4 Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 25 Jun 2026 15:49:39 +0100 Subject: [PATCH 1/2] Empty states for image reading --- app/lib/utils/reading.js | 14 +++- app/routes/reading.js | 96 ++++++++++++++++++++++++++-- app/views/reading/history.html | 5 +- app/views/reading/index-simple.html | 16 ++++- app/views/reading/no-more-cases.html | 32 ++++++++++ app/views/reading/session.html | 5 +- 6 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 app/views/reading/no-more-cases.html 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..4c7afe26 100644 --- a/app/routes/reading.js +++ b/app/routes/reading.js @@ -39,10 +39,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 +337,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 +365,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 +419,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( @@ -414,6 +461,7 @@ module.exports = (router) => { session, events: enhancedEvents, readingStatus, + sessionProgress, resumeEvent, autoFinaliseAt, clinic, @@ -592,7 +640,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 +726,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 +849,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 +1749,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 @@

{{ pageHeading }}

{% for session in sessions %} {% set p = session.progress %} - {% set isComplete = p and session.userCompletedCount >= p.targetSize %} + {% set sessionTotal = p.effectiveTargetSize if p else null %} + {% set isComplete = p and session.userCompletedCount >= sessionTotal %} @@ -78,7 +79,7 @@

{{ pageHeading }}

{% if p %} - {{ p.targetSize }} total + {{ sessionTotal }} total {% if not isComplete %}
{{ session.userCompletedCount }} read {% endif %} diff --git a/app/views/reading/index-simple.html b/app/views/reading/index-simple.html index 21e702e5..d1c03664 100644 --- a/app/views/reading/index-simple.html +++ b/app/views/reading/index-simple.html @@ -8,6 +8,7 @@ {% set currentUserId = data.currentUser.id %} {% set defaultSessionSize = data.settings.reading.defaultSessionSize or 25 %} +{% set sessionProgressById = sessionProgressById or {} %} {# Build a list of the current user's sessions, most-recent first. @@ -24,7 +25,8 @@ {% endif %} {% endfor %} {% if userReadCount > 0 %} - {% set targetSize = session.targetSize or session.eventIds | length %} + {% set progress = sessionProgressById[session.id] %} + {% set targetSize = progress.effectiveTargetSize if progress else (session.targetSize or session.eventIds | length) %} {% set userSessions = userSessions | push({ id: session.id, createdAt: session.createdAt, @@ -117,10 +119,20 @@

Resume image reading

href: "/reading/session/" + inProgressSession.id + "/resume" }) }} + {% elif backlogTotal == 0 %} + +

There are no cases to read

+

All cases have been read.

+ + {{ button({ + text: "Start now", + disabled: true + }) }} + {% else %}

Start image reading

-

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 %} + +
+ +
+ +{% endblock %} diff --git a/app/views/reading/session.html b/app/views/reading/session.html index 5356a2bf..76957f98 100644 --- a/app/views/reading/session.html +++ b/app/views/reading/session.html @@ -97,7 +97,8 @@

}) }} {# Number of slots not yet populated in a lazy session #} - {% set pendingCount = (session.targetSize - session.eventIds | length) if session.targetSize else 0 %} + {% set sessionTotalCount = sessionProgress.effectiveTargetSize if sessionProgress else session.targetSize %} + {% set pendingCount = (sessionTotalCount - (session.eventIds | length)) if sessionTotalCount else 0 %} {% set pendingCount = 0 if pendingCount < 0 else pendingCount %} {% if view == 'your-reads' %} @@ -217,7 +218,7 @@

Opinion summary

{% endif %}

Reading session cases

- {% set userSessionRemainingCount = session.targetSize - readingStatus.userReadCount - readingStatus.userAwaitingPriorsCount - deferredCount %} + {% set userSessionRemainingCount = sessionTotalCount - readingStatus.userReadCount - readingStatus.userAwaitingPriorsCount - deferredCount %} {% set userSessionRemainingCount = 0 if userSessionRemainingCount < 0 else userSessionRemainingCount %} {% if userSessionRemainingCount > 0 or readingStatus.userAwaitingPriorsCount > 0 or deferredCount > 0 %}

Progress: {{ readingStatus.userReadCount }} read{%- if readingStatus.userAwaitingPriorsCount > 0 -%}, {{ readingStatus.userAwaitingPriorsCount }} awaiting priors{%- endif -%}{%- if deferredCount > 0 -%}, {{ deferredCount }} deferred{%- endif -%}, {{ userSessionRemainingCount }} remaining

From 5d01ae4af6359e7c40b2faa681c7bb93b9e2815a Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 25 Jun 2026 16:57:57 +0100 Subject: [PATCH 2/2] Remove new session button when all cases read, disable start button when all cases read --- app/routes/reading.js | 14 +++++++++++++- app/views/reading/index-simple.html | 10 ++++++++-- app/views/reading/session.html | 16 +++++++++------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/routes/reading.js b/app/routes/reading.js index 4c7afe26..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') @@ -457,6 +460,14 @@ 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, @@ -465,6 +476,7 @@ module.exports = (router) => { resumeEvent, autoFinaliseAt, clinic, + backlogTotal, view: selectedView }) }) diff --git a/app/views/reading/index-simple.html b/app/views/reading/index-simple.html index d1c03664..f5d5fce8 100644 --- a/app/views/reading/index-simple.html +++ b/app/views/reading/index-simple.html @@ -102,11 +102,17 @@ {% set urgentBacklog = backlogEvents | filterEventsByDayRange(data.config.reading.urgentThreshold) | length %} {% set priorityBacklog = backlogEvents | filterEventsByDayRange(data.config.reading.priorityThreshold, data.config.reading.urgentThreshold - 1) | length %} +{# Cases the current user can actually read — used to gate the start button #} +{% set userReadableTotal = data.events + | filterEventsByEligibleForReading + | filterEventsByUserCanRead(currentUserId) + | length %} + {% block pageContent %}

{{ pageHeading }}

-

{{ backlogTotal }} cases require reading

+

{{ userReadableTotal }} {{ "case" | pluralise(userReadableTotal) }} available to read

{% if inProgressSession %} @@ -119,7 +125,7 @@

Resume image reading

href: "/reading/session/" + inProgressSession.id + "/resume" }) }} - {% elif backlogTotal == 0 %} + {% elif userReadableTotal == 0 %}

There are no cases to read

All cases have been read.

diff --git a/app/views/reading/session.html b/app/views/reading/session.html index 76957f98..befc48ad 100644 --- a/app/views/reading/session.html +++ b/app/views/reading/session.html @@ -58,13 +58,15 @@

{% set sessionCompletePanelHtml %}

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

- {{ button({ - text: "Start a new session", - href: "/reading/create-session?type=all_reads", - variant: "reverse", - classes: "nhsuk-u-margin-bottom-0" - }) }} - Return to reading dashboard + {% if backlogTotal > 0 %} + {{ button({ + text: "Start a new session", + href: "/reading/create-session?type=all_reads", + variant: "reverse", + classes: "nhsuk-u-margin-bottom-0" + }) }} + {% endif %} + Return to reading dashboard
{% endset %}