Skip to content

Commit

Permalink
CDPS-716 add no cell prisoners page
Browse files Browse the repository at this point in the history
  • Loading branch information
whitfield-mj committed May 24, 2024
1 parent b0a12b8 commit d3ae115
Show file tree
Hide file tree
Showing 30 changed files with 581 additions and 5 deletions.
1 change: 1 addition & 0 deletions helm_deploy/values-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ generic-service:
WELCOME_PEOPLE_INTO_PRISON_URL: https://welcome-dev.prison.service.justice.gov.uk
ACCREDITED_PROGRAMMES_URL: https://accredited-programmes-dev.hmpps.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


# Feature flags
Expand Down
1 change: 1 addition & 0 deletions helm_deploy/values-preprod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ generic-service:
WELCOME_PEOPLE_INTO_PRISON_URL: https://welcome-preprod.prison.service.justice.gov.uk
ACCREDITED_PROGRAMMES_URL: https://accredited-programmes-preprod.hmpps.service.justice.gov.uk
PRISONER_PROFILE_URL: https://prisoner-preprod.digital.prison.service.justice.gov.uk
CHANGE_SOMEONES_CELL_URL: https://change-someones-cell-preprod.prison.service.justice.gov.uk

# Feature flags
USE_OF_FORCE_PRISONS: "ACI,AGI,ASI,AYI,BAI,BCI,BFI,BHI,BLI,BMI,BNI,BRI,BSI,BWI,BXI,BZI,CDI,CFI,CLI,CWI,DAI,DGI,DHI,DMI,DNI,DTI,DWI,EEI,EHI,ESI,EWI,EXI,EYI,FBI,FDI,FEI,FHI,FKI,FMI,FNI,FSI,FWI,GHI,GMI,GNI,GTI,HBI,HCI,HDI,HEI,HHI,HII,HLI,HMI,HOI,HPI,HVI,ISI,IWI,KMI,KVI,LCI,LEI,LFI,LGI,LHI,LII,LLI,LNI,LPI,LTI,LWI,LYI,MDI,MHI,MRI,MSI,MTI,NHI,NLI,NMI,NSI,NWI,ONI,OWI,PBI,PDI,PFI,PNI,PRI,PVI,RCI,RHI,RNI,RSI,SDI,SFI,SHI,SKI,SLI,SNI,SPI,STI,SUI,SWI,TCI,TSI,UKI,UPI,VEI,WCI,WDI,WEI,WHI,WII,WLI,WMI,WRI,WSI,WTI,WWI"
Expand Down
1 change: 1 addition & 0 deletions helm_deploy/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ generic-service:
WELCOME_PEOPLE_INTO_PRISON_URL: https://welcome.prison.service.justice.gov.uk
ACCREDITED_PROGRAMMES_URL: https://accredited-programmes.hmpps.service.justice.gov.uk
PRISONER_PROFILE_URL: https://prisoner.digital.prison.service.justice.gov.uk
CHANGE_SOMEONES_CELL_URL: https://change-someones-cell.prison.service.justice.gov.uk

# Feature flags
ACTIVITIES_ENABLED_PRISONS: "RSI,LPI"
Expand Down
58 changes: 58 additions & 0 deletions integration_tests/e2e/noCellAllocated.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Page from '../pages/page'
import { Role } from '../../server/enums/role'
import NoCellAllocatedPage from '../pages/NoCellAllocated'
import { prisonerSearchMock } from '../../server/test/mocks/prisonerSearchMock'

function visitPageWithRoles(roles: string[]) {
cy.setupUserAuth({
roles,
caseLoads: [
{ caseloadFunction: '', caseLoadId: 'MDI', currentlyActive: true, description: 'Leeds (HMP)', type: '' },
],
})
cy.signIn({ redirectPath: '/establishment-roll/no-cell-allocated' })
cy.visit('/establishment-roll/no-cell-allocated')
}
context('In reception Page', () => {
beforeEach(() => {
cy.task('reset')
cy.task('stubPostAttributeSearch')
cy.task('stubGetOffenderCellHistory', prisonerSearchMock[0].bookingId)
cy.task('stubGetOffenderCellHistory', prisonerSearchMock[1].bookingId)
cy.task('getUserDetailsList')
})

it('Page is visible', () => {
visitPageWithRoles([`ROLE_PRISON`, `ROLE_${Role.GlobalSearch}`])

Page.verifyOnPage(NoCellAllocatedPage)
})

it('should display a table row for each wing level assignedRollCount', () => {
visitPageWithRoles([`ROLE_PRISON`, `ROLE_${Role.GlobalSearch}`])

const page = Page.verifyOnPage(NoCellAllocatedPage)
page.inReceptionRows().should('have.length', 2)

page.inReceptionRows().first().find('td').eq(1).should('contain.text', 'Shannon, Eddie')
page.inReceptionRows().first().find('td').eq(2).should('contain.text', 'A1234AB')
page.inReceptionRows().first().find('td').eq(3).should('contain.text', '1-1-2')
page.inReceptionRows().first().find('td').eq(4).should('contain.text', '00:00')
page.inReceptionRows().first().find('td').eq(5).should('contain.text', 'Edwin Shannon')
})

it('should display allocation link if user has cell move', () => {
visitPageWithRoles([`ROLE_PRISON`, `ROLE_${Role.GlobalSearch}`, `ROLE_${Role.CellMove}`])

const page = Page.verifyOnPage(NoCellAllocatedPage)
page.inReceptionRows().should('have.length', 2)

page
.inReceptionRows()
.first()
.find('td')
.eq(6)
.find('a[href="http://localhost:3002/prisoner/A1234AB/cell-move/search-for-cell"]')
.should('contain.text', 'Allocate cell')
})
})
35 changes: 35 additions & 0 deletions integration_tests/mockApis/prison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { movementsOutMock } from '../../server/test/mocks/movementsOutMock'
import { movementsEnRouteMock } from '../../server/test/mocks/movementsEnRouteMock'
import { movementsInReceptionMock } from '../../server/test/mocks/movementsInReceptionMock'
import { movementsRecentMock } from '../../server/test/mocks/movementsRecentMock'
import { offenderCellHistoryMock } from '../../server/test/mocks/offenderCellHistoryMock'
import { userDetailsMock } from '../../server/test/mocks/userDetailsMock'
import { pagedListMock } from '../../server/test/mocks/pagedListMock'

export default {
stubUserCaseLoads: (caseLoads: CaseLoad[] = []) => {
Expand Down Expand Up @@ -269,4 +272,36 @@ export default {
},
})
},

stubGetOffenderCellHistory: (bookingId = 123) => {
return stubFor({
request: {
method: 'GET',
url: `/prison/api/bookings/${bookingId}/cell-history?page=0&size=2000`,
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: pagedListMock(offenderCellHistoryMock),
},
})
},

getUserDetailsList: () => {
return stubFor({
request: {
method: 'POST',
url: '/prison/api/users/list',
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: userDetailsMock,
},
})
},
}
19 changes: 18 additions & 1 deletion integration_tests/mockApis/prisonerSearch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { stubFor } from './wiremock'
import { prisonerSearchMock } from '../../server/test/mocks/prisonerSearchMock'
import { pagedListMock } from '../../server/test/mocks/pagedListMock'

export default {
stubPostSearchPrisonersById: () => {
return stubFor({
request: {
method: 'POST',
urlPattern: '/prisoner-search/prisoner-search/prisoner-numbers',
url: '/prisoner-search/prisoner-search/prisoner-numbers',
},
response: {
status: 200,
Expand All @@ -17,4 +18,20 @@ export default {
},
})
},

stubPostAttributeSearch: () => {
return stubFor({
request: {
method: 'POST',
url: '/prisoner-search/attribute-search?size=2000',
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: pagedListMock(prisonerSearchMock),
},
})
},
}
9 changes: 9 additions & 0 deletions integration_tests/pages/NoCellAllocated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Page, { PageElement } from './page'

export default class NoCellAllocatedPage extends Page {
constructor() {
super('No cell allocated')
}

inReceptionRows = (): PageElement => cy.get('table.unallocated-roll__table tbody tr')
}
1 change: 0 additions & 1 deletion integration_tests/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ Cypress.Commands.add(
cy.task('stubSignIn', options)
cy.task('stubUserCaseLoads', options.caseLoads)
cy.task('stubUserLocations', options.locations)
cy.task('stubGetStaffRoles')
},
)
1 change: 1 addition & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export default {
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),
todayCacheTTL: Number(get('TODAY_CACHE_TTL', 0, requiredInProduction)),
Expand Down
19 changes: 19 additions & 0 deletions server/controllers/establishmentRollController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Request, RequestHandler, Response } from 'express'
import EstablishmentRollService from '../services/establishmentRollService'
import MovementsService from '../services/movementsService'
import { userHasRoles } from '../utils/utils'
import { Role } from '../enums/role'

export default class EstablishmentRollController {
constructor(
Expand Down Expand Up @@ -82,4 +84,21 @@ export default class EstablishmentRollController {
res.render('pages/inReception', { prisoners: prisonersEnRoute, prison: user.activeCaseLoad.description })
}
}

public getUnallocated(): RequestHandler {
return async (req: Request, res: Response) => {
const { user } = res.locals
const { clientToken } = req.middleware

const unallocatedPrisoners = await this.movementsService.getNoCellAllocatedPrisoners(
clientToken,
user.activeCaseLoadId,
)

res.render('pages/noCellAllocated', {
prisoners: unallocatedPrisoners,
userCanAllocateCell: userHasRoles([Role.CellMove], user.userRoles),
})
}
}
}
13 changes: 13 additions & 0 deletions server/data/interfaces/bedAssignment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface BedAssignment {
bookingId: number
livingUnitId: number
assignmentDate: string
assignmentReason: string
assignmentEndDate: string
assignmentEndDateTime: string
agencyId: string
description: string
bedAssignmentHistorySequence: number
movementMadeBy: string
offenderNo: string
}
28 changes: 28 additions & 0 deletions server/data/interfaces/pagedList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface PagedList<T> {
content: T[]
pageable?: {
sort: {
empty: boolean
sorted: boolean
unsorted: boolean
}
offset: number
pageSize: number
pageNumber: number
paged: boolean
unpaged: boolean
}
totalPages: number
last: boolean
totalElements: number
size: number
number: number
sort: {
empty: boolean
sorted: boolean
unsorted: boolean
}
first: boolean
numberOfElements: number
empty: boolean
}
5 changes: 5 additions & 0 deletions server/data/interfaces/prisonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { OffenderIn } from './offenderIn'
import { OffenderOut } from './offenderOut'
import { OffenderMovement } from './offenderMovement'
import { OffenderInReception } from './offenderInReception'
import { PagedList } from './pagedList'
import { BedAssignment } from './bedAssignment'
import { UserDetail } from './userDetail'

export interface PrisonApiClient {
getUserCaseLoads(): Promise<CaseLoad[]>
Expand All @@ -30,4 +33,6 @@ export interface PrisonApiClient {
getStaffRoles(staffId: number, agencyId: string): Promise<StaffRole[]>
setActiveCaseload(caseLoad: CaseLoad): Promise<Record<string, string>>
getPrisonerImage(offenderNumber: string, fullSizeImage: boolean): Promise<Readable>
getOffenderCellHistory(bookingId: number, params?: { page: number; size: number }): Promise<PagedList<BedAssignment>>
getUserDetailsList(usernames: string[]): Promise<UserDetail[]>
}
2 changes: 2 additions & 0 deletions server/data/interfaces/prisonerSearchClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Prisoner } from './prisoner'
import { PagedList } from './pagedList'

export interface PrisonerSearchClient {
getPrisonersById(prisonerNumbers: string[]): Promise<Prisoner[]>
getCswapPrisonersInEstablishment(prisonId: string): Promise<PagedList<Prisoner>>
}
14 changes: 14 additions & 0 deletions server/data/interfaces/userDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface UserDetail {
staffId: number
username: string
firstName: string
lastName: string
thumbnailId?: number
activeCaseLoadId?: string
accountStatus: 'ACTIVE' | 'INACT' | 'SUS' | 'CAREER' | 'MAT' | 'SAB' | 'SICK'
lockDate: string
expiryDate?: string
lockedFlag?: boolean
expiredFlag?: boolean
active: boolean
}
19 changes: 18 additions & 1 deletion server/data/prisonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { OffenderIn } from './interfaces/offenderIn'
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'

export default class PrisonApiRestClient implements PrisonApiClient {
constructor(private restClient: RestClient) {}
Expand Down Expand Up @@ -102,9 +105,23 @@ export default class PrisonApiRestClient implements PrisonApiClient {
return this.put<Record<string, string>>({ path: '/api/users/me/activeCaseLoad', data: caseLoad })
}

async getPrisonerImage(prisonerNumber: string, fullSizeImage: boolean): Promise<Readable> {
getPrisonerImage(prisonerNumber: string, fullSizeImage: boolean): Promise<Readable> {
return this.restClient.stream({
path: `/api/bookings/offenderNo/${prisonerNumber}/image/data?fullSizeImage=${fullSizeImage}`,
})
}

getOffenderCellHistory(
bookingId: number,
pagedParams: { page: number; size: number } = { page: 0, size: 2000 },
): Promise<PagedList<BedAssignment>> {
return this.get<PagedList<BedAssignment>>({
path: `/api/bookings/${bookingId}/cell-history`,
query: querystring.stringify(pagedParams),
})
}

getUserDetailsList(usernames: string[]): Promise<UserDetail[]> {
return this.post<UserDetail[]>({ path: '/api/users/list', data: usernames })
}
}
30 changes: 30 additions & 0 deletions server/data/prisonerSearchClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RestClient from './restClient'
import { Prisoner } from './interfaces/prisoner'
import { PrisonerSearchClient } from './interfaces/prisonerSearchClient'
import { PagedList } from './interfaces/pagedList'

export default class PrisonerSearchRestClient implements PrisonerSearchClient {
constructor(private restClient: RestClient) {}
Expand All @@ -11,4 +12,33 @@ export default class PrisonerSearchRestClient implements PrisonerSearchClient {
data: { prisonerNumbers },
})
}

async getCswapPrisonersInEstablishment(prisonId: string): Promise<PagedList<Prisoner>> {
const attributeRequest = {
joinType: 'AND',
queries: [
{
joinType: 'AND',
matchers: [
{
type: 'String',
attribute: 'prisonId',
condition: 'IS',
searchTerm: prisonId,
},
{
type: 'String',
attribute: 'cellLocation',
condition: 'IN',
searchTerm: 'CSWAP',
},
],
},
],
}
return this.restClient.post<PagedList<Prisoner>>({
path: '/attribute-search?size=2000',
data: attributeRequest,
})
}
}
1 change: 1 addition & 0 deletions server/routes/establishmentRollRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function establishmentRollRouter(services: Services): Router {
get('/out-today', establishmentRollController.getOutToday())
get('/en-route', establishmentRollController.getEnRoute())
get('/in-reception', establishmentRollController.getInReception())
get('/no-cell-allocated', establishmentRollController.getUnallocated())

return router
}
Loading

0 comments on commit d3ae115

Please sign in to comment.