diff --git a/app/routes/record-vaccinations.js b/app/routes/record-vaccinations.js index 1e20ad7f..5ad39340 100644 --- a/app/routes/record-vaccinations.js +++ b/app/routes/record-vaccinations.js @@ -846,7 +846,7 @@ module.exports = router => { return (expiryDate > dateToday) }) - .filter((batch) => !batch.depletedDate) + .filter((batch) => !batch.deactivatedDate) if (req.query.showError === 'yes') { diff --git a/app/routes/vaccines.js b/app/routes/vaccines.js index 1ed8a0f0..3bdab4e1 100644 --- a/app/routes/vaccines.js +++ b/app/routes/vaccines.js @@ -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 || [] @@ -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 }) }) @@ -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 @@ -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) => { @@ -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, @@ -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 } @@ -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 @@ -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', { @@ -221,7 +326,13 @@ module.exports = (router) => { today, totalPages, totalBatches, - page + page, + currentTab, + activeBatchesCount: batchesByStatus.active.length, + inactiveBatchesCount: batchesByStatus.inactive.length, + deactivated, + reactivated, + updatedBatchNumber }) }) @@ -253,7 +364,7 @@ module.exports = (router) => { res.render('vaccines/add-batch-to-site-check', { vaccine, - site, + site }) }) @@ -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 } @@ -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)}`) }) @@ -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)}`) }) } diff --git a/app/views/vaccines/deplete-batch.html b/app/views/vaccines/deactivate-batch.html similarity index 76% rename from app/views/vaccines/deplete-batch.html rename to app/views/vaccines/deactivate-batch.html index 869f1021..66c27daf 100644 --- a/app/views/vaccines/deplete-batch.html +++ b/app/views/vaccines/deactivate-batch.html @@ -1,6 +1,6 @@ {% extends 'layout.html' %} -{% set pageName = "Deplete batch" %} +{% set pageName = "Deactivate batch" %} {% set currentSection = "vaccines" %} @@ -34,13 +34,13 @@

{{ pageName }}

} ] }) }} -

Depleted batches will be made unavailable.

+

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.

-
+ {{ button({ - "text": "Confirm deplete batch" + "text": "Deactivate" }) }}
diff --git a/app/views/vaccines/deactivate-site.html b/app/views/vaccines/deactivate-site.html new file mode 100644 index 00000000..96ab6fb3 --- /dev/null +++ b/app/views/vaccines/deactivate-site.html @@ -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 %} +
+
+ +

{{ pageName }}

+ +

Are you sure you want to deactivate this site?

+ +
+ {{ button({ + "text": "Confirm deactivate site", + "classes": "nhsuk-button--warning" + }) }} +
+ +
+
+{% endblock %} diff --git a/app/views/vaccines/index.html b/app/views/vaccines/index.html index 86fcbfdb..368ce61d 100644 --- a/app/views/vaccines/index.html +++ b/app/views/vaccines/index.html @@ -11,12 +11,54 @@ {% endif %} {% endfor %} +{% set filteredSites = [] %} +{% for siteId in sites %} + {% set thisSite = currentOrganisation.sites | findById(siteId) %} + {% if currentSiteTab == "deactivated" %} + {% if thisSite.status == "closed" %} + {% set filteredSites = (filteredSites.push(siteId), filteredSites) %} + {% endif %} + {% else %} + {% if thisSite.status != "closed" %} + {% set filteredSites = (filteredSites.push(siteId), filteredSites) %} + {% endif %} + {% endif %} +{% endfor %} + {% block content %}
{% include "includes/notification.html" %} + {% if deactivatedSite %} + {% set html %} +

+ Site deactivated +

+

{{ (siteName + " has been deactivated.") if siteName else "Site has been deactivated." }}

+ {% endset %} + + {{ notificationBanner({ + html: html, + type: "success" + }) }} + {% endif %} + + {% if reactivatedSite %} + {% set html %} +

+ Site reactivated +

+

{{ (siteName + " has been reactivated.") if siteName else "Site has been reactivated." }}

+ {% endset %} + + {{ notificationBanner({ + html: html, + type: "success" + }) }} + {% endif %} +

Vaccines

@@ -47,14 +89,29 @@

Vaccines

{% from 'select/macro.njk' import select %} - {% if (sites | length) > 4 %} +
+ {{ appSecondaryNavigation({ + visuallyHiddenTitle: "Sites by status", + items: [{ + text: "Active sites (" + activeSitesCount + ")", + href: "/vaccines?siteTab=active", + current: (currentSiteTab == "active") + }, { + text: "Deactivated sites (" + deactivatedSitesCount + ")", + href: "/vaccines?siteTab=deactivated", + current: (currentSiteTab == "deactivated") + }] + }) }} +
+ + {% if (filteredSites | length) > 4 %} {% set filterItems = [{ text: "All sites", value: "", selected: true }] %} - {% for site in sites %} + {% for site in filteredSites %} {% set filterItems = (filterItems.push({text: currentOrganisation.sites[site].name, value: site}), filterItems) %} {% endfor %} @@ -74,16 +131,16 @@

Vaccines

- {% for siteId in sites %} + {% if (filteredSites | length) == 0 %} +

No {{ currentSiteTab }} sites found.

+ {% endif %} + + {% for siteId in filteredSites %} {% set site = currentOrganisation.sites | findById(siteId) %} - - +
{{ site.name }} - {% if site.status == "closed" %} - (closed) - {% endif %} -
+
{{ site.name }}
@@ -137,7 +194,7 @@

Vaccines

{% set thisVaccine = vaccine %} {% endif %} {% endfor %} - {{ thisVaccine.batches | length }} + {{ thisVaccine.activeBatchesCount | default(0) }}
@@ -152,6 +209,14 @@

Vaccines

{% endfor %}
+ + {% if site.status != "closed" %} +
+

No longer using this site? Deactivate {{ site.name }}

+ {% else %} +
+

Using this site again? Reactivate {{ site.name }}

+ {% endif %} {% endfor %} diff --git a/app/views/vaccines/product-page.html b/app/views/vaccines/product-page.html index ad970ffe..13ca0637 100644 --- a/app/views/vaccines/product-page.html +++ b/app/views/vaccines/product-page.html @@ -16,6 +16,34 @@
+ {% if deactivated %} + {% set html %} +

+ Batch deactivated +

+

{{ ("Batch " + updatedBatchNumber + " has been deactivated.") if updatedBatchNumber else "Batch has been deactivated." }}

+ {% endset %} + + {{ notificationBanner({ + html: html, + type: "success" + }) }} + {% endif %} + + {% if reactivated %} + {% set html %} +

+ Batch reactivated +

+

{{ ("Batch " + updatedBatchNumber + " has been reactivated.") if updatedBatchNumber else "Batch has been reactivated." }}

+ {% endset %} + + {{ notificationBanner({ + html: html, + type: "success" + }) }} + {% endif %} +

{{ vaccine.vaccineProduct }}

{{ summaryList({ @@ -60,9 +88,37 @@

{{ vaccine.vaccineProduct }}

+

Batches

+ + {% set activeCount = activeBatchesCount | default(0) %} + {% set inactiveCount = inactiveBatchesCount | default(0) %} + + {{ appSecondaryNavigation({ + visuallyHiddenTitle: "Batches by status", + items: [{ + text: "Active (" + activeCount + ")", + href: "/vaccines/" + vaccine.id + "?tab=active", + current: (currentTab == "active") + }, { + text: "Deactivated (" + inactiveCount + ")", + href: "/vaccines/" + vaccine.id + "?tab=inactive", + current: (currentTab == "inactive") + }] + }) }} - - + {% set columns = 2 %} + {% if currentTab == "inactive" %} + {% set columns = columns + 2 %} + {% endif %} + {% if vaccine.vaccineProduct == "Spikevax JN.1" %} + {% set columns = columns + 1 %} + {% endif %} + {% if site.status != "closed" %} + {% set columns = columns + 1 %} + {% endif %} + + +
Batches
- - - - + {% if currentTab == "inactive" %} + + + {% endif %} {% if site.status != "closed" %} @@ -93,48 +148,62 @@

{{ vaccine.vaccineProduct }}

- {% for batch in batches %} + {% if (batches | length) == 0 %} - - {% if vaccine.vaccineProduct == "Spikevax JN.1" %} + + {% else %} + {% for batch in batches %} + - {% endif %} - - - {% endif %} - - {% if site.status != "closed" %} - - {% endif %} + {% if currentTab == "inactive" %} + + + {% endif %} + {% if site.status != "closed" %} + + {% endif %} - - {% endfor %} + + {% endfor %} + {% endif %}
@@ -76,15 +132,14 @@

{{ vaccine.vaccineProduct }}

Expiry date - Status - - Depleted date - - + Date deactivated + + Reason deactivated +
- {{ batch.batchNumber }} + + No {{ currentTab }} batches found.
- {{ batch.packType }} + {{ batch.batchNumber }} - {{ batch.expiryDate | govukDate }} - - {% if batch.depletedDate %} - {{ tag({text: "Depleted", classes: "nhsuk-tag--grey" }) }} - {% elif (batch.expiryDate | daysAgo) > 0 %} - {{ tag({text: "Expired", classes: "nhsuk-tag--grey" }) }} - {% else %} - {{ tag({text: "Active" }) }} - {% endif %} - - {% if batch.depletedDate %} - {{ batch.depletedDate | govukDate }} + {% if vaccine.vaccineProduct == "Spikevax JN.1" %} + + {{ batch.packType }} + {% if (batch.expiryDate | daysAgo) > 0 %} {% else %} - Edit{{ batch.batchNumber }} -   - {% if batch.depletedDate %} - Reactivate{{ batch.batchNumber }} - {% else %} - Deplete{{ batch.batchNumber }} - {% endif %} - {% endif %} + + {{ batch.expiryDate | govukDate }} + {% if batch.deactivatedDate %} + {{ batch.deactivatedDate | govukDate }} + {% else %} + {{ batch.expiryDate | govukDate }} + {% endif %} + + {% if batch.deactivatedDate %} + Deactivated by user + {% elif (batch.expiryDate | daysAgo) > 0 %} + Expired + {% endif %} + + {% if currentTab == "inactive" %} + {% if batch.deactivatedDate %} + Reactivate{{ batch.batchNumber }} + {% endif %} + {% else %} + {% if (batch.expiryDate | daysAgo) > 0 %} + {% else %} + Edit{{ batch.batchNumber }} +   + Deactivate{{ batch.batchNumber }} + {% endif %} + {% endif %} +
@@ -143,19 +212,19 @@

{{ vaccine.vaccineProduct }}

{% for i in range(1, totalPages + 1) -%} {% set items = (items.push({ number: i, - href: "/vaccines/" + vaccine.id + "?page=" + i, + href: "/vaccines/" + vaccine.id + "?tab=" + currentTab + "&page=" + i, current: (i === page) }), items) %} {%- endfor %} - {% if totalPages > 0 %} + {% if totalBatches > 20 %} {{ pagination({ previous: { - href: "/vaccines/" + vaccine.id + "?page=" + (page - 1) if page != 1 + href: "/vaccines/" + vaccine.id + "?tab=" + currentTab + "&page=" + (page - 1) if page != 1 }, next: { - href: "/vaccines/" + vaccine.id + "?page=" + (page + 1) if page != totalPages + href: "/vaccines/" + vaccine.id + "?tab=" + currentTab + "&page=" + (page + 1) if page != totalPages }, items: items }) }} diff --git a/app/views/vaccines/reactivate-site.html b/app/views/vaccines/reactivate-site.html new file mode 100644 index 00000000..bdbc38af --- /dev/null +++ b/app/views/vaccines/reactivate-site.html @@ -0,0 +1,27 @@ +{% extends 'layout.html' %} + +{% set pageName = "Reactivate " + site.name %} + +{% set currentSection = "vaccines" %} + +{% block beforeContent %} + {{ backLink({ href: "/vaccines?siteTab=deactivated", text: "Back" }) }} +{% endblock %} + +{% block content %} +
+
+ +

{{ pageName }}

+ +

Are you sure you want to reactivate this site?

+ +
+ {{ button({ + "text": "Reactivate" + }) }} +
+ +
+
+{% endblock %} diff --git a/app/views/vaccines/reactivate.html b/app/views/vaccines/reactivate.html index 72c54fdc..fe493e27 100644 --- a/app/views/vaccines/reactivate.html +++ b/app/views/vaccines/reactivate.html @@ -1,6 +1,6 @@ {% extends 'layout.html' %} -{% set pageName = "Reactivate batch " + batch.batchNumber %} +{% set pageName = "Reactivate batch" %} {% set currentSection = "vaccines" %} @@ -14,7 +14,28 @@

{{ pageName }}

-

This batch will become ‘active’ immediately.

+ {{ summaryList({ + rows: [ + { + key: { + text: "Batch number" + }, + value: { + text: batch.batchNumber + } + }, + { + key: { + text: "Expiry date" + }, + value: { + text: batch.expiryDate | govukDate + } + } + ] + }) }} + +

Once it is reactivated, this batch will be available when you record or edit a vaccination.