From 4111cb5258e6c800172b79f19fcf69d7874a1213 Mon Sep 17 00:00:00 2001 From: Ash <96007411+ashleygyngell@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:50:32 +0000 Subject: [PATCH] MAP:1878-2 (#7) * MAP-1878: adding remaining views & pages * MAP-1878: updating utils, adding js file & eslint tweak * MAP-1878: adding feComponents header and footer * MAP-1878: imageController added and views tweaked. styling also added * MAP-1878: updating env example * MAP-1878: add stylesheets * MAP-1878: add breadcrumbs * MAP-1878: 404 page and print function * MAP-1878: update e2e mock * MAP-1878: update test * MAP-1878: refactor and add more helm values * MAP-1878: update variable paths to package.json for application info --- .env.example | 26 ++++- assets/images/printer_icon.png | Bin 0 -> 403 bytes assets/images/prisoner-profile-image.png | Bin 0 -> 16720 bytes assets/js/establishment-roll.js | 49 ++++++++ assets/js/govukFrontendInit.js | 3 + assets/js/index.js | 1 + assets/js/initMoj.js | 1 + assets/js/printPage.js | 10 ++ assets/scss/application.scss | 17 +++ assets/scss/components/_alert-flags.scss | 45 +++++++ assets/scss/components/_print-link.scss | 9 ++ assets/scss/components/_results-table.scss | 25 ++++ assets/scss/components/_sortable-table.scss | 59 ++++++++++ assets/scss/local.scss | 30 +++++ assets/scss/pages/_establishment-roll.scss | 110 ++++++++++++++++++ assets/scss/print.scss | 62 ++++++++++ cypress.config.ts | 10 +- eslint.config.mjs | 4 +- feature.env | 6 +- helm_deploy/values-dev.yaml | 18 ++- integration_tests/e2e/health.cy.ts | 1 + integration_tests/mockApis/feComponents.ts | 78 +++++++++++++ package-lock.json | 61 +++++++++- package.json | 1 + server/app.ts | 4 + server/applicationInfo.ts | 2 +- server/config.ts | 15 +++ server/controllers/imageController.ts | 31 +++++ server/data/feComponentsClient.ts | 51 ++++++++ server/data/index.ts | 12 +- server/data/prisonApiRestClient.ts | 12 +- server/middleware/getFeComponents.ts | 33 ++++++ server/middleware/setUpPageNotFound.ts | 14 +++ server/middleware/setUpStaticResources.ts | 11 ++ server/middleware/setUpWebSecurity.ts | 10 +- server/routes/index.ts | 32 ++--- server/services/feComponentsService.ts | 34 ++++++ server/services/index.ts | 23 ++-- server/services/movementsService.ts | 10 +- server/utils/nunjucksSetup.ts | 11 +- server/utils/utils.ts | 37 ++++++ server/views/macros/hmppsPagedListFooter.njk | 14 +++ server/views/macros/hmppsPagedListHeader.njk | 17 +++ server/views/macros/hmppsSortSelector.njk | 36 ++++++ server/views/macros/printLink.njk | 2 +- server/views/notFound.njk | 29 +++++ server/views/pages/arrivingToday.njk | 60 ++++++++++ server/views/pages/currentlyOut.njk | 60 ++++++++++ server/views/pages/enRoute.njk | 60 ++++++++++ server/views/pages/establishmentRoll.njk | 23 ++-- .../views/pages/establishmentRollLanding.njk | 63 ++++++++++ server/views/pages/inReception.njk | 58 +++++++++ server/views/pages/noCellAllocated.njk | 73 ++++++++++++ server/views/pages/outToday.njk | 58 +++++++++ server/views/partials/layout.njk | 50 +++++++- 55 files changed, 1496 insertions(+), 75 deletions(-) create mode 100644 assets/images/printer_icon.png create mode 100644 assets/images/prisoner-profile-image.png create mode 100644 assets/js/establishment-roll.js create mode 100644 assets/js/govukFrontendInit.js create mode 100644 assets/js/initMoj.js create mode 100644 assets/js/printPage.js create mode 100644 assets/scss/components/_alert-flags.scss create mode 100644 assets/scss/components/_print-link.scss create mode 100644 assets/scss/components/_results-table.scss create mode 100644 assets/scss/components/_sortable-table.scss create mode 100644 assets/scss/pages/_establishment-roll.scss create mode 100644 assets/scss/print.scss create mode 100644 integration_tests/mockApis/feComponents.ts create mode 100644 server/controllers/imageController.ts create mode 100644 server/data/feComponentsClient.ts create mode 100644 server/middleware/getFeComponents.ts create mode 100644 server/middleware/setUpPageNotFound.ts create mode 100644 server/services/feComponentsService.ts create mode 100644 server/views/macros/hmppsPagedListFooter.njk create mode 100644 server/views/macros/hmppsPagedListHeader.njk create mode 100644 server/views/macros/hmppsSortSelector.njk create mode 100644 server/views/notFound.njk create mode 100644 server/views/pages/arrivingToday.njk create mode 100644 server/views/pages/currentlyOut.njk create mode 100644 server/views/pages/enRoute.njk create mode 100644 server/views/pages/establishmentRollLanding.njk create mode 100644 server/views/pages/inReception.njk create mode 100644 server/views/pages/noCellAllocated.njk create mode 100644 server/views/pages/outToday.njk diff --git a/.env.example b/.env.example index f0508e1..a08b797 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,26 @@ REDIS_ENABLED=false -TOKEN_VERIFICATION_ENABLED=false +TOKEN_VERIFICATION_ENABLED=true +PRODUCT_ID=DPS118 + +HMPPS_AUTH_URL=https://sign-in-dev.hmpps.service.justice.gov.uk/auth +TOKEN_VERIFICATION_API_URL=https://token-verification-api-dev.prison.service.justice.gov.uk + +LOCATIONS_INSIDE_PRISON_API_URL=https://locations-inside-prison-api-dev.hmpps.service.justice.gov.uk +PRISON_API_URL=https://prison-api-dev.prison.service.justice.gov.uk +PRISONER_SEARCH_API_URL=https://prisoner-search-dev.prison.service.justice.gov.uk +COMPONENT_API_URL=https://frontend-components-dev.hmpps.service.justice.gov.uk +COMMON_COMPONENTS_ENABLED=true + +DIGITAL_PRISONS_URL=https://digital-dev.prison.service.justice.gov.uk +PRISONER_PROFILE_URL=https://prisoner-dev.digital.prison.service.justice.gov.uk +CHANGE_SOMEONES_CELL_URL=https://change-someones-cell-dev.prison.service.justice.gov.uk # Credentials for allowing user access -AUTH_CODE_CLIENT_ID=hmpps-typescript-template -AUTH_CODE_CLIENT_SECRET=clientsecret +AUTH_CODE_CLIENT_ID= +AUTH_CODE_CLIENT_SECRET= # Credentials for API calls -CLIENT_CREDS_CLIENT_ID=hmpps-typescript-template-system -CLIENT_CREDS_CLIENT_SECRET=clientsecret +CLIENT_CREDS_CLIENT_ID= +CLIENT_CREDS_CLIENT_SECRET= + +ENVIRONMENT_NAME=DEV diff --git a/assets/images/printer_icon.png b/assets/images/printer_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..14f892c237938a82a67d5c1e906db1df8d4176a8 GIT binary patch literal 403 zcmV;E0c`$>P)Px$O-V#SR9Fe^mCX&pFbstYf(!J>2yBDIUPz3=60E=m+}MCik>J3OQ&GvO+BDB8 z=p7O%YG`fe`HN$UVw|y+FmG^JqRry6Y}z)PmvG7 z=A8qIGwlpP^fC)fJ&a5+6HFzL>hJy?#^`OSI(q%{CFw;I`GKC`EHaT_nz?(Rhj%CF z31)%%>@4ft2QAdJPp&~t!?L#Y!FJz5_x1YRkTd-eTL{)kFUt}lajPm5d3u7DK4>DL zY7HW9K4P_@b@>#>yUa)EiO^^=uYD3~(2Fya9eoWBC7{2%%$w}>dvp!Op(kj*BndSL x5vM1JWCPVsATSh%o}i@$$GFJ>!2TzCf*<@PcYbY2g6aSO002ovPDHLkV1kiWto#4~ literal 0 HcmV?d00001 diff --git a/assets/images/prisoner-profile-image.png b/assets/images/prisoner-profile-image.png new file mode 100644 index 0000000000000000000000000000000000000000..1fed4c7df22ec09780786147096cd609e7ec8ae9 GIT binary patch literal 16720 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 Xwv%%6#>G!S35en&wTJmKCISBsFW+)p literal 0 HcmV?d00001 diff --git a/assets/js/establishment-roll.js b/assets/js/establishment-roll.js new file mode 100644 index 0000000..7a874b8 --- /dev/null +++ b/assets/js/establishment-roll.js @@ -0,0 +1,49 @@ +const landingRows = document.querySelectorAll('.establishment-roll__table__landing-row') +const spurRows = document.querySelectorAll('.establishment-roll__table__spur-row') +const wingRows = document.querySelectorAll('.establishment-roll__table__wing-row') +const totalsRow = document.querySelector('#roll-table-totals-row') + +function init() { + ;[...landingRows, ...spurRows].forEach(row => { + row.setAttribute('hidden', 'hidden') + }) + + wingRows.forEach((wingRow, index) => { + const wingId = wingRow.getAttribute('id') + const wingNameCell = wingRow.getElementsByTagName('td')[0] + const wingNameText = wingNameCell.innerText + const childRows = document.querySelectorAll('[data-wing-id="' + wingId + '"]') + const childrenIds = [...childRows].map(row => row.getAttribute('id')) + + const wingLink = document.createElement('a') + wingLink.setAttribute('href', '#') + wingLink.setAttribute('class', 'govuk-details__summary govuk-link--no-visited-state') + wingLink.setAttribute('aria-controls', childrenIds.join(' ')) + wingLink.innerHTML = wingNameText + + wingLink.addEventListener('click', function (event) { + event.preventDefault() + const nextRow = wingRows[index + 1] ? wingRows[index + 1] : totalsRow + + childRows.forEach(row => { + const isOpen = !row.getAttribute('hidden') + + if (isOpen) { + row.setAttribute('hidden', 'hidden') + wingLink.setAttribute('aria-expanded', 'false') + wingRow.classList.remove('open') + nextRow.classList.remove('next-wing-to-open') + } else { + row.removeAttribute('hidden') + wingLink.setAttribute('aria-expanded', 'true') + wingRow.classList.add('open') + nextRow.classList.add('next-wing-to-open') + } + }) + }) + + wingNameCell.replaceChildren(wingLink) + }) +} + +init() diff --git a/assets/js/govukFrontendInit.js b/assets/js/govukFrontendInit.js new file mode 100644 index 0000000..ee1be48 --- /dev/null +++ b/assets/js/govukFrontendInit.js @@ -0,0 +1,3 @@ +import { initAll } from '/assets/govuk/govuk-frontend.min.js' + +initAll() diff --git a/assets/js/index.js b/assets/js/index.js index 9243e49..a1e2d25 100644 --- a/assets/js/index.js +++ b/assets/js/index.js @@ -3,3 +3,4 @@ import * as mojFrontend from '@ministryofjustice/frontend' govukFrontend.initAll() mojFrontend.initAll() + diff --git a/assets/js/initMoj.js b/assets/js/initMoj.js new file mode 100644 index 0000000..5dd458d --- /dev/null +++ b/assets/js/initMoj.js @@ -0,0 +1 @@ +window.MOJFrontend.initAll() diff --git a/assets/js/printPage.js b/assets/js/printPage.js new file mode 100644 index 0000000..e9d8d56 --- /dev/null +++ b/assets/js/printPage.js @@ -0,0 +1,10 @@ +const printLinks = document.querySelectorAll('.print-link') + +if (printLinks?.length) { + printLinks.forEach(el => + el.addEventListener('click', evt => { + evt.preventDefault() + window.print() + }), + ) +} diff --git a/assets/scss/application.scss b/assets/scss/application.scss index 2050c2a..41dbc9d 100644 --- a/assets/scss/application.scss +++ b/assets/scss/application.scss @@ -6,6 +6,23 @@ $govuk-page-width: $moj-page-width; @import "govuk-frontend/dist/govuk/all"; @import "@ministryofjustice/frontend/moj/all"; +@import "node_modules/govuk-frontend/dist/govuk/base"; +@import "node_modules/govuk-frontend/dist/govuk/core"; +@import "node_modules/govuk-frontend/dist/govuk/objects"; +@import "node_modules/govuk-frontend/dist/govuk/components"; +@import "node_modules/govuk-frontend/dist/govuk/utilities"; +@import "node_modules/govuk-frontend/dist/govuk/overrides"; +@import 'node_modules/@ministryofjustice/hmpps-connect-dps-components/dist/assets/footer'; +@import 'node_modules/@ministryofjustice/hmpps-connect-dps-components/dist/assets/header-bar'; +@import 'node_modules/@ministryofjustice/hmpps-connect-dps-shared-items/dist/assets/scss/all'; + +@import './components/print-link'; +@import './components/alert-flags'; @import './components/header-bar'; +@import './components/results-table'; +@import './components/sortable-table'; +@import './pages/establishment-roll'; @import './local'; +@import './print'; + diff --git a/assets/scss/components/_alert-flags.scss b/assets/scss/components/_alert-flags.scss new file mode 100644 index 0000000..625a70c --- /dev/null +++ b/assets/scss/components/_alert-flags.scss @@ -0,0 +1,45 @@ +.alerts-list { + @extend %govuk-list; + margin-bottom: 0; + + @media screen { + display: flex; + flex-wrap: wrap; + } + + li { + margin-bottom: 10px; + margin-right: 10px; + } +} + +.cat-a-status { + @extend %status-style; + + &--a, + &--e { + color: #00437b; + border-color: #00437b; + } + + &--h { + background-color: #00437b; + color: white; + border-color: #00437b; + } + + &--p { + border: 3px dashed; + color: #00437b; + border-color: #00437b; + } +} + +.non-association { + @extend %status-style; + + background: lighten(#D4351C,40); + border-color: lighten(#D4351C,40); + font-weight: bold; + color: #D4351C; +} diff --git a/assets/scss/components/_print-link.scss b/assets/scss/components/_print-link.scss new file mode 100644 index 0000000..c3b836d --- /dev/null +++ b/assets/scss/components/_print-link.scss @@ -0,0 +1,9 @@ +.hmpps-print-link { + display: inline-block; + margin: 0 0 15px -10px; + position: relative; + padding: 0.5em 0.5em 0.5em 38px; + background: url('/assets/images/printer_icon.png') no-repeat 10px 50%; + background-size: 16px 18px; + cursor: pointer; +} diff --git a/assets/scss/components/_results-table.scss b/assets/scss/components/_results-table.scss new file mode 100644 index 0000000..fddf00c --- /dev/null +++ b/assets/scss/components/_results-table.scss @@ -0,0 +1,25 @@ +.results-table { + &__results { + overflow: auto; + + &__image { + width: 90px; + + @media print { + width: 60px; + } + } + + .govuk-table__header { + vertical-align: bottom; + } + + .govuk-table__cell { + vertical-align: middle; + } + + .alerts-list { + display: flex; + } + } +} diff --git a/assets/scss/components/_sortable-table.scss b/assets/scss/components/_sortable-table.scss new file mode 100644 index 0000000..fd8ea4e --- /dev/null +++ b/assets/scss/components/_sortable-table.scss @@ -0,0 +1,59 @@ +[aria-sort] a, +[aria-sort] a:hover { + background-color: rgba(0, 0, 0, 0); + border-width: 0; + -webkit-box-shadow: 0 0 0 0; + -moz-box-shadow: 0 0 0 0; + box-shadow: 0 0 0 0; + color: #005ea5; + cursor: pointer; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + padding: 0 10px 0 0; + position: relative; + text-align: inherit; + font-size: 1em; + margin: 0; + text-decoration: none; +} + +[aria-sort] a:before { + content: ' ▼'; + position: absolute; + right: -1px; + top: 9px; + font-size: 0.5em; +} + +[aria-sort] a:after { + content: ' ▲'; + position: absolute; + right: -1px; + top: 1px; + font-size: 0.5em; +} + +[aria-sort='descending'] a:before { + display: none; +} + +[aria-sort='descending'] a:after { + content: ' ▼'; + font-size: 0.8em; + position: absolute; + right: -5px; + top: 2px; +} + +[aria-sort='ascending'] a:before { + display: none; +} + +[aria-sort='ascending'] a:after { + content: ' ▲'; + font-size: 0.8em; + position: absolute; + right: -5px; + top: 2px; +} diff --git a/assets/scss/local.scss b/assets/scss/local.scss index 35cba2b..69a726e 100644 --- a/assets/scss/local.scss +++ b/assets/scss/local.scss @@ -1,3 +1,33 @@ .govuk-main-wrapper { min-height: 600px; + padding-top: 0; +} + +.text-align-right { + text-align: right; +} + +.text-align-left { + text-align: left; +} + +// Creates width helper classes to set input, paragraph or other element width based on EMs +// e.g. hmpps-width-20 sets the width of the element to 20em +// Classes created got from 1em to 40em +@mixin hmpps-width-x { + @for $i from 1 through 40 { + .hmpps-width-#{$i} { width: #{$i}em; } + } +} +@include hmpps-width-x; + +.govuk-link { + text-underline-offset: .1em; +} + +// Print styles +@include govuk-media-query($media-type: print) { + .govuk-template { + background-color: govuk-colour('white'); + } } diff --git a/assets/scss/pages/_establishment-roll.scss b/assets/scss/pages/_establishment-roll.scss new file mode 100644 index 0000000..fc2073c --- /dev/null +++ b/assets/scss/pages/_establishment-roll.scss @@ -0,0 +1,110 @@ +.establishment-roll { + &__label-and-value { + display: flex; + justify-content: space-between; + + @media (min-width: #{map-get($govuk-breakpoints, 'desktop')}) { + flex-basis: 50%; + padding-right: 90px; + } + + @media print { + padding-right: 0; + h2, p { + font-size: 14pt !important; + } + } + + &--with-space { + @media print, (min-width: #{map-get($govuk-breakpoints, 'desktop')}) { + flex-basis: 33%; + } + } + } + + &__table { + border-collapse: separate; + tbody { + tr:last-child { + * { + border-bottom: none; + } + } + } + + &__wing-row { + td { + padding: 13px 51px 7px 0; + } + + td:first-of-type { + a[aria-expanded='true']:before { + display: block; + width: 0; + height: 0; + -webkit-clip-path: polygon(0 0, 50% 100%, 100% 0); + clip-path: polygon(0 0, 50% 100%, 100% 0); + border-color: transparent; + border-style: solid; + border-width: 12.124px 7px 0; + border-top-color: inherit; + } + } + &.open { + td { + border-bottom: none; + } + } + + + } + + &__landing-row { + font-size: 16px; + + td:first-of-type { + display: block; + margin-left: 46px; + border-collapse: separate; + + a { + color: govuk-colour('blue'); + } + } + + &.last-in-group { + td { + border-bottom: none; + margin-bottom: 10px; + } + } + + } + + &__spur-row { + font-size: 16px; + + td:first-of-type { + display: block; + margin-left: 30px; + border-collapse: separate; + font-weight: bold; + padding-left: 15px; + font-size: 19px; + padding-top: 8px; + } + + td { + background-color: govuk-colour('light-grey'); + border-bottom: none; + padding-top: 10px; + } + } + + tr.next-wing-to-open { + td { + border-top: 1px solid govuk-colour('mid-grey'); + } + } + } +} \ No newline at end of file diff --git a/assets/scss/print.scss b/assets/scss/print.scss new file mode 100644 index 0000000..0fce2b0 --- /dev/null +++ b/assets/scss/print.scss @@ -0,0 +1,62 @@ +@page { + margin: 0; +} + +@media screen { + .print-only { + display: none; + } +} + +@include govuk-media-query($media-type: print) { + // govuk-frontend overrides + .govuk-heading-l { + font-size: 19pt; + } + + .govuk-heading-m { + font-size: 16pt; + } + + .govuk-heading-s { + font-size: 14pt; + } + + .govuk-body-l { + font-size: 12pt; + } + + .printed-page { + background: none; + } + + .govuk-table { + font-size: 10pt; + + // Remove styling from MOJ sortable table colum headers + [aria-sort] button { + color: inherit; + + &:before, + &:after { + content: ''; + } + } + } + + .govuk-link { + text-decoration: none; + + &:link, + &:visited { + color: $govuk-text-colour; + } + + // Remove url from links in printed view within a table + &[href^="/"], &[href^="http://"], &[href^="https://"] { + &::after { + content: ''; + } + } + } +} diff --git a/cypress.config.ts b/cypress.config.ts index 8a5d695..2dbd00b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,10 +1,11 @@ import { defineConfig } from 'cypress' import { resetStubs } from './integration_tests/mockApis/wiremock' import auth from './integration_tests/mockApis/auth' -import tokenVerification from './integration_tests/mockApis/tokenVerification' -import prisonerSearch from './integration_tests/mockApis/prisonerSearch' +import feComponents from './integration_tests/mockApis/feComponents' import locations from './integration_tests/mockApis/locations' import prison from './integration_tests/mockApis/prison' +import prisonerSearch from './integration_tests/mockApis/prisonerSearch' +import tokenVerification from './integration_tests/mockApis/tokenVerification' export default defineConfig({ chromeWebSecurity: false, @@ -21,10 +22,11 @@ export default defineConfig({ on('task', { reset: resetStubs, ...auth, - ...tokenVerification, - ...prison, + ...feComponents, ...locations, + ...prison, ...prisonerSearch, + ...tokenVerification, }) }, baseUrl: 'http://localhost:3007', diff --git a/eslint.config.mjs b/eslint.config.mjs index 91d53fb..de1f844 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,3 +1,5 @@ import hmppsConfig from '@ministryofjustice/eslint-config-hmpps' -export default hmppsConfig() +export default hmppsConfig({ + extraIgnorePaths: ['assets'], +}) diff --git a/feature.env b/feature.env index 9a44faa..d2138f5 100644 --- a/feature.env +++ b/feature.env @@ -1,4 +1,5 @@ PORT=3007 +PRODUCT_ID=DPS118 HMPPS_AUTH_URL=http://localhost:9091/auth TOKEN_VERIFICATION_API_URL=http://localhost:9091/verification TOKEN_VERIFICATION_ENABLED=true @@ -10,7 +11,8 @@ CLIENT_CREDS_CLIENT_ID=clientid CLIENT_CREDS_CLIENT_SECRET=clientsecret LOCATIONS_INSIDE_PRISON_API_URL=http://localhost:9091/locations PRISON_API_URL=http://localhost:9091/prison +DIGITAL_PRISONS_URL=http://localhost:9091/dps PRISONER_SEARCH_API_URL=http://localhost:9091/prisoner-search +PRISONER_PROFILE_URL=http://localhost:9091/prisoner-profile COMPONENT_API_URL=http://localhost:9091/frontend-components -MANAGE_USERS_API_URL=http://localhost:9091/manage-users-api -ENVIRONMENT_NAME=DEV +ENVIRONMENT_NAME=dev diff --git a/helm_deploy/values-dev.yaml b/helm_deploy/values-dev.yaml index dce09ab..d88a7e5 100644 --- a/helm_deploy/values-dev.yaml +++ b/helm_deploy/values-dev.yaml @@ -9,12 +9,24 @@ generic-service: env: INGRESS_URL: "https://prison-roll-count-dev.hmpps.service.justice.gov.uk" + + ENVIRONMENT_NAME: DEV + PRODUCT_ID: "DPS118" HMPPS_AUTH_URL: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth" - TOKEN_VERIFICATION_API_URL: "https://token-verification-api-dev.prison.service.justice.gov.uk" + HMPPS_COOKIE_DOMAIN: digital-dev.prison.service.justice.gov.uk + HMPPS_COOKIE_NAME: hmpps-session-dev + + # APIs + LOCATIONS_INSIDE_PRISON_API_URL: "https://locations-inside-prison-api-dev.hmpps.service.justice.gov.uk" PRISON_API_URL: "https://prison-api-dev.prison.service.justice.gov.uk" PRISONER_SEARCH_API_URL: "https://prisoner-search-dev.prison.service.justice.gov.uk" - LOCATIONS_INSIDE_PRISON_API_URL: "https://locations-inside-prison-api-dev.hmpps.service.justice.gov.uk" - ENVIRONMENT_NAME: DEV + TOKEN_VERIFICATION_API_URL: "https://token-verification-api-dev.prison.service.justice.gov.uk" + + # Service URLs + CHANGE_SOMEONES_CELL_URL: https://change-someones-cell-dev.prison.service.justice.gov.uk + DIGITAL_PRISONS_URL: https://digital-dev.prison.service.justice.gov.uk + PRISONER_PROFILE_URL: https://prisoner-dev.digital.prison.service.justice.gov.uk + AUDIT_ENABLED: "false" generic-prometheus-alerts: diff --git a/integration_tests/e2e/health.cy.ts b/integration_tests/e2e/health.cy.ts index 8d40eec..f40f11b 100644 --- a/integration_tests/e2e/health.cy.ts +++ b/integration_tests/e2e/health.cy.ts @@ -4,6 +4,7 @@ context('Healthcheck', () => { cy.task('reset') cy.task('stubAuthPing') cy.task('stubLocationsApiPing') + cy.task('stubFeComponentsPing') cy.task('stubPrisonApiPing') cy.task('stubPrisonerSearchApiPing') cy.task('stubTokenVerificationPing') diff --git a/integration_tests/mockApis/feComponents.ts b/integration_tests/mockApis/feComponents.ts new file mode 100644 index 0000000..336ebba --- /dev/null +++ b/integration_tests/mockApis/feComponents.ts @@ -0,0 +1,78 @@ +import type Service from '@ministryofjustice/hmpps-connect-dps-components/dist/types/Service' +import { stubFor } from './wiremock' +import { CaseLoad } from '../../server/data/interfaces/caseLoad' + +export default { + stubFeComponentsPing: () => + stubFor({ + request: { + method: 'GET', + urlPath: '/frontend-components/health/ping', + }, + response: { + status: 200, + }, + }), + + stubFeComponents: ( + options: { + caseLoads?: CaseLoad[] + services?: Service[] + residentialLocationsActive?: boolean + } = {}, + ) => { + const caseLoads = options.caseLoads || [ + { + caseLoadId: 'LEI', + currentlyActive: true, + description: 'Leeds (HMP)', + type: '', + caseloadFunction: '', + }, + ] + + return stubFor({ + request: { + method: 'GET', + urlPattern: '/frontend-components/components\\?.*', + }, + response: { + status: 200, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + jsonBody: { + header: { html: '', css: [], javascript: [] }, + footer: { html: '', css: [], javascript: [] }, + meta: { + caseLoads, + activeCaseLoad: caseLoads.find(caseLoad => caseLoad.currentlyActive === true), + services: options.services || [ + { + id: 'check-my-diary', + heading: 'Check my diary', + description: 'View your prison staff detail (staff rota) from home.', + href: 'http://localhost:3001', + navEnabled: true, + }, + { + id: 'key-worker-allocations', + heading: 'My key worker allocation', + description: 'View your key worker cases.', + href: 'http://localhost:3001/key-worker/111111', + navEnabled: true, + }, + { + id: 'residential-locations', + heading: 'Residential Locations', + description: 'Manage residential locations.', + href: 'http://localhost:3001/locations', + navEnabled: options.residentialLocationsActive, + }, + ], + }, + }, + }, + }) + }, +} diff --git a/package-lock.json b/package-lock.json index fd0d52f..0cc6ffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-sqs": "^3.699.0", "@ministryofjustice/frontend": "^3.1.0", + "@ministryofjustice/hmpps-connect-dps-components": "2.0.0", "@ministryofjustice/hmpps-connect-dps-shared-items": "^1.2.1", "@ministryofjustice/hmpps-monitoring": "^0.0.1-beta.2", "agentkeepalive": "^4.5.0", @@ -2394,6 +2395,65 @@ "jquery": "^3.6.0" } }, + "node_modules/@ministryofjustice/hmpps-connect-dps-components": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ministryofjustice/hmpps-connect-dps-components/-/hmpps-connect-dps-components-2.0.0.tgz", + "integrity": "sha512-WF7tkLbghfCTP/AN+dFfeme/RgkEaHDITApPEiITvvG1/P/Mluta+TFCSx6lL49sxShshcA5/OuDbNa7hGvJOg==", + "license": "MIT", + "dependencies": { + "@types/node": "^20.17.6", + "@types/nunjucks": "^3.2.6", + "nunjucks": "^3.2.4", + "superagent": "^9.0.2" + } + }, + "node_modules/@ministryofjustice/hmpps-connect-dps-components/node_modules/@types/node": { + "version": "20.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", + "integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@ministryofjustice/hmpps-connect-dps-components/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@ministryofjustice/hmpps-connect-dps-components/node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@ministryofjustice/hmpps-connect-dps-components/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/@ministryofjustice/hmpps-connect-dps-shared-items": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@ministryofjustice/hmpps-connect-dps-shared-items/-/hmpps-connect-dps-shared-items-1.2.1.tgz", @@ -3605,7 +3665,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz", "integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==", - "dev": true, "license": "MIT" }, "node_modules/@types/oauth": { diff --git a/package.json b/package.json index b079f43..47b3d1a 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@aws-sdk/client-sqs": "^3.699.0", "@ministryofjustice/frontend": "^3.1.0", "@ministryofjustice/hmpps-connect-dps-shared-items": "^1.2.1", + "@ministryofjustice/hmpps-connect-dps-components": "2.0.0", "@ministryofjustice/hmpps-monitoring": "^0.0.1-beta.2", "agentkeepalive": "^4.5.0", "applicationinsights": "^2.9.6", diff --git a/server/app.ts b/server/app.ts index e5827e9..6e872d9 100755 --- a/server/app.ts +++ b/server/app.ts @@ -2,6 +2,7 @@ import express from 'express' import createError from 'http-errors' +import getFrontendComponents from './middleware/getFeComponents' import nunjucksSetup from './utils/nunjucksSetup' import errorHandler from './errorHandler' import { appInsightsMiddleware } from './utils/azureAppInsights' @@ -21,6 +22,7 @@ import type { Services } from './services' import populateClientToken from './middleware/populateClientToken' import setUpEnvironmentName from './middleware/setUpEnvironmentName' +import setUpPageNotFound from './middleware/setUpPageNotFound' import { ensureActiveCaseLoadSet } from './middleware/ensureActiveCaseLoadSet' @@ -45,9 +47,11 @@ export default function createApp(services: Services): express.Application { app.use(setUpCurrentUser(services)) app.use(populateClientToken()) + app.get('*', getFrontendComponents(services)) app.use(ensureActiveCaseLoadSet(services.userService)) app.use(routes(services)) + app.use(setUpPageNotFound) app.use((req, res, next) => next(createError(404, 'Not found'))) app.use(errorHandler(process.env.NODE_ENV === 'production')) diff --git a/server/applicationInfo.ts b/server/applicationInfo.ts index c28c127..804d1b1 100644 --- a/server/applicationInfo.ts +++ b/server/applicationInfo.ts @@ -14,7 +14,7 @@ export type ApplicationInfo = { } export default (): ApplicationInfo => { - const packageJson = path.join(__dirname, '../../package.json') + const packageJson = path.join(__dirname, `${__dirname.endsWith('/dist/server') ? '../' : ''}../package.json`) const { name: applicationName } = JSON.parse(fs.readFileSync(packageJson).toString()) return { applicationName, buildNumber, gitRef, gitShortHash: gitRef.substring(0, 7), productId, branchName } } diff --git a/server/config.ts b/server/config.ts index 8a1a4d2..1826ce4 100755 --- a/server/config.ts +++ b/server/config.ts @@ -68,6 +68,16 @@ export default { expiryMinutes: Number(get('WEB_SESSION_TIMEOUT_IN_MINUTES', 120)), }, apis: { + frontendComponents: { + url: get('COMPONENT_API_URL', 'http://localhost:8082', requiredInProduction), + healthPath: '/health/ping', + timeout: { + response: Number(get('COMPONENT_API_TIMEOUT_SECONDS', 10000)), + deadline: Number(get('COMPONENT_API_TIMEOUT_SECONDS', 10000)), + }, + agent: new AgentConfig(Number(get('COMPONENT_API_TIMEOUT_SECONDS', 10000))), + enabled: get('COMMON_COMPONENTS_ENABLED', 'true') === 'true', + }, hmppsAuth: { url: get('HMPPS_AUTH_URL', 'http://localhost:9090/auth', requiredInProduction), healthPath: '/health/ping', @@ -120,6 +130,11 @@ export default { enabled: get('TOKEN_VERIFICATION_ENABLED', 'false') === 'true', }, }, + serviceUrls: { + digitalPrisons: get('DIGITAL_PRISONS_URL', 'http://localhost:3001', requiredInProduction), + prisonerProfile: get('PRISONER_PROFILE_URL', 'http://localhost:3002', requiredInProduction), + changeSomeonesCell: get('CHANGE_SOMEONES_CELL_URL', 'http://localhost:3002', requiredInProduction), + }, domain: get('INGRESS_URL', 'http://localhost:3000', requiredInProduction), sqs: { audit: auditConfig(), diff --git a/server/controllers/imageController.ts b/server/controllers/imageController.ts new file mode 100644 index 0000000..d73a928 --- /dev/null +++ b/server/controllers/imageController.ts @@ -0,0 +1,31 @@ +import { Request, RequestHandler, Response } from 'express' +import { RestClientBuilder } from '../data' +import { PrisonApiClient } from '../data/interfaces/prisonApiClient' + +const placeHolderImage = '/assets/images/prisoner-profile-image.png' + +export default class ImageController { + constructor(private readonly prisonApiClientBuilder: RestClientBuilder) {} + + public prisonerImage: RequestHandler = (req: Request, res: Response) => { + const prisonApiClient = this.prisonApiClientBuilder(req.middleware.clientToken) + const { prisonerNumber } = req.params + const fullSizeImage = req.query.fullSizeImage ? req.query.fullSizeImage === 'true' : true + + if (prisonerNumber === 'placeholder') { + res.redirect(placeHolderImage) + } else { + prisonApiClient + .getPrisonerImage(prisonerNumber, fullSizeImage) + .then(data => { + res.set('Cache-control', 'private, max-age=86400') + res.removeHeader('pragma') + res.type('image/jpeg') + data.pipe(res) + }) + .catch(_error => { + res.redirect(placeHolderImage) + }) + } + } +} diff --git a/server/data/feComponentsClient.ts b/server/data/feComponentsClient.ts new file mode 100644 index 0000000..0e32c02 --- /dev/null +++ b/server/data/feComponentsClient.ts @@ -0,0 +1,51 @@ +import config from '../config' +import RestClient from './restClient' + +export interface Component { + html: string + css: string[] + javascript: string[] +} + +export type AvailableComponent = 'header' | 'footer' + +type CaseLoad = { + caseLoadId: string + description: string + type: string + caseloadFunction: string + currentlyActive: boolean +} + +type Service = { + description: string + heading: string + href: string + id: string +} + +export interface FeComponentsMeta { + activeCaseLoad: CaseLoad + caseLoads: CaseLoad[] + services: Service[] +} + +export interface FeComponentsResponse { + header?: Component + footer?: Component + meta: FeComponentsMeta +} + +export default class FeComponentsClient { + private static restClient(token: string): RestClient { + return new RestClient('HMPPS Components Client', config.apis.frontendComponents, token) + } + + getComponents(components: T, userToken: string): Promise { + return FeComponentsClient.restClient(userToken).get({ + path: `/components`, + query: `component=${components.join('&component=')}`, + headers: { 'x-user-token': userToken }, + }) + } +} diff --git a/server/data/index.ts b/server/data/index.ts index 0eb9f84..72eec3f 100644 --- a/server/data/index.ts +++ b/server/data/index.ts @@ -18,6 +18,7 @@ import config, { ApiConfig } from '../config' import HmppsAuditClient from './hmppsAuditClient' import LocationsInsidePrisonApiRestClient from './locationsInsidePrisonApiClient' import PrisonApiRestClient from './prisonApiRestClient' +import FeComponentsClient from './feComponentsClient' import { PrisonApiClient } from './interfaces/prisonApiClient' import PrisonerSearchRestClient from './prisonerSearchClient' import RestClient, { RestClientBuilder as CreateRestClientBuilder } from './restClient' @@ -41,7 +42,13 @@ export const dataAccess = () => { applicationInfo, hmppsAuthClient, systemToken: (username?: string) => hmppsAuthClient.getSystemClientToken(username), + feComponentsClient: new FeComponentsClient(), hmppsAuditClient: new HmppsAuditClient(config.sqs.audit), + locationsInsidePrisonApiClientBuilder: restClientBuilder( + 'Locations Inside Prison API', + config.apis.locationsInsidePrisonApi, + LocationsInsidePrisonApiRestClient, + ), prisonApiClientBuilder: restClientBuilder( 'Prison API', config.apis.prisonApi, @@ -52,11 +59,6 @@ export const dataAccess = () => { config.apis.prisonerSearchApi, PrisonerSearchRestClient, ), - locationsInsidePrisonApiClientBuilder: restClientBuilder( - 'Locations Inside Prison API', - config.apis.locationsInsidePrisonApi, - LocationsInsidePrisonApiRestClient, - ), } } diff --git a/server/data/prisonApiRestClient.ts b/server/data/prisonApiRestClient.ts index 635d339..7c91849 100644 --- a/server/data/prisonApiRestClient.ts +++ b/server/data/prisonApiRestClient.ts @@ -1,19 +1,19 @@ import * as querystring from 'querystring' import { Readable } from 'stream' import RestClient from './restClient' -import { BedAssignment } from './interfaces/bedAssignment' +import { PrisonApiClient } from './interfaces/prisonApiClient' import { CaseLoad } from './interfaces/caseLoad' -import EstablishmentRollSummary from '../services/interfaces/EstablishmentRollSummary' import { Location } from './interfaces/location' import { Movements } from './interfaces/movements' import { OffenderIn } from './interfaces/offenderIn' -import { OffenderInReception } from './interfaces/offenderInReception' -import { OffenderMovement } from './interfaces/offenderMovement' import { OffenderOut } from './interfaces/offenderOut' +import { OffenderMovement } from './interfaces/offenderMovement' +import { OffenderInReception } from './interfaces/offenderInReception' +import { UserDetail } from './interfaces/userDetail' +import { BedAssignment } from './interfaces/bedAssignment' import { PagedList } from './interfaces/pagedList' import PrisonRollCount from './interfaces/prisonRollCount' -import { PrisonApiClient } from './interfaces/prisonApiClient' -import { UserDetail } from './interfaces/userDetail' +import EstablishmentRollSummary from '../services/interfaces/EstablishmentRollSummary' export default class PrisonApiRestClient implements PrisonApiClient { constructor(private restClient: RestClient) {} diff --git a/server/middleware/getFeComponents.ts b/server/middleware/getFeComponents.ts new file mode 100644 index 0000000..74a950e --- /dev/null +++ b/server/middleware/getFeComponents.ts @@ -0,0 +1,33 @@ +import type { RequestHandler } from 'express' + +import logger from '../../logger' +import { Services } from '../services' +import config from '../config' + +export default function getFrontendComponents({ feComponentsService }: Services): RequestHandler { + return async (_req, res, next) => { + if (!config.apis.frontendComponents.enabled) { + res.locals.feComponents = {} + return next() + } + + try { + const { header, footer, meta } = await feComponentsService.getComponents( + ['header', 'footer'], + res.locals.user.token, + ) + + res.locals.feComponents = { + header: header.html, + footer: footer.html, + cssIncludes: [...header.css, ...footer.css], + jsIncludes: [...header.javascript, ...footer.javascript], + meta, + } + return next() + } catch (error) { + logger.error(error, 'Failed to retrieve front end components') + return next() + } + } +} diff --git a/server/middleware/setUpPageNotFound.ts b/server/middleware/setUpPageNotFound.ts new file mode 100644 index 0000000..13fc914 --- /dev/null +++ b/server/middleware/setUpPageNotFound.ts @@ -0,0 +1,14 @@ +import { Request, Response } from 'express' + +export default (req: Request, res: Response) => { + res.status(404) + res.locals = { + ...res.locals, + user: { + ...res.locals.user, + showFeedbackBanner: false, + }, + hideBackLink: true, + } + res.render('notFound', { url: req.headers.referer || '/' }) +} diff --git a/server/middleware/setUpStaticResources.ts b/server/middleware/setUpStaticResources.ts index a1cc157..b9aa847 100644 --- a/server/middleware/setUpStaticResources.ts +++ b/server/middleware/setUpStaticResources.ts @@ -14,6 +14,9 @@ export default function setUpStaticResources(): Router { const staticResourcesConfig = { maxAge: config.staticResourceCacheDuration, redirect: false } Array.of( + '/assets', + '/assets/stylesheets', + '/assets/js', '/dist/assets', '/node_modules/govuk-frontend/dist/govuk/assets', '/node_modules/govuk-frontend/dist', @@ -23,6 +26,14 @@ export default function setUpStaticResources(): Router { router.use('/assets', express.static(path.join(process.cwd(), dir), staticResourcesConfig)) }) + Array.of('/node_modules/govuk_frontend_toolkit/images').forEach(dir => { + router.use('/assets/images/icons', express.static(path.join(process.cwd(), dir), staticResourcesConfig)) + }) + + Array.of('/node_modules/jquery/dist/jquery.min.js').forEach(dir => { + router.use('/assets/js/jquery.min.js', express.static(path.join(process.cwd(), dir), staticResourcesConfig)) + }) + // Don't cache dynamic resources router.use(noCache()) diff --git a/server/middleware/setUpWebSecurity.ts b/server/middleware/setUpWebSecurity.ts index 0a25aae..333d5c5 100644 --- a/server/middleware/setUpWebSecurity.ts +++ b/server/middleware/setUpWebSecurity.ts @@ -6,6 +6,9 @@ import config from '../config' export default function setUpWebSecurity(): Router { const router = express.Router() + const gaHosts = ['*.googletagmanager.com', '*.google-analytics.com', '*.analytics.google.com'] + + // // Secure code best practice - see: // 1. https://expressjs.com/en/advanced/best-practice-security.html, // 2. https://www.npmjs.com/package/helmet @@ -24,10 +27,13 @@ export default function setUpWebSecurity(): Router { // // This ensures only scripts we trust are loaded, and not anything injected into the // page by an attacker. - scriptSrc: ["'self'", (_req: Request, res: Response) => `'nonce-${res.locals.cspNonce}'`], + scriptSrc: ["'self'", (_req: Request, res: Response) => `'nonce-${res.locals.cspNonce}'`, ...gaHosts], styleSrc: ["'self'", (_req: Request, res: Response) => `'nonce-${res.locals.cspNonce}'`], - fontSrc: ["'self'"], + fontSrc: ["'self'", config.apis.frontendComponents.url], formAction: [`'self' ${config.apis.hmppsAuth.externalUrl}`], + connectSrc: ["'self'", ...gaHosts], + imgSrc: ["'self'", ...gaHosts], + mediaSrc: ["'self'", ...gaHosts], }, }, crossOriginEmbedderPolicy: true, diff --git a/server/routes/index.ts b/server/routes/index.ts index eb2675e..7cfa872 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -2,6 +2,8 @@ import { RequestHandler, Router } from 'express' import { Services } from '../services' import asyncMiddleware from '../middleware/asyncMiddleware' import EstablishmentRollController from '../controllers/establishmentRollController' +import ImageController from '../controllers/imageController' +import { dataAccess } from '../data' export default function establishmentRollRouter(services: Services): Router { const router = Router() @@ -12,28 +14,32 @@ export default function establishmentRollRouter(services: Services): Router { handlers.map(handler => asyncMiddleware(handler)), ) + const { prisonApiClientBuilder } = dataAccess() + const establishmentRollController = new EstablishmentRollController( services.establishmentRollService, services.movementsService, services.locationsService, ) - // TODO: IS /LOCATIONS NEEDED? + const imageController = new ImageController(prisonApiClientBuilder) + get('/', establishmentRollController.getEstablishmentRoll()) get('/locations/', establishmentRollController.getEstablishmentRoll(true)) - // TODO: REMAINING ROUTES - // get( - // ['/wing/:wingId/landing/:landingId', '/wing/:wingId/spur/:spurId/landing/:landingId'], - // establishmentRollController.getEstablishmentRollForLanding(), - // ) - // get('/arrived-today', establishmentRollController.getArrivedToday()) - // get('/out-today', establishmentRollController.getOutToday()) - // get('/en-route', establishmentRollController.getEnRoute()) - // get('/in-reception', establishmentRollController.getInReception()) - // get('/no-cell-allocated', establishmentRollController.getUnallocated()) - // get('/total-currently-out', establishmentRollController.getTotalCurrentlyOut()) - // get('/:livingUnitId/currently-out', establishmentRollController.getCurrentlyOut()) + get( + ['/wing/:wingId/landing/:landingId', '/wing/:wingId/spur/:spurId/landing/:landingId'], + establishmentRollController.getEstablishmentRollForLanding(), + ) + get('/arrived-today', establishmentRollController.getArrivedToday()) + get('/out-today', establishmentRollController.getOutToday()) + get('/en-route', establishmentRollController.getEnRoute()) + get('/in-reception', establishmentRollController.getInReception()) + get('/no-cell-allocated', establishmentRollController.getUnallocated()) + get('/total-currently-out', establishmentRollController.getTotalCurrentlyOut()) + get('/:livingUnitId/currently-out', establishmentRollController.getCurrentlyOut()) + + get('/prisonerImage/:prisonerNumber', imageController.prisonerImage) return router } diff --git a/server/services/feComponentsService.ts b/server/services/feComponentsService.ts new file mode 100644 index 0000000..1ca4d95 --- /dev/null +++ b/server/services/feComponentsService.ts @@ -0,0 +1,34 @@ +import FeComponentsClient, { AvailableComponent } from '../data/feComponentsClient' +import { kebabCase } from '../utils/utils' +import renderMacro from '../utils/renderMacro' + +export default class FeComponentsService { + constructor(private readonly feComponentsClient: FeComponentsClient) {} + + async getComponents(components: T, token: string) { + return this.feComponentsClient.getComponents(components, token) + } + + /** + * Return a filename name for a macro + * @param {string} macroName + * @returns {string} returns naming convention based macro name + */ + private macroNameToFilepath(macroName: string): string { + if (macroName.includes('govuk')) { + return `govuk/components/${kebabCase(macroName.replace(/^\b(govuk)/, ''))}` + } + + if (macroName.includes('moj')) { + return `moj/components/${kebabCase(macroName.replace(/^\b(moj)/, ''))}` + } + + return kebabCase(macroName.replace(/^\b(app)/, '')) + } + + getComponent(macroName: string, params = {}) { + const filename = this.macroNameToFilepath(macroName) + + return renderMacro(`${filename}/macro`, macroName, params) + } +} diff --git a/server/services/index.ts b/server/services/index.ts index ba6739d..5765009 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -1,39 +1,44 @@ import { dataAccess } from '../data' import AuditService from './auditService' -import UserService from './userService' import EstablishmentRollService from './establishmentRollService' -import MovementsService from './movementsService' +import FeComponentsService from './feComponentsService' import LocationService from './locationsService' +import MovementsService from './movementsService' +import UserService from './userService' export const services = () => { const { - prisonApiClientBuilder, - prisonerSearchApiClientBuilder, - locationsInsidePrisonApiClientBuilder, applicationInfo, + feComponentsClient, hmppsAuditClient, + locationsInsidePrisonApiClientBuilder, + prisonApiClientBuilder, + prisonerSearchApiClientBuilder, } = dataAccess() - const userService = new UserService(prisonApiClientBuilder) const auditService = new AuditService(hmppsAuditClient) const establishmentRollService = new EstablishmentRollService( prisonApiClientBuilder, locationsInsidePrisonApiClientBuilder, ) + const feComponentsService = new FeComponentsService(feComponentsClient) + const locationsService = new LocationService(prisonApiClientBuilder, locationsInsidePrisonApiClientBuilder) const movementsService = new MovementsService( prisonApiClientBuilder, prisonerSearchApiClientBuilder, locationsInsidePrisonApiClientBuilder, ) - const locationsService = new LocationService(prisonApiClientBuilder, locationsInsidePrisonApiClientBuilder) + const userService = new UserService(prisonApiClientBuilder) + return { dataAccess, applicationInfo, auditService, - userService, establishmentRollService, - movementsService, + feComponentsService, locationsService, + movementsService, + userService, } } diff --git a/server/services/movementsService.ts b/server/services/movementsService.ts index b234610..382c02d 100644 --- a/server/services/movementsService.ts +++ b/server/services/movementsService.ts @@ -1,13 +1,13 @@ import dpsShared from '@ministryofjustice/hmpps-connect-dps-shared-items' -import { BedAssignment } from '../data/interfaces/bedAssignment' -import { OffenderMovement } from '../data/interfaces/offenderMovement' +import { RestClientBuilder } from '../data' import { PrisonApiClient } from '../data/interfaces/prisonApiClient' -import { Prisoner } from '../data/interfaces/prisoner' import { PrisonerSearchClient } from '../data/interfaces/prisonerSearchClient' import { PrisonerWithAlerts } from './interfaces/PrisonerWithAlerts' -import { RestClientBuilder } from '../data' -import { LocationsInsidePrisonApiClient } from '../data/interfaces/locationsInsidePrisonApiClient' import { stripAgencyPrefix } from '../utils/utils' +import { Prisoner } from '../data/interfaces/prisoner' +import { BedAssignment } from '../data/interfaces/bedAssignment' +import { OffenderMovement } from '../data/interfaces/offenderMovement' +import { LocationsInsidePrisonApiClient } from '../data/interfaces/locationsInsidePrisonApiClient' export default class MovementsService { constructor( diff --git a/server/utils/nunjucksSetup.ts b/server/utils/nunjucksSetup.ts index 342c830..3a8503b 100644 --- a/server/utils/nunjucksSetup.ts +++ b/server/utils/nunjucksSetup.ts @@ -3,7 +3,7 @@ import path from 'path' import nunjucks from 'nunjucks' import express from 'express' import fs from 'fs' -import { initialiseName, prisonerBelongsToUsersCaseLoad, userHasAllRoles, userHasRoles } from './utils' +import { formatName, initialiseName, prisonerBelongsToUsersCaseLoad, userHasAllRoles, userHasRoles } from './utils' import { formatDate, formatDateTime, formatTime, timeFromDate, toUnixTimeStamp } from './dateHelpers' import config from '../config' import logger from '../../logger' @@ -12,7 +12,9 @@ export default function nunjucksSetup(app: express.Express): void { app.set('view engine', 'njk') app.locals.asset_path = '/assets/' - app.locals.applicationName = 'HMPPS Prison Roll Count' + app.locals.applicationName = 'Establishment roll' + app.locals.config = config + app.locals.dpsUrl = config.serviceUrls.digitalPrisons app.locals.environmentName = config.environmentName app.locals.environmentNameColour = config.environmentName === 'PRE-PRODUCTION' ? 'govuk-tag--green' : '' let assetManifest: Record = {} @@ -30,7 +32,11 @@ export default function nunjucksSetup(app: express.Express): void { [ path.join(__dirname, '../../server/views'), 'node_modules/govuk-frontend/dist/', + 'node_modules/govuk-frontend/dist/components/', 'node_modules/@ministryofjustice/frontend/', + 'node_modules/@ministryofjustice/frontend/moj/components/', + 'node_modules/@ministryofjustice/hmpps-connect-dps-components/dist/assets/', + 'node_modules/@ministryofjustice/hmpps-connect-dps-shared-items/dist/assets/', ], { autoescape: true, @@ -42,6 +48,7 @@ export default function nunjucksSetup(app: express.Express): void { njkEnv.addGlobal('userHasAllRoles', userHasAllRoles) njkEnv.addFilter('initialiseName', initialiseName) + njkEnv.addFilter('formatName', formatName) njkEnv.addFilter('assetMap', (url: string) => assetManifest[url] || url) njkEnv.addFilter('formatDate', formatDate) njkEnv.addFilter('formatDateTime', formatDateTime) diff --git a/server/utils/utils.ts b/server/utils/utils.ts index febb466..37625d0 100644 --- a/server/utils/utils.ts +++ b/server/utils/utils.ts @@ -24,6 +24,43 @@ export const initialiseName = (fullName?: string): string | null => { return `${array[0][0]}. ${array.reverse()[0]}` } +/** + * Format a person's name with proper capitalisation + * + * Correctly handles names with apostrophes, hyphens and spaces + * + * Examples, "James O'Reilly", "Jane Smith-Doe", "Robert Henry Jones" + * + * @param firstName - first name + * @param middleNames - middle names as space separated list + * @param lastName - last name + * @param options + * @param options.style - format to use for output name, e.g. `NameStyleFormat.lastCommaFirst` + * @returns formatted name string + */ + +export const formatName = ( + firstName: string, + middleNames: string, + lastName: string, + options?: { style: 'firstMiddleLast' | 'lastCommaFirstMiddle' | 'lastCommaFirst' | 'firstLast' }, +): string => { + const names = [firstName, middleNames, lastName] + if (options?.style === 'lastCommaFirstMiddle') { + names.unshift(`${names.pop()},`) + } else if (options?.style === 'lastCommaFirst') { + names.unshift(`${names.pop()},`) + names.pop() // Remove middleNames + } else if (options?.style === 'firstLast') { + names.splice(1, 1) + } + return names + .filter(s => s) + .map(s => s.trim().toLowerCase()) + .join(' ') + .replace(/(^\w)|([\s'-]+\w)/g, letter => letter.toUpperCase()) +} + /** * Whether or not the prisoner belongs to any of the users case loads * diff --git a/server/views/macros/hmppsPagedListFooter.njk b/server/views/macros/hmppsPagedListFooter.njk new file mode 100644 index 0000000..020677c --- /dev/null +++ b/server/views/macros/hmppsPagedListFooter.njk @@ -0,0 +1,14 @@ +{%- from './hmppsPagination.njk' import hmppsPagination -%} +{%- from './hmppsPaginationSummary.njk' import hmppsPaginationSummary -%} +{% macro hmppsPagedListFooter(listMetadata) %} + {% if listMetadata.pagination.totalElements %} + + {% endif %} +{% endmacro %} diff --git a/server/views/macros/hmppsPagedListHeader.njk b/server/views/macros/hmppsPagedListHeader.njk new file mode 100644 index 0000000..c4a0219 --- /dev/null +++ b/server/views/macros/hmppsPagedListHeader.njk @@ -0,0 +1,17 @@ +{%- from './hmppsPagination.njk' import hmppsPagination -%} +{%- from './hmppsPaginationSummary.njk' import hmppsPaginationSummary -%} +{%- from './hmppsSortSelector.njk' import hmppsSortSelector -%} + +{% macro hmppsPagedListHeader(listMetadata, options = {}) %} +
+
+ {% if listMetadata.sorting %} + {{ hmppsSortSelector(listMetadata.sorting) }} + {% endif %} + {{ hmppsPagination(listMetadata.pagination) }} +
+
+ {{ hmppsPaginationSummary(listMetadata.pagination) }} +
+
+{% endmacro %} \ No newline at end of file diff --git a/server/views/macros/hmppsSortSelector.njk b/server/views/macros/hmppsSortSelector.njk new file mode 100644 index 0000000..9da58f9 --- /dev/null +++ b/server/views/macros/hmppsSortSelector.njk @@ -0,0 +1,36 @@ +{% from "govuk/components/button/macro.njk" import govukButton %} + +{% macro hmppsSortSelector(params) %} +
+
+ + + {% for key, val in params.queryParams %} + {% if val and val is iterable and val is not string %} + {% for i in val %} + + {% endfor %} + {% elseif val %} + + {% endif %} + {% endfor %} + {{ govukButton({ + text: "Sort", + classes: "hmpps-sort-selector__sort-button", + preventDoubleClick: true + }) }} +
+
+ {% block pageScripts %} + + {% endblock %} +{% endmacro %} \ No newline at end of file diff --git a/server/views/macros/printLink.njk b/server/views/macros/printLink.njk index 68b8184..4d88f3c 100644 --- a/server/views/macros/printLink.njk +++ b/server/views/macros/printLink.njk @@ -1,6 +1,6 @@ {% macro printLink(linkText = 'Print this page', align = "left") %} diff --git a/server/views/notFound.njk b/server/views/notFound.njk new file mode 100644 index 0000000..23ddb27 --- /dev/null +++ b/server/views/notFound.njk @@ -0,0 +1,29 @@ +{% extends "./partials/layout.njk" %} +{% set mainClasses = "govuk-main-wrapper--auto-spacing" %} +{% from "govuk/components/button/macro.njk" import govukButton %} + +{% set pageTitle = "Page not found" %} + +{% block content %} +
+
+

{{ pageTitle }}

+

+ If you typed or pasted the web address, check it is correct. +

+

+ If you have clicked to go back, the details you entered have been saved to the service and you cannot return to the last page. +

+

+ You might also be trying to view a page that you do not have access to. +

+ +

+ {{ govukButton({ + text: "Continue", + href: url + }) }} +

+
+
+{% endblock %} \ No newline at end of file diff --git a/server/views/pages/arrivingToday.njk b/server/views/pages/arrivingToday.njk new file mode 100644 index 0000000..a5850ab --- /dev/null +++ b/server/views/pages/arrivingToday.njk @@ -0,0 +1,60 @@ +{% extends "../partials/layout.njk" %} +{% from "../macros/alertFlags.njk" import alertFlags %} +{% from "../macros/categoryFlag.njk" import categoryFlag %} + +{% set pageTitle = "Arrived today" %} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + +{% block content %} +
+
+

{{ pageTitle }}

+
+ +
+
+ + + + + + + + + + + + + + + {% for prisoner in prisoners %} + {% set prisonerName = prisoner.firstName | formatName("", prisoner.lastName, { style: 'lastCommaFirst' }) %} + + + + + + + + + + + {% endfor %} + + +
PictureNamePrison numberDate of birthLocationTime arrivedArrived fromAlert flags
Image of {{ prisonerName }}{{ prisonerName }}{{ prisoner.prisonerNumber }}{{ prisoner.dateOfBirth | formatDate('short')}}{{ prisoner.cellLocation }}{{ prisoner.movementTime | formatTime}}{{ prisoner.arrivedFrom }}{{ alertFlags(prisoner.alertFlags) }}{{categoryFlag("", prisoner.category) | safe}}
+
+
+
+{% endblock %} diff --git a/server/views/pages/currentlyOut.njk b/server/views/pages/currentlyOut.njk new file mode 100644 index 0000000..43537ae --- /dev/null +++ b/server/views/pages/currentlyOut.njk @@ -0,0 +1,60 @@ +{% extends "../partials/layout.njk" %} +{% from "../macros/alertFlags.njk" import alertFlags %} +{% from "../macros/categoryFlag.njk" import categoryFlag %} + +{% set pageTitle = "Currently out - " + locationName if locationName else "Total currently out"%} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + +{% block content %} +
+
+

{{ pageTitle }}

+
+ +
+
+ + + + + + + + + + + + + + + {% for prisoner in prisoners %} + {% set prisonerName = prisoner.firstName | formatName("", prisoner.lastName, { style: 'lastCommaFirst' }) %} + + + + + + + + + + + {% endfor %} + + +
PictureNamePrison numberDate of birthLocationAlert flagsCurrent locationComment
Image of {{ prisonerName }}{{ prisonerName }}{{ prisoner.prisonerNumber }}{{ prisoner.dateOfBirth | formatDate('short')}}{{ prisoner.cellLocation }}{{ alertFlags(prisoner.alertFlags) }}{{categoryFlag("", prisoner.category) | safe}}{{ prisoner.currentLocation }}{{ prisoner.movementComment }}
+
+
+
+{% endblock %} diff --git a/server/views/pages/enRoute.njk b/server/views/pages/enRoute.njk new file mode 100644 index 0000000..bdf1620 --- /dev/null +++ b/server/views/pages/enRoute.njk @@ -0,0 +1,60 @@ +{% extends "../partials/layout.njk" %} +{% from "../macros/alertFlags.njk" import alertFlags %} +{% from "../macros/categoryFlag.njk" import categoryFlag %} + +{% set pageTitle = "En route to " + prison %} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + +{% block content %} +
+
+

{{ pageTitle }}

+
+ +
+
+ + + + + + + + + + + + + + + {% for prisoner in prisoners %} + {% set prisonerName = prisoner.firstName | formatName("", prisoner.lastName, { style: 'lastCommaFirst' }) %} + + + + + + + + + + + {% endfor %} + + +
PictureNamePrison numberDate of birthDepartedEn route fromReasonAlert flags
Image of {{ prisonerName }}{{ prisonerName }}{{ prisoner.prisonerNumber }}{{ prisoner.dateOfBirth | formatDate('short')}}{{ prisoner.movementTime | formatTime }}
{{ prisoner.movementDate | formatDate('short')}}
{{ prisoner.from }}{{ prisoner.reason}}{{ alertFlags(prisoner.alertFlags) }}{{categoryFlag("", prisoner.category) | safe}}
+
+
+
+{% endblock %} diff --git a/server/views/pages/establishmentRoll.njk b/server/views/pages/establishmentRoll.njk index 72830a2..f863cc7 100644 --- a/server/views/pages/establishmentRoll.njk +++ b/server/views/pages/establishmentRoll.njk @@ -2,7 +2,6 @@ {% from "../macros/printLink.njk" import printLink %} {% from "../macros/establishmentRollStat.njk" import establishmentRollStat %} -{% set pageTitle = "Establishment roll" %} {% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} {% set todayStats = establishmentRollCounts.todayStats %} {% set totals = establishmentRollCounts.totals %} @@ -11,7 +10,7 @@ {% set breadCrumbs = [ { text: 'Digital Prison Services', - href: '/' + href: dpsUrl } ] %} @@ -22,7 +21,7 @@ > {% if type === "LANDING" %} - {{ block.localName or block.locationCode }} + {{ block.localName or block.locationCode }} {% else %} {{ block.localName or block.locationCode }} {% endif %} @@ -31,7 +30,7 @@ {{ block.rollCount.currentlyInCell }} {% if block.rollCount.currentlyOut > 0 %} - {{block.rollCount.currentlyOut}} + {{block.rollCount.currentlyOut}} {% else %} 0 {% endif %} {{ block.rollCount.workingCapacity }} @@ -43,7 +42,7 @@ {% block content %}
-

{{ pageTitle }} for {{ date | formatDate('full') }}

+

Establishment roll for {{ date | formatDate('full') }}

{{ printLink(align = "right") }}
@@ -73,7 +72,7 @@ establishmentRollStat( heading = "Arrived today", value = todayStats.inToday, - href = "/establishment-roll/arrived-today", + href = "/arrived-today", qaTag = "in-today" ) }} @@ -84,7 +83,7 @@ establishmentRollStat( heading = "In reception", value = todayStats.unassignedIn, - href = "/establishment-roll/in-reception", + href = "/in-reception", qaTag = "unassigned-in" ) }} @@ -95,7 +94,7 @@ establishmentRollStat( heading = "Still to arrive", value = todayStats.enroute, - href = "/establishment-roll/en-route", + href = "/en-route", qaTag = "enroute" ) }} @@ -107,7 +106,7 @@ establishmentRollStat( heading = "Out today", value = todayStats.outToday, - href = "/establishment-roll/out-today", + href = "/out-today", qaTag = "out-today" ) }} @@ -118,7 +117,7 @@ establishmentRollStat( heading = "No cell allocated", value = todayStats.noCellAllocated, - href = "/establishment-roll/no-cell-allocated", + href = "/no-cell-allocated", qaTag = "no-cell-allocated" ) }} @@ -161,7 +160,7 @@ {{ totals.currentlyInCell }} {% if totals.currentlyOut > 0 %} - {{totals.currentlyOut}} + {{totals.currentlyOut}} {% else %} 0 {% endif %} {{ totals.workingCapacity }} @@ -175,4 +174,4 @@ {% block pageScripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/server/views/pages/establishmentRollLanding.njk b/server/views/pages/establishmentRollLanding.njk new file mode 100644 index 0000000..b4ea48c --- /dev/null +++ b/server/views/pages/establishmentRollLanding.njk @@ -0,0 +1,63 @@ +{% extends "../partials/layout.njk" %} +{% from "../macros/printLink.njk" import printLink %} +{% from "../macros/establishmentRollStat.njk" import establishmentRollStat %} + +{% set pageTitle = wingName + " - " + (spurName + " - " if spurName else "") + landingName %} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} +{% set todayStats = establishmentRollCounts.todayStats %} +{% set capacityLabel = "Working capacity" if useWorkingCapacity else "Operational capacity" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + + +{% block content %} +
+
+

{{ pageTitle }}

+ {{ printLink(align = "right") }} +
+ +
+
+ + + + + + + + + + + + + {% for cell in cellRollCounts %} + + + + + + + + + {% endfor %} + + +
Beds in useCurrently in cellCurrently out{{ capacityLabel }}Net vacancies
{{ cell.localName or cell.locationCode }}{{ cell.rollCount.bedsInUse }}{{ cell.rollCount.currentlyInCell }} + {% if cell.rollCount.currentlyOut > 0 %} + {{cell.rollCount.currentlyOut}} + {% else %} 0 {% endif %} + {{ cell.rollCount.workingCapacity }}{{ cell.rollCount.netVacancies }}
+
+
+
+{% endblock %} diff --git a/server/views/pages/inReception.njk b/server/views/pages/inReception.njk new file mode 100644 index 0000000..dc75b02 --- /dev/null +++ b/server/views/pages/inReception.njk @@ -0,0 +1,58 @@ +{% extends "../partials/layout.njk" %} +{% from "../macros/alertFlags.njk" import alertFlags %} +{% from "../macros/categoryFlag.njk" import categoryFlag %} + +{% set pageTitle = "In reception"%} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + +{% block content %} +
+
+

{{ pageTitle }}

+
+ +
+
+ + + + + + + + + + + + + + {% for prisoner in prisoners %} + {% set prisonerName = prisoner.firstName | formatName("", prisoner.lastName, { style: 'lastCommaFirst' }) %} + + + + + + + + + + {% endfor %} + + +
PictureNamePrison numberDate of birthTime arrivedArrived fromAlert flags
Image of {{ prisonerName }}{{ prisonerName }}{{ prisoner.prisonerNumber }}{{ prisoner.dateOfBirth | formatDate('short')}}{{ prisoner.timeArrived | formatTime }}{{ prisoner.from }}{{ alertFlags(prisoner.alertFlags) }}{{categoryFlag("", prisoner.category) | safe}}
+
+
+
+{% endblock %} diff --git a/server/views/pages/noCellAllocated.njk b/server/views/pages/noCellAllocated.njk new file mode 100644 index 0000000..5423f33 --- /dev/null +++ b/server/views/pages/noCellAllocated.njk @@ -0,0 +1,73 @@ +{% extends "../partials/layout.njk" %} + +{% set pageTitle = "No cell allocated"%} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + +{% block content %} +
+ {% if prisoners.length %} +
+
+

{{ pageTitle }}

+

These people have been moved out of their cell to create a space for someone else and do not currently have a cell allocated.

+
+
+ +
+
+ + + + + + + + + + {% if userCanAllocateCell %} + + {% endif %} + + + + {% for prisoner in prisoners %} + {% set prisonerName = prisoner.firstName | formatName("", prisoner.lastName, { style: 'lastCommaFirst' }) %} + + + + + + + + {% if userCanAllocateCell %} + + {% endif %} + + {% endfor %} + +
PictureNamePrisoner numberPrevious cellTime moved outMoved out byAllocate
Image of {{ prisonerName }}{{ prisonerName }}{{ prisoner.prisonerNumber }}{{ prisoner.previousCell }}{{ prisoner.timeOut | timeFromDate }}{{ prisoner.movedBy }} + Allocate cell +
+
+
+ {% else %} +
+
+

{{ pageTitle }}

+

There are no prisoners without a cell.

+
+
+ {% endif %} +
+{% endblock %} diff --git a/server/views/pages/outToday.njk b/server/views/pages/outToday.njk new file mode 100644 index 0000000..306fdbc --- /dev/null +++ b/server/views/pages/outToday.njk @@ -0,0 +1,58 @@ +{% extends "../partials/layout.njk" %} +{% from "../macros/alertFlags.njk" import alertFlags %} +{% from "../macros/categoryFlag.njk" import categoryFlag %} + +{% set pageTitle = "Out today" %} +{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} + +{% set breadCrumbs = [ + { + text: 'Digital Prison Services', + href: dpsUrl + }, + { + text: 'Establishment roll', + href: '/' + } +] %} + +{% block content %} +
+
+

{{ pageTitle }}

+
+ +
+
+ + + + + + + + + + + + + + {% for prisoner in prisoners %} + {% set prisonerName = prisoner.firstName | formatName("", prisoner.lastName, { style: 'lastCommaFirst' }) %} + + + + + + + + + + {% endfor %} + + +
PictureNamePrison numberDate of birthTime outReasonAlert flags
Image of {{ prisonerName }}{{ prisonerName }}{{ prisoner.prisonerNumber }}{{ prisoner.dateOfBirth | formatDate('short')}}{{ prisoner.timeOut | formatTime }}{{ prisoner.reasonDescription}}{{ alertFlags(prisoner.alertFlags) }}{{categoryFlag("", prisoner.category) | safe}}
+
+
+
+{% endblock %} diff --git a/server/views/partials/layout.njk b/server/views/partials/layout.njk index 65779e3..601fefe 100644 --- a/server/views/partials/layout.njk +++ b/server/views/partials/layout.njk @@ -1,18 +1,62 @@ +{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %} {% extends "govuk/template.njk" %} {% block head %} + + + + + + + {% if feComponents.jsIncludes %} + {% for js in feComponents.jsIncludes %} + + {% endfor %} + {% endif %} + + {% if feComponents.cssIncludes %} + {% for css in feComponents.cssIncludes %} + + {% endfor %} + {% endif %} + + {% endblock %} -{% block pageTitle %}{{pageTitle | default(applicationName)}}{% endblock %} +{% block pageTitle %}{{ pageTitle + ' - ' + applicationName if pageTitle else applicationName }}{% endblock %} {% block header %} - {% include "./header.njk" %} + {% if feComponents.header %} + {{ feComponents.header | safe }} + {% else %} + {% include "./header.njk" %} + {% endif %} +{% endblock %} + +{% block beforeContent %} + {{ govukBreadcrumbs({ + items: breadCrumbs | default([]), + classes: 'govuk-!-display-none-print' + }) }} {% endblock %} {% block bodyStart %} {% endblock %} {% block bodyEnd %} - + + + + + + {% block pageScripts %} + {% endblock %} +{% endblock %} + +{% block footer %} + {{ feComponents.footer | safe }} {% endblock %}