diff --git a/app/controllers/session.js b/app/controllers/session.js index 576084da0..7671dc41d 100644 --- a/app/controllers/session.js +++ b/app/controllers/session.js @@ -745,8 +745,14 @@ export const sessionController = { } } + const sessionWithFullContext = new Session(session, data) + if (session.type === SessionType.Clinic) { + response.locals.appointmentsToCancel = + sessionWithFullContext.appointmentsToCancel + } + // Give access to the data needed for the summaryRows - response.locals.session = new Session(session, data) + response.locals.session = sessionWithFullContext // Show back link to session page response.locals.back = session.uri diff --git a/app/locales/en.js b/app/locales/en.js index 009f37d25..2b3029f06 100644 --- a/app/locales/en.js +++ b/app/locales/en.js @@ -2546,7 +2546,14 @@ export const en = { }, edit: { title: 'Edit session', - success: '{{session.name}} updated' + success: '{{session.name}} updated', + appointments: { + cancellation: { + title: 'Appointments will be cancelled', + description: + '{count, plural, one {Changes made to this session will result in the cancellation of **1 appointment**.\n\nA notification will be sent to the parent or guardian of the affected child, inviting them to book a new appointment.} other {Changes made to this session will result in the cancellation of **{count} appointments**.\n\nNotifications will be sent to the parents or guardians of affected children, inviting them to book a new appointment.}}' + } + } }, cancel: { bookings: { diff --git a/app/models/clinic-booking.js b/app/models/clinic-booking.js index ef46f7d2c..0716c00af 100644 --- a/app/models/clinic-booking.js +++ b/app/models/clinic-booking.js @@ -2,6 +2,7 @@ import { fakerEN_GB as faker } from '@faker-js/faker' import _ from 'lodash' import { ClinicAppointment, Contact } from '../models.js' +import { today } from '../utils/date.js' import { formatCode, stringToArray, stringToBoolean } from '../utils/string.js' /** @@ -10,6 +11,7 @@ import { formatCode, stringToArray, stringToBoolean } from '../utils/string.js' * @param {object} [context] - Context * @property {object} [context] - Context * @property {string} [uuid] - Clinic booking UUID + * @property {Date} [createdAt] - Created date * @property {string} [bookingReference] - Booking reference number * @property {Array} [invited_programme_ids] - IDs of programmes for which child was invited * @property {Contact} [contact] - Contact details for the booking; see appointments for parental relationship details @@ -19,6 +21,8 @@ export class ClinicBooking { constructor(options, context) { this.context = context this.uuid = options?.uuid || faker.string.uuid() + this.createdAt = options?.createdAt ? new Date(options.createdAt) : today() + this.bookingReference = options?.bookingReference || ClinicBooking.generateReference() this.invited_programme_ids = options?.invited_programme_ids diff --git a/app/models/clinic-vaccination-period.js b/app/models/clinic-vaccination-period.js index 44171edcb..97fac266e 100644 --- a/app/models/clinic-vaccination-period.js +++ b/app/models/clinic-vaccination-period.js @@ -12,9 +12,7 @@ import { * @param {object} options - property values * @property {string} [uuid] - Vaccination period UUID * @property {Date} [startAt] - Start time of first appointment slot - * @property {Date} [startAt_] - Start time of first appointment slot, from dateInput - see getter/setter * @property {Date} [endAt] - End time of final appointment slot - * @property {Date} [endAt_] - End time of final appointment slot, from dateInput - see getter/setter * @property {number} [vaccinatorCount] - The number of staff vaccinating in parallel during this period */ export class ClinicVaccinationPeriod { @@ -22,9 +20,7 @@ export class ClinicVaccinationPeriod { this.uuid = options?.uuid || faker.string.uuid() this.startAt = options?.startAt && new Date(options.startAt) - this.startAt_ = options?.startAt_ this.endAt = options?.endAt && new Date(options.endAt) - this.endAt_ = options?.endAt_ this.vaccinatorCount = options?.vaccinatorCount } @@ -92,6 +88,26 @@ export class ClinicVaccinationPeriod { } } + /** + * Does the given appointment start time fall within this period? + * + * @param {Date} appointmentTime - the start time of appointment + * @param {number} appointmentLengthInMinutes - the length of slots in minutes + * @returns {boolean} - true if the slot falls within this period, or false otherwise + */ + includesAppointmentTime(appointmentTime, appointmentLengthInMinutes) { + const firstSlotTime = this.startAt.getTime() + const lastSlotTime = addMinutes( + this.endAt, + -appointmentLengthInMinutes + ).getTime() + + return ( + appointmentTime.getTime() >= firstSlotTime && + appointmentTime.getTime() <= lastSlotTime + ) + } + /** * Get formatted values * diff --git a/app/models/session.js b/app/models/session.js index e6bf82ee2..0a6c9030c 100644 --- a/app/models/session.js +++ b/app/models/session.js @@ -620,6 +620,38 @@ export class Session { return appointments.filter(({ patient_uuid }) => !patient_uuid) } + /** + * Get a list of the appointments already made but for which we don't have capacity + * + * @returns {Array} - the appointments we'll need to cancel + */ + get appointmentsToCancel() { + const allAppointmentsByTime = _.groupBy(this.appointments, (appointment) => + appointment.startAt.getTime() + ) + + const appointmentsWithoutVaccinators = [] + Object.entries(allAppointmentsByTime).forEach(([key, appointments]) => { + const startAt = new Date() + startAt.setTime(Number(key)) + const vaccinationPeriod = this.vaccinationPeriods.find((period) => + period.includesAppointmentTime(startAt, this.appointmentLength) + ) + if (!vaccinationPeriod) { + // No longer part of a vaccination period, so cancel all appointments at this time + appointmentsWithoutVaccinators.push(...appointments) + } else if (vaccinationPeriod.vaccinatorCount < appointments.length) { + // Not enough vaccinators at this time, so those who booked first get to keep their appointments + appointments = _.sortBy(appointments, 'booking.createdAt') + appointmentsWithoutVaccinators.push( + ...appointments.slice(vaccinationPeriod.vaccinatorCount) + ) + } + }) + + return appointmentsWithoutVaccinators + } + /** * Get school * diff --git a/app/views/session/edit.njk b/app/views/session/edit.njk index 18850231d..bb74e8f42 100644 --- a/app/views/session/edit.njk +++ b/app/views/session/edit.njk @@ -86,5 +86,14 @@ totalAppointments: {} }) }) }} + + {% if appointmentsToCancel.length > 0 %} + {% set detailsHtml = __mf("session.edit.appointments.cancellation.description", { count: appointmentsToCancel.length }) | nhsukMarkdown %} + + {{ warningCallout({ + heading: __("session.edit.appointments.cancellation.title"), + html: detailsHtml + }) }} + {% endif %} {% endif %} {% endblock %}