diff --git a/app/assets/javascript/modal.js b/app/assets/javascript/modal.js index 671e4119..355df9b4 100644 --- a/app/assets/javascript/modal.js +++ b/app/assets/javascript/modal.js @@ -194,12 +194,15 @@ class AppModal { // forgot 'parentLayout or'. Bail out to a direct navigation rather than // injecting the full site into the modal. if (this.isFullPage(html)) { + // Capture the destination before close() — close() resets _loadUrl to + // null, so reading it after would navigate to a literal "null" URL. + const dest = this._loadUrl console.warn( 'Modal: full page returned — falling back to direct navigation', - this._loadUrl + dest ) this.close() - window.location.href = this._loadUrl + window.location.href = dest return } diff --git a/app/assets/sass/components/_annotate-v2.scss b/app/assets/sass/components/_annotate-v2.scss index 8ce421e0..94e2ea33 100644 --- a/app/assets/sass/components/_annotate-v2.scss +++ b/app/assets/sass/components/_annotate-v2.scss @@ -335,6 +335,7 @@ $ann-marker-shadow: // Cursor preview: yellow circle+plus that follows the mouse on image panels .app-ann-cursor-preview { position: absolute; + z-index: 4; // above zoom button (z-index: 3) and active markers (z-index: 2) width: $ann-marker-size; height: $ann-marker-size; border: $ann-marker-border-width solid $nhsuk-focus-colour; diff --git a/app/lib/generators/medical-information/symptoms-generator.js b/app/lib/generators/medical-information/symptoms-generator.js index 24b3b83f..4e79f84c 100644 --- a/app/lib/generators/medical-information/symptoms-generator.js +++ b/app/lib/generators/medical-information/symptoms-generator.js @@ -199,7 +199,7 @@ const generateSymptom = (options = {}) => { const dateTypeWeights = { ...Object.fromEntries(DATE_RANGE_OPTIONS.map((range) => [range, 0.1])), dateKnown: 0.3, - notSure: 0.1 + notKnown: 0.1 } // Generate basic symptom data matching form structure @@ -332,16 +332,16 @@ const generateSymptom = (options = {}) => { } } - // 20% chance of additional info + // 20% chance of symptom notes if (Math.random() < 0.2) { - const additionalInfoOptions = [ + const symptomNotesOptions = [ 'Noticed during self-examination', 'Partner noticed the change', 'Gets worse during certain times of month', 'No family history of breast problems', 'Concerned as mother had similar symptoms' ] - symptom.additionalInfo = faker.helpers.arrayElement(additionalInfoOptions) + symptom.symptomNotes = faker.helpers.arrayElement(symptomNotesOptions) } return symptom diff --git a/app/lib/generators/participant-generator.js b/app/lib/generators/participant-generator.js index 63d08361..c8c2c1be 100644 --- a/app/lib/generators/participant-generator.js +++ b/app/lib/generators/participant-generator.js @@ -103,20 +103,20 @@ const generatePhoneNumbers = () => { }) const result = { - mobilePhone: null, - homePhone: null + phone1: null, + phone2: null } switch (phoneConfig) { case 'mobile_only': - result.mobilePhone = generateUKMobileNumber() + result.phone1 = generateUKMobileNumber() break case 'both': - result.mobilePhone = generateUKMobileNumber() - result.homePhone = generateUKHomeNumber() + result.phone1 = generateUKMobileNumber() + result.phone2 = generateUKHomeNumber() break case 'home_only': - result.homePhone = generateUKHomeNumber() + result.phone2 = generateUKHomeNumber() break } @@ -216,8 +216,8 @@ const generateParticipant = ({ lastName: faker.person.lastName(), dateOfBirth: generateDateOfBirth(participantRiskLevel), address: generateBSUAppropriateAddress(assignedBSU), - mobilePhone: phoneNumbers.mobilePhone, - homePhone: phoneNumbers.homePhone, + phone1: phoneNumbers.phone1, + phone2: phoneNumbers.phone2, email: `${faker.internet.username().toLowerCase()}@example.com`, ethnicGroup: ethnicityData.ethnicGroup, ethnicBackground: ethnicityData.ethnicBackground diff --git a/app/lib/utils/prior-mammograms.js b/app/lib/utils/prior-mammograms.js index 2dc56105..86d753aa 100644 --- a/app/lib/utils/prior-mammograms.js +++ b/app/lib/utils/prior-mammograms.js @@ -129,21 +129,19 @@ const summarisePriorMammogram = (mammogram, options = {}) => { let location = '' switch (mammogram.location) { case 'bsu': - location = mammogram.bsu || 'NHS breast screening unit' + location = 'At another BSU' break case 'otherUk': - location = mammogram.otherUk || 'Other UK location' + location = 'Elsewhere in the UK' break case 'otherNonUk': - location = mammogram.otherNonUk - ? `Outside UK: ${mammogram.otherNonUk}` - : 'Outside UK' + location = 'Outside the UK' break case 'currentBsu': - location = unitName || 'Current BSU' + location = `At ${unitName || 'this BSU'}` break case 'preferNotToSay': - location = 'Location not given' + location = 'Location not provided' break default: location = '' @@ -152,14 +150,14 @@ const summarisePriorMammogram = (mammogram, options = {}) => { // Date detail — combine formatted date and relative time into parenthesised suffix const dateParts = [] if (mammogram.dateType === 'dateKnown' && mammogram.dateTaken) { - dateParts.push(formatDate(mammogram.dateTaken, 'MMMM YYYY')) + dateParts.push(formatDate(mammogram.dateTaken, 'MMM YYYY')) if (mammogram._rawDate) { dateParts.push(formatRelativeDate(mammogram._rawDate)) } } else if (mammogram.dateType === 'moreThanSixMonths') { - dateParts.push(mammogram.approximateDate || 'over 6 months ago') + dateParts.push('over 6 months ago') } else if (mammogram.dateType === 'lessThanSixMonths') { - dateParts.push(mammogram.approximateDate || 'less than 6 months ago') + dateParts.push('less than 6 months ago') } const dateDetail = dateParts.length > 0 ? `(${dateParts.join(', ')})` : '' diff --git a/app/lib/utils/reading.js b/app/lib/utils/reading.js index acc88ace..491b3b23 100644 --- a/app/lib/utils/reading.js +++ b/app/lib/utils/reading.js @@ -1366,6 +1366,16 @@ const shouldShowComparePage = function ( * @param {object} [options] - Options for determining eligibility * @returns {boolean} Whether the current user can read this event */ +/** + * Check if an event has been deferred from reading + * + * @param {object} event - The event to check + * @returns {boolean} Whether the event has been deferred + */ +const isDeferred = (event) => { + return !!event?.imageReading?.deferral?.deferredAt +} + const canUserReadEvent = function (event, userId = null, options = {}) { const { maxReadsPerEvent = 2 } = options @@ -1383,6 +1393,11 @@ const canUserReadEvent = function (event, userId = null, options = {}) { return false } + // Can't read if event has been deferred + if (isDeferred(event)) { + return false + } + const metadata = getReadingMetadata(event) // If we already have enough unique readers, no more reads needed @@ -1821,17 +1836,23 @@ const getSessionReadingProgress = ( const resolvedTargetSize = session.targetSize || sessionEvents.length + // Count deferred events so they count toward the session target + const deferredCount = sessionEvents.filter(isDeferred).length + return { ...progress, // How many events are currently loaded vs the overall target populatedCount: sessionEvents.length, targetSize: resolvedTargetSize, + // 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 - progress.userReadCount - - progress.userAwaitingPriorsCount + progress.userAwaitingPriorsCount - + deferredCount ) } } @@ -1884,6 +1905,7 @@ module.exports = { // Booleans userHasReadEvent, canUserReadEvent, + isDeferred, hasReads, needsArbitration, needsFirstRead, diff --git a/app/lib/utils/status.js b/app/lib/utils/status.js index 37e833ab..6937927e 100644 --- a/app/lib/utils/status.js +++ b/app/lib/utils/status.js @@ -283,6 +283,7 @@ const getStatusTagColour = (status) => { 'prior_pending': 'orange', 'prior_requested': 'yellow', 'priors_requested': 'yellow', + 'deferred': 'orange', 'prior_received': 'green', 'prior_not_available': 'grey', 'prior_not_needed': 'grey' diff --git a/app/lib/utils/strings.js b/app/lib/utils/strings.js index 5e58181c..f142468b 100644 --- a/app/lib/utils/strings.js +++ b/app/lib/utils/strings.js @@ -325,6 +325,8 @@ const formatPhoneNumber = (phoneNumber) => { if (!phoneNumber) return '' if (typeof phoneNumber !== 'string') return phoneNumber + phoneNumber = phoneNumber.replace(/\s/g, '') + if (phoneNumber.startsWith('07')) { return `${phoneNumber.slice(0, 5)} ${phoneNumber.slice(5)}` } diff --git a/app/routes/events.js b/app/routes/events.js index 1f5e5bd2..9ea633be 100644 --- a/app/routes/events.js +++ b/app/routes/events.js @@ -804,6 +804,17 @@ module.exports = (router) => { return res.redirect(modalBreakout(`/clinics/${clinicId}/`)) } + // Normalise approximateDate in session now, before any redirect, so the + // warning page doesn't display a raw array (e.g. ",June 2025") caused by + // both conditional inputs being submitted together. + if ( + previousMammogramTemp && + Array.isArray(previousMammogramTemp.approximateDate) + ) { + previousMammogramTemp.approximateDate = + previousMammogramTemp.approximateDate.find((v) => v) || '' + } + // Check if this is a recent mammogram (within 6 months) const isRecentMammogram = checkIfRecentMammogram(previousMammogramTemp) @@ -1131,17 +1142,12 @@ module.exports = (router) => { }) } - // Validate whether the symptom has been investigated (required) - if (!data.event?.symptomTemp?.hasBeenInvestigated) { - validationErrors.push({ - name: 'event[symptomTemp][hasBeenInvestigated]', - text: 'Select whether this has been investigated', - href: '#hasBeenInvestigated' - }) - } else if ( - data.event.symptomTemp.hasBeenInvestigated === 'yes' && - !data.event.symptomTemp.investigatedDescription - ) { + // Validate investigation details if the checkbox is checked + const hasBeenInvestigated = data.event?.symptomTemp?.hasBeenInvestigated + const isInvestigated = Array.isArray(hasBeenInvestigated) + ? hasBeenInvestigated.includes('yes') + : hasBeenInvestigated === 'yes' + if (isInvestigated && !data.event?.symptomTemp?.investigatedDescription) { validationErrors.push({ name: 'event[symptomTemp][investigatedDescription]', text: 'Provide details of the investigation', @@ -1210,8 +1216,7 @@ module.exports = (router) => { id: symptomTemp.id || generateId(), type: symptomType, dateType: symptomTemp.dateType, - hasBeenInvestigated: symptomTemp.hasBeenInvestigated, - additionalInfo: symptomTemp.additionalInfo + symptomNotes: symptomTemp.symptomNotes } // For new symptoms, add the creation timestamp @@ -1229,8 +1234,13 @@ module.exports = (router) => { } } - // Add investigation details if investigated - if (symptomTemp.hasBeenInvestigated === 'yes') { + // Normalise checkbox value and add investigation details if checked + const savedHasBeenInvestigated = symptomTemp.hasBeenInvestigated + const savedIsInvestigated = Array.isArray(savedHasBeenInvestigated) + ? savedHasBeenInvestigated.includes('yes') + : savedHasBeenInvestigated === 'yes' + symptom.hasBeenInvestigated = savedIsInvestigated ? 'yes' : 'no' + if (savedIsInvestigated) { symptom.investigatedDescription = symptomTemp.investigatedDescription } @@ -1247,7 +1257,7 @@ module.exports = (router) => { ].includes(symptomTemp.dateType) ) { symptom.approximateDuration = symptomTemp.dateType - } else if (symptomTemp.dateType === 'notSure') { + } else if (symptomTemp.dateType === 'notKnown') { delete symptom.approximateDuration } @@ -3342,6 +3352,17 @@ module.exports = (router) => { res.redirect(modalBreakout(returnUrl)) } ) + // Save participant data when contact details are updated from the participant tab. + // The contact-details form posts back to the participant tab URL via referrerChain, + // so we need this POST handler to persist the temp participant to the participants array. + router.post('/clinics/:clinicId/events/:eventId/participant', (req, res) => { + const { clinicId, eventId } = req.params + const data = req.session.data + saveTempParticipantToParticipant(data) + req.flash('success', 'Participant details updated') + res.redirect(`/clinics/${clinicId}/events/${eventId}/participant`) + }) + // General purpose dynamic template route for events // This should come after any more specific routes router.get( diff --git a/app/routes/reading.js b/app/routes/reading.js index 919c95cb..3631637b 100644 --- a/app/routes/reading.js +++ b/app/routes/reading.js @@ -40,9 +40,8 @@ module.exports = (router) => { // Reading index — choose layout based on setting router.get('/reading', (req, res) => { const layout = req.session.data?.settings?.reading?.indexLayout || 'simple' - const template = layout === 'complex' - ? 'reading/index-complex' - : 'reading/index-simple' + const template = + layout === 'complex' ? 'reading/index-complex' : 'reading/index-simple' res.render(template) }) @@ -550,6 +549,14 @@ module.exports = (router) => { ) } + // Check if event has been deferred from reading + const { isDeferred } = require('../lib/utils/reading') + if (isDeferred(event)) { + return res.redirect( + `/reading/session/${sessionId}/events/${eventId}/existing-read` + ) + } + // Delete temporary data from previous steps delete data.imageReadingTemp @@ -653,12 +660,16 @@ module.exports = (router) => { editHref: `/reading/session/${sessionId}/events/${eventId}/existing-read` } res.redirect( - `/reading/session/${sessionId}/events/${nextUnreadEvent.id}` + modalBreakout( + `/reading/session/${sessionId}/events/${nextUnreadEvent.id}` + ) ) } else if (session.skippedEvents.length > 0) { - res.redirect(`/reading/session/${sessionId}/skipped-review`) + res.redirect( + modalBreakout(`/reading/session/${sessionId}/skipped-review`) + ) } else { - res.redirect(`/reading/session/${sessionId}`) + res.redirect(modalBreakout(`/reading/session/${sessionId}`)) } } ) @@ -698,6 +709,143 @@ module.exports = (router) => { } ) + /************************************************************************ + // Case deferral + /***********************************************************************/ + + // Handle deferring a case from reading + router.post( + '/reading/session/:sessionId/events/:eventId/defer-case-answer', + (req, res) => { + const data = req.session.data + const { sessionId, eventId } = req.params + const currentUserId = data.currentUser?.id + + const reason = req.body.deferralReason || '' + + // Find the event and save deferral data + const event = data.events.find((e) => e.id === eventId) + if (event) { + if (!event.imageReading) { + event.imageReading = {} + } + + // Remove any existing read by this user — deferral replaces a prior opinion + if (event.imageReading.reads?.[currentUserId]) { + delete event.imageReading.reads[currentUserId] + } + + event.imageReading.deferral = { + deferredAt: new Date().toISOString(), + deferredBy: currentUserId, + reason: reason || null + } + + // Also update the mirrored event in data.event + if (data.event && data.event.id === eventId) { + data.event.imageReading = event.imageReading + } + } + + // Top up the session with the next eligible event if under target size + topUpSession(data, sessionId) + + // Find next readable event after current position + const session = getReadingSession(data, sessionId) + const sessionEvents = session.eventIds + .map((id) => data.events.find((e) => e.id === id)) + .filter(Boolean) + const nextUnreadEvent = getNextUserReadableEvent( + sessionEvents, + eventId, + currentUserId, + { wrap: false } + ) + + // Show a banner on the next case if there is one + if (nextUnreadEvent) { + const participant = data.participants.find( + (person) => person.id === event?.participantId + ) + const shortName = getShortName(participant) + data.readingOpinionBanner = { + text: `Case deferred for ${shortName}`, + participantName: shortName, + editHref: `/reading/session/${sessionId}/events/${eventId}/existing-read` + } + res.redirect( + modalBreakout( + `/reading/session/${sessionId}/events/${nextUnreadEvent.id}` + ) + ) + } else if (session.skippedEvents.length > 0) { + res.redirect( + modalBreakout(`/reading/session/${sessionId}/skipped-review`) + ) + } else { + res.redirect(modalBreakout(`/reading/session/${sessionId}`)) + } + } + ) + + // Undo a case deferral — removes the deferral so the case returns to reading + router.all( + '/reading/session/:sessionId/events/:eventId/undo-defer', + (req, res) => { + const data = req.session.data + const { sessionId, eventId } = req.params + + const event = data.events.find((e) => e.id === eventId) + if (event?.imageReading?.deferral) { + delete event.imageReading.deferral + + // Also update the mirrored event in data.event + if (data.event && data.event.id === eventId) { + data.event.imageReading = event.imageReading + } + } + + res.redirect(`/reading/session/${sessionId}/events/${eventId}/opinion`) + } + ) + + // Deferred cases management page + router.get('/reading/deferred', (req, res) => { + res.render('reading/deferred') + }) + + // Unflag a deferral from the deferred cases management page + // Keeps a record of the resolved deferral so the reason stays visible + router.post('/reading/deferred/undo', (req, res) => { + const data = req.session.data + const { eventId } = req.body + + const event = data.events.find((e) => e.id === eventId) + if (event?.imageReading?.deferral) { + if (!event.imageReading.deferralHistory) { + event.imageReading.deferralHistory = [] + } + event.imageReading.deferralHistory.push({ + ...event.imageReading.deferral, + resolvedAt: new Date().toISOString(), + resolvedBy: data.currentUser?.id + }) + delete event.imageReading.deferral + + if (data.event && data.event.id === eventId) { + data.event.imageReading = event.imageReading + } + + const participant = data.participants.find( + (p) => p.id === event.participantId + ) + const shortName = getShortName(participant) + req.flash('success', `${shortName} returned to reading queue`) + } + + res.redirect('/reading/deferred') + }) + // Render appropriate template for reading views router.get( '/reading/session/:sessionId/events/:eventId/:step', @@ -719,6 +867,7 @@ module.exports = (router) => { 'existing-read', 'compare', 'request-priors', + 'defer-case', 'medical-information' ] @@ -1311,7 +1460,8 @@ module.exports = (router) => { for (const side of ['right', 'left']) { const assessment = side === 'right' ? rightAssessment : leftAssessment - const annotations = side === 'right' ? rightAnnotations : leftAnnotations + const annotations = + side === 'right' ? rightAnnotations : leftAnnotations const sideLabel = side if (!assessment) continue @@ -1352,8 +1502,7 @@ module.exports = (router) => { }) } } - } - else if (assessment === 'normal' || assessment === 'clinical') { + } else if (assessment === 'normal' || assessment === 'clinical') { // Normal/clinical breast must not have annotations of M3 or higher if (highLevelAnnotations.length > 0) { errors.push({ diff --git a/app/views/_includes/forms/contact-details.njk b/app/views/_includes/forms/contact-details.njk index e42d1b68..00a50037 100644 --- a/app/views/_includes/forms/contact-details.njk +++ b/app/views/_includes/forms/contact-details.njk @@ -74,24 +74,24 @@ {{ input({ label: { - text: "Mobile (optional)", + text: "Phone number 1 (optional)", _classes: "nhsuk-label--m" }, - value: participant.demographicInformation.mobilePhone, - id: "mobile-phone-number", - name: "participant[demographicInformation][mobilePhone]", + value: participant.demographicInformation.phone1, + id: "phone-number-1", + name: "participant[demographicInformation][phone1]", classes: "nhsuk-u-width-one-half" }) }} {{ input({ label: { - text: "Home (optional)", + text: "Phone number 2 (optional)", _classes: "nhsuk-label--m" }, - value: participant.demographicInformation.homePhone, - id: "home-phone-number", - name: "participant[demographicInformation][homePhone]", + value: participant.demographicInformation.phone2, + id: "phone-number-2", + name: "participant[demographicInformation][phone2]", classes: "nhsuk-u-width-one-half" }) }} diff --git a/app/views/_includes/medical-information/index.njk b/app/views/_includes/medical-information/index.njk index eedbcb8e..b0849ca3 100644 --- a/app/views/_includes/medical-information/index.njk +++ b/app/views/_includes/medical-information/index.njk @@ -12,7 +12,7 @@ {# Mammogram history #} {% set sectionHeading = "Mammogram history" %} -{% set subHeading = "The last confirmed mammogram and any added manually since then" %} +{% set subHeading = "Previous mammograms from screening records and any reported by the participant" %} {% set sectionId = sectionHeading | kebabCase %} {% set scrollTo = sectionId %} @@ -275,4 +275,4 @@ html: otherRelevantInformationHtml, status: "To review" }) }} -{% endswitch %} \ No newline at end of file +{% endswitch %} diff --git a/app/views/_includes/medical-information/mammogram-history.njk b/app/views/_includes/medical-information/mammogram-history.njk index da6e6e47..46b52967 100644 --- a/app/views/_includes/medical-information/mammogram-history.njk +++ b/app/views/_includes/medical-information/mammogram-history.njk @@ -3,9 +3,6 @@ {% set hasAdditionalMammograms = event.previousMammograms | length > 0 %} {# System record - last known mammogram from BSU records #} -{% if hasAdditionalMammograms %} -

From screening records

-{% endif %} {% set systemRecordHtml %} {% set mostRecentClinic = data | getParticipantMostRecentClinic(participant.id) %} @@ -15,7 +12,7 @@ {{ mostRecentClinic.location.name }}
{{ mostRecentClinic.event.type | sentenceCase }} {% else %} - {{ "Not known" | asHint }} + {{ "No information" | asHint }} {% endif %} {% endset %} @@ -23,7 +20,7 @@ rows: [ { key: { - text: "Last known mammogram" + text: "Most recent mammogram on record" }, value: { html: systemRecordHtml @@ -126,4 +123,4 @@ {{ summaryList({ rows: userMammogramRows } | openInModal | removeLastRowBorder) }} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/app/views/_includes/reading/reading-status-bar.njk b/app/views/_includes/reading/reading-status-bar.njk index 1cb32a52..39623ce4 100644 --- a/app/views/_includes/reading/reading-status-bar.njk +++ b/app/views/_includes/reading/reading-status-bar.njk @@ -32,6 +32,9 @@ {%- if progress.userAwaitingPriorsCount > 0 -%} , {{ progress.userAwaitingPriorsCount }} awaiting priors {%- endif -%} + {%- if progress.deferredCount > 0 -%} + , {{ progress.deferredCount }} deferred + {%- endif -%} , {{ progress.targetRemaining }} remaining {%- if progress.skippedEvents | length > 0 %} ({{ progress.skippedEvents | length }} skipped) diff --git a/app/views/_includes/summary-lists/medical-information/symptoms/summary.njk b/app/views/_includes/summary-lists/medical-information/symptoms/summary.njk index 6b4ca60a..fe32d5b4 100644 --- a/app/views/_includes/summary-lists/medical-information/symptoms/summary.njk +++ b/app/views/_includes/summary-lists/medical-information/symptoms/summary.njk @@ -86,34 +86,35 @@ {{ symptom.dateStarted | formatMonthYear }} ({{ symptom.dateStarted | formatRelativeDate }}) {% elseif symptom.approximateDuration %} {{ symptom.approximateDuration }} ago - {% elseif symptom.dateType == 'notSure' %} - not sure + {% elseif symptom.dateType == 'notKnown' %} + not known {% else %} not provided {% endif %} {% endset %} {% set valueLines = valueLines | push(startText) %} - {# Stop date if applicable #} + {# Additional symptom information #} {% if symptom.isIntermittent %} {% set valueLines = valueLines | push("Symptom is intermittent") %} {% endif %} - {# Stop date if applicable #} {% if symptom.hasStopped and symptom.approximateDateStopped %} - {% set valueLines = valueLines | push("Stopped: " + symptom.approximateDateStopped) %} + {% set valueLines = valueLines | push("Recently resolved (" + symptom.approximateDateStopped + ")") %} {% endif %} {# Investigation status #} - {% if symptom.hasBeenInvestigated == "yes" and symptom.investigatedDescription %} - {% set valueLines = valueLines | push("Investigated: " + symptom.investigatedDescription) %} - {% elseif symptom.hasBeenInvestigated == "no" %} - {% set valueLines = valueLines | push("Not investigated") %} + {% if symptom.hasBeenInvestigated == "yes" %} + {% if symptom.investigatedDescription %} + {% set valueLines = valueLines | push("Previously investigated: " + symptom.investigatedDescription) %} + {% else %} + {% set valueLines = valueLines | push("Previously investigated") %} + {% endif %} {% endif %} - {# Additional info #} - {% if symptom.additionalInfo %} - {% set valueLines = valueLines | push("Additional info: " + symptom.additionalInfo) %} + {# Symptom notes #} + {% if symptom.symptomNotes %} + {% set valueLines = valueLines | push("Notes: " + symptom.symptomNotes) %} {% endif %} {# Join all value lines with line breaks #} diff --git a/app/views/_includes/summary-lists/rows/phone-numbers.njk b/app/views/_includes/summary-lists/rows/phone-numbers.njk index 345eebdd..9aee4346 100644 --- a/app/views/_includes/summary-lists/rows/phone-numbers.njk +++ b/app/views/_includes/summary-lists/rows/phone-numbers.njk @@ -1,6 +1,6 @@ {# /app/views/_includes/summary-lists/rows/phone-numbers.njk #} -{% set phoneNumbers = [participant.demographicInformation.mobilePhone, participant.demographicInformation.homePhone] | removeEmpty %} +{% set phoneNumbers = [participant.demographicInformation.phone1, participant.demographicInformation.phone2] | removeEmpty %} {% set phoneNumbersHtml %} {{ phoneNumbers | map("formatPhoneNumber") | join("
") | safe }} diff --git a/app/views/events/medical-information/symptoms/details.html b/app/views/events/medical-information/symptoms/details.html index eda86e94..771c23ee 100644 --- a/app/views/events/medical-information/symptoms/details.html +++ b/app/views/events/medical-information/symptoms/details.html @@ -467,13 +467,11 @@

}) %} {% set dateRadioItems = dateRadioItems | push({ - value: "notSure", - text: "Not sure" + value: "notKnown", + text: "Not known" }) %} - - - {# Radios for when were they taken - choices 'Date known' and 'Enter approximate date' and 'Not sure' #} + {# Radios for when were they taken - choices 'Date known' and 'Enter approximate date' and 'Not known' #} {{ radios({ idPrefix: "dateType", name: "event[symptomTemp][dateType]", @@ -503,18 +501,38 @@

} | populateErrors) }} {% endset %} - {# TODO: don't use br for spacing! #} -
+ {% set investigatedDescriptionHtml %} + {{ textarea({ + id: "investigatedDescription", + name: "event[symptomTemp][investigatedDescription]", + label: { + text: "Provide details" + }, + hint: { + text: "Include where, when and the outcome" + }, + value: event.symptomTemp.investigatedDescription + } | populateErrors) }} + {% endset %} - + {% call fieldset({ + legend: { + text: "Additional symptom information", + size: "m", + isPageHeading: false + } + }) %} {{ checkboxes({ idPrefix: "isIntermittent", name: "event[symptomTemp][isIntermittent]", + formGroup: { + classes: "nhsuk-u-margin-bottom-3" + }, items: [ { value: "yes", - text: "The symptom is intermittent", + text: "Intermittent", checked: true if event.symptomTemp.isIntermittent else false } ] @@ -523,10 +541,13 @@

{{ checkboxes({ idPrefix: "hasStopped", name: "event[symptomTemp][hasStopped]", + formGroup: { + classes: "nhsuk-u-margin-bottom-3" + }, items: [ { value: "yes", - text: "The symptom has recently resolved", + text: "Recently resolved", checked: true if event.symptomTemp.hasStopped else false, conditional: { html: stymptomStoppedHtml @@ -535,42 +556,22 @@

] }) }} - {{ radios({ - idPrefix: "hasBeenInvestigated", - name: "event[symptomTemp][hasBeenInvestigated]", - value: event.symptomTemp.hasBeenInvestigated, - fieldset: { - legend: { - text: "Has this been investigated?", - size: "m", - isPageHeading: false - } - }, - items: [ - { - value: "yes", - text: "Yes", - conditional: { - html: textarea({ - id: "investigatedDescription", - name: "event[symptomTemp][investigatedDescription]", - label: { - text: "Provide details" - }, - hint: { - text: "Include where, when and the outcome" - }, - value: event.symptomTemp.investigatedDescription, - _classes: "nhsuk-u-width-two-thirds" - } | populateErrors) + {{ checkboxes({ + idPrefix: "hasBeenInvestigated", + name: "event[symptomTemp][hasBeenInvestigated]", + values: event.symptomTemp.hasBeenInvestigated, + items: [ + { + value: "yes", + text: "Investigated by a medical professional", + conditional: { + html: investigatedDescriptionHtml + } } - }, - { - value: "no", - text: "No" - } - ] - } | populateErrors) }} + ] + }) }} + + {% endcall %} {# Only show if not significant by default #} {% if not symptomType.isSignificantByDefault %} @@ -611,29 +612,16 @@

] } | populateErrors) }} - {# {{ checkboxes({ - idPrefix: "isSignificant", - name: "event[symptomTemp][isSignificant]", - items: [ - { - value: "yes", - text: "Highlight this symptom to image readers", - checked: true if event.symptomTemp.isSignificant else false - } - ] - }) }} #} {% endif %} - - {{ textarea({ - name: "event[symptomTemp][additionalInfo]", + name: "event[symptomTemp][symptomNotes]", label: { - text: "Additional info (optional)", + text: "Symptom notes (optional)", size: "m" }, rows: 4, - value: event.symptomTemp.additionalInfo + value: event.symptomTemp.symptomNotes }) }}
diff --git a/app/views/events/previous-mammograms/appointment-should-not-take-place.html b/app/views/events/previous-mammograms/appointment-should-not-take-place.html new file mode 100644 index 00000000..8f1c660f --- /dev/null +++ b/app/views/events/previous-mammograms/appointment-should-not-take-place.html @@ -0,0 +1,66 @@ +{# app/views/events/previous-mammograms/appointment-should-not-begin.html #} + +{% extends parentLayout or 'layout-appointment.html' %} + +{% set previousMammogramCount = event.previousMammograms | length %} + +{% set pageHeading = "This appointment should not take place" %} + +{% set formAction = './save' | urlWithReferrer(referrerChain, query.scrollTo) %} + +{% block pageContent %} + + {% set previousMammograms = event.previousMammograms %} + + {% set unit = data.breastScreeningUnits | findById(clinic.breastScreeningUnitId) %} + +

+ + {{ participant | getFullName }} + + {{ pageHeading }} +

+ +

+ There is a mammogram on this participant's record from within the last 6 months. Breast x-rays should not be taken within 6 months of a previous mammogram. +

+ +{% set mammogramHistoryHtml %} + {% include "_includes/medical-information/mammogram-history.njk" %} +{% endset %} + + {{ radios({ + name: "event[appointmentStopped][needsReschedule]", + value: event.appointmentStopped.needsReschedule, + fieldset: { + legend: { + text: "Should the appointment be rescheduled?", + size: "m", + isPageHeading: false + } + }, + items: [ + { + value: "yes", + text: "Yes" + }, + { + value: "no-invite", + text: "No, invite to next routine appointment" + } + ] + } | populateErrors) }} + + {# Prevents redirect loop - signals to the save route that we've already shown this warning page #} + + +
+ {{ button({ + text: "Continue" + }) }} +
+ +

+ Proceed with this appointment +

+{% endblock %} diff --git a/app/views/events/previous-mammograms/form.html b/app/views/events/previous-mammograms/form.html index e1a9f166..78098e9b 100644 --- a/app/views/events/previous-mammograms/form.html +++ b/app/views/events/previous-mammograms/form.html @@ -289,7 +289,7 @@

}, value: event.previousMammogramTemp.otherDetails, hint: { - text: "For example, the reason for the mammograms and the outcome of the assessment" + text: "For example, the reason for the mammogram, the participant's previous address, and the outcome of the assessment" } }) }} diff --git a/app/views/reading/deferred.html b/app/views/reading/deferred.html new file mode 100644 index 00000000..22398762 --- /dev/null +++ b/app/views/reading/deferred.html @@ -0,0 +1,168 @@ +{# app/views/reading/deferred.html #} + +{% extends 'layout-app.html' %} + +{% from '_components/summary-list/macro.njk' import appSummaryList %} + +{% set pageHeading = "Deferred cases" %} +{% set gridColumn = "nhsuk-grid-column-two-thirds" %} + +{% set back = { + href: "/reading", + text: "Back" +} %} + +{% block pageContent %} + + {# Get all events that have been deferred #} + {% set deferredEvents = [] %} + {% for thisEvent in data.events %} + {% if thisEvent | isDeferred %} + {% set deferredEvents = deferredEvents | push(thisEvent) %} + {% endif %} + {% endfor %} + + {# Sort by deferral date, most recent first #} + {% set deferredEvents = deferredEvents | sort(true, false, 'imageReading.deferral.deferredAt') %} + + {# Collect resolved deferrals across all events, most recently resolved first #} + {% set resolvedDeferrals = [] %} + {% for thisEvent in data.events %} + {% for pastDeferral in thisEvent.imageReading.deferralHistory %} + {% set resolvedDeferrals = resolvedDeferrals | push({ event: thisEvent, deferral: pastDeferral }) %} + {% endfor %} + {% endfor %} + {% set resolvedDeferrals = resolvedDeferrals | sort(true, false, 'deferral.resolvedAt') %} + + Image reading +

{{ pageHeading }}

+ +

Cases deferred from reading require manual review before they can be returned to the reading queue.

+ + {% if deferredEvents | length == 0 %} +

No deferred cases.

+ {% else %} + + {% for thisEvent in deferredEvents %} + {% set thisParticipant = data | getParticipant(thisEvent.participantId) %} + {% set deferral = thisEvent.imageReading.deferral %} + + {% set screenedHtml %} + {% set daysSinceScreening = thisEvent.timing.startTime | daysSince %} + {% if daysSinceScreening >= data.config.reading.urgentThreshold %} + {{ "Urgent" | toTag }}
+ {% elseif daysSinceScreening >= data.config.reading.priorityThreshold %} + {{ "Due soon" | toTag }}
+ {% endif %} + {{ thisEvent.timing.startTime | formatDate }} + ({{ thisEvent.timing.startTime | formatRelativeDate }}) + {% endset %} + + {% set deferredHtml %} + {{ deferral.deferredAt | formatDate("D MMM YYYY") }} + ({{ deferral.deferredAt | formatRelativeDate }}) + {% endset %} + + {% set reasonHtml %} + {% if deferral.reason %} + {{ deferral.reason }} + {% else %} + No reason given + {% endif %} + {% if deferral.deferredBy %} +
+ + {{ deferral.deferredBy | getUsername({ format: "short", identifyCurrentUser: true }) }} + + {% endif %} + {% endset %} + +
+
+

+ + {{ thisParticipant | getFullName }} + +

+
+
+ + +
+
+
+
+ {{ appSummaryList({ + rows: [ + { key: { text: "Screened" }, value: { html: screenedHtml } }, + { key: { text: "Deferred" }, value: { html: deferredHtml } }, + { key: { text: "Reason" }, value: { html: reasonHtml } } + ] + }) }} +
+
+ {% endfor %} + + {% endif %} + + {% if resolvedDeferrals | length > 0 %} + +

Recently resolved

+

These cases have been unflagged and returned to the reading queue.

+ + {% for row in resolvedDeferrals %} + {% set thisEvent = row.event %} + {% set deferral = row.deferral %} + {% set thisParticipant = data | getParticipant(thisEvent.participantId) %} + + {% set deferredHtml %} + {{ deferral.deferredAt | formatDate("D MMM YYYY") }} + ({{ deferral.deferredAt | formatRelativeDate }}) + {% endset %} + + {% set reasonHtml %} + {% if deferral.reason %} + {{ deferral.reason }} + {% else %} + No reason given + {% endif %} + {% if deferral.deferredBy %} +
+ + {{ deferral.deferredBy | getUsername({ format: "short", identifyCurrentUser: true }) }} + + {% endif %} + {% endset %} + + {% set resolvedHtml %} + {{ deferral.resolvedAt | formatDate("D MMM YYYY") }} + {% if deferral.resolvedBy %} + by {{ deferral.resolvedBy | getUsername({ format: "short", identifyCurrentUser: true }) }} + {% endif %} + {% endset %} + +
+ +
+ {{ appSummaryList({ + rows: [ + { key: { text: "Deferred" }, value: { html: deferredHtml } }, + { key: { text: "Reason" }, value: { html: reasonHtml } }, + { key: { text: "Returned to queue" }, value: { html: resolvedHtml } } + ] + }) }} +
+
+ {% endfor %} + + {% endif %} + +{% endblock %} diff --git a/app/views/reading/index-complex.html b/app/views/reading/index-complex.html index 2ec79f03..5bd5475b 100644 --- a/app/views/reading/index-complex.html +++ b/app/views/reading/index-complex.html @@ -12,17 +12,21 @@

{{ pageHeading }}

Start new reading session

-{# Awaiting priors not automatically filtered #} +{# Awaiting priors — count across all eligible events, not just those needing reads #} +{% set awaitingPriorsEvents = [] %} +{% for thisEvent in data.events | filterEventsByEligibleForReading %} + {% if thisEvent | awaitingPriors %} + {% set awaitingPriorsEvents = awaitingPriorsEvents | push(thisEvent) %} + {% endif %} +{% endfor %} + {% set allReadsEventsWithAwaitingPriors = data.events | filterEventsByEligibleForReading | filterEventsByNeedsAnyRead | sortEventsByScreeningDate %} -{# Split into awaiting priors and available for reading #} +{# Split readable events into awaiting priors and available for reading #} {# Events with 'requested' status mammograms are held from reading #} {% set allReadsEvents = [] %} -{% set awaitingPriorsEvents = [] %} {% for thisEvent in allReadsEventsWithAwaitingPriors %} - {% if thisEvent | awaitingPriors %} - {% set awaitingPriorsEvents = awaitingPriorsEvents | push(thisEvent) %} - {% else %} + {% if not (thisEvent | awaitingPriors) %} {% set allReadsEvents = allReadsEvents | push(thisEvent) %} {% endif %} {% endfor %} @@ -70,7 +74,7 @@

Start new reading session

{% endif %} - {% set defaultSessionSize = 25 %} + {% set defaultSessionSize = data.settings.reading.defaultSessionSize or 25 %} {% set maxCases = allReadCount if allReadCount < defaultSessionSize else defaultSessionSize %} {% set actionLinkHtml %} @@ -172,6 +176,14 @@

Other options

href: "/reading/priors" }) }} +
  • + {{ card({ + heading: "Deferred cases", + headingClasses: "nhsuk-heading-s", + clickable: true, + href: "/reading/deferred" + }) }} +
  • {{ card({ heading: "Reading statistics", diff --git a/app/views/reading/index-simple.html b/app/views/reading/index-simple.html index 7e5bccab..21e702e5 100644 --- a/app/views/reading/index-simple.html +++ b/app/views/reading/index-simple.html @@ -7,7 +7,7 @@ {% set gridColumn = "none" %} {% set currentUserId = data.currentUser.id %} -{% set defaultSessionSize = 25 %} +{% set defaultSessionSize = data.settings.reading.defaultSessionSize or 25 %} {# Build a list of the current user's sessions, most-recent first. @@ -71,6 +71,20 @@ {% endif %} {% endfor %} +{% set deferredCount = 0 %} +{% for thisEvent in data.events %} + {% if thisEvent | isDeferred %} + {% set deferredCount = deferredCount + 1 %} + {% endif %} +{% endfor %} + +{% set awaitingPriorsCount = 0 %} +{% for thisEvent in data.events | filterEventsByEligibleForReading %} + {% if thisEvent | awaitingPriors %} + {% set awaitingPriorsCount = awaitingPriorsCount + 1 %} + {% endif %} +{% endfor %} + {% set newlyArrivedPriorsCount = 2 %} {% set actionTotal = arbitrationCount + newlyArrivedPriorsCount %} @@ -115,17 +129,31 @@

    Start image reading

    {% endif %} - {% if actionTotal > 0 %} + {# Awaiting priors inset #} + {% if awaitingPriorsCount > 0 %} + {% set insetHtml %} +

    {{ awaitingPriorsCount }} {{ "case" if awaitingPriorsCount == 1 else "cases" }} awaiting priors

    +

    These cases are on hold pending prior mammogram images.

    +

    Manage prior mammograms

    + {% endset %} + {{ insetText({ + html: insetHtml | safe + }) }} + {% endif %} + + {# Deferred cases inset #} + {% if deferredCount > 0 %} {% set insetHtml %} -

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

    -

    Flagged cases require action before they can be read.

    -

    Review flagged cases

    +

    {{ deferredCount }} deferred {{ "case" if deferredCount == 1 else "cases" }}

    +

    Deferred cases require review before they can be returned to the reading queue.

    +

    Review deferred cases

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

    Your previous image reading sessions