Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2a8c48a
Add empty state for no pharmacies
caitlinroach-nhs Jun 16, 2026
74c95ef
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jun 17, 2026
25aba43
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jun 17, 2026
bc7a20c
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jun 17, 2026
7c878c3
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jun 18, 2026
e8f6a32
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 2, 2026
849369f
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 6, 2026
fe671c1
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 13, 2026
7357ef2
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 20, 2026
0059675
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 21, 2026
2b44e2d
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 22, 2026
4b791da
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Jul 30, 2026
93723fd
Merge branch 'main' of https://github.com/NHSDigital/record-a-vaccina…
caitlinroach-nhs Aug 4, 2026
8644be1
Updates to vaccine product page and active/deactivate batch pages
caitlinroach-nhs Aug 4, 2026
f80d68a
Added new flow for when a user tries to add an already existing but d…
caitlinroach-nhs Aug 5, 2026
b7febc9
Added ability to deactivate and reactivate sites
caitlinroach-nhs Aug 5, 2026
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
2 changes: 1 addition & 1 deletion app/routes/record-vaccinations.js
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ module.exports = router => {

return (expiryDate > dateToday)
})
.filter((batch) => !batch.depletedDate)
.filter((batch) => !batch.deactivatedDate)

if (req.query.showError === 'yes') {

Expand Down
138 changes: 120 additions & 18 deletions app/routes/vaccines.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ module.exports = (router) => {
router.get('/vaccines', (req, res) => {
const currentOrganisation = res.locals.currentOrganisation
const data = req.session.data
const deactivatedSite = req.query.deactivatedSite === 'true'
const reactivatedSite = req.query.reactivatedSite === 'true'
const siteName = req.query.siteName
const siteTab = (req.query.siteTab || 'active').toLowerCase()
const validSiteTabs = ['active', 'deactivated']
const currentSiteTab = validSiteTabs.includes(siteTab) ? siteTab : 'active'

const organisationVaccines = res.locals.currentOrganisation.vaccines || []

Expand All @@ -18,10 +24,20 @@ module.exports = (router) => {
.map((vaccine) => vaccine.name)

const vaccineStock = data.vaccineStock.filter((vaccine) => vaccine.organisationId === currentOrganisation.id)
const siteIdsInUse = [...new Set(vaccineStock.map((vaccine) => vaccine.siteId))]
const sitesInUse = (currentOrganisation.sites || []).filter((site) => siteIdsInUse.includes(site.id))
const activeSitesCount = sitesInUse.filter((site) => site.status !== 'closed').length
const deactivatedSitesCount = sitesInUse.filter((site) => site.status === 'closed').length

res.render('vaccines/index', {
vaccineStock,
vaccinesThatCanBeRequested
vaccinesThatCanBeRequested,
deactivatedSite,
reactivatedSite,
siteName,
currentSiteTab,
activeSitesCount,
deactivatedSitesCount
})
})

Expand Down Expand Up @@ -140,7 +156,6 @@ module.exports = (router) => {

// Viewing check answers page
router.get('/vaccines/check', (req, res) => {

const data = req.session.data
const siteId = data.siteId

Expand All @@ -153,6 +168,50 @@ module.exports = (router) => {
})
})

// View page to deactivate a site
router.get('/vaccines/sites/:siteId/deactivate', (req, res) => {
const currentOrganisationSites = res.locals.currentOrganisation.sites || []
const site = currentOrganisationSites.find((item) => item.id === req.params.siteId)
if (!site) { res.redirect('/vaccines'); return }

res.render('vaccines/deactivate-site', {
site
})
})

// Mark site as closed
router.post('/vaccines/sites/:siteId/deactivated', (req, res) => {
const currentOrganisationSites = res.locals.currentOrganisation.sites || []
const site = currentOrganisationSites.find((item) => item.id === req.params.siteId)
if (!site) { res.redirect('/vaccines'); return }

site.status = 'closed'

res.redirect(`/vaccines?siteTab=deactivated&deactivatedSite=true&siteName=${encodeURIComponent(site.name)}`)
})

// View page to reactivate a site
router.get('/vaccines/sites/:siteId/reactivate', (req, res) => {
const currentOrganisationSites = res.locals.currentOrganisation.sites || []
const site = currentOrganisationSites.find((item) => item.id === req.params.siteId)
if (!site) { res.redirect('/vaccines'); return }

res.render('vaccines/reactivate-site', {
site
})
})

// Mark site as active
router.post('/vaccines/sites/:siteId/reactivated', (req, res) => {
const currentOrganisationSites = res.locals.currentOrganisation.sites || []
const site = currentOrganisationSites.find((item) => item.id === req.params.siteId)
if (!site) { res.redirect('/vaccines'); return }

delete site.status

res.redirect(`/vaccines?siteTab=active&reactivatedSite=true&siteName=${encodeURIComponent(site.name)}`)
})


// Adding a batch to an existing vaccine at a site
router.post('/vaccines/:id/added', (req, res) => {
Expand All @@ -164,6 +223,15 @@ module.exports = (router) => {

const expiryDate = new Date(data.batchExpiryDate.year, (parseInt(data.batchExpiryDate.month) - 1), data.batchExpiryDate.day, 12).toISOString().substring(0,10)

const matchingDeactivatedBatch = vaccine.batches.find((batch) => {
return batch.batchNumber === data.batchNumber && batch.expiryDate === expiryDate && batch.deactivatedDate
})

if (matchingDeactivatedBatch) {
res.redirect(`/vaccines/${vaccine.id}/add-batch-check`)
return
}

vaccine.batches.push({
id: generatedId,
batchNumber: data.batchNumber,
Expand All @@ -184,7 +252,11 @@ module.exports = (router) => {
router.get('/vaccines/:id', (req, res) => {
const data = req.session.data
const perPage = 20; // Max number of users to show per page
const page = parseInt(req.query.page) || 1 ; // Current page, default to 1
const queryPage = parseInt(req.query.page) || 1 ; // Current page, default to 1
const tab = (req.query.tab || 'active').toLowerCase()
const deactivated = req.query.deactivated === 'true'
const reactivated = req.query.reactivated === 'true'
const updatedBatchNumber = req.query.batchNumber

const vaccine = data.vaccineStock.find((vaccine) => vaccine.id === req.params.id)
if (!vaccine) { res.redirect('/vaccines'); return }
Expand All @@ -194,6 +266,8 @@ module.exports = (router) => {
const site = currentOrganisation.sites.find((site) => site.id === vaccine.siteId)

const today = new Date().toISOString().substring(0,10)
const validTabs = ['active', 'inactive']
const currentTab = validTabs.includes(tab) ? tab : 'active'

const allBatches = vaccine.batches.sort((a, b) => {
const expiryA = a.expiryDate
Expand All @@ -207,11 +281,21 @@ module.exports = (router) => {
return 0;
})

const totalBatches = allBatches.length
const indexStartFrom = (page - 1) * perPage
const totalPages = Math.ceil(totalBatches / perPage)
const isActiveBatch = (batch) => {
return !batch.deactivatedDate && batch.expiryDate >= today
}

const batches = allBatches.slice(indexStartFrom, indexStartFrom + perPage)
const batchesByStatus = {
active: allBatches.filter((batch) => isActiveBatch(batch)),
inactive: allBatches.filter((batch) => !isActiveBatch(batch))
}

const filteredBatches = batchesByStatus[currentTab]
const totalBatches = filteredBatches.length
const totalPages = Math.ceil(totalBatches / perPage)
const page = totalPages > 0 ? Math.min(queryPage, totalPages) : 1
const indexStartFrom = (page - 1) * perPage
const batches = filteredBatches.slice(indexStartFrom, indexStartFrom + perPage)


res.render('vaccines/product-page', {
Expand All @@ -221,7 +305,13 @@ module.exports = (router) => {
today,
totalPages,
totalBatches,
page
page,
currentTab,
activeBatchesCount: batchesByStatus.active.length,
inactiveBatchesCount: batchesByStatus.inactive.length,
deactivated,
reactivated,
updatedBatchNumber
})
})

Expand Down Expand Up @@ -251,9 +341,21 @@ module.exports = (router) => {

const site = currentOrganisationSites.find((site) => site.id == vaccine.siteId)

let matchingDeactivatedBatch
const { day, month, year } = data.batchExpiryDate || {}

if (data.batchNumber && day && month && year) {
const expiryDate = new Date(year, (parseInt(month) - 1), day, 12).toISOString().substring(0,10)

matchingDeactivatedBatch = vaccine.batches.find((batch) => {
return batch.batchNumber === data.batchNumber && batch.expiryDate === expiryDate && batch.deactivatedDate
})
}

res.render('vaccines/add-batch-to-site-check', {
vaccine,
site,
matchingDeactivatedBatch,
})
})

Expand Down Expand Up @@ -301,8 +403,8 @@ module.exports = (router) => {
res.redirect('/vaccines/' + vaccine.id)
})

// View page to deplete a batch
router.get('/vaccines/:vaccineId/:batchNumber/deplete', (req, res) => {
// View page to deactivate a batch
router.get('/vaccines/:vaccineId/:batchNumber/deactivate', (req, res) => {
const data = req.session.data
const vaccine = data.vaccineStock.find((vaccine) => vaccine.id === req.params.vaccineId)
if (!vaccine) { res.redirect('/vaccines'); return }
Expand All @@ -313,25 +415,25 @@ module.exports = (router) => {
const batch = vaccine.batches.find((batch) => batch.batchNumber === req.params.batchNumber)
if (!batch) { res.redirect(`/vaccines/${vaccine.id}`); return }

res.render('vaccines/deplete-batch', {
res.render('vaccines/deactivate-batch', {
vaccine,
site,
batch
})
})

// Mark batch as depleted
router.post('/vaccines/:vaccineId/:batchNumber/depleted', (req, res) => {
// Mark batch as deactivated
router.post('/vaccines/:vaccineId/:batchNumber/deactivated', (req, res) => {
const data = req.session.data
const vaccine = data.vaccineStock.find((vaccine) => vaccine.id === req.params.vaccineId)
if (!vaccine) { res.redirect('/vaccines'); return }
const batch = vaccine.batches.find((batch) => batch.batchNumber === req.params.batchNumber)

let depletedDate = new Date()
let deactivatedDate = new Date()

batch.depletedDate = depletedDate.toISOString().substring(0,10)
batch.deactivatedDate = deactivatedDate.toISOString().substring(0,10)

res.redirect('/vaccines/' + vaccine.id)
res.redirect(`/vaccines/${vaccine.id}?tab=inactive&deactivated=true&batchNumber=${encodeURIComponent(batch.batchNumber)}`)
})


Expand Down Expand Up @@ -362,8 +464,8 @@ module.exports = (router) => {
if (!vaccine) { res.redirect('/vaccines'); return }
const batch = vaccine.batches.find((batch) => batch.batchNumber === req.params.batchNumber)

batch.depletedDate = null
batch.deactivatedDate = null

res.redirect('/vaccines/' + vaccine.id)
res.redirect(`/vaccines/${vaccine.id}?tab=active&reactivated=true&batchNumber=${encodeURIComponent(batch.batchNumber)}`)
})
}
35 changes: 29 additions & 6 deletions app/views/vaccines/add-batch-to-site-check.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@

<h1 class="nhsuk-heading-l">Check and confirm</h1>

{% if matchingDeactivatedBatch %}
{% set html %}
<h3 class="nhsuk-notification-banner__heading">Batch already exists</h3>
<p>Batch {{ matchingDeactivatedBatch.batchNumber }} with expiry date {{ matchingDeactivatedBatch.expiryDate | govukDate }} already exists and is deactivated.</p>
<p>You can reactivate this batch instead of adding it again.</p>
{% endset %}

{{ notificationBanner({
html: html
}) }}
{% endif %}


{{ summaryList({
classes: "nhsuk-u-margin-bottom-2",
Expand Down Expand Up @@ -101,15 +113,26 @@ <h1 class="nhsuk-heading-l">Check and confirm</h1>
]
}) }}

<p class="nhsuk-u-margin-top-0 nhsuk-u-margin-bottom-8"><a class="nhsuk-link nhsuk-link--no-visited-state" href="/vaccines/{{ vaccine.id }}/add">Edit batch</a></p>
{% if not matchingDeactivatedBatch %}
<p class="nhsuk-u-margin-top-0 nhsuk-u-margin-bottom-8"><a class="nhsuk-link nhsuk-link--no-visited-state" href="/vaccines/{{ vaccine.id }}/add">Edit batch</a></p>
{% endif %}



<form action="/vaccines/{{ vaccine.id }}/added" method="post">
{{ button({
"text": "Confirm"
}) }}
</form>
{% if matchingDeactivatedBatch %}
<form action="/vaccines/{{ vaccine.id }}/{{ matchingDeactivatedBatch.batchNumber }}/reactivated" method="post" novalidate="true">
{{ button({
text: "Reactivate batch",
classes: "nhsuk-u-margin-top-6"
}) }}
</form>
{% else %}
<form action="/vaccines/{{ vaccine.id }}/added" method="post">
{{ button({
"text": "Confirm"
}) }}
</form>
{% endif %}


</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends 'layout.html' %}

{% set pageName = "Deplete batch" %}
{% set pageName = "Deactivate batch" %}

{% set currentSection = "vaccines" %}

Expand Down Expand Up @@ -34,13 +34,13 @@ <h1 class="nhsuks-heading-l">{{ pageName }}</h1>
}
]
}) }}
<p>Depleted batches will be made unavailable.</p>
<p>Deactivated batches will be made unavailable immediately. Batches can be reactivated if needed.</p>

<form action="/vaccines/{{ vaccine.id }}/{{ batch.batchNumber }}/depleted" method="post" novalidate="true">
<form action="/vaccines/{{ vaccine.id }}/{{ batch.batchNumber }}/deactivated" method="post" novalidate="true">


{{ button({
"text": "Confirm deplete batch"
"text": "Confirm deactivate batch"
}) }}

</form>
Expand Down
28 changes: 28 additions & 0 deletions app/views/vaccines/deactivate-site.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends 'layout.html' %}

{% set pageName = "Deactivate " + site.name %}

{% set currentSection = "vaccines" %}

{% block beforeContent %}
{{ backLink({ href: "/vaccines", text: "Back" }) }}
{% endblock %}

{% block content %}
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">

<h1 class="nhsuk-heading-l">{{ pageName }}</h1>

<p>Are you sure you want to deactivate this site?</p>

<form action="/vaccines/sites/{{ site.id }}/deactivated" method="post" novalidate="true">
{{ button({
"text": "Confirm deactivate site",
"classes": "nhsuk-button--warning"
}) }}
</form>

</div>
</div>
{% endblock %}
Loading