Skip to content

Commit

Permalink
APS-1678 AP day summary view
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Meredith committed Jan 16, 2025
1 parent 0a74d35 commit 74ad06c
Show file tree
Hide file tree
Showing 21 changed files with 558 additions and 19 deletions.
1 change: 1 addition & 0 deletions assets/sass/application.sass
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ $path: "/assets/images/"
@import './components/moj-identity-bar'
@import './components/status-tag'
@import './components/notification-banner'
@import './components/previous-next'

@import './pages/assessments-index'
@import './pages/applications-pages-attach-document'
Expand Down
21 changes: 21 additions & 0 deletions assets/sass/components/_previous-next.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.prev-next {
font-weight: bold;
margin-bottom: govuk-spacing(3);

.prev-next--prev {
margin-right: govuk-spacing(3);

&::before {
content: "\2190";
margin-right: govuk-spacing(2);
font-size:30px;
}
}

.prev-next--next::after {
content: "\2192";
margin-left: govuk-spacing(2);

font-size:30px;
}
}
28 changes: 28 additions & 0 deletions integration_tests/mockApis/premises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
Cas1PremiseCapacity,
Cas1Premises,
Cas1PremisesBasicSummary,
Cas1PremisesDaySummary,
ExtendedPremisesSummary,
ApprovedPremisesSummary as PremisesSummary,
StaffMember,
Expand Down Expand Up @@ -110,11 +111,38 @@ const stubPremiseCapacity = (args: {
},
})

const stubPremiseDaySummary = (args: {
premisesId: string
date: string
premisesDaySummary: Cas1PremisesDaySummary
sortBy?: string
sortDirection?: string
}) => {
const queryString: string = createQueryString({
sortBy: args.sortBy || undefined,
sortDirection: args.sortDirection || undefined,
})
return stubFor({
request: {
method: 'GET',
url: `${paths.premises.daySummary({ premisesId: args.premisesId, date: args.date })}${queryString ? `?${queryString}` : ''}`,
},
response: {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
jsonBody: args.premisesDaySummary,
},
})
}

export default {
stubAllPremises,
stubCas1AllPremises,
stubPremisesSummary,
stubSinglePremises,
stubPremisesStaffMembers,
stubPremiseCapacity,
stubPremiseDaySummary,
}
39 changes: 39 additions & 0 deletions integration_tests/pages/manage/occupancyDayView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Cas1PremisesBasicSummary, Cas1PremisesDaySummary } from '@approved-premises/api'
import Page from '../page'
import { DateFormats } from '../../../server/utils/dateUtils'
import paths from '../../../server/paths/manage'
import { daySummaryRows, generateDaySummaryText } from '../../../server/utils/premises/occupancy'

export default class OccupancyDayViewPage extends Page {
constructor(private pageTitle: string) {
super(pageTitle)
}

static visit(premises: Cas1PremisesBasicSummary, date: string): OccupancyDayViewPage {
cy.visit(paths.premises.occupancy.day({ premisesId: premises.id, date }))
return new OccupancyDayViewPage(DateFormats.isoDateToUIDate(date))
}

shouldShowDaySummaryDetails(premisesDaySummary: Cas1PremisesDaySummary) {
this.shouldContainSummaryListItems(daySummaryRows(premisesDaySummary).rows)
}

shouldShowDaySummaryWarningContent(premisesDaySummary: Cas1PremisesDaySummary) {
const warningContent = generateDaySummaryText(premisesDaySummary.capacity.characteristicAvailability)
if (warningContent) {
this.shouldShowBanner(warningContent)
}
}

shouldNavigateToDay(linkLabel: string, date: string) {
cy.contains(linkLabel).click()
cy.get('h1').contains(DateFormats.isoDateToUIDate(date))
}

static visitUnauthorised(premises: Cas1PremisesBasicSummary, date: string): OccupancyDayViewPage {
cy.visit(paths.premises.occupancy.day({ premisesId: premises.id, date }), {
failOnStatusCode: false,
})
return new OccupancyDayViewPage(`Authorisation Error`)
}
}
67 changes: 67 additions & 0 deletions integration_tests/tests/manage/occupancyDayView.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { faker } from '@faker-js/faker'
import { addDays } from 'date-fns'
import { cas1PremisesBasicSummaryFactory, cas1PremisesDaySummaryFactory } from '../../../server/testutils/factories'

import { signIn } from '../signIn'
import { DateFormats } from '../../../server/utils/dateUtils'
import OccupancyDayViewPage from '../../pages/manage/occupancyDayView'

context('Premises day occupancy', () => {
describe('day', () => {
const dateObj = faker.date.soon()
const date = DateFormats.dateObjToIsoDate(dateObj)

const premisesDaySummary = cas1PremisesDaySummaryFactory.build({ forDate: date })
const premises = cas1PremisesBasicSummaryFactory.build()

beforeEach(() => {
cy.task('reset')

// Given there is a premises in the database
cy.task('stubSinglePremises', premises)
cy.task('stubPremiseDaySummary', { premisesId: premises.id, date, premisesDaySummary })
})

describe('with premises view permission', () => {
beforeEach(() => {
// Given I am logged in as a future manager with premises_view permission
signIn(['future_manager'], ['cas1_premises_view'])
})

it('should show the day summary', () => {
// When I visit premises day summary page
const summaryPage = OccupancyDayViewPage.visit(premises, date)
// I should see the occupancy summary for the day
summaryPage.shouldShowDaySummaryDetails(premisesDaySummary)
// And I should see a warning banner
summaryPage.shouldShowDaySummaryWarningContent(premisesDaySummary)
})

it('should allow navigation to the next day and back again', () => {
const nextDate = DateFormats.dateObjToIsoDate(addDays(dateObj, 1))
const premisesNextDaySummary = cas1PremisesDaySummaryFactory.build({ forDate: nextDate })
cy.task('stubPremiseDaySummary', {
premisesId: premises.id,
date: nextDate,
premisesDaySummary: premisesNextDaySummary,
})
// Given I visit premises day summary page
const summaryPage = OccupancyDayViewPage.visit(premises, date)
// When I click on Next day, Then I navigate to the next day
summaryPage.shouldNavigateToDay('Next day', nextDate)
// When I click on Previous day, Then I navigate back to the date I started on
summaryPage.shouldNavigateToDay('Previous day', date)
})
})

describe('Without premises view permission', () => {
it('should not be availble if the user lacks premises_view permission', () => {
// Given I am logged in as a future manager without premises_view permission
signIn(['future_manager'])
// When I navigate to the view premises occupancy page
// Then I should see an error
OccupancyDayViewPage.visitUnauthorised(premises, date)
})
})
})
})
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { Cas1Premises } from '@approved-premises/api'
import type { Cas1Premises, Cas1PremisesDaySummary } from '@approved-premises/api'
import type { NextFunction, Request, Response } from 'express'
import { DeepMocked, createMock } from '@golevelup/ts-jest'

import { PremisesService } from 'server/services'
import ApOccupancyViewController from './apOccupancyViewController'

import { cas1PremiseCapacityFactory, cas1PremisesFactory } from '../../../testutils/factories'
import {
cas1PremiseCapacityFactory,
cas1PremisesDaySummaryFactory,
cas1PremisesFactory,
} from '../../../testutils/factories'

import paths from '../../../paths/manage'
import { occupancyCalendar } from '../../../utils/premises/occupancy'
import { daySummaryRows, generateDaySummaryText, occupancyCalendar } from '../../../utils/premises/occupancy'
import { DateFormats } from '../../../utils/dateUtils'

describe('AP occupancyViewController', () => {
Expand Down Expand Up @@ -111,4 +115,59 @@ describe('AP occupancyViewController', () => {
expect(premisesService.getCapacity).not.toHaveBeenCalled()
})
})
describe('dayView', () => {
const mockPremises = async (date: string = DateFormats.dateObjToIsoDate(new Date())) => {
const premisesSummary: Cas1Premises = cas1PremisesFactory.build({ id: premisesId })
const premisesDaySummary: Cas1PremisesDaySummary = cas1PremisesDaySummaryFactory.build({ forDate: date })
premisesService.getDaySummary.mockResolvedValue(premisesDaySummary)
premisesService.find.mockResolvedValue(premisesSummary)
request.params.date = date
const requestHandler = occupancyViewController.dayView()
await requestHandler(request, response, next)
return {
premisesSummary,
premisesDaySummary,
}
}

it('should render the premises day summary', async () => {
const date = '2025-01-01'
const { premisesSummary, premisesDaySummary } = await mockPremises(date)

expect(response.render).toHaveBeenCalledWith(
'manage/premises/occupancy/dayView',
expect.objectContaining({
premises: premisesSummary,
pageHeading: DateFormats.isoDateToUIDate(date),
backLink: paths.premises.occupancy.view({ premisesId }),
previousDayLink: paths.premises.occupancy.day({ premisesId, date: '2024-12-31' }),
nextDayLink: paths.premises.occupancy.day({ premisesId, date: '2025-01-02' }),
daySummaryRows: daySummaryRows(premisesDaySummary),
daySummaryText: generateDaySummaryText(premisesDaySummary.capacity.characteristicAvailability),
}),
)
expect(premisesService.find).toHaveBeenCalledWith(token, premisesId)
expect(premisesService.getDaySummary).toHaveBeenCalledWith({ token, premisesId, date })
})

it('should render error if date is invalid', async () => {
request = createMock<Request>({
user: { token },
params: { premisesId, date: '2023-02-29' },
flash: jest.fn(),
})
const requestHandler = occupancyViewController.dayView()
await requestHandler(request, response, next)

expect(response.render).toHaveBeenCalledWith(
'manage/premises/occupancy/dayView',
expect.objectContaining({
backLink: paths.premises.occupancy.view({ premisesId }),
errorSummary: [{ text: 'Date "2023-02-29" is invalid.', href: '#date' }],
}),
)
expect(premisesService.find).toHaveBeenCalledWith(token, premisesId)
expect(premisesService.getDaySummary).not.toHaveBeenCalled()
})
})
})
39 changes: 38 additions & 1 deletion server/controllers/manage/premises/apOccupancyViewController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import type { Request, RequestHandler, Response } from 'express'

import { ObjectWithDateParts } from '@approved-premises/ui'
import { Cas1PremisesDaySummary } from '@approved-premises/api'
import { PremisesService } from '../../../services'

import paths from '../../../paths/manage'
import { Calendar, durationSelectOptions, occupancyCalendar } from '../../../utils/premises/occupancy'
import {
Calendar,
daySummaryRows,
durationSelectOptions,
generateDaySummaryText,
occupancyCalendar,
} from '../../../utils/premises/occupancy'
import { DateFormats, dateAndTimeInputsAreValidDates, daysToWeeksAndDays } from '../../../utils/dateUtils'
import { placementDates } from '../../../utils/match'
import { fetchErrorsAndUserInput, generateErrorMessages, generateErrorSummary } from '../../../utils/validation'
Expand Down Expand Up @@ -61,4 +68,34 @@ export default class ApOccupancyViewController {
})
}
}

dayView(): RequestHandler {
return async (req: Request, res: Response) => {
const { token } = req.user
const { premisesId, date } = req.params
const { errorSummary } = fetchErrorsAndUserInput(req)
try {
DateFormats.isoToDateObj(date)
} catch (e) {
const dateError = { date: `Date "${date}" is invalid.` }
errorSummary.push(generateErrorSummary(dateError)[0])
}
const premises = await this.premisesService.find(token, premisesId)
let daySummary: Cas1PremisesDaySummary
if (!errorSummary.length) {
daySummary = await this.premisesService.getDaySummary({ token, premisesId, date })
}

return res.render('manage/premises/occupancy/dayView', {
premises,
pageHeading: daySummary ? DateFormats.isoDateToUIDate(daySummary.forDate) : '',
backLink: paths.premises.occupancy.view({ premisesId }),
previousDayLink: daySummary && paths.premises.occupancy.day({ premisesId, date: daySummary.previousDate }),
nextDayLink: daySummary && paths.premises.occupancy.day({ premisesId, date: daySummary.nextDate }),
daySummaryRows: daySummary && daySummaryRows(daySummary),
daySummaryText: daySummary && generateDaySummaryText(daySummary.capacity.characteristicAvailability),
errorSummary,
})
}
}
}
Loading

0 comments on commit 74ad06c

Please sign in to comment.