From 53c600e9026073c9233be086a9bdd91f8a657cc0 Mon Sep 17 00:00:00 2001 From: Mal Ross Date: Tue, 12 May 2026 17:04:29 +0100 Subject: [PATCH 1/3] Summarise earlier choices while finding a clinic slot Previously, we had either hard-coded text before the questions around location, date and time, but now it reflects your actual choices. The preferred location search now also gives responses that match your search term, albeit with ludicrous names like "Chelsea upon Tyne". But it at least now recognises when you've entered a postcode or outcode and skips the 'places that match' page accordingly. Check answers page still needs doing though. --- app/controllers/book-into-a-clinic.js | 16 +++++++++-- app/enums.js | 10 +++++++ app/locales/en.js | 22 ++++++++++----- app/utils/clinic-appointment.js | 20 +++++++++---- app/utils/geolocation.js | 28 +++++++++++++++++++ .../form/_session-summary.njk | 15 ++++++++++ .../form/appointment-time-range.njk | 17 ++--------- .../form/appointment-time.njk | 17 ++--------- .../book-into-a-clinic/form/clinic-date.njk | 5 ++-- .../form/clinic-location.njk | 3 +- .../form/preferred-location-matches.njk | 10 +++---- 11 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 app/utils/geolocation.js create mode 100644 app/views/book-into-a-clinic/form/_session-summary.njk diff --git a/app/controllers/book-into-a-clinic.js b/app/controllers/book-into-a-clinic.js index b8673bdd9..076dcf8d9 100644 --- a/app/controllers/book-into-a-clinic.js +++ b/app/controllers/book-into-a-clinic.js @@ -279,7 +279,7 @@ export const bookIntoClinicController = { }) response.locals.clinicLocationItems = clinicLocationItems } else if (view === 'clinic-date') { - const scheduledClinics = _.sortBy( + const scheduledClinicSessions = _.sortBy( Session.findAll(data).filter( (session) => session.type === SessionType.Clinic && @@ -290,7 +290,7 @@ export const bookIntoClinicController = { ) const clinicDateItems = [] - scheduledClinics.forEach((session) => { + scheduledClinicSessions.forEach((session) => { const midday = new Date(session.date) setMidday(midday) @@ -313,6 +313,10 @@ export const bookIntoClinicController = { }) }) response.locals.clinicDateItems = clinicDateItems + response.locals.clinicSummary = { + location: scheduledClinicSessions.at(0)?.formatted.location, + date: 'To be decided' + } } else if (view === 'appointment-time-range') { const session = Session.findOne(appointment.session_id, data) const availableTimesByHour = _.groupBy( @@ -338,6 +342,10 @@ export const bookIntoClinicController = { } }) response.locals.timeRangeItems = timeRangeItems + response.locals.clinicSummary = { + location: session.formatted.location, + date: session.formatted.date + } } else if (view === 'appointment-time') { const session = Session.findOne(appointment.session_id, data) const availableTimesByHour = _.groupBy( @@ -374,6 +382,10 @@ export const bookIntoClinicController = { } ) response.locals.appointmentTimeItems = appointmentTimeItems + response.locals.clinicSummary = { + location: session.formatted.location, + date: session.formatted.date + } } // All health questions use the same view diff --git a/app/enums.js b/app/enums.js index 5789f3f8c..9dcdc7277 100644 --- a/app/enums.js +++ b/app/enums.js @@ -819,3 +819,13 @@ export const VaccineSideEffect = { TemperatureShiver: 'a high temperature, or feeling hot and shivery', Unwell: 'generally feeling unwell' } + +/** + * @readonly + * @enum {string} + */ +export const LocationSearchType = { + Postcode: 'Postcode', + Outcode: 'Outcode', + Place: 'Place' +} diff --git a/app/locales/en.js b/app/locales/en.js index a9c77db8b..941c58212 100644 --- a/app/locales/en.js +++ b/app/locales/en.js @@ -427,7 +427,7 @@ export const en = { } }, preferredLocationMatches: { - title: 'We found 3 places that match “Newcastle”', + title: 'We found 3 places that match “%s”', hits: { label: 'Choose one of the following:' }, @@ -438,16 +438,19 @@ export const en = { hint: 'Select the same location and date as an earlier child, or find a different clinic.' }, clinicLocation: { - title: 'Choose a clinic location for %s', - hint: 'The following clinics are ordered by distance from NE12 7ET' + title: 'Choose a clinic location for %s’s appointment', + hint: 'The following clinics are ordered by distance from %s' }, clinicDate: { - title: 'Choose a clinic date for %s', - location: - 'Location: Killingworth Library, White Swan Centre, Killingworth, NE12 6SS', + title: 'Choose a clinic date for %s’s appointment', date: { label: 'Clinic date' }, + clinicSummary: { + location: { + label: 'Location' + } + }, hint: { morning: 'Morning available', afternoon: 'Afternoon available', @@ -457,7 +460,12 @@ export const en = { timeRange: { title: 'Choose a time range for %s’s appointment', clinicSummary: { - title: 'Clinic' + location: { + label: 'Location' + }, + date: { + label: 'Date' + } }, ranges: { label: 'Available time ranges' diff --git a/app/utils/clinic-appointment.js b/app/utils/clinic-appointment.js index dbcda99f4..f94fce258 100644 --- a/app/utils/clinic-appointment.js +++ b/app/utils/clinic-appointment.js @@ -1,8 +1,9 @@ import _ from 'lodash' -import { ReplyDecision } from '../enums.js' +import { LocationSearchType, ReplyDecision } from '../enums.js' import { ClinicAppointment, ClinicBooking, Session } from '../models.js' +import { getLocationSearchType } from './geolocation.js' import { camelToKebabCase } from './string.js' /** @@ -75,14 +76,23 @@ export const getAllAppointmentPaths = ( } : {}), [`/${booking_uuid}/new/${appointment_uuid}/preferred-location`]: { - [`/${booking_uuid}/new/${appointment_uuid}/clinic-location`]: { - data: 'transaction.preferredLocation', - value: 'NE12 7ET' + [`/${booking_uuid}/new/${appointment_uuid}/clinic-location`]: () => { + const searchTerm = sessionData.transaction.preferredLocation + const searchType = getLocationSearchType(searchTerm) + switch (searchType) { + case LocationSearchType.Postcode: + case LocationSearchType.Outcode: + sessionData.transaction.preferredPostcode = searchTerm + return true + case LocationSearchType.Place: + default: + return false + } } }, [`/${booking_uuid}/new/${appointment_uuid}/preferred-location-matches`]: { [`/${booking_uuid}/new/${appointment_uuid}/preferred-location`]: { - data: 'transaction.preferredLocation', + data: 'transaction.preferredPostcode', value: 'retry' } }, diff --git a/app/utils/geolocation.js b/app/utils/geolocation.js new file mode 100644 index 000000000..ce74cb958 --- /dev/null +++ b/app/utils/geolocation.js @@ -0,0 +1,28 @@ +import { LocationSearchType } from '../enums.js' + +/** + * Get the type of location represented by the location search term + * + * @param {string} searchTerm - the location that the user entered + * @returns {LocationSearchType|undefined} the type of value entered by the user + */ +export const getLocationSearchType = (searchTerm) => { + if (!searchTerm) { + return undefined + } + const cleanInput = searchTerm.trim().toUpperCase() + + // Regex for a full UK postcode (e.g., SW1A 1AA or NE12 7ET) + const fullPostcodeRegex = /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/ + if (fullPostcodeRegex.test(cleanInput)) { + return LocationSearchType.Postcode + } + + // Regex for a postcode Outcode (e.g., NE12, SW1A, B1) + const outcodeRegex = /^[A-Z]{1,2}\d[A-Z\d]?$/ + if (outcodeRegex.test(cleanInput)) { + return LocationSearchType.Outcode + } + + return LocationSearchType.Place +} diff --git a/app/views/book-into-a-clinic/form/_session-summary.njk b/app/views/book-into-a-clinic/form/_session-summary.njk new file mode 100644 index 000000000..b75671dd4 --- /dev/null +++ b/app/views/book-into-a-clinic/form/_session-summary.njk @@ -0,0 +1,15 @@ +{{ + summaryList({ + lastRowBorder: false, + rows: [ + { + key: { text: __('session.location.label') }, + value: { text: clinicSummary.location } + }, + { + key: { text: __('session.date.label') }, + value: { text: clinicSummary.date } + } + ] + }) +}} diff --git a/app/views/book-into-a-clinic/form/appointment-time-range.njk b/app/views/book-into-a-clinic/form/appointment-time-range.njk index 9943585ed..cf00fdd7a 100644 --- a/app/views/book-into-a-clinic/form/appointment-time-range.njk +++ b/app/views/book-into-a-clinic/form/appointment-time-range.njk @@ -8,24 +8,13 @@ caption: __("clinicBooking.appointment.caption", fullName) if childCount > 1 }) }} - {# - {{ summaryList({ - card: { - heading: __("clinicBooking.timeRange.clinicSummary.title"), - headingSize: "m" - }, - lastRowBorder: false, - rows: summaryRows(appointment, { - location: {}, - date: {} - }) - }) }} - #} + {% include "book-into-a-clinic/form/_session-summary.njk" %} {{ radios({ fieldset: { legend: { - text: __("clinicBooking.timeRange.ranges.label") + text: __("clinicBooking.timeRange.ranges.label"), + size: "m" } }, items: timeRangeItems, diff --git a/app/views/book-into-a-clinic/form/appointment-time.njk b/app/views/book-into-a-clinic/form/appointment-time.njk index 4476bf6fb..4d6f09f53 100644 --- a/app/views/book-into-a-clinic/form/appointment-time.njk +++ b/app/views/book-into-a-clinic/form/appointment-time.njk @@ -11,24 +11,13 @@ caption: __("clinicBooking.appointment.caption", fullName) if childCount > 1 }) }} - {# - {{ summaryList({ - card: { - heading: __("clinicBooking.time.clinicSummary.title"), - headingSize: "m" - }, - lastRowBorder: false, - rows: summaryRows(appointment, { - location: {}, - date: {} - }) - }) }} - #} + {% include "book-into-a-clinic/form/_session-summary.njk" %} {{ radios({ fieldset: { legend: { - text: __("clinicBooking.time.times.label") + text: __("clinicBooking.time.times.label"), + size: "m" } }, items: appointmentTimeItems, diff --git a/app/views/book-into-a-clinic/form/clinic-date.njk b/app/views/book-into-a-clinic/form/clinic-date.njk index 477e6350c..d19e814b8 100644 --- a/app/views/book-into-a-clinic/form/clinic-date.njk +++ b/app/views/book-into-a-clinic/form/clinic-date.njk @@ -8,12 +8,13 @@ caption: __("clinicBooking.appointment.caption", fullName) if childCount > 1 }) }} - {{ __("clinicBooking.clinicDate.location") | nhsukMarkdown }} + {% include "book-into-a-clinic/form/_session-summary.njk" %} {{ radios({ fieldset: { legend: { - text: __("clinicBooking.clinicDate.date.label") + text: __("clinicBooking.clinicDate.date.label"), + size: "m" } }, items: clinicDateItems, diff --git a/app/views/book-into-a-clinic/form/clinic-location.njk b/app/views/book-into-a-clinic/form/clinic-location.njk index 9430c6d7d..e86191933 100644 --- a/app/views/book-into-a-clinic/form/clinic-location.njk +++ b/app/views/book-into-a-clinic/form/clinic-location.njk @@ -1,6 +1,7 @@ {% extends "_layouts/form.njk" %} {% set title = __("clinicBooking.clinicLocation.title", firstName) %} +{% set postcode = transaction.preferredPostcode if transaction.preferredPostcode.length else "CV32" %} {% block form %} {{ radios({ @@ -12,7 +13,7 @@ }) } }, - hint: { text: __('clinicBooking.clinicLocation.hint') }, + hint: { text: __('clinicBooking.clinicLocation.hint', transaction.preferredPostcode) }, items: clinicLocationItems, decorate: "transaction.clinic_id" }) }} diff --git a/app/views/book-into-a-clinic/form/preferred-location-matches.njk b/app/views/book-into-a-clinic/form/preferred-location-matches.njk index 94ebd88a5..3bcb6619b 100644 --- a/app/views/book-into-a-clinic/form/preferred-location-matches.njk +++ b/app/views/book-into-a-clinic/form/preferred-location-matches.njk @@ -1,6 +1,6 @@ {% extends "_layouts/form.njk" %} -{% set title = __("clinicBooking.preferredLocationMatches.title") %} +{% set title = __("clinicBooking.preferredLocationMatches.title", transaction.preferredLocation) %} {% block form %} {{ appHeading({ @@ -13,15 +13,15 @@ legend: { text: __("clinicBooking.preferredLocationMatches.hits.label") } }, items: [{ - text: 'Newcastle upon Tyne, NE1', + text: transaction.preferredLocation + ' upon Tyne, NE1', value: 'NE1' }, { - text: 'Newcastle-under-Lyme, ST5', + text: transaction.preferredLocation + '-under-Lyme, ST5', value: 'ST5' }, { - text: 'Newcastle, SY7', + text: transaction.preferredLocation + ', SY7', value: 'SY7' }, { @@ -31,6 +31,6 @@ text: __('clinicBooking.preferredLocationMatches.tryAgain'), value: 'retry' }], - decorate: "transaction.preferredLocation" + decorate: "transaction.preferredPostcode" }) }} {% endblock %} From 8590751e1e0b4369e56bfb242663eda2750c8d83 Mon Sep 17 00:00:00 2001 From: Mal Ross Date: Tue, 12 May 2026 17:04:54 +0100 Subject: [PATCH 2/3] Simplify question text for flu vaccine choice --- app/locales/en.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locales/en.js b/app/locales/en.js index 941c58212..d18e75d5c 100644 --- a/app/locales/en.js +++ b/app/locales/en.js @@ -381,7 +381,7 @@ export const en = { hint: 'Each vaccine is given separately' }, fluChoice: { - title: 'Which of the flu vaccines do you agree to %s having?', + title: 'Which flu vaccine do you agree to %s having?', nasal: { label: 'I agree to the nasal spray vaccine', hint: 'This is the recommended option and gives the best protection against flu' From a251f10089ad4773bf03274e8f1d1b4bca3ce5d2 Mon Sep 17 00:00:00 2001 From: Mal Ross Date: Tue, 12 May 2026 17:28:29 +0100 Subject: [PATCH 3/3] Fix the reporting of preferred postcode for clinic --- app/views/book-into-a-clinic/form/clinic-location.njk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/book-into-a-clinic/form/clinic-location.njk b/app/views/book-into-a-clinic/form/clinic-location.njk index e86191933..26bdf076b 100644 --- a/app/views/book-into-a-clinic/form/clinic-location.njk +++ b/app/views/book-into-a-clinic/form/clinic-location.njk @@ -1,7 +1,7 @@ {% extends "_layouts/form.njk" %} {% set title = __("clinicBooking.clinicLocation.title", firstName) %} -{% set postcode = transaction.preferredPostcode if transaction.preferredPostcode.length else "CV32" %} +{% set postcode = data.transaction.preferredPostcode if data.transaction.preferredPostcode.length else "CV32" %} {% block form %} {{ radios({ @@ -13,7 +13,7 @@ }) } }, - hint: { text: __('clinicBooking.clinicLocation.hint', transaction.preferredPostcode) }, + hint: { text: __('clinicBooking.clinicLocation.hint', postcode) }, items: clinicLocationItems, decorate: "transaction.clinic_id" }) }}