diff --git a/app/data/organisations.js b/app/data/organisations.js index bfe3b7bd..71ff70bc 100644 --- a/app/data/organisations.js +++ b/app/data/organisations.js @@ -9119,6 +9119,566 @@ module.exports = [ } ] }, + { + id: "MCA401", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "12 North Street", + town: "London", + postcode: "N1 2AA" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS401", + name: "MediCare Pharmacy", + address: { + line1: "12 North Street", + town: "London", + postcode: "N1 2AA" + } + } + ] + }, + { + id: "MCA402", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "44 Kingsway", + town: "London", + postcode: "WC2B 6AA" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS402", + name: "MediCare Pharmacy", + address: { + line1: "44 Kingsway", + town: "London", + postcode: "WC2B 6AA" + } + } + ] + }, + { + id: "MCA403", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "7 Willow Lane", + town: "Croydon", + postcode: "CR0 1BT" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS403", + name: "MediCare Pharmacy", + address: { + line1: "7 Willow Lane", + town: "Croydon", + postcode: "CR0 1BT" + } + } + ] + }, + { + id: "MCA404", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "91 Station Road", + town: "Ilford", + postcode: "IG1 4AB" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS404", + name: "MediCare Pharmacy", + address: { + line1: "91 Station Road", + town: "Ilford", + postcode: "IG1 4AB" + } + } + ] + }, + { + id: "MCA405", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "18 Market Place", + town: "Harrow", + postcode: "HA1 1RW" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS405", + name: "MediCare Pharmacy", + address: { + line1: "18 Market Place", + town: "Harrow", + postcode: "HA1 1RW" + } + } + ] + }, + { + id: "MCA406", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "203 Green Road", + town: "Watford", + postcode: "WD17 2TT" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS406", + name: "MediCare Pharmacy", + address: { + line1: "203 Green Road", + town: "Watford", + postcode: "WD17 2TT" + } + } + ] + }, + { + id: "MCA407", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "60 Park Avenue", + town: "Enfield", + postcode: "EN2 6NL" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS407", + name: "MediCare Pharmacy", + address: { + line1: "60 Park Avenue", + town: "Enfield", + postcode: "EN2 6NL" + } + } + ] + }, + { + id: "MCA408", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "14 Bridge Road", + town: "Bromley", + postcode: "BR1 3PX" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS408", + name: "MediCare Pharmacy", + address: { + line1: "14 Bridge Road", + town: "Bromley", + postcode: "BR1 3PX" + } + } + ] + }, + { + id: "MCA409", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "9 High Road", + town: "Romford", + postcode: "RM1 2BD" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS409", + name: "MediCare Pharmacy", + address: { + line1: "9 High Road", + town: "Romford", + postcode: "RM1 2BD" + } + } + ] + }, + { + id: "MCA410", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "31 Chapel Street", + town: "Slough", + postcode: "SL1 1PX" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS410", + name: "MediCare Pharmacy", + address: { + line1: "31 Chapel Street", + town: "Slough", + postcode: "SL1 1PX" + } + } + ] + }, + { + id: "MCA411", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "48 Queens Road", + town: "Reading", + postcode: "RG1 4AP" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS411", + name: "MediCare Pharmacy", + address: { + line1: "48 Queens Road", + town: "Reading", + postcode: "RG1 4AP" + } + } + ] + }, + { + id: "MCA412", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "117 Albert Street", + town: "Luton", + postcode: "LU1 2NE" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS412", + name: "MediCare Pharmacy", + address: { + line1: "117 Albert Street", + town: "Luton", + postcode: "LU1 2NE" + } + } + ] + }, + { + id: "MCA413", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "82 Mill Lane", + town: "Cambridge", + postcode: "CB1 3NJ" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS413", + name: "MediCare Pharmacy", + address: { + line1: "82 Mill Lane", + town: "Cambridge", + postcode: "CB1 3NJ" + } + } + ] + }, + { + id: "MCA414", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "25 Priory Road", + town: "Chelmsford", + postcode: "CM1 1RF" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS414", + name: "MediCare Pharmacy", + address: { + line1: "25 Priory Road", + town: "Chelmsford", + postcode: "CM1 1RF" + } + } + ] + }, + { + id: "MCA415", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "5 Church Lane", + town: "Canterbury", + postcode: "CT1 2TT" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS415", + name: "MediCare Pharmacy", + address: { + line1: "5 Church Lane", + town: "Canterbury", + postcode: "CT1 2TT" + } + } + ] + }, + { + id: "MCA416", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "77 College Street", + town: "Brighton", + postcode: "BN1 4GX" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS416", + name: "MediCare Pharmacy", + address: { + line1: "77 College Street", + town: "Brighton", + postcode: "BN1 4GX" + } + } + ] + }, + { + id: "MCA417", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "29 George Street", + town: "Portsmouth", + postcode: "PO1 3BD" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS417", + name: "MediCare Pharmacy", + address: { + line1: "29 George Street", + town: "Portsmouth", + postcode: "PO1 3BD" + } + } + ] + }, + { + id: "MCA418", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "65 Union Street", + town: "Guildford", + postcode: "GU1 3TA" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS418", + name: "MediCare Pharmacy", + address: { + line1: "65 Union Street", + town: "Guildford", + postcode: "GU1 3TA" + } + } + ] + }, + { + id: "MCA419", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "11 Crown Road", + town: "Woking", + postcode: "GU21 4PS" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS419", + name: "MediCare Pharmacy", + address: { + line1: "11 Crown Road", + town: "Woking", + postcode: "GU21 4PS" + } + } + ] + }, + { + id: "MCA420", + name: "MediCare Pharmacy", + companyId: "P15951", + address: { + line1: "140 River Road", + town: "Milton Keynes", + postcode: "MK9 2AF" + }, + type: "Community Pharmacy", + status: "Active", + region: "Y56", + vaccines: [ + {name: "COVID-19", status: "enabled"}, + {name: "flu", status: "enabled"} + ], + sites: [ + { + id: "MCAS420", + name: "MediCare Pharmacy", + address: { + line1: "140 River Road", + town: "Milton Keynes", + postcode: "MK9 2AF" + } + } + ] + }, { id: "FT81513", name: "Holborn Pharmacy", diff --git a/app/data/session-data-defaults.js b/app/data/session-data-defaults.js index 6a081ab9..d91c5215 100644 --- a/app/data/session-data-defaults.js +++ b/app/data/session-data-defaults.js @@ -27,6 +27,15 @@ module.exports = { locationDataOptions: ["Site name", "Site ODS code", "Location type"], consentAndEligibilityDataOptions: ["Consent", "Eligibility", "Estimated due date"], vaccinationDataOptions: ["Vaccine type", "Product", "Batch", "Batch expiry date", "Dose sequence", "Dose amount", "Vaccination site", "Vaccination date", "Comments"], + + // Amanda White (6424325235325) — default temporary pharmacy access + groupAdminTemporaryPharmacyIds: ["FX9141", "FX4825"], + groupAdminTemporaryPharmacyAddedDates: { + "FX9141": "2026-06-20", + "FX4825": "2026-07-07" + }, + groupAdminTemporaryPharmacyDeactivatedDates: {}, + paymentDataOptions: [ {text: "Deposit ID", hint: "Set before we send the claim to NHSBSA"}, {text: "Claim ID", hint: "Set when NHSBSA have received the claim"} diff --git a/app/routes/auth.js b/app/routes/auth.js index cb3fb725..8c7238f9 100644 --- a/app/routes/auth.js +++ b/app/routes/auth.js @@ -1,5 +1,13 @@ module.exports = router => { + const asArray = (value) => { + if (value === undefined || value === null) { + return [] + } + + return Array.isArray(value) ? value : [value] + } + const getDefaultLandingPath = (data, organisationId) => { const organisation = (data.organisations || []).find((item) => item.id === organisationId) @@ -22,6 +30,95 @@ module.exports = router => { return '/home' } + const getCurrentUserFromSessionData = (data) => { + if (!data) { + return null + } + + if (data.currentUserId) { + return (data.users || []).find((user) => user.id === data.currentUserId) || null + } + + if (data.email) { + return (data.users || []).find((user) => user.email === data.email) || null + } + + return null + } + + const isTemporaryAccessInactive = (addedOnIsoDate, deactivatedOnIsoDate) => { + if (deactivatedOnIsoDate) { + return true + } + + if (!addedOnIsoDate) { + return false + } + + const expiryDate = new Date(`${addedOnIsoDate}T12:00:00`) + expiryDate.setDate(expiryDate.getDate() + 7) + + const today = new Date() + today.setHours(0, 0, 0, 0) + + return expiryDate < today + } + + const getGroupAdminSwitchContext = (data, user) => { + if (!data || !user) { + return { + isEligible: false, + groupOrganisation: null, + temporaryPharmacies: [] + } + } + + const activeGroupAdminOrganisationIds = (user.organisations || []) + .filter((organisation) => organisation.permissionLevel === 'Group administrator' && organisation.status === 'Active') + .map((organisation) => organisation.id) + + const groupOrganisation = (data.organisations || []).find((organisation) => { + return organisation.type === 'Pharmacy HQ' && activeGroupAdminOrganisationIds.includes(organisation.id) + }) + + if (!groupOrganisation) { + return { + isEligible: false, + groupOrganisation: null, + temporaryPharmacies: [] + } + } + + const temporaryPharmacyIds = [...new Set(asArray(data.groupAdminTemporaryPharmacyIds))] + const temporaryPharmacyAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + const temporaryPharmacyDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + + const temporaryPharmacies = temporaryPharmacyIds + .map((pharmacyId) => { + const organisation = (data.organisations || []).find((item) => item.id === pharmacyId) + + return { + organisation, + addedOn: temporaryPharmacyAddedDates[pharmacyId], + deactivatedOn: temporaryPharmacyDeactivatedDates[pharmacyId] + } + }) + .filter((item) => item.organisation) + .filter((organisation) => { + return organisation.organisation.status === 'Active' && organisation.organisation.companyId === groupOrganisation.id + }) + .filter((item) => { + return !isTemporaryAccessInactive(item.addedOn, item.deactivatedOn) + }) + .map((item) => item.organisation) + + return { + isEligible: temporaryPharmacies.length > 1, + groupOrganisation, + temporaryPharmacies + } + } + router.post('/auth/sign-in', (req, res) => { const data = req.session.data @@ -119,6 +216,97 @@ module.exports = router => { } }) + router.get('/auth/switch-organisation/work-type', (req, res) => { + const data = req.session.data + const user = getCurrentUserFromSessionData(data) + const context = getGroupAdminSwitchContext(data, user) + + if (!user || !context.isEligible) { + return res.redirect('/auth/select-organisation') + } + + res.render('auth/switch-work-type', { + groupOrganisation: context.groupOrganisation + }) + }) + + router.post('/auth/switch-organisation/work-type', (req, res) => { + const data = req.session.data + const user = getCurrentUserFromSessionData(data) + const context = getGroupAdminSwitchContext(data, user) + const switchOrganisationWorkType = req.body.switchOrganisationWorkType + + if (!user || !context.isEligible) { + return res.redirect('/auth/select-organisation') + } + + if (!switchOrganisationWorkType) { + return res.render('auth/switch-work-type', { + groupOrganisation: context.groupOrganisation, + error: { + text: 'Select the type of work you want to do' + } + }) + } + + if (switchOrganisationWorkType === 'group-admin') { + data.currentOrganisationId = context.groupOrganisation.id + return res.redirect(getDefaultLandingPath(data, context.groupOrganisation.id)) + } + + if (switchOrganisationWorkType === 'individual-pharmacy') { + return res.redirect('/auth/switch-organisation/select-pharmacy') + } + + res.render('auth/switch-work-type', { + groupOrganisation: context.groupOrganisation, + error: { + text: 'Select the type of work you want to do' + } + }) + }) + + router.get('/auth/switch-organisation/select-pharmacy', (req, res) => { + const data = req.session.data + const user = getCurrentUserFromSessionData(data) + const context = getGroupAdminSwitchContext(data, user) + + if (!user || !context.isEligible) { + return res.redirect('/auth/select-organisation') + } + + res.render('auth/select-pharmacy-for-access', { + groupOrganisation: context.groupOrganisation, + pharmacies: context.temporaryPharmacies + }) + }) + + router.post('/auth/switch-organisation/select-pharmacy', (req, res) => { + const data = req.session.data + const user = getCurrentUserFromSessionData(data) + const context = getGroupAdminSwitchContext(data, user) + const switchOrganisationPharmacyId = req.body.switchOrganisationPharmacyId + + if (!user || !context.isEligible) { + return res.redirect('/auth/select-organisation') + } + + const isValidSelection = context.temporaryPharmacies.some((pharmacy) => pharmacy.id === switchOrganisationPharmacyId) + + if (!switchOrganisationPharmacyId || !isValidSelection) { + return res.render('auth/select-pharmacy-for-access', { + groupOrganisation: context.groupOrganisation, + pharmacies: context.temporaryPharmacies, + error: { + text: 'Select a pharmacy to access' + } + }) + } + + data.currentOrganisationId = switchOrganisationPharmacyId + res.redirect(getDefaultLandingPath(data, switchOrganisationPharmacyId)) + }) + router.get('/sign-out', (req, res) => { req.session.data.currentUserId = null req.session.data.currentOrganisationId = null diff --git a/app/routes/pharmacies.js b/app/routes/pharmacies.js index f098f672..b325f9c3 100644 --- a/app/routes/pharmacies.js +++ b/app/routes/pharmacies.js @@ -23,6 +23,70 @@ const isoDaysAgo = (daysAgo) => { return date.toISOString() } +const isoDateToday = () => { + return new Date().toISOString().split('T')[0] +} + +const accessExpiryText = (addedOnIsoDate) => { + if (!addedOnIsoDate) { + return 'Not set' + } + + const expiryDate = new Date(`${addedOnIsoDate}T12:00:00`) + expiryDate.setDate(expiryDate.getDate() + 7) + + return new Intl.DateTimeFormat('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric' + }).format(expiryDate) +} + +const isTemporaryAccessExpired = (addedOnIsoDate) => { + if (!addedOnIsoDate) { + return false + } + + const expiryDate = new Date(`${addedOnIsoDate}T12:00:00`) + expiryDate.setDate(expiryDate.getDate() + 7) + + const today = new Date() + today.setHours(0, 0, 0, 0) + + return expiryDate < today +} + +const isTemporaryAccessInactive = (addedOnIsoDate, deactivatedOnIsoDate) => { + return Boolean(deactivatedOnIsoDate) || isTemporaryAccessExpired(addedOnIsoDate) +} + +const blockedGroupAdminSimulationEmail = 'simulategroupadmin@nhs.net' + +const getGroupAdminNameForPharmacy = (data, organisation) => { + if (!organisation || !organisation.companyId) { + return null + } + + const groupAdmins = (data.users || []).filter((user) => { + return (user.organisations || []).some((org) => { + return org.id === organisation.companyId && org.permissionLevel === 'Group administrator' && org.status === 'Active' + }) + }) + + if (groupAdmins.length === 0) { + return null + } + + const sortedGroupAdmins = [...groupAdmins].sort((a, b) => { + const nameA = `${a.firstName || ''} ${a.lastName || ''}`.trim() + const nameB = `${b.firstName || ''} ${b.lastName || ''}`.trim() + return nameA.localeCompare(nameB) + }) + + const chosenGroupAdmin = sortedGroupAdmins[0] + return `${chosenGroupAdmin.firstName || ''} ${chosenGroupAdmin.lastName || ''}`.trim() +} + const buildDefaultScenarioUsersForPharmacy = (organisation) => { if (!organisation || !scenarioCompanyIds.includes(organisation.companyId) || organisation.addedByUser) { return [] @@ -368,6 +432,405 @@ module.exports = router => { res.redirect(`/pharmacies/users?deactivatedGroupAdminId=${user.id}`) }) + router.get('/pharmacies/users/:userId/manage', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const currentOrganisation = res.locals.currentOrganisation + const currentTemporaryAccessTab = req.query.tab === 'expired' ? 'expired' : 'active' + const temporaryAccessAdded = req.query.temporaryAccessAdded === 'true' + const temporaryAccessAddedCount = Math.max(0, parseInt(req.query.temporaryAccessAddedCount || '0', 10) || 0) + const deactivatedTemporaryPharmacyId = req.query.deactivatedTemporaryPharmacyId + const renewedTemporaryPharmacyId = req.query.renewedTemporaryPharmacyId + const renewalLimitExceededPharmacyId = req.query.renewalLimitExceededPharmacyId + + const user = data.users.find((item) => item.id === userId) + + if (!user || !currentOrganisation) { + return res.redirect('/pharmacies/users') + } + + const groupRole = (user.organisations || []).find((org) => { + return org.id === currentOrganisation.id && org.permissionLevel === 'Group administrator' && org.status !== 'Deactivated' + }) + + if (!groupRole) { + return res.redirect('/pharmacies/users') + } + + const selectedPharmacyIds = data.groupAdminTemporaryPharmacyIds + ? (Array.isArray(data.groupAdminTemporaryPharmacyIds) ? data.groupAdminTemporaryPharmacyIds : [data.groupAdminTemporaryPharmacyIds]) + : [] + + const temporaryPharmacyAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + const temporaryPharmacyDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + + const temporaryAccessPharmacies = data.organisations + .filter((organisation) => { + return selectedPharmacyIds.includes(organisation.id) && organisation.companyId === currentOrganisation.id && organisation.status === 'Active' + }) + .map((organisation) => { + const addedOn = temporaryPharmacyAddedDates[organisation.id] + const deactivatedOn = temporaryPharmacyDeactivatedDates[organisation.id] + + return { + ...organisation, + accessExpires: accessExpiryText(addedOn), + accessExpired: isTemporaryAccessInactive(addedOn, deactivatedOn) + } + }) + .sort(sortByNameThenPostcode()) + + const activeTemporaryAccessPharmacies = temporaryAccessPharmacies.filter((organisation) => !organisation.accessExpired) + const expiredTemporaryAccessPharmacies = temporaryAccessPharmacies.filter((organisation) => organisation.accessExpired) + const currentTemporaryAccessPharmacies = currentTemporaryAccessTab === 'expired' + ? expiredTemporaryAccessPharmacies + : activeTemporaryAccessPharmacies + + const deactivatedTemporaryPharmacy = deactivatedTemporaryPharmacyId + ? data.organisations.find((org) => org.id === deactivatedTemporaryPharmacyId) + : undefined + + const renewedTemporaryPharmacy = renewedTemporaryPharmacyId + ? data.organisations.find((org) => org.id === renewedTemporaryPharmacyId) + : undefined + + const renewalLimitExceededPharmacy = renewalLimitExceededPharmacyId + ? data.organisations.find((org) => org.id === renewalLimitExceededPharmacyId) + : undefined + + res.render('pharmacies/users/manage-group-user', { + user, + groupRole, + groupName: currentOrganisation.name, + groupId: currentOrganisation.id, + temporaryAccessAdded, + temporaryAccessAddedCount, + temporaryAccessPharmacies, + activeTemporaryAccessPharmacies, + expiredTemporaryAccessPharmacies, + currentTemporaryAccessPharmacies, + currentTemporaryAccessTab, + deactivatedTemporaryPharmacy, + renewedTemporaryPharmacy, + renewalLimitExceededPharmacy + }) + }) + + router.get('/pharmacies/users/:userId/temporary-access/select-pharmacies', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const currentOrganisation = res.locals.currentOrganisation + + const user = data.users.find((item) => item.id === userId) + + if (!user || !currentOrganisation) { + return res.redirect('/pharmacies/users') + } + + const groupRole = (user.organisations || []).find((org) => { + return org.id === currentOrganisation.id && org.permissionLevel === 'Group administrator' && org.status !== 'Deactivated' + }) + + if (!groupRole) { + return res.redirect('/pharmacies/users') + } + + const activeNonGroupPharmacyIds = (user.organisations || []) + .filter((org) => org.permissionLevel !== 'Group administrator' && org.status !== 'Deactivated') + .map((org) => org.id) + + const selectedTemporaryPharmacyIds = data.groupAdminTemporaryPharmacyIds + ? (Array.isArray(data.groupAdminTemporaryPharmacyIds) ? data.groupAdminTemporaryPharmacyIds : [data.groupAdminTemporaryPharmacyIds]) + : [] + + const temporaryPharmacyAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + const temporaryPharmacyDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + const today = isoDateToday() + + const activeTemporaryPharmacyIds = selectedTemporaryPharmacyIds.filter((pharmacyId) => { + const addedOn = temporaryPharmacyAddedDates[pharmacyId] || today + const deactivatedOn = temporaryPharmacyDeactivatedDates[pharmacyId] + return !isTemporaryAccessInactive(addedOn, deactivatedOn) + }) + + const pharmacies = data.organisations + .filter((organisation) => { + return organisation.companyId === currentOrganisation.id && organisation.status === 'Active' && !activeNonGroupPharmacyIds.includes(organisation.id) && !activeTemporaryPharmacyIds.includes(organisation.id) + }) + .sort(sortByNameThenPostcode()) + + res.render('pharmacies/users/select-temporary-pharmacies', { + user, + pharmacies, + activeTemporaryPharmacyIds + }) + }) + + router.post('/pharmacies/users/:userId/temporary-access/select-pharmacies', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const currentOrganisation = res.locals.currentOrganisation + + const user = data.users.find((item) => item.id === userId) + + if (!user || !currentOrganisation) { + return res.redirect('/pharmacies/users') + } + + const groupRole = (user.organisations || []).find((org) => { + return org.id === currentOrganisation.id && org.permissionLevel === 'Group administrator' && org.status !== 'Deactivated' + }) + + if (!groupRole) { + return res.redirect('/pharmacies/users') + } + + const selectedPharmacyIdsToAdd = data.groupAdminTemporaryPharmacyIds + ? (Array.isArray(data.groupAdminTemporaryPharmacyIds) ? data.groupAdminTemporaryPharmacyIds : [data.groupAdminTemporaryPharmacyIds]) + : [] + + data.groupAdminTemporaryPharmacyIdsToAdd = [...new Set(selectedPharmacyIdsToAdd)] + + const existingActiveTemporaryPharmacyIds = data.existingActiveTemporaryPharmacyIds + ? (Array.isArray(data.existingActiveTemporaryPharmacyIds) ? data.existingActiveTemporaryPharmacyIds : [data.existingActiveTemporaryPharmacyIds]) + : [] + + const existingAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + const existingDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + const today = isoDateToday() + + const existingSelectedPharmacyIds = Object.keys(existingAddedDates) + const existingActiveTemporaryPharmacyIdsFromStore = existingSelectedPharmacyIds.filter((pharmacyId) => { + const addedOn = existingAddedDates[pharmacyId] || today + const deactivatedOn = existingDeactivatedDates[pharmacyId] + return !isTemporaryAccessInactive(addedOn, deactivatedOn) + }) + + data.groupAdminTemporaryPharmacyIds = [...new Set([ + ...existingSelectedPharmacyIds, + ...existingActiveTemporaryPharmacyIds, + ...data.groupAdminTemporaryPharmacyIdsToAdd + ])] + + const selectedPharmacyIdsToAddSet = new Set(data.groupAdminTemporaryPharmacyIdsToAdd) + + const projectedActiveSelectedPharmacyCount = data.groupAdminTemporaryPharmacyIds + .filter((pharmacyId) => { + if (selectedPharmacyIdsToAddSet.has(pharmacyId)) { + return true + } + + const addedOn = existingAddedDates[pharmacyId] || today + const deactivatedOn = existingDeactivatedDates[pharmacyId] + return !isTemporaryAccessInactive(addedOn, deactivatedOn) + }) + .length + + if (projectedActiveSelectedPharmacyCount > 40) { + const activeNonGroupPharmacyIds = (user.organisations || []) + .filter((org) => org.permissionLevel !== 'Group administrator' && org.status !== 'Deactivated') + .map((org) => org.id) + + const pharmacies = data.organisations + .filter((organisation) => { + return organisation.companyId === currentOrganisation.id && organisation.status === 'Active' && !activeNonGroupPharmacyIds.includes(organisation.id) && !existingActiveTemporaryPharmacyIdsFromStore.includes(organisation.id) + }) + .sort(sortByNameThenPostcode()) + + return res.render('pharmacies/users/select-temporary-pharmacies', { + user, + pharmacies, + activeTemporaryPharmacyIds: existingActiveTemporaryPharmacyIdsFromStore, + error: { + text: 'Group admininstrators cannot have access to more than 40 individual pharmacies.' + } + }) + } + + if (data.groupAdminTemporaryPharmacyIdsToAdd.length > 1) { + return res.redirect(`/pharmacies/users/${userId}/temporary-access/select-pharmacies-check`) + } + + if (data.groupAdminTemporaryPharmacyIdsToAdd.length === 1) { + return res.redirect(`/pharmacies/users/${userId}/temporary-access/confirm`) + } + + res.redirect(`/pharmacies/users/${userId}/manage`) + }) + + router.get('/pharmacies/users/:userId/temporary-access/confirm', (req, res) => { + const data = req.session.data + const userId = req.params.userId + + const selectedPharmacyIdsToAdd = data.groupAdminTemporaryPharmacyIdsToAdd + ? (Array.isArray(data.groupAdminTemporaryPharmacyIdsToAdd) ? data.groupAdminTemporaryPharmacyIdsToAdd : [data.groupAdminTemporaryPharmacyIdsToAdd]) + : [] + + const existingAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + const existingDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + const updatedAddedDates = {} + const updatedDeactivatedDates = {} + const today = isoDateToday() + const selectedPharmacyIds = [...new Set([ + ...Object.keys(existingAddedDates), + ...selectedPharmacyIdsToAdd + ])] + const selectedPharmacyIdsToAddSet = new Set(selectedPharmacyIdsToAdd) + + const newlyAddedPharmacyCount = selectedPharmacyIdsToAdd.filter((pharmacyId) => { + if (!existingAddedDates[pharmacyId]) { + return true + } + + return isTemporaryAccessInactive(existingAddedDates[pharmacyId], existingDeactivatedDates[pharmacyId]) + }).length + + for (const pharmacyId of selectedPharmacyIds) { + const existingAddedOn = existingAddedDates[pharmacyId] + const existingDeactivatedOn = existingDeactivatedDates[pharmacyId] + const shouldReactivate = selectedPharmacyIdsToAddSet.has(pharmacyId) && (!existingAddedOn || isTemporaryAccessInactive(existingAddedOn, existingDeactivatedOn)) + + updatedAddedDates[pharmacyId] = shouldReactivate ? today : existingAddedOn + + if (existingDeactivatedOn && !selectedPharmacyIdsToAddSet.has(pharmacyId)) { + updatedDeactivatedDates[pharmacyId] = existingDeactivatedOn + } + } + + data.groupAdminTemporaryPharmacyIds = selectedPharmacyIds + data.groupAdminTemporaryPharmacyIdsToAdd = [] + data.groupAdminTemporaryPharmacyAddedDates = updatedAddedDates + data.groupAdminTemporaryPharmacyDeactivatedDates = updatedDeactivatedDates + + if (newlyAddedPharmacyCount > 0) { + return res.redirect(`/pharmacies/users/${userId}/manage?temporaryAccessAdded=true&temporaryAccessAddedCount=${newlyAddedPharmacyCount}`) + } + + res.redirect(`/pharmacies/users/${userId}/manage`) + }) + + router.get('/pharmacies/users/:userId/temporary-access/select-pharmacies-check', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const currentOrganisation = res.locals.currentOrganisation + + const user = data.users.find((item) => item.id === userId) + + if (!user || !currentOrganisation) { + return res.redirect('/pharmacies/users') + } + + const groupRole = (user.organisations || []).find((org) => { + return org.id === currentOrganisation.id && org.permissionLevel === 'Group administrator' && org.status !== 'Deactivated' + }) + + if (!groupRole) { + return res.redirect('/pharmacies/users') + } + + const selectedPharmacyIds = data.groupAdminTemporaryPharmacyIdsToAdd + ? (Array.isArray(data.groupAdminTemporaryPharmacyIdsToAdd) ? data.groupAdminTemporaryPharmacyIdsToAdd : [data.groupAdminTemporaryPharmacyIdsToAdd]) + : [] + + const pharmacies = data.organisations.filter((organisation) => { + return selectedPharmacyIds.includes(organisation.id) && organisation.companyId === currentOrganisation.id && organisation.status === 'Active' + }) + + res.render('pharmacies/users/select-temporary-pharmacies-check', { + user, + pharmacies + }) + }) + + router.get('/pharmacies/users/:userId/temporary-access/select-pharmacies-check/remove/:pharmacyId', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const pharmacyId = req.params.pharmacyId + + const selectedPharmacyIdsToAdd = data.groupAdminTemporaryPharmacyIdsToAdd + ? (Array.isArray(data.groupAdminTemporaryPharmacyIdsToAdd) ? data.groupAdminTemporaryPharmacyIdsToAdd : [data.groupAdminTemporaryPharmacyIdsToAdd]) + : [] + + data.groupAdminTemporaryPharmacyIdsToAdd = selectedPharmacyIdsToAdd.filter((id) => id !== pharmacyId) + + const existingAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + data.groupAdminTemporaryPharmacyIds = [...new Set([ + ...Object.keys(existingAddedDates), + ...data.groupAdminTemporaryPharmacyIdsToAdd + ])] + + res.redirect(`/pharmacies/users/${userId}/temporary-access/select-pharmacies-check`) + }) + + router.get('/pharmacies/users/:userId/temporary-access/:pharmacyId/renew', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const pharmacyId = req.params.pharmacyId + const tab = req.query.tab === 'expired' ? 'expired' : 'active' + + const selectedPharmacyIds = data.groupAdminTemporaryPharmacyIds + ? (Array.isArray(data.groupAdminTemporaryPharmacyIds) ? data.groupAdminTemporaryPharmacyIds : [data.groupAdminTemporaryPharmacyIds]) + : [] + + if (!selectedPharmacyIds.includes(pharmacyId)) { + return res.redirect(`/pharmacies/users/${userId}/manage?tab=${tab}`) + } + + const existingAddedDates = data.groupAdminTemporaryPharmacyAddedDates || {} + const existingDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + const today = isoDateToday() + + const activeSelectedPharmacyCount = selectedPharmacyIds + .filter((id) => { + const addedOn = existingAddedDates[id] || today + const deactivatedOn = existingDeactivatedDates[id] + return !isTemporaryAccessInactive(addedOn, deactivatedOn) + }) + .length + + const renewalAddedOn = existingAddedDates[pharmacyId] || today + const renewalDeactivatedOn = existingDeactivatedDates[pharmacyId] + const renewalIsInactive = isTemporaryAccessInactive(renewalAddedOn, renewalDeactivatedOn) + + if (renewalIsInactive && activeSelectedPharmacyCount >= 40) { + return res.redirect(`/pharmacies/users/${userId}/manage?tab=${tab}&renewalLimitExceededPharmacyId=${pharmacyId}`) + } + + data.groupAdminTemporaryPharmacyAddedDates = { + ...existingAddedDates, + [pharmacyId]: isoDateToday() + } + + data.groupAdminTemporaryPharmacyDeactivatedDates = Object.fromEntries( + Object.entries(existingDeactivatedDates).filter(([id]) => id !== pharmacyId) + ) + + res.redirect(`/pharmacies/users/${userId}/manage?tab=${tab}&renewedTemporaryPharmacyId=${pharmacyId}`) + }) + + router.get('/pharmacies/users/:userId/temporary-access/:pharmacyId/deactivate', (req, res) => { + const data = req.session.data + const userId = req.params.userId + const pharmacyId = req.params.pharmacyId + const tab = req.query.tab === 'expired' ? 'expired' : 'active' + + const selectedPharmacyIds = data.groupAdminTemporaryPharmacyIds + ? (Array.isArray(data.groupAdminTemporaryPharmacyIds) ? data.groupAdminTemporaryPharmacyIds : [data.groupAdminTemporaryPharmacyIds]) + : [] + + if (!selectedPharmacyIds.includes(pharmacyId)) { + return res.redirect(`/pharmacies/users/${userId}/manage?tab=${tab}`) + } + + const existingDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates || {} + data.groupAdminTemporaryPharmacyDeactivatedDates = { + ...existingDeactivatedDates, + [pharmacyId]: isoDateToday() + } + + res.redirect(`/pharmacies/users/${userId}/manage?tab=${tab}&deactivatedTemporaryPharmacyId=${pharmacyId}`) + }) + router.get('/pharmacies/users/new',(req, res) => { res.render('pharmacies/users/new') @@ -471,6 +934,20 @@ module.exports = router => { const data = req.session.data const groupAdministrator = data.groupAdministrator const pharmacyIds = data.pharmacyIds || [] + const enteredEmail = (data.email || '').toLowerCase().trim() + + if (enteredEmail === blockedGroupAdminSimulationEmail) { + const currentOrganisation = data.organisations.find((organisation) => organisation.id === res.locals.currentOrganisation.id) + const groupAdminName = getGroupAdminNameForPharmacy(data, currentOrganisation) || 'group administrator' + const pharmacies = data.organisations.filter((organisation) => pharmacyIds.includes(organisation.id)) + + return res.render('pharmacies/users/check', { + pharmacies, + error: { + text: `Group administrator cannot be added by the pharmacy. Ask ${groupAdminName} to add themselves via the group admin interface.` + } + }) + } const user = { id: Math.floor(Math.random() * 10000000).toString(), @@ -613,6 +1090,19 @@ module.exports = router => { existingUser = data.users.find((user) => user.id === data.userId) } + const enteredEmail = (data.email || '').toLowerCase().trim() + if (!existingUser && enteredEmail === blockedGroupAdminSimulationEmail) { + const groupAdminName = getGroupAdminNameForPharmacy(data, organisation) || 'group administrator' + + return res.render('pharmacies/add-user-permission-level', { + organisation, + existingUser, + error: { + text: `This user is a Group administrator. Group administrators cannot be added by the pharmacy. Ask ${groupAdminName} to add themselves via the group administrator interface.` + } + }) + } + res.render('pharmacies/add-user-check', { organisation, existingUser @@ -624,6 +1114,20 @@ module.exports = router => { const id = req.params.id const organisation = data.organisations.find((organisation) => organisation.id === id) const existingUser = data.users.find((user) => user.id === data.userId) + const enteredEmail = (data.email || '').toLowerCase().trim() + + if (!existingUser && enteredEmail === blockedGroupAdminSimulationEmail) { + const groupAdminName = getGroupAdminNameForPharmacy(data, organisation) || 'group administrator' + + return res.render('pharmacies/add-user-permission-level', { + organisation, + existingUser, + error: { + text: `This user is a Group administrator. Group administrators cannot be added by the pharmacy. Ask ${groupAdminName} to add themselves via the group administrator interface.` + } + }) + } + let addedUserId if (existingUser) { diff --git a/app/routes/user-admin.js b/app/routes/user-admin.js index adff57a0..551d042e 100644 --- a/app/routes/user-admin.js +++ b/app/routes/user-admin.js @@ -1,5 +1,7 @@ module.exports = (router) => { + const blockedGroupAdminSimulationEmail = 'simulategroupadmin@nhs.net' + router.get('/user-admin', (req, res) => { const perPage = 20; // Max number of users to show per page @@ -176,6 +178,9 @@ module.exports = (router) => { emailError = 'Enter an email address' } else if (!(email.endsWith('nhs.net') || email.endsWith('.nhs.uk'))) { emailError = 'Enter an allowed email address' + } else if (email.toLowerCase().trim() === blockedGroupAdminSimulationEmail) { + const enteredUserName = `${firstName || ''} ${lastName || ''}`.trim() || 'this user' + emailError = `This user is a group administrator and cannot be added by the pharmacy. Ask ${enteredUserName} to add themselves via the group administrator interface.` } else if (existingUserWithSameEmail && existingUserWithSameEmail.status !== 'Deactivated') { emailError = 'This email address has already been added' } @@ -206,6 +211,15 @@ module.exports = (router) => { const data = req.session.data const email = data.email + const emailLower = (email || '').toLowerCase().trim() + + if (emailLower === blockedGroupAdminSimulationEmail) { + const enteredUserName = `${data.firstName || ''} ${data.lastName || ''}`.trim() || 'this user' + + return res.render('user-admin/add-user', { + emailError: `This user is a group administrator and cannot be added by the pharmacy. Ask ${enteredUserName} to add themselves via the group administrator interface.` + }) + } const existingUserWithSameEmail = data.users.find((user) => user.email === email) diff --git a/app/views/auth/select-pharmacy-for-access.html b/app/views/auth/select-pharmacy-for-access.html new file mode 100644 index 00000000..887a0549 --- /dev/null +++ b/app/views/auth/select-pharmacy-for-access.html @@ -0,0 +1,58 @@ +{% extends 'layout.html' %} + +{% set pageName = "Choose a pharmacy" %} + +{% block beforeContent %} + {{ backLink({ + href: "/auth/switch-organisation/work-type" + }) }} +{% endblock %} + +{% block content %} +
+
+ {% if error %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [error] + }) }} + {% endif %} + +
+ {% set items = [] %} + {% for pharmacy in pharmacies %} + {% set items = (items.push({ + value: pharmacy.id, + text: pharmacy.name + " (" + pharmacy.id + ")", + hint: { + text: (pharmacy.address.line1 + ", " if pharmacy.address.line1 else "") + (pharmacy.address.town + ", " if pharmacy.address.town else "") + pharmacy.address.postcode + } + }), items) %} + {% endfor %} + + {{ radios({ + name: "switchOrganisationPharmacyId", + fieldset: { + legend: { + text: "Which pharmacy do you want to access?", + classes: "nhsuk-fieldset__legend--l nhsuk-u-margin-bottom-5", + isPageHeading: true + } + }, + hint: { + text: "These are pharmacies where you have temporary access through " + groupOrganisation.name + "." + }, + value: data.switchOrganisationPharmacyId, + errorMessage: { + text: error.text + } if error, + items: items + }) }} + + {{ button({ + text: "Continue" + }) }} +
+
+
+{% endblock %} diff --git a/app/views/auth/switch-work-type.html b/app/views/auth/switch-work-type.html new file mode 100644 index 00000000..ac4aa0ca --- /dev/null +++ b/app/views/auth/switch-work-type.html @@ -0,0 +1,53 @@ +{% extends 'layout.html' %} + +{% set pageName = "Choose how you want to work" %} + +{% block beforeContent %} + {{ backLink({ + href: "/home" + }) }} +{% endblock %} + +{% block content %} +
+
+ {% if error %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [error] + }) }} + {% endif %} + +
+ {{ radios({ + name: "switchOrganisationWorkType", + fieldset: { + legend: { + text: "What type of work do you want to do?", + classes: "nhsuk-fieldset__legend--l nhsuk-u-margin-bottom-5", + isPageHeading: true + } + }, + value: data.switchOrganisationWorkType, + errorMessage: { + text: error.text + } if error, + items: [ + { + value: "group-admin", + text: "Operate as group administrator for " + groupOrganisation.name + " (" + groupOrganisation.id + ")" + }, + { + value: "individual-pharmacy", + text: "Access an individual pharmacy" + } + ] + }) }} + + {{ button({ + text: "Continue" + }) }} +
+
+
+{% endblock %} diff --git a/app/views/includes/header.html b/app/views/includes/header.html index 2f48b59a..a53139dc 100644 --- a/app/views/includes/header.html +++ b/app/views/includes/header.html @@ -130,10 +130,47 @@ {% endif %} +{% set switchOrganisationHref = "/auth/select-organisation" %} +{% set canUseGroupAdminTemporaryAccessSwitcher = false %} + +{% if currentUser and currentOrganisation %} + {% set temporaryPharmacyIds = data.groupAdminTemporaryPharmacyIds | asArray %} + {% set temporaryPharmacyAddedDates = data.groupAdminTemporaryPharmacyAddedDates | default({}) %} + {% set temporaryPharmacyDeactivatedDates = data.groupAdminTemporaryPharmacyDeactivatedDates | default({}) %} + {% set hasActiveGroupAdminHqRole = false %} + {% set availableTemporaryPharmacyCount = 0 %} + + {% for userOrganisation in (currentUser.organisations | default([])) %} + {% if userOrganisation.permissionLevel === "Group administrator" and userOrganisation.status === "Active" %} + {% set groupOrganisation = data.organisations | findById(userOrganisation.id) %} + + {% if groupOrganisation and groupOrganisation.type === "Pharmacy HQ" %} + {% set hasActiveGroupAdminHqRole = true %} + + {% for pharmacyId in temporaryPharmacyIds %} + {% set temporaryPharmacy = data.organisations | findById(pharmacyId) %} + {% set addedOn = temporaryPharmacyAddedDates[pharmacyId] %} + {% set deactivatedOn = temporaryPharmacyDeactivatedDates[pharmacyId] %} + {% set accessAgeDays = addedOn | daysSince %} + + {% if temporaryPharmacy and temporaryPharmacy.companyId === groupOrganisation.id and temporaryPharmacy.status === "Active" and not deactivatedOn and (not addedOn or (accessAgeDays != false and accessAgeDays <= 7)) %} + {% set availableTemporaryPharmacyCount = availableTemporaryPharmacyCount + 1 %} + {% endif %} + {% endfor %} + {% endif %} + {% endif %} + {% endfor %} + + {% if hasActiveGroupAdminHqRole and availableTemporaryPharmacyCount > 1 %} + {% set canUseGroupAdminTemporaryAccessSwitcher = true %} + {% set switchOrganisationHref = "/auth/switch-organisation/work-type" %} + {% endif %} +{% endif %} + {% set organisationHtml %} {{ currentOrganisation.name | truncate(30) }} - {% if currentUser and currentOrganisation and ((currentUser.organisations | length) > 1) %} - Change organisation + {% if currentUser and currentOrganisation and (((currentUser.organisations | length) > 1) or canUseGroupAdminTemporaryAccessSwitcher) %} + Change organisation {% endif %} {% endset %} diff --git a/app/views/pharmacies/add-user-permission-level.html b/app/views/pharmacies/add-user-permission-level.html index 0c991cd0..20a5208f 100644 --- a/app/views/pharmacies/add-user-permission-level.html +++ b/app/views/pharmacies/add-user-permission-level.html @@ -18,6 +18,13 @@ {% block content %}
+ {% if error %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [error] + }) }} + {% endif %} +

{% if existingUser %} {{ existingUser.firstName }} {{ existingUser.lastName }}'s role at {{ organisation.name }} ({{ organisation.id }}) @@ -57,7 +64,10 @@

}, name: "email", type: "email", - value: data.email + value: data.email, + errorMessage: { + text: error.text + } if error }) }} {% endif %} diff --git a/app/views/pharmacies/users/check.html b/app/views/pharmacies/users/check.html index 1b82c8cc..7e4fa342 100644 --- a/app/views/pharmacies/users/check.html +++ b/app/views/pharmacies/users/check.html @@ -12,6 +12,12 @@ {% block content %}
+ {% if error %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [error] + }) }} + {% endif %}

{% if data.groupAdministrator == "yes" %} diff --git a/app/views/pharmacies/users/index.html b/app/views/pharmacies/users/index.html index 9b57c9ad..f82e8bd1 100644 --- a/app/views/pharmacies/users/index.html +++ b/app/views/pharmacies/users/index.html @@ -81,7 +81,7 @@

Group administrators

{{ user.email }} {% if (groupAdministrators | length) >= 2 %} - Deactivate {{ user.firstName }} {{ user.lastName }} + Manage {{ user.firstName }} {{ user.lastName }} {% endif %} diff --git a/app/views/pharmacies/users/manage-group-user.html b/app/views/pharmacies/users/manage-group-user.html new file mode 100644 index 00000000..4ce5402d --- /dev/null +++ b/app/views/pharmacies/users/manage-group-user.html @@ -0,0 +1,179 @@ +{% extends 'layout.html' %} + +{% set currentSection = "pharmacies-users" %} +{% set pageName = user.firstName + " " + user.lastName %} + +{% block beforeContent %} + {{ backLink({ + href: "/pharmacies/users" + }) }} +{% endblock %} + +{% block content %} +
+
+ {% if renewedTemporaryPharmacy %} + {% set html %} +

+ Access renewed +

+

{{ user.firstName }} {{ user.lastName }}'s access to {{ renewedTemporaryPharmacy.name }} ({{ renewedTemporaryPharmacy.id }}) has been renewed. It will expire in 7 calendar days.

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

+ Access deactivated +

+

{{ user.firstName }} {{ user.lastName }} has been deactivated from {{ deactivatedTemporaryPharmacy.name }} ({{ deactivatedTemporaryPharmacy.id }}).

+ {% endset %} + + {{ notificationBanner({ + html: html, + type: "success" + }) }} + {% endif %} + + {% if temporaryAccessAdded and temporaryAccessAddedCount > 0 %} + {% set html %} +

+ Temporary access granted to pharmacies +

+

{{ user.firstName }} {{ user.lastName }} has been granted temporary access to {{ temporaryAccessAddedCount | plural('pharmacy') }}.

+ {% endset %} + + {{ notificationBanner({ + html: html, + type: "success" + }) }} + {% endif %} + + {% if renewalLimitExceededPharmacy %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [{ + text: user.firstName + " " + user.lastName + " cannot have access to more than 40 pharmacies. Access to " + renewalLimitExceededPharmacy.name + " (" + renewalLimitExceededPharmacy.id + ") was not renewed. To renew access, deactivate an active pharmacy and try again." + }] + }) }} + {% endif %} + +

{{ user.firstName }} {{ user.lastName }}

+ + {{ summaryList({ + rows: [ + { + key: { + text: "Email" + }, + value: { + text: user.email + } + }, + { + key: { + text: "User type" + }, + value: { + text: "Group administrator" + } + } + ] + }) }} + +

+ Deactivate group administrator +

+
+
+ +
+
+

Temporary pharmacy access

+ +
+
+

You can give this group administrator temporary access to up to 40 individual pharmacies in this pharmacy group.

+
+
+ + {% if (activeTemporaryAccessPharmacies | length) < 40 %} + {{ button({ + text: "Select pharmacies", + href: "/pharmacies/users/" + user.id + "/temporary-access/select-pharmacies", + classes: "nhsuk-button--secondary" + }) }} + {% endif %} + + {% if temporaryAccessPharmacies and (temporaryAccessPharmacies | length) > 0 %} + {{ appSecondaryNavigation({ + visuallyHiddenTitle: "Temporary pharmacy access by status", + items: [{ + text: "Active (" + (activeTemporaryAccessPharmacies | length) + ")", + href: "/pharmacies/users/" + user.id + "/manage?tab=active", + current: (currentTemporaryAccessTab == "active") + }, { + text: "Expired/Deactivated (" + (expiredTemporaryAccessPharmacies | length) + ")", + href: "/pharmacies/users/" + user.id + "/manage?tab=expired", + current: (currentTemporaryAccessTab == "expired") + }] + }) }} + + {% set rows = [] %} + + {% for pharmacy in currentTemporaryAccessPharmacies %} + {% set actionHtml %} + Renew access for {{ pharmacy.name }} + {% if currentTemporaryAccessTab == "active" %} +   + Deactivate access for {{ pharmacy.name }} + {% endif %} + {% endset %} + + {% if currentTemporaryAccessTab == "expired" %} + {% set rows = (rows.push([ + { text: pharmacy.name + ", " + pharmacy.address.postcode + " (" + pharmacy.id + ")" }, + { html: actionHtml } + ]), rows) %} + {% else %} + {% set rows = (rows.push([ + { text: pharmacy.name + ", " + pharmacy.address.postcode + " (" + pharmacy.id + ")" }, + { text: pharmacy.accessExpires }, + { html: actionHtml } + ]), rows) %} + {% endif %} + {% endfor %} + + {% if (rows | length) > 0 %} + {% if currentTemporaryAccessTab == "expired" %} + {{ table({ + head: [ + { text: "Pharmacy" }, + { text: "", classes: "app-table__cell--right-aligned" } + ], + rows: rows + }) }} + {% else %} + {{ table({ + head: [ + { text: "Pharmacy" }, + { text: "Access expires" }, + { text: "", classes: "app-table__cell--right-aligned" } + ], + rows: rows + }) }} + {% endif %} + {% elif currentTemporaryAccessTab == "expired" %} +

No expired temporary pharmacy access.

+ {% else %} +

No active temporary pharmacy access.

+ {% endif %} + {% endif %} +
+
+{% endblock %} diff --git a/app/views/pharmacies/users/manage-ind-user.html b/app/views/pharmacies/users/manage-ind-user.html index 64c0db5a..6ae33fc5 100644 --- a/app/views/pharmacies/users/manage-ind-user.html +++ b/app/views/pharmacies/users/manage-ind-user.html @@ -83,6 +83,14 @@

{{ user.firstName }} {{ user.lastName }}

value: { text: user.email } + }, + { + key: { + text: "User type" + }, + value: { + text: "Individual pharmacy user" + } } ] }) }} diff --git a/app/views/pharmacies/users/select-temporary-pharmacies-check.html b/app/views/pharmacies/users/select-temporary-pharmacies-check.html new file mode 100644 index 00000000..823c3344 --- /dev/null +++ b/app/views/pharmacies/users/select-temporary-pharmacies-check.html @@ -0,0 +1,60 @@ +{% extends 'layout.html' %} + +{% set currentSection = "pharmacies-users" %} + +{% block beforeContent %} + {{ backLink({ + href: "/pharmacies/users/" + user.id + "/temporary-access/select-pharmacies" + }) }} +{% endblock %} + +{% block content %} +
+
+ +

Check pharmacies

+ +

You are giving {{ user.firstName }} {{ user.lastName }} temporary access to these pharmacies.

+ + {% set rows = [] %} + + {% for pharmacy in pharmacies %} + + {% set removeHtml %} + Remove + {% endset %} + + {% set rows = (rows.push([ + { text: pharmacy.name + ", " + pharmacy.address.postcode + " (" + pharmacy.id + ")" }, + { html: removeHtml } + ]), rows) %} + {% endfor %} + + {% if (pharmacies | length) > 0 %} + {{ table({ + head: [ + { text: "Pharmacy" }, + { text: "" } + ], + rows: rows + }) }} + {% else %} +

No pharmacies selected.

+ {% endif %} + + {% set continueHref = "/pharmacies/users/" + user.id + "/manage" %} + + {% if (pharmacies | length) > 0 %} + {% set continueHref = "/pharmacies/users/" + user.id + "/temporary-access/confirm" %} + {% endif %} + + + {{ button({ + text: "Continue", + href: continueHref + }) }} + +
+
+ +{% endblock %} diff --git a/app/views/pharmacies/users/select-temporary-pharmacies.html b/app/views/pharmacies/users/select-temporary-pharmacies.html new file mode 100644 index 00000000..08e18b06 --- /dev/null +++ b/app/views/pharmacies/users/select-temporary-pharmacies.html @@ -0,0 +1,99 @@ +{% extends 'layout.html' %} + +{% set currentSection = "pharmacies-users" %} +{% set pageName = "Select pharmacies" %} + +{% block beforeContent %} + {{ backLink({ + href: "/pharmacies/users/" + user.id + "/manage" + }) }} +{% endblock %} + +{% block content %} +
+
+ {% if error %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [{ + text: error.text, + href: "#group-admin-temporary-pharmacy-ids" + }] + }) }} + {% endif %} + +
+ + {% for pharmacyId in (activeTemporaryPharmacyIds | default([])) %} + + {% endfor %} + + {% set items = [] %} + + {% for pharmacy in pharmacies %} + {% set items = (items.push({ + text: pharmacy.name + ", " + pharmacy.address.postcode + " (" + pharmacy.id + ")", + value: pharmacy.id + }), items) %} + {% endfor %} + + {% call fieldset({ + legend: { + text: "Which pharmacies should " + user.firstName + " " + user.lastName + " have temporary access to?", + size: "l" + } + }) %} + + {% if (pharmacies | length) > 10 %} + {{ input({ + id: "pharmacy-search", + name: "pharmacySearch", + type: "search", + label: { + text: "Search" + }, + classes: "nhsuk-input--width-20", + attributes: { + "data-module": "app-checkbox-filter" + }, + formGroup: { + classes: "nhsuk-u-margin-bottom-4" + } + }) }} + {% endif %} + + {% if (pharmacies | length) > 0 %} +
+ {{ checkboxes({ + id: "group-admin-temporary-pharmacy-ids", + name: "groupAdminTemporaryPharmacyIds", + values: data.groupAdminTemporaryPharmacyIdsToAdd, + errorMessage: { + text: error.text + } if error, + items: items + }) }} +
+ {% else %} +

There are no active pharmacies available to add for temporary access.

+ {% endif %} + + {% endcall %} + + {% set insetTextHtml %} +

This temporary access will last for 7 calendar days and will default to Lead administrator access. Group administrators may only hold access to 40 pharmacies at a time.

+ {% endset %} + + {{ insetText({ + html: insetTextHtml + }) }} + + {{ button({ + text: "Continue" + }) }} + +
+
+
+ +{% endblock %}