Skip to content

Commit

Permalink
PI-2049: Added risk sections
Browse files Browse the repository at this point in the history
  • Loading branch information
pmcphee77 committed Apr 4, 2024
1 parent a65366d commit f633f17
Show file tree
Hide file tree
Showing 22 changed files with 1,270 additions and 2 deletions.
71 changes: 71 additions & 0 deletions integration_tests/e2e/risk.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Page from '../pages/page'
import RiskPage from '../pages/risk'
import RemovedRiskPage from '../pages/removedRisk'
import RemovedRiskDetailPage from '../pages/removedRiskDetail'
import RiskDetailPage from '../pages/riskDetail'

context('Risk', () => {
it('Risk overview page is rendered', () => {
cy.visit('/case/X000001/risk')
const page = Page.verifyOnPage(RiskPage)
page.getRowData('riskOfHarmInCommunity', 'overall', 'Value').should('contain.text', 'HIGH')
page.getRowData('riskOfHarmInCommunity', 'veryHigh', 'Value').should('contain.text', 'Staff')
page.getRowData('riskOfHarmInCommunity', 'high', 'Value').should('contain.text', 'Public')
page.getRowData('riskOfHarmInCommunity', 'medium', 'Value').should('contain.text', 'Known Adult')
page.getRowData('riskOfHarmInCommunity', 'low', 'Value').should('contain.text', 'Children')
page
.getRowData('riskOfHarmInCommunity', 'who', 'Value')
.should('contain.text', 'NOD-849Meaningful content for AssSumm Testing')
page
.getRowData('riskOfHarmInCommunity', 'nature', 'Value')
.should('contain.text', 'NOD-849 Meaningful content for AssSumm Testing')
page
.getRowData('riskOfHarmInCommunity', 'imminence', 'Value')
.should('contain.text', 'NOD-849 R10.3Meaningful content for AssSumm Testing')
page
.getRowData('riskOfHarmToThemselves', 'riskOfSuicideOrSelfHarm', 'Value')
.should('contain.text', 'Immediate concerns about suicide and self-harm')
page
.getRowData('riskOfHarmToThemselves', 'copingInCustody', 'Value')
.should(
'contain.text',
'Immediate concerns about coping in custody and previous concerns about coping in a hostel',
)
page
.getRowData('riskOfHarmToThemselves', 'vulnerability', 'Value')
.should('contain.text', 'Immediate concerns about a vulnerability')
page.getRowData('riskFlags', 'risk1Description', 'Value').should('contain.text', 'Restraining Order')
page.getRowData('riskFlags', 'risk2Description', 'Value').should('contain.text', 'Domestic Abuse Perpetrator')
page.getRowData('riskFlags', 'risk3Description', 'Value').should('contain.text', 'Risk to Known Adult')
})
it('Removed risk page is rendered', () => {
cy.visit('/case/X000001/risk/removed-risk-flags')
const page = Page.verifyOnPage(RemovedRiskPage)
page.getRowData('removedRisks', 'removedRisk1', 'Value').should('contain.text', 'Restraining Order')
page.getRowData('removedRisks', 'removedRisk2', 'Value').should('contain.text', 'Domestic Abuse Perpetrator')
page.getRowData('removedRisks', 'removedRisk3', 'Value').should('contain.text', 'Risk to Known Adult')
})
it('Removed Risk Detail page is rendered', () => {
cy.visit('/case/X000001/risk/flag/4')
const page = Page.verifyOnPage(RemovedRiskDetailPage)
page.getRowData('riskFlagRemoved', 'removalDate', 'Value').should('contain.text', '18 November 2022 by Paul Smith')
page.getRowData('riskFlagRemoved', 'removalNotes', 'Value').should('contain.text', 'Some removal notes')
page.getCardHeader('riskFlag').should('contain.text', 'Before it was removed')
page.getRowData('riskFlag', 'riskFlagNotes', 'Value').should('contain.text', 'Some notes')
page
.getRowData('riskFlag', 'mostRecentReviewDate', 'Value')
.should('contain.text', '12 December 2023 by Paul Smith')
page.getRowData('riskFlag', 'createdDate', 'Value').should('contain.text', '12 December 2023 by Paul Smith')
})
it('Risk Detail page is rendered', () => {
cy.visit('/case/X000001/risk/flag/2')
const page = Page.verifyOnPage(RiskDetailPage)
page.getCardHeader('riskFlag').should('contain.text', 'About this flag')
page.getRowData('riskFlag', 'riskFlagNotes', 'Value').should('contain.text', 'Some notes')
page.getRowData('riskFlag', 'nextReviewDate', 'Value').should('contain.text', '18 August 2025')
page
.getRowData('riskFlag', 'mostRecentReviewDate', 'Value')
.should('contain.text', '18 December 2023 by Paul Smith')
page.getRowData('riskFlag', 'createdDate', 'Value').should('contain.text', '18 December 2022 by Paul Smith')
})
})
7 changes: 7 additions & 0 deletions integration_tests/pages/removedRisk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Page from './page'

export default class RemovedRiskPage extends Page {
constructor() {
super('Removed risk flags')
}
}
7 changes: 7 additions & 0 deletions integration_tests/pages/removedRiskDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Page from './page'

export default class RemovedRiskDetailPage extends Page {
constructor() {
super('Restraining Order')
}
}
7 changes: 7 additions & 0 deletions integration_tests/pages/risk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Page from './page'

export default class RiskPage extends Page {
constructor() {
super('Risk')
}
}
7 changes: 7 additions & 0 deletions integration_tests/pages/riskDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Page from './page'

export default class RiskDetailPage extends Page {
constructor() {
super('Domestic Abuse Perpetrator')
}
}
9 changes: 9 additions & 0 deletions server/data/masApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { AddressOverview, PersonSummary } from './model/common'
import { SentenceDetails } from './model/sentenceDetails'
import { PersonActivity } from './model/activityLog'
import { PersonRiskFlag, PersonRiskFlags } from './model/risk'

export default class MasApiClient extends RestClient {
constructor(token: string) {
Expand Down Expand Up @@ -69,4 +70,12 @@ export default class MasApiClient extends RestClient {
async getPersonActivityLog(crn: string): Promise<PersonActivity> {
return this.get({ path: `/activity/${crn}`, handle404: false })
}

async getPersonRiskFlags(crn: string): Promise<PersonRiskFlags> {
return this.get({ path: `/risk-flags/${crn}`, handle404: false })
}

async getPersonRiskFlag(crn: string, id: string): Promise<PersonRiskFlag> {
return this.get({ path: `/risk-flags/${crn}/${id}`, handle404: false })
}
}
30 changes: 30 additions & 0 deletions server/data/model/risk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Name, PersonSummary } from './common'

export interface PersonRiskFlags {
personSummary: PersonSummary
riskFlags: RiskFlag[]
removedRiskFlags: RiskFlag[]
}

export interface PersonRiskFlag {
personSummary: PersonSummary
riskFlag: RiskFlag
}

export interface RemovalHistory {
notes?: string
removalDate: string
removedBy: Name
}

export interface RiskFlag {
id: number
description: string
notes?: string
nextReviewDate?: string
mostRecentReviewDate?: string
createdDate: string
createdBy: Name
removed: boolean
removalHistory: RemovalHistory[]
}
2 changes: 2 additions & 0 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import personalDetailRoutes from './personalDetails'
import sentenceRoutes from './sentence'
import scheduleRoutes from './schedule'
import activityLogRoutes from './activityLog'
import risksRoute from './risks'

export default function routes(services: Services): Router {
const router = Router()
Expand All @@ -18,6 +19,7 @@ export default function routes(services: Services): Router {
personalDetailRoutes(router, services)
sentenceRoutes(router, services)
scheduleRoutes(router, services)
risksRoute(router, services)
activityLogRoutes(router, services)
return router
}
78 changes: 78 additions & 0 deletions server/routes/risks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { type RequestHandler, Router } from 'express'
import { auditService } from '@ministryofjustice/hmpps-audit-client'
import { v4 } from 'uuid'
import asyncMiddleware from '../middleware/asyncMiddleware'
import type { Services } from '../services'
import MasApiClient from '../data/masApiClient'
import ArnsApiClient from '../data/arnsApiClient'

export default function risksRoute(router: Router, { hmppsAuthClient }: Services) {
const get = (path: string | string[], handler: RequestHandler) => router.get(path, asyncMiddleware(handler))

get('/case/:crn/risk', async (req, res, _next) => {
const { crn } = req.params
const token = await hmppsAuthClient.getSystemClientToken(res.locals.user.username)
const masClient = new MasApiClient(token)
const arnsClient = new ArnsApiClient(token)

await auditService.sendAuditMessage({
action: 'VIEW_MAS_RISKS',
who: res.locals.user.username,
subjectId: crn,
subjectType: 'CRN',
correlationId: v4(),
service: 'hmpps-manage-a-supervision-ui',
})

const [personRisk, risks] = await Promise.all([masClient.getPersonRiskFlags(crn), arnsClient.getRisks(crn)])
res.render('pages/risk', {
personRisk,
risks,
crn,
})
})

get('/case/:crn/risk/flag/:id', async (req, res, _next) => {
const { crn, id } = req.params
const token = await hmppsAuthClient.getSystemClientToken(res.locals.user.username)
const masClient = new MasApiClient(token)

await auditService.sendAuditMessage({
action: 'VIEW_MAS_RISK_DETAIL',
who: res.locals.user.username,
subjectId: crn,
subjectType: 'CRN',
correlationId: v4(),
service: 'hmpps-manage-a-supervision-ui',
})

const personRiskFlag = await masClient.getPersonRiskFlag(crn, id)

res.render('pages/risk/flag', {
personRiskFlag,
crn,
})
})

get('/case/:crn/risk/removed-risk-flags', async (req, res, _next) => {
const { crn } = req.params
const token = await hmppsAuthClient.getSystemClientToken(res.locals.user.username)
const masClient = new MasApiClient(token)

await auditService.sendAuditMessage({
action: 'VIEW_MAS_REMOVED_RISKS',
who: res.locals.user.username,
subjectId: crn,
subjectType: 'CRN',
correlationId: v4(),
service: 'hmpps-manage-a-supervision-ui',
})

const personRisk = await masClient.getPersonRiskFlags(crn)

res.render('pages/risk/removed-risk-flags', {
personRisk,
crn,
})
})
}
4 changes: 4 additions & 0 deletions server/utils/nunjucksSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getCurrentRisksToThemselves,
getPreviousRisksToThemselves,
getRisksToThemselves,
getRisksWithScore,
getTagClass,
govukTime,
initialiseName,
Expand All @@ -28,6 +29,7 @@ import {
lastUpdatedBy,
lastUpdatedDate,
monthsOrDaysElapsed,
removeEmpty,
scheduledAppointments,
toYesNo,
yearsSince,
Expand Down Expand Up @@ -87,6 +89,8 @@ export default function nunjucksSetup(app: express.Express, applicationInfo: App
njkEnv.addFilter('toYesNo', toYesNo)
njkEnv.addFilter('compactActivityLogDate', compactActivityLogDate)
njkEnv.addFilter('activityLogDate', activityLogDate)
njkEnv.addFilter('removeEmpty', removeEmpty)
njkEnv.addGlobal('getRisksWithScore', getRisksWithScore)
njkEnv.addGlobal('activityLog', activityLog)
njkEnv.addGlobal('getRisksToThemselves', getRisksToThemselves)
njkEnv.addGlobal('getCurrentRisksToThemselves', getCurrentRisksToThemselves)
Expand Down
22 changes: 22 additions & 0 deletions server/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import {
fullName,
getAppointmentsToAction,
getRisksToThemselves,
getRisksWithScore,
getTagClass,
govukTime,
initialiseName,
isInThePast,
isToday,
monthsOrDaysElapsed,
pastAppointments,
removeEmpty,
scheduledAppointments,
toYesNo,
yearsSince,
Expand Down Expand Up @@ -340,3 +342,23 @@ describe('filters activity log', () => {
expect(activityLog(a, b)[0]).toEqual(appointment)
})
})

describe('removes empty array', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const arr: never[] = [{ value: 'a value' }, {}]
it.each([['Filters empty object', arr, 1]])('%s removeEmpty(%s, %s)', (_: string, a: never[], size: number) => {
expect(removeEmpty(a).length).toEqual(size)
})
})

describe('removes empty array', () => {
const array: string[] = ['Children', 'Staff']
const risk: Partial<Record<RiskScore, string[]>> = { VERY_HIGH: array }
it.each([['Filters empty object', risk, 'VERY_HIGH', array]])(
'%s activityLog(%s, %s)',
(_: string, a: Partial<Record<RiskScore, string[]>>, b: RiskScore, expected: string[]) => {
expect(getRisksWithScore(a, b)).toEqual(expected)
},
)
})
12 changes: 12 additions & 0 deletions server/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ export const toYesNo = (value: boolean) => {
return 'Yes'
}

export const getRisksWithScore = (risk: Partial<Record<RiskScore, string[]>>, score: RiskScore): string[] => {
const risks: string[] = []
if (risk[score]) {
return risk[score]
}
return risks
}

export const filterEntriesByCategory = (category: string) => {
return function filterActivity(activity: Activity) {
const { isAppointment } = activity
Expand Down Expand Up @@ -303,6 +311,10 @@ export const compactActivityLogDate = (datetimeString: string) => {
return date.toFormat('ccc d MMM yyyy')
}

export const removeEmpty = (array: never[]) => {
return array.filter((value: NonNullable<unknown>) => Object.keys(value).length !== 0)
}

export const activityLog = (contacts: Activity[], category: string) => {
return contacts.filter(filterEntriesByCategory(category)).sort((a, b) => (a.startDateTime < b.startDateTime ? 1 : -1))
}
2 changes: 1 addition & 1 deletion server/views/pages/overview.njk
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
{% set overallRosh %}
{% if hasRiskAssessment %}
<table><tr>
<td class="govuk-tag {{ getTagClass(risks.summary.overallRiskLevel) }}" align="center" width="120">{{ risks.summary.overallRiskLevel | replace('_',' ') }}</td>
<td class="govuk-tag {{ getTagClass(risks.summary.overallRiskLevel) }}" align="center">{{ risks.summary.overallRiskLevel + ' risk of serious harm' | replace('_',' ') }}</td>
</table>
{% else %}
There is no OASys risk assessment
Expand Down
42 changes: 42 additions & 0 deletions server/views/pages/risk.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends "../partials/case.njk" %}
{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %}
{% set pageTitle = applicationName + " - Risk" %}
{% set currentNavSection = 'risk' %}
{% set currentSectionName = 'Risk' %}
{% set headerPersonName = personRisk.personSummary.name | fullName %}
{% set headerCRN = crn %}
{% set hasRiskAssessment = risks.assessedOn %}

{% block beforeContent %}
{{ govukBreadcrumbs({
items: [
{
text: "Your cases",
href: "/search"
},
{
text: headerPersonName,
href: "/case/" + crn
},
{
text: currentSectionName
}
]
}) }}
{% endblock %}

{% block pageContent %}

{% if hasRiskAssessment %}
{% include './risk/_risk-in-the-community.njk' %}
{% include './risk/_risk-to-themselves.njk' %}
{% else %}
{% set html %}
<h2 class="govuk-heading-m">There is no OASys risk assessment for {{ overview.personalDetails.name | fullName }}</h2>
{% include './risk/_no-oasys-risk-assessment.njk' %}
{% endset %}
{{ govukNotificationBanner({ html: html }) }}
{% endif %}
{% include './risk/_risk-flags.njk' %}

{% endblock %}
Loading

0 comments on commit f633f17

Please sign in to comment.