From 2b24c5b5b538b10fd5aa6c2fcb0c8caf5ce72e73 Mon Sep 17 00:00:00 2001 From: Matthew Whitfield Date: Mon, 10 Jun 2024 14:19:46 +0100 Subject: [PATCH] CDPS-778 update establishement roll page to use new endpoint --- integration_tests/e2e/establishmentRoll.cy.ts | 208 +++++++++--------- integration_tests/mockApis/prison.ts | 17 ++ server/data/interfaces/prisonApiClient.ts | 4 +- server/data/interfaces/prisonRollCount.ts | 39 ++++ server/data/prisonApiClient.ts | 13 +- server/mocks/prisonRollCountMock.ts | 127 +++++++++++ .../services/establishmentRollService.test.ts | 112 ++-------- server/services/establishmentRollService.ts | 53 +---- .../EstablishmentRollCount.ts | 22 +- server/services/utils/nestRollBlocks.test.ts | 129 ----------- server/services/utils/nestRollBlocks.ts | 38 ---- server/test/mocks/prisonApiClientMock.ts | 3 +- server/views/pages/establishmentRoll.njk | 58 ++--- 13 files changed, 360 insertions(+), 463 deletions(-) create mode 100644 server/data/interfaces/prisonRollCount.ts create mode 100644 server/mocks/prisonRollCountMock.ts delete mode 100644 server/services/utils/nestRollBlocks.test.ts delete mode 100644 server/services/utils/nestRollBlocks.ts diff --git a/integration_tests/e2e/establishmentRoll.cy.ts b/integration_tests/e2e/establishmentRoll.cy.ts index 9435c09..144b10c 100644 --- a/integration_tests/e2e/establishmentRoll.cy.ts +++ b/integration_tests/e2e/establishmentRoll.cy.ts @@ -14,12 +14,7 @@ context('Establishment Roll Page', () => { { caseloadFunction: '', caseLoadId: 'LEI', currentlyActive: true, description: 'Leeds (HMP)', type: '' }, ], }) - cy.task('stubRollCount', { payload: assignedRollCountWithSpursMock, query: '?wingOnly=false' }) - cy.task('stubRollCountUnassigned') - cy.task('stubMovements') - cy.task('stubEnrouteRollCount') - cy.task('stubGetLocationsForPrison') - cy.task('getAttributesForLocation') + cy.task('stubPrisonRollCount') cy.signIn({ redirectPath: '/establishment-roll' }) cy.visit('/establishment-roll') }) @@ -28,130 +23,129 @@ context('Establishment Roll Page', () => { Page.verifyOnPage(EstablishmentRollPage) }) - context('Outage Banner', () => { - it('should display todays stats', () => { - const page = Page.verifyOnPage(EstablishmentRollPage) - page.todaysStats().unlockRoll().should('contain.text', '1815') - page.todaysStats().currentPopulation().should('contain.text', '1823') - page.todaysStats().arrivedToday().should('contain.text', '17') - page.todaysStats().inReception().should('contain.text', '23') - page.todaysStats().stillToArrive().should('contain.text', '1') - page.todaysStats().outToday().should('contain.text', '9') - page.todaysStats().noCellAllocated().should('contain.text', '31') - }) + it('should display todays stats', () => { + const page = Page.verifyOnPage(EstablishmentRollPage) + page.todaysStats().unlockRoll().should('contain.text', '100') + page.todaysStats().currentPopulation().should('contain.text', '200') + page.todaysStats().arrivedToday().should('contain.text', '300') + page.todaysStats().inReception().should('contain.text', '400') + page.todaysStats().stillToArrive().should('contain.text', '500') + page.todaysStats().outToday().should('contain.text', '600') + page.todaysStats().noCellAllocated().should('contain.text', '700') + }) - it('should display a table row for each wing level assignedRollCount', () => { - const page = Page.verifyOnPage(EstablishmentRollPage) - page.assignedRollCountRows().should('have.length', 6) - - page.assignedRollCountRows().first().find('td').eq(0).should('contain.text', 'A') - page.assignedRollCountRows().first().find('td').eq(1).should('contain.text', '76') - page.assignedRollCountRows().first().find('td').eq(2).should('contain.text', '900') - page.assignedRollCountRows().first().find('td').eq(3).should('contain.text', '5') - page.assignedRollCountRows().first().find('td').eq(4).should('contain.text', '60') - page.assignedRollCountRows().first().find('td').eq(5).should('contain.text', '-16') - page.assignedRollCountRows().first().find('td').eq(6).should('contain.text', '0') - }) + it('should display a table row for each wing level assignedRollCount', () => { + const page = Page.verifyOnPage(EstablishmentRollPage) + page.assignedRollCountRows().should('have.length', 7) + + page.assignedRollCountRows().first().find('td').eq(0).should('contain.text', 'B Wing') + page.assignedRollCountRows().first().find('td').eq(1).should('contain.text', '194') + page.assignedRollCountRows().first().find('td').eq(2).should('contain.text', '192') + page.assignedRollCountRows().first().find('td').eq(3).should('contain.text', '1') + page.assignedRollCountRows().first().find('td').eq(4).should('contain.text', '470') + page.assignedRollCountRows().first().find('td').eq(5).should('contain.text', '276') + page.assignedRollCountRows().first().find('td').eq(6).should('contain.text', '0') + }) - it('should display a table row for totals', () => { - const page = Page.verifyOnPage(EstablishmentRollPage) + it('should display a table row for totals', () => { + const page = Page.verifyOnPage(EstablishmentRollPage) - page.assignedRollCountRows().last().find('td').eq(0).should('contain.text', 'Totals') - page.assignedRollCountRows().last().find('td').eq(1).should('contain.text', '152') - page.assignedRollCountRows().last().find('td').eq(2).should('contain.text', '1800') - page.assignedRollCountRows().last().find('td').eq(3).should('contain.text', '10') - page.assignedRollCountRows().last().find('td').eq(4).should('contain.text', '120') - page.assignedRollCountRows().last().find('td').eq(5).should('contain.text', '-32') - page.assignedRollCountRows().last().find('td').eq(6).should('contain.text', '0') - }) + page.assignedRollCountRows().last().find('td').eq(0).should('contain.text', 'Totals') + page.assignedRollCountRows().last().find('td').eq(1).should('contain.text', '10') + page.assignedRollCountRows().last().find('td').eq(2).should('contain.text', '20') + page.assignedRollCountRows().last().find('td').eq(3).should('contain.text', '30') + page.assignedRollCountRows().last().find('td').eq(4).should('contain.text', '40') + page.assignedRollCountRows().last().find('td').eq(5).should('contain.text', '50') + page.assignedRollCountRows().last().find('td').eq(6).should('contain.text', '60') + }) - it('should reveal spurs and landings when click on link', () => { - const page = Page.verifyOnPage(EstablishmentRollPage) + it('should reveal spurs and landings when click on link', () => { + const page = Page.verifyOnPage(EstablishmentRollPage) - page.assignedRollCountRows().eq(0).find('td').eq(0).should('contain.text', 'A').should('be.visible') - page.assignedRollCountRows().eq(1).find('td').eq(0).should('contain.text', 'Spur A1').should('not.be.visible') - page.assignedRollCountRows().eq(2).find('td').eq(0).should('contain.text', 'Landing A1X').should('not.be.visible') - page.assignedRollCountRows().eq(3).find('td').eq(0).should('contain.text', 'B').should('be.visible') - page.assignedRollCountRows().eq(4).find('td').eq(0).should('contain.text', 'LANDING BY').should('not.be.visible') + page.assignedRollCountRows().eq(0).find('td').eq(0).should('contain.text', 'B Wing').should('be.visible') + page.assignedRollCountRows().eq(1).find('td').eq(0).should('contain.text', '1').should('not.be.visible') + page.assignedRollCountRows().eq(2).find('td').eq(0).should('contain.text', '2').should('not.be.visible') + page.assignedRollCountRows().eq(3).find('td').eq(0).should('contain.text', 'C Wing').should('be.visible') + page.assignedRollCountRows().eq(4).find('td').eq(0).should('contain.text', 'C-1').should('not.be.visible') + page.assignedRollCountRows().eq(5).find('td').eq(0).should('contain.text', 'C-1-1').should('not.be.visible') - const wing1Reveal = page.assignedRollCountRows().eq(0).find('td').eq(0).find('a') - wing1Reveal.click() - page.assignedRollCountRows().eq(1).find('td').eq(0).should('be.visible') - page.assignedRollCountRows().eq(2).find('td').eq(0).should('be.visible') + const wing1Reveal = page.assignedRollCountRows().eq(0).find('td').eq(0).find('a') + wing1Reveal.click() + page.assignedRollCountRows().eq(1).find('td').eq(0).should('be.visible') + page.assignedRollCountRows().eq(2).find('td').eq(0).should('be.visible') - wing1Reveal.click() - page.assignedRollCountRows().eq(1).find('td').eq(0).should('not.be.visible') - page.assignedRollCountRows().eq(2).find('td').eq(0).should('not.be.visible') + wing1Reveal.click() + page.assignedRollCountRows().eq(1).find('td').eq(0).should('not.be.visible') + page.assignedRollCountRows().eq(2).find('td').eq(0).should('not.be.visible') - const wing2Reveal = page.assignedRollCountRows().eq(3).find('td').eq(0).find('a') - wing2Reveal.click() - page.assignedRollCountRows().eq(4).find('td').eq(0).should('be.visible') + const wing2Reveal = page.assignedRollCountRows().eq(3).find('td').eq(0).find('a') + wing2Reveal.click() + page.assignedRollCountRows().eq(4).find('td').eq(0).should('be.visible') - wing2Reveal.click() - page.assignedRollCountRows().eq(4).find('td').eq(0).should('not.be.visible') + wing2Reveal.click() + page.assignedRollCountRows().eq(4).find('td').eq(0).should('not.be.visible') + }) + + it('should show link to landing pages when wing has spur', () => { + cy.task('stubGetLocation', { locationId: 20000, payload: { ...locationMock, description: 'WING 1' } }) + cy.task('stubGetLocation', { locationId: 100, payload: { ...locationMock, description: 'Spur 1' } }) + cy.task('stubGetLocation', { locationId: 12729, payload: { ...locationMock, description: 'Landing 1' } }) + cy.task('stubRollCount', { + payload: assignedRollCountWithSpursMock, + query: '?wingOnly=false&showCells=true&parentLocationId=12729', }) - it('should show link to landing pages when wing has spur', () => { - cy.task('stubGetLocation', { locationId: 1, payload: { ...locationMock, description: 'WING 1' } }) - cy.task('stubGetLocation', { locationId: 2, payload: { ...locationMock, description: 'Spur 1' } }) - cy.task('stubGetLocation', { locationId: 3, payload: { ...locationMock, description: 'Landing 1' } }) - cy.task('stubRollCount', { - payload: assignedRollCountWithSpursMock, - query: '?wingOnly=false&showCells=true&parentLocationId=3', - }) + const page = Page.verifyOnPage(EstablishmentRollPage) - const page = Page.verifyOnPage(EstablishmentRollPage) + const wing2Reveal = page.assignedRollCountRows().eq(3).find('td').eq(0).find('a') + wing2Reveal.click() + page.assignedRollCountRows().eq(5).find('td').eq(0).find('a').click() - const wing1Reveal = page.assignedRollCountRows().eq(0).find('td').eq(0).find('a') - wing1Reveal.click() - page.assignedRollCountRows().eq(2).find('td').eq(0).find('a').click() + const landingPage = Page.verifyOnPageWithTitle(LandingRollPage, 'WING 1 - Spur 1 - Landing 1') - const landingPage = Page.verifyOnPageWithTitle(LandingRollPage, 'WING 1 - Spur 1 - Landing 1') + landingPage.rollCountRows().should('have.length', 5) - landingPage.rollCountRows().should('have.length', 5) + landingPage.rollCountRows().eq(0).find('td').eq(0).should('contain.text', 'A') + landingPage.rollCountRows().eq(1).find('td').eq(0).should('contain.text', 'Spur A1') + landingPage.rollCountRows().eq(2).find('td').eq(0).should('contain.text', 'Landing A1X') + landingPage.rollCountRows().eq(3).find('td').eq(0).should('contain.text', 'B') + landingPage.rollCountRows().eq(4).find('td').eq(0).should('contain.text', 'LANDING BY') - landingPage.rollCountRows().eq(0).find('td').eq(0).should('contain.text', 'A') - landingPage.rollCountRows().eq(1).find('td').eq(0).should('contain.text', 'Spur A1') - landingPage.rollCountRows().eq(2).find('td').eq(0).should('contain.text', 'Landing A1X') - landingPage.rollCountRows().eq(3).find('td').eq(0).should('contain.text', 'B') - landingPage.rollCountRows().eq(4).find('td').eq(0).should('contain.text', 'LANDING BY') + landingPage.rollCountRows().first().find('td').eq(1).should('contain.text', '76') + landingPage.rollCountRows().first().find('td').eq(2).should('contain.text', '900') + landingPage.rollCountRows().first().find('td').eq(3).should('contain.text', '5') + landingPage.rollCountRows().first().find('td').eq(4).should('contain.text', '60') + landingPage.rollCountRows().first().find('td').eq(5).should('contain.text', '-16') + }) - landingPage.rollCountRows().first().find('td').eq(1).should('contain.text', '76') - landingPage.rollCountRows().first().find('td').eq(2).should('contain.text', '900') - landingPage.rollCountRows().first().find('td').eq(3).should('contain.text', '5') - landingPage.rollCountRows().first().find('td').eq(4).should('contain.text', '60') - landingPage.rollCountRows().first().find('td').eq(5).should('contain.text', '-16') + it('should show link to landing pages when wing does not have spur', () => { + cy.task('stubGetLocation', { locationId: 10000, payload: { ...locationMock, description: 'WING 1' } }) + cy.task('stubGetLocation', { locationId: 12714, payload: { ...locationMock, description: 'Landing 1' } }) + cy.task('stubRollCount', { + payload: assignedRollCountWithSpursMock, + query: '?wingOnly=false&showCells=true&parentLocationId=12714', }) - it('should show link to landing pages when wing does not have spur', () => { - cy.task('stubGetLocation', { locationId: 4, payload: { ...locationMock, description: 'WING 1' } }) - cy.task('stubGetLocation', { locationId: 5, payload: { ...locationMock, description: 'Landing 1' } }) - cy.task('stubRollCount', { - payload: assignedRollCountWithSpursMock, - query: '?wingOnly=false&showCells=true&parentLocationId=5', - }) + const page = Page.verifyOnPage(EstablishmentRollPage) - const page = Page.verifyOnPage(EstablishmentRollPage) + const wing2Reveal = page.assignedRollCountRows().eq(0).find('td').eq(0).find('a') + wing2Reveal.click() + page.assignedRollCountRows().eq(1).find('td').eq(0).find('a').click() - const wing2Reveal = page.assignedRollCountRows().eq(3).find('td').eq(0).find('a') - wing2Reveal.click() - page.assignedRollCountRows().eq(4).find('td').eq(0).find('a').click() + const landingPage = Page.verifyOnPageWithTitle(LandingRollPage, 'WING 1 - Landing 1') - const landingPage = Page.verifyOnPageWithTitle(LandingRollPage, 'WING 1 - Landing 1') + landingPage.rollCountRows().should('have.length', 5) - landingPage.rollCountRows().should('have.length', 5) + landingPage.rollCountRows().eq(0).find('td').eq(0).should('contain.text', 'A') + landingPage.rollCountRows().eq(1).find('td').eq(0).should('contain.text', 'Spur A1') + landingPage.rollCountRows().eq(2).find('td').eq(0).should('contain.text', 'Landing A1X') + landingPage.rollCountRows().eq(3).find('td').eq(0).should('contain.text', 'B') + landingPage.rollCountRows().eq(4).find('td').eq(0).should('contain.text', 'LANDING BY') - landingPage.rollCountRows().eq(0).find('td').eq(0).should('contain.text', 'A') - landingPage.rollCountRows().eq(1).find('td').eq(0).should('contain.text', 'Spur A1') - landingPage.rollCountRows().eq(2).find('td').eq(0).should('contain.text', 'Landing A1X') - landingPage.rollCountRows().eq(3).find('td').eq(0).should('contain.text', 'B') - landingPage.rollCountRows().eq(4).find('td').eq(0).should('contain.text', 'LANDING BY') - - landingPage.rollCountRows().first().find('td').eq(1).should('contain.text', '76') - landingPage.rollCountRows().first().find('td').eq(2).should('contain.text', '900') - landingPage.rollCountRows().first().find('td').eq(3).should('contain.text', '5') - landingPage.rollCountRows().first().find('td').eq(4).should('contain.text', '60') - landingPage.rollCountRows().first().find('td').eq(5).should('contain.text', '-16') - }) + landingPage.rollCountRows().first().find('td').eq(1).should('contain.text', '76') + landingPage.rollCountRows().first().find('td').eq(2).should('contain.text', '900') + landingPage.rollCountRows().first().find('td').eq(3).should('contain.text', '5') + landingPage.rollCountRows().first().find('td').eq(4).should('contain.text', '60') + landingPage.rollCountRows().first().find('td').eq(5).should('contain.text', '-16') }) }) diff --git a/integration_tests/mockApis/prison.ts b/integration_tests/mockApis/prison.ts index dfae21f..02282de 100644 --- a/integration_tests/mockApis/prison.ts +++ b/integration_tests/mockApis/prison.ts @@ -13,6 +13,7 @@ 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' +import { prisonRollCountMock } from '../../server/mocks/prisonRollCountMock' export default { stubUserCaseLoads: (caseLoads: CaseLoad[] = []) => { @@ -63,6 +64,22 @@ export default { }) }, + stubPrisonRollCount: ({ prisonCode = 'LEI', payload = prisonRollCountMock } = {}) => { + return stubFor({ + request: { + method: 'GET', + url: `/prison/api/prison/roll-count/${prisonCode}`, + }, + response: { + status: 200, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + jsonBody: payload, + }, + }) + }, + stubRollCountUnassigned: (prisonCode = 'LEI') => { return stubFor({ request: { diff --git a/server/data/interfaces/prisonApiClient.ts b/server/data/interfaces/prisonApiClient.ts index b30149a..223ab75 100644 --- a/server/data/interfaces/prisonApiClient.ts +++ b/server/data/interfaces/prisonApiClient.ts @@ -12,6 +12,7 @@ import { OffenderInReception } from './offenderInReception' import { PagedList } from './pagedList' import { BedAssignment } from './bedAssignment' import { UserDetail } from './userDetail' +import PrisonRollCount from './prisonRollCount' export interface PrisonApiClient { getUserCaseLoads(): Promise @@ -20,8 +21,6 @@ export interface PrisonApiClient { prisonId: string, options?: { unassigned?: boolean; wingOnly?: boolean; showCells?: boolean; parentLocationId?: number }, ): Promise - getEnrouteRollCount(prisonId: string): Promise - getLocationsForPrison(prisonId: string): Promise getLocation(locationId: string): Promise getAttributesForLocation(locationId: number): Promise getMovements(prisonId: string): Promise @@ -37,4 +36,5 @@ export interface PrisonApiClient { getUserDetailsList(usernames: string[]): Promise getPrisonersCurrentlyOutOfLivingUnit(livingUnitId: string): Promise getPrisonersCurrentlyOutOfPrison(prisonId: string): Promise + getPrisonRollCount(prisonId: string): Promise } diff --git a/server/data/interfaces/prisonRollCount.ts b/server/data/interfaces/prisonRollCount.ts new file mode 100644 index 0000000..eacf8fc --- /dev/null +++ b/server/data/interfaces/prisonRollCount.ts @@ -0,0 +1,39 @@ +export default interface PrisonRollCount { + prisonId: string + numUnlockRollToday: number + numCurrentPopulation: number + numArrivedToday: number + numInReception: number + numStillToArrive: number + numOutToday: number + numNoCellAllocated: number + totals: { + bedsInUse: number + currentlyInCell: number + currentlyOut: number + workingCapacity: number + netVacancies: number + outOfOrder: number + } + locations: ResidentialLocation[] +} + +export interface ResidentialLocation { + locationId: string + locationType: 'WING' | 'LAND' | 'SPUR' | 'CELL' | 'ROOM' + locationCode: string + fullLocationPath: string + certified: boolean + localName?: string + rollCount: LocationRollCount + subLocations: ResidentialLocation[] +} + +interface LocationRollCount { + bedsInUse: number + currentlyInCell: number + currentlyOut: number + workingCapacity: number + netVacancies: number + outOfOrder: number +} diff --git a/server/data/prisonApiClient.ts b/server/data/prisonApiClient.ts index ea3a4e8..bdcd620 100644 --- a/server/data/prisonApiClient.ts +++ b/server/data/prisonApiClient.ts @@ -15,6 +15,7 @@ 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' export default class PrisonApiRestClient implements PrisonApiClient { constructor(private restClient: RestClient) {} @@ -73,14 +74,6 @@ export default class PrisonApiRestClient implements PrisonApiClient { return this.get({ path: `/api/movements/rollcount/${prisonId}/in-reception` }) } - getEnrouteRollCount(prisonId: string): Promise { - return this.get({ path: `/api/movements/rollcount/${prisonId}/enroute` }) - } - - getLocationsForPrison(prisonId: string): Promise { - return this.get({ path: `/api/agencies/${prisonId}/locations` }) - } - getLocation(locationId: string): Promise { return this.get({ path: `/api/locations/${locationId}` }) } @@ -132,4 +125,8 @@ export default class PrisonApiRestClient implements PrisonApiClient { getPrisonersCurrentlyOutOfPrison(prisonId: string): Promise { return this.get({ path: `/api/movements/agency/${prisonId}/currently-out` }) } + + getPrisonRollCount(prisonId: string): Promise { + return this.get({ path: `/api/prison/roll-count/${prisonId}` }) + } } diff --git a/server/mocks/prisonRollCountMock.ts b/server/mocks/prisonRollCountMock.ts new file mode 100644 index 0000000..bdea27a --- /dev/null +++ b/server/mocks/prisonRollCountMock.ts @@ -0,0 +1,127 @@ +import PrisonRollCount from '../data/interfaces/prisonRollCount' + +export const prisonRollCountMock: PrisonRollCount = { + prisonId: 'MDI', + numUnlockRollToday: 100, + numCurrentPopulation: 200, + numArrivedToday: 300, + numInReception: 400, + numStillToArrive: 500, + numOutToday: 600, + numNoCellAllocated: 700, + totals: { + bedsInUse: 10, + currentlyInCell: 20, + currentlyOut: 30, + workingCapacity: 40, + netVacancies: 50, + outOfOrder: 60, + }, + locations: [ + { + locationId: '10000', + locationType: 'WING', + locationCode: 'B', + fullLocationPath: 'B', + certified: true, + localName: 'B Wing', + rollCount: { + bedsInUse: 194, + currentlyInCell: 192, + currentlyOut: 1, + workingCapacity: 470, + netVacancies: 276, + outOfOrder: 0, + }, + subLocations: [ + { + locationId: '12714', + locationType: 'LAND', + locationCode: '1', + fullLocationPath: 'B-1', + certified: true, + localName: '1', + rollCount: { + bedsInUse: 27, + currentlyInCell: 27, + currentlyOut: 0, + workingCapacity: 28, + netVacancies: 1, + outOfOrder: 0, + }, + subLocations: [], + }, + { + locationId: '12729', + locationType: 'LAND', + locationCode: '2', + fullLocationPath: 'B-2', + certified: true, + localName: '2', + rollCount: { + bedsInUse: 39, + currentlyInCell: 38, + currentlyOut: 1, + workingCapacity: 46, + netVacancies: 7, + outOfOrder: 0, + }, + subLocations: [], + }, + ], + }, + { + locationId: '20000', + locationType: 'WING', + locationCode: 'C', + fullLocationPath: 'C', + certified: true, + localName: 'C Wing', + rollCount: { + bedsInUse: 194, + currentlyInCell: 192, + currentlyOut: 1, + workingCapacity: 470, + netVacancies: 276, + outOfOrder: 0, + }, + subLocations: [ + { + locationId: '100', + locationType: 'SPUR', + locationCode: '1', + fullLocationPath: 'C-1', + certified: true, + localName: 'C-1', + rollCount: { + bedsInUse: 27, + currentlyInCell: 27, + currentlyOut: 0, + workingCapacity: 28, + netVacancies: 1, + outOfOrder: 0, + }, + subLocations: [ + { + locationId: '12729', + locationType: 'LAND', + locationCode: 'C-1-1', + fullLocationPath: 'C-1-1', + certified: true, + localName: 'C-1-1', + rollCount: { + bedsInUse: 39, + currentlyInCell: 38, + currentlyOut: 1, + workingCapacity: 46, + netVacancies: 7, + outOfOrder: 0, + }, + subLocations: [], + }, + ], + }, + ], + }, + ], +} diff --git a/server/services/establishmentRollService.test.ts b/server/services/establishmentRollService.test.ts index 8777733..db21aa8 100644 --- a/server/services/establishmentRollService.test.ts +++ b/server/services/establishmentRollService.test.ts @@ -1,6 +1,7 @@ import EstablishmentRollService from './establishmentRollService' import prisonApiClientMock from '../test/mocks/prisonApiClientMock' -import { assignedRollCountWithSpursMock, unassignedRollCountMock } from '../mocks/rollCountMock' +import { assignedRollCountWithSpursMock } from '../mocks/rollCountMock' +import { prisonRollCountMock } from '../mocks/prisonRollCountMock' describe('establishmentRollService', () => { let establishmentRollService: EstablishmentRollService @@ -11,105 +12,40 @@ describe('establishmentRollService', () => { describe('getEstablishmentRollCounts', () => { beforeEach(() => { - prisonApiClientMock.getRollCount = jest - .fn() - .mockResolvedValueOnce(assignedRollCountWithSpursMock) - .mockResolvedValueOnce(unassignedRollCountMock) - prisonApiClientMock.getMovements = jest.fn().mockResolvedValue({ in: 4, out: 5 }) - prisonApiClientMock.getEnrouteRollCount = jest.fn().mockResolvedValue(3) - prisonApiClientMock.getLocationsForPrison = jest.fn().mockResolvedValue([]) - prisonApiClientMock.getAttributesForLocation = jest.fn().mockResolvedValue({ noOfOccupants: 31 }) + prisonApiClientMock.getPrisonRollCount = jest.fn().mockResolvedValueOnce(prisonRollCountMock) }) - it('should return nested establishment roll counts', async () => { + it('should return data from API for the today stats', async () => { const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - expect(establishmentRollCounts.assignedRollBlocksCounts).toEqual([ - { - ...assignedRollCountWithSpursMock[0], - spurs: [{ ...assignedRollCountWithSpursMock[1], landings: [assignedRollCountWithSpursMock[2]] }], - }, - { ...assignedRollCountWithSpursMock[3], landings: [assignedRollCountWithSpursMock[4]] }, - ]) - }) - - it('should calculate unlock roll', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.unlockRoll).toEqual(1824) - }) - - it('should return inToday', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.inToday).toEqual(4) - }) - - it('should return outToday', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.inToday).toEqual(4) - }) - - it('should return unassignedIn by summing currentlyInCell, outOfLivingUnits from unassigned roll call', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.unassignedIn).toEqual(23) - }) - - it('should return currentRoll by summing currentlyInCell, outOfLivingUnits an unassignedIn', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.currentRoll).toEqual(1823) - }) - - it('should return enroute count from api', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.enroute).toEqual(3) - }) - - it('should return 0 for noCellAllocated if no CSWAP locations', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.noCellAllocated).toEqual(0) - }) - - it('should call api for noCellAllocated if CSWAP location', async () => { - prisonApiClientMock.getLocationsForPrison = jest.fn().mockResolvedValue([{ description: 'CSWAP', locationId: 1 }]) - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.noCellAllocated).toEqual(31) - }) - - it('should return total of currentlyOut from assigned counts', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.totalCurrentlyOut).toEqual(10) - }) - - it('should return total of bedsInUse from assigned counts', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.bedsInUse).toEqual(152) - }) - - it('should return total of currentlyInCell from assigned counts', async () => { - const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - - expect(establishmentRollCounts.todayStats.currentlyInCell).toEqual(1800) + expect(establishmentRollCounts.todayStats).toEqual({ + currentRoll: 200, + enroute: 500, + inToday: 300, + noCellAllocated: 700, + outToday: 600, + unassignedIn: 400, + unlockRoll: 100, + }) }) - it('should return total of netVacancies from assigned counts', async () => { + it('should return data from API for the total stats', async () => { const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - expect(establishmentRollCounts.todayStats.netVacancies).toEqual(-32) + expect(establishmentRollCounts.totals).toEqual({ + bedsInUse: 10, + currentlyInCell: 20, + currentlyOut: 30, + netVacancies: 50, + outOfOrder: 60, + workingCapacity: 40, + }) }) - it('should return total of outOfOrder from assigned counts', async () => { + it('should return wing data from API', async () => { const establishmentRollCounts = await establishmentRollService.getEstablishmentRollCounts('token', 'LEI') - expect(establishmentRollCounts.todayStats.outOfOrder).toEqual(0) + expect(establishmentRollCounts.wings).toEqual(prisonRollCountMock.locations) }) }) diff --git a/server/services/establishmentRollService.ts b/server/services/establishmentRollService.ts index c2452f2..15bcfb1 100644 --- a/server/services/establishmentRollService.ts +++ b/server/services/establishmentRollService.ts @@ -1,59 +1,26 @@ import { RestClientBuilder } from '../data' import { PrisonApiClient } from '../data/interfaces/prisonApiClient' -import { BlockRollCount } from '../data/interfaces/blockRollCount' import EstablishmentRollCount from './interfaces/establishmentRollService/EstablishmentRollCount' -import nestRollBlocks, { splitRollBlocks } from './utils/nestRollBlocks' - -const getTotals = (array: BlockRollCount[], figure: keyof BlockRollCount): number => - array.reduce((accumulator, block) => accumulator + ((block[figure] as number) || 0), 0) export default class EstablishmentRollService { constructor(private readonly prisonApiClientBuilder: RestClientBuilder) {} public async getEstablishmentRollCounts(clientToken: string, caseLoadId: string): Promise { const prisonApi = this.prisonApiClientBuilder(clientToken) - const [assignedRollBlocksCounts, unassignedRollBlocksCount, movementsCount, enrouteCount, caseLoadLocations] = - await Promise.all([ - prisonApi.getRollCount(caseLoadId, { wingOnly: false }), - prisonApi.getRollCount(caseLoadId, { unassigned: true }), - prisonApi.getMovements(caseLoadId), - prisonApi.getEnrouteRollCount(caseLoadId), - prisonApi.getLocationsForPrison(caseLoadId), - ]) - - const cellSwapLocation = caseLoadLocations.find(location => location.description === 'CSWAP') - - const cellSwapDetails = cellSwapLocation - ? await prisonApi.getAttributesForLocation(cellSwapLocation.locationId) - : { noOfOccupants: 0 } - - const wingsSpursLandingsAssigned = splitRollBlocks(assignedRollBlocksCounts) - const assignedWingsRollCount = wingsSpursLandingsAssigned.wings - - const unassignedIn = - getTotals(unassignedRollBlocksCount, 'currentlyInCell') + getTotals(unassignedRollBlocksCount, 'outOfLivingUnits') - const currentRoll = - getTotals(assignedWingsRollCount, 'currentlyInCell') + - getTotals(assignedWingsRollCount, 'outOfLivingUnits') + - unassignedIn + const rollCount = await prisonApi.getPrisonRollCount(caseLoadId) return { todayStats: { - unlockRoll: currentRoll - movementsCount.in + movementsCount.out, - inToday: movementsCount.in, - outToday: movementsCount.out, - currentRoll, - unassignedIn, - enroute: enrouteCount, - noCellAllocated: cellSwapDetails?.noOfOccupants ?? 0, - totalCurrentlyOut: getTotals(assignedWingsRollCount, 'currentlyOut') ?? 0, - bedsInUse: getTotals(assignedWingsRollCount, 'bedsInUse') ?? 0, - currentlyInCell: getTotals(assignedWingsRollCount, 'currentlyInCell') ?? 0, - operationalCapacity: getTotals(assignedWingsRollCount, 'operationalCapacity') ?? 0, - netVacancies: getTotals(assignedWingsRollCount, 'netVacancies') ?? 0, - outOfOrder: getTotals(assignedWingsRollCount, 'outOfOrder') ?? 0, + unlockRoll: rollCount.numUnlockRollToday, + inToday: rollCount.numArrivedToday, + outToday: rollCount.numOutToday, + currentRoll: rollCount.numCurrentPopulation, + unassignedIn: rollCount.numInReception, + enroute: rollCount.numStillToArrive, + noCellAllocated: rollCount.numNoCellAllocated, }, - assignedRollBlocksCounts: nestRollBlocks(wingsSpursLandingsAssigned), + totals: rollCount.totals, + wings: rollCount.locations, } } diff --git a/server/services/interfaces/establishmentRollService/EstablishmentRollCount.ts b/server/services/interfaces/establishmentRollService/EstablishmentRollCount.ts index 1836c9d..e7f3bf0 100644 --- a/server/services/interfaces/establishmentRollService/EstablishmentRollCount.ts +++ b/server/services/interfaces/establishmentRollService/EstablishmentRollCount.ts @@ -1,4 +1,4 @@ -import { BlockRollCount } from '../../../data/interfaces/blockRollCount' +import PrisonRollCount, { ResidentialLocation } from '../../../data/interfaces/prisonRollCount' export default interface EstablishmentRollCount { todayStats: { @@ -9,23 +9,7 @@ export default interface EstablishmentRollCount { unassignedIn: number enroute: number noCellAllocated: number - totalCurrentlyOut: number - bedsInUse: number - currentlyInCell: number - operationalCapacity: number - netVacancies: number - outOfOrder: number } - assignedRollBlocksCounts: Wing[] -} - -export interface Landing extends BlockRollCount {} - -export interface Spur extends BlockRollCount { - landings?: BlockRollCount[] -} - -export interface Wing extends BlockRollCount { - spurs?: BlockRollCount[] - landings?: BlockRollCount[] + totals: PrisonRollCount['totals'] + wings: ResidentialLocation[] } diff --git a/server/services/utils/nestRollBlocks.test.ts b/server/services/utils/nestRollBlocks.test.ts deleted file mode 100644 index 3aa1a96..0000000 --- a/server/services/utils/nestRollBlocks.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { BlockRollCount } from '../../data/interfaces/blockRollCount' -import nestRollBlocks, { splitRollBlocks } from './nestRollBlocks' - -describe('nestRollBlocks', () => { - describe('when there are wings and landings', () => { - const blocks: Partial[] = [ - { livingUnitId: 1 }, - { livingUnitId: 2 }, - { livingUnitId: 3, parentLocationId: 1 }, - { livingUnitId: 4, parentLocationId: 1 }, - { livingUnitId: 5, parentLocationId: 2 }, - { livingUnitId: 6, parentLocationId: 2 }, - ] - - it('should nest the landings withing the parent wing', () => { - const splitBlocks = splitRollBlocks(blocks as BlockRollCount[]) - const response = nestRollBlocks(splitBlocks) - expect(response).toEqual([ - { - livingUnitId: 1, - landings: [ - { livingUnitId: 3, parentLocationId: 1 }, - { livingUnitId: 4, parentLocationId: 1 }, - ], - }, - { - livingUnitId: 2, - landings: [ - { livingUnitId: 5, parentLocationId: 2 }, - { livingUnitId: 6, parentLocationId: 2 }, - ], - }, - ]) - }) - }) - - describe('when there are wings, spurs and landings', () => { - const blocks: Partial[] = [ - { livingUnitId: 1 }, - { livingUnitId: 2 }, - { livingUnitId: 3, parentLocationId: 1 }, - { livingUnitId: 4, parentLocationId: 1 }, - { livingUnitId: 5, parentLocationId: 2 }, - { livingUnitId: 6, parentLocationId: 2 }, - { livingUnitId: 7, parentLocationId: 3 }, - { livingUnitId: 8, parentLocationId: 4 }, - { livingUnitId: 9, parentLocationId: 5 }, - { livingUnitId: 10, parentLocationId: 6 }, - ] - - it('should nest the wings, spurs and landings', () => { - const splitBlocks = splitRollBlocks(blocks as BlockRollCount[]) - const response = nestRollBlocks(splitBlocks) - expect(response).toEqual([ - { - livingUnitId: 1, - spurs: [ - { - livingUnitId: 3, - parentLocationId: 1, - landings: [{ livingUnitId: 7, parentLocationId: 3 }], - }, - { - livingUnitId: 4, - parentLocationId: 1, - landings: [{ livingUnitId: 8, parentLocationId: 4 }], - }, - ], - }, - { - livingUnitId: 2, - spurs: [ - { - livingUnitId: 5, - parentLocationId: 2, - landings: [{ livingUnitId: 9, parentLocationId: 5 }], - }, - { - livingUnitId: 6, - parentLocationId: 2, - landings: [{ livingUnitId: 10, parentLocationId: 6 }], - }, - ], - }, - ]) - }) - }) - - describe('when there is a mixture of wings, spurs and landings', () => { - const blocks: Partial[] = [ - { livingUnitId: 1 }, - { livingUnitId: 2 }, - { livingUnitId: 3, parentLocationId: 1 }, - { livingUnitId: 4, parentLocationId: 1 }, - { livingUnitId: 5, parentLocationId: 2 }, - { livingUnitId: 6, parentLocationId: 2 }, - { livingUnitId: 7, parentLocationId: 5 }, - { livingUnitId: 8, parentLocationId: 5 }, - ] - - it('should nest the wings, spurs and landings', () => { - const splitBlocks = splitRollBlocks(blocks as BlockRollCount[]) - const response = nestRollBlocks(splitBlocks) - expect(response).toEqual([ - { - livingUnitId: 1, - landings: [ - { livingUnitId: 3, parentLocationId: 1 }, - { livingUnitId: 4, parentLocationId: 1 }, - ], - }, - { - livingUnitId: 2, - spurs: [ - { - livingUnitId: 5, - parentLocationId: 2, - landings: [ - { livingUnitId: 7, parentLocationId: 5 }, - { livingUnitId: 8, parentLocationId: 5 }, - ], - }, - ], - landings: [{ livingUnitId: 6, parentLocationId: 2 }], - }, - ]) - }) - }) -}) diff --git a/server/services/utils/nestRollBlocks.ts b/server/services/utils/nestRollBlocks.ts deleted file mode 100644 index 37ba948..0000000 --- a/server/services/utils/nestRollBlocks.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BlockRollCount } from '../../data/interfaces/blockRollCount' -import { Landing, Spur, Wing } from '../interfaces/establishmentRollService/EstablishmentRollCount' - -const blockHasParent = (block: BlockRollCount) => !!block.parentLocationId -const blockHasChildren = (block: BlockRollCount, allBlocks: BlockRollCount[]) => - !!allBlocks.find(b => b.parentLocationId === block.livingUnitId) - -interface WingsSpursLandings { - wings: Wing[] - spurs: Spur[] - landings: Landing[] -} - -export const splitRollBlocks = (rollBlocks: BlockRollCount[]): WingsSpursLandings => { - return rollBlocks.reduce( - (acc, block) => { - if (!blockHasParent(block)) return { ...acc, wings: [...acc.wings, { ...block }] } - if (blockHasChildren(block, rollBlocks)) return { ...acc, spurs: [...acc.spurs, { ...block }] } - return { ...acc, landings: [...acc.landings, { ...block }] } - }, - { wings: [], spurs: [], landings: [] }, - ) -} - -export default ({ wings, spurs, landings }: WingsSpursLandings): Wing[] => { - const spursWithLandings = spurs.map(spur => { - const spurLandings = landings.filter(landing => landing.parentLocationId === spur.livingUnitId) - return { ...spur, landings: spurLandings } - }) - - return wings.map(wing => { - const wingSpurs = spursWithLandings.filter(spur => spur.parentLocationId === wing.livingUnitId) - const wingWithSpurs = wingSpurs.length ? { ...wing, spurs: wingSpurs } : wing - - const wingLandings = landings.filter(landing => landing.parentLocationId === wing.livingUnitId) - return wingLandings.length ? { ...wingWithSpurs, landings: wingLandings } : wingWithSpurs - }) -} diff --git a/server/test/mocks/prisonApiClientMock.ts b/server/test/mocks/prisonApiClientMock.ts index d409111..c30ec3e 100644 --- a/server/test/mocks/prisonApiClientMock.ts +++ b/server/test/mocks/prisonApiClientMock.ts @@ -4,8 +4,6 @@ const prisonApiClientMock: PrisonApiClient = { getUserCaseLoads: jest.fn(), getUserLocations: jest.fn(), getRollCount: jest.fn(), - getEnrouteRollCount: jest.fn(), - getLocationsForPrison: jest.fn(), getAttributesForLocation: jest.fn(), getMovements: jest.fn(), getStaffRoles: jest.fn(), @@ -21,6 +19,7 @@ const prisonApiClientMock: PrisonApiClient = { getUserDetailsList: jest.fn(), getPrisonersCurrentlyOutOfLivingUnit: jest.fn(), getPrisonersCurrentlyOutOfPrison: jest.fn(), + getPrisonRollCount: jest.fn(), } export default prisonApiClientMock diff --git a/server/views/pages/establishmentRoll.njk b/server/views/pages/establishmentRoll.njk index 576aa09..aab3cff 100644 --- a/server/views/pages/establishmentRoll.njk +++ b/server/views/pages/establishmentRoll.njk @@ -5,6 +5,8 @@ {% set pageTitle = "Establishment roll" %} {% set mainClasses = "govuk-body govuk-main-wrapper--auto-spacing" %} {% set todayStats = establishmentRollCounts.todayStats %} +{% set totals = establishmentRollCounts.totals %} +{% set wings = establishmentRollCounts.wings %} {% set breadCrumbs = [ { @@ -15,26 +17,26 @@ {% macro blockRow(block, type, wing, spur, lastInGroup) %} {% if type === "LANDING" %} - {{ block.livingUnitDesc }} + {{ block.localName or block.locationCode }} {% else %} - {{ block.livingUnitDesc }} + {{ block.localName or block.locationCode }} {% endif %} - {{ block.bedsInUse }} - {{ block.currentlyInCell }} + {{ block.rollCount.bedsInUse }} + {{ block.rollCount.currentlyInCell }} - {% if block.currentlyOut > 0 %} - {{block.currentlyOut}} + {% if block.rollCount.currentlyOut > 0 %} + {{block.rollCount.currentlyOut}} {% else %} 0 {% endif %} - {{ block.operationalCapacity }} - {{ block.netVacancies }} - {{ block.outOfOrder }} + {{ block.rollCount.workingCapacity }} + {{ block.rollCount.netVacancies }} + {{ block.rollCount.outOfOrder }} {% endmacro %} @@ -136,33 +138,35 @@ - {% for wing in establishmentRollCounts.assignedRollBlocksCounts %} + {% for wing in wings %} {{ blockRow(block=wing) }} - {% for spur in wing.spurs %} - {{ blockRow(block=spur, type='SPUR', wing=wing.livingUnitId) }} - {% for landing in spur.landings %} - {{ blockRow(block=landing, type='LANDING', wing=wing.livingUnitId, spur=spur.livingUnitId, lastInGroup=loop.index === spur.landings.length) }} - {% endfor %} - {% endfor %} - + {% for subLocation in wing.subLocations %} + {% set style = 'SPUR' if subLocation.subLocations.length else 'LANDING' %} - {% for landing in wing.landings %} - {{ blockRow(block=landing, type='LANDING', wing=wing.livingUnitId, lastInGroup=loop.index === wing.landings.length) }} + {% if style === 'SPUR' %} + {{ blockRow(block=subLocation, type='SPUR', wing=wing.locationId) }} + {% for landing in subLocation.subLocations %} + {{ blockRow(block=landing, type='LANDING', wing=wing.locationId, spur=subLocation.locationId, lastInGroup=loop.index === subLocation.subLocations.length) }} + {% endfor %} + {% else %} + {{ blockRow(block=subLocation, type='LANDING', wing=wing.locationId, lastInGroup=loop.index === wing.subLocations.length) }} + {% endif %} {% endfor %} + {% endfor %} Totals - {{ todayStats.bedsInUse }} - {{ todayStats.currentlyInCell }} + {{ totals.bedsInUse }} + {{ totals.currentlyInCell }} - {% if todayStats.totalCurrentlyOut > 0 %} - {{todayStats.totalCurrentlyOut}} + {% if totals.currentlyOut > 0 %} + {{totals.currentlyOut}} {% else %} 0 {% endif %} - {{ todayStats.operationalCapacity }} - {{ todayStats.netVacancies }} - {{ todayStats.outOfOrder }} + {{ totals.workingCapacity }} + {{ totals.netVacancies }} + {{ totals.outOfOrder }}