diff --git a/app/assets/sass/components/_workflow.scss b/app/assets/sass/components/_workflow.scss
new file mode 100644
index 0000000..efa6183
--- /dev/null
+++ b/app/assets/sass/components/_workflow.scss
@@ -0,0 +1,201 @@
+// app/assets/sass/components/_workflow.scss
+
+@use "nhsuk-frontend/dist/nhsuk/core" as *;
+
+// Wider container for workflow mode
+.nhsuk-width-container--wide {
+ @include nhsuk-width-container(1200px);
+}
+
+.app-workflow-mode .nhsuk-width-container {
+ @include nhsuk-width-container(1200px);
+}
+
+// Use flexbox for workflow layout to allow nav column to stretch for sticky positioning
+.app-workflow-container__row {
+ @include nhsuk-media-query($from: desktop) {
+ display: flex;
+ }
+}
+
+.app-workflow-container__sidebar,
+.app-workflow-container__main {
+ @include nhsuk-media-query($from: desktop) {
+ float: none;
+ }
+}
+
+// Wrapper needs to fill the column height for sticky to work
+.app-workflow-side-nav-wrapper {
+ height: 100%;
+ position: sticky;
+ top: nhsuk-spacing(4);
+ padding-bottom: 15px;
+}
+
+.app-workflow-side-nav {
+ position: static;
+}
+
+.app-workflow-side-nav__heading {
+ margin-bottom: nhsuk-spacing(3);
+}
+
+.app-workflow-side-nav__list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.app-workflow-side-nav__item {
+ position: relative;
+ border-left: 4px solid transparent;
+ margin-left: -1px;
+}
+
+.app-workflow-side-nav__link {
+ @include nhsuk-font($size: 16);
+
+ display: flex;
+ align-items: center;
+ gap: nhsuk-spacing(2);
+ padding: nhsuk-spacing(2) 0 nhsuk-spacing(2) nhsuk-spacing(2);
+ text-decoration: none;
+ position: relative;
+}
+
+// Only apply link styles to actual links
+a.app-workflow-side-nav__link {
+ @include nhsuk-link-style-default;
+ @include nhsuk-link-style-no-visited-state;
+
+ &:hover {
+ .app-workflow-side-nav__label {
+ text-decoration: underline;
+ text-decoration-thickness: max(3px, 0.1875rem, 0.12em);
+ }
+ }
+
+ &:focus {
+ @include nhsuk-focused-text;
+ color: $nhsuk-focus-text-colour;
+ }
+}
+
+.app-workflow-side-nav__link--disabled {
+ color: $nhsuk-secondary-text-colour;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+// Clickable links (not current, not completed, not disabled) have blue circles
+.app-workflow-side-nav__link--clickable {
+ .app-workflow-side-nav__number {
+ background-color: nhsuk-colour("blue");
+ color: nhsuk-colour("white");
+ }
+}
+
+.app-workflow-side-nav__number {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 24px;
+ width: 24px;
+ height: 24px;
+ background-color: nhsuk-colour("grey-3");
+ color: nhsuk-colour("grey-1");
+ border-radius: 50%;
+ font-weight: 600;
+ font-size: 14px;
+ text-align: center;
+ line-height: 1;
+ padding-left: 1px; // Nudge numbers slightly right
+
+ .app-icon--tick {
+ width: 16px;
+ height: 16px;
+
+ path {
+ fill: none;
+ stroke: nhsuk-colour("white");
+ stroke-width: 4;
+ }
+ }
+}
+
+.app-workflow-side-nav__label {
+ flex: 1;
+}
+
+// Item states
+.app-workflow-side-nav__item--current {
+ border-left-color: $nhsuk-brand-colour;
+
+ .app-workflow-side-nav__link {
+ color: $nhsuk-text-colour;
+ }
+
+ .app-workflow-side-nav__label {
+ font-weight: bold;
+ }
+
+ .app-workflow-side-nav__number {
+ background-color: nhsuk-colour("blue");
+ color: nhsuk-colour("white");
+ }
+}
+
+// Clickable links (not current, not completed, not disabled) have blue circles
+.app-workflow-side-nav__link--clickable {
+ .app-workflow-side-nav__number {
+ background-color: nhsuk-colour("blue");
+ color: nhsuk-colour("white");
+ }
+}
+
+.app-workflow-side-nav__item--completed {
+ .app-workflow-side-nav__link--clickable .app-workflow-side-nav__number,
+ .app-workflow-side-nav__number {
+ background-color: nhsuk-colour("green");
+ color: nhsuk-colour("white");
+
+ .app-icon--tick path {
+ stroke: nhsuk-colour("white");
+ }
+ }
+}
+
+.app-workflow-side-nav__item--disabled {
+ .app-workflow-side-nav__link--clickable .app-workflow-side-nav__number,
+ .app-workflow-side-nav__number {
+ background-color: nhsuk-colour("grey-4");
+ color: nhsuk-colour("grey-1");
+ }
+}
+
+// Responsive - use full width on mobile
+@media (max-width: 768px) {
+ .app-workflow-side-nav {
+ position: static;
+ }
+}
+
+// Status text shown after the NHS number in the appointment header (e.g. "Added to worklist")
+.app-header-status {
+ margin-left: nhsuk-spacing(3);
+
+ .app-header-status__icon {
+ height: 1em;
+ width: 1em;
+ vertical-align: -0.15em;
+ margin-right: -1px;
+ // Aiming for #48B788
+ fill: nhsuk-tint(nhsuk-colour("green"), 40%);
+ }
+}
+
+.app-header-status--fail .app-header-status__icon {
+ // Aiming for #E98881
+ fill: nhsuk-tint(nhsuk-colour("red"), 44%);
+}
diff --git a/app/assets/sass/main.scss b/app/assets/sass/main.scss
index 1b9b32a..b511c44 100755
--- a/app/assets/sass/main.scss
+++ b/app/assets/sass/main.scss
@@ -14,13 +14,14 @@
@forward "components/secondary-navigation";
@forward "components/participant-table";
//@forward "components/participant-search";
+@forward "components/workflow";
//{% set containerClasses = "nhsuk-width-container-fluid" %}
-
// Add your custom CSS/Sass styles below.
.app-width-extended {
width: 1280px;
}
+
.app-card-editable {
display: flex;
justify-content: space-between;
diff --git a/app/lib/utils/strings.js b/app/lib/utils/strings.js
new file mode 100644
index 0000000..a166347
--- /dev/null
+++ b/app/lib/utils/strings.js
@@ -0,0 +1,25 @@
+// app/lib/utils/strings.js
+
+const padStart = (value, length = 2, char = '0') => {
+ if (value === null || value === undefined) {
+ return ''
+ }
+
+ return String(value).padStart(length, char)
+}
+
+const zeroPad = (value, length = 2) => padStart(value, length, '0')
+
+const toJSON = (value) => {
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return 'null'
+ }
+}
+
+module.exports = {
+ padStart,
+ zeroPad,
+ toJSON
+}
diff --git a/app/views/september-iteration-2/macros/clinic-listing.html b/app/views/_macros/clinic-listing.html
similarity index 100%
rename from app/views/september-iteration-2/macros/clinic-listing.html
rename to app/views/_macros/clinic-listing.html
diff --git a/app/views/clinics/01-set-times.html b/app/views/clinics/01-set-times.html
new file mode 100644
index 0000000..93d4523
--- /dev/null
+++ b/app/views/clinics/01-set-times.html
@@ -0,0 +1,301 @@
+{% extends 'layout.html' %}
+
+{% set pageName = "Clinics" %}
+
+{% from "_includes/primary-navigation.html" import primaryNavigation %}
+{% block header %}
+ {{ primaryNavigation("Clinics", serviceName) }}
+{% endblock %}
+
+{% block beforeContent %}
+
+{{ breadcrumb({
+ items: [
+ {
+ href: "#",
+ text: "Clinics"
+ },
+ {
+ href: "#",
+ text: "Templates"
+ },
+ {
+ href: "#",
+ text: "Session templates"
+ }
+ ]
+}) }}
+
+{% endblock %}
+
+{% block content %}
+
+{% set tickSvg %}
+
+
+
+{% endset %}
+
+
+
+
+
+
+
+
+ Create session template
+
+ Workflow steps
+
+
+
+
+
+ 1
+
+ Set times
+
+
+
+
+ 2
+ Organise slots
+
+
+
+
+ 3
+ Save template
+
+
+
+
+
+
+
+
+
+
+
Set times
+
Potentially some real brief help and scene setting. Mainly to point out you can punch holes in it as you go?
+
+
+
+
+ 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
new file mode 100644
index 0000000..34a42a4
--- /dev/null
+++ b/app/views/clinics/02-organise-slots.html
@@ -0,0 +1,499 @@
+{% extends 'layout.html' %}
+
+{% set pageName = "Clinics" %}
+
+{% from "_includes/primary-navigation.html" import primaryNavigation %}
+{% block header %}
+ {{ primaryNavigation("Clinics", serviceName) }}
+{% endblock %}
+
+{% block beforeContent %}
+
+{{ breadcrumb({
+ items: [
+ {
+ href: "#",
+ text: "Clinics"
+ },
+ {
+ href: "#",
+ text: "Templates"
+ },
+ {
+ href: "#",
+ text: "Session templates"
+ }
+ ]
+}) }}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+{% set tickSvg %}
+
+
+
+{% endset %}
+
+
+
+
+
+
+
+
+ Create session template
+
+ Workflow steps
+
+
+
+
+
+ {{ tickSvg | safe }}
+
+ Set times
+
+
+
+
+ 2
+ Organise slots
+
+
+
+
+ 3
+ Save template
+
+
+
+
+
+
+
+
+
+
+
Organise slots
+
Potentially some real brief help and scene setting. Selection and controls
+
+ 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 to book of {{ data.newSession.totalSlots }} total
+
+
Click to select · Shift+click for a range · ⌘/Ctrl+click for multiple
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block pageScripts %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/views/clinics/day-slot-view.html b/app/views/clinics/day-slot-view.html
index f62178b..81e01c9 100644
--- a/app/views/clinics/day-slot-view.html
+++ b/app/views/clinics/day-slot-view.html
@@ -45,35 +45,248 @@
Tuesday
1 September 2026
- {% set secondaryNavItems = [] %}
- {% for item in [
- { href: '#', label: 'One modality name (42 slots available)', selected: 'true' },
- { href: '#', label: 'Another modality name (0 slots available)' }
- ] %}
- {% set secondaryNavItems = secondaryNavItems | push({
- text: item.label | safe,
- href: item.href | trim,
- current: true if item.selected == 'true'
- }) %}
- {% endfor %}
-
- {{ appSecondaryNavigation({
- visuallyHiddenTitle: "Secondary menu",
- items: secondaryNavItems
- }) }}
-
-
- Morning session
- 08:30 to 12:30
-
+
+
Whole day size controls:
+
+ Using session template X? Can I change the template?
+ Can I add slots now, eiter before or after the established ones?
+ Cancel the whole day
+
-
-
-
- 18 slots available of 21
-
-
-
+
+ [num] slots available of [total]
+
+
+
+
+
+
+
+ Time
+
+
+
+
+
+
+
+
+
+
+ 08:30
+
+
+ Joanne Marquardt
+ NHS: 999 054 3666
+
+
+ 15 March 1960
+ (66 years)
+
+
+
+ Special appointment
+
+
+
+
+ Rebook participant
+
+
+ Secondary actions
+
+
+
+ 08:40
+ Held: why?
+
+ Release slot
+
+
+ Secondary actions
+
+
+
+
+ 08:50
+ +09:00
+
+
+ Daphne Hart
+ NHS: 821 374 0952
+
+
+ 22 July 1958
+ (67 years)
+
+
+
+ Special appointment x 2
+
+
+
+
+ Rebook participant
+
+
+ Secondary actions
+
+
+
+
+ 09:10
+
+
+ Marta O'Neill
+ NHS: 702 195 4836
+
+
+ 3 May 1971
+ (55 years)
+
+
+ View appointment or a tiny note
+
+
+ Rebook participant
+
+
+ Secondary actions
+
+
+
+
+
+ 09:10
+ 614 287 9305
+ Susan Carter
+ 19 September 1967
+
+
+ 09:20
+ 505 463 1129
+ Olivia Barnes
+ 12 January 1975
+
+
+ 09:30
+ 336 774 2201
+ Rachel Price
+ 27 April 1962
+
+
+ 09:40
+ 223 546 8870
+ Hannah Wallace
+ 6 October 1959
+
+
+ 09:50
+ 410 559 3322
+ Fiona Lambert
+ 14 December 1973
+
+
+ 10:00
+ 196 804 7753
+ Georgia Mills
+ 30 June 1968
+
+
+ 10:10
+ 378 290 4601
+ Elaine Fox
+ 11 February 1976
+
+
+ 10:20
+ 957 613 2084
+ Tessa King
+ 25 August 1963
+
+
+ 10:30
+ 604 118 9756
+ Nadia Singh
+ 7 April 1957
+
+
+ 10:40
+ 842 039 6617
+ Jane Reed
+ 18 November 1970
+
+
+ 10:50
+ 731 259 1148
+ Laura White
+ 29 May 1965
+
+
+ 11:00
+ 665 880 3320
+ Marissa Young
+ 9 March 1961
+
+
+ 11:10
+ 548 901 7732
+ Naomi Bell
+ 21 September 1974
+
+
+ 11:20
+ 439 216 5087
+ Paige Long
+ 4 July 1969
+
+
+ 11:30
+ 329 487 9901
+ Catherine Wood
+ 16 January 1956
+
+
+ 11:40
+ 214 603 1774
+ Victoria Hale
+ 13 October 1966
+
+
+ 11:50
+ 188 905 6423
+ Imogen Frost
+ 31 December 1972
+
+
+ 12:00
+ 977 332 1108
+ Dara Moss
+ 24 June 1960
+
+
+ 12:10
+ 861 427 5930
+ Sophie Hartley
+ 2 August 1962
+
+
+ 12:20
+ 750 218 3446
+ Emma Cole
+ 17 April 1975
+
+
+
+
-
-
-
-
-
-
-
-
-
- Afternoon session
- 13:30 to 17:30
-
-
-
-
-
- 24 slots available of 24
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
{% endblock %}
diff --git a/app/views/clinics/z-older-versions/2026-06-day-slot-view.html b/app/views/clinics/z-older-versions/2026-06-day-slot-view.html
new file mode 100644
index 0000000..f62178b
--- /dev/null
+++ b/app/views/clinics/z-older-versions/2026-06-day-slot-view.html
@@ -0,0 +1,536 @@
+{% extends 'layout.html' %}
+
+{% set pageName = "Day clinic view" %}
+
+{% from "_includes/primary-navigation.html" import primaryNavigation %}
+{% block header %}
+ {{ primaryNavigation("Clinics", serviceName) }}
+{% endblock %}
+
+{% block beforeContent %}
+{{ breadcrumb({
+ items: [
+ {
+ href: "#",
+ text: "Home"
+ },
+ {
+ href: "/clinics/",
+ text: "Clinics"
+ },
+ {
+ href: "clinic-detail",
+ text: "Worthing C2C test 1"
+ }
+ ]
+}) }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ Tuesday
+ 1 September 2026
+
+ {% set secondaryNavItems = [] %}
+ {% for item in [
+ { href: '#', label: 'One modality name (42 slots available)', selected: 'true' },
+ { href: '#', label: 'Another modality name (0 slots available)' }
+ ] %}
+ {% set secondaryNavItems = secondaryNavItems | push({
+ text: item.label | safe,
+ href: item.href | trim,
+ current: true if item.selected == 'true'
+ }) %}
+ {% endfor %}
+
+ {{ appSecondaryNavigation({
+ visuallyHiddenTitle: "Secondary menu",
+ items: secondaryNavItems
+ }) }}
+
+
+ Morning session
+ 08:30 to 12:30
+
+
+
+
+
+ 18 slots available of 21
+
+
+
+
+
+
+
08:30
+
Held
+
+ Release hold
+ More actions...
+
+
+
+
+
+
+ 08:50
+
+
+ 09:00
+
+
+
+
+
+
+
+
+
+
+
+ 09:30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Afternoon session
+ 13:30 to 17:30
+
+
+
+
+
+ 24 slots available of 24
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/app/views/september-iteration-2/clickthru/03-select-clinic.html b/app/views/september-iteration-2/clickthru/03-select-clinic.html
index 78a1aa2..257a582 100644
--- a/app/views/september-iteration-2/clickthru/03-select-clinic.html
+++ b/app/views/september-iteration-2/clickthru/03-select-clinic.html
@@ -19,7 +19,7 @@
- {% from "september-iteration-2/macros/clinic-listing.html" import clinic_listing %}
+ {% from "_macros/clinic-listing.html" import clinic_listing %}
{{ clinic_listing({
controls: true,
clinics: [
diff --git a/app/views/september-iteration-2/clickthru/05-review.html b/app/views/september-iteration-2/clickthru/05-review.html
index 5e04f16..1df5e4b 100644
--- a/app/views/september-iteration-2/clickthru/05-review.html
+++ b/app/views/september-iteration-2/clickthru/05-review.html
@@ -72,7 +72,7 @@
into {{ data.clinicName }}
- {% from "september-iteration-2/macros/clinic-listing.html" import clinic_listing %}
+ {% from "_macros/clinic-listing.html" import clinic_listing %}
{{ clinic_listing({
controls: false,
clinics: [