Skip to content

Commit

Permalink
Merge pull request #1972 from ministryofjustice/APS-912-OoS-bed-timeline
Browse files Browse the repository at this point in the history
APS 912 - Out of Service bed timeline
  • Loading branch information
richpjames authored Jul 8, 2024
2 parents 3e17799 + b446ccd commit 6cd4ec9
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 103 deletions.
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# Changes in this PR

- [] I have run the E2E tests locally and they passed
<!-- [] I have run the E2E tests locally and they passed -->

## Screenshots of UI changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import paths from '../../../../server/paths/manage'
import Page from '../../page'
import { DateFormats } from '../../../../server/utils/dateUtils'
import { BedDetail, Cas1OutOfServiceBed as OutOfServiceBed, Premises } from '../../../../server/@types/shared'
import { sentenceCase } from '../../../../server/utils/utils'

export class OutOfServiceBedShowPage extends Page {
constructor(private readonly outOfServiceBed: OutOfServiceBed) {
Expand All @@ -23,16 +24,83 @@ export class OutOfServiceBedShowPage extends Page {

shouldShowOutOfServiceBedDetail(): void {
const latestRevision = this.outOfServiceBed.revisionHistory[0]
this.assertDefinition('Start date', DateFormats.isoDateToUIDate(latestRevision.startDate, { format: 'long' }))
this.assertDefinition('End date', DateFormats.isoDateToUIDate(latestRevision.endDate, { format: 'long' }))
this.assertDefinition('Reason', latestRevision.reason.name)
this.assertDefinition('Reference number', latestRevision.referenceNumber)
this.assertDefinition('Additional information', latestRevision.notes)

if (latestRevision.startDate) {
this.assertDefinition('Start date', DateFormats.isoDateToUIDate(latestRevision.startDate, { format: 'long' }))
}
if (latestRevision.endDate) {
this.assertDefinition('End date', DateFormats.isoDateToUIDate(latestRevision.endDate, { format: 'long' }))
}
if (latestRevision.reason) this.assertDefinition('Reason', latestRevision.reason.name)
if (latestRevision.referenceNumber) this.assertDefinition('Reference number', latestRevision.referenceNumber)
if (latestRevision.notes) this.assertDefinition('Additional information', latestRevision.notes)
}

shouldShowCharacteristics(bed: BedDetail): void {
bed.characteristics.forEach(characteristic => {
cy.get('li').contains(characteristic.name)
})
}

shouldShowTimeline() {
const timelineEvents = this.outOfServiceBed.revisionHistory

cy.get('.moj-timeline').within(() => {
cy.get('.moj-timeline__item').should('have.length', timelineEvents.length)

cy.get('.moj-timeline__item').each(($el, i) => {
cy.wrap($el).within(() => {
// Revision type(s)
timelineEvents[i].revisionType.forEach(element => {
cy.get('.moj-timeline__header').should('contain', sentenceCase(element))
})

// Updated by
if (timelineEvents[i].updatedBy?.name) {
cy.get('.moj-timeline__header > .moj-timeline__byline').should('contain', timelineEvents[i].updatedBy.name)
}

// Date time
cy.get('time').should('have.attr', { time: timelineEvents[i].updatedAt })
cy.get('time').should('contain', DateFormats.isoDateTimeToUIDateTime(timelineEvents[i].updatedAt))

// Revision details
if (timelineEvents[i].startDate) {
cy.get('.govuk-summary-list__key').should('contain', 'Start date')
cy.get('.govuk-summary-list__value').should(
'contain',
DateFormats.isoDateToUIDate(timelineEvents[i].startDate, { format: 'long' }),
)
}

if (timelineEvents[i].endDate) {
cy.get('.govuk-summary-list__key').should('contain', 'End date')
cy.get('.govuk-summary-list__value').should(
'contain',
DateFormats.isoDateToUIDate(timelineEvents[i].endDate, { format: 'long' }),
)
}

if (timelineEvents[i].reason) {
cy.get('.govuk-summary-list__key').should('contain', 'Reason')
cy.get('.govuk-summary-list__value').should('contain', timelineEvents[i].reason.name)
}

if (timelineEvents[i].referenceNumber) {
cy.get('.govuk-summary-list__key').should('contain', 'Reference number')
cy.get('.govuk-summary-list__value').should('contain', timelineEvents[i].referenceNumber)
}

if (timelineEvents[i].notes) {
cy.get('.govuk-summary-list__key').should('contain', 'Notes')
cy.get('.govuk-summary-list__value').should('contain', timelineEvents[i].notes)
}
})
})
})
}

clickTab(tab: 'Details' | 'Timeline'): void {
cy.get('.moj-sub-navigation__link').contains(tab).click()
}
}
80 changes: 63 additions & 17 deletions integration_tests/tests/v2Manage/outOfServiceBeds.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
bookingFactory,
extendedPremisesSummaryFactory,
outOfServiceBedFactory,
outOfServiceBedRevisionFactory,
premisesFactory,
} from '../../../server/testutils/factories'

import { sortOutOfServiceBedRevisionsByUpdatedAt } from '../../../server/utils/outOfServiceBedUtils'
import {
OutOfServiceBedCreatePage,
OutOfServiceBedIndexPage,
Expand Down Expand Up @@ -113,27 +114,72 @@ context('OutOfServiceBeds', () => {
page.shouldShowDateConflictErrorMessages(conflictingBooking, 'booking')
})

it('should show a out of service bed', () => {
// Given I am signed in as a future manager
signIn(['future_manager'])
describe('for a new out of service bed with all nullable fields present in the initial OoS bed revision', () => {
it('should show a out of service bed', () => {
// Given I am signed in as a future manager
signIn(['future_manager'])

// And I have created a out of service bed
const bed = { name: 'abc', id: '123' }
const premises = premisesFactory.build()
const outOfServiceBed = outOfServiceBedFactory.build({ bed })
const bedDetail = bedDetailFactory.build({ id: bed.id })
// And I have created a out of service bed
const bed = { name: 'abc', id: '123' }
const premises = premisesFactory.build()
const outOfServiceBed = outOfServiceBedFactory.build({ bed })
outOfServiceBed.revisionHistory = sortOutOfServiceBedRevisionsByUpdatedAt(outOfServiceBed.revisionHistory)
const bedDetail = bedDetailFactory.build({ id: bed.id })

cy.task('stubOutOfServiceBed', { premisesId: premises.id, outOfServiceBed })
cy.task('stubBed', { premisesId: premises.id, bedDetail })

// And I visit that out of service bed's show page
const page = OutOfServiceBedShowPage.visit(premises.id, outOfServiceBed)

// Then I should see the latest details of that out of service bed
page.shouldShowOutOfServiceBedDetail()

// And I should see the bed characteristics
page.shouldShowCharacteristics(bedDetail)

cy.task('stubOutOfServiceBed', { premisesId: premises.id, outOfServiceBed })
cy.task('stubBed', { premisesId: premises.id, bedDetail })
// When I click the 'Timeline' tab
page.clickTab('Timeline')

// And I visit that out of service bed's show page
const page = OutOfServiceBedShowPage.visit(premises.id, outOfServiceBed)
// Then I should see the timeline of that out of service bed's revision
page.shouldShowTimeline()
})
})

describe('for a legacy "lost bed" records migrated with all nullable fields not present in the initial OoS bed revision', () => {
it('should show a out of service bed', () => {
// Given I am signed in as a future manager
signIn(['future_manager'])

// And I have created a out of service bed
const bed = { name: 'abc', id: '123' }
const premises = premisesFactory.build()
const outOfServiceBedRevision = outOfServiceBedRevisionFactory.build({
updatedBy: undefined,
startDate: undefined,
endDate: undefined,
reason: undefined,
referenceNumber: undefined,
notes: undefined,
})
const outOfServiceBed = outOfServiceBedFactory.build({
bed,
revisionHistory: [outOfServiceBedRevision],
})
const bedDetail = bedDetailFactory.build({ id: bed.id })

cy.task('stubOutOfServiceBed', { premisesId: premises.id, outOfServiceBed })
cy.task('stubBed', { premisesId: premises.id, bedDetail })

// Then I should see the latest details of that out of service bed
page.shouldShowOutOfServiceBedDetail()
// And I visit that out of service bed's show page
const page = OutOfServiceBedShowPage.visit(premises.id, outOfServiceBed)

// And I should see the bed characteristics
page.shouldShowCharacteristics(bedDetail)
// When I click the 'Timeline' tab
page.clickTab('Timeline')

// Then I should see the timeline of that out of service bed's revision
page.shouldShowTimeline()
})
})

describe('CRU Member lists all OOS beds', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('OutOfServiceBedsController', () => {
const errorsAndUserInput = createMock<ErrorsAndUserInput>()
when(fetchErrorsAndUserInput).calledWith(request).mockReturnValue(errorsAndUserInput)
when(outOfServiceBedService.getOutOfServiceBed)
.calledWith(request.user.token, request.params.premisesId, request.params.id)
.calledWith(request.user.token, premisesId, outOfServiceBed.id)
.mockResolvedValue(outOfServiceBed)

const requestHandler = outOfServiceBedController.show()
Expand Down
9 changes: 6 additions & 3 deletions server/controllers/v2Manage/outOfServiceBedsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DateFormats } from '../../utils/dateUtils'
import { SanitisedError } from '../../sanitisedError'
import { getPaginationDetails } from '../../utils/getPaginationDetails'
import { OutOfServiceBedService, PremisesService } from '../../services'
import { sortOutOfServiceBedRevisionsByUpdatedAt } from '../../utils/outOfServiceBedUtils'

export default class OutOfServiceBedsController {
constructor(
Expand Down Expand Up @@ -161,11 +162,13 @@ export default class OutOfServiceBedsController {

show(): RequestHandler {
return async (req: Request, res: Response) => {
const activeTab = req.params.tab
const { premisesId, bedId, id } = req.params
const { premisesId, bedId, id, tab = 'details' } = req.params
const referrer = req.headers.referer

const outOfServiceBed = await this.outOfServiceBedService.getOutOfServiceBed(req.user.token, premisesId, id)

outOfServiceBed.revisionHistory = sortOutOfServiceBedRevisionsByUpdatedAt(outOfServiceBed.revisionHistory)

const { characteristics } = await this.premisesService.getBed(req.user.token, premisesId, bedId)

return res.render('v2Manage/outOfServiceBeds/show', {
Expand All @@ -174,7 +177,7 @@ export default class OutOfServiceBedsController {
bedId,
id,
referrer,
activeTab,
activeTab: tab,
characteristics,
})
}
Expand Down
2 changes: 2 additions & 0 deletions server/testutils/factories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
newOutOfServiceBedFactory,
outOfServiceBedCancellationFactory,
outOfServiceBedFactory,
outOfServiceBedRevisionFactory,
} from './outOfServiceBed'
import paginatedResponseFactory from './paginatedResponse'
import { fullPersonFactory as personFactory, restrictedPersonFactory } from './person'
Expand Down Expand Up @@ -148,6 +149,7 @@ export {
oasysSelectionFactory,
outOfServiceBedFactory,
outOfServiceBedCancellationFactory,
outOfServiceBedRevisionFactory,
newOutOfServiceBedFactory,
paginatedResponseFactory,
personFactory,
Expand Down
2 changes: 2 additions & 0 deletions server/utils/nunjucksSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ export default function nunjucksSetup(app: express.Express, path: pathModule.Pla
njkEnv.addFilter('removeBlankSummaryListItems', removeBlankSummaryListItems)
njkEnv.addFilter('sentenceCase', sentenceCase)
njkEnv.addFilter('kebabCase', kebabCase)
njkEnv.addFilter('addCommasToList', (arg, notLastItem) => (notLastItem ? `${arg}, ` : arg))

njkEnv.addFilter('linebreaksToParagraphs', linebreaksToParagraphs)

njkEnv.addGlobal('numberToOrdinal', numberToOrdinal)
Expand Down
71 changes: 70 additions & 1 deletion server/utils/outOfServiceBedUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { add, sub } from 'date-fns'
import { outOfServiceBedFactory, userDetailsFactory } from '../testutils/factories'
import { outOfServiceBedFactory, outOfServiceBedRevisionFactory, userDetailsFactory } from '../testutils/factories'
import { DateFormats } from './dateUtils'
import {
actionCell,
allOutOfServiceBedsTableHeaders,
allOutOfServiceBedsTableRows,
bedRevisionDetails,
outOfServiceBedCountForToday,
outOfServiceBedTableHeaders,
outOfServiceBedTableRows,
referenceNumberCell,
sortOutOfServiceBedRevisionsByUpdatedAt,
} from './outOfServiceBedUtils'
import { getRandomInt } from './utils'
import { ApprovedPremisesUserRole, Cas1OutOfServiceBedSortField as OutOfServiceBedSortField } from '../@types/shared'
Expand Down Expand Up @@ -193,4 +195,71 @@ describe('outOfServiceBedUtils', () => {
).toEqual(`${outOfServiceBedsForToday.length} beds`)
})
})

describe('bedRevisionDetails', () => {
it('adds a formatted start date the summary list', () => {
const startDate = new Date(2024, 2, 1)
const revision = outOfServiceBedRevisionFactory.build({
startDate: DateFormats.dateObjToIsoDate(startDate),
})

expect(bedRevisionDetails(revision)).toEqual(
expect.arrayContaining([
{ key: { text: 'Start date' }, value: { text: DateFormats.dateObjtoUIDate(startDate) } },
]),
)
})

it('adds a formatted end date the summary list', () => {
const endDate = new Date(2024, 2, 1)
const revision = outOfServiceBedRevisionFactory.build({
endDate: DateFormats.dateObjToIsoDate(endDate),
})

expect(bedRevisionDetails(revision)).toEqual(
expect.arrayContaining([{ key: { text: 'End date' }, value: { text: DateFormats.dateObjtoUIDate(endDate) } }]),
)
})

it('adds a reason the summary list', () => {
const revision = outOfServiceBedRevisionFactory.build({
reason: { id: 'reasonId', name: 'reasonName' },
})

expect(bedRevisionDetails(revision)).toEqual(
expect.arrayContaining([{ key: { text: 'Reason' }, value: { text: revision.reason.name } }]),
)
})

it('adds a reference the summary list', () => {
const revision = outOfServiceBedRevisionFactory.build({
referenceNumber: '123',
})

expect(bedRevisionDetails(revision)).toEqual(
expect.arrayContaining([{ key: { text: 'Reference number' }, value: { text: revision.referenceNumber } }]),
)
})

it('adds a notes item to the summary list', () => {
const revision = outOfServiceBedRevisionFactory.build({ notes: 'some note' })

expect(bedRevisionDetails(revision)).toEqual(
expect.arrayContaining([{ key: { text: 'Notes' }, value: { text: 'some note' } }]),
)
})
})

describe('sortOutOfServiceBedRevisionsByUpdatedAt', () => {
it('sorts revisions by updatedAt in descending order', () => {
const revisions = [
outOfServiceBedRevisionFactory.build({ updatedAt: '2024-01-01T00:00:00Z' }),
outOfServiceBedRevisionFactory.build({ updatedAt: '2024-01-02T00:00:00Z' }),
outOfServiceBedRevisionFactory.build({ updatedAt: '2024-01-03T00:00:00Z' }),
]
const sortedRevisions = sortOutOfServiceBedRevisionsByUpdatedAt(revisions)

expect(sortedRevisions).toEqual([revisions[0], revisions[1], revisions[2]])
})
})
})
Loading

0 comments on commit 6cd4ec9

Please sign in to comment.