- While this clinic is primarily for {{ booking.formatted.primaryProgramme }} vaccinations, our records show that
+ While this clinic is primarily for {{ data.clinicInvite.programmeNames }} vaccinations, our records show that
{{ firstName }} is also eligible for other vaccinations.
diff --git a/app/views/book-into-a-clinic/start.njk b/app/views/book-into-a-clinic/start.njk
index c70c8c33a..33a3fa7af 100644
--- a/app/views/book-into-a-clinic/start.njk
+++ b/app/views/book-into-a-clinic/start.njk
@@ -7,17 +7,12 @@
{{ appHeading({
size: "xl",
- title: __("clinicBooking.start.title." + sessionPreset.name)
+ title: __("clinicBooking.start.title")
}) }}
-
- If your child has not been vaccinated at school, or is not up to date with their vaccinations for any other reason, you can book into a clinic.
-
+ {{ __("clinicBooking.start.intro") | nhsukMarkdown }}
-
- Clinics have recently been set up to offer {{ __("clinicBooking.start.primaryProgrammeInSentence." + sessionPreset.name) }} vaccinations, but your child may be able to catch up on any outstanding
- vaccinations during their appointment.
-
+ {{ __("clinicBooking.start.programmes", { programmeNames: data.clinicInvite.programmeNames }) | nhsukMarkdown }}
{{ __("clinicBooking.start.confirm.title") }}
@@ -25,7 +20,7 @@
{{ button({
text: __("clinicBooking.start.confirm.buttonText"),
- href: "/book-into-a-clinic/" + sessionPreset.slug + "/new"
+ href: "/book-into-a-clinic/new"
}) }}
diff --git a/app/views/index.njk b/app/views/index.njk
index 8acab4cc2..14945fdb3 100644
--- a/app/views/index.njk
+++ b/app/views/index.njk
@@ -83,7 +83,7 @@
{{ actionLink({
classes: "nhsuk-u-margin-bottom-2",
text: "Start page for parents to book into a clinic for " + preset.name | replace("Flu", "flu"),
- href: navigation.clinicBookingUrl[preset.name] + "/start"
+ href: navigation.clinicInviteUrl[preset.name]
}) }}
{% endif %}
{% endfor %}
diff --git a/app/views/review/_navigation.njk b/app/views/review/_navigation.njk
index 2ea9ed31d..6666a1df1 100644
--- a/app/views/review/_navigation.njk
+++ b/app/views/review/_navigation.njk
@@ -19,6 +19,11 @@
href: "/consents",
current: params.view == "consent"
},
+ {
+ text: __("appointments.list.label") + appCount(data.counts.appointments),
+ href: "/appointments",
+ current: params.view == "appointments"
+ },
{
text: __("move.list.label") + appCount(data.counts.moves),
href: "/moves",
diff --git a/app/views/session/_session-clinic-summary.njk b/app/views/session/_session-clinic-summary.njk
index b3ef43a9b..dbd15e467 100644
--- a/app/views/session/_session-clinic-summary.njk
+++ b/app/views/session/_session-clinic-summary.njk
@@ -52,7 +52,7 @@
colour: "orange" if session.unmatchedAppointments.length > 0,
compact: true,
data: session.unmatchedAppointments.length,
- href: session.uri + "/edit"
+ href: session.uri + "/appointments"
}) }}
{% for programme in session.programmes %}
diff --git a/lib/create-data.js b/lib/create-data.js
index 9de60b896..96f6a8c8d 100644
--- a/lib/create-data.js
+++ b/lib/create-data.js
@@ -11,7 +11,6 @@ import usersData from '../app/datasets/users.js'
import vaccinesData from '../app/datasets/vaccines.js'
import {
ArchiveRecordReason,
- ConsentOutcome,
ConsentWindow,
PatientStatus,
ProgrammeType,
@@ -34,7 +33,6 @@ import { generateClinicVaccinationPeriods } from '../app/generators/clinic-vacci
import { generateConsent } from '../app/generators/consent.js'
import { generateInstruction } from '../app/generators/instruction.js'
import { generateNotice } from '../app/generators/notice.js'
-import { generateParent } from '../app/generators/parent.js'
import { generatePatient } from '../app/generators/patient.js'
import { generatePDSRecord } from '../app/generators/pds-record.js'
import { generateSession } from '../app/generators/session.js'
@@ -71,7 +69,6 @@ import { generateDataFile } from './generate-data-file.js'
const totalUsers = Number(process.env.USERS) || 20
const totalTeams = Number(process.env.TEAMS) || 5
const totalBatches = Number(process.env.BATCHES) || 100
-const totalClinicBookings = Number(process.env.CLINIC_BOOKINGS) || 10
const totalPatients = Number(process.env.RECORDS) || 4000
// Context
@@ -163,7 +160,7 @@ context.uploads[cohortUpload.id] = cohortUpload
for (const school of Object.values(context.schools)) {
const patient_uuids = Object.values(context.patients)
.filter(({ school_id }) => school_id === school.id)
- .flatMap(({ uuid }) => uuid)
+ .map(({ uuid }) => uuid)
const schoolUpload = generateUpload(
patient_uuids,
@@ -184,7 +181,7 @@ for (const preset of Object.values(SessionPresets)) {
// Adolescent programmes are only held at secondary schools
preset.adolescent ? phase === SchoolPhase.Secondary : phase
)
- .flatMap(({ id }) => id)
+ .map(({ id }) => id)
// Schedule school sessions
for (const school_id of ids) {
@@ -195,7 +192,6 @@ for (const preset of Object.values(SessionPresets)) {
}
// Schedule clinic sessions
- // TODO: Get clinics from team (linked to patient’s school)
const clinicsPerPreset = 3
const clinic_ids = faker.helpers.arrayElements(
Object.values(context.teams).flatMap((team) => team.clinic_ids),
@@ -222,113 +218,42 @@ if (!hasSessionToday) {
context.sessions[earliestPlannedSchoolSession.id].date = today()
}
-// Clinic bookings
-context.clinicBookings = {}
-Array.from([...range(1, totalClinicBookings)]).forEach(() => {
- const booking = generateEmptyClinicBooking(context)
- context.clinicBookings[booking.uuid] = booking
-})
-
-// Clinic appointments
-for (const booking of Object.values(context.clinicBookings)) {
- // Create the first appointment for the booking
- const firstAppointment = generateClinicAppointment(booking, context)
- if (!firstAppointment) {
- // TEMP fix while I figure out what's causing the failure to find a clinic session with the relevant preset
- continue
- }
-
- // Generate parent details based on first child, updating both the booking and appointment with this info
- const patient = firstAppointment.patient
- booking.parent =
- patient?.parent1 ||
- patient?.parent2 ||
- generateParent(firstAppointment.lastName, faker.datatype.boolean(0.5))
- firstAppointment.parentalRelationship = booking.parent.relationship
- firstAppointment.parentalRelationshipOther = booking.parent.relationshipOther
- firstAppointment.parentHasParentalResponsibility =
- booking.parent.hasParentalResponsibility
-
- // Make any additional appointments for this booking
- const additionalAppointmentsCount = faker.datatype.boolean(0.8)
- ? 0
- : faker.helpers.weightedArrayElement([
- { value: 1, weight: 90 },
- { value: 2, weight: 9 },
- { value: 3, weight: 1 }
- ])
- Array.from({ length: additionalAppointmentsCount }).forEach(() =>
- generateClinicAppointment(booking, context)
- )
-}
-
// Invite
// TODO: Don’t invite patients who’ve already had a programme’s vaccination
context.patientSessions = {}
-for (let session of Object.values(context.sessions)) {
+for (let session of Object.values(context.sessions).filter(
+ ({ type }) => type === SessionType.School
+)) {
session = new Session(session, context)
- if (session.type === SessionType.School) {
- const patientsInsideSchool = Object.values(context.patients).filter(
- ({ school_id }) => school_id === session.school_id
- )
-
- for (let patient of patientsInsideSchool) {
- patient = new Patient(patient, context)
-
- for (const programme_id of session.programme_ids) {
- const { canInviteToSession } = patient.programmes[programme_id]
-
- if (canInviteToSession) {
- const patientSession = new PatientSession(
- {
- createdAt: session.openAt,
- patient_uuid: patient.uuid,
- programme_id,
- session_id: session.id
- },
- context
- )
-
- // Add patient to session
- patient.addToSession(patientSession)
+ const patientsInsideSchool = Object.values(context.patients).filter(
+ ({ school_id }) => school_id === session.school_id
+ )
- // 2️⃣🅰️ REQUEST CONSENT
- patient.requestConsent(patientSession)
+ for (let patient of patientsInsideSchool) {
+ patient = new Patient(patient, context)
- context.patientSessions[patientSession.uuid] = patientSession
- }
- }
- }
- }
-
- if (session.type === SessionType.Clinic) {
- const patientsOutsideSchool = Object.values(context.patients).filter(
- ({ school_id }) => ['888888', '999999'].includes(school_id)
- )
+ for (const programme_id of session.programme_ids) {
+ const { canInviteToSession } = patient.programmes[programme_id]
- for (const patient of patientsOutsideSchool) {
- for (const programme_id of session.programme_ids) {
- const { canInviteToSession } = patient.programmes[programme_id]
-
- if (canInviteToSession) {
- const patientSession = new PatientSession(
- {
- patient_uuid: patient.uuid,
- programme_id,
- session_id: session.id
- },
- context
- )
+ if (canInviteToSession) {
+ const patientSession = new PatientSession(
+ {
+ createdAt: session.openAt,
+ patient_uuid: patient.uuid,
+ programme_id,
+ session_id: session.id
+ },
+ context
+ )
- // Add patient to session
- patient.addToSession(patientSession)
+ // Add patient to session
+ patient.addToSession(patientSession)
- // 2️⃣🅱️ INVITE home-educated/school unknown patient to clinic
- patient.requestConsent(patientSession)
+ // 2️⃣🅰️ REQUEST CONSENT
+ patient.requestConsent(patientSession)
- context.patientSessions[patientSession.uuid] = patientSession
- }
+ context.patientSessions[patientSession.uuid] = patientSession
}
}
}
@@ -535,48 +460,91 @@ for (const patientSession of Object.values(context.patientSessions)) {
}
}
-// Invite remaining unvaccinated patients to clinics
-for (const programme of Object.values(context.programmes)) {
- const programmeSchoolSessions = Object.values(context.sessions).filter(
- ({ programme_ids }) => programme_ids.includes(programme.id)
- )
+// Clinic invites
+// for children who are clinic ready e.g. home-educated or missed school session, but
+// only do it for half of the schools (so we leave some children in the clinic-ready state)
+const invited_school_ids = new Set([
+ ...Object.keys(context.schools).filter((_, index) => index % 2 === 0),
+ '888888',
+ '999999'
+])
+for (const patient of Patient.findAll(context)) {
+ // Skip this school to avoid inviting everyone?
+ if (!invited_school_ids.has(patient.school_id)) {
+ continue
+ }
+ const clinicReadyProgramme_ids = patient.clinicReadyProgramme_ids
- const programmeClinicSession = Object.values(context.sessions)
- .filter(({ programme_ids }) => programme_ids.includes(programme.id))
- .find(({ type }) => type === SessionType.Clinic)
+ // Invite to book a clinic appointment...
+ if (clinicReadyProgramme_ids.length) {
+ patient.inviteToClinic(clinicReadyProgramme_ids)
+ Patient.update(patient.uuid, patient, context)
+ }
+}
- // Move patients without outcome in a completed school session to a clinic
- for (const session of programmeSchoolSessions) {
- if (session.isCompleted) {
- // TODO: Patients have no context, so won’t have outcomes to filter on
- const patientSessions = session.patients
- .filter(({ report }) => report !== PatientStatus.Vaccinated)
- .filter(({ screen }) => screen !== ScreenOutcome.DoNotVaccinate)
- .filter(({ consent }) => consent !== ConsentOutcome.Refused)
- .filter(({ consent }) => consent !== ConsentOutcome.FinalRefusal)
+// Clinic appointments
+// To prevent us just filling every possible clinic slot, decide how full we want each clinic to get
+const clinicSessions = Object.values(context.sessions).filter(
+ ({ type }) => type === SessionType.Clinic
+)
+const clinicTargets = new Map(
+ clinicSessions.map((session) => [
+ session,
+ faker.number.int({ min: 30, max: 100 })
+ ])
+)
+context.clinicBookings = {}
+for (const patient of Patient.findAll(context)) {
+ // Exclude anyone not invited to clinic yet
+ if (!patient.clinicProgramme_ids?.length) {
+ continue
+ }
- for (let patientSession of patientSessions) {
- const patient = new Patient(patientSession.patient, context)
+ // Choose a clinic session in which we'll book an appointment
+ const matchingClinicSessions = clinicSessions.filter((session) => {
+ const matchingProgramme_ids = [
+ ...new Set(patient.clinicProgramme_ids).intersection(
+ new Set(session.programme_ids)
+ )
+ ]
- const clinicPatientSession = new PatientSession(
- {
- createdBy_uid: nurse.uid,
- patient_uuid: patient.uuid,
- programme_id: programme.id,
- session_id: programmeClinicSession.id
- },
- context
- )
- context.patientSessions[clinicPatientSession.uuid] =
- clinicPatientSession
+ if (!matchingProgramme_ids.length) {
+ return false
+ }
- // Add patient to community clinic
- patient.addToSession(clinicPatientSession)
+ return session.percentBooked < clinicTargets.get(session)
+ })
+ if (!matchingClinicSessions.length) {
+ continue
+ }
+ const session = faker.helpers.arrayElement(matchingClinicSessions)
- // 2️⃣ INVITE TO BOOK CLINIC APPOINTMENT
- patient.inviteToClinic(programmeClinicSession.programme_ids)
- }
- }
+ // Create a single child's appointment and containing booking
+ // TODO: find or create siblings to add as well
+ const booking = generateEmptyClinicBooking(context)
+ const appointment = generateClinicAppointment(patient, session, booking)
+
+ // Store the booking on the context
+ context.clinicBookings[booking.uuid] = booking
+
+ // If we've matched the child, formally add them to the session (otherwise the appointment will appear as an umatched appointment)
+ if (appointment.patient) {
+ // Create a patient session for each programme being vaccinated, assuming the child
+ // will be vaccinated for everything for which they're clinic-ready
+ appointment.patient.clinicProgramme_ids.forEach((programme_id) => {
+ const patientSession = new PatientSession(
+ {
+ patient_uuid: appointment.patient.uuid,
+ programme_id,
+ session_id: session.id
+ },
+ context
+ )
+
+ appointment.patient.addToSession(patientSession)
+
+ context.patientSessions[patientSession.uuid] = patientSession
+ })
}
}
diff --git a/package-lock.json b/package-lock.json
index 18befc42a..5077c90cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1619,9 +1619,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1642,9 +1639,6 @@
"cpu": [
"arm"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1665,9 +1659,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1688,9 +1679,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1711,9 +1699,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1734,9 +1719,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2171,9 +2153,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2188,9 +2167,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2205,9 +2181,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2222,9 +2195,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2239,9 +2209,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2256,9 +2223,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2273,9 +2237,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2290,9 +2251,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -13322,7 +13280,6 @@
"cpu": [
"arm"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -13339,7 +13296,6 @@
"cpu": [
"arm64"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -13356,7 +13312,6 @@
"cpu": [
"arm"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -13373,7 +13328,6 @@
"cpu": [
"arm64"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -13390,7 +13344,6 @@
"cpu": [
"riscv64"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -13407,7 +13360,6 @@
"cpu": [
"x64"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -13424,7 +13376,6 @@
"cpu": [
"riscv64"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -13441,7 +13392,6 @@
"cpu": [
"x64"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [