diff --git a/integration_tests/e2e/risk.cy.ts b/integration_tests/e2e/risk.cy.ts new file mode 100644 index 00000000..fe0057fd --- /dev/null +++ b/integration_tests/e2e/risk.cy.ts @@ -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') + }) +}) diff --git a/integration_tests/pages/removedRisk.ts b/integration_tests/pages/removedRisk.ts new file mode 100644 index 00000000..977d4871 --- /dev/null +++ b/integration_tests/pages/removedRisk.ts @@ -0,0 +1,7 @@ +import Page from './page' + +export default class RemovedRiskPage extends Page { + constructor() { + super('Removed risk flags') + } +} diff --git a/integration_tests/pages/removedRiskDetail.ts b/integration_tests/pages/removedRiskDetail.ts new file mode 100644 index 00000000..1c2e3fb3 --- /dev/null +++ b/integration_tests/pages/removedRiskDetail.ts @@ -0,0 +1,7 @@ +import Page from './page' + +export default class RemovedRiskDetailPage extends Page { + constructor() { + super('Restraining Order') + } +} diff --git a/integration_tests/pages/risk.ts b/integration_tests/pages/risk.ts new file mode 100644 index 00000000..967a2056 --- /dev/null +++ b/integration_tests/pages/risk.ts @@ -0,0 +1,7 @@ +import Page from './page' + +export default class RiskPage extends Page { + constructor() { + super('Risk') + } +} diff --git a/integration_tests/pages/riskDetail.ts b/integration_tests/pages/riskDetail.ts new file mode 100644 index 00000000..607b4ec2 --- /dev/null +++ b/integration_tests/pages/riskDetail.ts @@ -0,0 +1,7 @@ +import Page from './page' + +export default class RiskDetailPage extends Page { + constructor() { + super('Domestic Abuse Perpetrator') + } +} diff --git a/server/data/masApiClient.ts b/server/data/masApiClient.ts index 220810b3..e7ff0343 100644 --- a/server/data/masApiClient.ts +++ b/server/data/masApiClient.ts @@ -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) { @@ -69,4 +70,12 @@ export default class MasApiClient extends RestClient { async getPersonActivityLog(crn: string): Promise { return this.get({ path: `/activity/${crn}`, handle404: false }) } + + async getPersonRiskFlags(crn: string): Promise { + return this.get({ path: `/risk-flags/${crn}`, handle404: false }) + } + + async getPersonRiskFlag(crn: string, id: string): Promise { + return this.get({ path: `/risk-flags/${crn}/${id}`, handle404: false }) + } } diff --git a/server/data/model/risk.ts b/server/data/model/risk.ts new file mode 100644 index 00000000..25e8f2be --- /dev/null +++ b/server/data/model/risk.ts @@ -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[] +} diff --git a/server/routes/index.ts b/server/routes/index.ts index 814617d7..f1d6b007 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -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() @@ -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 } diff --git a/server/routes/risks.ts b/server/routes/risks.ts new file mode 100644 index 00000000..afb2122e --- /dev/null +++ b/server/routes/risks.ts @@ -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, + }) + }) +} diff --git a/server/utils/nunjucksSetup.ts b/server/utils/nunjucksSetup.ts index 8c6a0c06..28388992 100644 --- a/server/utils/nunjucksSetup.ts +++ b/server/utils/nunjucksSetup.ts @@ -20,6 +20,7 @@ import { getCurrentRisksToThemselves, getPreviousRisksToThemselves, getRisksToThemselves, + getRisksWithScore, getTagClass, govukTime, initialiseName, @@ -28,6 +29,7 @@ import { lastUpdatedBy, lastUpdatedDate, monthsOrDaysElapsed, + removeEmpty, scheduledAppointments, toYesNo, yearsSince, @@ -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) diff --git a/server/utils/utils.test.ts b/server/utils/utils.test.ts index 070e4fe4..6e0f2908 100644 --- a/server/utils/utils.test.ts +++ b/server/utils/utils.test.ts @@ -15,6 +15,7 @@ import { fullName, getAppointmentsToAction, getRisksToThemselves, + getRisksWithScore, getTagClass, govukTime, initialiseName, @@ -22,6 +23,7 @@ import { isToday, monthsOrDaysElapsed, pastAppointments, + removeEmpty, scheduledAppointments, toYesNo, yearsSince, @@ -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> = { VERY_HIGH: array } + it.each([['Filters empty object', risk, 'VERY_HIGH', array]])( + '%s activityLog(%s, %s)', + (_: string, a: Partial>, b: RiskScore, expected: string[]) => { + expect(getRisksWithScore(a, b)).toEqual(expected) + }, + ) +}) diff --git a/server/utils/utils.ts b/server/utils/utils.ts index f677e12d..d29e3925 100644 --- a/server/utils/utils.ts +++ b/server/utils/utils.ts @@ -222,6 +222,14 @@ export const toYesNo = (value: boolean) => { return 'Yes' } +export const getRisksWithScore = (risk: Partial>, 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 @@ -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) => 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)) } diff --git a/server/views/pages/overview.njk b/server/views/pages/overview.njk index aa627e4c..2f8d14f1 100644 --- a/server/views/pages/overview.njk +++ b/server/views/pages/overview.njk @@ -227,7 +227,7 @@ {% set overallRosh %} {% if hasRiskAssessment %} - +
{{ risks.summary.overallRiskLevel | replace('_',' ') }}{{ risks.summary.overallRiskLevel + ' risk of serious harm' | replace('_',' ') }}
{% else %} There is no OASys risk assessment diff --git a/server/views/pages/risk.njk b/server/views/pages/risk.njk new file mode 100644 index 00000000..0af7ad8e --- /dev/null +++ b/server/views/pages/risk.njk @@ -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 %} +

There is no OASys risk assessment for {{ overview.personalDetails.name | fullName }}

+ {% include './risk/_no-oasys-risk-assessment.njk' %} + {% endset %} + {{ govukNotificationBanner({ html: html }) }} + {% endif %} + {% include './risk/_risk-flags.njk' %} + +{% endblock %} \ No newline at end of file diff --git a/server/views/pages/risk/_no-oasys-risk-assessment.njk b/server/views/pages/risk/_no-oasys-risk-assessment.njk new file mode 100644 index 00000000..7324b0e9 --- /dev/null +++ b/server/views/pages/risk/_no-oasys-risk-assessment.njk @@ -0,0 +1,8 @@ +

We do not know:

+
    +
  • Risk of serious harm (ROSH) in the community
  • +
  • Risk of serious harm to themselves
  • +
+

+ View OASys +

diff --git a/server/views/pages/risk/_risk-flags.njk b/server/views/pages/risk/_risk-flags.njk new file mode 100644 index 00000000..9132ffba --- /dev/null +++ b/server/views/pages/risk/_risk-flags.njk @@ -0,0 +1,55 @@ +{% set riskFlags %} +
+
+ {% if case.riskFlags.length == 0 %} +

There are no current risk flags. Add risk flags in Delius.

+ {% endif %} +

Risk flags (registrations) show circumstances that need prominent and constant visibility. Review flags regularly, and remove them when they are no longer appropriate.

+
+
+ + {% if personRisk.riskFlags.length > 0 %} + + + + + + + + + + {% for flag in personRisk.riskFlags %} + {% if not flag.rosh %} + + + + + + {% endif %} + {% endfor %} + +
FlagNotesReview due
{{ flag.description }}{{ flag.notes or 'No notes' }}{{ flag.nextReviewDate | dateWithYear }}
+ {% endif %} + + {% set removedRiskFlagsCount = personRisk.removedRiskFlags.length %} + {% if removedRiskFlagsCount > 0 %} +

+ View removed risk flags ({{ removedRiskFlagsCount }}) +

+ {% endif %} +{% endset %} + +{{ appSummaryCard({ + attributes: {'data-qa': 'riskFlagsCard'}, + titleText: "Risk flags", + classes: 'govuk-!-margin-bottom-6 app-summary-card--large-title', + html: riskFlags, + actions: { + items: [ + { + href: '/case/' + crn + '/handoff/delius', + text: "Add a risk flag in Delius" + } + ] + } +}) }} diff --git a/server/views/pages/risk/_risk-in-the-community.njk b/server/views/pages/risk/_risk-in-the-community.njk new file mode 100644 index 00000000..a673a051 --- /dev/null +++ b/server/views/pages/risk/_risk-in-the-community.njk @@ -0,0 +1,105 @@ + +{% set veryHighRisks = getRisksWithScore(risks.summary.riskInCommunity, 'VERY_HIGH') %} +{% set highRisks = getRisksWithScore(risks.summary.riskInCommunity, 'HIGH') %} +{% set mediumRisks = getRisksWithScore(risks.summary.riskInCommunity, 'MEDIUM') %} +{% set lowRisks = getRisksWithScore(risks.summary.riskInCommunity, 'LOW') %} + +{% set roshHtml %} + {% set overallRiskHtml %} +

+ {{ govukTag({ + text: risks.summary.overallRiskLevel + ' risk of serious harm', + classes: getTagClass(risks.summary.overallRiskLevel) + }) }} +

+ + {% endset %} + + {% set veryHighRiskOfHarm %} +
    + {% for risk in veryHighRisks %} +
  • {{ risk }}
  • + {% endfor %} +
+ {% endset %} + + {% set highRiskOfHarm %} +
    + {% for risk in highRisks %} +
  • {{ risk }}
  • + {% endfor %} +
+ {% endset %} + + {% set mediumRiskOfHarm %} +
    + {% for risk in mediumRisks %} +
  • {{ risk }}
  • + {% endfor %} +
+ {% endset %} + + {% set lowRiskOfHarm %} +
    + {% for risk in lowRisks %} +
  • {{ risk }}
  • + {% endfor %} +
+ {% endset %} + + {{ govukSummaryList({ + rows: [ + { + key: { text: "Overall" }, + value: { html: '' + overallRiskHtml + '' } + }, + { + key: { text: "Very high risk" }, + value: { html: '' + veryHighRiskOfHarm + ''} + } if veryHighRisks.length > 0, + { + key: { text: "High risk" }, + value: { html: '' + highRiskOfHarm + '' } + } if highRisks.length > 0 , + { + key: { text: "Medium risk" }, + value: { html: '' + mediumRiskOfHarm + '' } + } if mediumRisks.length > 0, + { + key: { text: "Low risk" }, + value: { html: '' + lowRiskOfHarm + '' } + } if lowRisks.length > 0, + { + key: { text: "Who is at risk" }, + value: { html: '' + risks.summary.whoIsAtRisk | nl2br + '' if risks.summary.whoIsAtRisk else 'No detail given' } + }, + { + key: { text: "Nature of risk" }, + value: { html: '' + risks.summary.natureOfRisk | nl2br + '' if risks.summary.natureOfRisk else 'No detail given' } + }, + { + key: { text: "When is risk greatest" }, + value: { html: '' + risks.summary.riskImminence | nl2br + '' if risks.summary.riskImminence else 'No detail given' } + } + ] + }) }} + +

+ OASys assessment completed on {{ risks.assessedOn | dateWithYear }} +

+{% endset %} + +{{ appSummaryCard({ + attributes: {'data-qa': 'riskOfHarmInCommunityCard'}, + titleText: "Risk of serious harm (ROSH) in the community", + classes: 'govuk-!-margin-bottom-6 app-summary-card--large-title', + html: roshHtml, + actions: { + items: [ + { + href: "/case/" + crn + "/handoff/oasys", + text: "View OASys" + } + ] + } +}) }} diff --git a/server/views/pages/risk/_risk-to-themselves.njk b/server/views/pages/risk/_risk-to-themselves.njk new file mode 100644 index 00000000..a6d015b4 --- /dev/null +++ b/server/views/pages/risk/_risk-to-themselves.njk @@ -0,0 +1,163 @@ +{% set themselvesHtml %} + {% set suicideSelfHarm %} + {% set hasCurrentSuicideRisk = risks.riskToSelf.suicide.current == 'YES' %} + {% set hasCurrentSelfHarmRisk = risks.riskToSelf.selfHarm.current == 'YES' %} + {% set hasPreviousSuicideRisk = risks.riskToSelf.suicide.previous == 'YES'%} + {% set hasPreviousSelfHarmRisk = risks.riskToSelf.selfHarm.previous == 'YES' %} + + {% set currentNotes = risks.riskToSelf.suicide.currentConcernsText or risks.riskToSelf.selfHarm.currentConcernsText %} + {% set previousNotes = risks.riskToSelf.suicide.previousConcernsText or risks.riskToSelf.selfHarm.previousConcernsText %} + + {% if hasCurrentSuicideRisk and hasCurrentSelfHarmRisk %} + {% set text = 'Immediate concerns about suicide and self-harm' %} + {% elseif hasCurrentSuicideRisk and not hasPreviousSelfHarmRisk %} + {% set text = 'Immediate concerns about suicide' %} + {% elseif hasCurrentSelfHarmRisk and not hasPreviousSuicideRisk %} + {% set text = 'Immediate concerns about self-harm' %} + {% elseif hasCurrentSuicideRisk and hasPreviousSelfHarmRisk %} + {% set text = 'Immediate concerns about suicide and previous concerns about self-harm' %} + {% elseif hasCurrentSelfHarmRisk and hasPreviousSuicideRisk %} + {% set text = 'Immediate concerns about self-harm and previous concerns about suicide' %} + {% elseif hasPreviousSuicideRisk and hasPreviousSelfHarmRisk %} + {% set text = 'Previous concerns about self-harm and suicide' %} + {% elseif hasPreviousSuicideRisk %} + {% set text = 'Previous concerns about suicide' %} + {% elseif hasPreviousSelfHarmRisk %} + {% set text = 'Previous concerns about self-harm' %} + {% else %} + {% set text = 'No concerns' %} + {% endif %} + + {% if currentNotes or previousNotes %} + {% set detailsHtml %} +

Current circumstances, issues and needs

+ {{ currentNotes or 'No detail given' | nl2br | safe }} +
+

Previous circumstances, issues and needs

+ {{ previousNotes or 'No detail given' | nl2br | safe or 'None' }} + {% endset %} + + {{ govukDetails({ + summaryText: text, + html: detailsHtml + }) }} + {% else %} + {{ text }} + {% endif %} + {% endset %} + + {% set copingCustodyHostel %} + {% set hasCurrentCustodyRisk = risks.riskToSelf.custody.current == 'YES' %} + {% set hasCurrentHostelRisk = risks.riskToSelf.hostelSetting.current == 'YES' %} + {% set hasPreviousCustodyRisk = risks.riskToSelf.custody.previous == 'YES' %} + {% set hasPreviousHostelRisk = risks.riskToSelf.hostelSetting.previous == 'YES' %} + + {% set currentNotes = risks.riskToSelf.custody.currentConcernsText or case.riskToSelf.hostelSetting.currentConcernsText %} + {% set previousNotes = risks.riskToSelf.custody.previousConcernsText or case.riskToSelf.hostelSetting.previousConcernsText %} + + {% if hasCurrentCustodyRisk and hasCurrentHostelRisk %} + {% set text = 'Immediate concerns about coping in custody and in a hostel' %} + {% elseif hasCurrentCustodyRisk and not hasPreviousHostelRisk %} + {% set text = 'Immediate concerns about coping in custody' %} + {% elseif hasCurrentHostelRisk and not hasPreviousCustodyRisk %} + {% set text = 'Immediate concerns about coping in a hostel' %} + {% elseif hasCurrentCustodyRisk and hasPreviousHostelRisk %} + {% set text = 'Immediate concerns about coping in custody and previous concerns about coping in a hostel' %} + {% elseif hasCurrentHostelRisk and hasPreviousCustodyRisk %} + {% set text = 'Immediate concerns about coping in a hostel and previous concerns about coping in custody' %} + {% elseif hasPreviousCustodyRisk and hasPreviousHostelRisk %} + {% set text = 'Previous concerns about coping in custody or in a hostel' %} + {% elseif hasPreviousCustodyRisk %} + {% set text = 'Previous concerns about coping in custody' %} + {% elseif hasPreviousHostelRisk %} + {% set text = 'Previous concerns about coping in a hostel' %} + {% else %} + {% set text = 'No concerns' %} + {% endif %} + + {% if currentNotes or previousNotes %} + {% set detailsHtml %} +

Current circumstances, issues and needs

+ {{ currentNotes or 'No detail given' | nl2br | safe }} +
+

Previous circumstances, issues and needs

+ {{ previousNotes or 'No detail given' | nl2br | safe or 'None' }} + {% endset %} + + {{ govukDetails({ + summaryText: text, + html: detailsHtml + }) }} + {% else %} + {{ text }} + {% endif %} + {% endset %} + + {% set vulnerability %} + {% set hasCurrentRisk = risks.riskToSelf.vulnerability.current == 'YES' %} + {% set hasPreviousRisk = risks.riskToSelf.vulnerability.previous == 'YES' %} + {% set currentNotes = risks.riskToSelf.vulnerability.currentConcernsText %} + {% set previousNotes = risks.riskToSelf.vulnerability.previousConcernsText %} + + {% if hasCurrentRisk %} + {% set text = 'Immediate concerns about a vulnerability' %} + {% elseif hasPreviousRisk %} + {% set text = 'Previous concerns about a vulnerability' %} + {% else %} + {% set text = 'No concerns' %} + {% endif %} + + {% if currentNotes or previousNotes %} + {% set detailsHtml %} +

Current circumstances, issues and needs

+ {{ currentNotes or 'No detail given' | nl2br | safe }} +
+

Previous circumstances, issues and needs

+ {{ previousNotes or 'No detail given' | nl2br | safe or 'None' }} + {% endset %} + + {{ govukDetails({ + summaryText: text, + html: detailsHtml + }) }} + {% else %} + {{ text }} + {% endif %} + {% endset %} + + {{ govukSummaryList({ + rows: [ + { + key: { text: "Risk of suicide or self harm" }, + value: { html: '' + suicideSelfHarm + ''} + }, + { + key: { text: "Coping in custody or a hostel" }, + value: { html: '' + copingCustodyHostel + ''} + }, + { + key: { text: "Vulnerability (eg victimisation, being bullied or exploited)" }, + value: { html: '' + vulnerability + '' } + } + ] + }) }} + +

+ OASys assessment completed on {{ risks.assessedOn | dateWithYear }} +

+{% endset %} + +{{ appSummaryCard({ + attributes: {'data-qa': 'riskOfHarmToThemselvesCard'}, + titleText: "Risk of serious harm to themselves", + classes: 'govuk-!-margin-bottom-6 app-summary-card--large-title', + html: themselvesHtml, + actions: { + items: [ + { + href: "/case/" + crn + "/handoff/oasys", + text: "View OASys" + } + ] + } +}) }} diff --git a/server/views/pages/risk/flag.njk b/server/views/pages/risk/flag.njk new file mode 100644 index 00000000..fd4a3c08 --- /dev/null +++ b/server/views/pages/risk/flag.njk @@ -0,0 +1,197 @@ +{% extends "../../partials/layout.njk" %} +{% set title = personRiskFlag.riskFlag.description %} +{% set removed = personRiskFlag.riskFlag.removed %} +{% set flag = personRiskFlag.riskFlag %} + +{% block pageTitle %}{{ title }}{% endblock %} + +{% block beforeContent %} +{{ govukBreadcrumbs({ + items: [ + { + text: "Your cases", + href: "/search" + }, + { + text: personRiskFlag.personSummary.name | fullName, + href: "/case/" + crn + }, + { + text: "Risk", + href: "/case/" + crn + "/risk" + }, + { + text: "Removed risk flags", + href: "/case/" + crn + "/risk/removed-risk-flags" + } if removed, + { + text: title + } + ] | removeEmpty +}) }} +{% endblock %} + +{% block content %} +
+
+

+ {{ 'Removed risk flag' if removed else 'Risk flag' }} + {{ title }} +

+
+
+ + +{% for removal in flag.removalHistory %} + +{% set removedFlagSummaryList %} + {{ govukSummaryList({ + rows: [ + { + key: { + text: "Date removed" + }, + value: { + html: '' + removal.removalDate | dateWithYear + ' by ' + removal.removedBy | fullName + '' + } + } if removed, + { + key: { + text: "Why it was removed" + }, + value: { + html: '' + removal.notes + '' if removal.notes else 'No notes given' + } + } if removed + ] + }) }} +{% endset %} + +{% if removed %} + {{ appSummaryCard({ + attributes: {'data-qa': 'riskFlagRemovedCard'}, + titleText: "Flag removed", + classes: 'govuk-!-margin-bottom-6 app-summary-card--large-title', + html: removedFlagSummaryList + }) }} +{% endif %} + +{% endfor %} + +{% set aboutThisFlagSummaryList %} + {{ govukSummaryList({ + rows: [ + { + key: { + text: "Notes" + }, + value: { + html: '' + personRiskFlag.riskFlag.notes + '' if personRiskFlag.riskFlag.notes else 'No notes' + }, + actions: { + items: [ + { + href: "/case/" + crn + "/handoff/delius", + text: "Change", + visuallyHiddenText: "notes" + } + ] + } + }, + { + key: { + text: "Next review" + }, + value: { + html: '' + personRiskFlag.riskFlag.nextReviewDate | dateWithYear + '' + }, + actions: { + items: [ + { + href: "/case/" + crn + "/handoff/delius", + text: "Review risk flag" + } + ] + } + } if not removed, + { + key: { + text: "Most recent review" + }, + value: { + html: '' + personRiskFlag.riskFlag.mostRecentReviewDate | dateWithYear + ' by ' + personRiskFlag.riskFlag.createdBy | fullName + '' if personRiskFlag.riskFlag.mostRecentReviewDate else 'Not reviewed yet' + }, + actions: { + items: [ + { + href: "/case/" + crn + "/handoff/delius", + text: "View review" + } + ] + } if flag.mostRecentReviewDate + }, + { + key: { + text: "Date added" + }, + value: { + html: '' + personRiskFlag.riskFlag.createdDate | dateWithYear + " by " + personRiskFlag.riskFlag.createdBy | fullName + '' + } + } + ] + }) }} +{% endset %} + + +{{ appSummaryCard({ + attributes: {'data-qa': 'riskFlagCard'}, + titleText: "Before it was removed" if removed else "About this flag", + classes: 'govuk-!-margin-bottom-8 app-summary-card--large-title', + html: aboutThisFlagSummaryList, + actions: { + items: [ + { + href: "/case/" + crn + "/handoff/delius", + text: "Remove this flag on Delius" + } + ] + } if not removed +}) }} + +
+
+

Guidance using this risk flag

+

Purpose of use

+ +

To identify offenders convicted under the Sexual Offences Act 2003 and therefore subject to the notification period and requirements of the Sex Offender Register.

+ +

Suggested review frequency

+ +

Every 3 months

+ +

Termination

+ +

Remove at termination, except for life sentences.

+ +

Further information

+ +

Notification Periods for offenders sentenced under the Sexual Offences Act 2003:

+
    +
  • Imprisonment for a fixed period of 30 months or more, Imprisonment for an indefinite period, imprisonment for public protection, or admission to hospital under restriction order, or subject to an Order for Lifelong Restriction: Indefinitely
  • +
  • Imprisonment for more than 6 months but less than 30 months: 10 years
  • +
  • Imprisonment for 6 months or less, or admission to hospital without restriction order: 7 years
  • +
  • Caution: 2 years
  • +
  • Conditional discharge or (in Scotland) a probation order: Period of discharge or probation
  • +
  • Any other: 5 years
  • +
  • Finite notification periods are halved if the person is under 18 when convicted or cautioned.
  • +
+ +

If an offender is on the register for an indefinite period they can apply to the police area managing them to come off the register 15 years from their initial notification (if made upon release from prison) or first registration upon release from custody (in case they registered upon conviction).

+ +

(Also extended licences will impact on this as well and will render people to be placed on the register indefinitely – stated case is R v Wiles.) +

+
+{% endblock %} diff --git a/server/views/pages/risk/removed-risk-flags.njk b/server/views/pages/risk/removed-risk-flags.njk new file mode 100644 index 00000000..884e2ce3 --- /dev/null +++ b/server/views/pages/risk/removed-risk-flags.njk @@ -0,0 +1,63 @@ +{% extends "../../partials/layout.njk" %} +{% set title = "Removed risk flags" %} +{% block pageTitle %}{{ title }}{% endblock %} + +{% block beforeContent %} +{{ govukBreadcrumbs({ + items: [ + { + text: "Your cases", + href: "/cases" + }, + { + text: personRisk.personSummary.name | fullName, + href: "/cases/" + crm + }, + { + text: "Risk", + href: "/case/" + crn + "/risk" + }, + { + text: title + } + ] +}) }} +{% endblock %} + +{% block content %} +
+
+

+ {{ title }} +

+
+
+ +{% set removedRiskFlagsCount = personRisk.removedRiskFlags.length %} +{% if removedRiskFlagsCount > 0 %} + + + + + + + + + + {% for flag in personRisk.removedRiskFlags %} + {% if flag.removalHistory.length > 0 %} + {% set removal = flag.removalHistory[0] %} + + + + + + {% endif %} + {% endfor %} + +
FlagWhy flag was removedDate removed
{{ flag.description }}{{ removal.notes or 'No notes given' }}{{ removal.removalDate | dateWithYear }}
+{% else %} +

No risk flags have been removed.

+{% endif %} + +{% endblock %} diff --git a/wiremock/mappings/X000001-full.json b/wiremock/mappings/X000001-full.json index 899e976d..d72eedc2 100644 --- a/wiremock/mappings/X000001-full.json +++ b/wiremock/mappings/X000001-full.json @@ -174,7 +174,8 @@ "riskInCommunity": { "LOW": ["Children", "Staff"], "MEDIUM": ["Known Adult"], - "HIGH": ["Public"] + "HIGH": ["Public"], + "VERY_HIGH": ["Staff"] }, "riskInCustody": { "LOW": ["Children", "Public", "Known Adult", "Staff", "Prisoners"] diff --git a/wiremock/mappings/X000001-risk.json b/wiremock/mappings/X000001-risk.json new file mode 100644 index 00000000..d324221e --- /dev/null +++ b/wiremock/mappings/X000001-risk.json @@ -0,0 +1,378 @@ +{ + "mappings": [ + { + "request": { + "urlPattern": "/mas/risk-flags/X000001", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eula", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlags": [ + { + "id": 1, + "description": "Restraining Order", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": false + }, + { + "id": 2, + "description": "Domestic Abuse Perpetrator", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": false + }, + { + "id": 3, + "description": "Risk to Known Adult", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": false + } + ], + "removedRiskFlags": [ + { + "id": 4, + "description": "Restraining Order", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": true, + "removalHistory": [ + { + "notes": "Some removal notes", + "removalDate": "2022-11-18", + "removedBy": { + "forename": "Paul", + "surname": "Smith" + } + } + ] + }, + { + "id": 5, + "description": "Domestic Abuse Perpetrator", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": true, + "removalHistory": [ + { + "notes": "Some removal notes", + "removalDate": "2022-11-18", + "removedBy": { + "forename": "Paul", + "surname": "Smith" + } + } + ] + }, + { + "id": 6, + "description": "Risk to Known Adult", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": true, + "removalHistory": [ + { + "notes": "Some removal notes", + "removalDate": "2022-11-18", + "removedBy": { + "forename": "Paul", + "surname": "Smith" + } + } + ] + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "urlPattern": "/mas/risk-flags/X000001/1", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eulaaaa", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlag": { + "id": 2, + "description": "Restraining Order", + "notes": "Some notes", + "nextReviewDate": "2025-08-12", + "mostRecentReviewDate": "2023-12-12", + "createdDate": "2022-12-12", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": false + } + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "urlPattern": "/mas/risk-flags/X000001/2", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eula", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlag": { + "id": 2, + "description": "Domestic Abuse Perpetrator", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": false + } + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "urlPattern": "/mas/risk-flags/X000001/3", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eula", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlag": { + "id": 3, + "description": "Risk to Known Adult", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": false + } + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "urlPattern": "/mas/risk-flags/X000001/4", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eulaaaa", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlag": { + "id": 4, + "description": "Restraining Order", + "notes": "Some notes", + "nextReviewDate": "2025-08-12", + "mostRecentReviewDate": "2023-12-12", + "createdDate": "2022-12-12", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": true, + "removalHistory": [ + { + "notes": "Some removal notes", + "removalDate": "2022-11-18", + "removedBy": { + "forename": "Paul", + "surname": "Smith" + } + } + ] + } + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "urlPattern": "/mas/risk-flags/X000001/5", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eula", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlag": { + "id": 5, + "description": "Domestic Abuse Perpetrator", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": true, + "removalHistory": [ + { + "notes": "Some removal notes", + "removalDate": "2022-11-18", + "removedBy": { + "forename": "Paul", + "surname": "Smith" + } + } + ] + } + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "urlPattern": "/mas/risk-flags/X000001/6", + "method": "GET" + }, + "response": { + "status": 200, + "jsonBody": { + "personSummary": { + "name": { + "forename": "Eula", + "surname": "Schmeler" + }, + "crn": "X000001", + "dateOfBirth": "1979-08-18" + }, + "riskFlag": { + "id": 6, + "description": "Risk to Known Adult", + "notes": "Some notes", + "nextReviewDate": "2025-08-18", + "mostRecentReviewDate": "2023-12-18", + "createdDate": "2022-12-18", + "createdBy": { + "forename": "Paul", + "surname": "Smith" + }, + "removed": true, + "removalHistory": [ + { + "notes": "Some removal notes", + "removalDate": "2022-11-18", + "removedBy": { + "forename": "Paul", + "surname": "Smith" + } + } + ] + } + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ] +}