From 80c23da1d87fd5ab5fe8c1cc35061e246a64c61f Mon Sep 17 00:00:00 2001 From: roobottom Date: Mon, 16 Mar 2026 22:17:20 +0000 Subject: [PATCH 1/2] Add dedicated session break flow --- app/assets/sass/main.scss | 13 +- app/routes/bookings.js | 22 +- app/routes/sessions.js | 321 +++++++++++++++++++++ app/routes/shared/session-edit.js | 159 +++++++++- app/views/sites/bookings.html | 11 +- app/views/sites/session-break-check.html | 76 +++++ app/views/sites/session-break-form.html | 55 ++++ app/views/sites/session-break-success.html | 47 +++ 8 files changed, 686 insertions(+), 18 deletions(-) create mode 100644 app/views/sites/session-break-check.html create mode 100644 app/views/sites/session-break-form.html create mode 100644 app/views/sites/session-break-success.html diff --git a/app/assets/sass/main.scss b/app/assets/sass/main.scss index 7996968..2b345db 100755 --- a/app/assets/sass/main.scss +++ b/app/assets/sass/main.scss @@ -78,6 +78,7 @@ .app-table__free-row { color: $nhsuk-secondary-text-colour; + box-shadow: inset 4px 0 nhsuk-colour("yellow"); &, &:hover { background-color: nhsuk-colour("grey-5"); @@ -86,17 +87,7 @@ .app-table__break-row, .app-table__break-row:hover { - //background-color: #F0F0F0; -} - -.app-table__free-row--multi-slot>td { - //padding-top: nhsuk-spacing(5); - //padding-bottom: nhsuk-spacing(5); - box-shadow: - inset 0 -2px 0 0 white, - inset 0 -3px 0 0 $nhsuk-border-colour, - inset 0 -4px 0 0 white, - inset 0 -5px 0 0 white + background-color: #FFFDEF; } // Utilities diff --git a/app/routes/bookings.js b/app/routes/bookings.js index d1fadb2..1fe61ad 100644 --- a/app/routes/bookings.js +++ b/app/routes/bookings.js @@ -21,7 +21,27 @@ function bookings (router, shared) { const ctx = bookingsCtx(req, 'day') const sessions = getSessionsForDate(site, ctx.anchorDate) const scheduledSessions = sessions.map(({ availability: avail, slots }) => { - const scheduledRows = buildDisplayRows(slots, avail, data) + let breakIndex = 0 + const scheduledRows = buildDisplayRows(slots, avail, data).map(row => { + if (row.type === 'break') { + const mapped = { + ...row, + changeHref: `/sites/${site.id}/session/${avail.id}/${ctx.anchorDate}/breaks/${breakIndex}/change`, + removeHref: `/sites/${site.id}/session/${avail.id}/${ctx.anchorDate}/breaks/${breakIndex}/remove` + } + breakIndex += 1 + return mapped + } + + if (row.type === 'free') { + return { + ...row, + addBreakHref: `/sites/${site.id}/session/${avail.id}/${ctx.anchorDate}/breaks/add?start=${encodeURIComponent(row.startTime)}&end=${encodeURIComponent(row.endTime)}` + } + } + + return row + }) const serviceNames = avail.services .map(id => (data.services[id] ? data.services[id].name : id)) .join(', ') diff --git a/app/routes/sessions.js b/app/routes/sessions.js index 7c2e5cf..8024020 100644 --- a/app/routes/sessions.js +++ b/app/routes/sessions.js @@ -5,15 +5,103 @@ function sessions (router, shared) { getSessionBookingsForDate, moveBookings, ensureSessionEditDraft, + buildSessionBreakDraft, + finaliseSessionBreakDraft, renderSessionChangePage, applySessionEditDraft, + getSessionBreakRows, getSessionEditInfoHref, getSessionEditSummaryHref, + parseTime, parseSubmittedTimeBlocks, updateSessionEditDraftImpacts, groupedServices } = shared + function getBookingsHref (siteId, date) { + return `/sites/${siteId}/bookings?date=${date}` + } + + function getSessionBreaksBaseHref (siteId, availId, date) { + return `/sites/${siteId}/session/${availId}/${date}/breaks` + } + + function getSessionBreakBackHref (siteId, availId, date, draft) { + const base = getSessionBreaksBaseHref(siteId, availId, date) + if (draft.breakAction === 'change' && Number.isInteger(draft.breakIndex)) return `${base}/${draft.breakIndex}/change` + if (draft.breakAction === 'remove') return getBookingsHref(siteId, date) + return `${base}/add` + } + + function getBreakAtIndex (avail, breakIndex) { + return getSessionBreakRows(avail)[breakIndex] || null + } + + function renderBreakForm (req, res, avail, options = {}) { + const action = options.action || 'add' + const date = req.params.date + const siteId = req.site.id + const base = getSessionBreaksBaseHref(siteId, avail.id, date) + const formAction = action === 'change' && Number.isInteger(options.breakIndex) + ? `${base}/${options.breakIndex}/change` + : `${base}/add` + + res.render('sites/session-break-form', { + site: req.site, + sessionLabel: avail.label, + date, + action, + errors: options.errors || [], + startError: options.startError || null, + endError: options.endError || null, + breakError: options.breakError || null, + startValue: options.startValue || '', + endValue: options.endValue || '', + formAction, + backHref: options.backHref || getBookingsHref(siteId, date) + }) + } + + function renderBreakInputError (req, res, avail, options) { + const errors = [] + let startError = null + let endError = null + + if (options.startMessage) { + startError = { text: options.startMessage } + errors.push({ text: options.startMessage, href: '#start' }) + } + + if (options.endMessage) { + endError = { text: options.endMessage } + errors.push({ text: options.endMessage, href: '#end' }) + } + + if (options.breakMessage) { + errors.push({ text: options.breakMessage, href: '#start' }) + } + + renderBreakForm(req, res, avail, { + action: options.action, + breakIndex: options.breakIndex, + startValue: options.startValue, + endValue: options.endValue, + startError, + endError, + breakError: options.breakMessage, + errors, + backHref: options.backHref + }) + } + + function storeSessionBreakDraft (req, avail, operation) { + const draft = buildSessionBreakDraft(avail, req.params.date, operation) + if (draft.breakErrorMessage) return { errorMessage: draft.breakErrorMessage } + finaliseSessionBreakDraft(req.site, avail, req.params.date, draft) + req.session.data.sessionEditDraft = draft + return { draft } + } + router.get('/sites/:siteId/session/:availId/:date', (req, res) => { const avail = req.site.availability.find(a => a.id === req.params.availId) if (!avail) return res.status(404).send('Availability not found') @@ -131,6 +219,239 @@ function sessions (router, shared) { res.redirect(getSessionEditSummaryHref(req.site.id, avail.id, req.params.date)) }) + router.get('/sites/:siteId/session/:availId/:date/breaks/add', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + if (!avail) return res.status(404).send('Availability not found') + + renderBreakForm(req, res, avail, { + action: 'add', + startValue: String(req.query.start || '').trim(), + endValue: String(req.query.end || '').trim(), + backHref: getBookingsHref(req.site.id, req.params.date) + }) + }) + + router.post('/sites/:siteId/session/:availId/:date/breaks/add', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + if (!avail) return res.status(404).send('Availability not found') + + const startValue = String(req.body.start || '').trim() + const endValue = String(req.body.end || '').trim() + const start = parseTime(startValue) + const end = parseTime(endValue) + + if (!start || !end) { + return renderBreakInputError(req, res, avail, { + action: 'add', + startValue, + endValue, + startMessage: start ? null : 'Enter a valid break start time', + endMessage: end ? null : 'Enter a valid break end time', + backHref: getBookingsHref(req.site.id, req.params.date) + }) + } + + const { draft, errorMessage } = storeSessionBreakDraft(req, avail, { type: 'add', start, end }) + if (errorMessage) { + return renderBreakInputError(req, res, avail, { + action: 'add', + startValue, + endValue, + breakMessage: errorMessage, + backHref: getBookingsHref(req.site.id, req.params.date) + }) + } + + if ((draft.affectedBookingIds || []).length > 0) { + return res.redirect(`${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/warning`) + } + + res.redirect(`${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/check`) + }) + + router.get('/sites/:siteId/session/:availId/:date/breaks/:breakIndex/change', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + if (!avail) return res.status(404).send('Availability not found') + + const breakIndex = parseInt(req.params.breakIndex, 10) + const breakRow = getBreakAtIndex(avail, breakIndex) + if (!breakRow) return res.status(404).send('Break not found') + + renderBreakForm(req, res, avail, { + action: 'change', + breakIndex, + startValue: breakRow.start, + endValue: breakRow.end, + backHref: getBookingsHref(req.site.id, req.params.date) + }) + }) + + router.post('/sites/:siteId/session/:availId/:date/breaks/:breakIndex/change', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + if (!avail) return res.status(404).send('Availability not found') + + const breakIndex = parseInt(req.params.breakIndex, 10) + const breakRow = getBreakAtIndex(avail, breakIndex) + if (!breakRow) return res.status(404).send('Break not found') + + const startValue = String(req.body.start || '').trim() + const endValue = String(req.body.end || '').trim() + const start = parseTime(startValue) + const end = parseTime(endValue) + + if (!start || !end) { + return renderBreakInputError(req, res, avail, { + action: 'change', + breakIndex, + startValue, + endValue, + startMessage: start ? null : 'Enter a valid break start time', + endMessage: end ? null : 'Enter a valid break end time', + backHref: getBookingsHref(req.site.id, req.params.date) + }) + } + + const { draft, errorMessage } = storeSessionBreakDraft(req, avail, { + type: 'change', + breakIndex, + start, + end, + originalBreak: breakRow + }) + if (errorMessage) { + return renderBreakInputError(req, res, avail, { + action: 'change', + breakIndex, + startValue, + endValue, + breakMessage: errorMessage, + backHref: getBookingsHref(req.site.id, req.params.date) + }) + } + + if ((draft.affectedBookingIds || []).length > 0) { + return res.redirect(`${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/warning`) + } + + res.redirect(`${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/check`) + }) + + router.get('/sites/:siteId/session/:availId/:date/breaks/:breakIndex/remove', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + if (!avail) return res.status(404).send('Availability not found') + + const breakIndex = parseInt(req.params.breakIndex, 10) + const breakRow = getBreakAtIndex(avail, breakIndex) + if (!breakRow) return res.status(404).send('Break not found') + + const { errorMessage } = storeSessionBreakDraft(req, avail, { + type: 'remove', + breakIndex, + start: breakRow.start, + end: breakRow.end, + originalBreak: breakRow + }) + if (errorMessage) return res.status(400).send(errorMessage) + + res.redirect(`${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/check`) + }) + + router.get('/sites/:siteId/session/:availId/:date/breaks/warning', (req, res) => { + const draft = req.session.data.sessionEditDraft + if (!draft) return res.redirect(getBookingsHref(req.site.id, req.params.date)) + + const { site } = req + const date = req.params.date + const base = getSessionBreaksBaseHref(site.id, req.params.availId, date) + const affectedIds = new Set(draft.affectedBookingIds || []) + const allBookings = getSessionBookingsForDate(site, req.params.availId, date) + const affected = allBookings.filter(booking => affectedIds.has(booking.id)) + const allServices = req.data.services + const affectedBookingRows = affected + .sort((left, right) => left.time.localeCompare(right.time)) + .map(booking => ({ + time: booking.time, + name: booking.name, + serviceName: allServices[booking.service] ? allServices[booking.service].name : booking.service + })) + + res.render('sites/session-edit-warning', { + site, + changeDescription: draft.changeDescription || 'breaks', + affectedCount: affected.length, + affectedBookingRows, + formAction: `${base}/warning`, + backHref: getSessionBreakBackHref(site.id, req.params.availId, date, draft) + }) + }) + + router.post('/sites/:siteId/session/:availId/:date/breaks/warning', (req, res) => { + const draft = req.session.data.sessionEditDraft + if (!draft) return res.redirect(getBookingsHref(req.site.id, req.params.date)) + + draft.bookingsChoice = req.body.bookingsChoice || 'keep' + res.redirect(`${getSessionBreaksBaseHref(req.site.id, req.params.availId, req.params.date)}/check`) + }) + + router.get('/sites/:siteId/session/:availId/:date/breaks/check', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + const draft = req.session.data.sessionEditDraft + if (!avail) return res.status(404).send('Availability not found') + if (!draft) return res.redirect(getBookingsHref(req.site.id, req.params.date)) + + const backHref = (draft.affectedBookingIds || []).length > 0 + ? `${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/warning` + : getSessionBreakBackHref(req.site.id, avail.id, req.params.date, draft) + + res.render('sites/session-break-check', { + site: req.site, + sessionLabel: avail.label, + date: req.params.date, + draft, + formAction: `${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/check`, + backHref, + changeHref: draft.breakAction === 'change' && Number.isInteger(draft.breakIndex) + ? `${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/${draft.breakIndex}/change` + : (draft.breakAction === 'add' ? `${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/add` : null) + }) + }) + + router.post('/sites/:siteId/session/:availId/:date/breaks/check', (req, res) => { + const avail = req.site.availability.find(a => a.id === req.params.availId) + const draft = req.session.data.sessionEditDraft + if (!avail) return res.status(404).send('Availability not found') + if (!draft) return res.redirect(getBookingsHref(req.site.id, req.params.date)) + + applySessionEditDraft(req, avail, draft) + res.redirect(`${getSessionBreaksBaseHref(req.site.id, avail.id, req.params.date)}/success`) + }) + + router.get('/sites/:siteId/session/:availId/:date/breaks/success', (req, res) => { + const sd = req.session.data.sessionEditSuccess || {} + res.render('sites/session-break-success', { + site: req.site, + sessionLabel: sd.sessionLabel || 'Session', + date: sd.date || req.params.date, + breakAction: sd.breakAction || 'change', + breakStart: sd.breakStart, + breakEnd: sd.breakEnd, + bookingCount: sd.bookingCount || 0, + bookingsChoice: sd.bookingsChoice || 'keep', + contactableCount: sd.contactableCount || 0, + uncontactableCount: sd.uncontactableCount || 0, + notifyPageHref: `/sites/${req.site.id}/session/${req.params.availId}/${req.params.date}/breaks/not-notified` + }) + }) + + router.get('/sites/:siteId/session/:availId/:date/breaks/not-notified', (req, res) => { + const sd = req.session.data.sessionEditSuccess || {} + res.render('sites/availability-shared-not-notified', { + site: req.site, + people: sd.uncontactablePeople || [], + backHref: `/sites/${req.site.id}/session/${req.params.availId}/${req.params.date}/breaks/success` + }) + }) + router.post('/sites/:siteId/session/:availId/:date/edit', (req, res) => { const avail = req.site.availability.find(a => a.id === req.params.availId) if (!avail) return res.status(404).send('Availability not found') diff --git a/app/routes/shared/session-edit.js b/app/routes/shared/session-edit.js index 4a6c9e6..5d16398 100644 --- a/app/routes/shared/session-edit.js +++ b/app/routes/shared/session-edit.js @@ -128,6 +128,151 @@ function buildSessionEditDraft (avail, date) { } } +function getChangeDescription (draft, site, availId) { + const { startTime, endTime, breakTimes } = availFromTimeBlocks(draft.timeBlocks) + const servicesChanged = !arraysEqualAsSets(draft.services || [], getSessionAvailabilityServices(site, availId)) + const startChanged = draft.originalStartTime !== startTime + const endChanged = draft.originalEndTime !== endTime + const breaksChanged = JSON.stringify(draft.originalBreakTimes) !== JSON.stringify(breakTimes) + const slotChanged = String(draft.originalSlotLength) !== String(draft.slotLength) + const sessionTimesChanged = startChanged || endChanged + + if ((sessionTimesChanged || breaksChanged) && slotChanged) return 'session times, breaks and booking length' + if (sessionTimesChanged && breaksChanged) return 'session times and breaks' + if (slotChanged) return 'booking length' + if (servicesChanged) return 'services' + if (breaksChanged) return 'breaks' + return 'session times' +} + +function cloneTimeBlocks (timeBlocks = []) { + return timeBlocks.map(block => ({ ...block })) +} + +function getBreaksFromTimeBlocks (timeBlocks = []) { + const breaks = [] + for (let i = 0; i < timeBlocks.length - 1; i++) { + breaks.push({ start: timeBlocks[i].end, end: timeBlocks[i + 1].start }) + } + return breaks +} + +function getSessionBreakRows (avail) { + return (avail.breakTimes || []).map((breakTime, index) => ({ + ...breakTime, + index, + duration: formatDuration(timeToMinutes(breakTime.end) - timeToMinutes(breakTime.start)) + })) +} + +function applyBreakToTimeBlocks (timeBlocks, operation) { + const blocks = cloneTimeBlocks(timeBlocks) + + if (operation.type === 'remove') { + const block = blocks[operation.breakIndex] + const nextBlock = blocks[operation.breakIndex + 1] + if (!block || !nextBlock) { + return { errorMessage: 'Break not found', nextBlocks: blocks } + } + + return { + errorMessage: null, + nextBlocks: [ + ...blocks.slice(0, operation.breakIndex), + { start: block.start, end: nextBlock.end }, + ...blocks.slice(operation.breakIndex + 2) + ] + } + } + + const startMinutes = timeToMinutes(operation.start) + const endMinutes = timeToMinutes(operation.end) + if (endMinutes <= startMinutes) { + return { errorMessage: 'End time must be after start time', nextBlocks: blocks } + } + + if (operation.type === 'add') { + const blockIndex = blocks.findIndex(block => timeToMinutes(block.start) <= startMinutes && endMinutes <= timeToMinutes(block.end)) + if (blockIndex === -1) { + return { errorMessage: 'Break must be within one existing available time period', nextBlocks: blocks } + } + + const block = blocks[blockIndex] + const replacement = [] + if (startMinutes > timeToMinutes(block.start)) replacement.push({ start: block.start, end: operation.start }) + if (endMinutes < timeToMinutes(block.end)) replacement.push({ start: operation.end, end: block.end }) + if (replacement.length === 0) { + return { errorMessage: 'Break must leave some available time in the session', nextBlocks: blocks } + } + + return { + errorMessage: null, + nextBlocks: [ + ...blocks.slice(0, blockIndex), + ...replacement, + ...blocks.slice(blockIndex + 1) + ] + } + } + + if (operation.type === 'change') { + const leftBlock = blocks[operation.breakIndex] + const rightBlock = blocks[operation.breakIndex + 1] + if (!leftBlock || !rightBlock) { + return { errorMessage: 'Break not found', nextBlocks: blocks } + } + + const mergedStart = timeToMinutes(leftBlock.start) + const mergedEnd = timeToMinutes(rightBlock.end) + if (startMinutes < mergedStart || endMinutes > mergedEnd) { + return { errorMessage: 'Break must stay within the surrounding session times', nextBlocks: blocks } + } + + const replacement = [] + if (startMinutes > mergedStart) replacement.push({ start: leftBlock.start, end: operation.start }) + if (endMinutes < mergedEnd) replacement.push({ start: operation.end, end: rightBlock.end }) + if (replacement.length === 0) { + return { errorMessage: 'Break must leave some available time in the session', nextBlocks: blocks } + } + + return { + errorMessage: null, + nextBlocks: [ + ...blocks.slice(0, operation.breakIndex), + ...replacement, + ...blocks.slice(operation.breakIndex + 2) + ] + } + } + + return { errorMessage: 'Unsupported break action', nextBlocks: blocks } +} + +function buildSessionBreakDraft (avail, date, operation = {}) { + const draft = buildSessionEditDraft(avail, date) + draft.changeDescription = 'breaks' + draft.breakAction = operation.type || 'add' + draft.breakIndex = Number.isInteger(operation.breakIndex) ? operation.breakIndex : null + draft.breakStart = operation.start || '' + draft.breakEnd = operation.end || '' + draft.originalBreak = operation.originalBreak || null + draft.breakSummary = operation.summary || null + + const applied = applyBreakToTimeBlocks(draft.timeBlocks, operation) + if (applied.errorMessage) { + draft.breakErrorMessage = applied.errorMessage + return draft + } + + draft.timeBlocks = applied.nextBlocks + return draft +} + +function finaliseSessionBreakDraft (site, avail, date, draft) { + updateSessionEditDraftImpacts(site, avail.id, date, draft) + return draft +} + function getSessionAvailabilityServices (site, availId) { const avail = site.availability.find(a => a.id === availId) return avail ? [...(avail.services || [])] : [] @@ -149,10 +294,7 @@ function updateSessionEditDraftImpacts (site, availId, date, draft) { JSON.stringify(draft.originalBreakTimes) !== JSON.stringify(breakTimes) const slotChanged = String(draft.originalSlotLength) !== String(draft.slotLength) - if (timesChanged && slotChanged) draft.changeDescription = 'session times and booking length' - else if (slotChanged) draft.changeDescription = 'booking length' - else if (!arraysEqualAsSets(draft.services || [], getSessionAvailabilityServices(site, availId))) draft.changeDescription = 'services' - else draft.changeDescription = 'session times' + draft.changeDescription = getChangeDescription(draft, site, availId) if (bookings.length === 0 || !constraintsChanged) { draft.affectedBookingIds = [] @@ -266,6 +408,10 @@ function applySessionEditDraft (req, avail, draft) { req.session.data.sessionEditSuccess = { sessionLabel, date, + changeDescription: draft.changeDescription || 'session times', + breakAction: draft.breakAction || null, + breakStart: draft.breakStart || null, + breakEnd: draft.breakEnd || null, bookingCount: affected.length, bookingsChoice, contactableCount: contactable.length, @@ -280,11 +426,16 @@ function applySessionEditDraft (req, avail, draft) { module.exports = { buildDisplayRows, + buildSessionBreakDraft, + getSessionBreakRows, + getBreaksFromTimeBlocks, + applyBreakToTimeBlocks, mapBookingToDisplayRow, updateSessionEditDraftImpacts, getSessionEditSummaryHref, getSessionEditInfoHref, ensureSessionEditDraft, + finaliseSessionBreakDraft, renderSessionChangePage, applySessionEditDraft } diff --git a/app/views/sites/bookings.html b/app/views/sites/bookings.html index 352b656..2064a7f 100644 --- a/app/views/sites/bookings.html +++ b/app/views/sites/bookings.html @@ -74,19 +74,26 @@

{{ session.label }}

{% if row.type === 'break' %} {{ row.startTime | formatTime(false) }} - Break ({{ row.duration }}) + Break ({{ row.duration }}) + + Change break starting at {{ row.startTime | formatTime(false) }}
+ Remove break starting at {{ row.startTime | formatTime(false) }} + {% elif row.type === 'free' %} {{ row.startTime | formatTime(false) }} - + {% if row.count === 1 %} No one has booked this slot ({{ row.duration }}) {% else %} No one has booked these {{ row.count }} slots ({{ row.duration }}) {% endif %} + + Add break in the available time starting at {{ row.startTime | formatTime(false) }} + {% else %} diff --git a/app/views/sites/session-break-check.html b/app/views/sites/session-break-check.html new file mode 100644 index 0000000..441e42c --- /dev/null +++ b/app/views/sites/session-break-check.html @@ -0,0 +1,76 @@ +{% extends 'layout.html' %} + +{% set pageName = "Check break changes — " + site.name %} + +{% block beforeContent %} + {{ backLink({ href: backHref }) }} +{% endblock %} + +{% block content %} +
+
+ +

Check break changes

+ +
+
+

Session details

+
+
+ {{ summaryList({ rows: [ + { + key: { text: "Session" }, + value: { text: sessionLabel } + }, + { + key: { text: "Date" }, + value: { text: date | formatDateLong } + } + ] }) }} +
+
+ +
+
+

Break details

+ {% if changeHref %} + + {% endif %} +
+
+ {{ summaryList({ rows: [ + { + key: { text: "Action" }, + value: { text: "Remove break" if draft.breakAction === "remove" else ("Change break" if draft.breakAction === "change" else "Add break") } + }, + { + key: { text: "Time" }, + value: { text: (draft.breakStart | formatTime) + " to " + (draft.breakEnd | formatTime) } + } + ] }) }} +
+
+ + {% if draft.affectedBookingIds.length > 0 %} + {% call insetText({}) %} +

+ {% if draft.bookingsChoice === "cancel" %} + {{ draft.affectedBookingIds.length }} {{ "booking will" if draft.affectedBookingIds.length === 1 else "bookings will" }} be cancelled when you save these changes. + {% else %} + {{ draft.affectedBookingIds.length }} {{ "booking will" if draft.affectedBookingIds.length === 1 else "bookings will" }} move to Kept bookings so {{ "it can" if draft.affectedBookingIds.length === 1 else "they can" }} be rescheduled. + {% endif %} +

+ {% endcall %} + {% endif %} + +
+ {{ button({ text: "Save changes" }) }} +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/app/views/sites/session-break-form.html b/app/views/sites/session-break-form.html new file mode 100644 index 0000000..6fb59fa --- /dev/null +++ b/app/views/sites/session-break-form.html @@ -0,0 +1,55 @@ +{% extends 'layout.html' %} + +{% set pageName = ("Change break" if action === "change" else "Add break") + " — " + site.name %} + +{% block beforeContent %} + {{ backLink({ href: backHref }) }} +{% endblock %} + +{% block content %} +
+
+ + {% if errors.length > 0 %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: errors + }) }} + {% endif %} + + {{ sessionLabel }} on {{ date | formatDateLong }} +

{{ "Change break" if action === "change" else "Add break" }}

+ +
+ + {% if breakError %} +

{{ breakError }}

+ {% endif %} + + {{ input({ + id: "start", + name: "start", + label: { text: "Start time", classes: "nhsuk-label--m" }, + hint: { text: "For example, 11am or 11:15." }, + classes: "nhsuk-input--width-5", + value: startValue, + errorMessage: startError + }) }} + + {{ input({ + id: "end", + name: "end", + label: { text: "End time", classes: "nhsuk-label--m" }, + hint: { text: "For example, 11:15 or 11:30." }, + classes: "nhsuk-input--width-5", + value: endValue, + errorMessage: endError + }) }} + + {{ button({ text: "Continue" }) }} + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/app/views/sites/session-break-success.html b/app/views/sites/session-break-success.html new file mode 100644 index 0000000..3c8d8f8 --- /dev/null +++ b/app/views/sites/session-break-success.html @@ -0,0 +1,47 @@ +{% extends 'layout.html' %} +{% from "macros/app-outcomes.njk" import appCancellationNotifications, appNextSteps %} + +{% set pageName = "Break updated — " + site.name %} + +{% block content %} +
+
+ + {% set titleText = "Break removed" if breakAction === "remove" else ("Break changed" if breakAction === "change" else "Break added") %} + + {% call panel({ titleText: titleText, classes: "nhsuk-panel--confirmation nhsuk-u-margin-top-0" }) %} +

{{ breakStart | formatTime }} to {{ breakEnd | formatTime }} in "{{ sessionLabel }}" on {{ date | formatDateLong }}

+ {% endcall %} + +

The session has been updated for {{ date | formatDateLong }}.

+ + {% if bookingCount > 0 %} + + {% if bookingsChoice === "cancel" %} + + {{ appCancellationNotifications({ + bookingCount: bookingCount, + contactableCount: contactableCount, + uncontactableCount: uncontactableCount, + notifyPageHref: notifyPageHref + }) }} + + {% else %} + {{ appCancellationNotifications({ + bookingCount: bookingCount, + bookingsChoice: bookingsChoice, + keptMessage: "Bookings affected by this break change move to the Kept bookings section so they can be rescheduled." + }) }} + {% endif %} + + {% endif %} + + {{ appNextSteps([ + { href: "/sites/" + site.id + "/bookings?date=" + date, text: "Return to bookings" }, + { href: "/sites/" + site.id + "/availability", text: "View availability" }, + { href: "/sites/" + site.id + "/dashboard", text: "Back to dashboard" } + ]) }} + +
+
+{% endblock %} \ No newline at end of file From 0ac3558667c35764047502b949e5e61aea1ff2c5 Mon Sep 17 00:00:00 2001 From: roobottom Date: Mon, 16 Mar 2026 22:18:42 +0000 Subject: [PATCH 2/2] Adjust break row highlight colour --- app/assets/sass/main.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/sass/main.scss b/app/assets/sass/main.scss index 2b345db..7c1c275 100755 --- a/app/assets/sass/main.scss +++ b/app/assets/sass/main.scss @@ -87,7 +87,7 @@ .app-table__break-row, .app-table__break-row:hover { - background-color: #FFFDEF; + background-color: #FFDEF1; } // Utilities