From a5dae5c0cdc818696ea5d62c6eb37906c66813eb Mon Sep 17 00:00:00 2001 From: rivalee Date: Wed, 27 May 2026 17:02:48 +0100 Subject: [PATCH 1/9] Add accession no to worklist status --- app/views/_includes/appointment-status-bar.njk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/_includes/appointment-status-bar.njk b/app/views/_includes/appointment-status-bar.njk index 15eaaefe..1a51ced9 100644 --- a/app/views/_includes/appointment-status-bar.njk +++ b/app/views/_includes/appointment-status-bar.njk @@ -15,12 +15,12 @@ {% if data.settings.screening.addedToWorklist == 'false' %} {{ appIcon("cross", { classes: "app-header-status__icon" }) }} - Not added to worklist (Retry) + Not added to worklist (ID: A123BC4) (Retry) {% else %} {{ appIcon("tick", { classes: "app-header-status__icon" }) }} - Added to worklist + Added to worklist (ID: A123BC4) {% endif %} {% endset %} From 057ae157d7df31c3247ad551db575345b4686487 Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 28 May 2026 11:35:47 +0100 Subject: [PATCH 2/9] Add scenarios and success messages --- .../javascript/retry-worklist-connection.js | 54 +++++++ app/routes/events.js | 143 ++++++++++++++++- .../_includes/appointment-status-bar.njk | 8 +- .../events/retry-worklist-connection.html | 52 ++++++ app/views/reading/index-simple.html | 149 ++++++++++++++++++ 5 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascript/retry-worklist-connection.js create mode 100644 app/views/events/retry-worklist-connection.html create mode 100644 app/views/reading/index-simple.html diff --git a/app/assets/javascript/retry-worklist-connection.js b/app/assets/javascript/retry-worklist-connection.js new file mode 100644 index 00000000..f4f103f0 --- /dev/null +++ b/app/assets/javascript/retry-worklist-connection.js @@ -0,0 +1,54 @@ +// app/assets/javascript/retry-worklist-connection.js +// +// Adds a brief "Attempting to reconnect" transient state to the Retry +// connection button so the user sees feedback before the page reloads. + +(function () { + const RECONNECT_DELAY_MS = 1500 + const button = document.querySelector('[data-retry-connection-button]') + if (!button) return + + const form = button.form + if (!form) return + + const originalText = button.textContent + + // Reset the button on initial load and when restored from bfcache, so it + // doesn't get stuck in the "Attempting to reconnect" disabled state after + // the post-redirect page load. + const resetButton = function () { + button.disabled = false + button.textContent = originalText + } + resetButton() + window.addEventListener('pageshow', resetButton) + + let isSubmitting = false + + form.addEventListener('submit', function (event) { + // Only intercept when the user clicked the Retry button (not the + // secondary "Switch to manual" button, which uses its own formaction). + if (event.submitter !== button) return + + // After the simulated reconnect delay we re-submit programmatically; + // let that submission through without re-intercepting it. + if (isSubmitting) return + + event.preventDefault() + + button.disabled = true + button.textContent = 'Attempting to reconnect' + + window.setTimeout(function () { + isSubmitting = true + // Re-enable so the value is submitted, then submit using the + // button's formaction. + button.disabled = false + if (typeof form.requestSubmit === 'function') { + form.requestSubmit(button) + } else { + form.submit() + } + }, RECONNECT_DELAY_MS) + }) +})() diff --git a/app/routes/events.js b/app/routes/events.js index 1903f4e5..642b45b1 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -5,6 +5,7 @@ const _ = require('lodash') const { getParticipant, getFullName, + getAge, saveTempParticipantToParticipant } = require('../lib/utils/participants') const { @@ -25,7 +26,7 @@ const { } = require('../lib/utils/referrers') const { createDynamicTemplateRoute } = require('../lib/utils/dynamic-routing') const { isAppointmentWorkflow } = require('../lib/utils/status') -const { sentenceCase } = require('../lib/utils/strings') +const { sentenceCase, formatNhsNumber } = require('../lib/utils/strings') const { getImageSetForEvent } = require('../lib/utils/mammogram-images') const { ensureSeedProfilesState, @@ -2228,10 +2229,133 @@ module.exports = (router) => { } ) + // Worklist connection retry routes + + // Helper: only allow same-origin app paths as return URLs. + const safeReturnUrl = (url) => { + if (typeof url !== 'string') return null + if (!url.startsWith('/')) return null + if (url.startsWith('//')) return null + return url + } + + // GET the retry page - capture where the user came from so we can send them + // back after a successful reconnect. + router.get('/clinics/:clinicId/events/:eventId/retry-worklist-connection', (req, res) => { + const data = req.session.data + + const fromQuery = safeReturnUrl(req.query.returnUrl) + if (fromQuery) { + data.worklistRetryReturnUrl = fromQuery + } + + res.render('events/retry-worklist-connection') + }) + + // Handle "Retry connection" button. + // The first attempt always fails (updates the "last retry attempt" time). + // The second (and any subsequent) attempt succeeds: marks the appointment as + // added to the worklist, flashes a success message, and returns the user to + // wherever they clicked Retry from. + router.post('/clinics/:clinicId/events/:eventId/retry-worklist-connection', (req, res) => { + const { clinicId, eventId } = req.params + const data = req.session.data + + data.settings = data.settings || {} + data.settings.screening = data.settings.screening || {} + + const attempts = (data.worklistRetryAttempts || 0) + 1 + + if (attempts >= 2) { + // Success: connection restored. + data.settings.screening.addedToWorklist = 'true' + delete data.settings.screening.worklistLastRetryAt + delete data.worklistRetryAttempts + + const returnUrl = + safeReturnUrl(data.worklistRetryReturnUrl) || + `/clinics/${clinicId}/events/${eventId}/take-images` + delete data.worklistRetryReturnUrl + + const participantName = getFullName(data.participant) + req.flash('success', { + html: `

${participantName} is now on the worklist

+

Image information will be sent automatically from the mammogram machine

` + }) + return res.redirect(returnUrl) + } + + // Failed attempt. + data.worklistRetryAttempts = attempts + data.settings.screening.worklistLastRetryAt = new Date().toISOString() + + res.redirect(`/clinics/${clinicId}/events/${eventId}/retry-worklist-connection`) + }) + + // Handle "Switch to manual image mode" button - enable manual mode, return + // the user to where they clicked Retry from, and flash a success banner + // explaining what they need to do on the mammogram machine. + router.post('/clinics/:clinicId/events/:eventId/switch-to-manual-image-mode', (req, res) => { + const { clinicId, eventId } = req.params + const data = req.session.data + + data.settings = data.settings || {} + data.settings.screening = data.settings.screening || {} + data.settings.screening.manualImageCollection = 'true' + + const returnUrl = + safeReturnUrl(data.worklistRetryReturnUrl) || + `/clinics/${clinicId}/events/${eventId}/take-images` + + delete data.worklistRetryAttempts + delete data.worklistRetryReturnUrl + delete data.settings.screening.worklistLastRetryAt + + const participant = data.participant || {} + const demographic = participant.demographicInformation || {} + const medical = participant.medicalInformation || {} + const participantName = getFullName(participant) + const nhsNumber = formatNhsNumber(medical.nhsNumber) + const dob = demographic.dateOfBirth + const dobFormatted = dob ? dayjs(dob).format('D MMMM YYYY') : '' + const age = getAge(participant) + const dobValue = dobFormatted + ? `${dobFormatted}${age ? ` (${age} years old)` : ''}` + : '' + + const html = ` +

Manual image mode enabled

+

Set up an unscheduled appointment for ${participantName} on the mammogram machine before taking images.

+

Add the following details so mammograms can be matched to the correct participant:

+
+
+
NHS number
+
${nhsNumber}
+
+
+
Full name
+
${participantName}
+
+
+
Date of birth
+
${dobValue}
+
+
` + + req.flash('success', { + title: 'Success', + html + }) + + res.redirect(returnUrl) + }) + // Manual imaging routes - // Handle take-images route - redirect to appropriate page based on state - router.get('/clinics/:clinicId/events/:eventId/take-images', (req, res) => { + // Handle take-images route - redirect to appropriate page based on state. + // Use `all` so the gate applies to the POST from review-medical-information + // as well as direct GET navigation. + router.all('/clinics/:clinicId/events/:eventId/take-images', (req, res) => { const { clinicId, eventId } = req.params const data = req.session.data @@ -2240,6 +2364,19 @@ module.exports = (router) => { const imagesStageCompleted = data.event?.workflowStatus?.['take-images'] === 'completed' + // Gate: if the appointment was not added to the worklist and the user + // hasn't yet switched to manual image mode, divert to the retry page + // before letting them into the image-taking step. + const isAddedToWorklist = + data.settings?.screening?.addedToWorklist !== 'false' + + if (!isAddedToWorklist && !isManualImageCollection) { + return res.redirect( + `/clinics/${clinicId}/events/${eventId}/retry-worklist-connection?returnUrl=` + + encodeURIComponent(`/clinics/${clinicId}/events/${eventId}/take-images`) + ) + } + // If manual flow and images already completed, redirect to details page for editing if ( isManualImageCollection && diff --git a/app/views/_includes/appointment-status-bar.njk b/app/views/_includes/appointment-status-bar.njk index 1a51ced9..cb403051 100644 --- a/app/views/_includes/appointment-status-bar.njk +++ b/app/views/_includes/appointment-status-bar.njk @@ -15,12 +15,16 @@ {% if data.settings.screening.addedToWorklist == 'false' %} {{ appIcon("cross", { classes: "app-header-status__icon" }) }} - Not added to worklist (ID: A123BC4) (Retry) + {% if data.settings.screening.manualImageCollection == 'true' %} + Not added to worklist (manual image mode enabled) + {% else %} + Not added to worklist (ID: KOX20260527A1246) Retry + {% endif %} {% else %} {{ appIcon("tick", { classes: "app-header-status__icon" }) }} - Added to worklist (ID: A123BC4) + Added to worklist (ID: KOX20260527A1246) {% endif %} {% endset %} diff --git a/app/views/events/retry-worklist-connection.html b/app/views/events/retry-worklist-connection.html new file mode 100644 index 00000000..8f8c6ad5 --- /dev/null +++ b/app/views/events/retry-worklist-connection.html @@ -0,0 +1,52 @@ +{# app/views/events/retry-worklist-connection.html #} + +{% extends 'layout-appointment.html' %} + +{% set hideBackLink = true %} +{% set activeWorkflowStep = 'take-images' %} +{% set activeTab = 'images' %} + +{% set pageHeading = (participant | getFullName) + " has not been added to the worklist" %} + +{% set gridColumn = "nhsuk-grid-column-two-thirds" %} + +{% block pageContent %} + + {{ backLink({ + href: "javascript:history.back();", + text: "Back", + classes: "nhsuk-u-margin-top-0 nhsuk-u-margin-bottom-4" + }) }} + +

{{ pageHeading }}

+ +

Due to a connection issue, image information for this appointment cannot be transferred automatically from the mammogram machine.

+ +
+
+ {{ button({ + text: "Retry connection", + attributes: { + formaction: "./retry-worklist-connection", + "data-retry-connection-button": "" + } + }) }} + + {{ button({ + text: "Switch to manual image mode", + classes: "nhsuk-button--secondary", + attributes: { + formaction: "./switch-to-manual-image-mode" + } + }) }} +
+
+ + {% set lastRetryAt = data.settings.screening.worklistLastRetryAt %} + {% if lastRetryAt %} +

Connection failed on retry. Last retry attempt at {{ lastRetryAt | formatTime('HH:mm:ss') }}

+ {% endif %} + + + +{% endblock %} diff --git a/app/views/reading/index-simple.html b/app/views/reading/index-simple.html new file mode 100644 index 00000000..ad66060d --- /dev/null +++ b/app/views/reading/index-simple.html @@ -0,0 +1,149 @@ +{# app/views/reading/index-simple.html #} + +{% extends 'layout-app.html' %} + +{% set pageHeading = "Image reading" %} +{% set hideBackLink = true %} +{% set gridColumn = "none" %} + +{% set currentUserId = data.currentUser.id %} +{% set defaultSessionSize = 25 %} + +{# + Build a list of the current user's sessions, most-recent first. + A session counts as "the user's" if they've already read at least one case in it. + Used both to detect an in-progress session and to list previous sessions. +#} +{% set userSessions = [] %} +{% for sessionId, session in data.readingSessions %} + {% set userReadCount = 0 %} + {% for eventId in session.eventIds %} + {% set thisEvent = data.events | find('id', eventId) %} + {% if thisEvent and (thisEvent | userHasReadEvent(currentUserId)) %} + {% set userReadCount = userReadCount + 1 %} + {% endif %} + {% endfor %} + {% if userReadCount > 0 %} + {% set targetSize = session.targetSize or session.eventIds | length %} + {% set userSessions = userSessions | push({ + id: session.id, + createdAt: session.createdAt, + targetSize: targetSize, + userReadCount: userReadCount, + isComplete: userReadCount >= targetSize + }) %} + {% endif %} +{% endfor %} + +{% set userSessions = userSessions | sort(true, false, 'createdAt') %} + +{# In-progress = the user's most recent session that isn't complete #} +{% set inProgressSession = null %} +{% for session in userSessions %} + {% if not inProgressSession and not session.isComplete %} + {% set inProgressSession = session %} + {% endif %} +{% endfor %} + +{# Previous (completed) sessions — show 3 most recent #} +{% set previousSessions = [] %} +{% for session in userSessions %} + {% if session.isComplete and previousSessions | length < 3 %} + {% set previousSessions = previousSessions | push(session) %} + {% endif %} +{% endfor %} + +{# + Inset counts. + + arbitrationCount is derived from real data — events whose computed outcome is + arbitration_pending. + + newlyArrivedPriorsCount is faked for the prototype. In a real implementation + this would be the count of cases where priors the reader requested or was + waiting on have since arrived. Edit the value below (or set it to 0) to see + the empty state of the inset. +#} +{% set arbitrationCount = 0 %} +{% for thisEvent in data.events %} + {% if (thisEvent | getOutcome) == 'arbitration_pending' %} + {% set arbitrationCount = arbitrationCount + 1 %} + {% endif %} +{% endfor %} + +{% set newlyArrivedPriorsCount = 2 %} + +{% set actionTotal = arbitrationCount + newlyArrivedPriorsCount %} + +{# + Backlog counts — all eligible cases that still need reading, banded by + age relative to the configured priority/urgent thresholds. +#} +{% set backlogEvents = data.events + | filterEventsByEligibleForReading + | filterEventsByNeedsAnyRead %} +{% set backlogTotal = backlogEvents | length %} +{% set urgentBacklog = backlogEvents | filterEventsByDayRange(data.config.reading.urgentThreshold) | length %} +{% set priorityBacklog = backlogEvents | filterEventsByDayRange(data.config.reading.priorityThreshold, data.config.reading.urgentThreshold - 1) | length %} + +{% block pageContent %} +
+
+

{{ pageHeading }}

+

{{ backlogTotal }} cases require reading

+ + {% if inProgressSession %} + +

Resume image reading

+

You have an image reading session in progress. Finish it before starting a new one.

+

{{ inProgressSession.userReadCount }} of {{ inProgressSession.targetSize }} cases read.

+ + {{ button({ + text: "Resume session", + href: "/reading/session/" + inProgressSession.id + "/resume" + }) }} + + {% else %} + +

Start image reading

+

Start a new image reading session with {{ defaultSessionSize }} cases.

+ + {{ button({ + text: "Start now", + href: "/reading/create-session?type=all_reads" + }) }} + + {% endif %} + + {% if actionTotal > 0 %} + {% set insetHtml %} +

{{ actionTotal }} flagged {{ "case" if actionTotal == 1 else "cases" }}

+

Flagged cases require action before they can be read.

+

Review flagged cases

+ {% endset %} + {{ insetText({ + html: insetHtml | safe + }) }} + {% endif %} + + {% if previousSessions | length > 0 %} +

Your previous image reading sessions

+ +

See all

+ {% endif %} + +
+ +
+
+
+ +{% endblock %} \ No newline at end of file From 86409399399be8cac551369b65281a37aaa06150 Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 28 May 2026 12:15:19 +0100 Subject: [PATCH 3/9] Add missing p tag --- app/views/reading/index-simple.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/reading/index-simple.html b/app/views/reading/index-simple.html index dd5fdc5b..4f3ffe62 100644 --- a/app/views/reading/index-simple.html +++ b/app/views/reading/index-simple.html @@ -106,7 +106,7 @@

Resume image reading

{% else %}

Start image reading

-Give your opinion on the next {{ defaultSessionSize }} cases due for reading.

+

Give your opinion on the next {{ defaultSessionSize }} cases due for reading.

{{ button({ text: "Start now", From 48a1592c81358ef4fc1a9016c8aeecf405094cef Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 28 May 2026 13:50:30 +0100 Subject: [PATCH 4/9] Update manual image page with content --- app/routes/events.js | 36 ++--------------------------- app/views/events/images-manual.html | 30 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/app/routes/events.js b/app/routes/events.js index 642b45b1..45456fe7 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -5,7 +5,6 @@ const _ = require('lodash') const { getParticipant, getFullName, - getAge, saveTempParticipantToParticipant } = require('../lib/utils/participants') const { @@ -26,7 +25,7 @@ const { } = require('../lib/utils/referrers') const { createDynamicTemplateRoute } = require('../lib/utils/dynamic-routing') const { isAppointmentWorkflow } = require('../lib/utils/status') -const { sentenceCase, formatNhsNumber } = require('../lib/utils/strings') +const { sentenceCase } = require('../lib/utils/strings') const { getImageSetForEvent } = require('../lib/utils/mammogram-images') const { ensureSeedProfilesState, @@ -2311,40 +2310,9 @@ module.exports = (router) => { delete data.worklistRetryReturnUrl delete data.settings.screening.worklistLastRetryAt - const participant = data.participant || {} - const demographic = participant.demographicInformation || {} - const medical = participant.medicalInformation || {} - const participantName = getFullName(participant) - const nhsNumber = formatNhsNumber(medical.nhsNumber) - const dob = demographic.dateOfBirth - const dobFormatted = dob ? dayjs(dob).format('D MMMM YYYY') : '' - const age = getAge(participant) - const dobValue = dobFormatted - ? `${dobFormatted}${age ? ` (${age} years old)` : ''}` - : '' - - const html = ` -

Manual image mode enabled

-

Set up an unscheduled appointment for ${participantName} on the mammogram machine before taking images.

-

Add the following details so mammograms can be matched to the correct participant:

-
-
-
NHS number
-
${nhsNumber}
-
-
-
Full name
-
${participantName}
-
-
-
Date of birth
-
${dobValue}
-
-
` - req.flash('success', { title: 'Success', - html + html: '

Manual image mode enabled

' }) res.redirect(returnUrl) diff --git a/app/views/events/images-manual.html b/app/views/events/images-manual.html index aabb1163..324e7769 100644 --- a/app/views/events/images-manual.html +++ b/app/views/events/images-manual.html @@ -4,7 +4,7 @@ {% set hideBackLink = true %} {% set activeWorkflowStep = 'take-images' %} -{% set pageHeading = "Record images taken" %} +{% set pageHeading = "Manually record image information" %} {% set gridColumn = "nhsuk-grid-column-two-thirds" %} @@ -79,6 +79,34 @@

{{ pageHeading }}

] }) }} #} + {# When the worklist connection failed and the user chose manual mode, + show the details they need to enter on the mammogram machine to set up + an unscheduled appointment. #} + {% if data.settings.screening.addedToWorklist == 'false' and data.settings.screening.manualImageCollection == 'true' %} +

Set up an unscheduled appointment on the mammogram machine before taking images.

+

Add the following details so your admin team can match mammograms to the correct participant once the appointment has been completed:

+ {{ summaryList({ + rows: [ + { + key: { text: "ID" }, + value: { text: "KOX20260527A1246" } + }, + { + key: { text: "NHS number" }, + value: { text: participant.medicalInformation.nhsNumber | formatNhsNumber } + }, + { + key: { text: "Full name" }, + value: { text: participant | getFullName } + }, + { + key: { text: "Date of birth" }, + value: { text: participant.demographicInformation.dateOfBirth | formatDate } + } + ] + }) }} + {% endif %} + {{ radios({ idPrefix: "imagingComplete", name: "event[mammogramDataTemp][isStandardSet]", From 97528cd4ca11748d3e188c4072fbc4dd08bd4beb Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 28 May 2026 15:43:48 +0100 Subject: [PATCH 5/9] Add reason capture when the user switches from troubleshooting content --- app/routes/events.js | 7 +++++++ app/views/events/images-manual.html | 28 ++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/routes/events.js b/app/routes/events.js index 45456fe7..3ea3be03 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -2310,6 +2310,13 @@ module.exports = (router) => { delete data.worklistRetryReturnUrl delete data.settings.screening.worklistLastRetryAt + // Clear any failover flag from a prior automatic→manual switch so we + // don't incorrectly show the "Reason for switching" input here (the + // reason is implicit when arriving via the retry-connection flow). + if (data.event?.mammogramDataTemp) { + delete data.event.mammogramDataTemp.isManualFailover + } + req.flash('success', { title: 'Success', html: '

Manual image mode enabled

' diff --git a/app/views/events/images-manual.html b/app/views/events/images-manual.html index 324e7769..3594a219 100644 --- a/app/views/events/images-manual.html +++ b/app/views/events/images-manual.html @@ -79,10 +79,13 @@

{{ pageHeading }}

] }) }} #} - {# When the worklist connection failed and the user chose manual mode, - show the details they need to enter on the mammogram machine to set up - an unscheduled appointment. #} - {% if data.settings.screening.addedToWorklist == 'false' and data.settings.screening.manualImageCollection == 'true' %} + {# Show unscheduled-appointment setup details whenever the appointment is + being handled in manual image collection mode. The reason-for-switching + input is only shown when the user manually switched mid-appointment via + the troubleshooting link on the automatic images page (failover). #} + {% set isManualFailover = event.mammogramDataTemp.isManualFailover %} + + {% if data.settings.screening.manualImageCollection == 'true' or isManualFailover %}

Set up an unscheduled appointment on the mammogram machine before taking images.

Add the following details so your admin team can match mammograms to the correct participant once the appointment has been completed:

{{ summaryList({ @@ -105,6 +108,23 @@

{{ pageHeading }}

} ] }) }} + + {% if isManualFailover %} + {% call details({ + summaryText: "Add reason for switching to manual" + }) %} + {{ textarea({ + label: { + text: "Reason for switching to manual", + classes: "nhsuk-label--s" + }, + id: "manualImageModeReason", + name: "event[mammogramDataTemp][manualImageModeReason]", + rows: 3, + value: mammogramSource.manualImageModeReason + }) }} + {% endcall %} + {% endif %} {% endif %} {{ radios({ From 5a2becf8bf0cf1ad343cf5259d4a11c5545ae7ee Mon Sep 17 00:00:00 2001 From: rivalee Date: Wed, 3 Jun 2026 11:55:21 +0100 Subject: [PATCH 6/9] Update troubleshooting content links and content variants for different issues --- app/routes/events.js | 17 +++++++++++++++++ .../_includes/images/image-troubleshooting.njk | 6 +++--- app/views/events/images-manual.html | 12 ++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/routes/events.js b/app/routes/events.js index 3ea3be03..59645204 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -2382,6 +2382,12 @@ module.exports = (router) => { router.get('/clinics/:clinicId/events/:eventId/images-manual', (req, res) => { const { clinicId, eventId } = req.params const data = req.session.data + const validTroubleshootingIssues = [ + 'worklist-participant', + 'wrong-image-count', + 'incorrect-image-labels' + ] + const troubleshootingIssue = req.query.issue // If mammogramData exists and is manual entry, prepopulate temp for editing if (data.event?.mammogramData?.isManualEntry) { @@ -2410,6 +2416,17 @@ module.exports = (router) => { } } + // Persist troubleshooting issue context when navigating from troubleshooting links + if (validTroubleshootingIssues.includes(troubleshootingIssue)) { + if (!data.event.mammogramDataTemp) { + data.event.mammogramDataTemp = {} + } + data.event.mammogramDataTemp.troubleshootingIssue = troubleshootingIssue + } else if (data.event?.mammogramDataTemp?.troubleshootingIssue) { + // Clear stale issue context for non-troubleshooting entry points + delete data.event.mammogramDataTemp.troubleshootingIssue + } + // Let the dynamic routing handle the actual rendering res.render('events/images-manual') }) diff --git a/app/views/_includes/images/image-troubleshooting.njk b/app/views/_includes/images/image-troubleshooting.njk index 562b8cbd..2db4334e 100644 --- a/app/views/_includes/images/image-troubleshooting.njk +++ b/app/views/_includes/images/image-troubleshooting.njk @@ -19,7 +19,7 @@

If this needs to be changed, update the mammogram machine and wait a few seconds for the image details to refresh.

-

If the issue is not resolved, enable manual image mode.

+

If the issue is not resolved, enable manual image mode.

Images details will be reconciled after the appointment.

@@ -29,7 +29,7 @@ summaryText: "The wrong number of images are displayed" }) %} -

Enable manual image mode.

+

Enable manual image mode.

Images details will be reconciled after the appointment.

@@ -41,7 +41,7 @@

Update image information on the mammogram machine and wait a few seconds for the image details to refresh.

-

If the issue is not resolved, enable manual image mode.

+

If the issue is not resolved, enable manual image mode.

Images details will be reconciled after the appointment.

diff --git a/app/views/events/images-manual.html b/app/views/events/images-manual.html index 3594a219..5e2148cb 100644 --- a/app/views/events/images-manual.html +++ b/app/views/events/images-manual.html @@ -84,10 +84,14 @@

{{ pageHeading }}

input is only shown when the user manually switched mid-appointment via the troubleshooting link on the automatic images page (failover). #} {% set isManualFailover = event.mammogramDataTemp.isManualFailover %} - - {% if data.settings.screening.manualImageCollection == 'true' or isManualFailover %} -

Set up an unscheduled appointment on the mammogram machine before taking images.

-

Add the following details so your admin team can match mammograms to the correct participant once the appointment has been completed:

+ {% set troubleshootingIssue = event.mammogramDataTemp.troubleshootingIssue %} + {% set hasTroubleshootingIssue = troubleshootingIssue in ['worklist-participant', 'wrong-image-count', 'incorrect-image-labels'] %} + {% set showWorklistMatchingContent = troubleshootingIssue == 'worklist-participant' %} + {% set showDefaultManualContent = data.settings.screening.manualImageCollection == 'true' or isManualFailover %} + + {% if (hasTroubleshootingIssue and showWorklistMatchingContent) or (not hasTroubleshootingIssue and showDefaultManualContent) %} +

Manually add participant details

+

Set up an unscheduled appointment using the following information so mammograms can be assigned correctly:

{{ summaryList({ rows: [ { From bc3452da7420f0a6e49e7fe8384cb96e6c7766a4 Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 4 Jun 2026 10:24:42 +0100 Subject: [PATCH 7/9] Refine workflow status info chunks --- app/assets/sass/_utils.scss | 4 +++ .../_includes/appointment-status-bar.njk | 26 ++++++++++++++----- app/views/events/images-manual.html | 6 ++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/assets/sass/_utils.scss b/app/assets/sass/_utils.scss index 00cef5bc..fa027ce2 100644 --- a/app/assets/sass/_utils.scss +++ b/app/assets/sass/_utils.scss @@ -17,3 +17,7 @@ .app-display-none { display: none; } + +.app-code-font { + font-family: $nhsuk-code-font; +} diff --git a/app/views/_includes/appointment-status-bar.njk b/app/views/_includes/appointment-status-bar.njk index cb403051..4ca23d25 100644 --- a/app/views/_includes/appointment-status-bar.njk +++ b/app/views/_includes/appointment-status-bar.njk @@ -4,7 +4,7 @@ {% set appointmentRowItems = [] %} {# Date and time #} -{% set appointmentHtml %} +{% set appointmentDateTimeHtml %} {{ clinic.date | formatDate }} at {{ event.timing.startTime | formatTimeString }} {% if event | isSpecialAppointment %} {{ tag({ @@ -12,25 +12,39 @@ classes: "nhsuk-tag--yellow nhsuk-u-margin-left-1" })}} {% endif %} +{% endset %} +{% set appointmentRowItems = appointmentRowItems | push({ + key: 'Appt:', + value: appointmentDateTimeHtml +}) %} + +{# Worklist accession number #} +{% set appointmentRowItems = appointmentRowItems | push({ + key: 'Accn:', + value: 'KOX 20260527 A1246' +}) %} + +{# Worklist status #} +{% set worklistStatusHtml %} {% if data.settings.screening.addedToWorklist == 'false' %} {{ appIcon("cross", { classes: "app-header-status__icon" }) }} {% if data.settings.screening.manualImageCollection == 'true' %} Not added to worklist (manual image mode enabled) {% else %} - Not added to worklist (ID: KOX20260527A1246) Retry + Not added to worklist Retry {% endif %} {% else %} {{ appIcon("tick", { classes: "app-header-status__icon" }) }} - Added to worklist (ID: KOX20260527A1246) + Added to worklist {% endif %} {% endset %} {% set appointmentRowItems = appointmentRowItems | push({ - key: "Appointment:", - value: appointmentHtml + key: 'Worklist status', + value: worklistStatusHtml }) %} {# Appointment type #} @@ -88,7 +102,7 @@ {# NHS Number #} {% set participantRowItems = participantRowItems | push({ key: "NHS:", - value: participant.medicalInformation.nhsNumber | formatNhsNumber + value: '' + (participant.medicalInformation.nhsNumber | formatNhsNumber | replace(' ', ' ')) + '' }) %} {{ appStatusBar({ diff --git a/app/views/events/images-manual.html b/app/views/events/images-manual.html index 5e2148cb..1e74204a 100644 --- a/app/views/events/images-manual.html +++ b/app/views/events/images-manual.html @@ -95,12 +95,12 @@

Manually add participant details

{{ summaryList({ rows: [ { - key: { text: "ID" }, - value: { text: "KOX20260527A1246" } + key: { html: 'Accn' }, + value: { html: 'KOX 20260527 A1246' } }, { key: { text: "NHS number" }, - value: { text: participant.medicalInformation.nhsNumber | formatNhsNumber } + value: { html: '' + (participant.medicalInformation.nhsNumber | formatNhsNumber | replace(' ', ' ')) + '' } }, { key: { text: "Full name" }, From 6c46de2711551d82ca1b05c9d6a008e7253b4d7e Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 4 Jun 2026 14:02:18 +0100 Subject: [PATCH 8/9] Fix status messages and spacing --- app/assets/sass/components/_status-bar.scss | 8 ++++++++ app/routes/events.js | 1 + app/views/_components/status-bar/template.njk | 2 +- .../_includes/appointment-status-bar.njk | 20 ++++++++----------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/assets/sass/components/_status-bar.scss b/app/assets/sass/components/_status-bar.scss index 480c19c8..19512280 100644 --- a/app/assets/sass/components/_status-bar.scss +++ b/app/assets/sass/components/_status-bar.scss @@ -28,6 +28,10 @@ align-items: center; } +.app-status-bar__row--compact { + gap: 10px; +} + .app-status-bar__row + .app-status-bar__row { margin-top: 8px; padding-top: 8px; @@ -45,6 +49,10 @@ opacity: 0.9; } +.app-status-bar__worklist-status { + margin-left: 4px; +} + .app-status-bar__viewer-link { margin-left: auto; } diff --git a/app/routes/events.js b/app/routes/events.js index 59645204..dfabd5da 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -2301,6 +2301,7 @@ module.exports = (router) => { data.settings = data.settings || {} data.settings.screening = data.settings.screening || {} data.settings.screening.manualImageCollection = 'true' + data.settings.screening.manualImageModeEnabledByUser = 'true' const returnUrl = safeReturnUrl(data.worklistRetryReturnUrl) || diff --git a/app/views/_components/status-bar/template.njk b/app/views/_components/status-bar/template.njk index b31f0a1a..b771bb61 100644 --- a/app/views/_components/status-bar/template.njk +++ b/app/views/_components/status-bar/template.njk @@ -6,7 +6,7 @@
{% for row in params.rows %} -
+
{% for item in row.items %}
{% if item.key and item.value %} diff --git a/app/views/_includes/appointment-status-bar.njk b/app/views/_includes/appointment-status-bar.njk index 4ca23d25..198bc561 100644 --- a/app/views/_includes/appointment-status-bar.njk +++ b/app/views/_includes/appointment-status-bar.njk @@ -18,33 +18,29 @@ value: appointmentDateTimeHtml }) %} -{# Worklist accession number #} -{% set appointmentRowItems = appointmentRowItems | push({ - key: 'Accn:', - value: 'KOX 20260527 A1246' -}) %} - {# Worklist status #} {% set worklistStatusHtml %} {% if data.settings.screening.addedToWorklist == 'false' %} {{ appIcon("cross", { classes: "app-header-status__icon" }) }} - {% if data.settings.screening.manualImageCollection == 'true' %} - Not added to worklist (manual image mode enabled) + {% if data.settings.screening.manualImageCollection == 'true' and data.settings.screening.manualImageModeEnabledByUser == 'true' %} + Worklist issue, manual mode enabled {% else %} - Not added to worklist Retry + Not on worklist (Retry) {% endif %} {% else %} {{ appIcon("tick", { classes: "app-header-status__icon" }) }} - Added to worklist + On worklist {% endif %} {% endset %} + +{# Worklist accession number with inline worklist status #} {% set appointmentRowItems = appointmentRowItems | push({ - key: 'Worklist status', - value: worklistStatusHtml + key: 'Accn:', + value: 'KOX 20260527 A1246' + worklistStatusHtml + '' }) %} {# Appointment type #} From 5bc1084684e2401920942a272ed41b7db3aee084 Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 4 Jun 2026 15:23:38 +0100 Subject: [PATCH 9/9] Fix bits and bobs --- app/routes/events.js | 14 +++++++++++-- app/routes/settings.js | 32 +++++++++++++++++++++++++++++ app/views/events/appointment.html | 11 ++++++++++ app/views/events/images-manual.html | 2 +- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/app/routes/events.js b/app/routes/events.js index dfabd5da..5dbb5ecb 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -2335,6 +2335,18 @@ module.exports = (router) => { const { clinicId, eventId } = req.params const data = req.session.data + const isAddedToWorklist = + data.settings?.screening?.addedToWorklist !== 'false' + + // When a participant is on the worklist, image transfer should use the + // automatic flow. Clear any stale manual-mode state from prior actions. + if (isAddedToWorklist) { + data.settings = data.settings || {} + data.settings.screening = data.settings.screening || {} + data.settings.screening.manualImageCollection = 'false' + delete data.settings.screening.manualImageModeEnabledByUser + } + const isManualImageCollection = data.settings?.screening?.manualImageCollection === 'true' const imagesStageCompleted = @@ -2343,8 +2355,6 @@ module.exports = (router) => { // Gate: if the appointment was not added to the worklist and the user // hasn't yet switched to manual image mode, divert to the retry page // before letting them into the image-taking step. - const isAddedToWorklist = - data.settings?.screening?.addedToWorklist !== 'false' if (!isAddedToWorklist && !isManualImageCollection) { return res.redirect( diff --git a/app/routes/settings.js b/app/routes/settings.js index b144deac..597dc8d7 100644 --- a/app/routes/settings.js +++ b/app/routes/settings.js @@ -64,6 +64,38 @@ const getCustomOverridesFromBody = (body = {}, fallbackProfile = {}) => { } module.exports = (router) => { + // Keep worklist simulation settings consistent with image collection mode: + // - "Not successful": clear any stale "manual mode enabled by user" marker + // so retry messaging is shown + // - "Successful": return to automatic image collection mode + router.get('/settings', (req, res, next) => { + const addedToWorklistFromQuery = + req.query?.settings?.screening?.addedToWorklist + + if (addedToWorklistFromQuery === 'false') { + if (!req.session.data.settings) { + req.session.data.settings = {} + } + if (!req.session.data.settings.screening) { + req.session.data.settings.screening = {} + } + + delete req.session.data.settings.screening.manualImageModeEnabledByUser + } else if (addedToWorklistFromQuery === 'true') { + if (!req.session.data.settings) { + req.session.data.settings = {} + } + if (!req.session.data.settings.screening) { + req.session.data.settings.screening = {} + } + + req.session.data.settings.screening.manualImageCollection = 'false' + delete req.session.data.settings.screening.manualImageModeEnabledByUser + } + + next() + }) + // Ensure any POST to settings resolves to a GET render router.post('/settings', (req, res) => { return res.redirect(303, '/settings') diff --git a/app/views/events/appointment.html b/app/views/events/appointment.html index c7762bad..9fb8f7d1 100644 --- a/app/views/events/appointment.html +++ b/app/views/events/appointment.html @@ -252,6 +252,17 @@ } | openInModal ] if event.status != "event_complete" else [] } + }, + { + key: { + text: "Accession number" + }, + value: { + html: 'KOX 20260527 A1246' + }, + actions: { + items: [] + } } ] } | handleSummaryListMissingInformation) }} diff --git a/app/views/events/images-manual.html b/app/views/events/images-manual.html index 1e74204a..b5ec4eb1 100644 --- a/app/views/events/images-manual.html +++ b/app/views/events/images-manual.html @@ -95,7 +95,7 @@

Manually add participant details

{{ summaryList({ rows: [ { - key: { html: 'Accn' }, + key: { text: "Accession number" }, value: { html: 'KOX 20260527 A1246' } }, {