From 1947cf06078bcda6905cdb5aca4ccf8b4619fbd7 Mon Sep 17 00:00:00 2001 From: Fred Marecesche Date: Thu, 9 Jan 2025 12:22:32 +0000 Subject: [PATCH] Update booking confirmation screen This does a few things: - Ensures the booking confirmation flow uses the arrival date and departure date submitted, rather than relying on filter start date and duration - Removes the `apType` parameter as it is not used - Updates the booking confirmation template to show relevent information - Refactors a lot of the flow to present occupancy view with retained filters where relevant --- e2e/pages/match/bookingPage.ts | 5 +- e2e/steps/match.ts | 4 +- integration_tests/mockApis/spaceBooking.ts | 9 - .../admin/placementApplications/listPage.ts | 6 +- .../pages/match/bookASpacePage.ts | 64 +++---- integration_tests/pages/match/searchPage.ts | 4 - integration_tests/tests/match/match.cy.ts | 99 ++++------- server/controllers/match/index.ts | 2 +- .../occupancyViewController.test.ts | 38 ++--- .../occupancyViewController.ts | 29 ++-- .../spaceBookingsController.test.ts | 160 +++++++++++------- .../spaceBookingsController.ts | 98 ++++++----- server/paths/api.ts | 2 +- server/paths/match.ts | 2 +- server/routes/match.ts | 7 +- server/utils/match/index.test.ts | 75 +++++--- server/utils/match/index.ts | 68 +++++--- .../placementRequests/occupancyView/view.njk | 9 +- .../placementRequests/spaceBookings/new.njk | 57 ++----- 19 files changed, 375 insertions(+), 363 deletions(-) diff --git a/e2e/pages/match/bookingPage.ts b/e2e/pages/match/bookingPage.ts index e6e74591e7..e5644b76ed 100644 --- a/e2e/pages/match/bookingPage.ts +++ b/e2e/pages/match/bookingPage.ts @@ -1,12 +1,11 @@ import { Page, expect } from '@playwright/test' import { E2EDatesOfPlacement } from 'e2e/steps/assess' import { BasePage } from '../basePage' -import { Premises } from '../../../server/@types/shared' import { DateFormats } from '../../../server/utils/dateUtils' export class BookingPage extends BasePage { - static async initialize(page: Page, premisesName: Premises['name']) { - await expect(page.locator('h1')).toContainText(`Book space in ${premisesName}`) + static async initialize(page: Page) { + await expect(page.locator('h1')).toContainText('Confirm booking') return new BookingPage(page) } diff --git a/e2e/steps/match.ts b/e2e/steps/match.ts index 0644068e4d..9b94ee6397 100644 --- a/e2e/steps/match.ts +++ b/e2e/steps/match.ts @@ -99,7 +99,7 @@ export const matchAndBookApplication = async ({ await occupancyViewPage.clickContinue() // Then I should see the booking screen for that AP - const bookingPage = await BookingPage.initialize(page, premisesName) + const bookingPage = await BookingPage.initialize(page) // Should show the booking details (inc. new dates) const newDatesOfPlacement: E2EDatesOfPlacement = { @@ -109,7 +109,7 @@ export const matchAndBookApplication = async ({ await bookingPage.shouldShowDatesOfPlacement(newDatesOfPlacement) // And I confirm the booking - const premisesId = page.url().match(/premisesId=(.[^&]*)/)[1] // premisesId=338e22f3-70be-4519-97ab-f08c6c2dfb0b + const premisesId = page.url().match(/space-bookings\/(.[^/]*)/)[1] // Path: /match/placement-requests/:id/space-bookings/:premisesId/new await bookingPage.clickConfirm() // Then I should see the Matched tab on the CRU dashboard diff --git a/integration_tests/mockApis/spaceBooking.ts b/integration_tests/mockApis/spaceBooking.ts index aa975fdc27..81e3b209e3 100644 --- a/integration_tests/mockApis/spaceBooking.ts +++ b/integration_tests/mockApis/spaceBooking.ts @@ -2,7 +2,6 @@ import type { Cas1SpaceBooking, Cas1SpaceBookingResidency, Cas1SpaceBookingSummary, - PlacementRequest, TimelineEvent, } from '@approved-premises/api' @@ -26,14 +25,6 @@ export default { }, }), - verifySpaceBookingCreate: async (placementRequest: PlacementRequest) => - ( - await getMatchingRequests({ - method: 'POST', - url: paths.placementRequests.spaceBookings.create({ id: placementRequest.id }), - }) - ).body.requests, - stubSpaceBookingShow: (placement: Cas1SpaceBooking) => stubFor({ request: { diff --git a/integration_tests/pages/admin/placementApplications/listPage.ts b/integration_tests/pages/admin/placementApplications/listPage.ts index 467247af77..4cd935f859 100644 --- a/integration_tests/pages/admin/placementApplications/listPage.ts +++ b/integration_tests/pages/admin/placementApplications/listPage.ts @@ -24,8 +24,10 @@ export default class ListPage extends Page { return new ListPage() } - shouldShowSpaceBookingConfirmation(premisesName: string, personName: string) { - this.shouldShowBanner(`Space booked for ${personName} in ${premisesName}`) + shouldShowSpaceBookingConfirmation() { + this.shouldShowBanner( + 'You have now booked a place in this AP for this person. An email will be sent to the AP, to inform them of the booking.', + ) } shouldShowPlacementRequests(placementRequests: Array, status?: PlacementRequestStatus): void { diff --git a/integration_tests/pages/match/bookASpacePage.ts b/integration_tests/pages/match/bookASpacePage.ts index 1e8e61deff..7f0d8411bd 100644 --- a/integration_tests/pages/match/bookASpacePage.ts +++ b/integration_tests/pages/match/bookASpacePage.ts @@ -1,54 +1,58 @@ import { - ApType, + Cas1PremisesSummary, Cas1SpaceBookingCharacteristic, - Cas1SpaceCharacteristic, - PlacementDates, PlacementRequestDetail, Premises, } from '@approved-premises/api' +import { differenceInDays } from 'date-fns' import Page from '../page' import paths from '../../../server/paths/match' -import { createQueryString, sentenceCase } from '../../../server/utils/utils' -import { DateFormats } from '../../../server/utils/dateUtils' -import { placementDates, placementLength as placementLengthInDaysAndWeeks } from '../../../server/utils/match' -import { placementCriteriaLabels } from '../../../server/utils/placementCriteriaUtils' -import { apTypeLabels } from '../../../server/utils/apTypeLabels' +import { createQueryString } from '../../../server/utils/utils' +import { DateFormats, daysToWeeksAndDays } from '../../../server/utils/dateUtils' +import { requirementsHtmlString } from '../../../server/utils/match' +import { allReleaseTypes } from '../../../server/utils/applications/releaseTypeUtils' export default class BookASpacePage extends Page { - constructor(premisesName: string) { - super(`Book space in ${premisesName}`) + constructor() { + super(`Confirm booking`) } static visit( placementRequest: PlacementRequestDetail, - startDate: string, - durationDays: PlacementDates['duration'], - premisesName: Premises['name'], premisesId: Premises['id'], - apType: ApType, + arrivalDate: string, + departureDate: string, criteria: Array, ) { - const queryString = createQueryString({ startDate, durationDays, premisesName, premisesId, apType, criteria }) - const path = `${paths.v2Match.placementRequests.spaceBookings.new({ id: placementRequest.id })}?${queryString}` + const queryString = createQueryString({ arrivalDate, departureDate, criteria }) + const path = `${paths.v2Match.placementRequests.spaceBookings.new({ + id: placementRequest.id, + premisesId, + })}?${queryString}` + cy.visit(path) - return new BookASpacePage(premisesName) + + return new BookASpacePage() } shouldShowBookingDetails( placementRequest: PlacementRequestDetail, - startDate: string, - duration: PlacementDates['duration'], - apType: ApType, - criteria?: Array, + premises: Cas1PremisesSummary, + arrivalDate: string, + departureDate: string, + criteria: Array, ): void { - const { endDate, placementLength } = placementDates(startDate, duration.toString()) - cy.get('dd').contains(apTypeLabels[apType]) - cy.get('dd').contains(DateFormats.isoDateToUIDate(startDate)) - cy.get('dd').contains(DateFormats.isoDateToUIDate(endDate)) - cy.get('dd').contains(placementLengthInDaysAndWeeks(placementLength)) - cy.get('dd').contains(sentenceCase(placementRequest.gender)) - ;(criteria || []).forEach(requirement => { - cy.get('li').contains(placementCriteriaLabels[requirement]) - }) + this.shouldContainSummaryListItems([ + { key: { text: 'Approved Premises' }, value: { text: premises.name } }, + // { key: { text: 'Address' }, value: { text: premises.fullAddress } }, + { key: { text: 'Space type' }, value: { html: requirementsHtmlString(criteria) } }, + { key: { text: 'Arrival date' }, value: { text: DateFormats.isoDateToUIDate(arrivalDate) } }, + { key: { text: 'Departure date' }, value: { text: DateFormats.isoDateToUIDate(departureDate) } }, + { + key: { text: 'Length of stay' }, + value: { text: DateFormats.formatDuration(daysToWeeksAndDays(differenceInDays(departureDate, arrivalDate))) }, + }, + { key: { text: 'Release type' }, value: { text: allReleaseTypes[placementRequest.releaseType] } }, + ]) } } diff --git a/integration_tests/pages/match/searchPage.ts b/integration_tests/pages/match/searchPage.ts index 1ebbf628a3..cdbd160925 100644 --- a/integration_tests/pages/match/searchPage.ts +++ b/integration_tests/pages/match/searchPage.ts @@ -73,10 +73,6 @@ export default class SearchPage extends Page { } shouldHaveSearchParametersInLinks(newSearchParameters: SpaceSearchParametersUi): void { - cy.get('.govuk-summary-card__actions .govuk-link') - .invoke('attr', 'href') - .should('contain', `apType=${newSearchParameters.requirements.apType}`) - newSearchParameters.requirements.spaceCharacteristics.forEach(spaceCharacteristic => { const isSpaceBookingCharacteristic = Object.keys(occupancyCriteriaMap).includes(spaceCharacteristic) cy.get('.govuk-summary-card__actions .govuk-link') diff --git a/integration_tests/tests/match/match.cy.ts b/integration_tests/tests/match/match.cy.ts index c2ba75a9fe..55cb7ff746 100644 --- a/integration_tests/tests/match/match.cy.ts +++ b/integration_tests/tests/match/match.cy.ts @@ -1,8 +1,9 @@ import { Cas1PremiseCapacity, Cas1PremisesSummary, + Cas1SpaceBookingCharacteristic, Cas1SpaceSearchParameters, - PlacementCriteria, + FullPerson, } from '@approved-premises/api' import { addDays } from 'date-fns' import SearchPage from '../../pages/match/searchPage' @@ -22,11 +23,12 @@ import Page from '../../pages/page' import { signIn } from '../signIn' import ListPage from '../../pages/admin/placementApplications/listPage' -import { filterOutAPTypes, filterToSpaceBookingCharacteristics, placementDates } from '../../../server/utils/match' +import { filterOutAPTypes } from '../../../server/utils/match' import BookASpacePage from '../../pages/match/bookASpacePage' import OccupancyViewPage from '../../pages/match/occupancyViewPage' import applicationFactory from '../../../server/testutils/factories/application' import DayAvailabilityPage from '../../pages/match/dayAvailabilityPage' +import apiPaths from '../../../server/paths/api' context('Placement Requests', () => { beforeEach(() => { @@ -150,20 +152,6 @@ context('Placement Requests', () => { occupancyViewPage.shouldShowErrorSummaryAndErrorMessage('The departure date must be after the arrival date') }) - it('allows me to submit valid dates in the book your placement form on occupancy view page and redirects to book a space', () => { - const { occupancyViewPage, placementRequest, premises } = - shouldVisitOccupancyViewPageAndShowMatchingDetails(defaultLicenceExpiryDate) - - // When I submit valid dates - const arrivalDate = '2024-11-25' - occupancyViewPage.shouldFillBookYourPlacementFormDates(arrivalDate, '2024-11-26') - occupancyViewPage.clickContinue() - - // Then I should land on the Book a space page (with the new dates overriding the original ones) - const bookASpacePage = Page.verifyOnPage(BookASpacePage, premises.name) - bookASpacePage.shouldShowBookingDetails(placementRequest, arrivalDate, 1, 'normal') - }) - const shouldVisitOccupancyViewPageAndShowMatchingDetails = (licenceExpiryDate: string | undefined) => { const apType = 'normal' const durationDays = 15 @@ -296,49 +284,38 @@ context('Placement Requests', () => { // Given I am signed in as a cru_member signIn(['cru_member'], ['cas1_space_booking_create']) - const premisesName = 'Hope House' - const premisesId = 'abc123' - const apType = 'normal' - const durationDays = 15 - const startDate = '2024-07-23' - const { endDate } = placementDates(startDate, durationDays.toString()) + const premises = cas1PremisesSummaryFactory.build() + cy.task('stubSinglePremises', premises) + + const arrivalDate = '2024-07-23' + const departureDate = '2024-08-08' + const criteria: Array = ['isWheelchairDesignated', 'hasEnSuite'] // And there is a placement request waiting for me to match const person = personFactory.build() - const essentialCharacteristics: Array = ['hasEnSuite'] - const desirableCharacteristics: Array = ['isCatered'] const placementRequest = placementRequestDetailFactory.build({ person, - status: 'notMatched', - duration: durationDays, - essentialCriteria: [], - desirableCriteria: desirableCharacteristics, }) // When I visit the 'Book a space' page cy.task('stubPlacementRequest', placementRequest) - const page = BookASpacePage.visit( - placementRequest, - startDate, - durationDays, - premisesName, - premisesId, - apType, - filterToSpaceBookingCharacteristics(essentialCharacteristics), - ) + const page = BookASpacePage.visit(placementRequest, premises.id, arrivalDate, departureDate, criteria) - // Then I should see the details of the space I am booking - page.shouldShowBookingDetails( - placementRequest, - startDate, - durationDays, - apType, - filterToSpaceBookingCharacteristics(essentialCharacteristics), - ) + // Then I should see the details of the case I am matching + page.shouldShowPersonHeader(placementRequest.person as FullPerson) + + // And I should see the details of the space I am booking + page.shouldShowBookingDetails(placementRequest, premises, arrivalDate, departureDate, criteria) // And when I complete the form - const requirements = spaceBookingRequirementsFactory.build() - const spaceBooking = cas1SpaceBookingFactory.build({ requirements }) + const requirements = spaceBookingRequirementsFactory.build({ + essentialCharacteristics: criteria, + }) + const spaceBooking = cas1SpaceBookingFactory.upcoming().build({ + expectedArrivalDate: arrivalDate, + expectedDepartureDate: departureDate, + requirements, + }) cy.task('stubSpaceBookingCreate', { placementRequestId: placementRequest.id, spaceBooking }) cy.task('stubPlacementRequestsDashboard', { placementRequests: [placementRequest], status: 'matched' }) page.clickSubmit() @@ -347,23 +324,21 @@ context('Placement Requests', () => { const cruDashboard = Page.verifyOnPage(ListPage) // And I should see a success message - cruDashboard.shouldShowSpaceBookingConfirmation(premisesName, person.name) + cruDashboard.shouldShowSpaceBookingConfirmation() // And the booking details should have been sent to the API - cy.task('verifySpaceBookingCreate', placementRequest).then(requests => { - expect(requests).to.have.length(1) - const body = JSON.parse(requests[0].body) - - expect(body).to.deep.equal({ - arrivalDate: startDate, - departureDate: endDate, - premisesId, - requirements: { - ...spaceBooking.requirements, - essentialCharacteristics, - }, - }) - }) + cy.task('verifyApiPost', apiPaths.placementRequests.spaceBookings.create({ id: placementRequest.id })).then( + body => { + expect(body).to.deep.equal({ + arrivalDate, + departureDate, + premisesId: premises.id, + requirements: { + essentialCharacteristics: criteria, + }, + }) + }, + ) }) it('allows me to mark a placement request as unable to match', () => { diff --git a/server/controllers/match/index.ts b/server/controllers/match/index.ts index 0b5ac6c3cc..b373a08260 100644 --- a/server/controllers/match/index.ts +++ b/server/controllers/match/index.ts @@ -19,7 +19,7 @@ export const controllers = (services: Services) => { ) const spaceSearchController = new SpaceSearchController(spaceService, placementRequestService) const placementRequestBookingsController = new BookingsController(placementRequestService) - const spaceBookingsController = new SpaceBookingsController(placementRequestService, spaceService) + const spaceBookingsController = new SpaceBookingsController(placementRequestService, premisesService, spaceService) const occupancyViewController = new OccupancyViewController(placementRequestService, premisesService) return { diff --git a/server/controllers/match/placementRequests/occupancyViewController.test.ts b/server/controllers/match/placementRequests/occupancyViewController.test.ts index 5e393a0ab2..ab863bf72a 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.test.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.test.ts @@ -52,6 +52,7 @@ describe('OccupancyViewController', () => { occupancyViewController = new OccupancyViewController(placementRequestService, premisesService) request = createMock({ user: { token }, + query: {}, flash: flashSpy, headers: { referer: '/referrerPath', @@ -276,18 +277,12 @@ describe('OccupancyViewController', () => { describe('bookSpace', () => { const startDate = '2025-08-15' const durationDays = '22' - const arrivalDay = '11' - const arrivalMonth = '2' - const arrivalYear = '2026' const validBookingBody = { - apType, - startDate, - durationDays, - criteria: '', - 'arrivalDate-day': arrivalDay, - 'arrivalDate-month': arrivalMonth, - 'arrivalDate-year': arrivalYear, + criteria: 'hasEnSuite,isStepFreeDesignated', + 'arrivalDate-day': '11', + 'arrivalDate-month': '2', + 'arrivalDate-year': '2026', 'departureDate-day': '21', 'departureDate-month': '2', 'departureDate-year': '2026', @@ -299,27 +294,30 @@ describe('OccupancyViewController', () => { const requestHandler = occupancyViewController.bookSpace() await requestHandler({ ...request, params, body: validBookingBody }, response, next) - const expectedDurationDays = 10 - const expectedStartDate = `${arrivalYear}-0${arrivalMonth}-${arrivalDay}` - const expectedParams = `apType=${apType}&startDate=${expectedStartDate}&durationDays=${expectedDurationDays}` + const expectedQueryString = + 'arrivalDate=2026-02-11&departureDate=2026-02-21&criteria=hasEnSuite&criteria=isStepFreeDesignated' + expect(response.redirect).toHaveBeenCalledWith( - `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestDetail.id })}?${expectedParams}`, + `${matchPaths.v2Match.placementRequests.spaceBookings.new(params)}?${expectedQueryString}`, ) }) - it(`should redirect to occupancy view and add errors messages when date validation fails`, async () => { + it(`should redirect to occupancy view with existing query string and add errors messages when date validation fails`, async () => { jest.spyOn(validationUtils, 'addErrorMessageToFlash') - const emptyDay = '' const body = { ...validBookingBody, - 'arrivalDate-day': emptyDay, + 'arrivalDate-day': '', criteria: 'isWheelchairDesignated,isSuitedForSexOffenders', } + const query = { + startDate, + durationDays, + } const params = { id: placementRequestDetail.id, premisesId: premises.id } const requestHandler = occupancyViewController.bookSpace() - await requestHandler({ ...request, params, body }, response, next) + await requestHandler({ ...request, params, query, body }, response, next) expect(validationUtils.addErrorMessageToFlash).toHaveBeenCalledWith( request, @@ -327,13 +325,13 @@ describe('OccupancyViewController', () => { 'arrivalDate', ) - const expectedParams = `apType=${apType}&startDate=${startDate}&durationDays=${durationDays}&criteria=isWheelchairDesignated&criteria=isSuitedForSexOffenders` + const expectedQueryString = `startDate=${startDate}&durationDays=${durationDays}&criteria=isWheelchairDesignated&criteria=isSuitedForSexOffenders` expect(response.redirect).toHaveBeenCalledWith( `${matchPaths.v2Match.placementRequests.search.occupancy({ id: placementRequestDetail.id, premisesId: premises.id, - })}?${expectedParams}`, + })}?${expectedQueryString}`, ) }) }) diff --git a/server/controllers/match/placementRequests/occupancyViewController.ts b/server/controllers/match/placementRequests/occupancyViewController.ts index 6e894d50d6..bb9c724884 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.ts @@ -1,6 +1,5 @@ import { Request, Response, TypedRequestHandler } from 'express' import type { ApType, Cas1SpaceBookingCharacteristic } from '@approved-premises/api' -import { differenceInDays } from 'date-fns' import type { ObjectWithDateParts } from '@approved-premises/ui' import { PlacementRequestService, PremisesService } from '../../../services' import { @@ -176,7 +175,11 @@ export default class { bookSpace(): TypedRequestHandler { return async (req: Request, res: Response) => { const { body } = req + const { criteria: criteriaBody } = body + const criteria = criteriaBody.split(',') + const errors = validateSpaceBooking(body) + if (this.hasErrors(errors)) { if (errors.arrivalDate) { addErrorMessageToFlash(req, errors.arrivalDate, 'arrivalDate') @@ -184,14 +187,14 @@ export default class { if (errors.departureDate) { addErrorMessageToFlash(req, errors.departureDate, 'departureDate') } - const { startDate, durationDays, apType, criteria } = body + + const { startDate, durationDays } = req.query const redirectUrl = occupancyViewLink({ placementRequestId: req.params.id, premisesId: req.params.premisesId, - apType, - startDate, - durationDays, - spaceCharacteristics: criteria.split(','), + startDate: startDate as string, + durationDays: durationDays as string, + spaceCharacteristics: criteria, }) res.redirect(redirectUrl) } else { @@ -205,15 +208,11 @@ export default class { ) const redirectUrl = redirectToSpaceBookingsNew({ placementRequestId: req.params.id, - premisesName: body.premisesName, - premisesId: body.premisesId, - apType: body.apType, - startDate: arrivalDate, - durationDays: differenceInDays( - DateFormats.isoToDateObj(departureDate), - DateFormats.isoToDateObj(arrivalDate), - ).toString(), - criteria: body.criteria ? body.criteria : undefined, + premisesId: req.params.premisesId, + ...req.query, + arrivalDate, + departureDate, + criteria, }) res.redirect(redirectUrl) } diff --git a/server/controllers/match/placementRequests/spaceBookingsController.test.ts b/server/controllers/match/placementRequests/spaceBookingsController.test.ts index f8751ae40f..042cf40dbe 100644 --- a/server/controllers/match/placementRequests/spaceBookingsController.test.ts +++ b/server/controllers/match/placementRequests/spaceBookingsController.test.ts @@ -1,98 +1,103 @@ import type { NextFunction, Request, Response } from 'express' import { DeepMocked, createMock } from '@golevelup/ts-jest' -import { faker } from '@faker-js/faker' +import { when } from 'jest-when' import { Cas1SpaceBookingCharacteristic } from '@approved-premises/api' import SpaceBookingsController from './spaceBookingsController' -import { PlacementRequestService, SpaceService } from '../../../services' +import { PlacementRequestService, PremisesService, SpaceService } from '../../../services' import { + cas1PremisesSummaryFactory, cas1SpaceBookingFactory, newSpaceBookingFactory, - personFactory, placementRequestDetailFactory, spaceBookingRequirementsFactory, } from '../../../testutils/factories' -import { filterOutAPTypes, occupancyViewLink, placementDates } from '../../../utils/match' import paths from '../../../paths/admin' -import { fetchErrorsAndUserInput } from '../../../utils/validation' -import { occupancyCriteriaMap } from '../../../utils/match/occupancy' +import matchPaths from '../../../paths/match' +import * as validationUtils from '../../../utils/validation' +import { createQueryString } from '../../../utils/utils' +import { spaceBookingConfirmationSummaryListRows } from '../../../utils/match' -jest.mock('../../../utils/validation') describe('SpaceBookingsController', () => { const token = 'SOME_TOKEN' - const request: DeepMocked = createMock({ user: { token } }) + let request: Readonly> const response: DeepMocked = createMock({}) const next: DeepMocked = createMock({}) const placementRequestService = createMock({}) + const premisesService = createMock({}) const spaceService = createMock({}) + const premises = cas1PremisesSummaryFactory.build() + const placementRequestDetail = placementRequestDetailFactory.build({ duration: 84 }) + let spaceBookingsController: SpaceBookingsController beforeEach(() => { jest.resetAllMocks() - spaceBookingsController = new SpaceBookingsController(placementRequestService, spaceService) + spaceBookingsController = new SpaceBookingsController(placementRequestService, premisesService, spaceService) + request = createMock({ user: { token } }) + + placementRequestService.getPlacementRequest.mockResolvedValue(placementRequestDetail) + premisesService.find.mockResolvedValue(premises) + + jest.spyOn(validationUtils, 'fetchErrorsAndUserInput') + when(validationUtils.fetchErrorsAndUserInput as jest.Mock) + .calledWith(request) + .mockReturnValue({ + errors: {}, + errorSummary: [], + userInput: {}, + }) }) describe('new', () => { - it('should render the new space booking template', async () => { - const person = personFactory.build({ name: 'John Wayne' }) - const placementRequestDetail = placementRequestDetailFactory.build({ person }) - const startDate = '2024-07-26' - const durationDays = '40' - const premisesName = 'Hope House' - const premisesId = 'abc123' - const apType = 'esap' - const criteria = faker.helpers.arrayElements(Object.keys(occupancyCriteriaMap), { - min: 0, - max: 3, - }) as Array - const backLink = occupancyViewLink({ - placementRequestId: placementRequestDetail.id, - premisesId, - apType, - startDate, - durationDays, - spaceCharacteristics: criteria, - }) - ;(fetchErrorsAndUserInput as jest.Mock).mockReturnValue({ errors: [], errorSummary: {}, userInput: {} }) - placementRequestService.getPlacementRequest.mockResolvedValue(placementRequestDetail) + it('should render the new space booking confirmation template', async () => { + const startDate = '2024-07-20' + const durationDays = '84' + const criteria: Array = ['hasEnSuite', 'isArsonSuitable'] + const arrivalDate = '2024-07-26' + const departureDate = '2024-09-04' // 40 days later const query = { startDate, durationDays, - premisesName, - premisesId, - apType, - criteria: criteria.join(','), + criteria, + arrivalDate, + departureDate, } - const params = { id: placementRequestDetail.id } + const params = { id: placementRequestDetail.id, premisesId: premises.id } const requestHandler = spaceBookingsController.new() - request.params = params - request.query = query - await requestHandler(request, response, next) + await requestHandler({ ...request, params, query }, response, next) + + const expectedSubmitLink = `${matchPaths.v2Match.placementRequests.spaceBookings.create(params)}?startDate=2024-07-20&durationDays=84&criteria=hasEnSuite&criteria=isArsonSuitable&arrivalDate=2024-07-26&departureDate=2024-09-04` + const expectedBackLink = `${matchPaths.v2Match.placementRequests.search.occupancy(params)}?startDate=2024-07-20&durationDays=84&criteria=hasEnSuite&criteria=isArsonSuitable` expect(response.render).toHaveBeenCalledWith('match/placementRequests/spaceBookings/new', { - pageHeading: `Book space in ${premisesName}`, + backLink: expectedBackLink, + submitLink: expectedSubmitLink, placementRequest: placementRequestDetail, - premisesName, - premisesId, - apType, - startDate, - durationDays, - errorSummary: {}, - errors: [], - dates: placementDates(startDate, durationDays), - essentialCharacteristics: criteria, - desirableCharacteristics: filterOutAPTypes(placementRequestDetail.desirableCriteria), - backLink, + premises, + arrivalDate, + departureDate, + criteria, + summaryListRows: spaceBookingConfirmationSummaryListRows( + placementRequestDetail, + premises, + arrivalDate, + departureDate, + criteria, + ), + errorSummary: [], + errors: {}, }) expect(placementRequestService.getPlacementRequest).toHaveBeenCalledWith(token, placementRequestDetail.id) + expect(premisesService.find).toHaveBeenCalledWith(token, premises.id) }) }) @@ -101,24 +106,18 @@ describe('SpaceBookingsController', () => { ['empty', { essentialCharacteristics: [] }], ['populated', {}], ])( - 'should call the createSpaceBooking method on the spaceService and redirect the user to the CRU dashboard with characteristics %1', - async (text, requirementsOverride) => { - const personName = 'John Doe' - const premisesName = 'Hope House' - const id = 'placement-request-id' + 'should call the createSpaceBooking method on the spaceService and redirect the user to the CRU dashboard with characteristics %s', + async (_, requirementsOverride) => { const requirements = spaceBookingRequirementsFactory.build(requirementsOverride) - const newSpaceBooking = newSpaceBookingFactory.build({ requirements }) + const newSpaceBooking = newSpaceBookingFactory.build({ premisesId: premises.id, requirements }) const spaceBooking = cas1SpaceBookingFactory.build() const body = { arrivalDate: newSpaceBooking.arrivalDate, departureDate: newSpaceBooking.departureDate, - premisesId: newSpaceBooking.premisesId, - essentialCharacteristics: newSpaceBooking.requirements.essentialCharacteristics.toString(), - personName, - premisesName, + criteria: newSpaceBooking.requirements.essentialCharacteristics.toString(), } - const params = { id } + const params = { id: placementRequestDetail.id, premisesId: premises.id } spaceService.createSpaceBooking.mockResolvedValue(spaceBooking) const flash = jest.fn() @@ -126,10 +125,43 @@ describe('SpaceBookingsController', () => { const requestHandler = spaceBookingsController.create() await requestHandler({ ...request, flash, params, body }, response, next) - expect(spaceService.createSpaceBooking).toHaveBeenCalledWith(token, id, newSpaceBooking) - expect(flash).toHaveBeenCalledWith('success', `Space booked for ${personName} in ${premisesName}`) + expect(spaceService.createSpaceBooking).toHaveBeenCalledWith(token, placementRequestDetail.id, newSpaceBooking) + expect(flash).toHaveBeenCalledWith( + 'success', + `You have now booked a place in this AP for this person. An email will be sent to the AP, to inform them of the booking.`, + ) expect(response.redirect).toHaveBeenCalledWith(`${paths.admin.cruDashboard.index({})}?status=matched`) }, ) + + describe('when errors are raised by the API', () => { + beforeEach(() => { + jest.spyOn(validationUtils, 'catchValidationErrorOrPropogate').mockReturnValue() + }) + + it('redirects to the confirm screen maintaining the query string', async () => { + const body = newSpaceBookingFactory.build() + const params = { id: placementRequestDetail.id, premisesId: premises.id } + const query = { startDate: '2025-06-12', durationDays: '84', criteria: ['hasEnSuite'] } + + const apiError = new Error() + spaceService.createSpaceBooking.mockRejectedValue(apiError) + + const requestHandler = spaceBookingsController.create() + await requestHandler({ ...request, params, query, body }, response, next) + + const expectedRedirect = `${matchPaths.v2Match.placementRequests.spaceBookings.new({ + id: placementRequestDetail.id, + premisesId: premises.id, + })}?${createQueryString(query)}` + + expect(validationUtils.catchValidationErrorOrPropogate).toHaveBeenCalledWith( + request, + response, + apiError, + expectedRedirect, + ) + }) + }) }) }) diff --git a/server/controllers/match/placementRequests/spaceBookingsController.ts b/server/controllers/match/placementRequests/spaceBookingsController.ts index 55d45ade75..f15f95c997 100644 --- a/server/controllers/match/placementRequests/spaceBookingsController.ts +++ b/server/controllers/match/placementRequests/spaceBookingsController.ts @@ -1,65 +1,70 @@ import type { Request, RequestHandler, Response, TypedRequestHandler } from 'express' -import type { ApType, Cas1NewSpaceBooking, PlacementCriteria } from '@approved-premises/api' -import { PlacementRequestService, SpaceService } from '../../../services' -import { - filterOutAPTypes, - filterToSpaceBookingCharacteristics, - occupancyViewLink, - placementDates, -} from '../../../utils/match' +import type { Cas1NewSpaceBooking, Cas1SpaceBookingCharacteristic } from '@approved-premises/api' +import { PlacementRequestService, PremisesService, SpaceService } from '../../../services' import { catchValidationErrorOrPropogate, fetchErrorsAndUserInput } from '../../../utils/validation' import paths from '../../../paths/admin' import matchPaths from '../../../paths/match' import { createQueryString } from '../../../utils/utils' +import { occupancyViewLink, spaceBookingConfirmationSummaryListRows } from '../../../utils/match' interface NewRequest extends Request { - params: { id: string } + params: { id: string; premisesId: string } query: { - startDate: string - durationDays: string - premisesName: string - premisesId: string - apType: ApType - criteria: string + arrivalDate: string + departureDate: string + criteria?: Array + startDate?: string + durationDays?: string } } export default class { constructor( private readonly placementRequestService: PlacementRequestService, + private readonly premisesService: PremisesService, private readonly spaceService: SpaceService, ) {} new(): TypedRequestHandler { return async (req: NewRequest, res: Response) => { - const placementRequest = await this.placementRequestService.getPlacementRequest(req.user.token, req.params.id) - const { startDate, durationDays, premisesName, premisesId, apType, criteria } = req.query + const { token } = req.user + const { id, premisesId } = req.params + const { arrivalDate, departureDate, criteria: criteriaParams, startDate, durationDays } = req.query + const criteria = [criteriaParams].flat() as Array + + const placementRequest = await this.placementRequestService.getPlacementRequest(token, id) + const premises = await this.premisesService.find(token, premisesId) + const { errors, errorSummary } = fetchErrorsAndUserInput(req) - const essentialCharacteristics = filterToSpaceBookingCharacteristics( - (criteria ? criteria.split(',') : []) as Array, - ) + + const submitLink = `${matchPaths.v2Match.placementRequests.spaceBookings.create(req.params)}?${createQueryString(req.query)}` const backLink = occupancyViewLink({ - placementRequestId: placementRequest.id, + placementRequestId: id, premisesId, - apType, startDate, durationDays, - spaceCharacteristics: essentialCharacteristics, + spaceCharacteristics: criteria, }) + + const summaryListRows = spaceBookingConfirmationSummaryListRows( + placementRequest, + premises, + arrivalDate, + departureDate, + criteria, + ) + res.render('match/placementRequests/spaceBookings/new', { - pageHeading: `Book space in ${premisesName}`, + backLink, + submitLink, placementRequest, - premisesName, - premisesId, - apType, - startDate, - durationDays, - dates: placementDates(startDate, durationDays), - essentialCharacteristics, - desirableCharacteristics: filterOutAPTypes(placementRequest.desirableCriteria), + premises, + arrivalDate, + departureDate, + criteria, + summaryListRows, errors, errorSummary, - backLink, }) } } @@ -67,7 +72,9 @@ export default class { create(): RequestHandler { return async (req: Request, res: Response) => { const { - body: { arrivalDate, departureDate, durationDays, premisesId, premisesName, essentialCharacteristics, apType }, + body: { arrivalDate, departureDate, criteria }, + params: { id, premisesId }, + user: { token }, } = req const newSpaceBooking: Cas1NewSpaceBooking = { @@ -75,26 +82,25 @@ export default class { departureDate, premisesId, requirements: { - essentialCharacteristics: essentialCharacteristics ? essentialCharacteristics.split(',') : [], + essentialCharacteristics: criteria ? criteria.split(',') : [], }, } try { - await this.spaceService.createSpaceBooking(req.user.token, req.params.id, newSpaceBooking) - req.flash('success', `Space booked for ${req.body.personName} in ${req.body.premisesName}`) + await this.spaceService.createSpaceBooking(token, id, newSpaceBooking) + req.flash( + 'success', + 'You have now booked a place in this AP for this person. An email will be sent to the AP, to inform them of the booking.', + ) return res.redirect(`${paths.admin.cruDashboard.index({})}?status=matched`) } catch (error) { - const queryString = createQueryString({ - startDate: arrivalDate, - durationDays, - premisesName, - premisesId, - apType, - }) return catchValidationErrorOrPropogate( req, res, - error as Error, - `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: req.params.id })}?${queryString}`, + error, + `${matchPaths.v2Match.placementRequests.spaceBookings.new({ + id, + premisesId, + })}?${createQueryString(req.query)}`, ) } } diff --git a/server/paths/api.ts b/server/paths/api.ts index a338611b23..7df5bead03 100644 --- a/server/paths/api.ts +++ b/server/paths/api.ts @@ -13,7 +13,7 @@ const cas1OutOfServiceBedsSingle = cas1OutOfServiceBeds.path(':id') const cas1SpaceBookingSingle = cas1PremisesSingle.path('space-bookings/:placementId') const cas1Capacity = cas1PremisesSingle.path('capacity') -const cas1SpaceBookings = cas1Namespace.path('/placement-requests/:id/space-bookings') +const cas1SpaceBookings = cas1Namespace.path('placement-requests/:id/space-bookings') const cas1Reports = cas1Namespace.path('reports') diff --git a/server/paths/match.ts b/server/paths/match.ts index 1e7773059e..cd0738b003 100644 --- a/server/paths/match.ts +++ b/server/paths/match.ts @@ -5,7 +5,7 @@ const v2PlacementRequestsPath = v2MatchPath.path('placement-requests') const v2PlacementRequestPath = v2PlacementRequestsPath.path(':id') const v2PlacementRequestSearchPath = v2PlacementRequestPath.path('space-search') const v2PlacementRequestSearchOccupancyPath = v2PlacementRequestSearchPath.path('occupancy/:premisesId') -const v2SpaceBookingsPath = v2PlacementRequestPath.path('space-bookings') +const v2SpaceBookingsPath = v2PlacementRequestPath.path('space-bookings/:premisesId') const v2Match = { placementRequests: { diff --git a/server/routes/match.ts b/server/routes/match.ts index 8a68f63c9a..0822d086c8 100644 --- a/server/routes/match.ts +++ b/server/routes/match.ts @@ -43,10 +43,6 @@ export default function routes(controllers: Controllers, router: Router, service auditEvent: 'CREATE_BOOKING_FROM_PLACEMENT_REQUEST', }) - get(paths.v2Match.placementRequests.spaceBookings.new.pattern, spaceBookingsController.new(), { - auditEvent: 'NEW_SPACE_BOOKING', - }) - get(paths.v2Match.placementRequests.search.occupancy.pattern, occupancyViewController.view(), { auditEvent: 'OCCUPANCY_VIEW', }) @@ -58,6 +54,9 @@ export default function routes(controllers: Controllers, router: Router, service auditEvent: 'OCCUPANCY_VIEW_DAY', }) + get(paths.v2Match.placementRequests.spaceBookings.new.pattern, spaceBookingsController.new(), { + auditEvent: 'NEW_SPACE_BOOKING', + }) post(paths.v2Match.placementRequests.spaceBookings.create.pattern, spaceBookingsController.create(), { auditEvent: 'CREATE_SPACE_BOOKING', }) diff --git a/server/utils/match/index.test.ts b/server/utils/match/index.test.ts index 287cabe1d2..1115fe7d6b 100644 --- a/server/utils/match/index.test.ts +++ b/server/utils/match/index.test.ts @@ -9,6 +9,7 @@ import { when } from 'jest-when' import type { SummaryListItem } from '@approved-premises/ui' import paths from '../../paths/match' import { + cas1PremisesSummaryFactory, personFactory, placementRequestDetailFactory, premisesFactory, @@ -54,6 +55,7 @@ import { redirectToSpaceBookingsNew, releaseTypeRow, requirementsHtmlString, + spaceBookingConfirmationSummaryListRows, spaceBookingPersonNeedsSummaryCardRows, spaceBookingPremisesSummaryCardRows, spaceRequirementsRow, @@ -62,7 +64,6 @@ import { totalCapacityRow, } from '.' import { placementCriteriaLabels } from '../placementCriteriaUtils' -import { createQueryString } from '../utils' import * as formUtils from '../formUtils' import { retrieveOptionalQuestionResponseFromFormArtifact } from '../retrieveQuestionResponseFromFormArtifact' import PreferredAps from '../../form-pages/apply/risk-and-need-factors/location-factors/preferredAps' @@ -71,6 +72,7 @@ import { textValue } from '../applications/helpers' import { preferredApsRow } from '../placementRequests/preferredApsRow' import { placementRequirementsRow } from '../placementRequests/placementRequirementsRow' import applicationFactory from '../../testutils/factories/application' +import { allReleaseTypes } from '../applications/releaseTypeUtils' jest.mock('../retrieveQuestionResponseFromFormArtifact') @@ -258,7 +260,6 @@ describe('matchUtils', () => { it('returns a link to the occupancy view page', () => { const placementRequestId = '123' const premisesId = 'abc' - const apType = 'pipe' const startDate = '2025-04-14' const durationDays = '84' const spaceCharacteristics: Array = ['isWheelchairDesignated', 'isSingle'] @@ -266,7 +267,6 @@ describe('matchUtils', () => { const result = occupancyViewLink({ placementRequestId, premisesId, - apType, startDate, durationDays, spaceCharacteristics, @@ -276,14 +276,13 @@ describe('matchUtils', () => { `${paths.v2Match.placementRequests.search.occupancy({ id: placementRequestId, premisesId, - })}?apType=pipe&startDate=2025-04-14&durationDays=84&criteria=isWheelchairDesignated&criteria=isSingle`, + })}?startDate=2025-04-14&durationDays=84&criteria=isWheelchairDesignated&criteria=isSingle`, ) }) it('filters out non booking-specific search criteria', () => { const placementRequestId = '123' const premisesId = 'abc' - const apType = 'pipe' const startDate = '2025-04-14' const durationDays = '84' const spaceCharacteristics: Array = [ @@ -302,7 +301,6 @@ describe('matchUtils', () => { const result = occupancyViewLink({ placementRequestId, premisesId, - apType, startDate, durationDays, spaceCharacteristics: filterToSpaceBookingCharacteristics(spaceCharacteristics), @@ -312,43 +310,39 @@ describe('matchUtils', () => { `${paths.v2Match.placementRequests.search.occupancy({ id: placementRequestId, premisesId, - })}?apType=pipe&startDate=2025-04-14&durationDays=84&criteria=isWheelchairDesignated&criteria=isSingle&criteria=hasEnSuite&criteria=isArsonSuitable`, + })}?startDate=2025-04-14&durationDays=84&criteria=isWheelchairDesignated&criteria=isSingle&criteria=hasEnSuite&criteria=isArsonSuitable`, ) }) }) describe('redirectToSpaceBookingsNew', () => { - it('returns a link to the confirm page with the premises name and bed', () => { + it('returns a link to the confirm booking page with dates, criteria and existing query parameters', () => { const placementRequestId = '123' - const premisesName = 'Hope House' const premisesId = 'abc' - const apType = 'pipe' - const startDate = '2022-01-01' - const durationDays = '1' - const criteria: Array = ['hasEnSuite', 'isArsonSuitable'] + const arrivalDate = '2022-01-01' + const departureDate = '2022-03-05' + const criteria: Array = ['hasEnSuite', 'isWheelchairDesignated'] + const existingQuery = { + foo: 'bar', + } const result = redirectToSpaceBookingsNew({ placementRequestId, - premisesName, premisesId, - apType, - startDate, - durationDays, + arrivalDate, + departureDate, criteria, + ...existingQuery, }) + const expectedQueryString = + 'arrivalDate=2022-01-01&departureDate=2022-03-05&criteria=hasEnSuite&criteria=isWheelchairDesignated&foo=bar' + expect(result).toEqual( - `${paths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestId })}${createQueryString( - { - premisesName: 'Hope House', - premisesId: 'abc', - apType: 'pipe', - startDate: '2022-01-01', - durationDays: '1', - criteria: ['hasEnSuite', 'isArsonSuitable'], - }, - { addQueryPrefix: true, arrayFormat: 'repeat' }, - )}`, + `${paths.v2Match.placementRequests.spaceBookings.new({ + id: placementRequestId, + premisesId, + })}?${expectedQueryString}`, ) }) }) @@ -524,6 +518,31 @@ describe('matchUtils', () => { }) }) + describe('spaceBookingConfirmationSummaryListRows', () => { + it('returns summary list items for the space booking confirmation screen', () => { + const placementRequest = placementRequestDetailFactory.build() + const premises = cas1PremisesSummaryFactory.build() + const arrivalDate = '2025-05-23' + const departureDate = '2025-07-18' + const criteria: Array = ['hasEnSuite', 'isArsonSuitable'] + + expect( + spaceBookingConfirmationSummaryListRows(placementRequest, premises, arrivalDate, departureDate, criteria), + ).toEqual([ + { key: { text: 'Approved Premises' }, value: { text: premises.name } }, + // { key: { text: 'Address' }, value: { text: premises.fullAddress } }, + { + key: { text: 'Space type' }, + value: { html: '
  • En-suite bathroom
  • Arson offences
' }, + }, + { key: { text: 'Arrival date' }, value: { text: 'Fri 23 May 2025' } }, + { key: { text: 'Departure date' }, value: { text: 'Fri 18 Jul 2025' } }, + { key: { text: 'Length of stay' }, value: { text: '8 weeks' } }, + { key: { text: 'Release type' }, value: { text: allReleaseTypes[placementRequest.releaseType] } }, + ]) + }) + }) + describe('filterOutAPTypes', () => { it('should exclude requirements related to premises type', () => { const requirements: Array = [ diff --git a/server/utils/match/index.ts b/server/utils/match/index.ts index 041cb385e2..97e47491b8 100644 --- a/server/utils/match/index.ts +++ b/server/utils/match/index.ts @@ -1,7 +1,8 @@ -import { addDays } from 'date-fns' +import { addDays, differenceInDays } from 'date-fns' import type { ApType, ApprovedPremisesApplication, + Cas1PremisesSummary, Cas1SpaceBookingCharacteristic, Gender, PlacementCriteria, @@ -17,6 +18,7 @@ import type { SpaceSearchParametersUi, SummaryListItem, } from '@approved-premises/ui' +import { ParsedQs } from 'qs' import { DateFormats, daysToWeeksAndDays } from '../dateUtils' import { createQueryString, sentenceCase } from '../utils' import matchPaths from '../../paths/match' @@ -87,14 +89,12 @@ export const placementLength = (lengthInDays: number): string => { export const occupancyViewLink = ({ placementRequestId, premisesId, - apType, startDate, durationDays, spaceCharacteristics = [], }: { placementRequestId: string premisesId: string - apType: string startDate: string durationDays: string spaceCharacteristics: Array @@ -104,7 +104,6 @@ export const occupancyViewLink = ({ premisesId, })}${createQueryString( { - apType, startDate, durationDays, criteria: spaceCharacteristics, @@ -114,31 +113,27 @@ export const occupancyViewLink = ({ export const redirectToSpaceBookingsNew = ({ placementRequestId, - premisesName, premisesId, - apType, - startDate, - durationDays, + arrivalDate, + departureDate, criteria, -}: { + ...existingQuery +}: ParsedQs & { placementRequestId: string - premisesName: string premisesId: string - apType: string - startDate: string - durationDays: string + arrivalDate: string + departureDate: string criteria: Array }): string => { - return `${matchPaths.v2Match.placementRequests.spaceBookings.new({ id: placementRequestId })}${createQueryString( + return `${matchPaths.v2Match.placementRequests.spaceBookings.new({ + id: placementRequestId, + premisesId, + })}${createQueryString( + { arrivalDate, departureDate, criteria, ...existingQuery }, { - premisesName, - premisesId, - apType, - startDate, - durationDays, - criteria, + addQueryPrefix: true, + arrayFormat: 'repeat', }, - { addQueryPrefix: true, arrayFormat: 'repeat' }, )}` } @@ -195,6 +190,37 @@ export const occupancyViewSummaryListForMatchingDetails = ( ] } +export const textRow = (label: string, value: string): SummaryListItem => ({ + key: { text: label }, + value: { text: value }, +}) + +export const htmlRow = (label: string, value: string): SummaryListItem => ({ + key: { text: label }, + value: { html: value }, +}) + +export const spaceBookingConfirmationSummaryListRows = ( + placementRequest: PlacementRequestDetail, + premises: Cas1PremisesSummary, + arrivalDate: string, + departureDate: string, + criteria: Array, +): Array => { + return [ + textRow('Approved Premises', premises.name), + // textRow('Address', premises.fullAddress), + htmlRow('Space type', requirementsHtmlString(criteria)), + textRow('Arrival date', DateFormats.isoDateToUIDate(arrivalDate)), + textRow('Departure date', DateFormats.isoDateToUIDate(departureDate)), + textRow( + 'Length of stay', + DateFormats.formatDuration(daysToWeeksAndDays(differenceInDays(departureDate, arrivalDate))), + ), + textRow('Release type', allReleaseTypes[placementRequest.releaseType]), + ] +} + export const filterOutAPTypes = (requirements: Array): Array => { return requirements.filter( requirement => diff --git a/server/views/match/placementRequests/occupancyView/view.njk b/server/views/match/placementRequests/occupancyView/view.njk index 9c41be3622..d82501e13e 100644 --- a/server/views/match/placementRequests/occupancyView/view.njk +++ b/server/views/match/placementRequests/occupancyView/view.njk @@ -129,14 +129,9 @@

Book your placement

-
+ - - - - - + {{ govukDateInput({ diff --git a/server/views/match/placementRequests/spaceBookings/new.njk b/server/views/match/placementRequests/spaceBookings/new.njk index e46d718eaf..519d731b97 100644 --- a/server/views/match/placementRequests/spaceBookings/new.njk +++ b/server/views/match/placementRequests/spaceBookings/new.njk @@ -16,52 +16,23 @@ {% block content %} {{ showErrorSummary(errorSummary, errorTitle) }} -
-
-
-

{{ pageHeading }}

- {{ govukSummaryList({ - card: { - title: { - text: "Premises details" - } - }, - classes: 'govuk-summary-list--no-border', - rows: MatchUtils.spaceBookingPremisesSummaryCardRows(premisesName, apType) - }) }} +

Confirm booking

- {{ govukSummaryList({ - card: { - title: { - text: "Booking request" - } - }, - classes: 'govuk-summary-list--no-border', - rows: MatchUtils.spaceBookingPersonNeedsSummaryCardRows(dates, placementRequest.gender, essentialCharacteristics, desirableCharacteristics) - }) }} - -

This booking will be sent to {{ premisesName }} Approved Premises for confirmation.

+ {{ govukSummaryList({ + rows: summaryListRows + }) }} - - + + - - - - - - - - + + + - {{ govukButton({ - text: "Confirm and submit", - preventDoubleClick: true - }) }} - -
-
-
+ {{ govukButton({ + text: "Confirm and submit", + preventDoubleClick: true + }) }} + {% endblock %}