From 01a9cd1b9094e0d429c9c0bcd0ee151521b44d8b Mon Sep 17 00:00:00 2001 From: danjohnstonUX <21033686+danjohnstonUX@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:58:13 +0100 Subject: [PATCH 01/14] added initial search ideation and added sortable and striped tables to participant lists --- app/assets/images/nhsuk-icon-arrow-down.svg | 3 + .../images/nhsuk-icon-arrow-up-down.svg | 5 + app/assets/images/nhsuk-icon-arrow-up.svg | 3 + app/assets/sass/main.scss | 4 + app/routes.js | 63 ++++++++- app/views/batches/participants/index.html | 58 +++++---- app/views/participants/index.html | 46 ++----- app/views/participants/search-results.html | 99 ++++++++++++++ app/views/participants/search.html | 121 ++++++++++++++++++ 9 files changed, 340 insertions(+), 62 deletions(-) create mode 100644 app/assets/images/nhsuk-icon-arrow-down.svg create mode 100644 app/assets/images/nhsuk-icon-arrow-up-down.svg create mode 100644 app/assets/images/nhsuk-icon-arrow-up.svg create mode 100644 app/views/participants/search-results.html create mode 100644 app/views/participants/search.html diff --git a/app/assets/images/nhsuk-icon-arrow-down.svg b/app/assets/images/nhsuk-icon-arrow-down.svg new file mode 100644 index 00000000..619714f5 --- /dev/null +++ b/app/assets/images/nhsuk-icon-arrow-down.svg @@ -0,0 +1,3 @@ + diff --git a/app/assets/images/nhsuk-icon-arrow-up-down.svg b/app/assets/images/nhsuk-icon-arrow-up-down.svg new file mode 100644 index 00000000..ab69b3c7 --- /dev/null +++ b/app/assets/images/nhsuk-icon-arrow-up-down.svg @@ -0,0 +1,5 @@ + diff --git a/app/assets/images/nhsuk-icon-arrow-up.svg b/app/assets/images/nhsuk-icon-arrow-up.svg new file mode 100644 index 00000000..ed28b96a --- /dev/null +++ b/app/assets/images/nhsuk-icon-arrow-up.svg @@ -0,0 +1,3 @@ + diff --git a/app/assets/sass/main.scss b/app/assets/sass/main.scss index cdf5f741..2ba9852b 100755 --- a/app/assets/sass/main.scss +++ b/app/assets/sass/main.scss @@ -214,6 +214,10 @@ a { } } +.app-search-criteria dd { + margin-inline-start: 0; +} + @include nhsuk-media-query($from: tablet) { .app-participants-sort-form { flex-shrink: 0; diff --git a/app/routes.js b/app/routes.js index 965a0818..bc5a69a8 100644 --- a/app/routes.js +++ b/app/routes.js @@ -20,6 +20,7 @@ const participantDateFromToday = (today, days) => { } const formatParticipantDate = (date) => `${String(date.getDate()).padStart(2, '0')} ${participantMonths[date.getMonth()]} ${date.getFullYear()}` +const formatParticipantIsoDate = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` const formatParticipantFullDate = (date) => `${String(date.getDate()).padStart(2, '0')} ${participantFullMonths[date.getMonth()]} ${date.getFullYear()}` const formatParticipantRelativeDate = (date, today, daysThreshold = 7) => { @@ -52,9 +53,11 @@ const refreshParticipantDates = (participant, today, dateFormatter = formatParti last_screened_date: lastScreenedDate ? dateFormatter(lastScreenedDate) : 'Never screened', next_test_due_date: dateFormatter(nextTestDueDate), next_test_due_date_value: nextTestDueDate.getTime(), + next_test_due_date_iso: formatParticipantIsoDate(nextTestDueDate), next_test_due_date_relative: formatParticipantRelativeDate(nextTestDueDate, today, 28), next_appointment_date: nextAppointmentDate ? dateFormatter(nextAppointmentDate) : 'Not known', next_appointment_date_value: nextAppointmentDate ? nextAppointmentDate.getTime() : null, + next_appointment_date_iso: nextAppointmentDate ? formatParticipantIsoDate(nextAppointmentDate) : '9999-12-31', next_appointment_date_relative: nextAppointmentDate ? formatParticipantRelativeDate(nextAppointmentDate, today) : 'Not known' } } @@ -911,7 +914,65 @@ router.get(['/participants', '/batches/participants'], function (req, res) { }) }) -router.get(['/participants/:participantId', '/batches/participants/:participantId'], function (req, res) { +const participantSearchFields = ['firstName', 'lastName', 'nhsNumber', 'sxNumber', 'dobDay', 'dobMonth', 'dobYear', 'postcode', 'emailAddress'] + +const participantSearchCriteria = query => Object.fromEntries(participantSearchFields + .map(field => [field, String(query[field] || '').trim()])) + +const normaliseSearchValue = value => String(value || '').toLowerCase().replace(/\s+/g, '') + +const matchesParticipantSearch = (participant, criteria) => { + const includes = (value, term) => !term || normaliseSearchValue(value).includes(normaliseSearchValue(term)) + const [dobDay, dobMonthName, dobYear] = participant.date_of_birth.split(' ') + const dobMonth = participantFullMonths.indexOf(dobMonthName) + 1 + const matchesDatePart = (actual, term) => !term || parseInt(term, 10) === parseInt(actual, 10) + const matchesYear = !criteria.dobYear || (criteria.dobYear.length === 2 + ? dobYear.endsWith(criteria.dobYear) + : parseInt(criteria.dobYear, 10) === parseInt(dobYear, 10)) + + return includes(participant.full_name.split(', ')[1], criteria.firstName) && + includes(participant.surname_sort_value, criteria.lastName) && + includes(participant.nhs_number, criteria.nhsNumber) && + includes(participant.sx_number, criteria.sxNumber) && + includes(participant.address.postcode, criteria.postcode) && + includes(participant.email, criteria.emailAddress) && + matchesDatePart(dobDay, criteria.dobDay) && + matchesDatePart(dobMonth, criteria.dobMonth) && + matchesYear +} + +router.get('/participants/search', function (req, res) { + res.render('participants/search', { + participantsPath: '/participants', + criteria: participantSearchCriteria(req.query) + }) +}) + +router.get('/participants/search-results', function (req, res) { + const criteria = participantSearchCriteria(req.query) + const today = new Date() + today.setHours(0, 0, 0, 0) + const participants = ((req.session.data.participants && req.session.data.participants.default) || []) + .filter(participant => matchesParticipantSearch(participant, criteria)) + .map(participant => refreshParticipantDates(participant, today)) + .sort((a, b) => a.full_name.localeCompare(b.full_name)) + const searchQuery = new URLSearchParams(Object.entries(criteria).filter(([, value]) => value)).toString() + + res.render('participants/search-results', { + participants, + participantsPath: '/participants', + criteria, + hasCriteria: Boolean(searchQuery), + searchQuery + }) +}) + +router.get(['/participants/:participantId', '/batches/participants/:participantId'], function (req, res, next) { + // Let static pages such as /participants/search fall through to auto-routing + if (!/^p\d+$/.test(req.params.participantId)) { + return next() + } + const participantsPath = req.path.startsWith('/batches/') ? '/batches/participants' : '/participants' const participants = (req.session.data.participants && req.session.data.participants.default) || [] const today = new Date() diff --git a/app/views/batches/participants/index.html b/app/views/batches/participants/index.html index 1ee942bd..af46e431 100644 --- a/app/views/batches/participants/index.html +++ b/app/views/batches/participants/index.html @@ -99,45 +99,47 @@
Showing {{ firstRecord }} to {{ lastRecord }} of {{ totalParticipants }} participants
-| Name | -Postcode | -NHS number | -Next test due date | -Next screening | ++ Name + | ++ Postcode + | ++ NHS number + | ++ Next test due date + | ++ Next screening + | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ participant.full_name }} {{ participant.age }} years old |
+
+
+ {{ participant.age }} years old + |
{{ participant.address.postcode }} | {{ participant.nhs_number }} | -{{ participant.next_test_due_date }} {{ participant.next_test_due_date_relative }} |
- {% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif %}{{ participant.next_appointment_date_relative }} |
+
+ {{ participant.next_test_due_date }} + {{ participant.next_test_due_date_relative }} + |
+
+ {% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif + %} + {{ participant.next_appointment_date_relative }} + |
| + | Name | -+ | Postcode | -+ | NHS number | -+ | Next test due date | -+ | Next screening | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
- {{ participant.full_name }} - {{ participant.age }} years old + + {{ participant.age }} years old |
{{ participant.address.postcode }} | {{ participant.nhs_number }} | -+ |
{{ participant.next_test_due_date }} {{ participant.next_test_due_date_relative }} |
- + |
{% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif %} {{ participant.next_appointment_date_relative }} diff --git a/app/views/participants/search-results.html b/app/views/participants/search-results.html new file mode 100644 index 00000000..a30422fe --- /dev/null +++ b/app/views/participants/search-results.html @@ -0,0 +1,99 @@ +{% extends 'layout.html' %} + +{% set pageName = "Search results" %} + +{% from "_includes/primary-navigation.html" import primaryNavigation %} +{% block header %} +{{ primaryNavigation("Participants", serviceName) }} +{% endblock %} + +{% block beforeContent %}{% endblock %} + +{% block content %} + +
+
+
+
+
+
+
+ {{ pageName }}+
+
+
+{% endblock %}
diff --git a/app/views/participants/search.html b/app/views/participants/search.html
new file mode 100644
index 00000000..9a57ff02
--- /dev/null
+++ b/app/views/participants/search.html
@@ -0,0 +1,121 @@
+{% extends 'layout.html' %}
+
+{% set pageName = "Participants" %}
+
+{% from "_includes/primary-navigation.html" import primaryNavigation %}
+{% block header %}
+{{ primaryNavigation("Batches" if participantsPath == "/batches/participants" else "Participants", serviceName) }}
+{% endblock %}
+
+{% block beforeContent %}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+ + You searched for ++ {% if hasCriteria %} +
No search terms entered + {% endif %} + + +
+
+
+
+
+
+ {% if participants | length %}
+ + {{ participants | length }} participant{% if participants | length != 1 %}s{% endif %} found + +
No participants match your search. Check the details you entered or change your search. + {% endif %} + +
+
+
+
+
+
+
+ Search for a participant+
+
+
+
+{% endblock %}
\ No newline at end of file
From 3d4d363148e8e4f3f465085f557d236577b45837 Mon Sep 17 00:00:00 2001
From: danjohnstonUX <21033686+danjohnstonUX@users.noreply.github.com>
Date: Thu, 8 Oct 2026 15:46:13 +0100
Subject: [PATCH 02/14] Changes to name formatting
---
app/data/participants.js | 3 ++-
app/views/batches/participants/detail.html | 2 +-
app/views/batches/participants/index.html | 4 ++--
app/views/index.html | 3 +++
app/views/participants/detail.html | 2 +-
app/views/participants/index.html | 4 ++--
6 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/app/data/participants.js b/app/data/participants.js
index 32d46d07..55721619 100644
--- a/app/data/participants.js
+++ b/app/data/participants.js
@@ -180,7 +180,8 @@ const createParticipant = (index, today) => {
return {
participantId: `p${pad(index + 1)}`,
- full_name: `${surname.toUpperCase()}, ${firstName}${middleName}`,
+ full_name: `${firstName} ${middleName} ${surname}`,
+ display_name: `${surname.toUpperCase()}, ${firstName}`,
surname_sort_value: surname.toUpperCase(),
nhs_number: `999 ${pad((index * 37) % 1000, 3)} ${pad((index * 97 + 1) % 10000, 4)}`,
date_of_birth: formatDate(dateOfBirth),
diff --git a/app/views/batches/participants/detail.html b/app/views/batches/participants/detail.html
index 5af807a9..56403b7b 100644
--- a/app/views/batches/participants/detail.html
+++ b/app/views/batches/participants/detail.html
@@ -20,7 +20,7 @@
+
+
+
+
+
Participant details
-
{{ participant.full_name }}+{{ participant.display_name }}Search and filter{% for participant in participants %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
-
- {{ participant.age }} years old +{{ participant.display_name }} +{{ participant.age }} years old |
{{ participant.address.postcode }} | {{ participant.nhs_number }} | diff --git a/app/views/index.html b/app/views/index.html index 091f5dee..180abbed 100755 --- a/app/views/index.html +++ b/app/views/index.html @@ -54,6 +54,9 @@||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
-
- {{ participant.age }} years old +{{ participant.display_name }} +{{ participant.age }} years old |
{{ participant.address.postcode }} | {{ participant.nhs_number }} | From 65f482d451f2374608b4492f65de164e60d7aa96 Mon Sep 17 00:00:00 2001 From: danjohnstonUX <21033686+danjohnstonUX@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:47:12 +0100 Subject: [PATCH 03/14] name format fix --- app/views/participants/search-results.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/participants/search-results.html b/app/views/participants/search-results.html index a30422fe..8bf2fa06 100644 --- a/app/views/participants/search-results.html +++ b/app/views/participants/search-results.html @@ -71,7 +71,7 @@||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
- {{ participant.full_name }} + {{ participant.display_name }} {{ participant.age }} years old |
{{ participant.date_of_birth }} | From 6be6fcf11abf8ca940b06d3ed6d148de5a813ba8 Mon Sep 17 00:00:00 2001 From: danjohnstonUX <21033686+danjohnstonUX@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:50:02 +0100 Subject: [PATCH 04/14] added sortable and striped tables to search journey --- app/views/participants/search-results.html | 37 ++++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/app/views/participants/search-results.html b/app/views/participants/search-results.html index 8bf2fa06..c7b40b16 100644 --- a/app/views/participants/search-results.html +++ b/app/views/participants/search-results.html @@ -49,40 +49,49 @@
| Name | -Date of birth | -Postcode | -NHS number | -Next test due date | -Next screening | ++ Name + | ++ Postcode + | ++ NHS number + | ++ Next test due date + | ++ Next screening + | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
- {{ participant.display_name }} - {{ participant.age }} years old + {{ participant.display_name }} +{{ participant.age }} years old |
- {{ participant.date_of_birth }} | {{ participant.address.postcode }} | {{ participant.nhs_number }} | -+ |
{{ participant.next_test_due_date }} {{ participant.next_test_due_date_relative }} |
-
- {% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif %} + |
+ {% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif + %} {{ participant.next_appointment_date_relative }} |
Postcode | -+ | NHS number | -+ | Next test due date | -+ | Next screening | @@ -131,11 +131,11 @@{{ participant.address.postcode }} | {{ participant.nhs_number }} | -+ |
{{ participant.next_test_due_date }} {{ participant.next_test_due_date_relative }} |
- + |
{% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif %} {{ participant.next_appointment_date_relative }} diff --git a/app/views/participants/index.html b/app/views/participants/index.html index a47aaa52..a73c4ea5 100644 --- a/app/views/participants/index.html +++ b/app/views/participants/index.html @@ -215,10 +215,10 @@
|
{{ participant.address.postcode }} | {{ participant.nhs_number }} | -+ |
{{ participant.next_test_due_date }} {{ participant.next_test_due_date_relative }} |
- + |
{% if participant.next_appointment_date != 'Not known' %}{{ participant.next_appointment_date }} {% endif %} {{ participant.next_appointment_date_relative }} diff --git a/app/views/participants/search-results.html b/app/views/participants/search-results.html index c7b40b16..db934dec 100644 --- a/app/views/participants/search-results.html +++ b/app/views/participants/search-results.html @@ -65,7 +65,7 @@
Search and filter{% if totalPages > 1 %} {% endif %} From b65687512fefaf070bf80d003f70ccc3e0e24a80 Mon Sep 17 00:00:00 2001 From: danjohnstonUX <21033686+danjohnstonUX@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:20:10 +0100 Subject: [PATCH 07/14] Updated header to include BSO name --- app/views/participants/detail.html | 2 +- app/views/participants/index.html | 2 +- app/views/participants/search-results.html | 2 +- app/views/participants/search.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/participants/detail.html b/app/views/participants/detail.html index 26c0b4a2..d81b7dcc 100644 --- a/app/views/participants/detail.html +++ b/app/views/participants/detail.html @@ -4,7 +4,7 @@ {% from "_includes/primary-navigation.html" import primaryNavigation %} {% block header %} -{{ primaryNavigation("Batches" if participantsPath == "/batches/participants" else "Participants", serviceName) }} +{{ primaryNavigation("Batches" if participantsPath == "/batches/participants" else "Participants", serviceName, bsoName) }} {% endblock %} {% block content %} diff --git a/app/views/participants/index.html b/app/views/participants/index.html index a73c4ea5..3535f7d9 100644 --- a/app/views/participants/index.html +++ b/app/views/participants/index.html @@ -4,7 +4,7 @@ {% from "_includes/primary-navigation.html" import primaryNavigation %} {% block header %} -{{ primaryNavigation("Batches" if participantsPath == "/batches/participants" else "Participants", serviceName) }} +{{ primaryNavigation("Participants", serviceName, bsoName) }} {% endblock %} {% block beforeContent %}{% endblock %} diff --git a/app/views/participants/search-results.html b/app/views/participants/search-results.html index db934dec..b2294c79 100644 --- a/app/views/participants/search-results.html +++ b/app/views/participants/search-results.html @@ -4,7 +4,7 @@ {% from "_includes/primary-navigation.html" import primaryNavigation %} {% block header %} -{{ primaryNavigation("Participants", serviceName) }} +{{ primaryNavigation("Participants", serviceName, bsoName) }} {% endblock %} {% block beforeContent %}{% endblock %} diff --git a/app/views/participants/search.html b/app/views/participants/search.html index 9a57ff02..da0cc415 100644 --- a/app/views/participants/search.html +++ b/app/views/participants/search.html @@ -4,7 +4,7 @@ {% from "_includes/primary-navigation.html" import primaryNavigation %} {% block header %} -{{ primaryNavigation("Batches" if participantsPath == "/batches/participants" else "Participants", serviceName) }} +{{ primaryNavigation("Participants", serviceName, bsoName) }} {% endblock %} {% block beforeContent %}{% endblock %} From 3b0237cde12080bce9cf0286ba5dd35a33aaacad Mon Sep 17 00:00:00 2001 From: danjohnstonUX <21033686+danjohnstonUX@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:58:13 +0100 Subject: [PATCH 08/14] added initial search ideation and added sortable and striped tables to participant lists --- app/assets/images/nhsuk-icon-arrow-down.svg | 3 + .../images/nhsuk-icon-arrow-up-down.svg | 5 + app/assets/images/nhsuk-icon-arrow-up.svg | 3 + app/assets/sass/main.scss | 4 + app/routes.js | 63 ++++++++- app/views/batches/participants/index.html | 58 +++++---- app/views/participants/index.html | 46 ++----- app/views/participants/search-results.html | 99 ++++++++++++++ app/views/participants/search.html | 121 ++++++++++++++++++ 9 files changed, 340 insertions(+), 62 deletions(-) create mode 100644 app/assets/images/nhsuk-icon-arrow-down.svg create mode 100644 app/assets/images/nhsuk-icon-arrow-up-down.svg create mode 100644 app/assets/images/nhsuk-icon-arrow-up.svg create mode 100644 app/views/participants/search-results.html create mode 100644 app/views/participants/search.html diff --git a/app/assets/images/nhsuk-icon-arrow-down.svg b/app/assets/images/nhsuk-icon-arrow-down.svg new file mode 100644 index 00000000..619714f5 --- /dev/null +++ b/app/assets/images/nhsuk-icon-arrow-down.svg @@ -0,0 +1,3 @@ + diff --git a/app/assets/images/nhsuk-icon-arrow-up-down.svg b/app/assets/images/nhsuk-icon-arrow-up-down.svg new file mode 100644 index 00000000..ab69b3c7 --- /dev/null +++ b/app/assets/images/nhsuk-icon-arrow-up-down.svg @@ -0,0 +1,5 @@ + diff --git a/app/assets/images/nhsuk-icon-arrow-up.svg b/app/assets/images/nhsuk-icon-arrow-up.svg new file mode 100644 index 00000000..ed28b96a --- /dev/null +++ b/app/assets/images/nhsuk-icon-arrow-up.svg @@ -0,0 +1,3 @@ + diff --git a/app/assets/sass/main.scss b/app/assets/sass/main.scss index 5b6e65b4..edeba8dc 100755 --- a/app/assets/sass/main.scss +++ b/app/assets/sass/main.scss @@ -192,6 +192,10 @@ a { } } +.app-search-criteria dd { + margin-inline-start: 0; +} + @include nhsuk-media-query($from: tablet) { .app-participants-sort-form { flex-shrink: 0; diff --git a/app/routes.js b/app/routes.js index 2a98cbc1..c6b389bb 100644 --- a/app/routes.js +++ b/app/routes.js @@ -27,6 +27,7 @@ const participantDateFromToday = (today, days) => { } const formatParticipantDate = (date) => `${String(date.getDate()).padStart(2, '0')} ${participantMonths[date.getMonth()]} ${date.getFullYear()}` +const formatParticipantIsoDate = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` const formatParticipantFullDate = (date) => `${String(date.getDate()).padStart(2, '0')} ${participantFullMonths[date.getMonth()]} ${date.getFullYear()}` const formatParticipantRelativeDate = (date, today, daysThreshold = 7) => { @@ -59,9 +60,11 @@ const refreshParticipantDates = (participant, today, dateFormatter = formatParti last_screened_date: lastScreenedDate ? dateFormatter(lastScreenedDate) : 'Never screened', next_test_due_date: dateFormatter(nextTestDueDate), next_test_due_date_value: nextTestDueDate.getTime(), + next_test_due_date_iso: formatParticipantIsoDate(nextTestDueDate), next_test_due_date_relative: formatParticipantRelativeDate(nextTestDueDate, today, 28), next_appointment_date: nextAppointmentDate ? dateFormatter(nextAppointmentDate) : 'Not known', next_appointment_date_value: nextAppointmentDate ? nextAppointmentDate.getTime() : null, + next_appointment_date_iso: nextAppointmentDate ? formatParticipantIsoDate(nextAppointmentDate) : '9999-12-31', next_appointment_date_relative: nextAppointmentDate ? formatParticipantRelativeDate(nextAppointmentDate, today) : 'Not known' } } @@ -914,7 +917,65 @@ router.get(['/participants', '/batches/participants'], function (req, res) { }) }) -router.get(['/participants/:participantId', '/batches/participants/:participantId'], function (req, res) { +const participantSearchFields = ['firstName', 'lastName', 'nhsNumber', 'sxNumber', 'dobDay', 'dobMonth', 'dobYear', 'postcode', 'emailAddress'] + +const participantSearchCriteria = query => Object.fromEntries(participantSearchFields + .map(field => [field, String(query[field] || '').trim()])) + +const normaliseSearchValue = value => String(value || '').toLowerCase().replace(/\s+/g, '') + +const matchesParticipantSearch = (participant, criteria) => { + const includes = (value, term) => !term || normaliseSearchValue(value).includes(normaliseSearchValue(term)) + const [dobDay, dobMonthName, dobYear] = participant.date_of_birth.split(' ') + const dobMonth = participantFullMonths.indexOf(dobMonthName) + 1 + const matchesDatePart = (actual, term) => !term || parseInt(term, 10) === parseInt(actual, 10) + const matchesYear = !criteria.dobYear || (criteria.dobYear.length === 2 + ? dobYear.endsWith(criteria.dobYear) + : parseInt(criteria.dobYear, 10) === parseInt(dobYear, 10)) + + return includes(participant.full_name.split(', ')[1], criteria.firstName) && + includes(participant.surname_sort_value, criteria.lastName) && + includes(participant.nhs_number, criteria.nhsNumber) && + includes(participant.sx_number, criteria.sxNumber) && + includes(participant.address.postcode, criteria.postcode) && + includes(participant.email, criteria.emailAddress) && + matchesDatePart(dobDay, criteria.dobDay) && + matchesDatePart(dobMonth, criteria.dobMonth) && + matchesYear +} + +router.get('/participants/search', function (req, res) { + res.render('participants/search', { + participantsPath: '/participants', + criteria: participantSearchCriteria(req.query) + }) +}) + +router.get('/participants/search-results', function (req, res) { + const criteria = participantSearchCriteria(req.query) + const today = new Date() + today.setHours(0, 0, 0, 0) + const participants = ((req.session.data.participants && req.session.data.participants.default) || []) + .filter(participant => matchesParticipantSearch(participant, criteria)) + .map(participant => refreshParticipantDates(participant, today)) + .sort((a, b) => a.full_name.localeCompare(b.full_name)) + const searchQuery = new URLSearchParams(Object.entries(criteria).filter(([, value]) => value)).toString() + + res.render('participants/search-results', { + participants, + participantsPath: '/participants', + criteria, + hasCriteria: Boolean(searchQuery), + searchQuery + }) +}) + +router.get(['/participants/:participantId', '/batches/participants/:participantId'], function (req, res, next) { + // Let static pages such as /participants/search fall through to auto-routing + if (!/^p\d+$/.test(req.params.participantId)) { + return next() + } + const participantsPath = req.path.startsWith('/batches/') ? '/batches/participants' : '/participants' const participants = (req.session.data.participants && req.session.data.participants.default) || [] const today = new Date() diff --git a/app/views/batches/participants/index.html b/app/views/batches/participants/index.html index 1ee942bd..af46e431 100644 --- a/app/views/batches/participants/index.html +++ b/app/views/batches/participants/index.html @@ -99,45 +99,47 @@Search and filter
-
+ Showing {{ firstRecord }} to {{ lastRecord }} of {{ totalParticipants }} participants -
|