Skip to content

Commit

Permalink
CDPS-1085: Create dietary requirements page (#185)
Browse files Browse the repository at this point in the history
* CDPS-1085: Create dietary requirements page

* CDPS-1085: Add role check to dietary requirements page

* CDPS-1085: Linting

* CDPS-1085: Fix integration tests

* CDPS-1085: Print page

* CDPS-1085: Print page role protection

* CDPS-1085: Update layout
  • Loading branch information
danielburnley authored Jan 22, 2025
1 parent 48dcff2 commit 388fd63
Show file tree
Hide file tree
Showing 22 changed files with 1,017 additions and 2 deletions.
2 changes: 2 additions & 0 deletions assets/scss/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ $govuk-page-width: $moj-page-width;
@import './components/print-link';
@import 'components/alert-flags';
@import './components/results-table';
@import './components/pagination';
@import './components/sortable-table';
@import './pages/help';
@import './pages/homepage';
@import './pages/managed-page';
Expand Down
52 changes: 52 additions & 0 deletions assets/scss/components/_pagination.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.hmpps-pagination-view-all {
text-align: right;
margin: 15px 0 7px 0;
}

.moj-pagination__results {
padding: 5px 0;
}

.hmpps-paged-list-header {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
padding-bottom: 30px;
margin-bottom: 30px;

.moj-pagination {
margin-top: 30px;
margin-left: 0;
margin-right: 0;
}

p.moj-pagination__results {
margin-left: -5px;
}
}

.hmpps-paged-list-footer {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 30px;

&__no-pagination {
justify-content: flex-end;
}

.moj-pagination {
margin-left: 0;
margin-right: 0;
}

p.moj-pagination__results {
margin-left: -5px;
}
}

.hmpps-paged-list-header__with-divider {
border-bottom: 1px solid govuk-colour('dark-grey');
}
2 changes: 1 addition & 1 deletion assets/scss/components/_print-link.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.print-link {
.hmpps-print-link {
display: inline-block;
margin: 0 0 15px -10px;
position: relative;
Expand Down
59 changes: 59 additions & 0 deletions assets/scss/components/_sortable-table.scss
Original file line number Diff line number Diff line change
@@ -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;
}
55 changes: 55 additions & 0 deletions integration_tests/e2e/dieteryRequirements.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Role } from '../../server/enums/role'
import DietaryRequirementsPage from '../pages/dietaryRequirements'
import Page from '../pages/page'

context('Currently Out Page', () => {
beforeEach(() => {
cy.task('reset')
cy.setupUserAuth({ roles: [`ROLE_PRISON`, `ROLE_${Role.GlobalSearch}`, `ROLE_${Role.DpsApplicationDeveloper}`] })
cy.setupComponentsData()
})

it('Page is visible for mock data', () => {
cy.signIn({ redirectPath: `/dietary-requirements/LEI` })
cy.visit(`/dietary-requirements/LEI`)
Page.verifyOnPage(DietaryRequirementsPage)
})

it('Displays the prisoner information', () => {
cy.signIn({ redirectPath: `/dietary-requirements/LEI` })
cy.visit(`/dietary-requirements/LEI`)
const page = Page.verifyOnPage(DietaryRequirementsPage)
page.dietaryRequirements().row(0).nameAndPrisonNumber().should('include.text', 'Richard Smith')
page.dietaryRequirements().row(0).nameAndPrisonNumber().should('include.text', 'G4879UP')
page.dietaryRequirements().row(0).location().should('include.text', 'C-3-010')
page.dietaryRequirements().row(0).dietaryRequirements().medical().should('include.text', 'Nutrient Deficiency')
page.dietaryRequirements().row(0).dietaryRequirements().foodAllergies().should('include.text', 'Egg')
page.dietaryRequirements().row(2).dietaryRequirements().personal().should('include.text', 'Kosher')
})

context('Sorting', () => {
it('Defaults to no sorting', () => {
cy.signIn({ redirectPath: `/dietary-requirements/LEI` })
cy.visit(`/dietary-requirements/LEI`)
const page = Page.verifyOnPage(DietaryRequirementsPage)
page.dietaryRequirements().sorting().nameAndNumber().should('have.attr', 'aria-sort', 'none')
page.dietaryRequirements().sorting().location().should('have.attr', 'aria-sort', 'none')
})

it('Allows sorting', () => {
cy.signIn({ redirectPath: `/dietary-requirements/LEI` })
cy.visit(`/dietary-requirements/LEI`)
const page = Page.verifyOnPage(DietaryRequirementsPage)
page.dietaryRequirements().sorting().nameAndNumber().click()
page.dietaryRequirements().sorting().nameAndNumber().should('have.attr', 'aria-sort', 'ascending')
page.dietaryRequirements().sorting().location().should('have.attr', 'aria-sort', 'none')
page.dietaryRequirements().sorting().nameAndNumber().click()
page.dietaryRequirements().sorting().nameAndNumber().should('have.attr', 'aria-sort', 'descending')
page.dietaryRequirements().sorting().location().click()
page.dietaryRequirements().sorting().nameAndNumber().should('have.attr', 'aria-sort', 'none')
page.dietaryRequirements().sorting().location().should('have.attr', 'aria-sort', 'ascending')
page.dietaryRequirements().sorting().location().click()
page.dietaryRequirements().sorting().location().should('have.attr', 'aria-sort', 'descending')
})
})
})
23 changes: 23 additions & 0 deletions integration_tests/pages/dietaryRequirements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Page from './page'

export default class DietaryRequirementsPage extends Page {
constructor() {
super(`People with dietary requirements in Prison Name`)
}

dietaryRequirements = () => ({
row: (i: number) => ({
nameAndPrisonNumber: () => cy.get('table tbody tr').eq(i).find('td').eq(0),
location: () => cy.get('table tbody tr').eq(i).find('td').eq(1),
dietaryRequirements: () => ({
medical: () => cy.get('table tbody tr').eq(i).find('td').eq(2).find('[data-qa="medical-requirements"]'),
foodAllergies: () => cy.get('table tbody tr').eq(i).find('td').eq(2).find('[data-qa="food-allergies"]'),
personal: () => cy.get('table tbody tr').eq(i).find('td').eq(2).find('[data-qa="personal-requirements"]'),
}),
}),
sorting: () => ({
nameAndNumber: () => cy.get('table thead th').eq(0),
location: () => cy.get('table thead th').eq(1),
}),
})
}
152 changes: 152 additions & 0 deletions server/controllers/dietaryRequirementsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Request, RequestHandler, Response } from 'express'
import { format } from 'date-fns'
import { DietaryRequirementsQueryParams, generateListMetadata, mapToQueryString } from '../utils/generateListMetadata'
import { userHasRoles } from '../utils/utils'
import { Role } from '../enums/role'

export default class DietaryRequirementsController {
private content = [
{
name: 'Richard Smith',
prisonerNumber: 'G4879UP',
location: 'C-3-010',
dietaryRequirements: {
medical: ['Coeliac (cannot eat gluten)', 'Nutrient Deficiency', 'Other: has to eat a low copper diet'],
foodAllergies: ['Egg', 'Other: broccoli allergy'],
personal: [] as string[],
},
},
{
name: 'George Harrison',
prisonerNumber: 'G6333VK',
location: 'B-1-042',
dietaryRequirements: {
medical: [],
foodAllergies: ['Sesame'],
personal: [],
},
},
{
name: 'Harry Thompson',
prisonerNumber: 'G3101UO',
location: 'F-5-031',
dietaryRequirements: {
medical: [],
foodAllergies: [],
personal: ['Kosher'],
},
},
]

public get(): RequestHandler {
return async (req: Request, res: Response) => {
if (!userHasRoles([Role.DpsApplicationDeveloper], res.locals.user.userRoles)) {
return res.render('notFound', { url: '/' })
}

const queryParams: DietaryRequirementsQueryParams = {}
if (req.query.page) queryParams.page = +req.query.page
if (req.query.showAll) queryParams.showAll = Boolean(req.query.showAll)
if (req.query.nameAndNumber) queryParams.nameAndNumber = req.query.nameAndNumber as string
if (req.query.location) queryParams.location = req.query.location as string

const sortNameQuery = () => {
let direction = 'ASC'

if (queryParams.nameAndNumber && queryParams.nameAndNumber === 'ASC') {
direction = 'DESC'
}

return mapToQueryString({ ...queryParams, nameAndNumber: direction, location: null })
}

const sortLocationQuery = () => {
let direction = 'ASC'

if (queryParams.location && queryParams.location === 'ASC') {
direction = 'DESC'
}

return mapToQueryString({ ...queryParams, location: direction, nameAndNumber: null })
}

const sortParamToDirection = (param: string) => {
switch (param) {
case 'ASC':
return 'ascending'
case 'DESC':
return 'descending'
default:
return 'none'
}
}

const sorting = {
nameAndNumber: {
direction: sortParamToDirection(queryParams.nameAndNumber as string),
url: `/dietary-requirements/${req.params.locationId}?${sortNameQuery()}`,
},
location: {
direction: sortParamToDirection(queryParams.location as string),
url: `/dietary-requirements/${req.params.locationId}?${sortLocationQuery()}`,
},
}

// Remove page as this comes from the API
delete queryParams.page

const listMetadata = generateListMetadata(
{
content: this.content,
totalElements: 200,
last: false,
totalPages: 10,
size: 10,
number: 0,
sort: { empty: false, sorted: false, unsorted: true },
first: false,
numberOfElements: 20,
empty: false,
pageable: {
pageNumber: 5,
pageSize: 20,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
offset: 0,
unpaged: false,
paged: true,
},
},
queryParams,
'result',
[],
'',
true,
)

return res.render('pages/dietaryRequirements', {
content: this.content,
listMetadata,
sorting,
locationId: req.params.locationId,
})
}
}

public printAll(): RequestHandler {
return async (req: Request, res: Response) => {
if (!userHasRoles([Role.DpsApplicationDeveloper], res.locals.user.userRoles)) {
return res.render('notFound', { url: '/' })
}

return res.render('pages/printDietaryRequirements', {
date: format(new Date(), 'cccc c MMMM yyyy'),
content: this.content,
locationId: req.params.locationId,
})
}
}
}
1 change: 1 addition & 0 deletions server/enums/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export enum Role {
ReceptionUser = 'PRISON_RECEPTION',
CellMove = 'CELL_MOVE',
KeyWorker = 'KW',
DpsApplicationDeveloper = 'DPS_APPLICATION_DEVELOPER',
}
21 changes: 21 additions & 0 deletions server/routes/dietaryRequirementsRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { RequestHandler, Router } from 'express'
import { Services } from '../services'
import asyncMiddleware from '../middleware/asyncMiddleware'
import DietaryRequirementsController from '../controllers/dietaryRequirementsController'

export default function dietaryRequirementsRouter(_services: Services): Router {
const router = Router()

const get = (path: string | string[], ...handlers: RequestHandler[]) =>
router.get(
path,
handlers.map(handler => asyncMiddleware(handler)),
)

const dietaryRequirementsController = new DietaryRequirementsController()

get('/:locationId', dietaryRequirementsController.get())
get('/:locationId/print-all', dietaryRequirementsController.printAll())

return router
}
Loading

0 comments on commit 388fd63

Please sign in to comment.