diff --git a/app/filters/nunjucks.js b/app/filters/nunjucks.js
index 28780fa9b..11b74e2c3 100644
--- a/app/filters/nunjucks.js
+++ b/app/filters/nunjucks.js
@@ -110,7 +110,7 @@ const getUsername = function (userId, options = {}) {
const user = users.find((u) => u.id === userId)
if (!user) return userId
- // Format options: full (default), short (initial + surname), initial (just initials)
+ // Format options: full (default), short (initial + surname), initial (just initials), reversed (SURNAME, First)
const format = options.format || 'full'
let formattedName
@@ -121,6 +121,9 @@ const getUsername = function (userId, options = {}) {
case 'initial':
formattedName = `${user.firstName.charAt(0)}${user.lastName.charAt(0)}`
break
+ case 'reversed':
+ formattedName = `${user.lastName.toUpperCase()}, ${user.firstName}`
+ break
case 'full':
default:
formattedName = `${user.firstName} ${user.lastName}`
diff --git a/app/lib/utils/roles-and-permissions.js b/app/lib/utils/roles-and-permissions.js
index 367d5065e..6345af477 100644
--- a/app/lib/utils/roles-and-permissions.js
+++ b/app/lib/utils/roles-and-permissions.js
@@ -1,6 +1,7 @@
// app/lib/utils/roles-and-permissions.js
const { isMedicalHistoryItemRemoved } = require('./medical-information')
+const { getUsername } = require('../../filters/nunjucks')
// Implanted devices that need a mammographer with implant imaging training.
// The prototype treats every device type as needing it - narrow this list if
@@ -79,17 +80,9 @@ const hasAnyPermission = (user, permissions) => {
return permissions.some((permission) => hasPermission(user, permission))
}
-/**
- * Check whether an appointment needs a user with implant imaging training
- *
- * Breast implants and implanted medical devices both need it, but only while
- * they are still in place - once removed, any clinician can image the
- * participant.
- *
- * @param {object} appointment - Appointment object
- * @returns {boolean} Whether implant imaging training is needed
- */
-const requiresImplantImaging = (appointment) => {
+// Breakdown of what on an appointment needs implant imaging training. Kept
+// unexported so it doesn't become a Nunjucks filter.
+const getImplantImagingNeeds = (appointment) => {
const medicalHistory = appointment?.medicalInformation?.medicalHistory || {}
const hasActiveBreastImplants = (
@@ -104,9 +97,77 @@ const requiresImplantImaging = (appointment) => {
}
)
+ return { hasActiveBreastImplants, hasActiveDevice }
+}
+
+/**
+ * Check whether an appointment needs a user with implant imaging training
+ *
+ * Breast implants and implanted medical devices both need it, but only while
+ * they are still in place - once removed, any clinician can image the
+ * participant.
+ *
+ * @param {object} appointment - Appointment object
+ * @returns {boolean} Whether implant imaging training is needed
+ */
+const requiresImplantImaging = (appointment) => {
+ const { hasActiveBreastImplants, hasActiveDevice } =
+ getImplantImagingNeeds(appointment)
+
return hasActiveBreastImplants || hasActiveDevice
}
+/**
+ * Describe why an appointment needs a mammographer with implant imaging
+ * training, for use in page content
+ *
+ * @param {object} appointment - Appointment object
+ * @returns {string} Reason text, or '' when implant imaging is not needed
+ */
+const getImplantImagingReason = (appointment) => {
+ const { hasActiveBreastImplants, hasActiveDevice } =
+ getImplantImagingNeeds(appointment)
+
+ if (hasActiveBreastImplants && hasActiveDevice) {
+ return 'breast implants and an implanted medical device'
+ }
+ if (hasActiveBreastImplants) return 'breast implants'
+ if (hasActiveDevice) return 'an implanted medical device'
+ return ''
+}
+
+/**
+ * Check whether an authorised mammographer other than the current user has
+ * been nominated to take the images for an appointment
+ *
+ * @param {object} appointment - Appointment object
+ * @returns {boolean} Whether an authorised mammographer has been nominated
+ */
+const hasNominatedAuthorisedMammographer = (appointment) => {
+ return Boolean(
+ appointment?.authorisedMammographerId ||
+ appointment?.authorisedMammographerOtherName
+ )
+}
+
+/**
+ * Get the display name of the authorised mammographer nominated for an
+ * appointment
+ *
+ * Returns '' when nobody has been nominated, so callers can decide their own
+ * fallback.
+ *
+ * @param {object} appointment - Appointment object
+ * @param {object} [options] - Display options, as accepted by getUsername
+ * @returns {string} Nominated authorised mammographer's name, or ''
+ */
+const getAuthorisedMammographerName = function (appointment, options = {}) {
+ if (appointment?.authorisedMammographerId) {
+ return getUsername.call(this, appointment.authorisedMammographerId, options)
+ }
+ return appointment?.authorisedMammographerOtherName || ''
+}
+
/**
* Check whether a user is able to screen a given appointment
*
@@ -208,6 +269,9 @@ module.exports = {
hasPermission,
hasAnyPermission,
requiresImplantImaging,
+ getImplantImagingReason,
+ hasNominatedAuthorisedMammographer,
+ getAuthorisedMammographerName,
canUserScreenAppointment,
isClinician,
isAdministrative,
diff --git a/app/routes/appointments/lifecycle.js b/app/routes/appointments/lifecycle.js
index d22896382..dae3a227e 100644
--- a/app/routes/appointments/lifecycle.js
+++ b/app/routes/appointments/lifecycle.js
@@ -337,6 +337,94 @@ module.exports = (router) => {
}
)
+ // Change the authorised mammographer from the in-page modal, then return to
+ // the page it was opened from with a success banner if it actually changed
+ router.post(
+ '/clinics/:clinicId/appointments/:appointmentId/change-authorised-mammographer-answer',
+ (req, res) => {
+ const { clinicId, appointmentId } = req.params
+ const data = req.session.data
+ const appointmentUrl = `/clinics/${clinicId}/appointments/${appointmentId}`
+
+ const authorisedMammographerTemp =
+ data.appointment?.authorisedMammographerTemp || {}
+ const selectedUserId = authorisedMammographerTemp.userId
+ const otherName = (authorisedMammographerTemp.otherName || '')
+ .toString()
+ .trim()
+
+ const errors = []
+ if (!selectedUserId) {
+ errors.push({
+ text: 'Select who is taking the images',
+ name: 'appointment[authorisedMammographerTemp][userId]',
+ href: '#authorisedMammographerUserId'
+ })
+ } else if (selectedUserId === 'other' && !otherName) {
+ errors.push({
+ text: "Enter the mammographer's full name",
+ name: 'appointment[authorisedMammographerTemp][otherName]',
+ href: '#authorisedMammographerOtherName'
+ })
+ }
+
+ if (errors.length) {
+ errors.forEach((error) => req.flash('error', error))
+ // Back to the modal page - _modal is threaded by the modal middleware,
+ // the referrer chain has to be carried by hand
+ return res.redirect(
+ urlWithReferrer(
+ `${appointmentUrl}/change-authorised-mammographer`,
+ req.query.referrerChain
+ )
+ )
+ }
+
+ const previousAuthorisedMammographerId =
+ data.appointment?.authorisedMammographerId
+ const previousAuthorisedMammographerOtherName =
+ data.appointment?.authorisedMammographerOtherName
+
+ if (selectedUserId === 'other') {
+ data.appointment.authorisedMammographerId = null
+ data.appointment.authorisedMammographerOtherName = otherName
+ } else {
+ // Nominating yourself is the same as no nomination at all
+ data.appointment.authorisedMammographerId =
+ selectedUserId === data.currentUser?.id ? null : selectedUserId
+ data.appointment.authorisedMammographerOtherName = null
+ }
+
+ // Clear the transient modal fields so they don't leak into other forms
+ delete data.appointment.authorisedMammographerTemp
+
+ const hasChanged =
+ data.appointment.authorisedMammographerId !==
+ (previousAuthorisedMammographerId || null) ||
+ data.appointment.authorisedMammographerOtherName !==
+ (previousAuthorisedMammographerOtherName || null)
+
+ if (hasChanged) {
+ const selectedUser = (data.users || []).find(
+ (user) => user.id === selectedUserId
+ )
+ const newAuthorisedMammographerName = selectedUser
+ ? `${selectedUser.lastName.toUpperCase()}, ${selectedUser.firstName}`
+ : otherName
+ req.flash(
+ 'success',
+ `Authorised mammographer updated to ${newAuthorisedMammographerName}`
+ )
+ }
+
+ const returnUrl = getReturnUrl(
+ `${appointmentUrl}/check-information`,
+ req.query.referrerChain
+ )
+ res.redirect(modalBreakout(returnUrl))
+ }
+ )
+
// Appointment within clinic context
router.get('/clinics/:clinicId/appointments/:appointmentId', (req, res) => {
const { clinicId, appointmentId } = req.params
diff --git a/app/routes/clinics.js b/app/routes/clinics.js
index 380519127..4751d62f2 100644
--- a/app/routes/clinics.js
+++ b/app/routes/clinics.js
@@ -482,6 +482,9 @@ module.exports = (router) => {
}
const clinicData = getClinicData(req.session.data, req.params.id)
+ if (!clinicData) {
+ return res.redirect('/clinics')
+ }
let remainingCount = filterAppointmentsByStatus(
clinicData.appointments,
'remaining'
diff --git a/app/views/_includes/authorised-mammographer.njk b/app/views/_includes/authorised-mammographer.njk
new file mode 100644
index 000000000..6e2d4b918
--- /dev/null
+++ b/app/views/_includes/authorised-mammographer.njk
@@ -0,0 +1,56 @@
+{# app/views/_includes/authorised-mammographer.njk #}
+
+{#
+ The authorised mammographer for an in-progress appointment, alongside the
+ button that continues the workflow.
+
+ Where the current user cannot image this participant themselves and nobody
+ else has been nominated, the workflow is blocked until an authorised
+ mammographer with implant imaging training is chosen.
+
+ Set continueButtonText before including.
+#}
+
+{% set authorisedMammographerChangeHref = (appointmentUrl + "/change-authorised-mammographer") | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) %}
+
+{% set needsNominatedAuthorisedMammographer = appointment.workflowStatus['review-medical-information'] != 'completed'
+ and not (currentUser | canUserScreenAppointment(appointment))
+ and not (appointment | hasNominatedAuthorisedMammographer) %}
+
+{% if needsNominatedAuthorisedMammographer %}
+
+ {% set insetTextHtml %}
+
Due to {{ appointment | getImplantImagingReason }} you do not have the relevant permissions to take all the mammograms required for this appointment. Change the mammographer to someone authorised to take these images or {{ appLink({ text: "exit the appointment", href: (appointmentUrl + "/exit-appointment") | urlWithReferrer(referrerChain) }) | trim | safe }}.
+ {% endset %}
+
+ {{ insetText({
+ html: insetTextHtml
+ }) }}
+
+
+
+
+ {{ button({
+ text: "Change mammographer",
+ href: authorisedMammographerChangeHref,
+ classes: "nhsuk-u-margin-bottom-0"
+ } | openInModal) }}
+
+
+
+
+{% else %}
+
+ {% set authorisedMammographerName = (appointment | getAuthorisedMammographerName({ format: "reversed" })) or (currentUser.id | getUsername({ format: "reversed" })) %}
+
+
+
+ {{ button({
+ text: continueButtonText,
+ classes: "nhsuk-u-margin-bottom-0 js-complete-all-sections"
+ }) }}
+
Authorised mammographer: {{ authorisedMammographerName }} ({{ appLink({ text: "change", href: authorisedMammographerChangeHref } | openInModal) | trim | safe }})
+
+
+
+{% endif %}
diff --git a/app/views/_includes/summary-lists/medical-information/mammogram-image-data.njk b/app/views/_includes/summary-lists/medical-information/mammogram-image-data.njk
index 0364ae4ca..15afd0cba 100644
--- a/app/views/_includes/summary-lists/medical-information/mammogram-image-data.njk
+++ b/app/views/_includes/summary-lists/medical-information/mammogram-image-data.njk
@@ -137,7 +137,7 @@
}) %}
{% endif %}
-{# Machine room #}
+{# Mammogram location #}
{% if appointment.mammogramData.machineRoom %}
{# Check if this is a mobile clinic #}
{% set isMobileClinic = (clinic.location.type == 'mobile_unit') %}
@@ -148,14 +148,14 @@
{
href: "./images-room-selection" | urlWithReferrer(referrerChain, scrollTo),
text: "Change",
- visuallyHiddenText: "machine room"
+ visuallyHiddenText: "mammogram location"
}
] %}
{% endif %}
{% set summaryRows = summaryRows | push({
key: {
- text: "Machine room"
+ text: "Mammogram location"
},
value: {
text: appointment.mammogramData.machineRoom
@@ -166,6 +166,42 @@
}) %}
{% endif %}
+{# Authorised mammographer - whoever was nominated to take the images, falling
+ back to whoever ran the appointment #}
+{% set authorisedMammographerName = appointment | getAuthorisedMammographerName({ format: "reversed" }) %}
+{% if not authorisedMammographerName %}
+ {% if appointment.sessionDetails.endedBy %}
+ {% set authorisedMammographerName = appointment.sessionDetails.endedBy | getUsername({ format: "reversed" }) %}
+ {% else %}
+ {% set authorisedMammographerName = appointment.sessionDetails.startedBy | getUsername({ format: "reversed" }) %}
+ {% endif %}
+{% endif %}
+
+{% if authorisedMammographerName %}
+ {% set authorisedMammographerActions = [] %}
+ {% if allowEdits and authorisedMammographerChangeHref %}
+ {% set authorisedMammographerActions = [
+ ({
+ href: authorisedMammographerChangeHref,
+ text: "Change",
+ visuallyHiddenText: "authorised mammographer"
+ } | openInModal)
+ ] %}
+ {% endif %}
+
+ {% set summaryRows = summaryRows | push({
+ key: {
+ text: "Authorised mammographer"
+ },
+ value: {
+ text: authorisedMammographerName
+ },
+ actions: {
+ items: authorisedMammographerActions
+ }
+ }) %}
+{% endif %}
+
{# Combined views taken (showing all view information in one row) #}
{# Sort views by side and standard order before displaying #}
{% set sortedViews = [] %}
diff --git a/app/views/appointments/appointment.html b/app/views/appointments/appointment.html
index 04c54ae7a..29a39c0df 100644
--- a/app/views/appointments/appointment.html
+++ b/app/views/appointments/appointment.html
@@ -229,11 +229,11 @@
},
{
key: {
- text: "Screened by"
+ text: "Run by"
},
value: {
- html: (appointment.sessionDetails.endedBy | getUsername) +
- ((" and " + (appointment.sessionDetails.authors | last).userId | getUsername)
+ html: (appointment.sessionDetails.endedBy | getUsername({ format: "reversed" })) +
+ ((" and " + ((appointment.sessionDetails.authors | last).userId | getUsername({ format: "reversed" })))
if appointment.sessionDetails.authors and appointment.sessionDetails.authors.length > 0
and (appointment.sessionDetails.authors | last).userId != appointment.sessionDetails.endedBy
else "")
@@ -242,6 +242,17 @@
items: []
}
} if appointment.status == "complete" and appointment.sessionDetails.endedBy else {},
+ {
+ key: {
+ text: "Authorised mammographer"
+ },
+ value: {
+ html: (appointment | getAuthorisedMammographerName({ format: "reversed" })) or (appointment.sessionDetails.endedBy | getUsername({ format: "reversed" })) or (appointment.sessionDetails.startedBy | getUsername({ format: "reversed" }))
+ },
+ actions: {
+ items: []
+ }
+ } if appointment.status == "complete" and appointment.sessionDetails.endedBy else {},
{
key: {
text: "Special appointment"
diff --git a/app/views/appointments/change-authorised-mammographer.html b/app/views/appointments/change-authorised-mammographer.html
new file mode 100644
index 000000000..b3c8b03f0
--- /dev/null
+++ b/app/views/appointments/change-authorised-mammographer.html
@@ -0,0 +1,100 @@
+{# app/views/appointments/change-authorised-mammographer.html #}
+
+{% extends parentLayout or '_templates/layout-appointment.html' %}
+
+{# Wording differs depending on whether images have already been taken #}
+{% set imagesTaken = appointment.workflowStatus['take-images'] == 'completed' %}
+
+{% set pageHeading = "Change mammographer" %}
+{% set formAction = "./change-authorised-mammographer-answer" | urlWithReferrer(referrerChain) %}
+
+{% set legendText = "Who took the images for this appointment?" if imagesTaken else "Who is taking images for this appointment?" %}
+
+{# Where Save and Cancel return to - the page the modal was opened from #}
+{% set returnPageHref = (appointmentUrl + "/check-information") | getReturnUrl(referrerChain) %}
+
+{% block pageContent %}
+
+ {{ pageHeading }}
+
+ {% if not imagesTaken %}
+ {{ insetText({
+ text: "Ensure the person selected is also logged into the mammogram machine before images are taken"
+ }) }}
+ {% endif %}
+
+ {# Current user sits at the top, followed by every other clinician able to
+ screen this appointment - only those with implant imaging training when
+ the participant has implants or a device #}
+ {% set authorisedMammographerItems = [
+ {
+ value: data.currentUser.id,
+ text: data.currentUser.id | getUsername({ format: "reversed", identifyCurrentUser: true })
+ }
+ ] %}
+
+ {% for user in data.users | sort(false, false, "lastName") %}
+ {% if user.id != data.currentUser.id and (user | canUserScreenAppointment(appointment)) %}
+ {% set authorisedMammographerItems = authorisedMammographerItems | push({
+ value: user.id,
+ text: user.id | getUsername({ format: "reversed" })
+ }) %}
+ {% endif %}
+ {% endfor %}
+
+ {% set otherNameHtml %}
+ {{ input({
+ id: "authorisedMammographerOtherName",
+ name: "appointment[authorisedMammographerTemp][otherName]",
+ value: appointment.authorisedMammographerTemp.otherName or appointment.authorisedMammographerOtherName,
+ label: {
+ text: "Enter the mammographer's full name"
+ },
+ autocomplete: "off"
+ } | populateErrors) }}
+ {% endset %}
+
+ {% set authorisedMammographerItems = authorisedMammographerItems | push({ divider: "or" }) %}
+ {% set authorisedMammographerItems = authorisedMammographerItems | push({
+ value: "other",
+ text: "Other",
+ conditional: {
+ html: otherNameHtml
+ }
+ }) %}
+
+ {# Nominating yourself is stored as no nomination, so default to the current user #}
+ {% set selectedAuthorisedMammographer = data.currentUser.id %}
+ {% if appointment.authorisedMammographerOtherName %}
+ {% set selectedAuthorisedMammographer = "other" %}
+ {% endif %}
+ {% if appointment.authorisedMammographerId %}
+ {% set selectedAuthorisedMammographer = appointment.authorisedMammographerId %}
+ {% endif %}
+ {% if appointment.authorisedMammographerTemp.userId %}
+ {% set selectedAuthorisedMammographer = appointment.authorisedMammographerTemp.userId %}
+ {% endif %}
+
+ {{ radios({
+ idPrefix: "authorisedMammographerUserId",
+ name: "appointment[authorisedMammographerTemp][userId]",
+ value: selectedAuthorisedMammographer,
+ fieldset: {
+ legend: {
+ text: legendText
+ }
+ },
+ items: authorisedMammographerItems
+ } | populateErrors) }}
+
+
+ {{ button({
+ text: "Save"
+ }) }}
+ {{ appLink({ text: "Cancel", href: returnPageHref, attributes: { "data-modal-action": "close" } }) | trim | safe }}
+ {% if not imagesTaken %}
+ {{ appLink({ text: "Exit appointment", href: (appointmentUrl + "/exit-appointment") | urlWithReferrer(referrerChain) }) | trim | safe }}
+ {% endif %}
+
+
+{% endblock %}
diff --git a/app/views/appointments/check-information.html b/app/views/appointments/check-information.html
index b7462ff7a..9cc3eccc8 100644
--- a/app/views/appointments/check-information.html
+++ b/app/views/appointments/check-information.html
@@ -22,6 +22,7 @@
{% set activeTab = 'review' %}
{% set showReviewAfterImagingReminder = appointment.workflowStatus['review-breast-features-after-imaging'] == 'yes' %}
+{% set authorisedMammographerChangeHref = (appointmentUrl + "/change-authorised-mammographer") | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) %}
diff --git a/app/views/appointments/images-automatic.html b/app/views/appointments/images-automatic.html
index 7f17a9522..34f1a4db9 100644
--- a/app/views/appointments/images-automatic.html
+++ b/app/views/appointments/images-automatic.html
@@ -50,6 +50,15 @@
{{ pageHeading }}
+ {# The authorised mammographer defaults to the logged in user, unless an alternative has been nominated #}
+ {% set authorisedMammographerName = (appointment | getAuthorisedMammographerName({ format: "reversed" })) or (currentUser.id | getUsername({ format: "reversed" })) %}
+ {% set authorisedMammographerChangeHref = (appointmentUrl + "/change-authorised-mammographer") | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) %}
+
+
+ Authorised mammographer: {{ authorisedMammographerName }}
+ ({{ appLink({ text: "change", href: authorisedMammographerChangeHref } | openInModal) | trim | safe }})
+
+
Observations during mammogram
diff --git a/app/views/appointments/images-manual.html b/app/views/appointments/images-manual.html
index 742612618..37dd76ea0 100644
--- a/app/views/appointments/images-manual.html
+++ b/app/views/appointments/images-manual.html
@@ -18,6 +18,9 @@
{{ pageHeading }}
{% set mammogramSource = appointment.mammogramDataTemp or appointment.mammogramData %}
+ {# The authorised mammographer defaults to the logged in user, unless an alternative has been nominated #}
+ {% set authorisedMammographerName = (appointment | getAuthorisedMammographerName({ format: "reversed" })) or (currentUser.id | getUsername({ format: "reversed" })) %}
+
{# Determine current room name #}
{% set currentRoomName = "" %}
{% set isMobileClinic = (clinic.location.type == 'mobile_unit') %}
@@ -38,10 +41,15 @@
{{ pageHeading }}
Mammogram location: {{ currentRoomName }}.
{% if not isMobileClinic %}
-
Change room or machine details
+ (change)
{% endif %}
+
+ Authorised mammographer: {{ authorisedMammographerName }}
+ ({{ appLink({ text: "change", href: (appointmentUrl + "/change-authorised-mammographer") | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) } | openInModal) | trim | safe }})
+
+
{{ appHiddenInput({
name: "appointment[mammogramDataTemp][machineRoom]",
value: currentRoomName
@@ -148,7 +156,7 @@
Manually add participant details
name: "appointment[mammogramDataTemp][isStandardSet]",
fieldset: {
legend: {
- text: "Have you taken a standard set of images?",
+ text: "Were a standard set of images taken?",
size: "m",
isPageHeading: false
}
diff --git a/app/views/appointments/review-medical-information.html b/app/views/appointments/review-medical-information.html
index b61b10a50..e49d75f16 100644
--- a/app/views/appointments/review-medical-information.html
+++ b/app/views/appointments/review-medical-information.html
@@ -29,26 +29,20 @@
value: appointment.workflowStatus['review-breast-features-after-imaging']
}) }}
-
-
- {{ button({
- text: "Complete all and continue" if appointment.workflowStatus['review-medical-information'] != 'completed' else "Next section",
- classes: "nhsuk-u-margin-bottom-0 nhsuk-button js-complete-all-sections"
- }) }}
-
-
+ {% set continueButtonText = "Complete all and continue" if appointment.workflowStatus['review-medical-information'] != 'completed' else "Next section" %}
+ {% include "_includes/authorised-mammographer.njk" %}
{% include "_includes/medical-information/index.njk" %}
{% endblock %}
diff --git a/app/views/choose-user.html b/app/views/choose-user.html
index 81cee64f5..270b3eb67 100644
--- a/app/views/choose-user.html
+++ b/app/views/choose-user.html
@@ -32,6 +32,15 @@
{{ role | sentenceCase -}}
{% if not loop.last %}, {% endif %}
{% endfor %}
+ {% if user.permissions | length %}
+
+ Permissions:
+ {% for permission in user.permissions %}
+ {{ permission | kebabCase | formatWords("-") | sentenceCase -}}
+ {% if not loop.last %}, {% endif %}
+ {% endfor %}
+
+ {% endif %}