Skip to content

Commit

Permalink
APS-1497 Placement details page - Handle offline apps
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Meredith committed Jan 10, 2025
1 parent 5492e9b commit 8db4c32
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 16 deletions.
44 changes: 38 additions & 6 deletions integration_tests/tests/manage/placements/viewPlacement.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,26 @@ import { PlacementShowPage } from '../../../pages/manage'

import { signIn } from '../../signIn'

type Mode = 'normal' | 'lao' | 'offline'
context('Placements', () => {
describe('show', () => {
const setup = (permissions: Array<ApprovedPremisesUserPermission>, placementParameters = {}, lao = false) => {
const setup = (
permissions: Array<ApprovedPremisesUserPermission>,
placementParameters = {},
mode: Mode = 'normal',
) => {
cy.task('reset')
signIn([], permissions)
const premises = premisesSummaryFactory.build()
const person = lao ? restrictedPersonFactory.build() : personFactory.build()
const person = mode === 'lao' ? restrictedPersonFactory.build() : personFactory.build()
const application = applicationFactory.build({ person, personStatusOnSubmission: (person as FullPerson).status })
const assessment = assessmentFactory.build()
const placementRequest = placementRequestFactory.build()
const placement = cas1SpaceBookingFactory.upcoming().build({
...placementParameters,
applicationId: application.id,
assessmentId: assessment.id,
requestForPlacementId: placementRequest.id,
assessmentId: mode === 'offline' ? undefined : assessment.id,
requestForPlacementId: mode === 'offline' ? undefined : placementRequest.id,
premises,
person,
})
Expand Down Expand Up @@ -101,8 +106,8 @@ context('Placements', () => {
})

it('should disable tabs if person is LAO', () => {
// Given that I am logged in with permission to view a placement and a mocked placement
const { placement } = setup(['cas1_space_booking_view'], {}, true)
// Given that I am logged in with permission to view a placement and a mocked placement for a Limited Access person
const { placement } = setup(['cas1_space_booking_view'], {}, 'lao')
// When I visit the placement page
const placementShowPage = PlacementShowPage.visit(placement)
// And I click on the application tab
Expand All @@ -115,8 +120,35 @@ context('Placements', () => {
placementShowPage.shouldHaveActiveTab('Placement details')
// When I select the timeline tab
placementShowPage.clickTab('Timeline')
// Then the timeline tab should be selected
placementShowPage.shouldHaveActiveTab('Timeline')
})

it('should disable tabs if offline application', () => {
// Given that I am logged in with permission to view a placement and a mocked placement for an offline application
const { placement } = setup(['cas1_space_booking_view'], {}, 'offline')
// When I visit the placement page
const placementShowPage = PlacementShowPage.visit(placement)
// Then I should see the offline application warning banner
placementShowPage.shouldShowBanner('This booking was imported from NDelius')
// Then I wil be on the placement details tab
placementShowPage.shouldHaveActiveTab('Placement details')
// When I click on the assessment tab
placementShowPage.clickTab('Assessment')
// Then I should remain on the placement details tab
placementShowPage.shouldHaveActiveTab('Placement details')
// When I click on the Request for placement tab
placementShowPage.clickTab('Request for placement')
// Then I should remain on the Placement details tab
placementShowPage.shouldHaveActiveTab('Placement details')
// When I select the timeline tab
placementShowPage.clickTab('Timeline')
// When the timeline tab should be selected
placementShowPage.shouldHaveActiveTab('Timeline')
// When I select the Application tab
placementShowPage.clickTab('Application')
// When the Application tab should be selected
placementShowPage.shouldHaveActiveTab('Application')
})

it('should select a tab from the path', () => {
Expand Down
33 changes: 30 additions & 3 deletions server/controllers/manage/placementController.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextFunction, Request, Response } from 'express'
import { DeepMocked, createMock } from '@golevelup/ts-jest'

import { TabItem } from 'server/utils/tasks/listTable'
import {
applicationFactory,
assessmentFactory,
Expand Down Expand Up @@ -39,7 +40,7 @@ describe('placementController', () => {
const referrer = 'referrer/path'
const user = { name: 'username' }

const setUp = () => {
const setUp = (offlineApplication = false) => {
jest.resetAllMocks()
jest.useFakeTimers()

Expand All @@ -52,8 +53,8 @@ describe('placementController', () => {
const timeLine = timelineEventFactory.buildList(10)
const placement = cas1SpaceBookingFactory.build({
applicationId: application.id,
assessmentId: assessment.id,
requestForPlacementId: placementRequestDetail.id,
assessmentId: offlineApplication ? undefined : assessment.id,
requestForPlacementId: offlineApplication ? undefined : placementRequestDetail.id,
canonicalArrivalDate: '2024-11-16',
canonicalDepartureDate: '2025-03-26',
})
Expand Down Expand Up @@ -178,5 +179,31 @@ describe('placementController', () => {
expect(placementRequestService.getPlacementRequest).not.toHaveBeenCalled()
expect(placementService.getTimeline).toHaveBeenCalledWith({ token, premisesId, placementId: placement.id })
})

it('should show a banner and disable some tabs if placement is for an offline application', async () => {
const { request, response, placement } = setUp(true)

await placementController.show()(request, response, next)

expect(response.render).toHaveBeenCalledWith(
'manage/premises/placements/show',
expect.objectContaining({
placement,
pageHeading: '16 Nov 2024 to 26 Mar 2025',
user,
backLink: null,
activeTab: 'placement',
isOfflineApplication: true,
}),
)
const renderCall = response.render.mock.calls[0][1] as unknown as { tabItems: Array<TabItem> }
expect(renderCall.tabItems[1]).toEqual(
expect.objectContaining({ text: 'Assessment', classes: 'disabled', href: null }),
)
expect(renderCall.tabItems[2]).toEqual(
expect.objectContaining({ text: 'Request for placement', classes: 'disabled', href: null }),
)
expect(renderCall.tabItems[3]).toEqual(expect.objectContaining({ text: 'Placement details', classes: '' }))
})
})
})
3 changes: 3 additions & 0 deletions server/controllers/manage/placementController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export default class PlacementController {
let application: ApprovedPremisesApplication = null
let assessment: ApprovedPremisesAssessment = null
let placementRequestDetail: PlacementRequestDetail = null
const isOfflineApplication = !placement.assessmentId

if (activeTab === 'timeline') {
timelineEvents = await this.placementService.getTimeline({ token: req.user.token, premisesId, placementId })
}
Expand Down Expand Up @@ -70,6 +72,7 @@ export default class PlacementController {
assessment,
placementRequestDetail,
showTitle: true,
isOfflineApplication,
})
}
}
Expand Down
15 changes: 8 additions & 7 deletions server/utils/placements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,16 +214,17 @@ export const renderKeyworkersSelectOptions = (

export type PlacementTab = 'application' | 'assessment' | 'placementRequest' | 'placement' | 'timeline'

export const tabMap: Record<PlacementTab, { label: string; disableRestricted?: boolean }> = {
export const tabMap: Record<PlacementTab, { label: string; disableRestricted?: boolean; disableOffline?: boolean }> = {
application: { label: 'Application', disableRestricted: true },
assessment: { label: 'Assessment', disableRestricted: true },
placementRequest: { label: 'Request for placement' },
assessment: { label: 'Assessment', disableRestricted: true, disableOffline: true },
placementRequest: { label: 'Request for placement', disableOffline: true },
placement: { label: 'Placement details' },
timeline: { label: 'Timeline' },
}

export const placementTabItems = (placement: Cas1SpaceBooking, activeTab?: PlacementTab): Array<TabItem> => {
const isPersonRestricted = placement.person.type === 'RestrictedPerson'
const isOfflineApplication = !placement.assessmentId
const getSelfLink = (tab: PlacementTab): string => {
const pathRoot = paths.premises.placements
const premisesId = placement.premises.id
Expand All @@ -241,13 +242,13 @@ export const placementTabItems = (placement: Cas1SpaceBooking, activeTab?: Place
return pathRoot.show({ premisesId, placementId })
}
}
return Object.entries(tabMap).map(([key, { label, disableRestricted }]) => {
const isRestricted = isPersonRestricted && disableRestricted
return Object.entries(tabMap).map(([key, { label, disableRestricted, disableOffline }]) => {
const isDisabled = (isPersonRestricted && disableRestricted) || (isOfflineApplication && disableOffline)
return {
text: label,
active: activeTab === key,
href: isRestricted ? null : getSelfLink(key as PlacementTab),
classes: isRestricted ? 'disabled' : '',
href: isDisabled ? null : getSelfLink(key as PlacementTab),
classes: isDisabled ? 'disabled' : '',
}
})
}
Expand Down
12 changes: 12 additions & 0 deletions server/views/manage/premises/placements/show.njk
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
<h1>{{ pageHeading }}</h1>
{% endset %}

{% set offlineAppContent %}
<span class="govuk-heading-s">This booking was imported from NDelius</span>
<span>As such, Assessment and Request for Placement information isn't available.</span>
{% endset %}

{% block content %}

{% include "../../../_messages.njk" %}
Expand All @@ -44,6 +49,13 @@
},
menus: PlacementUtils.actions(placement, user)
}) }}
{% if isOfflineApplication %}
{{ govukNotificationBanner({
html: offlineAppContent,
classes: 'govuk-notification-banner--full-width-content',
titleText: 'Information'
}) }}
{% endif %}
{{ mojSubNavigation({
label: 'Sub navigation',
items: tabItems
Expand Down

0 comments on commit 8db4c32

Please sign in to comment.