Skip to content
Open
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
14 changes: 13 additions & 1 deletion app/lib/utils/reading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
110 changes: 103 additions & 7 deletions app/routes/reading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
})
})
Expand Down Expand Up @@ -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`)
}
}
})

Expand Down Expand Up @@ -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`))
}
}
}
)
Expand Down Expand Up @@ -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`))
}
}
}
)
Expand Down Expand Up @@ -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`))
}
}
}
)
Expand Down
5 changes: 3 additions & 2 deletions app/views/reading/history.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ <h1>{{ pageHeading }}</h1>
<tbody class="nhsuk-table__body">
{% 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 %}
<tr>
<td>
<a href="/reading/session/{{ session.id }}">
Expand All @@ -78,7 +79,7 @@ <h1>{{ pageHeading }}</h1>
</td>
<td>
{% if p %}
{{ p.targetSize }} total
{{ sessionTotal }} total
{% if not isComplete %}
<br>{{ session.userCompletedCount }} read
{% endif %}
Expand Down
24 changes: 21 additions & 3 deletions app/views/reading/index-simple.html
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -100,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 %}
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">
<h1>{{ pageHeading }}</h1>
<p>{{ backlogTotal }} cases require reading</p>
<p>{{ userReadableTotal }} {{ "case" | pluralise(userReadableTotal) }} available to read</p>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p>{{ userReadableTotal }} {{ "case" | pluralise(userReadableTotal) }} available to read</p>
<p class="nhsuk-u-secondary-text-colour nhsuk-body-l">{{ userReadableTotal }} {{ "case" | pluralise(userReadableTotal) }} are awaiting an outcome</p>

@dannychadburn dannychadburn Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related issue https://nhsd-jira.digital.nhs.uk/browse/DTOSS-13222

This distinguishes all reads from those available to the user to read - ie, "awaiting an outcome" could cover 1st or 2nd reads, in arbitration, awaiting priors, deferred, etc

Also, have suggested some visual distinction for this paragraph - lots of similar looking lines of text on the page currently


{% if inProgressSession %}

Expand All @@ -117,10 +125,20 @@ <h2>Resume image reading</h2>
href: "/reading/session/" + inProgressSession.id + "/resume"
}) }}

@dannychadburn dannychadburn Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}) }}
}) }}
<br>
<span class="nhsuk-u-secondary-text-colour app-nowrap nhsuk-body-s">{{ inProgressSession.userReadCount }} of {{ inProgressSession.targetSize }} cases read</span>

@dannychadburn dannychadburn Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest moving this below the start button to avoid lots of text paragraphs (similar suggestion made on the regular start page)


{% elif userReadableTotal == 0 %}

<h2>There are no cases to read</h2>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<h2>There are no cases to read</h2>
<h2>>There are no more cases available</h2>

@dannychadburn dannychadburn Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoiding duplicating 'read' and 'read' in heading and text

<p>All cases have been read.</p>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p>All cases have been read.</p>
<p>You have completed all the cases you are eligible to read.</p>


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

{% else %}

<h2>Start image reading</h2>
<p>Give your opinion on the next {{ defaultSessionSize }} cases due for reading.</p>
<p>Give your opinion on cases due for reading (up to {{ defaultSessionSize }}).</p>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p>Give your opinion on cases due for reading (up to {{ defaultSessionSize }}).</p>
<p>Give your opinion on cases due for reading.</p>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing mentions of any case numbers in this sentence as it doesn't flow well - suggest secondary text below the button to clarify what they session size is


{{ button({
text: "Start now",
Expand Down
32 changes: 32 additions & 0 deletions app/views/reading/no-more-cases.html
Original file line number Diff line number Diff line change
@@ -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" %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{% set pageHeading = "No more cases to read" %}
{% set pageHeading = "Session complete" %}


{% block pageContent %}

<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">

<span class="nhsuk-caption-l">Image reading</span>
<h1 class="nhsuk-heading-l">{{ pageHeading }}</h1>

<p>All eligible cases in this session have been read.</p>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p>All eligible cases in this session have been read.</p>
<p>You have given an opinion on all available cases.</p>


<div class="nhsuk-button-group">
{{ button({
text: "Review session outcomes",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
text: "Review session outcomes",
text: "See session overview",

href: "/reading/session/" + sessionId
}) }}
<a href="/reading" class="nhsuk-link nhsuk-button-group__item">Exit reading</a>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<a href="/reading" class="nhsuk-link nhsuk-button-group__item">Exit reading</a>
<a href="/reading" class="nhsuk-link nhsuk-button-group__item">Exit session</a>

</div>

</div>
</div>

{% endblock %}
21 changes: 12 additions & 9 deletions app/views/reading/session.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ <h1 class="nhsuk-heading-l">
{% set sessionCompletePanelHtml %}
<p class="nhsuk-body-m">An opinion has been provided for {{ readingStatus.userReadCount }} {{ "case" | pluralise(readingStatus.userReadCount) }}. The first opinion will be automatically finalised at {{ autoFinaliseAt | formatTime("h:mma") }}. <a href="/reading/clinics" class="nhsuk-link nhsuk-link--reverse">Finalise opinions now</a></p>
<div class="nhsuk-button-group nhsuk-u-margin-bottom-0">
{{ button({
text: "Start a new session",
href: "/reading/create-session?type=all_reads",
variant: "reverse",
classes: "nhsuk-u-margin-bottom-0"
}) }}
<a href="/dashboard" class="nhsuk-link nhsuk-link--reverse nhsuk-button-group__item">Return to reading dashboard</a>
{% 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 %}
<a href="/reading" class="nhsuk-link nhsuk-link--reverse nhsuk-button-group__item">Return to reading dashboard</a>
</div>
{% endset %}

Expand Down Expand Up @@ -97,7 +99,8 @@ <h1 class="nhsuk-heading-l">
}) }}

{# 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' %}
Expand Down Expand Up @@ -217,7 +220,7 @@ <h2>Opinion summary</h2>
{% endif %}

<h2>Reading session cases</h2>
{% 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 %}
<p class="nhsuk-body app-text-grey">Progress: {{ readingStatus.userReadCount }} read{%- if readingStatus.userAwaitingPriorsCount > 0 -%}, {{ readingStatus.userAwaitingPriorsCount }} awaiting priors{%- endif -%}{%- if deferredCount > 0 -%}, {{ deferredCount }} deferred{%- endif -%}, {{ userSessionRemainingCount }} remaining</p>
Expand Down