Skip to content
Open
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
30 changes: 30 additions & 0 deletions app/assets/sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,36 @@ a {
width: 100%;
}

.app-participants-toolbar {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 24px;
margin-bottom: 24px;

> p {
margin-bottom: 0;
}
}

.app-participants-sort-form {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 0;

.nhsuk-label {
margin-bottom: 0;
white-space: nowrap;
}
}

@include nhsuk-media-query($from: tablet) {
.app-participants-sort-form {
flex-shrink: 0;
}
}


.nhsuk-grid-column-four-fifths {
@include nhsuk-grid-column(four-fifths);
Expand Down
328 changes: 176 additions & 152 deletions app/data/participants.js

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions app/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,63 @@ const express = require('express')

const router = express.Router()

const participantMonths = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
]

const participantDateFromToday = (today, days) => {
const date = new Date(today)
date.setDate(date.getDate() + days)
return date
}

const formatParticipantDate = (date) => `${String(date.getDate()).padStart(2, '0')} ${participantMonths[date.getMonth()]} ${date.getFullYear()}`

const formatParticipantRelativeDate = (date, today) => {
const days = Math.max(0, Math.round((date - today) / 86400000))

if (days < 7) {
return `In ${days} day${days === 1 ? '' : 's'}`
}

const weeks = Math.round(days / 7)
if (weeks < 8) {
return `In ${weeks} week${weeks === 1 ? '' : 's'}`
}

const months = Math.max(1, Math.round(days / 30))
return `In ${months} month${months === 1 ? '' : 's'}`
}

const refreshParticipantDates = (participant, today) => {
const nextTestDueDate = participantDateFromToday(today, participant.next_test_due_days)
const nextAppointmentDate = participant.next_appointment_days === null
? null
: participantDateFromToday(today, participant.next_appointment_days)

return {
...participant,
next_test_due_date: formatParticipantDate(nextTestDueDate),
next_test_due_date_value: nextTestDueDate.getTime(),
next_test_due_date_relative: formatParticipantRelativeDate(nextTestDueDate, today),
next_appointment_date: nextAppointmentDate ? formatParticipantDate(nextAppointmentDate) : 'Not known',
next_appointment_date_value: nextAppointmentDate ? nextAppointmentDate.getTime() : null,
next_appointment_date_relative: nextAppointmentDate ? formatParticipantRelativeDate(nextAppointmentDate, today) : 'Not known'
}
}

const compareAppointmentDates = (a, b, direction) => {
const aUnknown = a.next_appointment_date_value === null
const bUnknown = b.next_appointment_date_value === null

if (aUnknown && bUnknown) return 0
if (aUnknown) return 1
if (bUnknown) return -1

return direction * (a.next_appointment_date_value - b.next_appointment_date_value)
}

//======= September test specific for now

// Utility function to generate calendar month data
Expand Down Expand Up @@ -724,6 +781,59 @@ router.post('/sessions/02-organise-slots', function (req, res) {
// Isolated September Test (Mission 1) routes
router.use(require('./routes/mission-1'));

router.get('/participants', function (req, res) {
const sort = req.query.sort || 'due-soonest'
const today = new Date()
today.setHours(0, 0, 0, 0)
const allParticipants = ((req.session.data.participants && req.session.data.participants.default) || [])
.map(participant => refreshParticipantDates(participant, today))
const sortComparators = {
'name-asc': (a, b) => a.surname_sort_value.localeCompare(b.surname_sort_value),
'name-desc': (a, b) => b.surname_sort_value.localeCompare(a.surname_sort_value),
'due-soonest': (a, b) => a.next_test_due_date_value - b.next_test_due_date_value,
'due-latest': (a, b) => b.next_test_due_date_value - a.next_test_due_date_value,
'screening-soonest': (a, b) => compareAppointmentDates(a, b, 1),
'screening-latest': (a, b) => compareAppointmentDates(a, b, -1),
'age-oldest': (a, b) => b.age - a.age,
'age-youngest': (a, b) => a.age - b.age
}

allParticipants.sort(sortComparators[sort] || sortComparators['name-asc'])

const pageSize = 100
const totalParticipants = allParticipants.length
const totalPages = Math.max(1, Math.ceil(totalParticipants / pageSize))
const requestedPage = parseInt(req.query.page, 10) || 1
const currentPage = Math.min(Math.max(requestedPage, 1), totalPages)
const firstRecord = totalParticipants ? ((currentPage - 1) * pageSize) + 1 : 0
const lastRecord = Math.min(currentPage * pageSize, totalParticipants)
const participants = allParticipants.slice(firstRecord - 1, lastRecord)

res.render('participants/index', {
participants,
selectedSort: sort,
currentPage,
totalPages,
firstRecord,
lastRecord,
totalParticipants,
paginationPages: Array.from({ length: totalPages }, (_, index) => index + 1)
})
})

router.get('/participants/:participantId', function (req, res) {
const participants = (req.session.data.participants && req.session.data.participants.default) || []
const today = new Date()
today.setHours(0, 0, 0, 0)
const participant = participants.find(item => item.participantId === req.params.participantId)

if (!participant) {
return res.status(404).send('Participant not found')
}

res.render('participants/detail', { participant: refreshParticipantDates(participant, today) })
})

// Isolated create capacity from zero routes
router.use(require('./routes/create-capacity-from-zero'));

Expand Down
2 changes: 1 addition & 1 deletion app/views/_includes/primary-navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
active: activeItem === "Clinics"
},
{
href: "#",
href: "/participants/",
text: "Participants",
active: activeItem === "Participants"
},
Expand Down
9 changes: 9 additions & 0 deletions app/views/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ <h1 class="nhsuk-heading-xl">
href: "/prototype-admin/reset?returnPage=" + (currentPage | urlencode)
}) }}

<h2 class="nhsuk-heading-m">September 2026</h2>
<p>Participant view</p>

<ol>
<li>
<a href="/participants/">View participant details</a>
</li>
</ol>

<h2 class="nhsuk-heading-m">August &ndash; September 2026</h2>
<p>Clinics with schedules ideation</p>
<ol>
Expand Down
162 changes: 162 additions & 0 deletions app/views/participants/detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
{% extends 'layout.html' %}

{% set pageName = 'Participant details' %}

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

{% block content %}
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-full">
<span class="nhsuk-caption-l">Participant details</span>
<h1 class="nhsuk-heading-l">{{ participant.full_name }}</h1>
</div>
</div>

<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">
<section class="nhsuk-card nhsuk-u-margin-bottom-5{% if participant.is_breaching %} app-participant-due-date-card--warning{% endif %}"
aria-labelledby="next-test-due-date-heading next-screening-date-heading">
<div class="nhsuk-card__content">
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-full">
{% if participant.is_breaching %}
<strong class="nhsuk-tag nhsuk-tag--red nhsuk-u-margin-bottom-4">At risk of breaching</strong>
{% endif %}
</div>
</div>
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-one-half">
<h2 class="nhsuk-card__heading nhsuk-heading-m" id="next-test-due-date-heading">Next test due date</h2>
<p class="nhsuk-heading-s nhsuk-u-margin-bottom-1">{{ participant.next_test_due_date }}</p>
<p class="nhsuk-hint nhsuk-u-margin-bottom-0">{{ participant.next_test_due_date_relative }}</p>
</div>
<div class="nhsuk-grid-column-one-half">
<h2 class="nhsuk-card__heading nhsuk-heading-m" id="next-screening-date-heading">Next screening date</h2>
{% if participant.next_appointment_date != 'Not known' %}
<p class="nhsuk-heading-s nhsuk-u-margin-bottom-1">{{ participant.next_appointment_date }}</p>
{% endif %}
<p class="nhsuk-hint nhsuk-u-margin-bottom-0">{{ participant.next_appointment_date_relative }}</p>
</div>
</div>
</div>
</section>
</div>
</div>

{% set stageText = {
'scheduled': 'Scheduled',
'mammograms': 'Mammograms',
'reading': 'Waiting for reading',
'assessment': 'At assessment',
'closed': 'Closed'
}[participant.episode_stage] %}
{% set stageColour = {
'scheduled': 'blue',
'mammograms': 'purple',
'reading': 'yellow',
'assessment': 'orange',
'closed': 'grey'
}[participant.episode_stage] %}

<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">
<section class="nhsuk-card nhsuk-u-margin-bottom-5">
<div class="nhsuk-card__content">
<h2 class="nhsuk-card__heading nhsuk-heading-m">Personal details</h2>
<dl class="nhsuk-summary-list">
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">NHS Number</dt>
<dd class="nhsuk-summary-list__value">{{ participant.nhs_number }}</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">SX Number</dt>
<dd class="nhsuk-summary-list__value">{{ participant.sx_number }}</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Full name</dt>
<dd class="nhsuk-summary-list__value">{{ participant.full_name }}</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Date of birth</dt>
<dd class="nhsuk-summary-list__value">{{ participant.date_of_birth }} <br><span
class="nhsuk-hint">({{ participant.age }} years old)</span></dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Ethnicity</dt>
<dd class="nhsuk-summary-list__value">{{ participant.ethnicity }}</dd>
</div>
</dl>
</div>
</section>

<section class="nhsuk-card nhsuk-u-margin-bottom-5">
<div class="nhsuk-card__content">
<h2 class="nhsuk-card__heading nhsuk-heading-m">Contact detail</h2>
<dl class="nhsuk-summary-list">
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Address</dt>
<dd class="nhsuk-summary-list__value">
{{ participant.address.houseNumber }} {{ participant.address.street }}<br>
{{ participant.address.town }}<br>
{{ participant.address.postcode }}
</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">
{% if participant.phone_numbers.mobile and participant.phone_numbers.home %}
Phone numbers
{% else %}
Phone number
{% endif %}
</dt>
<dd class="nhsuk-summary-list__value">
{% if participant.phone_numbers.mobile %}{{ participant.phone_numbers.mobile }}{% endif %}
{% if participant.phone_numbers.mobile and participant.phone_numbers.home %}<br>{% endif %}
{% if participant.phone_numbers.home %}{{ participant.phone_numbers.home }}{% endif %}
</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Email address</dt>
<dd class="nhsuk-summary-list__value">{{ participant.email }}</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Special appointment required</dt>
<dd class="nhsuk-summary-list__value">{{ participant.special_appointment_required }}</dd>
</div>
{% if participant.special_appointment_required == 'Yes' %}
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Special appointment information</dt>
<dd class="nhsuk-summary-list__value">{{ participant.special_appointment_information or '-' }}
</dd>
</div>
{% endif %}
</dl>
</div>
</section>

<section class="nhsuk-card nhsuk-u-margin-bottom-5">
<div class="nhsuk-card__content">
<h2 class="nhsuk-card__heading nhsuk-heading-m">Current screening episode</h2>
<dl class="nhsuk-summary-list">
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Stage</dt>
<dd class="nhsuk-summary-list__value">
<strong class="nhsuk-tag nhsuk-tag--{{ stageColour }}">{{ stageText }}</strong>
</dd>
</div>
<div class="nhsuk-summary-list__row">
<dt class="nhsuk-summary-list__key">Next screening appointment</dt>
<dd class="nhsuk-summary-list__value">
{% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }}<br>{% endif %}
<span class="nhsuk-hint">{{ participant.next_appointment_date_relative }}</span>
</dd>
</div>
</dl>
</div>
</section>
</div>
</div>
{% endblock %}
Loading