diff --git a/app/assets/sass/components/app-card.scss b/app/assets/sass/components/app-card.scss index 79ae51a..d88c122 100644 --- a/app/assets/sass/components/app-card.scss +++ b/app/assets/sass/components/app-card.scss @@ -4,6 +4,8 @@ border: 1px solid $nhsuk-border-colour; background-color: nhsuk-colour("white"); @include nhsuk-responsive-margin(4, bottom); + //NHS looking shadow + box-shadow: 0 2px 0 0 nhsuk-colour("grey-4"); &__header { display: flex; @@ -13,6 +15,7 @@ padding: nhsuk-spacing(3); background-color: nhsuk-colour("grey-5"); border-bottom: 1px solid $nhsuk-border-colour; + } &__title { @@ -22,45 +25,29 @@ } &__action { - @include nhsuk-font-size(14); + @include nhsuk-font-size(16); white-space: nowrap; } &__body { - padding: 0 nhsuk-spacing(3); - } - - &__table { - width: 100%; - margin: 0; - border-collapse: collapse; + padding: nhsuk-spacing(3); } - &__row { - border-bottom: 1px solid nhsuk-colour("grey-5"); + &__summary-list { + margin-bottom: 0; - &:last-child th, - &:last-child td { - border-bottom: 0; + .nhsuk-summary-list__key, + .nhsuk-summary-list__value, + .nhsuk-summary-list__actions { + @include nhsuk-font-size(16); + vertical-align: top; } - } - &__key, - &__value { - @include nhsuk-font-size(16); - padding: nhsuk-spacing(3) 0; - vertical-align: top; - } - - &__key { - width: 34%; - padding-right: nhsuk-spacing(3); - font-weight: $nhsuk-font-bold; - text-align: left; - } + .nhsuk-summary-list__row:last-child { + border-bottom: none; + } - &__value { - > :last-child { + .nhsuk-summary-list__value > :last-child { margin-bottom: 0; } } diff --git a/app/data/session-data-defaults.js b/app/data/session-data-defaults.js index 383863e..e8f6a31 100644 --- a/app/data/session-data-defaults.js +++ b/app/data/session-data-defaults.js @@ -54,7 +54,7 @@ const serviceDefinitions = [ { id: 'RSV:Adult', name: 'RSV Adult', vaccine: vaccineTypes.RSV, group: 'RSV', age: '18+', type: 'Adult' }, { id: 'RSV_COVID:12-17', name: 'RSV and COVID 12 to 17', vaccine: [vaccineTypes.RSV, vaccineTypes.COVID], group: 'RSV_AND_COVID', age: '12-17', type: 'Child' }, { id: 'RSV_COVID:18+', name: 'RSV and COVID 18+', vaccine: [vaccineTypes.RSV, vaccineTypes.COVID], group: 'RSV_AND_COVID', age: '18+', type: 'Adult' }, - { id: 'MENB:16-18', name: 'MenB 17 to 18', vaccine: vaccineTypes.MENB, group: 'MENB', age: '17-18', type: null } + { id: 'MENB:16-18', name: 'MenB Young People', vaccine: vaccineTypes.MENB, group: 'MENB', age: '17-18', type: null } ]; const SERVICES = Object.fromEntries( @@ -120,15 +120,10 @@ const base = { hideCard: true }, { - text: 'View appointments', - description: 'View and manage appointments for your site', + text: 'Clinics', + description: 'View appointments for your site', hrefTemplate: '/site/:id/availability/day' }, - { - text: 'Manage clinics', - description: 'Create clinics and review recently created clinic series', - hrefTemplate: '/site/:id/clinics' - }, { text: 'Change site details', description: 'Change site details and accessibility information', diff --git a/app/data/site1.config.js b/app/data/site1.config.js index a42963c..a3050a9 100644 --- a/app/data/site1.config.js +++ b/app/data/site1.config.js @@ -11,6 +11,10 @@ const ongoingSeriesEnd = today.plus({ months: 2 }).toISODate(); const futureSingleDate = today.plus({ days: 3 }).toISODate(); const childSessionIn2Days = today.plus({ days: 2 }).toISODate(); const childSessionIn3Days = today.plus({ days: 3 }).toISODate(); +const childSessionIn4Days = today.plus({ days: 4 }).toISODate(); +const childSessionIn5Days = today.plus({ days: 5 }).toISODate(); +const childSessionIn6Days = today.plus({ days: 6 }).toISODate(); +const childSessionIn7Days = today.plus({ days: 7 }).toISODate(); const nextTuesdayDate = today.plus({ days: (2 - today.weekday + 7) % 7 }).toISODate(); const SERVICE_IDS = { @@ -102,10 +106,30 @@ const clinics = [ label: 'Adult Flu and Covid clinics (extended)' }, { - date: childSessionIn3Days, + date: childSessionIn4Days, from: '10:00', until: '16:30', label: 'Adult Flu and Covid clinics (extended)' + }, + { + date: childSessionIn6Days, + services: [ + { + operation: 'add', + service: SERVICE_IDS.RSV_ADULT + } + ], + label: 'Adult Flu and Covid clinics (service add)' + }, + { + date: childSessionIn7Days, + services: [ + { + operation: 'remove', + service: SERVICE_IDS.FLU_65_PLUS + } + ], + label: 'Adult Flu and Covid clinics (service remove)' } ], closures: [ diff --git a/app/routes/base.js b/app/routes/base.js index 15911d5..e4554fd 100644 --- a/app/routes/base.js +++ b/app/routes/base.js @@ -15,46 +15,46 @@ function getToday() { return override_today || DateTime.now().toFormat('yyyy-MM-dd'); } -function asArray(value) { - if (Array.isArray(value)) return value; - if (value === undefined || value === null || value === '') return []; - return [value]; +function getRelativeDayLabel(dateISO, todayISO) { + const target = DateTime.fromISO(dateISO || '').startOf('day'); + const today = DateTime.fromISO(todayISO || '').startOf('day'); + if (!target.isValid || !today.isValid) return null; + + const diffDays = Math.round(target.diff(today, 'days').days); + if (diffDays === 0) return 'Today'; + if (diffDays === -1) return 'Yesterday'; + if (diffDays === 1) return 'Tomorrow'; + return null; } -function applyServiceOperations(baseServices = [], operations = []) { - let services = [...asArray(baseServices)]; - - for (const change of asArray(operations)) { - if (!change || typeof change !== 'object') continue; - - if (change.operation === 'replace') { - services = asArray(change.values); - continue; - } +function getRelativeWeekLabel(weekStartISO, todayISO) { + const targetWeek = DateTime.fromISO(weekStartISO || '').startOf('week'); + const thisWeek = DateTime.fromISO(todayISO || '').startOf('week'); + if (!targetWeek.isValid || !thisWeek.isValid) return null; - if (change.operation === 'add' && change.service) { - if (!services.includes(change.service)) { - services.push(change.service); - } - continue; - } - - if (change.operation === 'remove' && change.service) { - services = services.filter((service) => service !== change.service); - } - } - - return services; + const diffWeeks = Math.round(targetWeek.diff(thisWeek, 'weeks').weeks); + if (diffWeeks === 0) return 'This week'; + if (diffWeeks === -1) return 'Last week'; + if (diffWeeks === 1) return 'Next week'; + return null; } -function sortedStringArray(values = []) { - return asArray(values) - .map((value) => String(value)) - .sort((a, b) => a.localeCompare(b)); +function getRelativeMonthLabel(monthDateISO, todayISO) { + const targetMonth = DateTime.fromISO(monthDateISO || '').startOf('month'); + const thisMonth = DateTime.fromISO(todayISO || '').startOf('month'); + if (!targetMonth.isValid || !thisMonth.isValid) return null; + + const diffMonths = Math.round(targetMonth.diff(thisMonth, 'months').months); + if (diffMonths === 0) return 'This month'; + if (diffMonths === -1) return 'Last month'; + if (diffMonths === 1) return 'Next month'; + return null; } -function servicesEqual(left = [], right = []) { - return JSON.stringify(sortedStringArray(left)) === JSON.stringify(sortedStringArray(right)); +function asArray(value) { + if (Array.isArray(value)) return value; + if (value === undefined || value === null || value === '') return []; + return [value]; } function normalizeSessionType(type) { @@ -368,7 +368,6 @@ function clone(value) { function editFieldOptions(isSeries) { const options = [ - { value: 'name', text: 'Name' }, { value: 'date', text: isSeries ? 'Dates' : 'Date' }, ...(isSeries ? [{ value: 'days', text: 'Days' }] : []), { value: 'time', text: 'Time' }, @@ -408,14 +407,12 @@ function bookingCountText(count) { function resetEditOutcome(state) { state.bookingAction = null; state.affectedBookingIds = []; - state.childOverrideWarning = null; - state.postWarningPath = null; } function editFieldsForStep(step, isSeries) { switch (step) { case 'details': - return ['name', 'date']; + return ['date']; case 'days': return isSeries ? ['days'] : ['date']; case 'clinic-times': @@ -472,7 +469,6 @@ function editCaptionText(draft) { function editStepForField(field, isSeries) { switch (field) { - case 'name': case 'date': return 'details'; case 'days': @@ -501,7 +497,7 @@ function setEditTemplateData(res, data, state) { function updateDraftFromDetails(state, newSession = {}) { const editableFields = currentEditableFields(state); - if (editableFields.length === 0 || editableFields.includes('name')) { + if ((editableFields.length === 0 || editableFields.includes('name')) && newSession.name !== undefined) { state.draft.name = String(newSession.name || '').trim(); } @@ -731,11 +727,6 @@ function buildSummaryRowMap(draft, siteId, sessionId, data) { const closuresText = draft.closures?.length ? `${draft.closures.length} added` : 'None added'; return { - name: { - key: { text: 'Name' }, - value: { text: draft.name || 'Not provided' }, - actions: { items: [{ href: `/site/${siteId}/clinics/edit/${sessionId}/change/name`, text: 'Change', visuallyHiddenText: ' name' }] } - }, date: { key: { text: isSeries ? 'Dates' : 'Date' }, value: { text: dateText }, @@ -779,7 +770,6 @@ function buildSummaryRowMap(draft, siteId, sessionId, data) { function buildSummaryRowsForEdit(draft, siteId, sessionId, data) { const rowsByField = buildSummaryRowMap(draft, siteId, sessionId, data); const orderedFields = [ - 'name', 'date', 'days', 'time', @@ -796,8 +786,6 @@ function buildSummaryRowsForEdit(draft, siteId, sessionId, data) { function hasEditFieldChanged(original, draft, field) { switch (field) { - case 'name': - return String(original?.label || '') !== String(draft?.name || ''); case 'date': if (draft?.type === 'Clinic series') { return String(original?.startDate || '') !== String(draft?.startDate || '') @@ -835,28 +823,64 @@ function hasEditFieldChanged(original, draft, field) { } } -function buildChangedRowsForEdit(original, draft, state, siteId, sessionId, data) { - const rowsByField = buildSummaryRowMap(draft, siteId, sessionId, data); +function buildChangedFieldKeysForEdit(original, draft, state) { const editableFields = currentEditableFields(state); const candidateFields = editableFields.length > 0 ? editableFields - : ['name', 'date', 'days', 'time', 'capacity', 'duration', 'services', 'closures']; + : ['date', 'days', 'time', 'capacity', 'duration', 'services', 'closures']; - const changedRows = candidateFields - .filter((field) => rowsByField[field] && hasEditFieldChanged(original, draft, field)) - .map((field) => rowsByField[field]); + const changedFields = candidateFields + .filter((field) => hasEditFieldChanged(original, draft, field)); - if (changedRows.length > 0) { - return changedRows; + if (changedFields.length > 0) { + return changedFields; } if (editableFields.length > 0) { - return editableFields - .filter((field) => rowsByField[field]) - .map((field) => rowsByField[field]); + return editableFields; + } + + return candidateFields; +} + +function childSessionUnaffectedByField(childSession, field) { + switch (field) { + case 'time': + return Boolean(childSession?.from && childSession?.until); + case 'capacity': + return childSession?.capacity !== undefined && childSession?.capacity !== null; + case 'services': + return asArray(childSession?.services).length > 0; + default: + return false; } +} - return buildSummaryRowsForEdit(draft, siteId, sessionId, data); +function formatShortDate(dateISO) { + const dt = DateTime.fromISO(dateISO || ''); + return dt.isValid ? dt.toFormat('d MMM yyyy') : String(dateISO || ''); +} + +function buildUnaffectedChildClinicDates(model, changedFields, siteId) { + const childSessions = asArray(model?.childSessions).filter((childSession) => childSession?.date); + if (childSessions.length === 0) return []; + + const changed = asArray(changedFields).filter(Boolean); + if (changed.length === 0) return []; + + const merged = mergeDailyAvailability({}, String(siteId || ''), { [model.id]: model }); + + const unaffectedDates = childSessions + .filter((childSession) => changed.every((field) => childSessionUnaffectedByField(childSession, field))) + .map((childSession) => childSession.date) + .filter((dateISO) => { + const sessions = merged?.[dateISO]?.sessions || []; + return sessions.some((session) => String(session?.recurringId) === String(model?.id)); + }); + + return Array.from(new Set(unaffectedDates)) + .sort((a, b) => String(a).localeCompare(String(b))) + .map((dateISO) => formatShortDate(dateISO)); } function reviewBackPath(siteId, sessionId, state) { @@ -889,107 +913,8 @@ function prepareReviewAfterEdit(data, siteId, state) { ? `${editSummaryPath(siteId, state.sessionId)}/affected-bookings` : `${editSummaryPath(siteId, state.sessionId)}/check-answers`; - const warning = buildChildOverrideWarningContext( - state, - originalModel, - updatedModel - ); - - state.childOverrideWarning = warning; - state.postWarningPath = warning ? nextPath : null; - setEditState(data, state); - return warning - ? `${editSummaryPath(siteId, state.sessionId)}/child-clinic-overrides` - : nextPath; -} - -function buildChildOverrideWarningContext(state, originalModel, updatedModel) { - if (state?.draft?.type !== 'Clinic series') return null; - - const editableFields = currentEditableFields(state); - const modelChanges = { - time: String(originalModel?.from || '') !== String(updatedModel?.from || '') - || String(originalModel?.until || '') !== String(updatedModel?.until || ''), - capacity: (Number(originalModel?.capacity) || 1) !== (Number(updatedModel?.capacity) || 1), - services: !servicesEqual(originalModel?.services, updatedModel?.services) - }; - - let targetedFields = ['time', 'capacity', 'services'].filter((field) => editableFields.includes(field)); - if (targetedFields.length === 0) { - targetedFields = Object.entries(modelChanges) - .filter(([, changed]) => changed) - .map(([field]) => field); - } - - if (targetedFields.length === 0) return null; - - const changedFields = targetedFields.filter((field) => modelChanges[field]); - - if (changedFields.length === 0) return null; - - const parentServices = asArray(updatedModel?.services); - const parentLabel = String(updatedModel?.label || '').trim() || 'Clinic series'; - const rows = []; - - for (const child of asArray(updatedModel?.childSessions)) { - if (!child?.date) continue; - - const childHasPairedTime = Boolean(child?.from && child?.until); - const resolvedFrom = childHasPairedTime ? child.from : updatedModel?.from; - const resolvedUntil = childHasPairedTime ? child.until : updatedModel?.until; - const hasTimeOverride = childHasPairedTime - && (String(resolvedFrom) !== String(updatedModel?.from || '') - || String(resolvedUntil) !== String(updatedModel?.until || '')); - - const hasCapacityOverride = child?.capacity !== undefined - && child?.capacity !== null; - - const effectiveServices = child?.services - ? applyServiceOperations(parentServices, child.services) - : [...parentServices]; - const hasServicesOverride = asArray(child?.services).length > 0; - - const include = ( - (changedFields.includes('time') && hasTimeOverride) - || (changedFields.includes('capacity') && hasCapacityOverride) - || (changedFields.includes('services') && hasServicesOverride) - ); - - if (!include) continue; - - rows.push({ - dateISO: child.date, - parentLabel, - childLabel: child?.label || '', - from: resolvedFrom, - until: resolvedUntil, - capacity: Number(child?.capacity ?? updatedModel?.capacity) || 1, - effectiveServiceIds: asArray(effectiveServices) - }); - } - - if (rows.length === 0) return null; - - rows.sort((a, b) => String(a.dateISO).localeCompare(String(b.dateISO))); - - const originalParentServiceIds = sortedStringArray(originalModel?.services); - const updatedParentServiceIds = sortedStringArray(updatedModel?.services); - const parentServicesAddedIds = updatedParentServiceIds - .filter((serviceId) => !originalParentServiceIds.includes(serviceId)); - const parentServicesRemovedIds = originalParentServiceIds - .filter((serviceId) => !updatedParentServiceIds.includes(serviceId)); - - return { - count: rows.length, - hasTimeChange: changedFields.includes('time'), - hasCapacityChange: changedFields.includes('capacity'), - hasServicesChange: changedFields.includes('services'), - parentServicesAddedIds, - parentServicesRemovedIds, - rows, - changedFields - }; + return nextPath; } function calculateAffectedBookings(originalModel, updatedModel, siteBookings) { @@ -1211,7 +1136,7 @@ function slotMatchesSession(slot, session) { && (!slot?.recurringSessionId || !session?.recurringId || String(slot.recurringSessionId) === String(session.recurringId)); } -function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, servicesById, siteBookings, siteId, today, recurringSessionsById = {}) { +function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, servicesById, siteBookings, siteId, today, recurringSessionsById = {}, backHref = null) { return week.map((day) => { const sessions = sortSessionsForAvailability(dailyAvailability?.[day]?.sessions); const dateSlots = asArray(slotsByDate?.[day]); @@ -1222,6 +1147,10 @@ function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, serv const totalSlots = sessionSlots.length; const resolvedLabel = session.label || recurringSessionsById?.[session?.recurringId]?.label || ''; + const changeHrefBase = day < today || !session?.recurringId + ? null + : `/site/${siteId}/change/session/${session.id}`; + return { id: session.id, label: resolvedLabel, @@ -1238,7 +1167,9 @@ function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, serv })), bookedTotal, unbookedTotal: Math.max(0, totalSlots - bookedTotal), - actionHref: day < today || !session?.recurringId ? null : `/site/${siteId}/change/session/${session.id}` + actionHref: changeHrefBase + ? `${changeHrefBase}${backHref ? `?back=${encodeURIComponent(backHref)}` : ''}` + : null }; }); @@ -1299,7 +1230,8 @@ function buildMonthAvailabilitySummary(weekRanges, dailyAvailability, slotsByDat siteBookings, siteId, today, - recurringSessionsById + recurringSessionsById, + null ); const services = new Map(); @@ -1822,41 +1754,6 @@ router.all('/site/:id/clinics/edit/:sessionId/affected-bookings', (req, res) => }); }); -router.all('/site/:id/clinics/edit/:sessionId/child-clinic-overrides', (req, res) => { - const data = req.session.data; - const state = ensureEditStateForSession(data, req.site_id, req.params.sessionId); - if (!state) { - return res.redirect(`/site/${req.site_id}/clinics`); - } - - const warning = state.childOverrideWarning; - const nextPath = state.postWarningPath - || (asArray(state.affectedBookingIds).length > 0 - ? `${editSummaryPath(req.site_id, req.params.sessionId)}/affected-bookings` - : `${editSummaryPath(req.site_id, req.params.sessionId)}/check-answers`); - - if (!warning || !asArray(warning.rows).length) { - return res.redirect(nextPath); - } - - if (req.method === 'POST') { - return res.redirect(nextPath); - } - - const step = state.currentEditStep || editStepForField(state.currentEditField, state?.draft?.type === 'Clinic series'); - const backHref = step - ? editStepPath(req.site_id, req.params.sessionId, step) - : editSummaryPath(req.site_id, req.params.sessionId); - - return res.render('site/clinics/edit/child-clinic-overrides', { - sessionId: req.params.sessionId, - backHref, - formAction: `${editSummaryPath(req.site_id, req.params.sessionId)}/child-clinic-overrides`, - warning: warning, - rows: warning.rows - }); -}); - router.all('/site/:id/clinics/edit/:sessionId/check-answers', (req, res) => { const data = req.session.data; const state = ensureEditStateForSession(data, req.site_id, req.params.sessionId); @@ -1866,6 +1763,10 @@ router.all('/site/:id/clinics/edit/:sessionId/check-answers', (req, res) => { if (req.method === 'POST') { const updatedModel = draftToModel(state.draft); + const changedFields = buildChangedFieldKeysForEdit(state.original, state.draft, state); + const unaffectedChildClinics = state.draft.type === 'Clinic series' + ? buildUnaffectedChildClinicDates(updatedModel, changedFields, req.site_id) + : []; persistRecurringSession(data, req.site_id, updatedModel); const siteBookings = data?.bookings?.[req.site_id] || {}; @@ -1881,7 +1782,8 @@ router.all('/site/:id/clinics/edit/:sessionId/check-answers', (req, res) => { siteId: req.site_id, sessionId: req.params.sessionId, isSeries: state.draft.type === 'Clinic series', - cancelledBookingsSummary + cancelledBookingsSummary, + unaffectedChildClinics }); applyAffectedBookingAction(siteBookings, state.affectedBookingIds, state.bookingAction); @@ -1892,7 +1794,20 @@ router.all('/site/:id/clinics/edit/:sessionId/check-answers', (req, res) => { return res.render('site/clinics/edit/check-answers', { sessionId: req.params.sessionId, isSeries: state.draft.type === 'Clinic series', - rows: buildChangedRowsForEdit(state.original, state.draft, state, req.site_id, req.params.sessionId, data), + rowFields: buildChangedFieldKeysForEdit(state.original, state.draft, state), + draft: state.draft, + previous: { + startDate: state.original?.startDate, + endDate: state.original?.endDate, + days: asArray(state.original?.recurrencePattern?.byDay), + from: state.original?.from, + until: state.original?.until, + capacity: String(Number(state.original?.capacity) || 1), + duration: String(Number(state.original?.slotLength) || 10), + services: asArray(state.original?.services), + closuresCount: asArray(state.original?.closures).length + }, + checkAnswersMode: 'clinic-edit', affectedCount: asArray(state.affectedBookingIds).length, bookingAction: state.bookingAction, backHref: reviewBackPath(req.site_id, req.params.sessionId, state) @@ -1928,7 +1843,8 @@ router.get('/site/:id/clinics/edit/:sessionId/success', (req, res) => { return res.render('site/clinics/edit/success', { sessionId: req.params.sessionId, - cancelSummary + cancelSummary, + unaffectedChildClinics: matchingSuccessState?.unaffectedChildClinics || [] }); }); @@ -2273,13 +2189,91 @@ router.get('/site/:id/debug/recurring-expansion', (req, res) => { // VIEW AVAILABILITY // ----------------------------------------------------------------------------- router.get('/site/:id/availability/day', (req, res) => { + const data = req.session.data; + const site_id = req.site_id; const date = req.query.date || getToday(); + const today = getToday(); + const tomorrow = DateTime.fromISO(date).plus({ days: 1 }).toISODate(); + const yesterday = DateTime.fromISO(date).minus({ days: 1 }).toISODate(); + const daySummary = buildWeekAvailabilitySummary( + [date], + res.locals.dailyAvailability, + res.locals.slots, + data.services || {}, + data?.bookings?.[site_id] || {}, + site_id, + today, + data?.recurring_sessions?.[site_id] || {}, + `/site/${site_id}/availability/day?date=${date}` + )[0] || { + date, + sessions: [], + totalAppointments: 0, + bookedAppointments: 0, + unbookedAppointments: 0, + isToday: date === today, + isPast: date < today, + dayViewHref: `/site/${site_id}/availability/day?date=${date}` + }; + + if ((daySummary.sessions || []).length === 0 && (res.locals.dailyAvailability?.[date]?.sessions || []).length > 0) { + const dateSlots = asArray(res.locals.slots?.[date]); + const sessions = sortSessionsForAvailability(res.locals.dailyAvailability?.[date]?.sessions); + const siteBookings = data?.bookings?.[site_id] || {}; + const servicesById = data.services || {}; + + const fallbackSessions = sessions.map((session) => { + const sessionSlots = dateSlots.filter((slot) => ( + (slot?.sessionId && session?.id && String(slot.sessionId) === String(session.id)) + || ( + !slot?.sessionId + && slot?.group?.start === session?.from + && slot?.group?.end === session?.until + && (!slot?.recurringSessionId || !session?.recurringId || String(slot.recurringSessionId) === String(session.recurringId)) + ) + )); + + const bookedTotal = sessionSlots.filter((slot) => slot?.booking_status === 'scheduled').length; + const totalSlots = sessionSlots.length; + const resolvedLabel = session.label || data?.recurring_sessions?.[site_id]?.[session?.recurringId]?.label || ''; + + return { + id: session.id, + label: resolvedLabel, + from: session.from, + until: session.until, + services: asArray(session.services).map((serviceId) => ({ + id: serviceId, + name: servicesById?.[serviceId]?.name || serviceId, + bookedCount: sessionSlots.filter((slot) => ( + slot?.booking_status === 'scheduled' + && slot?.booking_id + && siteBookings?.[slot.booking_id]?.service === serviceId + )).length + })), + bookedTotal, + unbookedTotal: Math.max(0, totalSlots - bookedTotal), + actionHref: date < today || !session?.recurringId + ? null + : `/site/${site_id}/change/session/${session.id}?back=${encodeURIComponent(`/site/${site_id}/availability/day?date=${date}`)}` + }; + }); + + daySummary.sessions = fallbackSessions; + daySummary.totalAppointments = fallbackSessions.reduce((sum, session) => sum + session.bookedTotal + session.unbookedTotal, 0); + daySummary.bookedAppointments = fallbackSessions.reduce((sum, session) => sum + session.bookedTotal, 0); + daySummary.unbookedAppointments = Math.max(0, daySummary.totalAppointments - daySummary.bookedAppointments); + } res.render('site/availability/day', { date, - today: getToday(), - tomorrow: DateTime.fromISO(date).plus({ days: 1 }).toISODate(), - yesterday: DateTime.fromISO(date).minus({ days: 1 }).toISODate() + today, + tomorrow, + yesterday, + daySummary, + dayHeading: getRelativeDayLabel(date, today), + previousDayLabel: getRelativeDayLabel(yesterday, today), + nextDayLabel: getRelativeDayLabel(tomorrow, today) }); }); @@ -2315,7 +2309,8 @@ router.get('/site/:id/availability/week', (req, res) => { data?.bookings?.[site_id] || {}, site_id, today, - data?.recurring_sessions?.[site_id] || {} + data?.recurring_sessions?.[site_id] || {}, + `/site/${site_id}/availability/week?date=${startFromDate}` ); res.render('site/availability/week', { @@ -2324,7 +2319,10 @@ router.get('/site/:id/availability/week', (req, res) => { week, weekDays, previousWeek, - nextWeek + nextWeek, + weekHeading: getRelativeWeekLabel(week[0], today), + previousWeekLabel: getRelativeWeekLabel(previousWeek.start, today), + nextWeekLabel: getRelativeWeekLabel(nextWeek.start, today) }); }); @@ -2349,7 +2347,10 @@ router.get('/site/:id/availability/month', (req, res) => { currentDate: monthData.currentDate, previousMonthDate: monthData.previousMonthDate, nextMonthDate: monthData.nextMonthDate, - monthWeeks + monthWeeks, + monthHeading: getRelativeMonthLabel(monthData.currentDate, today), + previousMonthLabel: getRelativeMonthLabel(monthData.previousMonthDate, today), + nextMonthLabel: getRelativeMonthLabel(monthData.nextMonthDate, today) }); }); diff --git a/app/routes/change-session.js b/app/routes/change-session.js index 6a1c027..0e23807 100644 --- a/app/routes/change-session.js +++ b/app/routes/change-session.js @@ -107,6 +107,14 @@ function changeStepPath(siteId, itemId, step) { return `${changeSummaryPath(siteId, itemId)}/${step}`; } +function normalizeBackHref(back) { + if (typeof back !== 'string') return null; + const value = back.trim(); + if (!value.startsWith('/')) return null; + if (value.startsWith('//')) return null; + return value; +} + function changeFieldToStep(field) { switch (field) { case 'name': @@ -365,20 +373,16 @@ function hasFieldChanged(state, field) { } } -function buildChangedRowsForCheckAnswers(state, siteId, itemId, data) { - const rowMap = buildSummaryRowMap(state.draft, siteId, itemId, data); +function buildChangedFieldKeysForCheckAnswers(state) { const editableFields = currentEditableFields(state); - const rows = editableFields - .filter((field) => rowMap[field] && hasFieldChanged(state, field)) - .map((field) => rowMap[field]); + const changedFields = editableFields + .filter((field) => hasFieldChanged(state, field)); - if (rows.length > 0) { - return rows; + if (changedFields.length > 0) { + return changedFields; } - return editableFields - .filter((field) => rowMap[field]) - .map((field) => rowMap[field]); + return editableFields; } function normalizeIsoMinute(datetimeISO) { @@ -549,9 +553,14 @@ function ensureChangeStateForSession(req, res) { const data = req.session.data; const siteId = req.site_id; const itemId = req.params.itemId; + const requestedBackHref = normalizeBackHref(req.query?.back); const existing = getChangeState(data); if (existing && existing.siteId === siteId && existing.itemId === itemId) { + if (requestedBackHref) { + existing.returnTo = requestedBackHref; + setChangeState(data, existing); + } return existing; } @@ -570,6 +579,7 @@ function ensureChangeStateForSession(req, res) { originalParent: clone(parentModel), originalChild: clone(originalChild), draft: buildChildDraft(parentModel, target.session, target.date), + returnTo: requestedBackHref || null, currentEditStep: null, bookingAction: null, affectedBookingIds: [] @@ -601,7 +611,7 @@ router.get('/site/:id/change/:type/:itemId', (req, res) => { draft: state.draft, date: state.date, displayDate: formatDisplayDate(state.date), - weekHref: weekViewHref(req.site_id, state.date) + weekHref: state.returnTo || weekViewHref(req.site_id, state.date) }); }); @@ -800,7 +810,16 @@ router.all('/site/:id/change/session/:itemId/check-answers', (req, res) => { pageName: 'Check your answers', sessionId: req.params.itemId, isSeries: false, - rows: buildChangedRowsForCheckAnswers(state, req.site_id, req.params.itemId, data), + rowFields: buildChangedFieldKeysForCheckAnswers(state), + draft: state.draft, + previous: { + name: state.draft.parentName, + from: state.draft.parentFrom, + until: state.draft.parentUntil, + capacity: String(state.draft.parentCapacity), + services: asArray(state.draft.parentServices) + }, + checkAnswersMode: 'session-change', affectedCount: asArray(state.affectedBookingIds).length, bookingAction: state.bookingAction, formAction: `${changeSummaryPath(req.site_id, req.params.itemId)}/check-answers`, diff --git a/app/views/components/action-control-bar.njk b/app/views/components/action-control-bar.njk new file mode 100644 index 0000000..107aae4 --- /dev/null +++ b/app/views/components/action-control-bar.njk @@ -0,0 +1,29 @@ +{% from 'nhsuk/components/button/macro.njk' import button %} + +{% macro appActionControlBar(params) %} + {% set siteId = params.siteId %} + {% set createClinicHref = params.createClinicHref or ('/site/' ~ siteId ~ '/clinics/type-of-clinc?new=1') %} + {% set cancelDateRangeHref = params.cancelDateRangeHref or ('/site/' ~ siteId ~ '/cancel-availability/dates') %} + +
+ {{ button({ + text: 'Create clinics', + classes: 'nhsuk-button--small', + href: createClinicHref + }) }} + + {{ button({ + text: 'Cancel a date range', + classes: 'nhsuk-button--secondary nhsuk-button--small', + href: cancelDateRangeHref + }) }} + + {% if params.print %} + {{ button({ + text: 'Print page', + classes: 'nhsuk-button--secondary nhsuk-button--small', + href: params.print + }) }} + {% endif %} +
+{% endmacro %} \ No newline at end of file diff --git a/app/views/components/app-card.njk b/app/views/components/app-card.njk index 683bc8c..9576df4 100644 --- a/app/views/components/app-card.njk +++ b/app/views/components/app-card.njk @@ -1,4 +1,5 @@ {%- from "nhsuk/macros/attributes.njk" import nhsukAttributes -%} +{%- from "nhsuk/components/summary-list/macro.njk" import summaryList -%} {%- macro appCard(params) -%}
{%- if params.title or params.action %} @@ -22,30 +23,31 @@ {%- if params.html %} {{ params.html | safe }} {%- elif params.items %} - - + {% set rows = [] %} {%- for item in params.items %} {%- if item %} - - - - + {% set rowKey = { text: item.key } %} + {% if item.keyHtml %} + {% set rowKey = { html: item.keyHtml } %} + {% endif %} + + {% set rowValue = { text: item.value } %} + {% if item.html %} + {% set rowValue = { html: item.html } %} + {% endif %} + + {% set rows = rows.concat([{ + key: rowKey, + value: rowValue, + actions: item.actions + }]) %} {%- endif %} {%- endfor %} - -
- {%- if item.keyHtml %} - {{ item.keyHtml | safe }} - {%- else %} - {{ item.key }} - {%- endif %} - - {%- if item.html %} - {{ item.html | safe }} - {%- else %} - {{ item.value }} - {%- endif %} -
+ + {{ summaryList({ + classes: 'app-card__summary-list', + rows: rows + }) }} {%- endif %}
diff --git a/app/views/site/availability/day.html b/app/views/site/availability/day.html index ed7a084..678db21 100644 --- a/app/views/site/availability/day.html +++ b/app/views/site/availability/day.html @@ -1,7 +1,8 @@ {% extends '../../layouts/layout.html' %} {% from '../../components/secondary-navigation.njk' import appSecondaryNavigation %} +{% from '../../components/action-control-bar.njk' import appActionControlBar %} -{% set pageName = date | formatDate('EEEE, d LLLL yyyy') %} +{% set pageName = dayHeading or (date | formatDate('EEEE, d LLLL yyyy')) %} {% block content %} @@ -24,37 +25,25 @@ href: '/site/' ~ site_id ~ '/availability/month' }, { - text: 'Clinics', - href: '/site/' ~ site_id ~ '/clinics', - primary: true - } if features.allAvailability + text: 'Clinics list', + href: '/site/' ~ site_id ~ '/clinics' + } ] }) }} - - {% if features.cancelDateRange %} - {% include "../../includes/addCancelButtons.njk" %} - {% endif %} -

{{ pageName }}

+

{{ pageName }}

+ {{ appActionControlBar({ siteId: site_id, print: '#' }) }} {{ pagination({ previous: { - labelText: yesterday | formatDate('EEE, d LLL'), + labelText: previousDayLabel or (yesterday | formatDate('EEE, d LLL')), href: "/site/"~site_id~"/availability/day?date="~yesterday }, next: { - labelText: tomorrow | formatDate('EEE, d LLL'), + labelText: nextDayLabel or (tomorrow | formatDate('EEE, d LLL')), href: "/site/"~site_id~"/availability/day?date="~tomorrow } }) }} - - - {% macro orphanNote(count) -%} - {% set isAre = 'is' if count == 1 else 'are' %} - {% set appointmentAppointments = 'appointment' if count == 1 else 'appointments' %} - {% set itThem = 'it' if count == 1 else 'them' %} - {{ count }} booked {{appointmentAppointments}} {{isAre}} still scheduled until you cancel {{itThem}}. - {%- endmacro %} {% macro appointmentForToday(opts) %} {# calculate if there's any booked appointments of this type #} @@ -68,7 +57,6 @@

{{ pageName }}

{{ opts.title }}

{% if hasBookedAppointments %} - Print {{ opts.title | lower }} @@ -89,10 +77,6 @@

{{ opts.title }}

{% for slot in slots[date] %} {% if slot.booking_status == opts.status or slot.booking_status in opts.status %} {% set booking = data.bookings[site_id][slot.booking_id] %} - {% set clinicName = 'Clinic' %} - {% if data.recurring_sessions and data.recurring_sessions[site_id] and slot.recurringSessionId and data.recurring_sessions[site_id][slot.recurringSessionId] %} - {% set clinicName = data.recurring_sessions[site_id][slot.recurringSessionId].label %} - {% endif %}
{{ booking.datetime | formatDate('h:mm a') | lower }} @@ -125,10 +109,67 @@

{{ opts.title }}

{% endmacro %} + {% macro clinicsRunningToday() %} + {% set sessions = daySummary.sessions or [] %} + + {% if sessions | length %} + + + + + + + + + + + + {% for session in sessions %} + + + + + + + + {% endfor %} + +
TimeServicesBookedUnbookedAction
+ {{ session.from | nhsTime }} to {{ session.until | nhsTime }} + + {% for service in session.services %} + {{ service.name }}{% if not loop.last %}
{% endif %} + {% endfor %} +
+ {% for service in session.services %} + {{ service.bookedCount }} booked{% if not loop.last %}
{% endif %} + {% endfor %} +
{{ session.unbookedTotal }} unbooked + {% if session.actionHref %} + Change + {% endif %} +
+ {% else %} +

No clinics running today.

+ {% endif %} + {% endmacro %} + + {% set daySlots = slots[date] or [] %} + {% set scheduledCount = 0 %} + {% set cancelledCount = 0 %} + {% set clinicsRunningCount = (daySummary.sessions or []) | length %} + {% for slot in daySlots %} + {% if slot.booking_status == 'scheduled' or slot.booking_status == 'orphaned' %} + {% set scheduledCount = scheduledCount + 1 %} + {% elif slot.booking_status == 'cancelled' %} + {% set cancelledCount = cancelledCount + 1 %} + {% endif %} + {% endfor %} + {{ tabs({ items: [ { - label: "Scheduled", + label: "Scheduled (" ~ scheduledCount ~ ")", id: "scheduled", panel: { html: appointmentForToday({ @@ -139,7 +180,7 @@

{{ opts.title }}

} }, { - label: "Cancelled", + label: "Cancelled (" ~ cancelledCount ~ ")", id: "cancelled", panel: { html: appointmentForToday({ @@ -147,6 +188,13 @@

{{ opts.title }}

title: 'Cancelled appointments' }) } + }, + { + label: "Clinics running today (" ~ clinicsRunningCount ~ ")", + id: "clinics-running-today", + panel: { + html: clinicsRunningToday() + } } ] }) }} diff --git a/app/views/site/availability/month.html b/app/views/site/availability/month.html index 6030c7b..7501d00 100644 --- a/app/views/site/availability/month.html +++ b/app/views/site/availability/month.html @@ -1,8 +1,9 @@ {% extends '../../layouts/layout.html' %} {% from '../../components/secondary-navigation.njk' import appSecondaryNavigation %} +{% from '../../components/action-control-bar.njk' import appActionControlBar %} {% from 'nhsuk/components/card/macro.njk' import card %} -{% set pageName = currentDate | formatDate("LLLL yyyy") %} +{% set pageName = monthHeading or (currentDate | formatDate("LLLL yyyy")) %} {% block content %}
@@ -24,27 +25,23 @@ current: true }, { - text: 'Clinics', - href: '/site/' ~ site_id ~ '/clinics', - primary: true - } if features.allAvailability + text: 'Clinics list', + href: '/site/' ~ site_id ~ '/clinics' + } ] }) }} - {% if features.cancelDateRange %} - {% include "../../includes/addCancelButtons.njk" %} - {% endif %} - -

{{ pageName }}

+

{{ pageName }}

+ {{ appActionControlBar({ siteId: site_id }) }} {{ pagination({ previous: { - labelText: previousMonthDate | formatDate('LLLL yyyy'), + labelText: previousMonthLabel or (previousMonthDate | formatDate('LLLL yyyy')), href: "/site/" ~ site_id ~ "/availability/month?date=" ~ previousMonthDate }, next: { - labelText: nextMonthDate | formatDate('LLLL yyyy'), + labelText: nextMonthLabel or (nextMonthDate | formatDate('LLLL yyyy')), href: "/site/" ~ site_id ~ "/availability/month?date=" ~ nextMonthDate } }) }} diff --git a/app/views/site/availability/week.html b/app/views/site/availability/week.html index 71e6460..f5417f3 100644 --- a/app/views/site/availability/week.html +++ b/app/views/site/availability/week.html @@ -1,8 +1,9 @@ {% extends '../../layouts/layout.html' %} {% from '../../components/secondary-navigation.njk' import appSecondaryNavigation %} +{% from '../../components/action-control-bar.njk' import appActionControlBar %} {% from 'nhsuk/components/card/macro.njk' import card %} -{% set pageName = week[0] | formatDate('d LLLL') ~ ' to ' ~ week[6] | formatDate('d LLLL yyyy') %} +{% set pageName = weekHeading or (week[0] | formatDate('d LLLL') ~ ' to ' ~ week[6] | formatDate('d LLLL yyyy')) %} {% block content %}
@@ -24,27 +25,24 @@ href: '/site/' ~ site_id ~ '/availability/month' }, { - text: 'Clinics', - href: '/site/' ~ site_id ~ '/clinics', - primary: true - } if features.allAvailability + text: 'Clinics list', + href: '/site/' ~ site_id ~ '/clinics' + } ] }) }} +

{{ pageName }}

+ {{ appActionControlBar({ siteId: site_id }) }} - {% if features.cancelDateRange %} - {% include "../../includes/addCancelButtons.njk" %} - {% endif %} - -

{{ pageName }}

+ {{ pagination({ previous: { - labelText: previousWeek.start | formatDate('d') ~ " to " ~ previousWeek.end | formatDate('d LLL'), + labelText: previousWeekLabel or (previousWeek.start | formatDate('d') ~ " to " ~ previousWeek.end | formatDate('d LLL')), href: "/site/" ~ site_id ~ "/availability/week?date=" ~ previousWeek.start }, next: { - labelText: nextWeek.start | formatDate('d') ~ " to " ~ nextWeek.end | formatDate('d LLL'), + labelText: nextWeekLabel or (nextWeek.start | formatDate('d') ~ " to " ~ nextWeek.end | formatDate('d LLL')), href: "/site/" ~ site_id ~ "/availability/week?date=" ~ nextWeek.start } }) }} @@ -62,7 +60,7 @@

{{ pageName }}

- + @@ -73,9 +71,6 @@

{{ pageName }}

{% for session in day.sessions %}
Name and timeTime Services Booked Unbooked
- {% if session.label %} - {{ session.label }}
- {% endif %} {{ session.from | nhsTime }} to {{ session.until | nhsTime }}
@@ -111,6 +106,10 @@

{{ pageName }}

+

+ View day +

+ {% else %}

No appointments.

{% endif %} diff --git a/app/views/site/cancel-availability/dates.html b/app/views/site/cancel-availability/dates.html index f2caa18..c4a549f 100644 --- a/app/views/site/cancel-availability/dates.html +++ b/app/views/site/cancel-availability/dates.html @@ -4,7 +4,7 @@ {% block beforeContent %} {{ backLink({ - href: '/site/' ~ site_id ~ '/cancel-availability' + href: '/site/' ~ site_id ~ '/availability/day' }) }} {% endblock %} diff --git a/app/views/site/change-session/summary.html b/app/views/site/change-session/summary.html index a00e3db..7332d2b 100644 --- a/app/views/site/change-session/summary.html +++ b/app/views/site/change-session/summary.html @@ -31,21 +31,6 @@ {{ displayDate }}

{{ pageName }}

- {{ appCard({ - title: 'Clinic details', - action: { - href: changeBasePath ~ '/details', - text: 'Change', - visuallyHiddenText: ' clinic name' - }, - items: [ - { - key: 'Clinic name', - value: draft.name or 'Not provided' - } - ] - }) }} - {{ appCard({ title: 'Capacity calculation', action: { diff --git a/app/views/site/clinics/clinics.html b/app/views/site/clinics/clinics.html index 088f4e8..6d00575 100644 --- a/app/views/site/clinics/clinics.html +++ b/app/views/site/clinics/clinics.html @@ -1,18 +1,30 @@ {% extends '../../layouts/layout.html' %} +{% from '../../components/secondary-navigation.njk' import appSecondaryNavigation %} +{% from '../../components/action-control-bar.njk' import appActionControlBar %} -{% set pageName = "Clinics" %} +{% set pageName = "Clinics list" %} {% macro services(service_ids) %} {% set serviceTypes = [] %} {% for service_id in service_ids %} - {% set vaccineShortcode = data.services[service_id].cohort.type ~ " " ~ data.services[service_id].vaccine %} - {% if vaccineShortcode not in serviceTypes %} - {% set serviceTypes = serviceTypes.concat([vaccineShortcode]) %} + {% if data.services[service_id] %} + {% set vaccineShortcode = data.services[service_id].cohort.type ~ " " ~ data.services[service_id].vaccine %} + {% if vaccineShortcode not in serviceTypes %} + {% set serviceTypes = serviceTypes.concat([vaccineShortcode]) %} + {% endif %} {% endif %} {% endfor %} - {{ serviceTypes | join(', ') }} + {% if serviceTypes | length > 0 %} +
    + {% for serviceType in serviceTypes %} +
  • {{ serviceType }}
  • + {% endfor %} +
+ {% else %} + No services + {% endif %} {% endmacro %} {% macro timeLabel(session) %} @@ -39,11 +51,6 @@ text: text, classes: "nhsuk-tag--" ~ colour }) }} - -{% endmacro %} - -{% macro daysLabel(session) %} - {{ (session.days or session.recurrencePattern.byDay or []) | shortWeekdays }} {% endmacro %} {% macro dateLabel(session) %} @@ -65,86 +72,95 @@ {% endif %} {% endmacro %} +{% macro clinicsTable(sessions, includeActions) %} + + + + + + + {% if includeActions %} + + {% endif %} + + + + {% for session in sessions %} + + + + + {% if includeActions %} + + {% endif %} + + {% endfor %} + +
TypeTime and dateServicesActions
{{ typeLabel(session) }}{{ timeLabel(session) }}
{{ dateLabel(session) }}
{{ services(session.services) }} + View and edit +
+{% endmacro %} {% block content %}
+ {{ + appSecondaryNavigation({ + items: [ + { + text: 'Day view', + href: '/site/' ~ site_id ~ '/availability/day' + }, + { + text: 'Week view', + href: '/site/' ~ site_id ~ '/availability/week' + }, + { + text: 'Month view', + href: '/site/' ~ site_id ~ '/availability/month' + }, + { + text: 'Clinics list', + href: '/site/' ~ site_id ~ '/clinics', + current: true + } + ] + }) + }}

{{ pageName }}

+ {{ appActionControlBar({ siteId: site_id }) }} - {{ button({ - text: "Create clinic", - classes: "nhsuk-button--secondary nhsuk-button--small", - href: "/site/" + site_id + "/clinics/type-of-clinc?new=1" - }) }} - -

Active clinics

+ {% set sessionHistory = sessionHistory or [] %} - - {% if sessionHistory | length == 0 %} - {{ insetText({ - text: "No clinics have been created yet." - }) }} - {% endif %} - - {% if sessionHistory | length > 0 %} - - - - - - - - - - - - {% for session in sessionHistory %} - - - - - - - - {% endfor %} - -
NameTypeDateTimesActions
{{ session.label or 'Clinic' }}{{ typeLabel(session) | safe }}{{ dateLabel(session) }}{{ timeLabel(session) }}Edit
Remove
- {% endif %} - -

Completed clinics

- {% set pastSessionHistory = pastSessionHistory or [] %} - {% if pastSessionHistory | length == 0 %} - {{ insetText({ - text: "No completed clinics" - }) }} - {% endif %} - - {% if pastSessionHistory | length > 0 %} - - - - - - - - - - - {% for session in pastSessionHistory %} - - - - - - - {% endfor %} - -
NameTypeDateTimes
{{ session.label or 'Clinic' }}{{ typeLabel(session) }}{{ dateLabel(session) }}{{ timeLabel(session) }}
- {% endif %} - + {{ tabs({ + items: [ + { + label: "Active clinics (" ~ (sessionHistory | length) ~ ")", + id: "active-clinics", + panel: { + html: ( + insetText({ text: "No clinics have been created yet." }) + if (sessionHistory | length) == 0 + else clinicsTable(sessionHistory, true) + ) + } + }, + { + label: "Completed clinics (" ~ (pastSessionHistory | length) ~ ")", + id: "completed-clinics", + panel: { + html: ( + insetText({ text: "No completed clinics" }) + if (pastSessionHistory | length) == 0 + else clinicsTable(pastSessionHistory, false) + ) + } + } + ] + }) }}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/views/site/clinics/edit/check-answers.html b/app/views/site/clinics/edit/check-answers.html index 27170d2..4d3edd0 100644 --- a/app/views/site/clinics/edit/check-answers.html +++ b/app/views/site/clinics/edit/check-answers.html @@ -9,11 +9,40 @@ {% block content %}
+ {% macro servicesSummaryHtml(serviceIds) %} + {% set serviceNames = [] %} + {% for serviceId in serviceIds or [] %} + {% set serviceNames = serviceNames.concat([data.services[serviceId].name if data.services[serviceId] else serviceId]) %} + {% endfor %} + + {% if serviceNames | length > 0 %} +
    + {% for serviceName in serviceNames %} +
  • {{ serviceName }}
  • + {% endfor %} +
+ {% else %} + None selected + {% endif %} + {% endmacro %} + + {% macro servicesText(serviceIds) %} + {% set serviceNames = [] %} + {% for serviceId in serviceIds or [] %} + {% set serviceNames = serviceNames.concat([data.services[serviceId].name if data.services[serviceId] else serviceId]) %} + {% endfor %} + {{ serviceNames | join(', ') if (serviceNames | length) > 0 else 'None selected' }} + {% endmacro %} + + {% macro withPrevious(currentHtml, previousText) %} + {{ currentHtml | safe }}
Previously, {{ previousText or 'Not set' }} + {% endmacro %} +

{{ pageName or 'Check your answers' }}

{% set resolvedBookingActionText = 'No booking action selected' %} {% if bookingAction == 'cancel' %} - {% set resolvedBookingActionText = 'Cancel appointments' %} + {% set resolvedBookingActionText = 'Cancel ' ~ affectedCount ~ ' affected appointment' ~ ('s' if affectedCount != 1 else '') %} {% elif bookingAction == 'orphan' %} {% set resolvedBookingActionText = 'Keep booked appointments' %} {% endif %} @@ -21,11 +50,116 @@

{{ pageName or 'Check your answers' }}

{% set resolvedButtonText = buttonText or 'Save changes' %} {% set resolvedButtonClasses = '' %} {% if bookingAction == 'cancel' %} - {% set resolvedButtonText = 'Save changes and cancel bookings' %} + {% set resolvedButtonText = 'Save changes and cancel appointments' %} {% set resolvedButtonClasses = 'nhsuk-button--warning' %} {% endif %} - {% set allRows = rows %} + {% set allRows = [] %} + {% if rowFields and draft %} + {% for field in rowFields %} + {% set row = null %} + + {% if checkAnswersMode == 'clinic-edit' %} + {% if field == 'date' %} + {% set dateText = draft.startDate | formatDate('d MMM yyyy') %} + {% if isSeries %} + {% set dateText = (draft.startDate | formatDate('d MMM yyyy')) + ' to ' + (draft.endDate | formatDate('d MMM yyyy') ) %} + {% endif %} + {% set previousDateText = previous.startDate | formatDate('d MMM yyyy') if previous and previous.startDate else 'Not set' %} + {% if isSeries and previous and previous.endDate %} + {% set previousDateText = (previous.startDate | formatDate('d MMM yyyy')) + ' to ' + (previous.endDate | formatDate('d MMM yyyy')) %} + {% endif %} + {% set row = { + key: { text: 'Dates' if isSeries else 'Date' }, + value: { html: withPrevious(dateText, previousDateText) }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/date', text: 'Change', visuallyHiddenText: ' dates' }] } + } %} + {% elif field == 'days' and isSeries %} + {% set previousDaysText = (previous.days or []) | join(', ') if previous else 'Not set' %} + {% set row = { + key: { text: 'Days' }, + value: { html: withPrevious((draft.days or []) | join(', '), previousDaysText) }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/days', text: 'Change', visuallyHiddenText: ' days' }] } + } %} + {% elif field == 'time' %} + {% set previousTimeText = ((previous.from | nhsTime) + ' to ' + (previous.until | nhsTime)) if previous and previous.from and previous.until else 'Not set' %} + {% set row = { + key: { text: 'Time' }, + value: { html: withPrevious((draft.from | nhsTime) + ' to ' + (draft.until | nhsTime), previousTimeText) }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/time', text: 'Change', visuallyHiddenText: ' time' }] } + } %} + {% elif field == 'capacity' %} + {% set row = { + key: { text: 'Vaccinators and capacity' }, + value: { html: withPrevious(draft.capacity, previous.capacity if previous else 'Not set') }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/capacity', text: 'Change', visuallyHiddenText: ' vaccinators and capacity' }] } + } %} + {% elif field == 'duration' %} + {% set previousDurationText = (previous.duration + ' minutes') if previous and previous.duration else 'Not set' %} + {% set row = { + key: { text: 'Appointment length' }, + value: { html: withPrevious(draft.duration + ' minutes', previousDurationText) }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/duration', text: 'Change', visuallyHiddenText: ' appointment length' }] } + } %} + {% elif field == 'services' %} + {% set currentServicesHtml = servicesSummaryHtml(draft.services) %} + {% set previousServicesText = servicesText(previous.services) if previous else 'Not set' %} + {% set row = { + key: { text: 'Services' }, + value: { html: withPrevious(currentServicesHtml, previousServicesText) }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/services', text: 'Change', visuallyHiddenText: ' services' }] } + } %} + {% elif field == 'closures' and isSeries %} + {% set closuresText = 'None added' %} + {% if draft.closures and (draft.closures | length) > 0 %} + {% set closuresText = (draft.closures | length) + ' added' %} + {% endif %} + {% set previousClosuresText = (previous.closuresCount + ' added') if previous and previous.closuresCount and previous.closuresCount > 0 else 'None added' %} + {% set row = { + key: { text: 'Clinic closures' }, + value: { html: withPrevious(closuresText, previousClosuresText) }, + actions: { items: [{ href: '/site/' + site_id + '/clinics/edit/' + sessionId + '/change/closures', text: 'Change', visuallyHiddenText: ' clinic closures' }] } + } %} + {% endif %} + {% elif checkAnswersMode == 'session-change' %} + {% if field == 'name' %} + {% set row = { + key: { text: 'Name' }, + value: { html: withPrevious(draft.name or 'Not provided', previous.name if previous else 'Not set') }, + actions: { items: [{ href: '/site/' + site_id + '/change/session/' + sessionId + '/change/name', text: 'Change', visuallyHiddenText: ' name' }] } + } %} + {% elif field == 'time' %} + {% set previousTimeText = ((previous.from | nhsTime) + ' to ' + (previous.until | nhsTime)) if previous and previous.from and previous.until else 'Not set' %} + {% set row = { + key: { text: 'Time' }, + value: { html: withPrevious((draft.from | nhsTime) + ' to ' + (draft.until | nhsTime), previousTimeText) }, + actions: { items: [{ href: '/site/' + site_id + '/change/session/' + sessionId + '/change/time', text: 'Change', visuallyHiddenText: ' time' }] } + } %} + {% elif field == 'capacity' %} + {% set row = { + key: { text: 'Vaccinators' }, + value: { html: withPrevious(draft.capacity, previous.capacity if previous else 'Not set') }, + actions: { items: [{ href: '/site/' + site_id + '/change/session/' + sessionId + '/change/capacity', text: 'Change', visuallyHiddenText: ' vaccinators' }] } + } %} + {% elif field == 'services' %} + {% set currentServicesHtml = servicesSummaryHtml(draft.services) %} + {% set previousServicesText = servicesText(previous.services) if previous else 'Not set' %} + {% set row = { + key: { text: 'Services' }, + value: { html: withPrevious(currentServicesHtml, previousServicesText) }, + actions: { items: [{ href: '/site/' + site_id + '/change/session/' + sessionId + '/change/services', text: 'Change', visuallyHiddenText: ' services' }] } + } %} + {% endif %} + {% endif %} + + {% if row %} + {% set allRows = allRows.concat([row]) %} + {% endif %} + {% endfor %} + {% else %} + {% set allRows = rows or [] %} + {% endif %} + {% if affectedCount > 0 %} {% set allRows = allRows.concat([{ key: { text: 'Booked appointments' }, @@ -41,6 +175,7 @@

{{ pageName or 'Check your answers' }}

{% endif %} {{ summaryList({ rows: allRows }) }} +
{{ button({ diff --git a/app/views/site/clinics/edit/child-clinic-overrides.html b/app/views/site/clinics/edit/child-clinic-overrides.html deleted file mode 100644 index c8f26e6..0000000 --- a/app/views/site/clinics/edit/child-clinic-overrides.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends '../../../layouts/layout.html' %} - -{% block beforeContent %} -{{ backLink({ - href: backHref -}) }} -{% endblock %} - -{% block content %} -
-
- {% set clinicWord = 'clinic' if warning.count == 1 else 'clinics' %} - {% set pronoun = 'this clinic' if warning.count == 1 else 'these clinics' %} - - {% set headingPrefix = 'Clinics' %} - {% if warning.hasTimeChange and not warning.hasCapacityChange and not warning.hasServicesChange %} - {% set headingPrefix = 'Clinic times' %} - {% elif warning.hasCapacityChange and not warning.hasTimeChange and not warning.hasServicesChange %} - {% set headingPrefix = 'Number of vaccinators' %} - {% elif warning.hasServicesChange and not warning.hasTimeChange and not warning.hasCapacityChange %} - {% set headingPrefix = 'Services' %} - {% endif %} - - Edit clinic series -

{{ warning.count }} {{ clinicWord }} in this series will not be updated

- - {% if warning.hasTimeChange %} -

You already edited the times for {{ pronoun }}. The start and end times for {{ pronoun }} will not be updated as part of the changes to the series.

- {% endif %} - - {% if warning.hasCapacityChange %} -

You already edited the number of vaccinators for {{ pronoun }}. The number of vaccinators for {{ pronoun }} will not be updated as part of the changes to the series.

- {% endif %} - - {% call details({ - summaryText: "View clinics that will not be updated", - classes: "nhsuk-expander" - }) %} - - - - - {% if warning.hasTimeChange %} - - {% endif %} - {% if warning.hasCapacityChange %} - - {% endif %} - {% if warning.hasServicesChange %} - - {% endif %} - - - - {% for row in rows %} - - - {% if warning.hasTimeChange %} - - {% endif %} - {% if warning.hasCapacityChange %} - - {% endif %} - {% if warning.hasServicesChange %} - - {% endif %} - - {% endfor %} - -
DateTimesVaccinatorsServices this clinic will have
- {{ row.dateISO | nhsDate }} - {{ row.from }} to {{ row.until }}{{ row.capacity }} - {% if row.effectiveServiceIds and row.effectiveServiceIds.length %} -
    - {% for serviceId in row.effectiveServiceIds %} -
  • {{ data.services[serviceId].name if data.services[serviceId] else serviceId }}
  • - {% endfor %} -
- {% else %} - None selected - {% endif %} -
- {% endcall %} - - - - {{ button({ text: 'Continue' }) }} - -
-
-{% endblock %} diff --git a/app/views/site/clinics/edit/success.html b/app/views/site/clinics/edit/success.html index b5ac4be..fe8478f 100644 --- a/app/views/site/clinics/edit/success.html +++ b/app/views/site/clinics/edit/success.html @@ -15,24 +15,48 @@

{{ cancelSummary.unnotifiedCount }} {{ 'person' if c

View the list of people who have not been notified

{% endif %} -

What would you like to do next?

+ {% if unaffectedChildClinics and unaffectedChildClinics.length %} + {% set unaffectedCount = unaffectedChildClinics.length %} +

{{ unaffectedCount }} {{ 'clinic has' if unaffectedCount == 1 else 'clinics have' }} not been updated

+
    + {% for clinicDate in unaffectedChildClinics %} +
  • {{ clinicDate }}
  • + {% endfor %} +
+ {% endif %} - {% for action in cancelSummary.nextActions %} -

{{ action.text }}

- {% endfor %} +

What would you like to do next?

+
    + {% for action in cancelSummary.nextActions %} +
  • {{ action.text }}
  • + {% endfor %} +
{% else %} {{ panel({ titleText: titleText or 'Clinic updated' }) }} - {% if primaryHref %} -

{{ primaryText or 'Continue' }}

- {% elif sessionId %} -

Make another change

- {% endif %} - {% if secondaryHref %} -

{{ secondaryText }}

- {% elif not primaryHref or sessionId %} -

Back to clinics

+ {% if unaffectedChildClinics and unaffectedChildClinics.length %} + {% set unaffectedCount = unaffectedChildClinics.length %} +

{{ unaffectedCount }} {{ 'clinic has' if unaffectedCount == 1 else 'clinics have' }} not been updated

+
    + {% for clinicDate in unaffectedChildClinics %} +
  • {{ clinicDate }}
  • + {% endfor %} +
{% endif %} +

What would you like to do next?

+ {% endif %}
diff --git a/app/views/site/clinics/edit/summary.html b/app/views/site/clinics/edit/summary.html index a371212..78c1b95 100644 --- a/app/views/site/clinics/edit/summary.html +++ b/app/views/site/clinics/edit/summary.html @@ -10,6 +10,95 @@ {% block content %}
+ {% macro oneOffChangesSummary(childSession) %} + {% set hasChange = false %} +
+ {% if childSession.from and childSession.until %} + {% set hasChange = true %} +
Times
+
{{ childSession.from | nhsTime }} to {{ childSession.until | nhsTime }}
+ {% endif %} + + {% if childSession.capacity %} + {% set hasChange = true %} +
Vaccinators
+
{{ childSession.capacity }}
+ {% endif %} + + {% if childSession.services and (childSession.services | length) > 0 %} + {% set hasChange = true %} +
Services
+
+ {% set baseServices = draft.services or [] %} + {% set effectiveServices = baseServices %} + {% set isReplaceMode = false %} + + {% if childSession.services[0] is string %} + {% set effectiveServices = childSession.services %} + {% set isReplaceMode = true %} + {% else %} + {% for operation in childSession.services %} + {% set opService = operation.service %} + {% if operation.operation == 'replace' %} + {% if not isReplaceMode %} + {% set effectiveServices = [] %} + {% set isReplaceMode = true %} + {% endif %} + {% if opService and (opService not in effectiveServices) %} + {% set effectiveServices = effectiveServices.concat([opService]) %} + {% endif %} + {% elif operation.operation == 'add' %} + {% if opService and (opService not in effectiveServices) %} + {% set effectiveServices = effectiveServices.concat([opService]) %} + {% endif %} + {% elif operation.operation == 'remove' %} + {% set nextServices = [] %} + {% for existingService in effectiveServices %} + {% if existingService != opService %} + {% set nextServices = nextServices.concat([existingService]) %} + {% endif %} + {% endfor %} + {% set effectiveServices = nextServices %} + {% endif %} + {% endfor %} + {% endif %} + + {% set serviceLines = [] %} + + {% if isReplaceMode %} + {% for serviceId in effectiveServices %} + {% set serviceName = data.services[serviceId].name if data.services[serviceId] else serviceId %} + {% set serviceLines = serviceLines.concat([serviceName]) %} + {% endfor %} + {% else %} + {% for serviceId in baseServices %} + {% set serviceName = data.services[serviceId].name if data.services[serviceId] else serviceId %} + {% if serviceId in effectiveServices %} + {% set serviceLines = serviceLines.concat([serviceName]) %} + {% else %} + {% set serviceLines = serviceLines.concat(['' + serviceName + ' (removed)']) %} + {% endif %} + {% endfor %} + + {% for serviceId in effectiveServices %} + {% if serviceId not in baseServices %} + {% set serviceName = data.services[serviceId].name if data.services[serviceId] else serviceId %} + {% set serviceLines = serviceLines.concat([serviceName + ' (added)']) %} + {% endif %} + {% endfor %} + {% endif %} + + {{ serviceLines | join('
') | safe }} +
+ {% endif %} + + {% if not hasChange %} +
No changes
+
No one-off changes recorded
+ {% endif %} +
+ {% endmacro %} + {% set editBasePath = '/site/' ~ site_id ~ '/clinics/edit/' ~ sessionId %} {% set isSeries = draft.type == 'Clinic series' %} {% set pageName = 'Edit clinic series' if isSeries else 'Edit single clinic' %} @@ -74,10 +163,6 @@

{{ pageName }}

visuallyHiddenText: ' clinic details' }, items: [ - { - key: 'Clinic name', - value: draft.name or 'Not provided' - }, { key: 'Start date', value: draft.startDate | nhsDate @@ -175,6 +260,66 @@

{{ pageName }}

}, items: closuresItems }) }} + + {% set oneOffChangesItems = [] %} + {% if draft.childSessions and draft.childSessions.length %} + {% for childSession in draft.childSessions %} + {% set includeOneOffChange = true %} + {% if not childSession.date or childSession.date < draft.startDate or childSession.date > draft.endDate %} + {% set includeOneOffChange = false %} + {% endif %} + + {% if includeOneOffChange and draft.closures and draft.closures.length %} + {% for closure in draft.closures %} + {% if closure.startDate and closure.endDate and childSession.date >= closure.startDate and childSession.date <= closure.endDate %} + {% set includeOneOffChange = false %} + {% endif %} + {% endfor %} + {% endif %} + + {% if includeOneOffChange %} + {% set childDate = childSession.date | formatDate('d MMM yyyy') %} + {% set childInstanceId = '' %} + {% set daySessions = dailyAvailability[childSession.date].sessions if dailyAvailability and dailyAvailability[childSession.date] and dailyAvailability[childSession.date].sessions else [] %} + {% for daySession in daySessions %} + {% if daySession.recurringId == sessionId and not childInstanceId %} + {% set childInstanceId = daySession.id %} + {% endif %} + {% endfor %} + + {% if childInstanceId %} + {% set backHref = editBasePath %} + {% set oneOffChangesItems = oneOffChangesItems.concat([ + { + key: childDate, + html: oneOffChangesSummary(childSession), + actions: { + items: [ + { + href: '/site/' + site_id + '/change/session/' + childInstanceId + '?back=' + (backHref | urlencode), + text: 'Change', + visuallyHiddenText: ' one-off changes for ' + childDate + } + ] + } + } + ]) %} + {% endif %} + {% endif %} + {% endfor %} + {% endif %} + + {% if oneOffChangesItems | length > 0 %} + {{ appCard({ + title: 'Clinics with changes', + items: oneOffChangesItems + }) }} + {% else %} + {{ appCard({ + title: 'Clinics with changes', + html: '

No clinics with one-off changes.

' + }) }} + {% endif %} {% endif %}
diff --git a/app/views/site/clinics/series/check-answers.html b/app/views/site/clinics/series/check-answers.html index 2baa14a..056c3fd 100644 --- a/app/views/site/clinics/series/check-answers.html +++ b/app/views/site/clinics/series/check-answers.html @@ -101,22 +101,6 @@

{{ pageName }}

{{ summaryList({ rows: [ { - key: { - text: "Name" - }, - value: { - text: data.newSession.name or "Not provided" - }, - actions: { - items: [ - { - href: "/site/" + site_id + "/clinics/details", - text: "Change", - visuallyHiddenText: " clinic name" - } - ] - } - }, { key: { text: "Dates" }, diff --git a/app/views/site/clinics/series/clinic-closures-form.html b/app/views/site/clinics/series/clinic-closures-form.html index bfd1010..9e6ff82 100644 --- a/app/views/site/clinics/series/clinic-closures-form.html +++ b/app/views/site/clinics/series/clinic-closures-form.html @@ -32,7 +32,7 @@

There is a probl {{ input({ label: { - text: "Name", + text: "Closure label", classes: "nhsuk-label--m" }, id: "closure-name", diff --git a/app/views/site/clinics/series/details.html b/app/views/site/clinics/series/details.html index 6fe3821..949e02c 100644 --- a/app/views/site/clinics/series/details.html +++ b/app/views/site/clinics/series/details.html @@ -18,20 +18,6 @@

{{ pageName }}

- {{ input({ - label: { - text: "Series name", - classes: "nhsuk-label--m" - }, - id: "clinic-name", - name: "newSession[name]", - classes: "nhsuk-input--width-20", - hint: { - text: "A short name to help you identify this clinic series. For example, weekly flu clinic" - }, - value: data.newSession.name - }) }} - {% if not hideDateFields %} {{ dateInput({ id: "start-date", diff --git a/app/views/site/clinics/series/success.html b/app/views/site/clinics/series/success.html index 40e87fb..8b4e8df 100644 --- a/app/views/site/clinics/series/success.html +++ b/app/views/site/clinics/series/success.html @@ -9,8 +9,7 @@ titleText: pageName }) }} -

What happens next

-

You can review and change this clinic from the appointments screens.

+

What would you like to do next?

{{ insetText({ text: 'People will now be able to book appointments in this clinic.' diff --git a/app/views/site/clinics/single/check-answers.html b/app/views/site/clinics/single/check-answers.html index 6683bce..4ea7a5c 100644 --- a/app/views/site/clinics/single/check-answers.html +++ b/app/views/site/clinics/single/check-answers.html @@ -69,22 +69,6 @@

{{ pageName }}

{{ summaryList({ rows: [ { - key: { - text: "Name" - }, - value: { - text: data.newSession.name or "Not provided" - }, - actions: { - items: [ - { - href: "/site/" + site_id + "/clinics/details", - text: "Change", - visuallyHiddenText: " clinic name" - } - ] - } - }, { key: { text: "Date" }, diff --git a/app/views/site/clinics/single/details.html b/app/views/site/clinics/single/details.html index e289096..cbf8c2e 100644 --- a/app/views/site/clinics/single/details.html +++ b/app/views/site/clinics/single/details.html @@ -18,20 +18,6 @@

{{ pageName }}

- {{ input({ - label: { - text: "Clinic name", - classes: "nhsuk-label--m" - }, - id: "clinic-name", - name: "newSession[name]", - classes: "nhsuk-input--width-20", - hint: { - text: "A short name to help you identify this clinic series. For example, weekly flu clinic" - }, - value: data.newSession.name - }) }} - {{ dateInput({ id: "single-date", namePrefix: "newSession[singleDate]", diff --git a/app/views/site/clinics/single/success.html b/app/views/site/clinics/single/success.html index 40e87fb..8b4e8df 100644 --- a/app/views/site/clinics/single/success.html +++ b/app/views/site/clinics/single/success.html @@ -9,8 +9,7 @@ titleText: pageName }) }} -

What happens next

-

You can review and change this clinic from the appointments screens.

+

What would you like to do next?

{{ insetText({ text: 'People will now be able to book appointments in this clinic.'