Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions app/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const express = require('express')

const router = express.Router()

const isWholeNumber = str => /^\d+$/.test(str)

router.get('/september-iteration-2/clickthru/04a-example-search-result', function (req, res) {
const query = (req.query['search-params'] || '').trim()
const allParticipants = (req.session.data.participants && req.session.data.participants.default) || []
Expand Down Expand Up @@ -31,6 +33,64 @@ router.get('/september-iteration-2/clickthru/04a-example-search-result', functio
})
})

router.post('/sessions/02-organise-slots', function (req, res) {
const session = (req.body && req.body.newSession) || {}
const startTime = session.startTime || {}
const endTime = session.endTime || {}

const startHourStr = (startTime.hour || '').trim()
const startMinuteStr = (startTime.minute || '').trim()
const endHourStr = (endTime.hour || '').trim()
const endMinuteStr = (endTime.minute || '').trim()
const durationStr = (session.duration || '').trim()

const startHour = parseInt(startHourStr, 10)
const startMinute = parseInt(startMinuteStr, 10)
const endHour = parseInt(endHourStr, 10)
const endMinute = parseInt(endMinuteStr, 10)
const duration = parseInt(durationStr, 10)

const validStartHour = isWholeNumber(startHourStr) && startHour >= 0 && startHour <= 23
const validStartMinute = isWholeNumber(startMinuteStr) && startMinute >= 0 && startMinute <= 59
const validEndHour = isWholeNumber(endHourStr) && endHour >= 0 && endHour <= 23
const validEndMinute = isWholeNumber(endMinuteStr) && endMinute >= 0 && endMinute <= 59

const errors = {}

if (!validStartHour || !validStartMinute) {
errors.startTime = 'Start time must be entered, in 24 hour format'
}

if (!validEndHour || !validEndMinute) {
errors.endTime = 'End time must be entered, in 24 hour format'
}

const validDuration = isWholeNumber(durationStr) && duration > 0
if (!validDuration) {
errors.duration = 'Slot length must be entered, in minutes, as a whole number'
}

if (!errors.startTime && !errors.endTime) {
const startTotalMinutes = startHour * 60 + startMinute
const endTotalMinutes = endHour * 60 + endMinute
if (endTotalMinutes <= startTotalMinutes) {
errors.startTime = 'Start time must be earlier than end time'
errors.endTime = 'End time must be later than start time'
} else if (!errors.duration && duration > endTotalMinutes - startTotalMinutes) {
errors.duration = 'Slot length must be shorter than the total session duration'
}
}

req.session.data.newSession = session

if (Object.keys(errors).length > 0) {
return res.render('sessions/01-set-timings', {
errors
})
}
res.redirect('/sessions/02-organise-slots')
})

router.get('/action/stage/:participantId', function (req, res) {
const participants = (req.session.data.participants && req.session.data.participants.default) || []
const participantIndex = participants.findIndex((participant) => participant.participantId === req.params.participantId)
Expand Down
8 changes: 8 additions & 0 deletions app/views/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ <h1 class="nhsuk-heading-xl">

<hr>

<h2 class="nhsuk-heading-m">July 2026</h2>
<p>Sessions: first run</p>
<ol>
<li>
<a href="/sessions/01-set-timings">Set up session</a>
</li>
</ol>

<h2 class="nhsuk-heading-m">May–June 2026</h2>
<p>Clinics: first run</p>
<ol>
Expand Down
260 changes: 260 additions & 0 deletions app/views/sessions/01-set-timings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
{% extends 'layout.html' %}

{% set errorState = "true" if errors else "false" %}

{% set pageName = "Create session: set timings" %}

{% from "_includes/primary-navigation.html" import primaryNavigation %}
{% block header %}
{{ primaryNavigation("Clinics", serviceName) }}
{% endblock %}

{% block beforeContent %}{% endblock %}

{% block content %}

{% set tickSvg %}
<svg class="app-icon app-icon--tick" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" d="M18.4 7.8l-8.5 8.4L5.6 12"></path>
</svg>
{% endset %}

<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-one-quarter">

<div class="app-workflow-side-nav-wrapper">
<nav class="app-workflow-side-nav">
<h2 class="nhsuk-heading-m app-workflow-side-nav__heading">
<span class="nhsuk-caption-m">
Workflow steps
</span>
Create session
</h2>
<ol class="app-workflow-side-nav__list">
<li class="app-workflow-side-nav__item app-workflow-side-nav__item--current">
<span class="app-workflow-side-nav__link" aria-current="page">
<span class="app-workflow-side-nav__number">1</span>
<span class="app-workflow-side-nav__label">Set timings</span>
</span>
</li>
<li class="app-workflow-side-nav__item app-workflow-side-nav__item--disabled">
<span class="app-workflow-side-nav__link app-workflow-side-nav__link--disabled">
<span class="app-workflow-side-nav__number">2</span>
<span class="app-workflow-side-nav__label">Organise slots</span>
</span>
</li>
<li class="app-workflow-side-nav__item app-workflow-side-nav__item--disabled">
<span class="app-workflow-side-nav__link app-workflow-side-nav__link--disabled">
<span class="app-workflow-side-nav__number">3</span>
<span class="app-workflow-side-nav__label">Publish</span>
</span>
</li>
</ol>
</nav>
</div>

</div>
<div class="nhsuk-grid-column-three-quarters">
<div class="nhsuk-u-reading-width">

{% if errorState === "true" %}
{% set errorList = [] %}
{% if errors.startTime %}
{% set errorList = errorList.concat([{ text: errors.startTime, href: "#start-time-hour" }]) %}
{% endif %}
{% if errors.endTime %}
{% set errorList = errorList.concat([{ text: errors.endTime, href: "#end-time-hour" }]) %}
{% endif %}
{% if errors.duration %}
{% set errorList = errorList.concat([{ text: errors.duration, href: "#duration" }]) %}
{% endif %}
{{ errorSummary({
titleText: "There is a problem",
errorList: errorList
}) }}
{% endif %}

<h1 class="nhsuk-heading-l">Set timings</h1>

<form action="02-organise-slots" method="post">

<div class="nhsuk-form-group {% if errors and errors.startTime %}nhsuk-form-group--error{% endif %}">
<fieldset class="nhsuk-fieldset" role="group" aria-describedby="start-time-hint">
<legend class="nhsuk-fieldset__legend nhsuk-label--m">
Session start time
</legend>
<div id="start-time-hint" class="nhsuk-hint">
Use 24 hour format, for example, 10:00
</div>
{% if errors and errors.startTime %}
<span class="nhsuk-error-message">
<span class="nhsuk-u-visually-hidden">Error:</span> {{ errors.startTime }}
</span>
{% endif %}

<div id="start-time" class="nhsuk-date-input">
<div class="nhsuk-date-input__item">
<div class="nhsuk-form-group">
<label class="nhsuk-label nhsuk-date-input__label" for="start-time-hour">
Hour
</label>
<input class="nhsuk-input nhsuk-date-input__input {% if errors and errors.startTime %}nhsuk-input--error{% endif %} nhsuk-input--width-2" id="start-time-hour"
name="newSession[startTime][hour]" type="text" inputmode="numeric" value="{{ data.newSession.startTime.hour if data.newSession else '' }}">
</div>
</div>

<div class="nhsuk-date-input__item">
<div class="nhsuk-form-group">
<label class="nhsuk-label nhsuk-date-input__label" for="start-time-minute">
Minute
</label>
<input class="nhsuk-input nhsuk-date-input__input {% if errors and errors.startTime %}nhsuk-input--error{% endif %} nhsuk-input--width-2" id="start-time-minute"
name="newSession[startTime][minute]" type="text" inputmode="numeric" value="{{ data.newSession.startTime.minute if data.newSession else '' }}">
</div>
</div>
</div>
</fieldset>
</div>

<div class="nhsuk-form-group {% if errors and errors.endTime %}nhsuk-form-group--error{% endif %}">
<fieldset class="nhsuk-fieldset" role="group" aria-describedby="end-time-hint">
<legend class="nhsuk-fieldset__legend nhsuk-label--m">
Session end time
</legend>
<div id="end-time-hint" class="nhsuk-hint">
Use 24 hour format, for example, 17:00
</div>
{% if errors and errors.endTime %}
<span class="nhsuk-error-message">
<span class="nhsuk-u-visually-hidden">Error:</span> {{ errors.endTime }}
</span>
{% endif %}

<div id="end-time" class="nhsuk-date-input">
<div class="nhsuk-date-input__item">
<div class="nhsuk-form-group">
<label class="nhsuk-label nhsuk-date-input__label" for="end-time-hour">
Hour
</label>
<input class="nhsuk-input nhsuk-date-input__input {% if errors and errors.endTime %}nhsuk-input--error{% endif %} nhsuk-input--width-2" id="end-time-hour"
name="newSession[endTime][hour]" type="text" inputmode="numeric" value="{{ data.newSession.endTime.hour if data.newSession else '' }}">
</div>
</div>
<div class="nhsuk-date-input__item">
<div class="nhsuk-form-group">
<label class="nhsuk-label nhsuk-date-input__label" for="end-time-minute">
Minute
</label>
<input class="nhsuk-input nhsuk-date-input__input {% if errors and errors.endTime %}nhsuk-input--error{% endif %} nhsuk-input--width-2" id="end-time-minute"
name="newSession[endTime][minute]" type="text" inputmode="numeric" value="{{ data.newSession.endTime.minute if data.newSession else '' }}">
</div>
</div>
</div>
</fieldset>
</div>

<div class="nhsuk-form-group {% if errors and errors.duration %}nhsuk-form-group--error{% endif %}">
<label class="nhsuk-label nhsuk-label--m" for="duration">
Slot length
</label>
{% if errors and errors.duration %}
<span class="nhsuk-error-message">
<span class="nhsuk-u-visually-hidden">Error:</span> {{ errors.duration }}
</span>
{% endif %}
<div class="nhsuk-input-wrapper">
<input class="nhsuk-input nhsuk-input--width-2 {% if errors and errors.duration %}nhsuk-input--error{% endif %}" id="duration" name="newSession[duration]" type="text"
spellcheck="false" value="{{ data.newSession.duration if data.newSession else '' }}" aria-describedby="duration-hint" inputmode="numeric">
<div class="nhsuk-input-wrapper__suffix" aria-hidden="true">minutes</div>
</div>
</div>

<div class="nhsuk-inset-text">
<span class="nhsuk-u-visually-hidden">Information: </span>
<p class="nhsuk-body nhsuk-u-margin-bottom-1"><strong>Capacity calculator</strong></p>
<p class="nhsuk-body nhsuk-u-margin-bottom-1"><span data-capacity-calculator-total>X</span> total slots in the
session</p>
<input type="hidden" id="totalSlots" name="newSession[totalSlots]">
</div>

<button class="nhsuk-button" type="submit">Continue</button>

</form>
</div>
</div>
</div>

{% endblock %}

{% block pageScripts %}
<script>
(function () {
const fieldIds = [
'start-time-hour',
'start-time-minute',
'end-time-hour',
'end-time-minute',
'duration'
];

const fields = fieldIds
.map(id => document.getElementById(id))
.filter(Boolean);
const totalElement = document.querySelector('[data-capacity-calculator-total]');
const totalInput = document.getElementById('totalSlots');

if (!totalElement || !totalInput || fields.length === 0) {
return;
}

const parseInteger = value => {
const parsed = parseInt(value, 10);
return Number.isNaN(parsed) ? NaN : parsed;
};

const validTime = (hour, minute) =>
Number.isInteger(hour) && hour >= 0 && hour <= 23 &&
Number.isInteger(minute) && minute >= 0 && minute <= 59;

const calculateTotalSlots = (startHour, startMinute, endHour, endMinute, duration) => {
if (
!validTime(startHour, startMinute) ||
!validTime(endHour, endMinute) ||
!Number.isInteger(duration) ||
duration <= 0
) {
return 0;
}

const startMinutes = startHour * 60 + startMinute;
const endMinutes = endHour * 60 + endMinute;
const availableMinutes = endMinutes - startMinutes;

if (availableMinutes <= 0) {
return 0;
}

return Math.floor(availableMinutes / duration);
};

const updateTotal = () => {
const startHour = parseInteger(document.getElementById('start-time-hour').value);
const startMinute = parseInteger(document.getElementById('start-time-minute').value);
const endHour = parseInteger(document.getElementById('end-time-hour').value);
const endMinute = parseInteger(document.getElementById('end-time-minute').value);
const duration = parseInteger(document.getElementById('duration').value);

const totalSlots = calculateTotalSlots(startHour, startMinute, endHour, endMinute, duration);
totalElement.textContent = String(totalSlots);
totalInput.value = String(totalSlots);
};

fields.forEach(field => {
field.addEventListener('input', updateTotal);
field.addEventListener('change', updateTotal);
});

updateTotal();
})();
</script>
{% endblock %}
Loading