Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 86 additions & 27 deletions app/controllers/book-into-a-clinic.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
/**
* Record the session preset
*
* @param {*} request

Check warning on line 17 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} response

Check warning on line 18 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} next

Check warning on line 19 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} session_preset_slug

Check warning on line 20 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
*/
read(request, response, next, session_preset_slug) {
const serviceName = 'Book into a clinic'
Expand All @@ -43,8 +43,8 @@
/**
* Send to the start page
*
* @param {*} request

Check warning on line 46 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} response

Check warning on line 47 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
*/
redirect(request, response) {
const { sessionPreset } = response.locals
Expand All @@ -55,8 +55,8 @@
/**
* Start a new clinic booking for clinics with the primary programme we've been given
*
* @param {*} request

Check warning on line 58 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} response

Check warning on line 59 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
*/
new(request, response) {
const { data } = request.session
Expand All @@ -81,8 +81,8 @@
* This includes code to set up radio button groups for various pages (we set them up
* regardless of which specific route we're handling).
*
* @param {*} request

Check warning on line 84 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} response

Check warning on line 85 in app/controllers/book-into-a-clinic.js

View workflow job for this annotation

GitHub Actions / lint-and-create-data

Prefer a more specific type to `*`
* @param {*} next
*/
readForm(request, response, next) {
Expand All @@ -94,7 +94,9 @@
*
* 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
Expand All @@ -104,17 +106,13 @@
* - ...
* - 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.
Expand All @@ -136,8 +134,7 @@
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'
}
}
Expand All @@ -154,7 +151,7 @@
[`/${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`]: {
Expand All @@ -163,9 +160,12 @@
},
[`/${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'
}
Expand All @@ -180,9 +180,6 @@
data
),

// Check answers
[`/${session_preset_slug}/${booking_uuid}/new/check-answers`]: {},

// Confirmation! \o/
[`/${session_preset_slug}/${booking_uuid}/new/confirmation`]: {}
}
Expand Down Expand Up @@ -212,7 +209,7 @@
*/
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
Expand All @@ -227,6 +224,50 @@
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 })
},

Expand All @@ -237,7 +278,7 @@
* @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

Expand All @@ -257,8 +298,10 @@
_.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)
Expand All @@ -280,19 +323,35 @@
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)
})
},

/**
Expand Down
43 changes: 24 additions & 19 deletions app/generators/clinic-appointment.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,47 +10,53 @@ 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
}
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
Expand Down
4 changes: 4 additions & 0 deletions app/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
42 changes: 6 additions & 36 deletions app/models/clinic-appointment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

/**
Expand All @@ -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
}

/**
Expand All @@ -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
*
Expand Down
Loading