Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CDPS-766 add total currently out page #141

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions integration_tests/e2e/TotalOut.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Page from '../pages/page'
import { Role } from '../../server/enums/role'
import TotalOutPage from '../pages/totalOut'

context('Currently Out Page', () => {
beforeEach(() => {
cy.task('reset')
cy.setupUserAuth({
roles: [`ROLE_PRISON`, `ROLE_${Role.GlobalSearch}`],
caseLoads: [
{ caseloadFunction: '', caseLoadId: 'LEI', currentlyActive: true, description: 'Leeds (HMP)', type: '' },
],
})
cy.task('stubOutTotal')
cy.task('stubPostSearchPrisonersById')
cy.task('stubRecentMovements')
cy.task('stubGetLocation')
cy.signIn({ redirectPath: '/establishment-roll/total-currently-out' })
cy.visit('/establishment-roll/total-currently-out')
})

it('Page is visible', () => {
Page.verifyOnPage(TotalOutPage)
})

it('should display a table row for each prisoner en-route', () => {
const page = Page.verifyOnPage(TotalOutPage)
page.currentlyOutRows().should('have.length', 2)

page.currentlyOutRows().first().find('td').eq(1).should('contain.text', 'Shannon, Eddie')
page.currentlyOutRows().first().find('td').eq(2).should('contain.text', 'A1234AB')
page.currentlyOutRows().first().find('td').eq(3).should('contain.text', '01/01/1980')
page.currentlyOutRows().first().find('td').eq(4).should('contain.text', '1-1-1')
page.currentlyOutRows().first().find('td').eq(5).should('contain.text', '')
page.currentlyOutRows().first().find('td').eq(6).should('contain.text', 'Sheffield')
page.currentlyOutRows().first().find('td').eq(7).should('contain.text', 'Some Sheffield comment')
})

it('should display alerts and category if cat A', () => {
const page = Page.verifyOnPage(TotalOutPage)
page.currentlyOutRows().should('have.length', 2)

page.currentlyOutRows().eq(1).find('td').eq(5).should('contain.text', 'Hidden disability')
page.currentlyOutRows().eq(1).find('td').eq(5).should('contain.text', 'CAT A')
})
})
2 changes: 1 addition & 1 deletion integration_tests/e2e/currentlyOut.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Page from '../pages/page'
import { Role } from '../../server/enums/role'
import CurrentlyOutPage from '../pages/currentlyOut'

context('En Route Page', () => {
context('Currently Out Page', () => {
beforeEach(() => {
cy.task('reset')
cy.setupUserAuth({
Expand Down
16 changes: 16 additions & 0 deletions integration_tests/mockApis/prison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ export default {
})
},

stubOutTotal: (prisonCode = 'LEI') => {
return stubFor({
request: {
method: 'GET',
urlPattern: `/prison/api/movements/agency/${prisonCode}/currently-out`,
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: movementsOutMock,
},
})
},

stubMovementsInReception: (prisonCode = 'LEI') => {
return stubFor({
request: {
Expand Down
9 changes: 9 additions & 0 deletions integration_tests/pages/totalOut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Page, { PageElement } from './page'

export default class TotalOutPage extends Page {
constructor() {
super('Total currently out')
}

currentlyOutRows = (): PageElement => cy.get('table.currently-out-roll__table tbody tr')
}
17 changes: 17 additions & 0 deletions server/controllers/establishmentRollController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,21 @@ export default class EstablishmentRollController {
})
}
}

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

const prisonersCurrentlyOut = await this.movementsService.getOffendersCurrentlyOutTotal(
clientToken,
user.activeCaseLoadId,
)

res.render('pages/currentlyOut', {
prisoners: prisonersCurrentlyOut,
location: null,
})
}
}
}
1 change: 1 addition & 0 deletions server/data/interfaces/prisonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export interface PrisonApiClient {
getOffenderCellHistory(bookingId: number, params?: { page: number; size: number }): Promise<PagedList<BedAssignment>>
getUserDetailsList(usernames: string[]): Promise<UserDetail[]>
getPrisonersCurrentlyOutOfLivingUnit(livingUnitId: string): Promise<OffenderOut[]>
getPrisonersCurrentlyOutOfPrison(prisonId: string): Promise<OffenderOut[]>
}
4 changes: 4 additions & 0 deletions server/data/prisonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,8 @@ export default class PrisonApiRestClient implements PrisonApiClient {
getPrisonersCurrentlyOutOfLivingUnit(livingUnitId: string): Promise<OffenderOut[]> {
return this.get<OffenderOut[]>({ path: `/api/movements/livingUnit/${livingUnitId}/currently-out` })
}

getPrisonersCurrentlyOutOfPrison(prisonId: string): Promise<OffenderOut[]> {
return this.get<OffenderOut[]>({ path: `/api/movements/agency/${prisonId}/currently-out` })
}
}
1 change: 1 addition & 0 deletions server/routes/establishmentRollRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function establishmentRollRouter(services: Services): Router {
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())

return router
Expand Down
75 changes: 75 additions & 0 deletions server/services/movementsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,79 @@ describe('movementsService', () => {
expect(result).toEqual([])
})
})

describe('getOffendersCurrentlyOutTotal', () => {
beforeEach(() => {
prisonApiClientMock.getPrisonersCurrentlyOutOfPrison = jest.fn().mockResolvedValue(movementsOutMock)
prisonerSearchApiClientMock.getPrisonersById = jest.fn().mockResolvedValue(prisonerSearchMock)
prisonApiClientMock.getRecentMovements = jest.fn().mockResolvedValue(movementsRecentMock)
})

it('should search for prisoners for prisoners from getPrisonersCurrentlyOutOfPrison', async () => {
const result = await movementsService.getOffendersCurrentlyOutTotal('token', 'MDI')
expect(prisonerSearchApiClientMock.getPrisonersById).toHaveBeenCalledWith(['A1234AA', 'A1234AB'])
expect(prisonApiClientMock.getRecentMovements).toHaveBeenCalledWith(['A1234AA', 'A1234AB'])

expect(result).toEqual([
expect.objectContaining(prisonerSearchMock[0]),
expect.objectContaining(prisonerSearchMock[1]),
])
})

it('should decorate the alertFlags for each prisoner', async () => {
const result = await movementsService.getOffendersCurrentlyOutTotal('token', 'MDI')

expect(result).toEqual([
expect.objectContaining({
alertFlags: [],
}),
expect.objectContaining({
alertFlags: [
{
alertCodes: ['HID'],
alertIds: ['HID'],
classes: 'alert-status alert-status--medical',
label: 'Hidden disability',
},
],
}),
])
})

it('should add the currentLocation from latest movement toCity', async () => {
const result = await movementsService.getOffendersCurrentlyOutTotal('token', 'MDI')

expect(result).toEqual([
expect.objectContaining({
currentLocation: 'Sheffield',
}),
expect.objectContaining({
currentLocation: 'Doncaster',
}),
])
})

it('should add the movementComment from latest movement commentText', async () => {
const result = await movementsService.getOffendersCurrentlyOutTotal('token', 'MDI')

expect(result).toEqual([
expect.objectContaining({
movementComment: 'Some Sheffield comment',
}),
expect.objectContaining({
movementComment: 'Some Doncaster comment',
}),
])
})

it('should return empty api if no currently out prisoners', async () => {
prisonApiClientMock.getPrisonersCurrentlyOutOfPrison = jest.fn().mockResolvedValue([])

const result = await movementsService.getOffendersCurrentlyOutTotal('token', 'LEI')
expect(prisonerSearchApiClientMock.getPrisonersById).toBeCalledTimes(0)
expect(prisonApiClientMock.getRecentMovements).toBeCalledTimes(0)

expect(result).toEqual([])
})
})
})
40 changes: 35 additions & 5 deletions server/services/movementsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PrisonerWithAlerts } from './interfaces/establishmentRollService/Prison
import { stripAgencyPrefix } from '../utils/utils'
import { Prisoner } from '../data/interfaces/prisoner'
import { BedAssignment } from '../data/interfaces/bedAssignment'
import { OffenderMovement } from '../data/interfaces/offenderMovement'

export default class MovementsService {
constructor(
Expand All @@ -21,8 +22,8 @@ export default class MovementsService {
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const movements = await prisonApi.getMovementsIn(caseLoadId, new Date().toISOString())

if (!movements || !movements?.length) return []

const prisoners = await prisonerSearchClient.getPrisonersById(movements.map(movement => movement.offenderNo))

return prisoners
Expand All @@ -46,8 +47,8 @@ export default class MovementsService {
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const movements = await prisonApi.getMovementsOut(caseLoadId, new Date().toISOString())

if (!movements || !movements?.length) return []

const prisoners = await prisonerSearchClient.getPrisonersById(movements.map(movement => movement.offenderNo))

return prisoners
Expand All @@ -71,8 +72,8 @@ export default class MovementsService {
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const movements = await prisonApi.getMovementsEnRoute(caseLoadId)

if (!movements || !movements?.length) return []

const prisoners = await prisonerSearchClient.getPrisonersById(movements.map(movement => movement.offenderNo))

return prisoners
Expand All @@ -98,9 +99,9 @@ export default class MovementsService {
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const movements = await prisonApi.getMovementsInReception(caseLoadId)
const prisonerNumbers = movements.map(movement => movement.offenderNo)

if (!movements || !movements?.length) return []

const prisonerNumbers = movements.map(movement => movement.offenderNo)
const [prisoners, recentMovements] = await Promise.all([
prisonerSearchClient.getPrisonersById(prisonerNumbers),
prisonApi.getRecentMovements(prisonerNumbers),
Expand Down Expand Up @@ -178,14 +179,43 @@ export default class MovementsService {
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const outPrisoners = await prisonApi.getPrisonersCurrentlyOutOfLivingUnit(livingUnitId)
if (!outPrisoners || !outPrisoners?.length) return []
const prisonerNumbers = outPrisoners.map(prisoner => prisoner.offenderNo)

const [prisoners, recentMovements] = await Promise.all([
prisonerSearchClient.getPrisonersById(prisonerNumbers),
prisonApi.getRecentMovements(prisonerNumbers),
])

return this.mapCurrentlyOutPrisoners(prisoners, recentMovements)
}

public async getOffendersCurrentlyOutTotal(
clientToken: string,
caseLoadId: string,
): Promise<(PrisonerWithAlerts & { currentLocation: string; movementComment?: string })[]> {
const prisonApi = this.prisonApiClientBuilder(clientToken)
const prisonerSearchClient = this.prisonerSearchClientBuilder(clientToken)

const outPrisoners = await prisonApi.getPrisonersCurrentlyOutOfPrison(caseLoadId)
if (!outPrisoners || !outPrisoners?.length) return []
const prisonerNumbers = outPrisoners.map(prisoner => prisoner.offenderNo)

const [prisoners, recentMovements] = await Promise.all([
prisonerSearchClient.getPrisonersById(prisonerNumbers),
prisonApi.getRecentMovements(prisonerNumbers),
])

return this.mapCurrentlyOutPrisoners(prisoners, recentMovements)
}

private mapCurrentlyOutPrisoners(
prisoners: Prisoner[],
recentMovements: OffenderMovement[],
): (PrisonerWithAlerts & {
currentLocation: string
movementComment?: string
})[] {
return prisoners
.sort((a, b) => a.lastName.localeCompare(b.lastName, 'en', { ignorePunctuation: true }))
.map(prisoner => {
Expand Down
1 change: 1 addition & 0 deletions server/test/mocks/prisonApiClientMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const prisonApiClientMock: PrisonApiClient = {
getOffenderCellHistory: jest.fn(),
getUserDetailsList: jest.fn(),
getPrisonersCurrentlyOutOfLivingUnit: jest.fn(),
getPrisonersCurrentlyOutOfPrison: jest.fn(),
}

export default prisonApiClientMock
2 changes: 1 addition & 1 deletion server/views/pages/currentlyOut.njk
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% from "../macros/categoryFlag.njk" import categoryFlag %}

{% set locationName = location.userDescription if location.userDescription else location.description %}
{% set pageTitle = "Currently out - " + locationName %}
{% set pageTitle = "Currently out - " + locationName if location else "Total currently out"%}
{% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %}

{% set breadCrumbs = [
Expand Down
2 changes: 1 addition & 1 deletion server/views/pages/establishmentRoll.njk
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
<td class="govuk-table__cell">{{ todayStats.currentlyInCell }}</td>
<td class="govuk-table__cell">
{% if todayStats.totalCurrentlyOut > 0 %}
<a class="govuk-link" href="{{config.serviceUrls.digitalPrisons}}/establishment-roll/total-currently-out">{{todayStats.totalCurrentlyOut}}</a>
<a class="govuk-link" href="/establishment-roll/total-currently-out">{{todayStats.totalCurrentlyOut}}</a>
{% else %} 0 {% endif %}
</td>
<td class="govuk-table__cell">{{ todayStats.operationalCapacity }}</td>
Expand Down