From 123a7e1ba8cf5ddc1cf957eaf8cf20f9aac4aa11 Mon Sep 17 00:00:00 2001 From: Fred Marecesche Date: Thu, 19 Dec 2024 15:28:15 +0000 Subject: [PATCH] Add remaining template elements Commit integration tests. --- .../pages/match/dayAvailabilityPage.ts | 71 ++++++++++++++++ .../pages/match/occupancyViewPage.ts | 11 +++ integration_tests/tests/match/match.cy.ts | 85 ++++++++++--------- .../occupancyViewController.test.ts | 5 ++ .../occupancyViewController.ts | 3 + .../occupancyView/viewDay.njk | 34 +++++++- 6 files changed, 167 insertions(+), 42 deletions(-) create mode 100644 integration_tests/pages/match/dayAvailabilityPage.ts diff --git a/integration_tests/pages/match/dayAvailabilityPage.ts b/integration_tests/pages/match/dayAvailabilityPage.ts new file mode 100644 index 000000000..af8a16655 --- /dev/null +++ b/integration_tests/pages/match/dayAvailabilityPage.ts @@ -0,0 +1,71 @@ +import { Cas1PremiseCapacityForDay, Cas1SpaceBookingCharacteristic } from '@approved-premises/api' +import Page from '../page' +import { dayAvailabilityCount, occupancyCriteriaMap } from '../../../server/utils/match/occupancy' +import { DateFormats } from '../../../server/utils/dateUtils' + +type Availability = 'Available' | 'Overbooked' | 'Available for your criteria' + +export default class DayAvailabilityPage extends Page { + availability: Availability + + constructor( + private readonly dayCapacity: Cas1PremiseCapacityForDay, + private readonly criteria: Array = [], + ) { + let availability: Availability = dayAvailabilityCount(dayCapacity) > 0 ? 'Available' : 'Overbooked' + + if (criteria.length) { + if (dayAvailabilityCount(dayCapacity, criteria) > 0 && availability === 'Overbooked') { + availability = 'Available for your criteria' + } + } + + super(availability) + + this.availability = availability + } + + shouldShowDayAvailability() { + const uiDate = DateFormats.isoDateToUIDate(this.dayCapacity.date, { format: 'long' }) + cy.get('h2').should('contain.text', uiDate) + + if (this.availability === 'Available') { + cy.get('p').should('contain.text', 'The space you require is available.') + } else if (this.availability === 'Overbooked') { + cy.get('p').should('contain.text', 'This AP is full or overbooked. The space you require is not available.') + } else if (this.availability === 'Available for your criteria') { + cy.get('p').should( + 'contain.text', + 'This AP is full or overbooked, but the space you require is available as it is occupied by someone who does not need it.', + ) + } + + const summaryList = { + 'AP capacity': this.dayCapacity.totalBedCount, + 'Booked spaces': this.dayCapacity.bookingCount, + } + + if (this.criteria.length) { + this.criteria.forEach(criteria => { + const criteriaLabel = occupancyCriteriaMap[criteria] + const criteriaAvailability = this.dayCapacity.characteristicAvailability.find( + characteristic => characteristic.characteristic === criteria, + ) + summaryList[`${criteriaLabel} spaces available`] = + criteriaAvailability.availableBedsCount - criteriaAvailability.bookingsCount + }) + } else { + summaryList['Available spaces'] = dayAvailabilityCount(this.dayCapacity) + } + this.shouldContainAvailabilitySummary(summaryList) + } + + shouldContainAvailabilitySummary(items: Record) { + Object.entries(items).forEach(([key, value]) => { + cy.get('.govuk-summary-list__key') + .contains(key) + .siblings('.govuk-summary-list__value') + .should('contain.text', value) + }) + } +} diff --git a/integration_tests/pages/match/occupancyViewPage.ts b/integration_tests/pages/match/occupancyViewPage.ts index f47de2866..140434f3d 100644 --- a/integration_tests/pages/match/occupancyViewPage.ts +++ b/integration_tests/pages/match/occupancyViewPage.ts @@ -1,6 +1,7 @@ import { ApType, Cas1PremiseCapacity, + Cas1PremiseCapacityForDay, Cas1PremisesSummary, Cas1SpaceBookingCharacteristic, PlacementRequest, @@ -113,4 +114,14 @@ export default class OccupancyViewPage extends Page { cy.get('.govuk-error-summary').should('contain', message) cy.get(`.govuk-error-message`).should('contain', message) } + + getOccupancyForDate(date: Date, capacity: Cas1PremiseCapacity): Cas1PremiseCapacityForDay { + return capacity.capacity.find(day => day.date === DateFormats.dateObjToIsoDate(date)) + } + + clickCalendarDay(date: string) { + const calendarDate = DateFormats.isoDateToUIDate(date, { format: 'longNoYear' }) + + cy.get('.calendar__day').contains(calendarDate).click() + } } diff --git a/integration_tests/tests/match/match.cy.ts b/integration_tests/tests/match/match.cy.ts index a8c7e7d11..f618b7300 100644 --- a/integration_tests/tests/match/match.cy.ts +++ b/integration_tests/tests/match/match.cy.ts @@ -1,4 +1,10 @@ -import { Cas1SpaceSearchParameters, PlacementCriteria } from '@approved-premises/api' +import { + Cas1PremiseCapacity, + Cas1PremisesSummary, + Cas1SpaceSearchParameters, + PlacementCriteria, +} from '@approved-premises/api' +import { addDays } from 'date-fns' import SearchPage from '../../pages/match/searchPage' import UnableToMatchPage from '../../pages/match/unableToMatchPage' @@ -19,6 +25,7 @@ import ListPage from '../../pages/admin/placementApplications/listPage' import { filterOutAPTypes, placementDates } from '../../../server/utils/match' import BookASpacePage from '../../pages/match/bookASpacePage' import OccupancyViewPage from '../../pages/match/occupancyViewPage' +import DayAvailabilityPage from '../../pages/match/dayAvailabilityPage' context('Placement Requests', () => { beforeEach(() => { @@ -183,49 +190,45 @@ context('Placement Requests', () => { placementRequest, managerDetails, ) - return { occupancyViewPage, placementRequest, premiseCapacity, premises } + return { occupancyViewPage, placementRequest, premiseCapacity, premises, startDate } } - it('allows me to view spaces and occupancy capacity and filter the result', () => { - const apType = 'normal' - const durationDays = 15 - const startDate = '2024-07-23' - const endDate = '2024-08-07' - const totalCapacity = 10 - const managerDetails = 'John Doe' - - // Given I am signed in as a cru_member - signIn(['cru_member'], ['cas1_space_booking_create']) - - // And there is a placement request waiting for me to match - const person = personFactory.build() - const premises = cas1PremisesSummaryFactory.build({ bedCount: totalCapacity }) - const placementRequest = placementRequestDetailFactory.build({ - person, - expectedArrival: startDate, - duration: durationDays, + const shouldShowDayDetailsAndReturn = ( + occupancyViewPage: OccupancyViewPage, + date: Date, + premises: Cas1PremisesSummary, + premiseCapacity: Cas1PremiseCapacity, + ) => { + const dayCapacity = occupancyViewPage.getOccupancyForDate(date, premiseCapacity) + const premiseCapacityForDay = cas1PremiseCapacityFactory.build({ + premise: premiseCapacity.premise, + startDate: dayCapacity.date, + endDate: dayCapacity.date, + capacity: [dayCapacity], }) - const premiseCapacity = cas1PremiseCapacityFactory.build({ - premise: { id: premises.id, bedCount: totalCapacity, managerDetails }, - startDate, - endDate, + cy.task('stubPremiseCapacity', { + premisesId: premises.id, + startDate: dayCapacity.date, + endDate: dayCapacity.date, + premiseCapacity: premiseCapacityForDay, }) - cy.task('stubSinglePremises', premises) - cy.task('stubPlacementRequest', placementRequest) - cy.task('stubPremiseCapacity', { premisesId: premises.id, startDate, endDate, premiseCapacity }) + // When I click on a day on the calendar + occupancyViewPage.clickCalendarDay(dayCapacity.date) - // When I visit the occupancy view page - const occupancyViewPage = OccupancyViewPage.visit(placementRequest, premises, apType) + // Then I should see the page showing details for the day + const dayAvailabilityPage = new DayAvailabilityPage(dayCapacity) - // Then I should see the details of the case I am matching - occupancyViewPage.shouldShowMatchingDetails( - totalCapacity, - startDate, - durationDays, - placementRequest, - managerDetails, - ) + // And I should see availability details + dayAvailabilityPage.shouldShowDayAvailability() + + // When I click back + dayAvailabilityPage.clickBack() + } + + it('allows me to view spaces and occupancy capacity and filter the result', () => { + const { occupancyViewPage, premiseCapacity, premises, startDate } = + shouldVisitOccupancyViewPageAndShowMatchingDetails() // And I should see the filter form with populated values occupancyViewPage.shouldShowFilters(startDate, 'Up to 6 weeks', []) @@ -236,6 +239,12 @@ context('Placement Requests', () => { // And I should see an occupancy calendar occupancyViewPage.shouldShowOccupancyCalendar(premiseCapacity) + // Then I should see the calendar again + occupancyViewPage.shouldShowOccupancyCalendar(premiseCapacity) + + // And I should be able to see the day's availability details + shouldShowDayDetailsAndReturn(occupancyViewPage, addDays(startDate, 10), premises, premiseCapacity) + // When I filter with an invalid date occupancyViewPage.filterAvailability('2025-02-35') @@ -250,7 +259,7 @@ context('Placement Requests', () => { const newDuration = 'Up to 1 week' const newCriteria = ['Wheelchair accessible', 'Step-free'] const newPremiseCapacity = cas1PremiseCapacityFactory.build({ - premise: { id: premises.id, bedCount: totalCapacity, managerDetails }, + premise: { id: premises.id, bedCount: premises.bedCount }, startDate: newStartDate, endDate: newEndDate, }) diff --git a/server/controllers/match/placementRequests/occupancyViewController.test.ts b/server/controllers/match/placementRequests/occupancyViewController.test.ts index b52088256..5e393a0ab 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.test.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.test.ts @@ -53,6 +53,9 @@ describe('OccupancyViewController', () => { request = createMock({ user: { token }, flash: flashSpy, + headers: { + referer: '/referrerPath', + }, }) placementRequestService.getPlacementRequest.mockResolvedValue(placementRequestDetail) @@ -364,9 +367,11 @@ describe('OccupancyViewController', () => { expect(premisesService.getCapacity).toHaveBeenCalledWith('SOME_TOKEN', premises.id, date) expect(response.render).toHaveBeenCalledWith('match/placementRequests/occupancyView/viewDay', { + backlink: '/referrerPath', pageHeading: dayAvailabilityStatusMap[expectedStatus], placementRequest: placementRequestDetail, premises, + date, status: expectedStatus, availabilitySummaryListItems: dayAvailabilitySummaryListItems(dayCapacity, criteria), }) diff --git a/server/controllers/match/placementRequests/occupancyViewController.ts b/server/controllers/match/placementRequests/occupancyViewController.ts index 3a2460083..4f06c6006 100644 --- a/server/controllers/match/placementRequests/occupancyViewController.ts +++ b/server/controllers/match/placementRequests/occupancyViewController.ts @@ -229,6 +229,7 @@ export default class { const { id, premisesId, date } = req.params const { criteria } = req.query + const backlink = req.headers.referer const placementRequest = await this.placementRequestService.getPlacementRequest(token, id) const premises = await this.premisesService.find(token, premisesId) const premisesCapacity = await this.premisesService.getCapacity(token, premisesId, date) @@ -236,9 +237,11 @@ export default class { const status = dayAvailabilityStatus(dayCapacity, this.criteriaAsArray(criteria)) res.render('match/placementRequests/occupancyView/viewDay', { + backlink, pageHeading: dayAvailabilityStatusMap[status], placementRequest, premises, + date, status, availabilitySummaryListItems: dayAvailabilitySummaryListItems(dayCapacity, this.criteriaAsArray(criteria)), }) diff --git a/server/views/match/placementRequests/occupancyView/viewDay.njk b/server/views/match/placementRequests/occupancyView/viewDay.njk index e3ea38178..8a125b21c 100644 --- a/server/views/match/placementRequests/occupancyView/viewDay.njk +++ b/server/views/match/placementRequests/occupancyView/viewDay.njk @@ -1,13 +1,39 @@ +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} {% extends "../../layout-with-details.njk" %} {% set pageTitle = applicationName + " - " + pageHeading %} +{% block beforeContent %} + {{ govukBackLink({ + text: "Back", + href: backlink + }) }} +{% endblock %} + {% block content %} -

{{ pageHeading }}

+
+
+

{{ pageHeading }}

- {{ govukSummaryList({ - rows: availabilitySummaryListItems - }) }} +

{{ formatDate(date) }}

+ + {% if status === 'available' %} +

The space you require is available.

+ {% endif %} + {% if status === 'availableForCriteria' %} +

This AP is full or overbooked, but the space you require is available as it is occupied by someone + who does not need it.

+ {% endif %} + {% if status === 'overbooked' %} +

This AP is full or overbooked. The space you require is not available.

+ {% endif %} + + {{ govukSummaryList({ + rows: availabilitySummaryListItems + }) }} + +
+
{% endblock %}