diff --git a/app/controllers/book-into-a-clinic.js b/app/controllers/book-into-a-clinic.js index 346c0f551..52fd894e5 100644 --- a/app/controllers/book-into-a-clinic.js +++ b/app/controllers/book-into-a-clinic.js @@ -94,7 +94,9 @@ export const bookIntoClinicController = { * * The nature of the journey here is complex, as there are two separate sections in which we need to * iterate over children. Or over appointments, if you want to think of it that way (each child has - * their own appointment). + * their own appointment). And the second iteration - the health questions - has pages that are + * dependent on the answers given during the appointment booking (specifically, the choice of vaccines + * per child). * * So, it goes: * - Start page @@ -104,17 +106,13 @@ export const bookIntoClinicController = { * - ... * - Appointment time <-- final page of the per-child appointment journey; iterate to next child if required * - Parent info + * - Check answers * - Health questions? * - Health question 1 <-- first page of the per-child health question journey * - ... * - Health question n <-- final page of the per-child health question journey; iterate to next child if required - * - Check answers * - Confirmation * - * So, at the point where we start the health questions journey, we need to set up the iteration again, overriding - * the default paths.next given to us by the wizard() function so that we can re-inject the appointment_uuid into - * the path (the "Health questions?" page won't have that parameter). - * * */ // Create objects on the global context to allow us to check branching conditions, etc. @@ -136,8 +134,7 @@ export const bookIntoClinicController = { response.locals.childNumber = booking.appointments_ids.indexOf(appointment.uuid) + 1 response.locals.childCount = booking.appointments_ids.length - response.locals.firstName = - appointment.unmatchedFirstName || 'your child' + response.locals.firstName = appointment.firstName || 'your child' response.locals.fullName = appointment.fullName || 'your child' } } @@ -154,7 +151,7 @@ export const bookIntoClinicController = { [`/${session_preset_slug}/${booking_uuid}/new/child-count`]: {}, // Appointment journey; once per child - ...getAllAppointmentPaths(booking), + ...getAllAppointmentPaths(request.session.data, booking), // Parent journey [`/${session_preset_slug}/${booking_uuid}/new/parent`]: { @@ -163,9 +160,12 @@ export const bookIntoClinicController = { }, [`/${session_preset_slug}/${booking_uuid}/new/contact-preference`]: {}, + // Check answers + [`/${session_preset_slug}/${booking_uuid}/new/check-answers`]: {}, + // Health questions (optional) [`/${session_preset_slug}/${booking_uuid}/new/offer-health-questions`]: { - [`/${session_preset_slug}/${booking_uuid}/new/check-answers`]: { + [`/${session_preset_slug}/${booking_uuid}/new/confirmation`]: { data: 'transaction.optedIntoHealthQuestions', value: 'false' } @@ -180,9 +180,6 @@ export const bookIntoClinicController = { data ), - // Check answers - [`/${session_preset_slug}/${booking_uuid}/new/check-answers`]: {}, - // Confirmation! \o/ [`/${session_preset_slug}/${booking_uuid}/new/confirmation`]: {} } @@ -212,7 +209,7 @@ export const bookIntoClinicController = { */ showForm(request, response) { const { appointment } = response.locals - let { view } = request.params + let { booking_uuid, view } = request.params // All health questions use the same view let key @@ -227,6 +224,50 @@ export const bookIntoClinicController = { request.session.data )[key]?.conditional + // Build the options for the selection of a home address address from those already entered + if (view === 'address-selection') { + const booking = ClinicBooking.findOne( + booking_uuid, + request.session.data.wizard + ) + const previousAddressItems = booking.appointments + .map((appointment) => { + if (appointment.child?.address) { + const oneLineAddress = Object.values(appointment.child.address) + .filter((string) => string) + .join(', ') + return { + text: oneLineAddress, + value: appointment.uuid + } + } + + return null + }) + .filter(Boolean) + + response.locals.previousAddressItems = [ + ...previousAddressItems, + { + divider: 'or' + }, + { + text: 'Enter a different address', + value: 'new' + } + ] + } + + ///////////////////// + // console.log(`view: ${view}`) + // console.log( + // `data.wizard: ${JSON.stringify(request.session.data.wizard, null, 2)}` + // ) + // console.log( + // `data.appointment: ${JSON.stringify(request.session.data.appointment, null, 2)}` + // ) + ///////////////////// + response.render(`book-into-a-clinic/form/${view}`, { key, hasSubQuestions }) }, @@ -237,7 +278,7 @@ export const bookIntoClinicController = { * @param {*} response */ updateForm(request, response) { - const { booking_uuid, appointment_uuid } = request.params + const { booking_uuid, appointment_uuid, view } = request.params const { data } = request.session const { paths } = response.locals @@ -257,8 +298,10 @@ export const bookIntoClinicController = { _.merge(data.wizard.transaction, request.body.transaction) } - // If we've just set the child count, create the appointments we'll need - if (request.originalUrl.endsWith('/new/child-count')) { + let nextUrl = paths.next + + if (view === 'child-count') { + // We've just set the child count, so create the appointments we'll need const booking = ClinicBooking.findOne(booking_uuid, data.wizard) let desiredCount = Number(data.wizard.transaction.childCount) @@ -280,19 +323,35 @@ export const bookIntoClinicController = { ClinicAppointment.delete(appointment_uuid, data.wizard) } - // NB: request.session.save was needed to avoid race condition issues on heroku - // Flush to session store and start the appointment journey for the first child + // Start the appointment journey for the first child const firstAppointment = booking.appointments[0] const firstAppointmentUrl = `${request.baseUrl}/${booking.bookingUri}/new/${firstAppointment.appointmentUri}/child` - request.session.save((err) => { - if (!err) response.redirect(firstAppointmentUrl) - }) - } else { - // Flush to session store and continue to the next page in the journey - request.session.save((err) => { - if (!err) response.redirect(paths.next) - }) + nextUrl = firstAppointmentUrl + } else if ( + view === 'address-selection' && + request.body.transaction.previousAddress !== 'new' + ) { + // We've just selected a previous child's address for the current appointment, so copy + // that detail to the child record + const previous_appointment_uuid = request.body.transaction.previousAddress + const previousAppointment = ClinicAppointment.findOne( + previous_appointment_uuid, + data.wizard + ) + const currentAppointment = ClinicAppointment.findOne( + appointment_uuid, + data.wizard + ) + + if (previousAppointment && currentAppointment) { + currentAppointment.child.address = previousAppointment.child.address + } } + + // NB: request.session.save was needed to avoid race condition issues on heroku + request.session.save((err) => { + if (!err) response.redirect(nextUrl) + }) }, /** diff --git a/app/generators/clinic-appointment.js b/app/generators/clinic-appointment.js index cbcf141d5..839222f3d 100644 --- a/app/generators/clinic-appointment.js +++ b/app/generators/clinic-appointment.js @@ -1,6 +1,5 @@ import { fakerEN_GB as faker } from '@faker-js/faker' import { addMinutes } from 'date-fns' -import _ from 'lodash' import { ParentalRelationship, SessionType } from '../enums.js' import { ClinicAppointment } from '../models.js' @@ -11,22 +10,24 @@ const clinicSlotLength = Number(process.env.CLINIC_SLOT_LENGTH) || 10 /** * Generate fake clinic appointment * - * @param {ClinicBooking} booking - The booking this appointment will belong to + * @param {import('../models/clinic-booking.js').ClinicBooking} booking - The booking this appointment will belong to * @param {object} context - The other data already defined (sessions, children, etc.) * @returns {ClinicAppointment} A new, fake clinic appointment */ export function generateClinicAppointment(booking, context) { const uuid = faker.string.uuid() - // Choose a clinic session to book this appointment into + // Find clinic sessions for this programme const clinicSessions = Object.values(context.sessions).filter( - (s) => - s.type === SessionType.Clinic && - s.presetNames.includes(booking.sessionPreset.name) + (session) => + session.type === SessionType.Clinic && + session.presetNames.includes(booking.sessionPreset.name) ) - if (!clinicSessions.some(Boolean)) { + if (!clinicSessions.length) { return null } + + // Choose a clinic session to book this appointment into const clinicSession = faker.helpers.arrayElement(clinicSessions) if (!clinicSession) { return null @@ -34,24 +35,28 @@ export function generateClinicAppointment(booking, context) { const session_id = clinicSession.id // Work out the expected age range for children attending this session - const yearGroups = _.uniq( - clinicSession.programmes.flatMap((p) => p.yearGroups || []) - ) - const ageRanges = yearGroups.map((yg) => ({ min: yg + 4, max: yg + 5 })) - const allAgeLimits = ageRanges.flatMap((ar) => [ar.min, ar.max]) - const minAge = Math.min(allAgeLimits) || 4 - const maxAge = Math.max(allAgeLimits) || 15 + const yearGroups = clinicSession.programmes.flatMap((programme) => [ + ...new Set(programme.yearGroups) + ]) + const minAge = yearGroups.length ? Math.min(...yearGroups) + 4 : 4 + const maxAge = yearGroups.length ? Math.max(...yearGroups) + 5 : 15 - // Find/create a child of an appropriate age for the chosen clinic and its programmme + // Find/create a child of an appropriate age for the chosen clinic and its programme let matchedPatient if (faker.datatype.boolean(0.9)) { - const eligiblePatients = Object.values(context.patients).filter((p) => { - const age = getAge(p.dob) - return age >= minAge && age <= maxAge - }) + const eligiblePatients = Object.values(context.patients).filter( + (patient) => { + const age = getAge(patient.dob) + return age >= minAge && age <= maxAge + } + ) + if (!eligiblePatients.length) { + return null + } matchedPatient = faker.helpers.arrayElement(eligiblePatients) } const patient_uuid = matchedPatient?.uuid + // Unmatched child details, if required const unmatchedFirstName = matchedPatient ? undefined diff --git a/app/locales/en.js b/app/locales/en.js index 5d6b68730..04f55a218 100644 --- a/app/locales/en.js +++ b/app/locales/en.js @@ -312,6 +312,10 @@ export const en = { title: 'What is %s’s home address?', hint: 'Give the child’s primary address. We use this to confirm their identity.' }, + addressSelection: { + title: 'What is %s’s home address?', + hint: 'Select the child’s primary address. We use this to confirm their identity.' + }, parentalRelationship: { title: 'What is your relationship to %s?', hasParentalResponsibility: { diff --git a/app/models/clinic-appointment.js b/app/models/clinic-appointment.js index 90f5e666d..f2d546d4b 100644 --- a/app/models/clinic-appointment.js +++ b/app/models/clinic-appointment.js @@ -2,18 +2,14 @@ import { fakerEN_GB as faker } from '@faker-js/faker' import _ from 'lodash' import { + Child, ClinicBooking, Parent, Patient, Programme, Session } from '../models.js' -import { - convertIsoDateToObject, - convertObjectToIsoDate, - formatDate, - getDateValueDifference -} from '../utils/date.js' +import { formatDate, getDateValueDifference } from '../utils/date.js' import { stringToArray, stringToBoolean } from '../utils/string.js' /** @@ -24,10 +20,7 @@ import { stringToArray, stringToBoolean } from '../utils/string.js' * @property {string} uuid - Unique ID for this clinic appointment * @property {string} booking_uuid - Unique ID for the booking in which this appointment was made * @property {string} [patient_uuid] - Patient UUID (if matched to a patient record) - * @property {string} [unmatchedFirstName] - Child first name, if not matched to a patient record - * @property {string} [unmatchedLastName] - Child last name, if not matched to a patient record - * @property {Date} [unmatchedDob] - Child date of birth, if not matched to a patient record - * @property {object} [unmatchedDob_] - Child date of birth, if not matched to a patient record (for use with decorate) + * @property {import('./child.js').Child} [child] - child details recorded from form input values * @property {Boolean} needsExtraTime - Does the child need extra time for their vaccinations? * @property {string} [extraTimeReason] - The reason why the child needs extra time for their appointment * @property {ParentalRelationship} [parentalRelationship] - The relationship of the person booking the appointment to the child @@ -47,10 +40,7 @@ export class ClinicAppointment { this.booking_uuid = options?.booking_uuid this.patient_uuid = options?.patient_uuid - this.unmatchedFirstName = options?.unmatchedFirstName - this.unmatchedLastName = options?.unmatchedLastName - this.unmatchedDob = options?.unmatchedDob && new Date(options.unmatchedDob) - this.unmatchedDob_ = options?.unmatchedDob_ + this.child = (options?.child && new Child(options.child)) || new Child({}) this.needsExtraTime = stringToBoolean(options?.needsExtraTime) this.extraTimeReason = options?.extraTimeReason @@ -155,7 +145,7 @@ export class ClinicAppointment { * @returns {string} Child's first name */ get firstName() { - return this.patient ? this.patient.firstName : this.unmatchedFirstName + return this.patient ? this.patient.firstName : this.child.firstName } /** @@ -164,7 +154,7 @@ export class ClinicAppointment { * @returns {string} Child's last name */ get lastName() { - return this.patient ? this.patient.lastName : this.unmatchedLastName + return this.patient ? this.patient.lastName : this.child.lastName } /** @@ -176,26 +166,6 @@ export class ClinicAppointment { return `${this.firstName} ${this.lastName}` } - /** - * Get date of birth for `dateInput` - * - * @returns {object|undefined} `dateInput` object - */ - get unmatchedDob_() { - return convertIsoDateToObject(this.unmatchedDob) - } - - /** - * Set date of birth from `dateInput` - * - * @param {object} object - dateInput object - */ - set unmatchedDob_(object) { - if (object) { - this.unmatchedDob = convertObjectToIsoDate(object) - } - } - /** * Get the programmes selected for this appointment * diff --git a/app/utils/clinic-appointment.js b/app/utils/clinic-appointment.js index ab78e0326..5a1f360d9 100644 --- a/app/utils/clinic-appointment.js +++ b/app/utils/clinic-appointment.js @@ -5,10 +5,11 @@ import { camelToKebabCase } from './string.js' /** * Get wizard journey paths and forking details for all appointments in the given clinic booking * + * @param {object} sessionData - the request.session.data object * @param {ClinicBooking} booking - the clinic booking whose appointment journeys we're mapping * @returns {object} An object containing all relevants page and forks */ -export const getAllAppointmentPaths = (booking) => { +export const getAllAppointmentPaths = (sessionData, booking) => { const booking_uuid = booking.uuid const session_preset_slug = booking.sessionPreset.slug @@ -19,6 +20,15 @@ export const getAllAppointmentPaths = (booking) => { {}, [`/${session_preset_slug}/${booking_uuid}/new/${appointment_uuid}/dob`]: {}, + ...(booking.appointments_ids[0] !== appointment_uuid + ? { + [`/${session_preset_slug}/${booking_uuid}/new/${appointment_uuid}/address-selection`]: + { + [`/${session_preset_slug}/${booking_uuid}/new/${appointment_uuid}/parental-relationship`]: + () => sessionData.transaction.previousAddress !== 'new' + } + } + : {}), [`/${session_preset_slug}/${booking_uuid}/new/${appointment_uuid}/address`]: {}, [`/${session_preset_slug}/${booking_uuid}/new/${appointment_uuid}/parental-relationship`]: diff --git a/app/views/book-into-a-clinic/form/address-selection.njk b/app/views/book-into-a-clinic/form/address-selection.njk new file mode 100644 index 000000000..e20d53f83 --- /dev/null +++ b/app/views/book-into-a-clinic/form/address-selection.njk @@ -0,0 +1,19 @@ +{% extends "_layouts/form.njk" %} + +{% set title = __("clinicBooking.addressSelection.title", firstName) %} + +{% block form %} + {{ radios({ + fieldset: { + legend: { + html: appHeading({ + title: title, + caption: __("clinicBooking.appointment.caption", fullName) if childCount > 1 + }) + } + }, + hint: { text: __("clinicBooking.addressSelection.hint") }, + items: previousAddressItems, + decorate: "transaction.previousAddress" + }) }} +{% endblock %} diff --git a/app/views/book-into-a-clinic/form/child.njk b/app/views/book-into-a-clinic/form/child.njk index 0e2047129..01fa91d3f 100644 --- a/app/views/book-into-a-clinic/form/child.njk +++ b/app/views/book-into-a-clinic/form/child.njk @@ -17,12 +17,12 @@ {{ input({ label: { text: __("clinicBooking.child.firstName.label") }, hint: { text: __("clinicBooking.child.firstName.hint") }, - decorate: "appointment.unmatchedFirstName" + decorate: "appointment.child.firstName" }) }} {{ input({ label: { text: __("clinicBooking.child.lastName.label") }, hint: { text: __("clinicBooking.child.lastName.hint") }, - decorate: "appointment.unmatchedLastName" + decorate: "appointment.child.lastName" }) }} {% endblock %} diff --git a/app/views/book-into-a-clinic/form/dob.njk b/app/views/book-into-a-clinic/form/dob.njk index bdc5d2236..bde223874 100644 --- a/app/views/book-into-a-clinic/form/dob.njk +++ b/app/views/book-into-a-clinic/form/dob.njk @@ -13,6 +13,6 @@ } }, hint: { text: __("clinicBooking.dob.hint") }, - decorate: "clinicAppointment.unmatchedDob_" + decorate: "appointment.child.dob_" }) }} {% endblock %}