From ec5e17e09fe06f60f1f1727e006ea10f096c2117 Mon Sep 17 00:00:00 2001 From: Mat Johnson Date: Tue, 14 Jul 2026 15:30:43 +0100 Subject: [PATCH 1/7] Building out rev 1 of a single day clinic assembly --- app/views/clinics/00-create-day-clinic.html | 307 +++++++++++++++++ app/views/clinics/01-set-date-and-time.html | 309 ++++++++++++++++++ app/views/clinics/02-organise-slots.html | 203 +++++++----- app/views/clinics/03-publish-clinic.html | 121 +++++++ app/views/clinics/index.html | 2 +- ...imes.html => templating-01-set-times.html} | 0 6 files changed, 857 insertions(+), 85 deletions(-) create mode 100644 app/views/clinics/00-create-day-clinic.html create mode 100644 app/views/clinics/01-set-date-and-time.html create mode 100644 app/views/clinics/03-publish-clinic.html rename app/views/clinics/{01-set-times.html => templating-01-set-times.html} (100%) diff --git a/app/views/clinics/00-create-day-clinic.html b/app/views/clinics/00-create-day-clinic.html new file mode 100644 index 0000000..c656588 --- /dev/null +++ b/app/views/clinics/00-create-day-clinic.html @@ -0,0 +1,307 @@ +{% extends 'layout.html' %} + +{% set pageName = "Clinics" %} + +{% from "_includes/primary-navigation.html" import primaryNavigation %} +{% block header %} + {{ primaryNavigation("Clinics", serviceName) }} +{% endblock %} + +{% block beforeContent %}{% endblock %} + +{% block content %} + +{% set tickSvg %} + +{% endset %} + +
+
+ +
+ +
+ +
+
+
+ +

Clinic details

+

Potentially some real brief help and scene setting. Mainly to point out you can punch holes in it as you go?

+ +
+ +
+ +
+ A memorable name for this clinic. This is the name you’ll see everywhere for it, as titles and links. +
+ + + +
+ +
+
+ + Location + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + Unit + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+
+ + + +{% endblock %} + +{% block pageScripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/views/clinics/01-set-date-and-time.html b/app/views/clinics/01-set-date-and-time.html new file mode 100644 index 0000000..4e1e71d --- /dev/null +++ b/app/views/clinics/01-set-date-and-time.html @@ -0,0 +1,309 @@ +{% extends 'layout.html' %} + +{% set pageName = "Clinics" %} + +{% from "_includes/primary-navigation.html" import primaryNavigation %} +{% block header %} + {{ primaryNavigation("Clinics", serviceName) }} +{% endblock %} + +{% block beforeContent %}{% endblock %} + +{% block content %} + +{% set tickSvg %} + +{% endset %} + +
+
+ +
+ +
+ +
+
+
+ +

Set date and time

+

Potentially some real brief help and scene setting. Mainly to point out you can punch holes in it as you go?

+ +
+ +
+
+ + Date of clinic + +
+ For example, 18 6 2026 +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+
+ + Session start time + +
+ For example, 10:00 +
+ +
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+ +
+
+ + Session end time + +
+ For example, 17:00 +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+ +
+ + +
+
+ +
+ Information: +

Capacity calculator

+

X total slots in the + session

+ + +
+ + + +
+ +
+ Time must be in 24 hour format
+ Start time must be before end time and vice versa
+ Slots dont have to divide equally into the times, but it'll be interesting to work that out? +
+
+
+
+ +{% endblock %} + +{% block pageScripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/views/clinics/02-organise-slots.html b/app/views/clinics/02-organise-slots.html index 34a42a4..3db759a 100644 --- a/app/views/clinics/02-organise-slots.html +++ b/app/views/clinics/02-organise-slots.html @@ -7,26 +7,7 @@ {{ primaryNavigation("Clinics", serviceName) }} {% endblock %} -{% block beforeContent %} - -{{ breadcrumb({ - items: [ - { - href: "#", - text: "Clinics" - }, - { - href: "#", - text: "Templates" - }, - { - href: "#", - text: "Session templates" - } - ] -}) }} - -{% endblock %} +{% block beforeContent %}{% endblock %} {% block content %} @@ -52,24 +33,34 @@ .slot-tag { font-size: 11px; margin-top: 4px; font-weight: 500; } .slot.is-hold { + + } + + .slot.is-staff-break { background: transparent; border-style: dashed; } + .slot.is-special-appointment { + background-color: #fff9c4; + } + .slot.is-merged { border-bottom: none; margin-bottom: 0; } .slot.is-merged-child { - border-top: 1px dashed #d8d8d8; + border-top: 1px dashed #212b32; } .slot.is-merged-last-child { border-bottom: 2px solid #212b32; margin-bottom: 12px; } - .slot.is-hold.is-merged-last-child { + + .slot.is-staff-break.is-merged-last-child { border-bottom-style: dashed; } + .slot.is-merged-child > .slot-time, .slot.is-merged-child > .slot-end { display: none; @@ -102,21 +93,27 @@

  • - {{ tickSvg | safe }} + 1 - Set times + Clinic details
  • -
  • +
  • 2 + Set date and time + +
  • +
  • + + 3 Organise slots
  • - 3 - Save template + 4 + Publish clinic
  • @@ -129,40 +126,54 @@

    Organise slots

    Potentially some real brief help and scene setting. Selection and controls

    +

    + Click to select a slot.
    + Press shift and click to select a range
    + Press control or command (⌘) and click to select multiple.

    Session time {{ data.newSession.startTime.hour | zeroPad }}:{{ data.newSession.startTime.minute | zeroPad }} to {{ data.newSession.endTime.hour | zeroPad }}:{{ data.newSession.endTime.minute | zeroPad }}

    -
    -
    -
    +
    + +
    +
    +
    +
    +
    + +

    + {{ data.newSession.totalSlots }} slots available +

    +
    -
    -

    - {{ data.newSession.totalSlots }} slots available to book of {{ data.newSession.totalSlots }} total -

    -

    Click to select · Shift+click for a range · ⌘/Ctrl+click for multiple

    -
    -

    With selected:

    - + +
    +
    +

    + +

    -
    -
    +
    - - - {% endblock %} {% block pageScripts %} @@ -173,6 +184,7 @@

    With selected:

    let slotData = []; let selectedIndices = new Set(); let lastClickedIndex = null; + let totalSlotCount = 0; // ========== UTILITY FUNCTIONS ========== function parseMinutes(time) { @@ -227,7 +239,7 @@

    With selected:

    if (!parent.merged) return; const span = parent.span; - const parentHold = parent.hold; + const parentType = parent.type; for (let j = parentIndex; j < parentIndex + span; j++) { if (!slotData[j]) continue; @@ -236,13 +248,13 @@

    With selected:

    slotData[j].span = 1; slotData[j].isMergedInto = undefined; slotData[j].mergedEnd = null; - slotData[j].hold = parentHold; + slotData[j].type = parentType; } } - function holdSlots(indices, hold) { + function setSlotType(indices, type) { [...indices].forEach(i => { - if (slotData[i]) slotData[i].hold = hold; + if (slotData[i]) slotData[i].type = type; }); } @@ -257,20 +269,20 @@

    With selected:

    const first = sel[0]; const last = sel[sel.length - 1]; const span = sel.length; - const mergeHold = sel.every(i => slotData[i].hold); + const mergeType = slotData[first].type; // reset (important when reusing slots) for (let j = first; j <= last; j++) { slotData[j].merged = false; slotData[j].span = 1; slotData[j].isMergedInto = undefined; - slotData[j].hold = false; + slotData[j].type = null; } slotData[first].merged = true; slotData[first].span = span; slotData[first].mergedEnd = formatTime(slotData[last].end); - slotData[first].hold = mergeHold; + slotData[first].type = mergeType; for (let j = first + 1; j <= last; j++) { slotData[j].isMergedInto = first; @@ -302,6 +314,7 @@

    With selected:

    return; } + totalSlotCount = slotCount; const prevData = [...slotData]; slotData = []; @@ -316,7 +329,7 @@

    With selected:

    start: s, end: e, key, - hold: prev.hold || false, + type: prev.type || null, merged: prev.merged || false, span: prev.span || 1, isMergedInto: prev.isMergedInto ?? undefined, @@ -326,24 +339,19 @@

    With selected:

    selectedIndices = new Set([...selectedIndices].filter(i => i < slotCount)); renderGrid(); - setStats(totalMins, slotLen); + setStats(); } - function setStats(totalMins, slotLen) { + function setStats() { const heads = slotData.filter(s => s.isMergedInto === undefined); - const count = heads.length; - const holdC = heads.filter(s => s.hold).length; - const avail = count - holdC; - document.getElementById('total-slots').textContent = count || '—'; - document.getElementById('available-slots').textContent = count ? avail : '—'; + const nonAvail = heads.filter(s => s.type === 'hold' || s.type === 'staff-break' || s.merged).length; + const avail = totalSlotCount - nonAvail; + document.getElementById('available-slots').textContent = totalSlotCount ? avail : '—'; } function commit() { renderGrid(); - const startMins = parseMinutes(sessionData.startTime); - const endMins = parseMinutes(sessionData.endTime); - const slotLen = Number(sessionData.duration); - setStats(endMins - startMins, slotLen); + setStats(); } // ========== RENDERING FUNCTIONS ========== @@ -359,12 +367,14 @@

    With selected:

    const classes = ['slot']; const isSelected = selectedIndices.has(i) || (isChild && selectedIndices.has(headIndex)); const isMerged = sd.merged || isChild; - const isHeld = sd.hold || (isChild && headSlot?.hold); + const slotType = isChild ? headSlot?.type : sd.type; const isLastMergedChild = isChild && headSlot && i === headIndex + (headSlot.span || 1) - 1; if (isSelected) classes.push('selected'); if (isMerged) classes.push('is-merged'); - if (isHeld) classes.push('is-hold'); + if (slotType === 'hold') classes.push('is-hold'); + if (slotType === 'staff-break') classes.push('is-staff-break'); + if (slotType === 'special-appointment') classes.push('is-special-appointment'); if (isChild) classes.push('is-merged-child'); if (isLastMergedChild) classes.push('is-merged-last-child'); @@ -377,12 +387,13 @@

    With selected:

    `; if (isMerged) { - const mergedLabel = sd.merged - ? isHeld ? `Merged (${sd.span}) · Held` : `Merged (${sd.span})` - : 'Merged slot'; + let mergedLabel = `Merged (${sd.span})`; div.innerHTML += `
    ${mergedLabel}
    `; - } else if (isHeld) { - div.innerHTML += `
    Held
    `; + } + if (slotType && !isChild) { + const typeLabel = slotType === 'hold' ? 'Held' : slotType === 'staff-break' ? 'Staff break' : 'Special appointment'; + const tagClass = slotType === 'hold' ? 'nhsuk-tag--blue' : slotType === 'staff-break' ? 'nhsuk-tag--grey' : 'nhsuk-tag--yellow'; + div.innerHTML += `
    ${typeLabel}
    `; } div.addEventListener('click', e => handleSlotClick(e, i)); @@ -408,11 +419,17 @@

    With selected:

    const s = slotData[i]; return s.merged || s.isMergedInto !== undefined; }); - const anyHold = sel.some(i => slotData[i]?.hold); - const allHold = sel.every(i => slotData[i]?.hold); + + const typeSet = new Set(sel.map(i => slotData[i]?.type)); + const allHold = typeSet.size === 1 && typeSet.has('hold'); + const allStaffBreak = typeSet.size === 1 && typeSet.has('staff-break'); + const allSpecialAppointment = typeSet.size === 1 && typeSet.has('special-appointment'); + const anyType = sel.some(i => slotData[i]?.type !== null); document.getElementById('panel-hold').classList.toggle('disabled', !sel.length || allHold); - document.getElementById('panel-unhold').classList.toggle('disabled', !sel.length || !anyHold); + document.getElementById('panel-staff-break').classList.toggle('disabled', !sel.length || allStaffBreak); + document.getElementById('panel-special-appointment').classList.toggle('disabled', !sel.length || allSpecialAppointment); + document.getElementById('panel-clear-type').classList.toggle('disabled', !sel.length || !anyType); document.getElementById('panel-merge').classList.toggle('disabled', !canMerge); document.getElementById('panel-unmerge').classList.toggle('disabled', !hasMerged); } @@ -449,21 +466,39 @@

    With selected:

    // ========== EVENT ATTACHMENTS ========== // No DOM form inputs on this page; session data is sourced from the server-side session. const panelHold = document.getElementById('panel-hold'); - const panelUnhold = document.getElementById('panel-unhold'); + const panelStaffBreak = document.getElementById('panel-staff-break'); + const panelSpecialAppointment = document.getElementById('panel-special-appointment'); + const panelClearType = document.getElementById('panel-clear-type'); const panelMerge = document.getElementById('panel-merge'); const panelUnmerge = document.getElementById('panel-unmerge'); if (panelHold) { panelHold.addEventListener('click', () => { - holdSlots(selectedIndices, true); + setSlotType(selectedIndices, 'hold'); + selectedIndices.clear(); + commit(); + }); + } + + if (panelStaffBreak) { + panelStaffBreak.addEventListener('click', () => { + setSlotType(selectedIndices, 'staff-break'); + selectedIndices.clear(); + commit(); + }); + } + + if (panelSpecialAppointment) { + panelSpecialAppointment.addEventListener('click', () => { + setSlotType(selectedIndices, 'special-appointment'); selectedIndices.clear(); commit(); }); } - if (panelUnhold) { - panelUnhold.addEventListener('click', () => { - holdSlots(selectedIndices, false); + if (panelClearType) { + panelClearType.addEventListener('click', () => { + setSlotType(selectedIndices, null); selectedIndices.clear(); commit(); }); @@ -486,12 +521,12 @@

    With selected:

    } // Global keyboard events - document.addEventListener('keydown', e => { + /*document.addEventListener('keydown', e => { if (e.key === 'Escape') { selectedIndices.clear(); renderGrid(); } - }); + });*/ // ========== INITIALIZATION ========== render(); diff --git a/app/views/clinics/03-publish-clinic.html b/app/views/clinics/03-publish-clinic.html new file mode 100644 index 0000000..005bc64 --- /dev/null +++ b/app/views/clinics/03-publish-clinic.html @@ -0,0 +1,121 @@ +{% extends 'layout.html' %} + +{% set pageName = "Clinics" %} + +{% from "_includes/primary-navigation.html" import primaryNavigation %} +{% block header %} + {{ primaryNavigation("Clinics", serviceName) }} +{% endblock %} + +{% block beforeContent %}{% endblock %} + +{% block content %} + +{% set tickSvg %} + +{% endset %} + +
    +
    + +
    + +
    + +
    +
    +
    + +

    Publish clinic

    +

    Potentially some real brief help and scene setting. Mainly to point out you can punch holes in it as you go?

    + +
    +
    +
    + Clinic details +
    +
    +
    + {{ data.clinicName }}
    + {{ data.clinicId }} +
    + {{ data.unit }} at {{ data.location }} +
    +
    + Change clinic details +
    +
    +
    +
    + Date and time +
    +
    + Date
    + {{ data.newSession.startTime.hour | zeroPad }}:{{ data.newSession.startTime.minute | zeroPad }} to {{ data.newSession.endTime.hour | zeroPad }}:{{ data.newSession.endTime.minute | zeroPad }}
    + {{ data.newSession.duration }} minute slot length +
    +
    + Change date and time +
    +
    +
    +
    + Slots +
    +
    + XX slots available to book
    + XX special apppointments
    + XX held +
    +
    + Change slots +
    +
    +
    + +

    + Publish clinic +

    + +
    +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/app/views/clinics/index.html b/app/views/clinics/index.html index b5206c8..d82e1dc 100644 --- a/app/views/clinics/index.html +++ b/app/views/clinics/index.html @@ -26,7 +26,7 @@

    Clinics

    {{ button({ text: "Create a new clinic", - href: "#", + href: "00-create-day-clinic", classes: "nhsuk-button--secondary" }) }}
    diff --git a/app/views/clinics/01-set-times.html b/app/views/clinics/templating-01-set-times.html similarity index 100% rename from app/views/clinics/01-set-times.html rename to app/views/clinics/templating-01-set-times.html From bab75c3b99e3e342dbf5a604420efaedfdc8d7ae Mon Sep 17 00:00:00 2001 From: Mat Johnson Date: Tue, 14 Jul 2026 16:01:07 +0100 Subject: [PATCH 2/7] basic day journey with some layouts to test --- app/views/clinics/00-create-day-clinic.html | 337 +++++++------------- 1 file changed, 124 insertions(+), 213 deletions(-) diff --git a/app/views/clinics/00-create-day-clinic.html b/app/views/clinics/00-create-day-clinic.html index c656588..b2ffcc6 100644 --- a/app/views/clinics/00-create-day-clinic.html +++ b/app/views/clinics/00-create-day-clinic.html @@ -64,7 +64,6 @@

    Clinic details

    -

    Potentially some real brief help and scene setting. Mainly to point out you can punch holes in it as you go?

    @@ -76,123 +75,138 @@

    Clinic details

    A memorable name for this clinic. This is the name you’ll see everywhere for it, as titles and links.
    -