-
Notifications
You must be signed in to change notification settings - Fork 1
First click-through of the parents' clinic booking journey #223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
0159780
Add models for clinic bookings and appointments
malross bacde11
Generate fake data for clinic bookings and their appointments
malross fb055be
Data generation functions for clinic bookings and appointments
malross 32f100e
Improvements to generation of faked clinic bookings
malross c8149c2
Create fake parents with the Fosterer relationship
malross 5f5da58
Add initial routes and controller for clinic bookings.
malross c311088
Add dump-only views for show and list on clinic bookings
malross 2791244
refactor: De-duplicate the clinic booking reference generation
malross 3f14263
Allow inspect() to remove context from arrays as well as single records
malross e70d48c
fix: Generate expected number of fake clinic bookings
malross 53537d1
fix: redirect to correct route format after updating a clinic booking
malross a2dbf5e
Add clinic appointment faked data, routes and controller
malross d735cb9
Move parent details for clinic bookings into clinic appointments
malross 961b5aa
Present details of a single clinic appointment in a new view
malross 5e0e864
Show the details for a single clinic booking
malross 80e0b55
Add the primary programme to a clinic booking and its routes
malross a5368d1
Create views for early pages in the clinic booking journey
malross 42e355c
Fix content in the parent's clinic booking journey
malross e0a507a
Add more views to the parents' clinic booking journey
malross 3526385
Minor: typo fixes and comments around clinc booking
malross 52f6d7e
Hard-code health question sequence for clinic booking
malross 13389da
Add hard-coded check answers page for clinic booking
malross bae88d1
Add confirmation page for clinic booking journey
malross 2acd8f9
Use correct heading levels on clinic booking check answers page
malross 0f29679
Move clinics' health questions to after booking confirmation
malross c7a3bad
Fix the forking for health questions in clinic booking
malross ef0afe7
Caption all appointment pages of the clinic booking journey
malross b2b92af
Use HPV, not MMR, immune system health question.
malross 21534d7
Fix misplaced HTML in clinic booking's check answers page
malross acb7312
Fix all linter issues in clinic booking
malross 4662b06
Add homepage links for booking into clinic journeys
malross 67e7620
Create all appointment models in advance for clinic bookings
malross 3652fa2
Reorganise the parent details for clinic bookings
malross df7f0c3
Add a parental responsibility block in the clinic booking journey
malross 36d815e
Fix hard-coded date order in clinic booking
malross 07c3438
Hide SAIS team navigation in parent's clinic booking journey
malross 0fe0f82
Fix branching around parental responsibility in clinic booking
malross 85c2227
Fix the branching for contact preferences in clinic booking
malross 7aad1a5
Iterate over multiple appointments in clinic booking journey
malross f9553b6
Increase prominence of vaccine choice heading in clinic booking
malross 8c33a39
Sanitise any _unchecked values coming from the clinic booking process
malross 4f97d49
Fix linter issue with unused class import
malross 6cde551
Show health questions for chosen vaccines for all appointments in cli…
malross ab39740
Clarify the need for 3 doses of the HPV vaccine in contexts that hand…
malross 03f66a0
Clarify the points at which clinic booking subjourneys move onto the …
malross ddcff1f
Typography improvements
malross 44e25e6
Consistent heaading sizes in the clinic booking journey
malross ec18003
Show correct header on child name page in clinic booking
malross c85c47e
Format clinic booking references as codes.
malross c2b43fb
Avoid race condition with session storage in clinic booking
malross 574d038
Make clinic booking data generation more robust
malross ab51ae7
Fix generateClinicAppointment (#230)
malross 17c24fb
Clinic booking fixes (#232)
malross 576aeff
Fix linter warnings and remove/tidy up TODOs.
malross 8084671
Prefer forEach over index-based for loop
malross eb08002
Prefer forEach over index-based for loop
malross 1310b63
Don't use abbreviations for variable names
malross 7e54966
Use English, not class names, in parameter descriptions.
malross 1eba6c6
Favour the appHeading macro over hard-coded HTML
malross 83c5ad0
Rename createInContext to match repo conventions
malross File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,365 @@ | ||
| import { fakerEN_GB as faker } from '@faker-js/faker' | ||
| import wizard from '@x-govuk/govuk-prototype-wizard' | ||
| import _ from 'lodash' | ||
|
|
||
| import { ParentalRelationship, SessionPresets } from '../enums.js' | ||
| import { ClinicAppointment, ClinicBooking } from '../models.js' | ||
| import { | ||
| getAllAppointmentPaths, | ||
| getHealthQuestionPaths | ||
| } from '../utils/clinic-appointment.js' | ||
| import { kebabToCamelCase } from '../utils/string.js' | ||
|
|
||
| /** | ||
| * @typedef {import('express').Request} Request | ||
| * @typedef {import('express').Response} Response | ||
| * @typedef {import('express').NextFunction} Next | ||
| */ | ||
|
|
||
| export const bookIntoClinicController = { | ||
| /** | ||
| * Record the session preset | ||
| * | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| * @param {Next} next | ||
| * @param {string} session_preset_slug | ||
| */ | ||
| read(request, response, next, session_preset_slug) { | ||
| const serviceName = 'Book into a clinic' | ||
|
|
||
| response.locals.assetsName = 'public' | ||
| response.locals.serviceName = serviceName | ||
| response.locals.headerOptions = { service: { text: serviceName } } | ||
|
|
||
| // Record the session preset (aka "primary programme" to the parent) | ||
| const sessionPreset = | ||
| SessionPresets.find((preset) => preset.slug === session_preset_slug) ?? | ||
| SessionPresets[0] | ||
| response.locals.sessionPreset = sessionPreset | ||
|
|
||
| // Allow us to offer a phone booking if not wanting online (start.njk) | ||
| response.locals.bookingPhoneNumber = | ||
| request.session.data.teams[0]?.tel ?? | ||
| faker.helpers.replaceSymbols('01### ######') | ||
|
|
||
| next() | ||
| }, | ||
|
|
||
| /** | ||
| * Send to the start page | ||
| * | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| redirect(request, response) { | ||
| const { sessionPreset } = response.locals | ||
|
|
||
| response.redirect(`${request.baseUrl}/${sessionPreset.slug}/start`) | ||
| }, | ||
|
|
||
| /** | ||
| * Start a new clinic booking for clinics with the primary programme we've been given | ||
| * | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| new(request, response) { | ||
| const { data } = request.session | ||
| const { sessionPreset } = response.locals | ||
|
|
||
| // Create a new clinic booking in the wizard context | ||
| const booking = ClinicBooking.create( | ||
| { | ||
| sessionPreset | ||
| }, | ||
| data.wizard | ||
| ) | ||
|
|
||
| // Redirect to the first page in the booking journey (after the start page, that is) | ||
| const redirectUrl = `${request.baseUrl}/${booking.bookingUri}/new/child-count` | ||
| response.redirect(redirectUrl) | ||
| }, | ||
|
|
||
| /** | ||
| * Prepare a form-based page of the clinic booking journey. | ||
| * | ||
| * 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} request | ||
| * @param {Response} response | ||
| * @param {Next} next | ||
| */ | ||
| readForm(request, response, next) { | ||
| const { session_preset_slug, booking_uuid } = request.params | ||
| const appointment_uuid = request.params.appointment_uuid | ||
| const { data, referrer } = request.session | ||
|
|
||
| /** | ||
| * NOTE: | ||
| * | ||
| * 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). 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 | ||
| * - How many children? | ||
| * - Child name <-- first page of the per-child appointment journey | ||
| * - Child DOB | ||
| * - ... | ||
| * - 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 | ||
| * - Confirmation | ||
| * | ||
| */ | ||
|
|
||
| // Create objects on the global context to allow us to check branching conditions, etc. | ||
| // And make them available to the view. | ||
| let booking, appointment | ||
| if (booking_uuid) { | ||
| booking = new ClinicBooking( | ||
| ClinicBooking.findOne(booking_uuid, data?.wizard), | ||
| data | ||
| ) | ||
| response.locals.booking = booking | ||
|
|
||
| if (appointment_uuid) { | ||
| appointment = new ClinicAppointment( | ||
| ClinicAppointment.findOne(appointment_uuid, data?.wizard), | ||
| data | ||
| ) | ||
| response.locals.appointment = appointment | ||
| response.locals.childNumber = | ||
| booking.appointments_ids.indexOf(appointment.uuid) + 1 | ||
| response.locals.childCount = booking.appointments_ids.length | ||
| response.locals.firstName = appointment.firstName || 'your child' | ||
| response.locals.fullName = appointment.fullName || 'your child' | ||
| } | ||
| } | ||
|
|
||
| // Make sure the views have access to information about flow control e.g. for narrowing down a clinic search | ||
| let transaction | ||
| if (data.wizard?.transaction) { | ||
| transaction = data.wizard?.transaction | ||
| response.locals.transaction = transaction | ||
| } | ||
|
|
||
| const journey = { | ||
| [`/${session_preset_slug}`]: {}, | ||
| [`/${session_preset_slug}/${booking_uuid}/new/child-count`]: {}, | ||
|
|
||
| // Appointment journey; once per child | ||
| ...getAllAppointmentPaths(request.session.data, booking), | ||
|
|
||
| // Parent journey | ||
| [`/${session_preset_slug}/${booking_uuid}/new/parent`]: { | ||
| [`/${session_preset_slug}/${booking_uuid}/new/offer-health-questions`]: | ||
| () => !request.session.data.booking?.parent?.tel | ||
| }, | ||
| [`/${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/confirmation`]: { | ||
| data: 'transaction.optedIntoHealthQuestions', | ||
| value: 'false' | ||
| } | ||
| }, | ||
|
|
||
| // For each child being booked in, and their selected vaccinations, ask the | ||
| // relevant health questions and impairments/adjustments questions | ||
| ...getHealthQuestionPaths( | ||
| `/${session_preset_slug}/${booking_uuid}/new/`, | ||
| booking_uuid, | ||
| data.wizard, | ||
| data | ||
| ), | ||
|
|
||
| // Confirmation! \o/ | ||
| [`/${session_preset_slug}/${booking_uuid}/new/confirmation`]: {} | ||
| } | ||
|
|
||
| const paths = wizard(journey, request) | ||
| paths.back = referrer || paths.back | ||
| response.locals.paths = paths // used later to redirect in updateForm | ||
|
|
||
| // Prepare the radio options for the parental relationship page | ||
| response.locals.parentalRelationshipItems = Object.values( | ||
| ParentalRelationship | ||
| ) | ||
| .filter((relationship) => relationship !== ParentalRelationship.Unknown) | ||
| .map((relationship) => ({ | ||
| text: relationship, | ||
| value: relationship | ||
| })) | ||
|
|
||
| next() | ||
| }, | ||
|
|
||
| /** | ||
| * Render the requested form page | ||
| * | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| showForm(request, response) { | ||
| const { appointment } = response.locals | ||
| let { booking_uuid, view } = request.params | ||
|
|
||
| // All health questions use the same view | ||
| let key | ||
| if (view.startsWith('health-question-')) { | ||
| key = kebabToCamelCase(view.replace('health-question-', '')) | ||
| view = 'health-question' | ||
| } | ||
|
|
||
| // Only ask for details if question does not have sub-questions | ||
| const hasSubQuestions = | ||
| appointment?.getHealthQuestionsForSelectedProgrammes( | ||
| 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' | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| response.render(`book-into-a-clinic/form/${view}`, { key, hasSubQuestions }) | ||
| }, | ||
|
|
||
| /** | ||
| * Store the latest values entered into a form in the booking journey | ||
| * | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| updateForm(request, response) { | ||
| const { booking_uuid, appointment_uuid, view } = request.params | ||
| const { data } = request.session | ||
| const { paths } = response.locals | ||
|
|
||
| // Store values from the posted form | ||
| if (request.body.booking) { | ||
| ClinicBooking.update(booking_uuid, request.body.booking, data.wizard) | ||
| } | ||
| if (request.body.appointment) { | ||
| ClinicAppointment.update( | ||
| appointment_uuid, | ||
| request.body.appointment, | ||
| data.wizard | ||
| ) | ||
| } | ||
| if (request.body.transaction) { | ||
| data.wizard.transaction = data.wizard.transaction ?? {} | ||
| _.merge(data.wizard.transaction, request.body.transaction) | ||
| } | ||
|
|
||
| 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) | ||
| desiredCount = isNaN(desiredCount) || desiredCount < 1 ? 1 : desiredCount | ||
| const existingCount = booking.appointments_ids.length | ||
|
|
||
| const childrenToAdd = Math.max(0, desiredCount - existingCount) | ||
| const childrenToRemove = Math.max(0, existingCount - desiredCount) | ||
| Array.from({ length: childrenToAdd }).forEach(() => { | ||
| const appointment = ClinicAppointment.create( | ||
| { primary_programme_ids: booking.primaryProgrammeIDs }, | ||
| data.wizard | ||
| ) | ||
|
|
||
| booking.addAppointment(appointment) | ||
| }) | ||
| Array.from({ length: childrenToRemove }).forEach(() => { | ||
| const appointment_uuid = booking.removeLastAppointment() | ||
| ClinicAppointment.delete(appointment_uuid, data.wizard) | ||
| }) | ||
|
|
||
| // Start the appointment journey for the first child | ||
| const firstAppointment = booking.appointments[0] | ||
| const firstAppointmentUrl = `${request.baseUrl}/${booking.bookingUri}/new/${firstAppointment.appointmentUri}/child` | ||
| 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((error) => { | ||
| if (!error) response.redirect(nextUrl) | ||
| }) | ||
| }, | ||
|
|
||
| /** | ||
| * Catch-all for pages not needing to reference a given clinic booking | ||
| * | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| show(request, response) { | ||
| const view = request.params.view || 'start' | ||
|
|
||
| response.render(`book-into-a-clinic/${view}`) | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.