From 34ab2a2d1370871356a309a6061ac4c8aedc2aa1 Mon Sep 17 00:00:00 2001 From: rivalee Date: Wed, 22 Jul 2026 15:19:42 +0100 Subject: [PATCH 1/7] Redesign HRT, pregnancy, breastfeeding questions into something more simplified --- app/assets/javascript/main.js | 56 +++++++ app/lib/utils/medical-information.js | 64 ++------ .../appointments/medical-information.js | 95 +++++++++++ .../_includes/medical-information/index.njk | 14 +- .../other-relevant-information.njk | 151 ++++++------------ .../other-medical-information.html | 17 +- 6 files changed, 234 insertions(+), 163 deletions(-) diff --git a/app/assets/javascript/main.js b/app/assets/javascript/main.js index bfe28e13..3411f63e 100644 --- a/app/assets/javascript/main.js +++ b/app/assets/javascript/main.js @@ -95,6 +95,9 @@ document.addEventListener('DOMContentLoaded', () => { // Handle reset data in background setupResetSessionLink() + // Auto-save breast density factors when changed + setupBreastDensityFactorsAutosave() + // Reading workflow: auto-dismiss the opinion banner after a delay const opinionBanner = document.querySelector('[data-reading-opinion-banner]') if (opinionBanner) { @@ -185,6 +188,59 @@ document.addEventListener('DOMContentLoaded', () => { } }) +function setupBreastDensityFactorsAutosave() { + const saveTarget = document.querySelector('[data-breast-density-factors-save-url]') + if (!saveTarget) { + return + } + + const saveUrl = saveTarget.dataset.breastDensityFactorsSaveUrl + if (!saveUrl) { + return + } + + const selector = + 'input[name="appointment[medicalInformation][breastDensityFactors]"]' + const checkboxes = document.querySelectorAll(selector) + + if (checkboxes.length === 0) { + return + } + + const saveFactors = async () => { + const formData = new URLSearchParams() + const selectedCheckboxes = document.querySelectorAll(`${selector}:checked`) + + selectedCheckboxes.forEach((checkbox) => { + formData.append( + 'appointment[medicalInformation][breastDensityFactors]', + checkbox.value + ) + }) + + try { + const response = await fetch(saveUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: formData.toString() + }) + + if (!response.ok) { + throw new Error('Breast density factors auto-save failed') + } + } catch (error) { + console.error(error) + } + } + + checkboxes.forEach((checkbox) => { + checkbox.addEventListener('change', saveFactors) + }) +} + // Quick settings modal — press backtick (`) to open settings in a modal overlay. // On close, the page reloads to pick up any changes. document.addEventListener('keydown', (e) => { diff --git a/app/lib/utils/medical-information.js b/app/lib/utils/medical-information.js index 1834ab14..df9dbfae 100644 --- a/app/lib/utils/medical-information.js +++ b/app/lib/utils/medical-information.js @@ -429,59 +429,23 @@ const summariseOtherRelevantInformation = (medicalInformation) => { const summaries = [] - // HRT summary - const hrt = medicalInformation.hrt - if (hrt) { - if (hrt.hrtQuestion === 'yes') { - summaries.push( - `Taking HRT (started ${hrt.hrtDateStarted || 'date not specified'})` - ) - } else if (hrt.hrtQuestion === 'no-recently-stopped') { - if (hrt.hrtDateStopped) { - summaries.push(`Recently stopped HRT (stopped ${hrt.hrtDateStopped})`) - } else { - summaries.push('Recently stopped HRT') - } - } - // Don't add anything for 'no' - that's the default/negative state + const breastDensityFactorsRaw = medicalInformation.breastDensityFactors + const breastDensityFactors = Array.isArray(breastDensityFactorsRaw) + ? breastDensityFactorsRaw + : breastDensityFactorsRaw + ? [breastDensityFactorsRaw] + : [] + + if (breastDensityFactors.includes('hrt')) { + summaries.push('Taking HRT') } - // Pregnancy and breastfeeding summary - const pregBf = medicalInformation.pregnancyAndBreastfeeding - if (pregBf) { - // Pregnancy - if (pregBf.pregnancyStatus === 'yes') { - if (pregBf.pregnancyDueDate) { - summaries.push(`Pregnant (due ${pregBf.pregnancyDueDate})`) - } else { - summaries.push('Pregnant') - } - } else if (pregBf.pregnancyStatus === 'noButRecently') { - if (pregBf.pregnancyEndDate) { - summaries.push(`Recently pregnant (ended ${pregBf.pregnancyEndDate})`) - } else { - summaries.push('Recently pregnant') - } - } + if (breastDensityFactors.includes('pregnant')) { + summaries.push('Pregnant') + } - // Breastfeeding - if (pregBf.breastfeedingStatus === 'yes') { - if (pregBf.breastfeedingStartDate) { - summaries.push( - `Breastfeeding (started ${pregBf.breastfeedingStartDate})` - ) - } else { - summaries.push('Breastfeeding') - } - } else if (pregBf.breastfeedingStatus === 'recentlyStopped') { - if (pregBf.breastfeedingStopDate) { - summaries.push( - `Recently breastfeeding (stopped ${pregBf.breastfeedingStopDate})` - ) - } else { - summaries.push('Recently breastfeeding') - } - } + if (breastDensityFactors.includes('breastfeeding')) { + summaries.push('Breastfeeding') } // Other medical information (free text) diff --git a/app/routes/appointments/medical-information.js b/app/routes/appointments/medical-information.js index aee90aad..e77cdd3e 100644 --- a/app/routes/appointments/medical-information.js +++ b/app/routes/appointments/medical-information.js @@ -7,8 +7,103 @@ const { getReturnUrl, modalBreakout } = require('../../lib/utils/referrers') +const { getAppointmentData } = require('../../lib/utils/appointment-data') module.exports = (router) => { + // Auto-save breast density factors when checkboxes are changed + router.post( + '/clinics/:clinicId/appointments/:appointmentId/medical-information/breast-density-factors-save', + (req, res) => { + const data = req.session.data + const postedFactors = req.body?.appointment?.medicalInformation?.breastDensityFactors + + // No posted factors means all options are currently unchecked + const factors = Array.isArray(postedFactors) + ? postedFactors + : postedFactors + ? [postedFactors] + : [] + + if (!data.appointment) { + data.appointment = {} + } + + if (!data.appointment.medicalInformation) { + data.appointment.medicalInformation = {} + } + + data.appointment.medicalInformation.breastDensityFactors = factors + + // Keep a draft cache outside auto-stored form keys so later form posts + // cannot accidentally clear the latest autosaved checkbox state. + if (!data._medicalInformationDraft) { + data._medicalInformationDraft = {} + } + data._medicalInformationDraft.breastDensityFactors = factors + + res.status(204).send() + } + ) + + // Save other medical information while preserving breast density factors + router.post( + '/clinics/:clinicId/appointments/:appointmentId/medical-information/other-medical-information-save', + (req, res) => { + const { clinicId, appointmentId } = req.params + const data = req.session.data + const referrerChain = req.query.referrerChain + const scrollTo = req.query.scrollTo + + const appointmentMedicalInformation = data?.appointment?.medicalInformation + const postedBreastDensityFactors = + appointmentMedicalInformation?.breastDensityFactors + const cachedBreastDensityFactors = + data?._medicalInformationDraft?.breastDensityFactors + + const normalisedPostedBreastDensityFactors = + Array.isArray(postedBreastDensityFactors) + ? postedBreastDensityFactors + : postedBreastDensityFactors + ? [postedBreastDensityFactors] + : undefined + + if (normalisedPostedBreastDensityFactors) { + if (!data._medicalInformationDraft) { + data._medicalInformationDraft = {} + } + data._medicalInformationDraft.breastDensityFactors = + normalisedPostedBreastDensityFactors + } + + // If this submission did not include factors, keep the last saved values + if (!normalisedPostedBreastDensityFactors) + { + const savedAppointmentData = getAppointmentData(data, clinicId, appointmentId) + const savedBreastDensityFactors = + savedAppointmentData?.appointment?.medicalInformation?.breastDensityFactors + + if (cachedBreastDensityFactors) + { + data.appointment.medicalInformation.breastDensityFactors = + cachedBreastDensityFactors + } + else if (savedBreastDensityFactors) + { + data.appointment.medicalInformation.breastDensityFactors = + savedBreastDensityFactors + } + } + + const returnUrl = getReturnUrl( + `/clinics/${clinicId}/appointments/${appointmentId}/review-medical-information`, + referrerChain, + scrollTo + ) + + res.redirect(modalBreakout(returnUrl)) + } + ) + // Save breast features (includes converting JSON string to structured data) router.post( '/clinics/:clinicId/appointments/:appointmentId/medical-information/record-breast-features/save', diff --git a/app/views/_includes/medical-information/index.njk b/app/views/_includes/medical-information/index.njk index 78da700f..9237bfb3 100644 --- a/app/views/_includes/medical-information/index.njk +++ b/app/views/_includes/medical-information/index.njk @@ -229,19 +229,13 @@ {% include "_includes/summary-lists/medical-information/other-relevant-information.njk" %} {% endset %} -{% set otherRelevantInformationCount = 0 %} +{% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} -{% if appointment.medicalInformation.hrt.hrtQuestion == 'yes' %} - {% set otherRelevantInformationCount = otherRelevantInformationCount + 1 %} +{% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} + {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} {% endif %} -{% if appointment.medicalInformation.pregnancyAndBreastfeeding.pregnancyStatus == 'yes' %} - {% set otherRelevantInformationCount = otherRelevantInformationCount + 1 %} -{% endif %} - -{% if appointment.medicalInformation.pregnancyAndBreastfeeding.breastfeedingStatus == 'yes' %} - {% set otherRelevantInformationCount = otherRelevantInformationCount + 1 %} -{% endif %} +{% set otherRelevantInformationCount = selectedBreastDensityFactors | length if selectedBreastDensityFactors else 0 %} {% if otherRelevantInformationCount > 0 %} {% set otherRelevantInformationSummary = otherRelevantInformationCount ~ " other relevant information added" %} diff --git a/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk b/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk index 9e969635..871914b5 100644 --- a/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk +++ b/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk @@ -1,121 +1,68 @@ {# app/views/_includes/summary-lists/medical-information/other-relevant-information #} -{% set hrtHtml %} - {% set hrtData = appointment.medicalInformation.hrt %} +{% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} - {% if hrtData %} - {% if hrtData.hrtQuestion == 'yes' %} -

- Currently taking HRT -

-

- {# Example: "Started: September 2022" #} - Started: {{ hrtData.hrtDateStarted }} -

- {% elseif hrtData.hrtQuestion == 'no-recently-stopped' %} -

- Recently stopped taking HRT -

-

- Duration taken: {{ hrtData.hrtDurationBeforeStopping }}
- Date stopped: {{ hrtData.hrtDateStopped }}
-

- {% elseif hrtData.hrtQuestion == 'no' %} - Not taking HRT - {% else %} - No information provided - {% endif %} +{% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} + {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} +{% endif %} + +{% set breastDensityFactorsSummaryHtml %} + {% if selectedBreastDensityFactors and selectedBreastDensityFactors | length > 0 %} + {% else %} - {{ valueHtml }} +

No breast density factors added

{% endif %} {% endset %} -{% set pregnancyAndBreastfeedingHtml %} - {% set pregnancyAndBreastfeedingData = appointment.medicalInformation.pregnancyAndBreastfeeding %} - - {% if pregnancyAndBreastfeedingData %} - {% if pregnancyAndBreastfeedingData.pregnancyStatus == 'yes' %} -

- Currently pregnant -

-

- {# Example: "Due date: June 2026" #} - Due date: {{ pregnancyAndBreastfeedingData.pregnancyDueDate }} -

- {% elseif pregnancyAndBreastfeedingData.pregnancyStatus == 'noButRecently' %} -

- Recently pregnant -

-

- Pregnancy ended: {{ pregnancyAndBreastfeedingData.pregnancyEndDate }} -

- {% elseif pregnancyAndBreastfeedingData.pregnancyStatus == 'noNotPregnant' %} -

Not pregnant

- {% else %} -

No information provided

- {% endif %} - - {% if pregnancyAndBreastfeedingData.breastfeedingStatus == 'yes' %} -

- Currently breastfeeding -

-

- {# Example: "Started: January 2026" #} - Started: {{ pregnancyAndBreastfeedingData.breastfeedingStartDate }} -

- {% elseif pregnancyAndBreastfeedingData.breastfeedingStatus == 'recentlyStopped' %} -

- Recently breastfeeding -

-

- Date stopped: {{ pregnancyAndBreastfeedingData.breastfeedingStopDate }} -

- {% elseif pregnancyAndBreastfeedingData.breastfeedingStatus == 'no' %} -

Not breastfeeding

- {% else %} -

No information provided

- {% endif %} - {% else %} - {{ valueHtml }} - {% endif %} +{% set breastDensityFactorsInputHtml %} +
+ {{ checkboxes({ + name: "appointment[medicalInformation][breastDensityFactors]", + values: selectedBreastDensityFactors, + classes: "nhsuk-checkboxes--small", + hint: { + text: "Select any current or recent considerations" + }, + items: [ + { + value: "hrt", + text: "HRT (hormone replacement therapy)" + }, + { + value: "pregnant", + text: "Pregnant" + }, + { + value: "breastfeeding", + text: "Breastfeeding" + } + ] + }) }} {% endset %} {{ summaryList({ rows: [ { key: { - text: "Hormone replacement therapy (HRT)" + text: "Breast density factors" }, value: { - html: hrtHtml - }, - actions: { - items: [ - { - href: contextUrl + "/medical-information/hormone-replacement-therapy" | urlWithReferrer(referrerChain | appendReferrer(currentUrl), scrollTo), - text: "Change", - visuallyHiddenText: "hormone replacement therapy (HRT)" - } - ] - } if allowEdits - }, - { - key: { - text: "Pregnancy and breastfeeding" - }, - value: { - html: pregnancyAndBreastfeedingHtml - }, - actions: { - items: [ - { - href: contextUrl + "/medical-information/pregnancy-and-breastfeeding" | urlWithReferrer(referrerChain | appendReferrer(currentUrl), scrollTo), - text: "Change", - visuallyHiddenText: "pregnancy and breastfeeding" - } - ] - } if allowEdits + html: breastDensityFactorsInputHtml if allowEdits else breastDensityFactorsSummaryHtml + } }, { key: { diff --git a/app/views/appointments/medical-information/other-medical-information.html b/app/views/appointments/medical-information/other-medical-information.html index a83beb76..77d881d4 100644 --- a/app/views/appointments/medical-information/other-medical-information.html +++ b/app/views/appointments/medical-information/other-medical-information.html @@ -6,10 +6,16 @@ {% set gridColumn = "nhsuk-grid-column-two-thirds" %} -{% set formAction = './../review-medical-information' | getReturnUrl(referrerChain, query.scrollTo) %} +{% set formAction = './other-medical-information-save' | urlWithReferrer(referrerChain, query.scrollTo) %} {% block pageContent %} + {% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} + + {% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} + {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} + {% endif %} +

{{ participant | getFullName }} @@ -26,6 +32,15 @@

rows: 10 }) }} + {% if selectedBreastDensityFactors %} + {% for factor in selectedBreastDensityFactors %} + {{ appHiddenInput({ + name: "appointment[medicalInformation][breastDensityFactors]", + value: factor + }) }} + {% endfor %} + {% endif %} + {{ button({ text: "Save" }) }} From ff0fa2d3fccc9e368fa9a86851386ccc3228c349 Mon Sep 17 00:00:00 2001 From: rivalee Date: Mon, 27 Jul 2026 10:01:49 +0100 Subject: [PATCH 2/7] Split other relevant medical info into two sections and two review pages --- app/assets/javascript/main.js | 23 ++++- app/lib/utils/medical-information.js | 13 ++- .../appointments/medical-information.js | 46 +++++++++- .../_includes/medical-information/index.njk | 86 +++++++++++------- .../summary-lists/medical-info-summary.njk | 69 ++++++++++---- .../breast-density-factors.njk | 90 +++++++++++++++++++ .../other-relevant-information.njk | 64 +------------ app/views/appointments/check-information.html | 20 ++++- .../breast-density-factors.html | 45 ++++++++++ .../other-relevant-information.html | 12 ++- .../other-medical-information.html | 22 ++++- 11 files changed, 354 insertions(+), 136 deletions(-) create mode 100644 app/views/_includes/summary-lists/medical-information/breast-density-factors.njk create mode 100644 app/views/appointments/confirm-information/breast-density-factors.html diff --git a/app/assets/javascript/main.js b/app/assets/javascript/main.js index 3411f63e..416077ca 100644 --- a/app/assets/javascript/main.js +++ b/app/assets/javascript/main.js @@ -199,17 +199,21 @@ function setupBreastDensityFactorsAutosave() { return } - const selector = + const checkboxSelector = 'input[name="appointment[medicalInformation][breastDensityFactors]"]' - const checkboxes = document.querySelectorAll(selector) + const hrtRadioSelector = + 'input[name="appointment[medicalInformation][breastDensityFactorsHrt]"]' + const checkboxes = document.querySelectorAll(checkboxSelector) + const hrtRadios = document.querySelectorAll(hrtRadioSelector) - if (checkboxes.length === 0) { + if (checkboxes.length === 0 && hrtRadios.length === 0) { return } const saveFactors = async () => { const formData = new URLSearchParams() - const selectedCheckboxes = document.querySelectorAll(`${selector}:checked`) + const selectedCheckboxes = document.querySelectorAll(`${checkboxSelector}:checked`) + const selectedHrtRadio = document.querySelector(`${hrtRadioSelector}:checked`) selectedCheckboxes.forEach((checkbox) => { formData.append( @@ -218,6 +222,13 @@ function setupBreastDensityFactorsAutosave() { ) }) + if (selectedHrtRadio) { + formData.append( + 'appointment[medicalInformation][breastDensityFactorsHrt]', + selectedHrtRadio.value + ) + } + try { const response = await fetch(saveUrl, { method: 'POST', @@ -239,6 +250,10 @@ function setupBreastDensityFactorsAutosave() { checkboxes.forEach((checkbox) => { checkbox.addEventListener('change', saveFactors) }) + + hrtRadios.forEach((radio) => { + radio.addEventListener('change', saveFactors) + }) } // Quick settings modal — press backtick (`) to open settings in a modal overlay. diff --git a/app/lib/utils/medical-information.js b/app/lib/utils/medical-information.js index df9dbfae..df79a46f 100644 --- a/app/lib/utils/medical-information.js +++ b/app/lib/utils/medical-information.js @@ -435,10 +435,9 @@ const summariseOtherRelevantInformation = (medicalInformation) => { : breastDensityFactorsRaw ? [breastDensityFactorsRaw] : [] - - if (breastDensityFactors.includes('hrt')) { - summaries.push('Taking HRT') - } + const breastDensityFactorsHrt = + medicalInformation.breastDensityFactorsHrt || + (breastDensityFactors.includes('hrt') ? 'yes' : undefined) if (breastDensityFactors.includes('pregnant')) { summaries.push('Pregnant') @@ -448,6 +447,12 @@ const summariseOtherRelevantInformation = (medicalInformation) => { summaries.push('Breastfeeding') } + if (breastDensityFactorsHrt === 'yes') { + summaries.push('Taking HRT') + } else if (breastDensityFactorsHrt === 'no') { + summaries.push('Not taking HRT') + } + // Other medical information (free text) if (medicalInformation.otherMedicalInformation) { // Truncate if very long, otherwise show as-is diff --git a/app/routes/appointments/medical-information.js b/app/routes/appointments/medical-information.js index e77cdd3e..9fade30a 100644 --- a/app/routes/appointments/medical-information.js +++ b/app/routes/appointments/medical-information.js @@ -16,6 +16,7 @@ module.exports = (router) => { (req, res) => { const data = req.session.data const postedFactors = req.body?.appointment?.medicalInformation?.breastDensityFactors + const postedHrt = req.body?.appointment?.medicalInformation?.breastDensityFactorsHrt // No posted factors means all options are currently unchecked const factors = Array.isArray(postedFactors) @@ -23,6 +24,7 @@ module.exports = (router) => { : postedFactors ? [postedFactors] : [] + const nonHrtFactors = factors.filter((factor) => factor !== 'hrt') if (!data.appointment) { data.appointment = {} @@ -32,14 +34,22 @@ module.exports = (router) => { data.appointment.medicalInformation = {} } - data.appointment.medicalInformation.breastDensityFactors = factors + data.appointment.medicalInformation.breastDensityFactors = nonHrtFactors + + if (postedHrt === 'yes' || postedHrt === 'no') { + data.appointment.medicalInformation.breastDensityFactorsHrt = postedHrt + } // Keep a draft cache outside auto-stored form keys so later form posts // cannot accidentally clear the latest autosaved checkbox state. if (!data._medicalInformationDraft) { data._medicalInformationDraft = {} } - data._medicalInformationDraft.breastDensityFactors = factors + data._medicalInformationDraft.breastDensityFactors = nonHrtFactors + + if (postedHrt === 'yes' || postedHrt === 'no') { + data._medicalInformationDraft.breastDensityFactorsHrt = postedHrt + } res.status(204).send() } @@ -57,8 +67,12 @@ module.exports = (router) => { const appointmentMedicalInformation = data?.appointment?.medicalInformation const postedBreastDensityFactors = appointmentMedicalInformation?.breastDensityFactors + const postedBreastDensityFactorsHrt = + appointmentMedicalInformation?.breastDensityFactorsHrt const cachedBreastDensityFactors = data?._medicalInformationDraft?.breastDensityFactors + const cachedBreastDensityFactorsHrt = + data?._medicalInformationDraft?.breastDensityFactorsHrt const normalisedPostedBreastDensityFactors = Array.isArray(postedBreastDensityFactors) @@ -72,7 +86,15 @@ module.exports = (router) => { data._medicalInformationDraft = {} } data._medicalInformationDraft.breastDensityFactors = - normalisedPostedBreastDensityFactors + normalisedPostedBreastDensityFactors.filter((factor) => factor !== 'hrt') + } + + if (postedBreastDensityFactorsHrt === 'yes' || postedBreastDensityFactorsHrt === 'no') { + if (!data._medicalInformationDraft) { + data._medicalInformationDraft = {} + } + data._medicalInformationDraft.breastDensityFactorsHrt = + postedBreastDensityFactorsHrt } // If this submission did not include factors, keep the last saved values @@ -94,6 +116,24 @@ module.exports = (router) => { } } + if (!postedBreastDensityFactorsHrt) + { + const savedAppointmentData = getAppointmentData(data, clinicId, appointmentId) + const savedBreastDensityFactorsHrt = + savedAppointmentData?.appointment?.medicalInformation?.breastDensityFactorsHrt + + if (cachedBreastDensityFactorsHrt) + { + data.appointment.medicalInformation.breastDensityFactorsHrt = + cachedBreastDensityFactorsHrt + } + else if (savedBreastDensityFactorsHrt) + { + data.appointment.medicalInformation.breastDensityFactorsHrt = + savedBreastDensityFactorsHrt + } + } + const returnUrl = getReturnUrl( `/clinics/${clinicId}/appointments/${appointmentId}/review-medical-information`, referrerChain, diff --git a/app/views/_includes/medical-information/index.njk b/app/views/_includes/medical-information/index.njk index 9237bfb3..0091efeb 100644 --- a/app/views/_includes/medical-information/index.njk +++ b/app/views/_includes/medical-information/index.njk @@ -219,54 +219,80 @@ {% endswitch %} {# -------------------------------------------------------------- #} -{# Other relevant information #} -{% set sectionHeading = "Other relevant information" %} -{% set subHeading = "Including HRT, pregnancy, breastfeeding and mammographer notes" %} -{% set sectionId = sectionHeading | kebabCase %} -{% set scrollTo = sectionId %} +{# Breast density factors #} +{% set breastDensitySectionId = "breast-density-factors" %} +{% set scrollTo = breastDensitySectionId %} -{% set otherRelevantInformationHtml %} - {% include "_includes/summary-lists/medical-information/other-relevant-information.njk" %} +{% set breastDensityFactorsHtml %} + {% include "_includes/summary-lists/medical-information/breast-density-factors.njk" %} {% endset %} -{% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} +{# Count selected breast density factors for expander summary #} +{% set _bdf = appointment.medicalInformation.breastDensityFactors %} +{% if _bdf and not (_bdf | isArray) %}{% set _bdf = [_bdf] %}{% endif %} +{% set _bdfHrt = appointment.medicalInformation.breastDensityFactorsHrt %} +{% if not _bdfHrt and _bdf and _bdf | includes("hrt") %}{% set _bdfHrt = "yes" %}{% endif %} +{% set breastDensityCount = 0 %} +{% if _bdfHrt == "yes" %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} +{% if _bdf and _bdf | includes("pregnant") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} +{% if _bdf and _bdf | includes("breastfeeding") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} +{% set breastDensityContentsSummary = breastDensityCount ~ (" breast density factor added" if breastDensityCount == 1 else " breast density factors added") if breastDensityCount > 0 else "No breast density factors added" %} -{% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} - {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} -{% endif %} +{% switch displayFormat %} + {% case 'flat' %} +

Breast density factors

+ {{ breastDensityFactorsHtml | safe }} + {% case 'card' %} + {% call card({ + heading: "Breast density factors", + attributes: { + id: breastDensitySectionId + } + }) %} + {{ breastDensityFactorsHtml | safe }} + {% endcall %} + {% default %} + {{ appDetails({ + id: breastDensitySectionId, + classes: "nhsuk-expander js-expandable-section js-track-expanded", + summaryText: "Breast density factors", + contentsSummary: breastDensityContentsSummary, + html: breastDensityFactorsHtml, + status: "To review" + }) }} +{% endswitch %} -{% set otherRelevantInformationCount = selectedBreastDensityFactors | length if selectedBreastDensityFactors else 0 %} +{# -------------------------------------------------------------- #} +{# Other medical information #} +{% set otherMedicalInfoSectionId = "other-medical-information" %} +{% set scrollTo = otherMedicalInfoSectionId %} -{% if otherRelevantInformationCount > 0 %} - {% set otherRelevantInformationSummary = otherRelevantInformationCount ~ " other relevant information added" %} -{% else %} - {% set otherRelevantInformationSummary = "No other relevant information added" %} -{% endif %} +{% set otherMedicalInformationHtml %} + {% include "_includes/summary-lists/medical-information/other-relevant-information.njk" %} +{% endset %} + +{% set otherMedicalInfoContentsSummary = "Other medical information added" if appointment.medicalInformation.otherMedicalInformation else "No other medical information added" %} {% switch displayFormat %} {% case 'flat' %} -

{{ sectionHeading }}

- {{ otherRelevantInformationHtml | safe }} +

Other medical information

+ {{ otherMedicalInformationHtml | safe }} {% case 'card' %} {% call card({ - heading: sectionHeading, + heading: "Other medical information", attributes: { - id: sectionId + id: otherMedicalInfoSectionId } }) %} -

- {{ subHeading }} -

- {{ otherRelevantInformationHtml | safe }} + {{ otherMedicalInformationHtml | safe }} {% endcall %} {% default %} {{ appDetails({ - id: sectionId, + id: otherMedicalInfoSectionId, classes: "nhsuk-expander js-expandable-section js-track-expanded", - summaryText: sectionHeading, - subtitle: subHeading, - contentsSummary: otherRelevantInformationSummary, - html: otherRelevantInformationHtml, + summaryText: "Other medical information", + contentsSummary: otherMedicalInfoContentsSummary, + html: otherMedicalInformationHtml, status: "To review" }) }} {% endswitch %} diff --git a/app/views/_includes/summary-lists/medical-info-summary.njk b/app/views/_includes/summary-lists/medical-info-summary.njk index 4fb122de..9991a268 100644 --- a/app/views/_includes/summary-lists/medical-info-summary.njk +++ b/app/views/_includes/summary-lists/medical-info-summary.njk @@ -127,23 +127,36 @@ {# Build referrer chain for add journey: confirmation -> review #} {% set mammogramsAddReferrerChain = currentUrl | appendReferrer(contextUrl + "/confirm-information/previous-mammograms") %} -{# Other relevant information summary #} -{# Get other relevant information summaries #} -{% set otherRelevantInfoSummaries = appointment.medicalInformation | summariseOtherRelevantInformation %} -{% set otherRelevantInfoCount = otherRelevantInfoSummaries | length %} +{# Breast density factors summary #} +{% set _bdf = appointment.medicalInformation.breastDensityFactors %} +{% if _bdf and not (_bdf | isArray) %}{% set _bdf = [_bdf] %}{% endif %} +{% set _bdfHrt = appointment.medicalInformation.breastDensityFactorsHrt %} +{% if not _bdfHrt and _bdf and _bdf | includes("hrt") %}{% set _bdfHrt = "yes" %}{% endif %} -{# Build other relevant information HTML #} -{% set otherRelevantInfoHtml %} - {% if otherRelevantInfoCount == 0 %} -

No other information added

- {% elif otherRelevantInfoCount == 1 %} -

{{ otherRelevantInfoSummaries[0] }}

- {% else %} +{% set breastDensityFactorsHtml %} + {% if _bdfHrt == "yes" or (_bdf and _bdf | length > 0) %}
    - {% for summary in otherRelevantInfoSummaries %} -
  • {{ summary }}
  • - {% endfor %} + {% if _bdfHrt == "yes" %}
  • Taking HRT
  • {% endif %} + {% if _bdf and _bdf | includes("pregnant") %}
  • Pregnant
  • {% endif %} + {% if _bdf and _bdf | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %}
+ {% else %} +

No breast density factors added

+ {% endif %} +{% endset %} + +{% set breastDensityCount = 0 %} +{% if _bdfHrt == "yes" %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} +{% if _bdf and _bdf | includes("pregnant") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} +{% if _bdf and _bdf | includes("breastfeeding") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} + +{# Other medical information summary #} +{% set otherMedicalInfo = appointment.medicalInformation.otherMedicalInformation %} +{% set otherMedicalInfoHtml %} + {% if otherMedicalInfo %} +

{{ otherMedicalInfo }}

+ {% else %} +

No other medical information added

{% endif %} {% endset %} @@ -266,20 +279,40 @@ }) %} {% endif %} -{% if not showOnlyPopulated or otherRelevantInfoCount > 0 %} +{% if not showOnlyPopulated or breastDensityCount > 0 %} + {% set rows = rows | push({ + key: { + text: "Breast density factors" + }, + value: { + html: breastDensityFactorsHtml + }, + actions: { + items: [ + { + href: ("./confirm-information/breast-density-factors") | urlWithReferrer(currentUrl), + text: "View or change", + visuallyHiddenText: "breast density factors" + } + ] + } if allowEdits + }) %} +{% endif %} + +{% if not showOnlyPopulated or otherMedicalInfo %} {% set rows = rows | push({ key: { - text: "Other relevant information" + text: "Other medical information" }, value: { - html: otherRelevantInfoHtml + html: otherMedicalInfoHtml }, actions: { items: [ { href: ("./confirm-information/other-relevant-information") | urlWithReferrer(currentUrl), text: "View or change", - visuallyHiddenText: "other relevant information" + visuallyHiddenText: "other medical information" } ] } if allowEdits diff --git a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk new file mode 100644 index 00000000..efbd26bc --- /dev/null +++ b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk @@ -0,0 +1,90 @@ +{# app/views/_includes/summary-lists/medical-information/breast-density-factors.njk #} + +{% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} +{% set breastDensityFactorsHrt = appointment.medicalInformation.breastDensityFactorsHrt %} + +{% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} + {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} +{% endif %} + +{% if not breastDensityFactorsHrt and selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt") %} + {% set breastDensityFactorsHrt = "yes" %} +{% endif %} + +{% set breastDensityFactorsSummaryHtml %} + {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | length > 0) %} +
    + {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt")) %} +
  • Taking HRT
  • + {% endif %} + {% if selectedBreastDensityFactors | includes("pregnant") %} +
  • Pregnant
  • + {% endif %} + {% if selectedBreastDensityFactors | includes("breastfeeding") %} +
  • Breastfeeding
  • + {% endif %} +
+ {% else %} +

No breast density factors added

+ {% endif %} +{% endset %} + +{% set breastDensityFactorsInputHtml %} +
+ +

Select any current or recent considerations

+ + {{ checkboxes({ + name: "appointment[medicalInformation][breastDensityFactors]", + values: selectedBreastDensityFactors, + classes: "nhsuk-checkboxes--small", + items: [ + { + value: "pregnant", + text: "Pregnant" + }, + { + value: "breastfeeding", + text: "Breastfeeding" + } + ] + }) }} + + {{ radios({ + name: "appointment[medicalInformation][breastDensityFactorsHrt]", + value: breastDensityFactorsHrt, + classes: "nhsuk-radios--inline nhsuk-radios--small", + fieldset: { + legend: { + text: "HRT (hormone replacement therapy)", + classes: "nhsuk-fieldset__legend--s" + } + }, + items: [ + { + value: "yes", + text: "Yes" + }, + { + value: "no", + text: "No" + } + ] + }) }} +{% endset %} + +{{ summaryList({ + rows: [ + { + key: { + text: "Breast density factors" + }, + value: { + html: breastDensityFactorsInputHtml if allowEdits else breastDensityFactorsSummaryHtml + } + } + ] +} | openInModal | handleSummaryListMissingInformation | removeLastRowBorder ) }} diff --git a/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk b/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk index 871914b5..6a3aa8ef 100644 --- a/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk +++ b/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk @@ -1,69 +1,9 @@ {# app/views/_includes/summary-lists/medical-information/other-relevant-information #} - -{% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} - -{% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} - {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} -{% endif %} - -{% set breastDensityFactorsSummaryHtml %} - {% if selectedBreastDensityFactors and selectedBreastDensityFactors | length > 0 %} -
    - {% if selectedBreastDensityFactors | includes("hrt") %} -
  • Taking HRT
  • - {% endif %} - {% if selectedBreastDensityFactors | includes("pregnant") %} -
  • Pregnant
  • - {% endif %} - {% if selectedBreastDensityFactors | includes("breastfeeding") %} -
  • Breastfeeding
  • - {% endif %} -
- {% else %} -

No breast density factors added

- {% endif %} -{% endset %} - -{% set breastDensityFactorsInputHtml %} -
- - {{ checkboxes({ - name: "appointment[medicalInformation][breastDensityFactors]", - values: selectedBreastDensityFactors, - classes: "nhsuk-checkboxes--small", - hint: { - text: "Select any current or recent considerations" - }, - items: [ - { - value: "hrt", - text: "HRT (hormone replacement therapy)" - }, - { - value: "pregnant", - text: "Pregnant" - }, - { - value: "breastfeeding", - text: "Breastfeeding" - } - ] - }) }} -{% endset %} +{# Shows only the other medical information (free-text) row. + Breast density factors have their own include: breast-density-factors.njk #} {{ summaryList({ rows: [ - { - key: { - text: "Breast density factors" - }, - value: { - html: breastDensityFactorsInputHtml if allowEdits else breastDensityFactorsSummaryHtml - } - }, { key: { text: "Other medical information" diff --git a/app/views/appointments/check-information.html b/app/views/appointments/check-information.html index ea32df8d..8a061f52 100644 --- a/app/views/appointments/check-information.html +++ b/app/views/appointments/check-information.html @@ -144,16 +144,28 @@ } }) }} - {# Other relevant information card #} - {% set otherRelevantInformationHtml %} + {# Breast density factors card #} + {% set breastDensityFactorsHtml %} + {% include "_includes/summary-lists/medical-information/breast-density-factors.njk" %} + {% endset %} + + {{ card({ + heading: "Breast density factors", + headingLevel: "2", + feature: true, + descriptionHtml: breastDensityFactorsHtml + }) }} + + {# Other medical information card #} + {% set otherMedicalInformationHtml %} {% include "_includes/summary-lists/medical-information/other-relevant-information.njk" %} {% endset %} {{ card({ - heading: "Other relevant medical information", + heading: "Other medical information", headingLevel: "2", feature: true, - descriptionHtml: otherRelevantInformationHtml + descriptionHtml: otherMedicalInformationHtml }) }} {% endset %} diff --git a/app/views/appointments/confirm-information/breast-density-factors.html b/app/views/appointments/confirm-information/breast-density-factors.html new file mode 100644 index 00000000..18cae417 --- /dev/null +++ b/app/views/appointments/confirm-information/breast-density-factors.html @@ -0,0 +1,45 @@ +{# app/views/appointments/confirm-information/breast-density-factors.html #} + +{% extends 'layout-appointment.html' %} + +{% set pageHeading = "Review breast density factors" %} +{% set showNavigation = false %} +{% set activeWorkflowStep = 'check-information' %} +{% set hideBackLink = true %} + +{% set allowEdits = true %} + +{% set gridColumn = "nhsuk-grid-column-full" %} + +{% set scrollTo = "breast-density-factors" %} + +{% block pageContent %} + + {# TODO: Ideally this would be before the main element #} + {{ backLink({ + href: "../check-information" | getReturnUrl(referrerChain), + text: "Back", + classes: "nhsuk-u-margin-top-0 nhsuk-u-margin-bottom-4" + }) }} + +

{{ pageHeading }}

+ + {% set breastDensityFactorsHtml %} + {% include "_includes/summary-lists/medical-information/breast-density-factors.njk" %} + {% endset %} + + {{ card({ + heading: "Breast density factors", + headingLevel: "2", + feature: true, + descriptionHtml: breastDensityFactorsHtml + }) }} + +
+ {{ button({ + text: "Continue", + href: "../check-information" | getReturnUrl(referrerChain) + }) }} +
+ +{% endblock %} diff --git a/app/views/appointments/confirm-information/other-relevant-information.html b/app/views/appointments/confirm-information/other-relevant-information.html index 8b4a3761..a72a2be8 100644 --- a/app/views/appointments/confirm-information/other-relevant-information.html +++ b/app/views/appointments/confirm-information/other-relevant-information.html @@ -2,7 +2,7 @@ {% extends 'layout-appointment.html' %} -{% set pageHeading = "Review other relevant information" %} +{% set pageHeading = "Review other medical information" %} {% set showNavigation = false %} {% set activeWorkflowStep = 'check-information' %} {% set hideBackLink = true %} @@ -11,7 +11,7 @@ {% set gridColumn = "nhsuk-grid-column-full" %} -{% set scrollTo = "other-relevant-information" %} +{% set scrollTo = "other-medical-information" %} {% block pageContent %} @@ -24,19 +24,17 @@

{{ pageHeading }}

- {# Include the full other relevant information summary in a feature card #} - {% set otherRelevantInformationHtml %} + {% set otherMedicalInformationHtml %} {% include "_includes/summary-lists/medical-information/other-relevant-information.njk" %} {% endset %} {{ card({ - heading: "Other relevant medical information", + heading: "Other medical information", headingLevel: "2", feature: true, - descriptionHtml: otherRelevantInformationHtml + descriptionHtml: otherMedicalInformationHtml }) }} - {# Continue button back to check information #}
{{ button({ text: "Continue", diff --git a/app/views/appointments/medical-information/other-medical-information.html b/app/views/appointments/medical-information/other-medical-information.html index 77d881d4..de2d8873 100644 --- a/app/views/appointments/medical-information/other-medical-information.html +++ b/app/views/appointments/medical-information/other-medical-information.html @@ -11,11 +11,16 @@ {% block pageContent %} {% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} + {% set breastDensityFactorsHrt = appointment.medicalInformation.breastDensityFactorsHrt %} {% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} {% endif %} + {% if not breastDensityFactorsHrt and selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt") %} + {% set breastDensityFactorsHrt = "yes" %} + {% endif %} +

{{ participant | getFullName }} @@ -34,13 +39,22 @@

{% if selectedBreastDensityFactors %} {% for factor in selectedBreastDensityFactors %} - {{ appHiddenInput({ - name: "appointment[medicalInformation][breastDensityFactors]", - value: factor - }) }} + {% if factor != "hrt" %} + {{ appHiddenInput({ + name: "appointment[medicalInformation][breastDensityFactors]", + value: factor + }) }} + {% endif %} {% endfor %} {% endif %} + {% if breastDensityFactorsHrt %} + {{ appHiddenInput({ + name: "appointment[medicalInformation][breastDensityFactorsHrt]", + value: breastDensityFactorsHrt + }) }} + {% endif %} + {{ button({ text: "Save" }) }} From 005896832baec8641293ce9e90f311484ec658be Mon Sep 17 00:00:00 2001 From: rivalee Date: Mon, 27 Jul 2026 10:50:36 +0100 Subject: [PATCH 3/7] Fix bug --- app/assets/javascript/main.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/assets/javascript/main.js b/app/assets/javascript/main.js index 416077ca..e59dd167 100644 --- a/app/assets/javascript/main.js +++ b/app/assets/javascript/main.js @@ -210,6 +210,34 @@ function setupBreastDensityFactorsAutosave() { return } + const updateBreastDensityFactorsSummary = () => { + // Find the breast-density-factors section and update its contents summary + const section = document.getElementById('breast-density-factors') + if (!section) { + return + } + + // Count selected factors + const selectedCheckboxes = document.querySelectorAll(`${checkboxSelector}:checked`) + const selectedHrtRadio = document.querySelector(`${hrtRadioSelector}:checked`) + let count = selectedCheckboxes.length + if (selectedHrtRadio && selectedHrtRadio.value === 'yes') { + count += 1 + } + + // Find and update the contents summary span + const summarySpan = section.querySelector('.app-details__contents-summary') + if (summarySpan) { + if (count === 0) { + summarySpan.textContent = 'No breast density factors added' + } else if (count === 1) { + summarySpan.textContent = '1 breast density factor added' + } else { + summarySpan.textContent = count + ' breast density factors added' + } + } + } + const saveFactors = async () => { const formData = new URLSearchParams() const selectedCheckboxes = document.querySelectorAll(`${checkboxSelector}:checked`) @@ -242,6 +270,9 @@ function setupBreastDensityFactorsAutosave() { if (!response.ok) { throw new Error('Breast density factors auto-save failed') } + + // Update the summary text in the section after successful save + updateBreastDensityFactorsSummary() } catch (error) { console.error(error) } From 67f8c9e673eb13e602aea0783965c69b72a66d54 Mon Sep 17 00:00:00 2001 From: rivalee Date: Mon, 27 Jul 2026 14:22:24 +0100 Subject: [PATCH 4/7] fix order of things --- app/views/_includes/summary-lists/medical-info-summary.njk | 5 +++-- .../medical-information/breast-density-factors.njk | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/_includes/summary-lists/medical-info-summary.njk b/app/views/_includes/summary-lists/medical-info-summary.njk index 9991a268..a0e72818 100644 --- a/app/views/_includes/summary-lists/medical-info-summary.njk +++ b/app/views/_includes/summary-lists/medical-info-summary.njk @@ -134,11 +134,12 @@ {% if not _bdfHrt and _bdf and _bdf | includes("hrt") %}{% set _bdfHrt = "yes" %}{% endif %} {% set breastDensityFactorsHtml %} - {% if _bdfHrt == "yes" or (_bdf and _bdf | length > 0) %} + {% if _bdfHrt == "yes" or _bdfHrt == "no" or (_bdf and _bdf | length > 0) %}
    - {% if _bdfHrt == "yes" %}
  • Taking HRT
  • {% endif %} {% if _bdf and _bdf | includes("pregnant") %}
  • Pregnant
  • {% endif %} {% if _bdf and _bdf | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %} + {% if _bdfHrt == "yes" %}
  • Taking HRT
  • {% endif %} + {% if _bdfHrt == "no" %}
  • Not taking HRT
  • {% endif %}
{% else %}

No breast density factors added

diff --git a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk index efbd26bc..cbd1276f 100644 --- a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk +++ b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk @@ -14,15 +14,15 @@ {% set breastDensityFactorsSummaryHtml %} {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | length > 0) %}
    - {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt")) %} -
  • Taking HRT
  • - {% endif %} {% if selectedBreastDensityFactors | includes("pregnant") %}
  • Pregnant
  • {% endif %} {% if selectedBreastDensityFactors | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %} + {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt")) %} +
  • Taking HRT
  • + {% endif %}
{% else %}

No breast density factors added

From e0dece3e8e3a9dc57b5858ff52c11963e32eef1c Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 30 Jul 2026 11:35:32 +0100 Subject: [PATCH 5/7] Restructure question and edit content --- .../appointments/medical-information.js | 4 +- .../breast-density-factors.njk | 84 ++++++++++++------- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/app/routes/appointments/medical-information.js b/app/routes/appointments/medical-information.js index 9fade30a..ab0d6ddd 100644 --- a/app/routes/appointments/medical-information.js +++ b/app/routes/appointments/medical-information.js @@ -24,7 +24,7 @@ module.exports = (router) => { : postedFactors ? [postedFactors] : [] - const nonHrtFactors = factors.filter((factor) => factor !== 'hrt') + const nonHrtFactors = factors.filter((factor) => factor && factor !== 'hrt') if (!data.appointment) { data.appointment = {} @@ -86,7 +86,7 @@ module.exports = (router) => { data._medicalInformationDraft = {} } data._medicalInformationDraft.breastDensityFactors = - normalisedPostedBreastDensityFactors.filter((factor) => factor !== 'hrt') + normalisedPostedBreastDensityFactors.filter((factor) => factor && factor !== 'hrt') } if (postedBreastDensityFactorsHrt === 'yes' || postedBreastDensityFactorsHrt === 'no') { diff --git a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk index cbd1276f..eb5c2a2d 100644 --- a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk +++ b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk @@ -11,8 +11,17 @@ {% set breastDensityFactorsHrt = "yes" %} {% endif %} -{% set breastDensityFactorsSummaryHtml %} - {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | length > 0) %} +{% set participantName = participant | getFullName %} + +{% set breastDensityFactorsHrtQuestion = "Has " + participantName + " begun course of HRT since their last screening appointment?" %} +{% set pregnantOrBreastfeedingQuestion = "Is " + participantName + " pregnant or breastfeeding?" %} + +{% set breastDensityFactorsHrtSummaryHtml %} + {{ "Yes" if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt")) else "No" }} +{% endset %} + +{% set pregnantOrBreastfeedingSummaryHtml %} + {% if selectedBreastDensityFactors and (selectedBreastDensityFactors | includes("pregnant") or selectedBreastDensityFactors | includes("breastfeeding")) %}
    {% if selectedBreastDensityFactors | includes("pregnant") %}
  • Pregnant
  • @@ -20,46 +29,27 @@ {% if selectedBreastDensityFactors | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %} - {% if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt")) %} -
  • Taking HRT
  • - {% endif %}
{% else %} -

No breast density factors added

+

No

{% endif %} {% endset %} -{% set breastDensityFactorsInputHtml %} -
+
-

Select any current or recent considerations

- - {{ checkboxes({ - name: "appointment[medicalInformation][breastDensityFactors]", - values: selectedBreastDensityFactors, - classes: "nhsuk-checkboxes--small", - items: [ - { - value: "pregnant", - text: "Pregnant" - }, - { - value: "breastfeeding", - text: "Breastfeeding" - } - ] - }) }} +

Select any current or recent considerations

+{% set breastDensityFactorsHrtInputHtml %} {{ radios({ name: "appointment[medicalInformation][breastDensityFactorsHrt]", value: breastDensityFactorsHrt, classes: "nhsuk-radios--inline nhsuk-radios--small", fieldset: { legend: { - text: "HRT (hormone replacement therapy)", + text: breastDensityFactorsHrtQuestion, classes: "nhsuk-fieldset__legend--s" } }, @@ -76,14 +66,46 @@ }) }} {% endset %} +{% set pregnantOrBreastfeedingInputHtml %} + {{ checkboxes({ + name: "appointment[medicalInformation][breastDensityFactors]", + values: selectedBreastDensityFactors, + classes: "nhsuk-checkboxes--small", + fieldset: { + legend: { + text: pregnantOrBreastfeedingQuestion, + classes: "nhsuk-fieldset__legend--s" + } + }, + items: [ + { + value: "pregnant", + text: "Pregnant" + }, + { + value: "breastfeeding", + text: "Breastfeeding" + } + ] + }) }} +{% endset %} + {{ summaryList({ rows: [ { key: { - text: "Breast density factors" + text: "HRT (Hormone replacement therapy)" + }, + value: { + html: breastDensityFactorsHrtInputHtml if allowEdits else breastDensityFactorsHrtSummaryHtml + } + }, + { + key: { + text: "Pregnant or breastfeeding" }, value: { - html: breastDensityFactorsInputHtml if allowEdits else breastDensityFactorsSummaryHtml + html: pregnantOrBreastfeedingInputHtml if allowEdits else pregnantOrBreastfeedingSummaryHtml } } ] From 10fa76bc357ef5d13e85eecc0527cc6f5995e136 Mon Sep 17 00:00:00 2001 From: rivalee Date: Thu, 30 Jul 2026 13:08:28 +0100 Subject: [PATCH 6/7] . --- app/views/_includes/summary-lists/medical-info-summary.njk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/_includes/summary-lists/medical-info-summary.njk b/app/views/_includes/summary-lists/medical-info-summary.njk index a0e72818..d4bbab01 100644 --- a/app/views/_includes/summary-lists/medical-info-summary.njk +++ b/app/views/_includes/summary-lists/medical-info-summary.njk @@ -136,10 +136,10 @@ {% set breastDensityFactorsHtml %} {% if _bdfHrt == "yes" or _bdfHrt == "no" or (_bdf and _bdf | length > 0) %}
    - {% if _bdf and _bdf | includes("pregnant") %}
  • Pregnant
  • {% endif %} - {% if _bdf and _bdf | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %} {% if _bdfHrt == "yes" %}
  • Taking HRT
  • {% endif %} {% if _bdfHrt == "no" %}
  • Not taking HRT
  • {% endif %} + {% if _bdf and _bdf | includes("pregnant") %}
  • Pregnant
  • {% endif %} + {% if _bdf and _bdf | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %}
{% else %}

No breast density factors added

From 278904ed4e86e7aa6a082071ad1419c38bb1aa7c Mon Sep 17 00:00:00 2001 From: Ed Horsford Date: Mon, 3 Aug 2026 10:50:08 +0100 Subject: [PATCH 7/7] Simplify breast density factors implementation Follow-up to the breast density factors work, tidying up the method. - Remove the _medicalInformationDraft cache and the other-medical-information save route. auto-store-data merges nested objects, so a note being added could never have cleared the factors - the cache guarded against something that couldn't happen. - Fix the clobbering that could actually happen: the kit's unchecked-checkbox script adds an "_unchecked" value for every checkbox in the form being submitted, and auto-store-data replaces arrays rather than merging them, so submitting any other form on the page wiped the saved answers. The inputs are now named outside the appointment namespace, so only the autosave route writes them. - Add getBreastDensityFactors, summariseBreastDensityFactors and summariseOtherMedicalInformation, replacing the array coercion that was repeated across four templates. Count the factors separately from the answers, so a recorded "not taking HRT" still shows on the summary rather than hiding the row. - Retire the HRT and pregnancy/breastfeeding pages, generators and the dead summariseOtherRelevantInformation. Generate the new fields instead, so seeded appointments have data, and keep "no medical information" possible. - Move the include out of summary-lists into forms - it embeds inputs rather than being a summary list - and drop the openInModal and handleSummaryListMissingInformation filters, which were no-ops with no actions on the rows. - Fix copy: sentence case on the HRT key, a missing article in the HRT question, and drop the hint that only described the checkboxes. --- app/assets/javascript/main.js | 136 +++++++------- .../medical-information-generator.js | 25 +-- .../breast-density-factors-generator.js | 56 ++++++ .../medical-information/hrt-generator.js | 63 ------- .../pregnancy-and-breastfeeding-generator.js | 87 --------- app/lib/utils/medical-information.js | 107 +++++++---- .../appointments/medical-information.js | 161 +++++------------ .../forms/breast-density-factors.njk | 119 +++++++++++++ .../_includes/medical-information/index.njk | 12 +- .../summary-lists/medical-info-summary.njk | 23 +-- .../breast-density-factors.njk | 112 ------------ app/views/appointments/check-information.html | 2 +- .../breast-density-factors.html | 2 +- .../hormone-replacement-therapy.html | 115 ------------ .../other-medical-information.html | 31 +--- .../pregnancy-and-breastfeeding.html | 166 ------------------ app/views/index.html | 5 +- app/views/settings/seed-profiles/custom.html | 2 +- docs/DATA-GENERATOR-REFERENCE.md | 18 +- docs/MEDICAL-INFORMATION-GENERATOR-GUIDE.md | 110 +++++------- docs/utils-filter-reference.md | 123 ++++++------- 21 files changed, 492 insertions(+), 983 deletions(-) create mode 100644 app/lib/generators/medical-information/breast-density-factors-generator.js delete mode 100644 app/lib/generators/medical-information/hrt-generator.js delete mode 100644 app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js create mode 100644 app/views/_includes/forms/breast-density-factors.njk delete mode 100644 app/views/_includes/summary-lists/medical-information/breast-density-factors.njk delete mode 100644 app/views/appointments/medical-information/hormone-replacement-therapy.html delete mode 100644 app/views/appointments/medical-information/pregnancy-and-breastfeeding.html diff --git a/app/assets/javascript/main.js b/app/assets/javascript/main.js index e59dd167..bdc7aa2e 100644 --- a/app/assets/javascript/main.js +++ b/app/assets/javascript/main.js @@ -188,103 +188,99 @@ document.addEventListener('DOMContentLoaded', () => { } }) +// Breast density factors are edited in place rather than on their own page, +// so there's no submit button to save them - each change posts on its own. function setupBreastDensityFactorsAutosave() { - const saveTarget = document.querySelector('[data-breast-density-factors-save-url]') - if (!saveTarget) { + const container = document.querySelector('[data-breast-density-factors-save-url]') + if (!container) { return } - const saveUrl = saveTarget.dataset.breastDensityFactorsSaveUrl + const saveUrl = container.dataset.breastDensityFactorsSaveUrl if (!saveUrl) { return } - const checkboxSelector = - 'input[name="appointment[medicalInformation][breastDensityFactors]"]' - const hrtRadioSelector = - 'input[name="appointment[medicalInformation][breastDensityFactorsHrt]"]' - const checkboxes = document.querySelectorAll(checkboxSelector) - const hrtRadios = document.querySelectorAll(hrtRadioSelector) + const factorsName = 'breastDensityFactors' + const hrtName = 'breastDensityFactorsHrt' + + const checkboxes = container.querySelectorAll(`input[name="${factorsName}"]`) + const hrtRadios = container.querySelectorAll(`input[name="${hrtName}"]`) if (checkboxes.length === 0 && hrtRadios.length === 0) { return } - const updateBreastDensityFactorsSummary = () => { - // Find the breast-density-factors section and update its contents summary - const section = document.getElementById('breast-density-factors') - if (!section) { - return - } + // Keep the expander's "n factors added" line in step with the inputs. + // Only the review page wraps these in an expander, so this does nothing + // elsewhere. + const updateContentsSummary = () => { + const summary = container + .closest('.js-expandable-section') + ?.querySelector('.app-details__contents-summary') - // Count selected factors - const selectedCheckboxes = document.querySelectorAll(`${checkboxSelector}:checked`) - const selectedHrtRadio = document.querySelector(`${hrtRadioSelector}:checked`) - let count = selectedCheckboxes.length - if (selectedHrtRadio && selectedHrtRadio.value === 'yes') { - count += 1 + if (!summary) { + return } - // Find and update the contents summary span - const summarySpan = section.querySelector('.app-details__contents-summary') - if (summarySpan) { - if (count === 0) { - summarySpan.textContent = 'No breast density factors added' - } else if (count === 1) { - summarySpan.textContent = '1 breast density factor added' - } else { - summarySpan.textContent = count + ' breast density factors added' - } + // "No" to HRT is an answer, not a factor - match the count in + // getBreastDensityFactors so the two never disagree + const checkedFactors = container.querySelectorAll( + `input[name="${factorsName}"]:checked` + ).length + const hrtYes = container.querySelector(`input[name="${hrtName}"]:checked`)?.value === 'yes' + const count = checkedFactors + (hrtYes ? 1 : 0) + + if (count === 0) { + summary.textContent = 'No breast density factors added' + } else if (count === 1) { + summary.textContent = '1 breast density factor added' + } else { + summary.textContent = `${count} breast density factors added` } } - const saveFactors = async () => { + // Changes can land faster than the requests complete, so keep them in a + // queue - otherwise an earlier response could be the last one to arrive + let pendingSave = Promise.resolve() + + const saveFactors = () => { const formData = new URLSearchParams() - const selectedCheckboxes = document.querySelectorAll(`${checkboxSelector}:checked`) - const selectedHrtRadio = document.querySelector(`${hrtRadioSelector}:checked`) - - selectedCheckboxes.forEach((checkbox) => { - formData.append( - 'appointment[medicalInformation][breastDensityFactors]', - checkbox.value - ) - }) - if (selectedHrtRadio) { - formData.append( - 'appointment[medicalInformation][breastDensityFactorsHrt]', - selectedHrtRadio.value - ) + container + .querySelectorAll(`input[name="${factorsName}"]:checked`) + .forEach((checkbox) => formData.append(factorsName, checkbox.value)) + + const selectedHrt = container.querySelector(`input[name="${hrtName}"]:checked`) + if (selectedHrt) { + formData.append(hrtName, selectedHrt.value) } - try { - const response = await fetch(saveUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest' - }, - body: formData.toString() - }) + updateContentsSummary() - if (!response.ok) { - throw new Error('Breast density factors auto-save failed') - } + pendingSave = pendingSave + .then(async () => { + const response = await fetch(saveUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: formData.toString() + }) - // Update the summary text in the section after successful save - updateBreastDensityFactorsSummary() - } catch (error) { - console.error(error) - } + if (!response.ok) { + throw new Error( + `Breast density factors auto-save failed (${response.status})` + ) + } + }) + .catch((error) => console.error(error)) } - checkboxes.forEach((checkbox) => { - checkbox.addEventListener('change', saveFactors) - }) - - hrtRadios.forEach((radio) => { - radio.addEventListener('change', saveFactors) - }) + container + .querySelectorAll(`input[name="${factorsName}"], input[name="${hrtName}"]`) + .forEach((input) => input.addEventListener('change', saveFactors)) } // Quick settings modal — press backtick (`) to open settings in a modal overlay. diff --git a/app/lib/generators/medical-information-generator.js b/app/lib/generators/medical-information-generator.js index 0ae1137e..2b0440dc 100644 --- a/app/lib/generators/medical-information-generator.js +++ b/app/lib/generators/medical-information-generator.js @@ -1,10 +1,9 @@ // app/lib/generators/medical-information-generator.js const { generateSymptoms } = require('./medical-information/symptoms-generator') -const { generateHRT } = require('./medical-information/hrt-generator') const { - generatePregnancyAndBreastfeeding -} = require('./medical-information/pregnancy-and-breastfeeding-generator') + generateBreastDensityFactors +} = require('./medical-information/breast-density-factors-generator') const { generateOtherMedicalInformation } = require('./medical-information/other-medical-information-generator') @@ -59,23 +58,13 @@ const generateMedicalInformation = (options = {}) => { medicalInfo.symptoms = symptoms } - // Generate HRT information - const hrt = generateHRT({ - probability: probabilityOfHRT + // Generate breast density factors (HRT, pregnancy, breastfeeding) + const breastDensityFactors = generateBreastDensityFactors({ + probabilityOfHrt: probabilityOfHRT, + probabilityOfPregnancyBreastfeeding }) - if (hrt) { - medicalInfo.hrt = hrt - } - - // Generate pregnancy and breastfeeding information - const pregnancyAndBreastfeeding = generatePregnancyAndBreastfeeding({ - probability: probabilityOfPregnancyBreastfeeding - }) - - if (pregnancyAndBreastfeeding) { - medicalInfo.pregnancyAndBreastfeeding = pregnancyAndBreastfeeding - } + Object.assign(medicalInfo, breastDensityFactors) // Generate other medical information const otherMedicalInformation = generateOtherMedicalInformation({ diff --git a/app/lib/generators/medical-information/breast-density-factors-generator.js b/app/lib/generators/medical-information/breast-density-factors-generator.js new file mode 100644 index 00000000..5238e00c --- /dev/null +++ b/app/lib/generators/medical-information/breast-density-factors-generator.js @@ -0,0 +1,56 @@ +// app/lib/generators/medical-information/breast-density-factors-generator.js + +/** + * Generate breast density factors + * + * Replaces the old separate HRT and pregnancy/breastfeeding generators. The + * question is now a simple yes/no for HRT plus a checkbox group for pregnancy + * and breastfeeding, so the generated data is just as simple. + * + * Pregnancy and breastfeeding are rare in the screening cohort (routine + * screening starts at 50), so they stay well below the HRT rate. + * + * @param {object} [options] - Generation options + * @param {number} [options.probabilityOfHrt] - Chance of currently taking HRT (0-1) + * @param {number} [options.probabilityOfPregnancyBreastfeeding] - Chance of being pregnant or breastfeeding (0-1) + * @param {number} [options.probabilityOfBeingAsked=0.8] - Chance the question was asked at all + * @returns {object} Object with breastDensityFactors and breastDensityFactorsHrt, either may be absent + */ +const generateBreastDensityFactors = (options = {}) => { + const { + probabilityOfHrt = 0.3, + probabilityOfPregnancyBreastfeeding = 0.05, + probabilityOfBeingAsked = 0.8 + } = options + + const result = {} + + // Not every appointment has got as far as asking. Leaving the answer unset + // keeps "no medical information" a possible outcome, and lets the summaries + // distinguish "not answered" from a recorded "no" + if (Math.random() < probabilityOfBeingAsked) { + result.breastDensityFactorsHrt = + Math.random() < probabilityOfHrt ? 'yes' : 'no' + } + + const factors = [] + + if (Math.random() < probabilityOfPregnancyBreastfeeding) { + // Breastfeeding is the more likely of the two at screening age + if (Math.random() < 0.7) { + factors.push('breastfeeding') + } else { + factors.push('pregnant') + } + } + + if (factors.length > 0) { + result.breastDensityFactors = factors + } + + return result +} + +module.exports = { + generateBreastDensityFactors +} diff --git a/app/lib/generators/medical-information/hrt-generator.js b/app/lib/generators/medical-information/hrt-generator.js deleted file mode 100644 index 19467cca..00000000 --- a/app/lib/generators/medical-information/hrt-generator.js +++ /dev/null @@ -1,63 +0,0 @@ -// app/lib/generators/medical-information/hrt-generator.js - -const { faker } = require('@faker-js/faker') -const weighted = require('weighted') -const dayjs = require('dayjs') - -// Generate a past date in 'MMMM YYYY' format (e.g., 'September 2022') -const randomPastDate = (minMonthsAgo, maxMonthsAgo) => { - const monthsAgo = faker.number.int({ min: minMonthsAgo, max: maxMonthsAgo }) - return dayjs().subtract(monthsAgo, 'month').format('MMMM YYYY') -} - -// Example durations before stopping -const DURATION_BEFORE_STOPPING = [ - '6 months', - '1 year', - '2 years', - '3 years', - '5 years', - '8 years' -] - -/** - * Generate HRT information - * - * @param {object} [options] - Generation options - * @param {number} [options.probability] - Chance of having HRT data (0-1) - * @returns {object|null} HRT object or null if no HRT data - */ -const generateHRT = (options = {}) => { - const { probability } = options - - // Check if they have any HRT information - if (Math.random() > probability) { - return null - } - - // Weighted selection of HRT status - const hrtQuestion = weighted.select({ - 'yes': 0.5, // Currently taking - 'no-recently-stopped': 0.3, // Recently stopped - 'no': 0.2 // No HRT - }) - - const hrt = { hrtQuestion } - - // Add conditional fields based on status - if (hrtQuestion === 'yes') { - // Date HRT was started (6 months to 15 years ago) - hrt.hrtDateStarted = randomPastDate(6, 180) - } - else if (hrtQuestion === 'no-recently-stopped') { - // Date HRT was stopped (1-11 months ago) - hrt.hrtDateStopped = randomPastDate(1, 11) - hrt.hrtDurationBeforeStopping = faker.helpers.arrayElement(DURATION_BEFORE_STOPPING) - } - - return hrt -} - -module.exports = { - generateHRT -} diff --git a/app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js b/app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js deleted file mode 100644 index 32fade8f..00000000 --- a/app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js +++ /dev/null @@ -1,87 +0,0 @@ -// app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js - -const { faker } = require('@faker-js/faker') -const weighted = require('weighted') -const dayjs = require('dayjs') - -// Generate a past date in 'MMMM YYYY' format -const randomPastDate = (minMonthsAgo, maxMonthsAgo) => { - const monthsAgo = faker.number.int({ min: minMonthsAgo, max: maxMonthsAgo }) - return dayjs().subtract(monthsAgo, 'month').format('MMMM YYYY') -} - -// Generate a future date in 'MMMM YYYY' format -const randomFutureDate = (minMonthsAhead, maxMonthsAhead) => { - const monthsAhead = faker.number.int({ min: minMonthsAhead, max: maxMonthsAhead }) - return dayjs().add(monthsAhead, 'month').format('MMMM YYYY') -} - -/** - * Generate pregnancy and breastfeeding information - * - * @param {object} [options] - Generation options - * @param {number} [options.probability] - Chance of having pregnancy/breastfeeding data (0-1) - * @returns {object|null} Pregnancy and breastfeeding object or null - */ -const generatePregnancyAndBreastfeeding = (options = {}) => { - const { probability } = options - - // Check if they have any pregnancy/breastfeeding information - if (Math.random() > probability) { - return null - } - - const info = {} - - // Weighted selection of pregnancy status - info.pregnancyStatus = weighted.select({ - 'noNotPregnant': 0.7, // Most common - not pregnant - 'noButRecently': 0.2, // Recently gave birth - 'yes': 0.1 // Currently pregnant - }) - - // Add conditional fields based on pregnancy status - if (info.pregnancyStatus === 'yes') { - // Due date 1-9 months in future - info.pregnancyDueDate = randomFutureDate(1, 9) - } - else if (info.pregnancyStatus === 'noButRecently') { - // Pregnancy ended 1-5 months ago - info.pregnancyEndDate = randomPastDate(1, 5) - } - - // Weighted selection of breastfeeding status - // If recently pregnant, more likely to be breastfeeding - if (info.pregnancyStatus === 'noButRecently') { - info.breastfeedingStatus = weighted.select({ - 'yes': 0.6, // Likely breastfeeding if recently gave birth - 'recentlyStopped': 0.2, - 'no': 0.2 - }) - } else if (info.pregnancyStatus === 'yes') { - // If pregnant, not breastfeeding - info.breastfeedingStatus = 'no' - } else { - info.breastfeedingStatus = weighted.select({ - 'no': 0.8, // Most common - 'yes': 0.1, // Currently breastfeeding - 'recentlyStopped': 0.1 // Recently stopped - }) - } - - // Add conditional fields based on breastfeeding status - if (info.breastfeedingStatus === 'yes') { - // Started breastfeeding 2 weeks to 18 months ago - info.breastfeedingStartDate = randomPastDate(0, 18) - } - else if (info.breastfeedingStatus === 'recentlyStopped') { - // Stopped breastfeeding 1-4 months ago - info.breastfeedingStopDate = randomPastDate(1, 4) - } - - return info -} - -module.exports = { - generatePregnancyAndBreastfeeding -} diff --git a/app/lib/utils/medical-information.js b/app/lib/utils/medical-information.js index df79a46f..bf53d8c3 100644 --- a/app/lib/utils/medical-information.js +++ b/app/lib/utils/medical-information.js @@ -417,54 +417,95 @@ const summariseBreastFeatures = (features) => { } /** - * Summarise other relevant medical information (HRT, pregnancy/breastfeeding, other info) + * Read the breast density factors off an appointment's medical information + * + * A checkbox group posts a bare string when one box is ticked and an array + * when several are, so the stored value needs normalising before anything can + * read it. Doing that here means templates get a single shape to work with + * rather than repeating the coercion at every call site. * * @param {Object} medicalInformation - The medicalInformation object from appointment - * @returns {Array} Array of summary strings + * @returns {{factors: Array, hrt: string|undefined, count: number, answeredCount: number, summaries: Array}} */ -const summariseOtherRelevantInformation = (medicalInformation) => { - if (!medicalInformation) { - return [] +const getBreastDensityFactors = (medicalInformation) => { + const rawFactors = medicalInformation?.breastDensityFactors + const factors = Array.isArray(rawFactors) + ? rawFactors.filter(Boolean) + : rawFactors + ? [rawFactors] + : [] + + const hrt = medicalInformation?.breastDensityFactorsHrt + + // "Not taking HRT" is an answer, but it isn't a density factor - only + // count the things that actually affect density + const count = + (hrt === 'yes' ? 1 : 0) + + (factors.includes('pregnant') ? 1 : 0) + + (factors.includes('breastfeeding') ? 1 : 0) + + const summaries = summariseBreastDensityFactors(medicalInformation) + + return { + factors, + hrt, + count, + // Everything worth showing, including a recorded "no" to HRT - use this + // to decide whether to show the row at all, and count for "n added" + answeredCount: summaries.length, + summaries } +} + +/** + * Summarise breast density factors into an array of summary strings + * + * @param {Object} medicalInformation - The medicalInformation object from appointment + * @returns {Array} Array of summary strings + */ +const summariseBreastDensityFactors = (medicalInformation) => { + const rawFactors = medicalInformation?.breastDensityFactors + const factors = Array.isArray(rawFactors) + ? rawFactors.filter(Boolean) + : rawFactors + ? [rawFactors] + : [] + + const hrt = medicalInformation?.breastDensityFactorsHrt const summaries = [] - const breastDensityFactorsRaw = medicalInformation.breastDensityFactors - const breastDensityFactors = Array.isArray(breastDensityFactorsRaw) - ? breastDensityFactorsRaw - : breastDensityFactorsRaw - ? [breastDensityFactorsRaw] - : [] - const breastDensityFactorsHrt = - medicalInformation.breastDensityFactorsHrt || - (breastDensityFactors.includes('hrt') ? 'yes' : undefined) + if (hrt === 'yes') { + summaries.push('Taking HRT') + } else if (hrt === 'no') { + summaries.push('Not taking HRT') + } - if (breastDensityFactors.includes('pregnant')) { + if (factors.includes('pregnant')) { summaries.push('Pregnant') } - if (breastDensityFactors.includes('breastfeeding')) { + if (factors.includes('breastfeeding')) { summaries.push('Breastfeeding') } - if (breastDensityFactorsHrt === 'yes') { - summaries.push('Taking HRT') - } else if (breastDensityFactorsHrt === 'no') { - summaries.push('Not taking HRT') - } + return summaries +} - // Other medical information (free text) - if (medicalInformation.otherMedicalInformation) { - // Truncate if very long, otherwise show as-is - const otherInfo = medicalInformation.otherMedicalInformation.trim() - if (otherInfo.length > 100) { - summaries.push(otherInfo.substring(0, 100) + '...') - } else { - summaries.push(otherInfo) - } +/** + * Summarise the free-text other medical information, truncating if long + * + * @param {Object} medicalInformation - The medicalInformation object from appointment + * @returns {string|null} Summary string, or null if there's nothing recorded + */ +const summariseOtherMedicalInformation = (medicalInformation) => { + const otherInfo = medicalInformation?.otherMedicalInformation?.trim() + + if (!otherInfo) { + return null } - return summaries + return otherInfo.length > 100 ? otherInfo.substring(0, 100) + '...' : otherInfo } module.exports = { @@ -479,5 +520,7 @@ module.exports = { summariseSymptoms, summariseBreastFeature, summariseBreastFeatures, - summariseOtherRelevantInformation + getBreastDensityFactors, + summariseBreastDensityFactors, + summariseOtherMedicalInformation } diff --git a/app/routes/appointments/medical-information.js b/app/routes/appointments/medical-information.js index ab0d6ddd..11dd646d 100644 --- a/app/routes/appointments/medical-information.js +++ b/app/routes/appointments/medical-information.js @@ -7,143 +7,66 @@ const { getReturnUrl, modalBreakout } = require('../../lib/utils/referrers') -const { getAppointmentData } = require('../../lib/utils/appointment-data') module.exports = (router) => { - // Auto-save breast density factors when checkboxes are changed + // Auto-save breast density factors as they're changed + // + // These answers are edited in place on the review page rather than on a + // sub-page with its own submit, so there's no form post to carry them. + // Like the other medical information sections, this writes to the temp + // appointment and is committed when the appointment is completed or paused. + // + // The inputs are named outside the appointment[...] namespace on purpose. + // They render inside other forms, and the kit's unchecked-checkbox script + // adds an "_unchecked" value for every checkbox in whichever form is being + // submitted. Since auto-store-data replaces arrays rather than merging + // them, an appointment-namespaced name would let any other form on the page + // wipe these answers. Keeping them out of that namespace means only this + // route ever writes them. router.post( '/clinics/:clinicId/appointments/:appointmentId/medical-information/breast-density-factors-save', (req, res) => { + const { appointmentId } = req.params const data = req.session.data - const postedFactors = req.body?.appointment?.medicalInformation?.breastDensityFactors - const postedHrt = req.body?.appointment?.medicalInformation?.breastDensityFactorsHrt - - // No posted factors means all options are currently unchecked - const factors = Array.isArray(postedFactors) - ? postedFactors - : postedFactors - ? [postedFactors] - : [] - const nonHrtFactors = factors.filter((factor) => factor && factor !== 'hrt') - - if (!data.appointment) { - data.appointment = {} - } - if (!data.appointment.medicalInformation) { - data.appointment.medicalInformation = {} + // An unticked checkbox group posts nothing at all, so a missing value + // means "none selected" rather than "unchanged" + const postedFactors = req.body?.breastDensityFactors + const factors = ( + Array.isArray(postedFactors) + ? postedFactors + : postedFactors + ? [postedFactors] + : [] + ).filter((factor) => factor && factor !== '_unchecked') + + const postedHrt = req.body?.breastDensityFactorsHrt + + // The appointment context middleware has already made the temp copy, + // so this is only defensive + if (data.appointment?.id !== appointmentId) { + res.status(409).send() + return } - data.appointment.medicalInformation.breastDensityFactors = nonHrtFactors + const medicalInformation = data.appointment.medicalInformation || {} + medicalInformation.breastDensityFactors = factors - if (postedHrt === 'yes' || postedHrt === 'no') { - data.appointment.medicalInformation.breastDensityFactorsHrt = postedHrt + if (postedHrt) { + medicalInformation.breastDensityFactorsHrt = postedHrt } - // Keep a draft cache outside auto-stored form keys so later form posts - // cannot accidentally clear the latest autosaved checkbox state. - if (!data._medicalInformationDraft) { - data._medicalInformationDraft = {} - } - data._medicalInformationDraft.breastDensityFactors = nonHrtFactors + data.appointment.medicalInformation = medicalInformation - if (postedHrt === 'yes' || postedHrt === 'no') { - data._medicalInformationDraft.breastDensityFactorsHrt = postedHrt - } + // These aren't form fields for any other page - don't leave them in + // session data where auto-store-data has put them + delete data.breastDensityFactors + delete data.breastDensityFactorsHrt res.status(204).send() } ) - // Save other medical information while preserving breast density factors - router.post( - '/clinics/:clinicId/appointments/:appointmentId/medical-information/other-medical-information-save', - (req, res) => { - const { clinicId, appointmentId } = req.params - const data = req.session.data - const referrerChain = req.query.referrerChain - const scrollTo = req.query.scrollTo - - const appointmentMedicalInformation = data?.appointment?.medicalInformation - const postedBreastDensityFactors = - appointmentMedicalInformation?.breastDensityFactors - const postedBreastDensityFactorsHrt = - appointmentMedicalInformation?.breastDensityFactorsHrt - const cachedBreastDensityFactors = - data?._medicalInformationDraft?.breastDensityFactors - const cachedBreastDensityFactorsHrt = - data?._medicalInformationDraft?.breastDensityFactorsHrt - - const normalisedPostedBreastDensityFactors = - Array.isArray(postedBreastDensityFactors) - ? postedBreastDensityFactors - : postedBreastDensityFactors - ? [postedBreastDensityFactors] - : undefined - - if (normalisedPostedBreastDensityFactors) { - if (!data._medicalInformationDraft) { - data._medicalInformationDraft = {} - } - data._medicalInformationDraft.breastDensityFactors = - normalisedPostedBreastDensityFactors.filter((factor) => factor && factor !== 'hrt') - } - - if (postedBreastDensityFactorsHrt === 'yes' || postedBreastDensityFactorsHrt === 'no') { - if (!data._medicalInformationDraft) { - data._medicalInformationDraft = {} - } - data._medicalInformationDraft.breastDensityFactorsHrt = - postedBreastDensityFactorsHrt - } - - // If this submission did not include factors, keep the last saved values - if (!normalisedPostedBreastDensityFactors) - { - const savedAppointmentData = getAppointmentData(data, clinicId, appointmentId) - const savedBreastDensityFactors = - savedAppointmentData?.appointment?.medicalInformation?.breastDensityFactors - - if (cachedBreastDensityFactors) - { - data.appointment.medicalInformation.breastDensityFactors = - cachedBreastDensityFactors - } - else if (savedBreastDensityFactors) - { - data.appointment.medicalInformation.breastDensityFactors = - savedBreastDensityFactors - } - } - - if (!postedBreastDensityFactorsHrt) - { - const savedAppointmentData = getAppointmentData(data, clinicId, appointmentId) - const savedBreastDensityFactorsHrt = - savedAppointmentData?.appointment?.medicalInformation?.breastDensityFactorsHrt - - if (cachedBreastDensityFactorsHrt) - { - data.appointment.medicalInformation.breastDensityFactorsHrt = - cachedBreastDensityFactorsHrt - } - else if (savedBreastDensityFactorsHrt) - { - data.appointment.medicalInformation.breastDensityFactorsHrt = - savedBreastDensityFactorsHrt - } - } - - const returnUrl = getReturnUrl( - `/clinics/${clinicId}/appointments/${appointmentId}/review-medical-information`, - referrerChain, - scrollTo - ) - - res.redirect(modalBreakout(returnUrl)) - } - ) - // Save breast features (includes converting JSON string to structured data) router.post( '/clinics/:clinicId/appointments/:appointmentId/medical-information/record-breast-features/save', diff --git a/app/views/_includes/forms/breast-density-factors.njk b/app/views/_includes/forms/breast-density-factors.njk new file mode 100644 index 00000000..5aeac465 --- /dev/null +++ b/app/views/_includes/forms/breast-density-factors.njk @@ -0,0 +1,119 @@ +{# app/views/_includes/forms/breast-density-factors.njk #} +{# + Breast density factors - HRT, pregnancy and breastfeeding. + + Shown inline rather than on a sub-page, so when edits are allowed the inputs + render directly and save themselves as they're changed (see the autosave in + main.js). The save URL rides on the wrapper element. + + The inputs are named outside the appointment[...] namespace on purpose. This + section renders inside other forms (the review page wraps everything in a + "complete and continue" form), and the kit's unchecked-checkbox script adds + an "_unchecked" value for every checkbox in whichever form is submitted. + Since auto-store-data replaces arrays rather than merging them, an + appointment-namespaced name would let any other form on the page wipe these + answers. The save route maps these names onto the appointment. +#} + +{% set breastDensityFactors = appointment.medicalInformation | getBreastDensityFactors %} + +{% set participantName = participant | getFullName %} + +{% set hrtQuestion = "Has " + participantName + " begun a course of HRT since their last screening appointment?" %} +{% set pregnantOrBreastfeedingQuestion = "Is " + participantName + " pregnant or breastfeeding?" %} + +{% set hrtInputHtml %} + {{ radios({ + name: "breastDensityFactorsHrt", + value: breastDensityFactors.hrt, + classes: "nhsuk-radios--inline nhsuk-radios--small", + fieldset: { + legend: { + text: hrtQuestion, + classes: "nhsuk-fieldset__legend--s" + } + }, + items: [ + { + value: "yes", + text: "Yes" + }, + { + value: "no", + text: "No" + } + ] + }) }} +{% endset %} + +{% set pregnantOrBreastfeedingInputHtml %} + {{ checkboxes({ + name: "breastDensityFactors", + values: breastDensityFactors.factors, + classes: "nhsuk-checkboxes--small", + fieldset: { + legend: { + text: pregnantOrBreastfeedingQuestion, + classes: "nhsuk-fieldset__legend--s" + } + }, + items: [ + { + value: "pregnant", + text: "Pregnant" + }, + { + value: "breastfeeding", + text: "Breastfeeding" + } + ] + }) }} +{% endset %} + +{% set hrtSummaryHtml %} + {% if breastDensityFactors.hrt == "yes" %} +

Yes

+ {% elseif breastDensityFactors.hrt == "no" %} +

No

+ {% else %} +

Not answered

+ {% endif %} +{% endset %} + +{% set pregnantOrBreastfeedingSummaryHtml %} + {% if breastDensityFactors.factors | length %} +
    + {% if breastDensityFactors.factors | includes("pregnant") %} +
  • Pregnant
  • + {% endif %} + {% if breastDensityFactors.factors | includes("breastfeeding") %} +
  • Breastfeeding
  • + {% endif %} +
+ {% else %} +

No

+ {% endif %} +{% endset %} + +
+ {{ summaryList({ + rows: [ + { + key: { + text: "HRT (hormone replacement therapy)" + }, + value: { + html: hrtInputHtml if allowEdits else hrtSummaryHtml + } + }, + { + key: { + text: "Pregnant or breastfeeding" + }, + value: { + html: pregnantOrBreastfeedingInputHtml if allowEdits else pregnantOrBreastfeedingSummaryHtml + } + } + ] + } | removeLastRowBorder ) }} +
diff --git a/app/views/_includes/medical-information/index.njk b/app/views/_includes/medical-information/index.njk index 0091efeb..19505023 100644 --- a/app/views/_includes/medical-information/index.njk +++ b/app/views/_includes/medical-information/index.njk @@ -224,18 +224,10 @@ {% set scrollTo = breastDensitySectionId %} {% set breastDensityFactorsHtml %} - {% include "_includes/summary-lists/medical-information/breast-density-factors.njk" %} + {% include "_includes/forms/breast-density-factors.njk" %} {% endset %} -{# Count selected breast density factors for expander summary #} -{% set _bdf = appointment.medicalInformation.breastDensityFactors %} -{% if _bdf and not (_bdf | isArray) %}{% set _bdf = [_bdf] %}{% endif %} -{% set _bdfHrt = appointment.medicalInformation.breastDensityFactorsHrt %} -{% if not _bdfHrt and _bdf and _bdf | includes("hrt") %}{% set _bdfHrt = "yes" %}{% endif %} -{% set breastDensityCount = 0 %} -{% if _bdfHrt == "yes" %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} -{% if _bdf and _bdf | includes("pregnant") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} -{% if _bdf and _bdf | includes("breastfeeding") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} +{% set breastDensityCount = (appointment.medicalInformation | getBreastDensityFactors).count %} {% set breastDensityContentsSummary = breastDensityCount ~ (" breast density factor added" if breastDensityCount == 1 else " breast density factors added") if breastDensityCount > 0 else "No breast density factors added" %} {% switch displayFormat %} diff --git a/app/views/_includes/summary-lists/medical-info-summary.njk b/app/views/_includes/summary-lists/medical-info-summary.njk index d4bbab01..a815c1ba 100644 --- a/app/views/_includes/summary-lists/medical-info-summary.njk +++ b/app/views/_includes/summary-lists/medical-info-summary.njk @@ -128,31 +128,24 @@ {% set mammogramsAddReferrerChain = currentUrl | appendReferrer(contextUrl + "/confirm-information/previous-mammograms") %} {# Breast density factors summary #} -{% set _bdf = appointment.medicalInformation.breastDensityFactors %} -{% if _bdf and not (_bdf | isArray) %}{% set _bdf = [_bdf] %}{% endif %} -{% set _bdfHrt = appointment.medicalInformation.breastDensityFactorsHrt %} -{% if not _bdfHrt and _bdf and _bdf | includes("hrt") %}{% set _bdfHrt = "yes" %}{% endif %} +{% set breastDensityFactors = appointment.medicalInformation | getBreastDensityFactors %} +{# Show the row whenever there's an answer to show, including "not taking HRT" #} +{% set breastDensityCount = breastDensityFactors.answeredCount %} {% set breastDensityFactorsHtml %} - {% if _bdfHrt == "yes" or _bdfHrt == "no" or (_bdf and _bdf | length > 0) %} + {% if breastDensityFactors.summaries | length %}
    - {% if _bdfHrt == "yes" %}
  • Taking HRT
  • {% endif %} - {% if _bdfHrt == "no" %}
  • Not taking HRT
  • {% endif %} - {% if _bdf and _bdf | includes("pregnant") %}
  • Pregnant
  • {% endif %} - {% if _bdf and _bdf | includes("breastfeeding") %}
  • Breastfeeding
  • {% endif %} + {% for summary in breastDensityFactors.summaries %} +
  • {{ summary }}
  • + {% endfor %}
{% else %}

No breast density factors added

{% endif %} {% endset %} -{% set breastDensityCount = 0 %} -{% if _bdfHrt == "yes" %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} -{% if _bdf and _bdf | includes("pregnant") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} -{% if _bdf and _bdf | includes("breastfeeding") %}{% set breastDensityCount = breastDensityCount + 1 %}{% endif %} - {# Other medical information summary #} -{% set otherMedicalInfo = appointment.medicalInformation.otherMedicalInformation %} +{% set otherMedicalInfo = appointment.medicalInformation | summariseOtherMedicalInformation %} {% set otherMedicalInfoHtml %} {% if otherMedicalInfo %}

{{ otherMedicalInfo }}

diff --git a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk b/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk deleted file mode 100644 index eb5c2a2d..00000000 --- a/app/views/_includes/summary-lists/medical-information/breast-density-factors.njk +++ /dev/null @@ -1,112 +0,0 @@ -{# app/views/_includes/summary-lists/medical-information/breast-density-factors.njk #} - -{% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} -{% set breastDensityFactorsHrt = appointment.medicalInformation.breastDensityFactorsHrt %} - -{% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} - {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} -{% endif %} - -{% if not breastDensityFactorsHrt and selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt") %} - {% set breastDensityFactorsHrt = "yes" %} -{% endif %} - -{% set participantName = participant | getFullName %} - -{% set breastDensityFactorsHrtQuestion = "Has " + participantName + " begun course of HRT since their last screening appointment?" %} -{% set pregnantOrBreastfeedingQuestion = "Is " + participantName + " pregnant or breastfeeding?" %} - -{% set breastDensityFactorsHrtSummaryHtml %} - {{ "Yes" if breastDensityFactorsHrt == "yes" or (selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt")) else "No" }} -{% endset %} - -{% set pregnantOrBreastfeedingSummaryHtml %} - {% if selectedBreastDensityFactors and (selectedBreastDensityFactors | includes("pregnant") or selectedBreastDensityFactors | includes("breastfeeding")) %} -
    - {% if selectedBreastDensityFactors | includes("pregnant") %} -
  • Pregnant
  • - {% endif %} - {% if selectedBreastDensityFactors | includes("breastfeeding") %} -
  • Breastfeeding
  • - {% endif %} -
- {% else %} -

No

- {% endif %} -{% endset %} - -
- -

Select any current or recent considerations

- -{% set breastDensityFactorsHrtInputHtml %} - {{ radios({ - name: "appointment[medicalInformation][breastDensityFactorsHrt]", - value: breastDensityFactorsHrt, - classes: "nhsuk-radios--inline nhsuk-radios--small", - fieldset: { - legend: { - text: breastDensityFactorsHrtQuestion, - classes: "nhsuk-fieldset__legend--s" - } - }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ] - }) }} -{% endset %} - -{% set pregnantOrBreastfeedingInputHtml %} - {{ checkboxes({ - name: "appointment[medicalInformation][breastDensityFactors]", - values: selectedBreastDensityFactors, - classes: "nhsuk-checkboxes--small", - fieldset: { - legend: { - text: pregnantOrBreastfeedingQuestion, - classes: "nhsuk-fieldset__legend--s" - } - }, - items: [ - { - value: "pregnant", - text: "Pregnant" - }, - { - value: "breastfeeding", - text: "Breastfeeding" - } - ] - }) }} -{% endset %} - -{{ summaryList({ - rows: [ - { - key: { - text: "HRT (Hormone replacement therapy)" - }, - value: { - html: breastDensityFactorsHrtInputHtml if allowEdits else breastDensityFactorsHrtSummaryHtml - } - }, - { - key: { - text: "Pregnant or breastfeeding" - }, - value: { - html: pregnantOrBreastfeedingInputHtml if allowEdits else pregnantOrBreastfeedingSummaryHtml - } - } - ] -} | openInModal | handleSummaryListMissingInformation | removeLastRowBorder ) }} diff --git a/app/views/appointments/check-information.html b/app/views/appointments/check-information.html index 8a061f52..a9adb810 100644 --- a/app/views/appointments/check-information.html +++ b/app/views/appointments/check-information.html @@ -146,7 +146,7 @@ {# Breast density factors card #} {% set breastDensityFactorsHtml %} - {% include "_includes/summary-lists/medical-information/breast-density-factors.njk" %} + {% include "_includes/forms/breast-density-factors.njk" %} {% endset %} {{ card({ diff --git a/app/views/appointments/confirm-information/breast-density-factors.html b/app/views/appointments/confirm-information/breast-density-factors.html index 18cae417..90dcaab7 100644 --- a/app/views/appointments/confirm-information/breast-density-factors.html +++ b/app/views/appointments/confirm-information/breast-density-factors.html @@ -25,7 +25,7 @@

{{ pageHeading }}

{% set breastDensityFactorsHtml %} - {% include "_includes/summary-lists/medical-information/breast-density-factors.njk" %} + {% include "_includes/forms/breast-density-factors.njk" %} {% endset %} {{ card({ diff --git a/app/views/appointments/medical-information/hormone-replacement-therapy.html b/app/views/appointments/medical-information/hormone-replacement-therapy.html deleted file mode 100644 index 049fab23..00000000 --- a/app/views/appointments/medical-information/hormone-replacement-therapy.html +++ /dev/null @@ -1,115 +0,0 @@ -{# app/views/appointments/medical-information/hormone-replacement-therapy.html #} - -{% extends parentLayout or 'layout-appointment.html' %} - -{% set pageHeading = "Hormone replacement therapy (HRT)" %} - -{% set showBackLink = true %} -{# -{% set gridColumn = "nhsuk-grid-column-two-thirds" %} #} - -{% set formAction = './../review-medical-information' | getReturnUrl(referrerChain) %} - -{# {% set back = { - href: "/clinics/" + clinicId, - text: "Back to clinic" -} %} - #} - -{% block pageContent %} - - {#

- - {{ participant | getFullName }} - - {{ pageHeading }} -

#} - - {% set currentlyTakingHtml %} - - {{ input({ - name: "appointment[medicalInformation][hrt][hrtDateStarted]", - value: appointment.medicalInformation.hrt.hrtDateStarted, - label: { - text: "Approximate date started" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, " ~ (now() | remove(18, "months") | formatDate("MMMM YYYY")) - } - }) }} - - {% endset %} - - {% set recentlyStoppedHtml %} - - {{ input({ - name: "appointment[medicalInformation][hrt][hrtDateStopped]", - value: appointment.medicalInformation.hrt.hrtDateStopped, - label: { - text: "Approximate date stopped" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, " ~ (now() | remove(2, "months") | formatDate("MMMM YYYY")) - } - }) }} - - {{ input({ - name: "appointment[medicalInformation][hrt][hrtDurationBeforeStopping]", - value: appointment.medicalInformation.hrt.hrtDurationBeforeStopping, - label: { - text: "Approximate time taken for" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, 5 years" - } - }) }} - - {% endset %} - - - {{ participant | getShortName }} - - {{ radios({ - name: "appointment[medicalInformation][hrt][hrtQuestion]", - idPrefix: "hrtQuestion", - value: appointment.medicalInformation.hrt.hrtQuestion, - fieldset: { - legend: { - text: "Is " + (participant | getShortName) + " currently taking HRT?", - size: "l", - isPageHeading: true - } - }, - items: [ - { - value: "yes", - text: "Yes", - conditional: { - html: currentlyTakingHtml - } - }, - { - value: "no-recently-stopped", - text: "No, but stopped recently", - conditional: { - html: recentlyStoppedHtml - } - }, - { - value: "no", - text: "No" - } - ] - }) }} - - {{ button({ - text: "Continue" - }) }} - - {% include "screening-cannot-proceed-link.njk" %} - -{% endblock %} - diff --git a/app/views/appointments/medical-information/other-medical-information.html b/app/views/appointments/medical-information/other-medical-information.html index de2d8873..a83beb76 100644 --- a/app/views/appointments/medical-information/other-medical-information.html +++ b/app/views/appointments/medical-information/other-medical-information.html @@ -6,21 +6,10 @@ {% set gridColumn = "nhsuk-grid-column-two-thirds" %} -{% set formAction = './other-medical-information-save' | urlWithReferrer(referrerChain, query.scrollTo) %} +{% set formAction = './../review-medical-information' | getReturnUrl(referrerChain, query.scrollTo) %} {% block pageContent %} - {% set selectedBreastDensityFactors = appointment.medicalInformation.breastDensityFactors %} - {% set breastDensityFactorsHrt = appointment.medicalInformation.breastDensityFactorsHrt %} - - {% if selectedBreastDensityFactors and not (selectedBreastDensityFactors | isArray) %} - {% set selectedBreastDensityFactors = [selectedBreastDensityFactors] %} - {% endif %} - - {% if not breastDensityFactorsHrt and selectedBreastDensityFactors and selectedBreastDensityFactors | includes("hrt") %} - {% set breastDensityFactorsHrt = "yes" %} - {% endif %} -

{{ participant | getFullName }} @@ -37,24 +26,6 @@

rows: 10 }) }} - {% if selectedBreastDensityFactors %} - {% for factor in selectedBreastDensityFactors %} - {% if factor != "hrt" %} - {{ appHiddenInput({ - name: "appointment[medicalInformation][breastDensityFactors]", - value: factor - }) }} - {% endif %} - {% endfor %} - {% endif %} - - {% if breastDensityFactorsHrt %} - {{ appHiddenInput({ - name: "appointment[medicalInformation][breastDensityFactorsHrt]", - value: breastDensityFactorsHrt - }) }} - {% endif %} - {{ button({ text: "Save" }) }} diff --git a/app/views/appointments/medical-information/pregnancy-and-breastfeeding.html b/app/views/appointments/medical-information/pregnancy-and-breastfeeding.html deleted file mode 100644 index a3d02d9b..00000000 --- a/app/views/appointments/medical-information/pregnancy-and-breastfeeding.html +++ /dev/null @@ -1,166 +0,0 @@ - -{% extends parentLayout or 'layout-appointment.html' %} - -{% set pageHeading = "Pregnancy and breastfeeding" %} - -{% set gridColumn = "nhsuk-grid-column-two-thirds" %} - -{% set formAction = "./../review-medical-information" | getReturnUrl(referrerChain, query.scrollTo) %} - -{# {% set back = { - href: "/clinics/" + clinicId, - text: "Back to clinic" -} %} - #} - -{% block pageContent %} - - {% set unit = data.breastScreeningUnits | findById(clinic.breastScreeningUnitId) %} - -

- - {{ participant | getFullName }} - - {{ pageHeading }} -

- - {% set currentlyPregnant %} - {{ input({ - name: "appointment[medicalInformation][pregnancyAndBreastfeeding][pregnancyDueDate]", - value: appointment.medicalInformation.pregnancyAndBreastfeeding.pregnancyDueDate, - label: { - text: "Approximate due date" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, " ~ (now() | add(3, "months") | formatDate("MMMM YYYY")) - } - } ) }} - {% endset %} - - {% set recentlyPregnant %} - {{ input({ - name: "appointment[medicalInformation][pregnancyAndBreastfeeding][pregnancyEndDate]", - value: appointment.medicalInformation.pregnancyAndBreastfeeding.pregnancyEndDate, - label: { - text: "Approximate date pregnancy ended" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, " ~ (now() | remove(2, "months") | formatDate("MMMM YYYY")) - } - } ) }} - {% endset %} - - {% set currentlyBreastfeeding %} - {{ input({ - name: "appointment[medicalInformation][pregnancyAndBreastfeeding][breastfeedingStartDate]", - value: appointment.medicalInformation.pregnancyAndBreastfeeding.breastfeedingStartDate, - label: { - text: "Approximate date started" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, since " ~ (now() | remove(2, "months") | formatDate("MMMM YYYY")) - } - } ) }} - {% endset %} - - {% set recentlyBreastfeeding %} - {{ input({ - name: "appointment[medicalInformation][pregnancyAndBreastfeeding][breastfeedingStopDate]", - value: appointment.medicalInformation.pregnancyAndBreastfeeding.breastfeedingStopDate, - label: { - text: "Approximate date stopped" - }, - classes: "nhsuk-u-width-two-thirds", - hint: { - text: "For example, " ~ (now() | remove(3, "months") | formatDate("MMMM YYYY")) - } - } ) }} - {% endset %} - - {{ radios({ - name: "appointment[medicalInformation][pregnancyAndBreastfeeding][pregnancyStatus]", - value: appointment.medicalInformation.pregnancyAndBreastfeeding.pregnancyStatus, - fieldset: { - legend: { - text: "Is " + (participant | getFullName) + " pregnant?", - size: "m", - isPageHeading: false - } - }, - items: [ - { - value: "yes", - text: "Yes", - conditional: { - html: currentlyPregnant - } - }, - { - value: "noButRecently", - text: "No, but has been recently", - conditional: { - html: recentlyPregnant - } - }, - { - divider: "or" - }, - { - value: "noNotPregnant", - text: "No", - conditional: { - html: notPregnant - } - } - ] - } ) }} - - {{ radios({ - name: "appointment[medicalInformation][pregnancyAndBreastfeeding][breastfeedingStatus]", - value: appointment.medicalInformation.pregnancyAndBreastfeeding.breastfeedingStatus, - fieldset: { - legend: { - text: "Are they currently breastfeeding?", - size: "m", - isPageHeading: false - } - }, - items: [ - { - value: "yes", - text: "Yes", - conditional: { - html: currentlyBreastfeeding - } - }, - { - value: "recentlyStopped", - text: "No, but stopped recently", - conditional: { - html: recentlyBreastfeeding - } - }, - { - divider: "or" - }, - { - value: "no", - text: "No", - conditional: { - html: notBreastfeeding - } - } - ] - } ) }} - - {{ button({ - text: "Continue" - }) }} - - {% include "screening-cannot-proceed-link.njk" %} - -{% endblock %} - diff --git a/app/views/index.html b/app/views/index.html index 072fefc0..eb5234f3 100755 --- a/app/views/index.html +++ b/app/views/index.html @@ -147,10 +147,7 @@

Appointment during workflow

Record breast features
  • - Hormone replacement therapy -
  • -
  • - Pregnancy and breastfeeding + Breast density factors
  • diff --git a/app/views/settings/seed-profiles/custom.html b/app/views/settings/seed-profiles/custom.html index 28c16d38..87680e9b 100644 --- a/app/views/settings/seed-profiles/custom.html +++ b/app/views/settings/seed-profiles/custom.html @@ -80,7 +80,7 @@

    Reading backlog

    Medical information

    {{ seedProfilePercentageInput({ id: "probabilityOfSymptoms", name: "settings[seedProfiles][customForm][medicalInformation][probabilityOfSymptoms]", label: "Symptoms present", value: formProfile.medicalInformation.probabilityOfSymptoms * 100 }) }} - {{ seedProfilePercentageInput({ id: "probabilityOfHRT", name: "settings[seedProfiles][customForm][medicalInformation][probabilityOfHRT]", label: "Hormone replacement therapy", value: formProfile.medicalInformation.probabilityOfHRT * 100 }) }} + {{ seedProfilePercentageInput({ id: "probabilityOfHRT", name: "settings[seedProfiles][customForm][medicalInformation][probabilityOfHRT]", label: "Taking HRT", value: formProfile.medicalInformation.probabilityOfHRT * 100 }) }} {{ seedProfilePercentageInput({ id: "probabilityOfPregnancyBreastfeeding", name: "settings[seedProfiles][customForm][medicalInformation][probabilityOfPregnancyBreastfeeding]", label: "Pregnancy or breastfeeding", value: formProfile.medicalInformation.probabilityOfPregnancyBreastfeeding * 100 }) }} {{ seedProfilePercentageInput({ id: "probabilityOfOtherMedicalInfo", name: "settings[seedProfiles][customForm][medicalInformation][probabilityOfOtherMedicalInfo]", label: "Other medical information", value: formProfile.medicalInformation.probabilityOfOtherMedicalInfo * 100 }) }} {{ seedProfilePercentageInput({ id: "probabilityOfBreastFeatures", name: "settings[seedProfiles][customForm][medicalInformation][probabilityOfBreastFeatures]", label: "Breast features", value: formProfile.medicalInformation.probabilityOfBreastFeatures * 100 }) }} diff --git a/docs/DATA-GENERATOR-REFERENCE.md b/docs/DATA-GENERATOR-REFERENCE.md index d454277a..b1d09af8 100644 --- a/docs/DATA-GENERATOR-REFERENCE.md +++ b/docs/DATA-GENERATOR-REFERENCE.md @@ -28,8 +28,7 @@ generate-seed-data.js (main orchestrator) │ │ │ └─> medical-information-generator.js (for completed appointments only) │ ├─> symptoms-generator.js - │ ├─> hrt-generator.js - │ ├─> pregnancy-and-breastfeeding-generator.js + │ ├─> breast-density-factors-generator.js │ ├─> other-medical-information-generator.js │ ├─> breast-features-generator.js │ └─> medical-history-generator.js @@ -311,7 +310,9 @@ For complex data with multiple sub-generators, use an umbrella generator to orch // app/lib/generators/medical-information-generator.js const { generateSymptoms } = require('./medical-information/symptoms-generator') -const { generateHRT } = require('./medical-information/hrt-generator') +const { + generateBreastDensityFactors +} = require('./medical-information/breast-density-factors-generator') // ... other generators const generateMedicalInformation = (options = {}) => { @@ -335,13 +336,12 @@ const generateMedicalInformation = (options = {}) => { medicalInfo.symptoms = symptoms } - const hrt = generateHRT({ - probability: probabilityOfHRT + // Sub-generators can return several keys at once, merged into the parent object + const breastDensityFactors = generateBreastDensityFactors({ + probabilityOfHrt: probabilityOfHRT }) - if (hrt) { - medicalInfo.hrt = hrt - } + Object.assign(medicalInfo, breastDensityFactors) // Only return if we generated anything return medicalInfo @@ -484,7 +484,7 @@ app/lib/generators/ ├── medical-information-generator.js # Umbrella generator ├── medical-information/ # Sub-generators │ ├── symptoms-generator.js -│ ├── hrt-generator.js +│ ├── breast-density-factors-generator.js │ └── medical-history-generator.js └── [new]-generator.js # New generators here ``` diff --git a/docs/MEDICAL-INFORMATION-GENERATOR-GUIDE.md b/docs/MEDICAL-INFORMATION-GENERATOR-GUIDE.md index 0a4a0ac2..08e9bf68 100644 --- a/docs/MEDICAL-INFORMATION-GENERATOR-GUIDE.md +++ b/docs/MEDICAL-INFORMATION-GENERATOR-GUIDE.md @@ -8,8 +8,7 @@ The prototype generates seed data to populate a breast screening management syst - **Medical history** (breast cancer, mastectomy, implants, etc.) - ✅ _implemented (4 of 7 types)_ - **Symptoms** (lumps, pain, nipple changes, etc.) - ✅ _implemented_ -- **HRT** - ✅ _implemented_ -- **Pregnancy and breastfeeding** - ✅ _implemented_ +- **Breast density factors** (HRT, pregnancy, breastfeeding) - ✅ _implemented_ - **Other medical information** (freetext) - ✅ _implemented_ - **Breast features** (moles, scars, etc.) - ✅ _implemented_ @@ -37,8 +36,8 @@ appointment: { participantId: string, medicalInformation: { symptoms: [], // ✅ Array of symptom objects - hrt: {}, // ✅ HRT information - pregnancyAndBreastfeeding: {}, // ✅ Pregnancy and breastfeeding status + breastDensityFactorsHrt: string, // ✅ 'yes' or 'no' - absent if not asked + breastDensityFactors: [], // ✅ Any of 'pregnant', 'breastfeeding' - absent if none otherMedicalInformation: string, // ✅ Freetext medical info breastFeatures: [], // ✅ Array of breast feature objects medicalHistory: { // Object with arrays for each type @@ -69,10 +68,10 @@ app/lib/generators/ ├── medical-information-generator.js # ✅ Umbrella generator for all medical info ├── medical-information/ # ✅ Subfolder for medical info generators │ ├── symptoms-generator.js # ✅ Generates symptoms -│ ├── hrt-generator.js # ✅ Generates HRT data -│ ├── pregnancy-and-breastfeeding-generator.js # ✅ Generates pregnancy/breastfeeding data +│ ├── breast-density-factors-generator.js # ✅ Generates HRT, pregnancy and breastfeeding data │ ├── other-medical-information-generator.js # ✅ Generates freetext medical info -│ └── breast-features-generator.js # ✅ Generates breast features +│ ├── breast-features-generator.js # ✅ Generates breast features +│ └── medical-history-generator.js # ✅ Generates medical history ├── special-appointment-generator.js └── [new]-generator.js # Your new generator here ``` @@ -184,58 +183,38 @@ module.exports = { **Integration:** Called from umbrella generator for completed appointments only. -### HRT Generator +### Breast density factors generator -**File:** `app/lib/generators/medical-information/hrt-generator.js` +**File:** `app/lib/generators/medical-information/breast-density-factors-generator.js` + +Replaces the separate HRT and pregnancy/breastfeeding generators. The question is now a simple yes/no for HRT plus a checkbox group for pregnancy and breastfeeding, so the generated data is just as simple. **Key features:** -- Generates HRT (hormone replacement therapy) information -- Three status options: currently taking, recently stopped, or no HRT -- Conditional fields populate based on status -- 30% default probability of having HRT data +- Generates the factors that affect breast density: HRT, pregnancy and breastfeeding +- 80% default probability the question was asked at all — if it wasn't, `breastDensityFactorsHrt` is left unset, so summaries can distinguish “not answered” from a recorded “no” +- 30% default probability of currently taking HRT +- 5% default probability of being pregnant or breastfeeding (appropriate for the screening age group), split 70/30 towards breastfeeding +- Returns a plain object of keys the umbrella generator merges with `Object.assign`, rather than a nested sub-object -**Data structure:** +**Options:** ```javascript -{ - hrtQuestion: 'yes' | 'no-recently-stopped' | 'no', - // If 'yes': - hrtDateStarted: string, // e.g., 'September 2022' - // If 'no-recently-stopped': - hrtDateStopped: string, // e.g., 'January 2026' - hrtDurationBeforeStopping: string // e.g., '5 years' -} +generateBreastDensityFactors({ + probabilityOfHrt: 0.3, // Chance of currently taking HRT + probabilityOfPregnancyBreastfeeding: 0.05, // Chance of being pregnant or breastfeeding + probabilityOfBeingAsked: 0.8 // Chance the question was asked at all +}) ``` -**Integration:** Called from umbrella generator for completed appointments only. - -### Pregnancy and Breastfeeding Generator - -**File:** `app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js` - -**Key features:** - -- Generates pregnancy and breastfeeding status -- Smart logic: if pregnant → not breastfeeding; if recently pregnant → likely breastfeeding -- Conditional fields populate based on status -- 5% default probability (appropriate for screening age group) - **Data structure:** ```javascript { - pregnancyStatus: 'yes' | 'noButRecently' | 'noNotPregnant', - // If 'yes': - pregnancyDueDate: string, // e.g., 'June 2026' - // If 'noButRecently': - pregnancyEndDate: string, // e.g., 'January 2026' - - breastfeedingStatus: 'yes' | 'recentlyStopped' | 'no', - // If 'yes': - breastfeedingStartDate: string, // e.g., 'January 2026' - // If 'recentlyStopped': - breastfeedingStopDate: string // e.g., 'December 2025' + // Absent if the question wasn't asked + breastDensityFactorsHrt: 'yes' | 'no', + // Absent if neither applies + breastDensityFactors: ['pregnant' | 'breastfeeding'] } ``` @@ -426,10 +405,9 @@ if (isCompleted(appointmentStatus)) { // app/lib/generators/medical-information-generator.js const { generateSymptoms } = require('./medical-information/symptoms-generator') -const { generateHRT } = require('./medical-information/hrt-generator') const { - generatePregnancyAndBreastfeeding -} = require('./medical-information/pregnancy-and-breastfeeding-generator') + generateBreastDensityFactors +} = require('./medical-information/breast-density-factors-generator') const { generateOtherMedicalInformation } = require('./medical-information/other-medical-information-generator') @@ -445,8 +423,8 @@ const { * @param {object} options - Generation options * @param {string} [options.addedByUserId] - User ID who collected this information * @param {number} [options.probabilityOfSymptoms=0.85] - Chance of having symptoms - * @param {number} [options.probabilityOfHRT=0.30] - Chance of having HRT data - * @param {number} [options.probabilityOfPregnancyBreastfeeding=0.05] - Chance of pregnancy/breastfeeding + * @param {number} [options.probabilityOfHRT=0.30] - Chance of currently taking HRT + * @param {number} [options.probabilityOfPregnancyBreastfeeding=0.05] - Chance of being pregnant or breastfeeding * @param {number} [options.probabilityOfOtherMedicalInfo=0.15] - Chance of other medical info * @param {number} [options.probabilityOfBreastFeatures=0.20] - Chance of having breast features * @param {object} [options.config] - Participant config for overrides @@ -475,23 +453,14 @@ const generateMedicalInformation = (options = {}) => { medicalInfo.symptoms = symptoms } - // Generate HRT information - const hrt = generateHRT({ - probability: probabilityOfHRT + // Generate breast density factors (HRT, pregnancy, breastfeeding) + // This generator returns top-level keys, so its result is merged in + const breastDensityFactors = generateBreastDensityFactors({ + probabilityOfHrt: probabilityOfHRT, + probabilityOfPregnancyBreastfeeding }) - if (hrt) { - medicalInfo.hrt = hrt - } - - // Generate pregnancy and breastfeeding information - const pregnancyAndBreastfeeding = generatePregnancyAndBreastfeeding({ - probability: probabilityOfPregnancyBreastfeeding - }) - - if (pregnancyAndBreastfeeding) { - medicalInfo.pregnancyAndBreastfeeding = pregnancyAndBreastfeeding - } + Object.assign(medicalInfo, breastDensityFactors) // Generate other medical information const otherMedicalInformation = generateOtherMedicalInformation({ @@ -1219,6 +1188,8 @@ if (isCompleted(appointmentStatus)) { - Smart conditional logic (pregnant → not breastfeeding, etc.) - Data matches form structure exactly + _Both of these were later replaced by the breast density factors generator when the questions were redesigned into a single yes/no plus a checkbox group._ + 8. ✅ **Implemented other medical information generator:** - Created `app/lib/generators/medical-information/other-medical-information-generator.js` - 15% default probability @@ -1255,8 +1226,7 @@ if (isCompleted(appointmentStatus)) { - `app/lib/generators/medical-information-generator.js` - ✅ Umbrella generator (orchestrator) - `app/lib/generators/medical-information/symptoms-generator.js` - ✅ Symptoms generator -- `app/lib/generators/medical-information/hrt-generator.js` - ✅ HRT generator -- `app/lib/generators/medical-information/pregnancy-and-breastfeeding-generator.js` - ✅ Pregnancy/breastfeeding generator +- `app/lib/generators/medical-information/breast-density-factors-generator.js` - ✅ Breast density factors generator (HRT, pregnancy, breastfeeding) - `app/lib/generators/medical-information/other-medical-information-generator.js` - ✅ Other medical info generator - `app/lib/generators/medical-information/breast-features-generator.js` - ✅ Breast features generator - `app/lib/generators/medical-information/medical-history-generator.js` - ✅ Medical history generator (4 of 7 types) @@ -1268,8 +1238,8 @@ if (isCompleted(appointmentStatus)) { ### Routes & Views (for reference) - `app/routes/appointments.js` - Routes that handle medical information (shows expected data structure) -- `app/views/appointments/medical-information/hormone-replacement-therapy.html` - HRT form template -- `app/views/appointments/medical-information/pregnancy-and-breastfeeding.html` - Pregnancy/breastfeeding form template +- `app/views/_includes/forms/breast-density-factors.njk` - Breast density factors form fields +- `app/views/appointments/confirm-information/breast-density-factors.html` - Breast density factors edit and review page - `app/views/appointments/medical-information/other-medical-information.html` - Other medical info form template - `app/views/appointments/medical-information/record-breast-features.html` - Breast features diagram interface - `app/views/appointments/medical-information/medical-history/*.html` - Medical history form templates diff --git a/docs/utils-filter-reference.md b/docs/utils-filter-reference.md index 922b21f6..c89c0756 100644 --- a/docs/utils-filter-reference.md +++ b/docs/utils-filter-reference.md @@ -3,7 +3,7 @@ --- **Auto-generated** — do not edit manually. -- **Generated:** 2026-07-15 11:33 UTC +- **Generated:** 2026-08-03 09:24 UTC - **Source:** `app/lib/utils/` and `app/filters/` - **Regenerate:** `npm run docs` @@ -21,24 +21,24 @@ | `participants.js` | Participant lookups and derived data: full/short names, age, clinic history, and risk level. | 144 | | `appointment-data.js` | Appointment lookups and mutations in session data | 164 | | `episodes.js` | Episode lookups and stage changes | 178 | -| `clinics.js` | Clinic filtering by time period, slot formatting, and opening hours calculation. | 208 | -| `reading.js` | Image reading workflow: read state, progress tracking, batch management, per-user navigation, and filtering | 223 | -| `prior-mammograms.js` | Prior mammogram request state (awaiting, unrequested, resolved) and one-line summary helpers. | 279 | -| `medical-information.js` | Summarise medical history items, symptoms, breast features, and other clinical information into concise display strings. | 298 | -| `annotation-summary.js` | Summarise image reading annotations (abnormality type, level of concern, location) into concise display strings. | 319 | -| `arrays.js` | Array helpers: find by key/id, filter, push (immutable), remove empty | 332 | -| `objects.js` | Object utilities for extracting and flattening values. | 350 | -| `summary-list.js` | NHS summary list helpers: replace empty row values with "Enter X" links or "Not provided" text, and remove the bottom border from the last row. | 360 | -| `random.js` | Seeded random functions for stable prototype data | 371 | -| `referrers.js` | Referrer chain navigation for multi-level back links | 388 | -| `roles-and-permissions.js` | User role checks | 401 | -| `utility.js` | General-purpose type coercion (`falsify`) and limiting utilities. | 419 | +| `clinics.js` | Clinic filtering by time period, slot formatting, and opening hours calculation. | 209 | +| `reading.js` | Image reading workflow: read state, progress tracking, batch management, per-user navigation, and filtering | 224 | +| `prior-mammograms.js` | Prior mammogram request state (awaiting, unrequested, resolved) and one-line summary helpers. | 280 | +| `medical-information.js` | Summarise medical history items, symptoms, breast features, and other clinical information into concise display strings. | 299 | +| `annotation-summary.js` | Summarise image reading annotations (abnormality type, level of concern, location) into concise display strings. | 322 | +| `arrays.js` | Array helpers: find by key/id, filter, push (immutable), remove empty | 335 | +| `objects.js` | Object utilities for extracting and flattening values. | 353 | +| `summary-list.js` | NHS summary list helpers: replace empty row values with "Enter X" links or "Not provided" text, and remove the bottom border from the last row. | 363 | +| `random.js` | Seeded random functions for stable prototype data | 374 | +| `referrers.js` | Referrer chain navigation for multi-level back links | 391 | +| `roles-and-permissions.js` | User role checks | 404 | +| `utility.js` | General-purpose type coercion (`falsify`) and limiting utilities. | 422 | | | | | -| `formatting.js` | Display formatting for yes/no answers and ordinal names. (filter only) | 435 | -| `forms.js` | Injects matching flash error messages into NHS form component configs by field name. (filter only) | 447 | -| `nunjucks.js` | Nunjucks-specific helpers: joining arrays, resolving user names from IDs, template debugging, and template literal support. (filter only) | 459 | -| `tags.js` | Convert status strings to NHS `` HTML elements. (filter only) | 473 | -| `markdown.js` | Convert markdown strings to Nunjucks-safe HTML using markdown-it (filter only) | 483 | +| `formatting.js` | Display formatting for yes/no answers and ordinal names. (filter only) | 438 | +| `forms.js` | Injects matching flash error messages into NHS form component configs by field name. (filter only) | 450 | +| `nunjucks.js` | Nunjucks-specific helpers: joining arrays, resolving user names from IDs, template debugging, and template literal support. (filter only) | 462 | +| `tags.js` | Convert status strings to NHS `` HTML elements. (filter only) | 476 | +| `markdown.js` | Convert markdown strings to Nunjucks-safe HTML using markdown-it (filter only) | 486 | --- @@ -134,12 +134,12 @@ Appointment status checks and display helpers. Use these instead of comparing st | `isActive(input)` | Check if a status represents an active appointment | 134 | | `isAppointmentWorkflow(appointment, currentUser)` | Check if an appointment is in the appointment workflow for the current user | 146 | | `eligibleForReading(appointment)` | Check if a status indicates reading is eligible | 178 | -| `getStatusTagColour(status, [vocabulary])` | Map a status key to its NHS tag colour string — e.g. `getStatusTagColour('complete', 'appointment') // 'green'` | 309 | -| `getStatusText(status, [vocabulary])` | Map a status key to its display text — e.g. `getStatusText('complete', 'appointment') // 'Screened'` | 323 | -| `filterAppointmentsByStatus(appointments, filter)` | Filter appointments by status category | 337 | -| `isSpecialAppointment(appointment)` | Check if an appointment is a special appointment | 365 | -| `hasAppointmentNote(appointment)` | Check if an appointment has an appointment note | 375 | -| `hasSymptoms(appointment)` | Check if an appointment has recorded symptoms | 385 | +| `getStatusTagColour(status, [vocabulary])` | Map a status key to its NHS tag colour string — e.g. `getStatusTagColour('complete', 'appointment') // 'green'` | 313 | +| `getStatusText(status, [vocabulary])` | Map a status key to its display text — e.g. `getStatusText('complete', 'appointment') // 'Screened'` | 327 | +| `filterAppointmentsByStatus(appointments, filter)` | Filter appointments by status category | 341 | +| `isSpecialAppointment(appointment)` | Check if an appointment is a special appointment | 369 | +| `hasAppointmentNote(appointment)` | Check if an appointment has an appointment note | 379 | +| `hasSymptoms(appointment)` | Check if an appointment has recorded symptoms | 389 | ### participants.js @@ -183,27 +183,28 @@ Episode lookups and stage changes. An episode is one screening round - the conta | Function | Description | Line | |---|---|---| -| `appointmentProducedImages(appointment)` | Whether an appointment's status means mammograms were taken. | 89 | -| `buildMammogramEntry(appointment, [clinic])` | Build the episode's summary record of one set of mammograms. | 103 | -| `getEpisode(data, episodeId)` | Get an episode by ID | 144 | -| `getEpisodesForParticipant(data, participantId)` | Get all of a participant's episodes, oldest first | 165 | -| `getCurrentEpisode(data, participantId)` | Get a participant's current episode - their most recent one that hasn't | 195 | -| `getEpisodeAppointments(data, episode)` | Get an episode's appointments, oldest first | 211 | -| `getEpisodeReadingStatus(data, episode, [userId])` | Get the reading status of an episode, derived from its appointments. | 226 | -| `isEpisodeClosed(episode)` | Whether an episode has closed | 241 | -| `isEpisodeOpen(episode)` | Whether an episode is still open - anything that hasn't closed, whatever | 251 | -| `getEpisodeMammogramDate(episode)` | When this round's mammograms were taken, from the episode's own record. | 262 | -| `getLastMammogram(data, participantId)` | The participant's last mammogram on record, before today. | 277 | -| `getNextAppointment(data, participantId)` | The participant's next booked appointment, if they have one. | 324 | -| `getEpisodeStageText(stage)` | Display text for an episode's stage | 343 | -| `getEpisodeStageTagColour(stage)` | Tag colour for an episode's stage | 353 | -| `getEpisodeOutcomeText(outcome)` | Display text for an episode's outcome | 363 | -| `getEpisodeOutcomeTagColour(outcome)` | Tag colour for an episode's outcome | 373 | -| `updateEpisode(data, episodeId, updates)` | Update an episode, persisting the change for this session. | 383 | -| `updateEpisodeStage(data, episodeId, stage, [options])` | Advance an episode to a new stage, appending to its stageHistory. | 416 | -| `syncEpisodeMammogramsForAppointment(data, appointment)` | Keep an episode's mammograms record in step with one of its appointments. | 465 | -| `advanceEpisodeForAppointmentStatus(data, appointment)` | Move an appointment's episode to wherever the appointment's status leaves it. | 505 | -| `advanceEpisodeForReadingOutcome(data, appointment, readingOutcome)` | Move an appointment's episode to wherever its reading outcome leaves it. | 538 | +| `appointmentProducedImages(appointment)` | Whether an appointment's status means mammograms were taken. | 91 | +| `buildMammogramEntry(appointment, [clinic])` | Build the episode's summary record of one set of mammograms. | 105 | +| `getEpisode(data, episodeId)` | Get an episode by ID | 146 | +| `getEpisodesForParticipant(data, participantId)` | Get all of a participant's episodes, oldest first | 167 | +| `getCurrentEpisode(data, participantId)` | Get a participant's current episode - their most recent one that hasn't | 197 | +| `getEpisodeAppointments(data, episode)` | Get an episode's appointments, oldest first | 213 | +| `getEpisodeReadingStatus(data, episode, [userId])` | Get the reading status of an episode, derived from its appointments. | 228 | +| `isEpisodeClosed(episode)` | Whether an episode has closed | 243 | +| `isEpisodeOpen(episode)` | Whether an episode is still open - anything that hasn't closed, whatever | 253 | +| `getEpisodeMammogramDate(episode)` | When this round's mammograms were taken, from the episode's own record. | 264 | +| `getLastMammogram(data, participantId)` | The participant's last mammogram on record, before today. | 279 | +| `getNextAppointment(data, participantId)` | The participant's next booked appointment, if they have one. | 326 | +| `getEpisodeLabel(episode)` | Human name for an episode. Episodes are named by date, not number - | 352 | +| `getEpisodeStageText(stage)` | Display text for an episode's stage | 368 | +| `getEpisodeStageTagColour(stage)` | Tag colour for an episode's stage | 378 | +| `getEpisodeOutcomeText(outcome)` | Display text for an episode's outcome | 388 | +| `getEpisodeOutcomeTagColour(outcome)` | Tag colour for an episode's outcome | 398 | +| `updateEpisode(data, episodeId, updates)` | Update an episode, persisting the change for this session. | 408 | +| `updateEpisodeStage(data, episodeId, stage, [options])` | Advance an episode to a new stage, appending to its stageHistory. | 441 | +| `syncEpisodeMammogramsForAppointment(data, appointment)` | Keep an episode's mammograms record in step with one of its appointments. | 490 | +| `advanceEpisodeForAppointmentStatus(data, appointment)` | Move an appointment's episode to wherever the appointment's status leaves it. | 530 | +| `advanceEpisodeForReadingOutcome(data, appointment, readingOutcome)` | Move an appointment's episode to wherever its reading outcome leaves it. | 563 | ### clinics.js @@ -303,18 +304,20 @@ Summarise medical history items, symptoms, breast features, and other clinical i | Function | Description | Line | |---|---|---| -| `isValidMedicalHistoryType(type)` | Check whether a string names a medical history type, by type or slug | 5 | -| `getMedicalHistoryType(type)` | Get a medical history type object, by type or slug | 17 | -| `getMedicalHistoryKeyFromSlug(slug)` | Get the camelCase data key for a medical history type from its slug | 30 | -| `summariseMedicalHistoryItem(item)` | Summarise a single medical history item into a concise string | 41 | -| `summariseMedicalHistory(medicalHistory)` | Summarise all medical history items into an array of summary strings | 214 | -| `getMedicalHistoryItems(medicalHistory)` | Get all medical history items as a flat array | 243 | -| `countMedicalHistoryItems(medicalHistory)` | Count total number of medical history items | 265 | -| `summariseSymptom(symptom)` | Summarise a single symptom into a concise string | 287 | -| `summariseSymptoms(symptoms)` | Summarise all symptoms into an array of summary strings | 359 | -| `summariseBreastFeature(feature)` | Summarise a single breast feature into a concise string | 373 | -| `summariseBreastFeatures(features)` | Summarise all breast features into an array of summary strings | 395 | -| `summariseOtherRelevantInformation(medicalInformation)` | Summarise other relevant medical information (HRT, pregnancy/breastfeeding, other info) | 411 | +| `isValidMedicalHistoryType(type)` | Check whether a string names a medical history type, by type or slug | 6 | +| `getMedicalHistoryType(type)` | Get a medical history type object, by type or slug | 18 | +| `getMedicalHistoryKeyFromSlug(slug)` | Get the camelCase data key for a medical history type from its slug | 31 | +| `summariseMedicalHistoryItem(item)` | Summarise a single medical history item into a concise string | 42 | +| `summariseMedicalHistory(medicalHistory)` | Summarise all medical history items into an array of summary strings | 215 | +| `getMedicalHistoryItems(medicalHistory)` | Get all medical history items as a flat array | 244 | +| `countMedicalHistoryItems(medicalHistory)` | Count total number of medical history items | 266 | +| `summariseSymptom(symptom)` | Summarise a single symptom into a concise string | 288 | +| `summariseSymptoms(symptoms)` | Summarise all symptoms into an array of summary strings | 367 | +| `summariseBreastFeature(feature)` | Summarise a single breast feature into a concise string | 381 | +| `summariseBreastFeatures(features)` | Summarise all breast features into an array of summary strings | 403 | +| `getBreastDensityFactors(medicalInformation)` | Read the breast density factors off an appointment's medical information | 419 | +| `summariseBreastDensityFactors(medicalInformation)` | Summarise breast density factors into an array of summary strings | 455 | +| `summariseOtherMedicalInformation(medicalInformation)` | Summarise the free-text other medical information, truncating if long | 490 | ### annotation-summary.js @@ -466,9 +469,9 @@ Nunjucks-specific helpers: joining arrays, resolving user names from IDs, templa |---|---|---| | `log(a, [description])` | Render a value to the browser console via an inline script tag (for template debugging) | 5 | | `join(input, [delimiter], [attribute], [options], [options.filterEmpty], [options.toString])` | Safely join array elements with proper undefined/null handling — e.g. `join(['a', 'b', 'c'], ', ') // 'a, b, c'` | 22 | -| `getUsername(userId, [options], [options.identifyCurrentUser], [options.format])` | Get user name by user ID with format options | 94 | -| `getContext()` | Return the full Nunjucks template context — useful for debugging | 136 | -| `parseJsonString(value)` | Safely parse a JSON string and return the resulting object, or return structured data as-is | 145 | +| `getUsername(userId, [options], [options.identifyCurrentUser], [options.useYou], [options.format])` | Get user name by user ID with format options | 94 | +| `getContext()` | Return the full Nunjucks template context — useful for debugging | 142 | +| `parseJsonString(value)` | Safely parse a JSON string and return the resulting object, or return structured data as-is | 151 | ### tags.js