diff --git a/app/assets/javascript/main.js b/app/assets/javascript/main.js index bfe28e13..bdc7aa2e 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,101 @@ 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 container = document.querySelector('[data-breast-density-factors-save-url]') + if (!container) { + return + } + + const saveUrl = container.dataset.breastDensityFactorsSaveUrl + if (!saveUrl) { + return + } + + 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 + } + + // 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') + + if (!summary) { + return + } + + // "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` + } + } + + // 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() + + 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) + } + + updateContentsSummary() + + 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() + }) + + if (!response.ok) { + throw new Error( + `Breast density factors auto-save failed (${response.status})` + ) + } + }) + .catch((error) => console.error(error)) + } + + 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. // On close, the page reloads to pick up any changes. document.addEventListener('keydown', (e) => { 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 1834ab14..bf53d8c3 100644 --- a/app/lib/utils/medical-information.js +++ b/app/lib/utils/medical-information.js @@ -417,87 +417,97 @@ 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 = [] - // 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 + if (hrt === 'yes') { + summaries.push('Taking HRT') + } else if (hrt === 'no') { + summaries.push('Not 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') - } - } - - // 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 (factors.includes('pregnant')) { + summaries.push('Pregnant') } - // 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) - } + if (factors.includes('breastfeeding')) { + summaries.push('Breastfeeding') } return summaries } +/** + * 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 otherInfo.length > 100 ? otherInfo.substring(0, 100) + '...' : otherInfo +} + module.exports = { isValidMedicalHistoryType, getMedicalHistoryType, @@ -510,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 aee90aad..11dd646d 100644 --- a/app/routes/appointments/medical-information.js +++ b/app/routes/appointments/medical-information.js @@ -9,6 +9,64 @@ const { } = require('../../lib/utils/referrers') module.exports = (router) => { + // 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 + + // 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 + } + + const medicalInformation = data.appointment.medicalInformation || {} + medicalInformation.breastDensityFactors = factors + + if (postedHrt) { + medicalInformation.breastDensityFactorsHrt = postedHrt + } + + data.appointment.medicalInformation = medicalInformation + + // 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 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 78da700f..19505023 100644 --- a/app/views/_includes/medical-information/index.njk +++ b/app/views/_includes/medical-information/index.njk @@ -219,60 +219,72 @@ {% 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/forms/breast-density-factors.njk" %} {% endset %} -{% set otherRelevantInformationCount = 0 %} +{% 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" %} -{% if appointment.medicalInformation.hrt.hrtQuestion == 'yes' %} - {% set otherRelevantInformationCount = otherRelevantInformationCount + 1 %} -{% 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 %} -{% if appointment.medicalInformation.pregnancyAndBreastfeeding.pregnancyStatus == 'yes' %} - {% set otherRelevantInformationCount = otherRelevantInformationCount + 1 %} -{% endif %} +{# -------------------------------------------------------------- #} +{# Other medical information #} +{% set otherMedicalInfoSectionId = "other-medical-information" %} +{% set scrollTo = otherMedicalInfoSectionId %} -{% if appointment.medicalInformation.pregnancyAndBreastfeeding.breastfeedingStatus == 'yes' %} - {% set otherRelevantInformationCount = otherRelevantInformationCount + 1 %} -{% endif %} +{% set otherMedicalInformationHtml %} + {% include "_includes/summary-lists/medical-information/other-relevant-information.njk" %} +{% endset %} -{% if otherRelevantInformationCount > 0 %} - {% set otherRelevantInformationSummary = otherRelevantInformationCount ~ " other relevant information added" %} -{% else %} - {% set otherRelevantInformationSummary = "No other relevant information added" %} -{% endif %} +{% 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..a815c1ba 100644 --- a/app/views/_includes/summary-lists/medical-info-summary.njk +++ b/app/views/_includes/summary-lists/medical-info-summary.njk @@ -127,23 +127,30 @@ {# 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 breastDensityFactors = appointment.medicalInformation | getBreastDensityFactors %} +{# Show the row whenever there's an answer to show, including "not taking HRT" #} +{% set breastDensityCount = breastDensityFactors.answeredCount %} -{# Build other relevant information HTML #} -{% set otherRelevantInfoHtml %} - {% if otherRelevantInfoCount == 0 %} -

No other information added

- {% elif otherRelevantInfoCount == 1 %} -

{{ otherRelevantInfoSummaries[0] }}

- {% else %} +{% set breastDensityFactorsHtml %} + {% if breastDensityFactors.summaries | length %}
    - {% for summary in otherRelevantInfoSummaries %} + {% for summary in breastDensityFactors.summaries %}
  • {{ summary }}
  • {% endfor %}
+ {% else %} +

No breast density factors added

+ {% endif %} +{% endset %} + +{# Other medical information summary #} +{% set otherMedicalInfo = appointment.medicalInformation | summariseOtherMedicalInformation %} +{% set otherMedicalInfoHtml %} + {% if otherMedicalInfo %} +

{{ otherMedicalInfo }}

+ {% else %} +

No other medical information added

{% endif %} {% endset %} @@ -266,20 +273,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/other-relevant-information.njk b/app/views/_includes/summary-lists/medical-information/other-relevant-information.njk index 9e969635..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,122 +1,9 @@ {# app/views/_includes/summary-lists/medical-information/other-relevant-information #} - -{% set hrtHtml %} - {% set hrtData = appointment.medicalInformation.hrt %} - - {% 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 %} - {% else %} - {{ valueHtml }} - {% 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 %} - -{% 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: "Hormone replacement therapy (HRT)" - }, - 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 - }, { key: { text: "Other medical information" diff --git a/app/views/appointments/check-information.html b/app/views/appointments/check-information.html index ea32df8d..a9adb810 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/forms/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..90dcaab7 --- /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/forms/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/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/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