Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
4388b72
Update site tabs on vaccines home page
caitlinroach-nhs Aug 10, 2026
36e6abe
Batch tabs language update
caitlinroach-nhs Aug 10, 2026
1a45fbf
Changed add batch flow to normal for matching batch number and expiry…
caitlinroach-nhs Aug 10, 2026
139bd4e
Update deactivation message for vaccine batches
Anna-Sutton Aug 10, 2026
d458abe
Change button text to 'Reactivate' in reactivation form
Anna-Sutton Aug 10, 2026
7d3e6be
Update reactivation message in reactivate.html
Anna-Sutton Aug 10, 2026
38ee369
Fix punctuation in reactivation message
Anna-Sutton Aug 10, 2026
ff08246
Rephrase deactivation warning message in HTML
Anna-Sutton Aug 10, 2026
2c1e4b9
Update deactivation message in deactivate-batch.html
Anna-Sutton Aug 10, 2026
f4ca005
Update reactivation message for clarity
Anna-Sutton Aug 10, 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
151 changes: 131 additions & 20 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 @@ -17,11 +23,34 @@ module.exports = (router) => {
.filter((vaccine) => !vaccinesEnabledNames.includes(vaccine.name))
.map((vaccine) => vaccine.name)

const vaccineStock = data.vaccineStock.filter((vaccine) => vaccine.organisationId === currentOrganisation.id)
const today = new Date().toISOString().substring(0,10)
const vaccineStock = data.vaccineStock
.filter((vaccine) => vaccine.organisationId === currentOrganisation.id)
.map((vaccine) => {
const batches = Array.isArray(vaccine.batches) ? vaccine.batches : []
const activeBatchesCount = batches.filter((batch) => {
return !batch.deactivatedDate && batch.expiryDate >= today
}).length

return {
...vaccine,
activeBatchesCount
}
})
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 +169,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 +181,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 +236,23 @@ 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) {
matchingDeactivatedBatch.deactivatedDate = null

// Reset data
req.session.data.batchNumber = ''
req.session.data.batchExpiryDate.day = ''
req.session.data.batchExpiryDate.month = ''
req.session.data.batchExpiryDate.year = ''

res.redirect(`/vaccines/${vaccine.id}?tab=active&reactivated=true&batchNumber=${encodeURIComponent(matchingDeactivatedBatch.batchNumber)}`)
return
}

vaccine.batches.push({
id: generatedId,
batchNumber: data.batchNumber,
Expand All @@ -184,7 +273,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 +287,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 +302,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 +326,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 @@ -253,7 +364,7 @@ module.exports = (router) => {

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

Expand Down Expand Up @@ -301,8 +412,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 +424,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 +473,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)}`)
})
}
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>Once it is deactivated, you will not be able to select this batch when you record or edit a vaccination. You can reactivate it anytime.</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": "Deactivate"
}) }}

</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