Skip to content

Commit

Permalink
Pi 2054 compliance (#41)
Browse files Browse the repository at this point in the history
* PI-2054: Added compliance
  • Loading branch information
pmcphee77 authored Apr 9, 2024
1 parent 25f9517 commit 17f6db6
Show file tree
Hide file tree
Showing 19 changed files with 833 additions and 24 deletions.
36 changes: 36 additions & 0 deletions assets/scss/components/_summary-card.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@
float: right;
}


.app-compliance-panel {
padding: govuk-spacing(3);
color: govuk-colour("white");
background: govuk-colour("dark-grey");

p, ul, li, h2, h3, a {
color: inherit;
}
}

.app-compliance-panel--red {
background: govuk-colour("red");
}

.app-compliance-panel--green {
background: govuk-colour("green");
}

.app-compliance-panel--blue {
background: govuk-colour("blue");
}

.app-compliance-panel--orange {
background: govuk-colour("orange");
}

.app-summary-card--compliance {
border-bottom-width: 0;
}

.app-tag--dark-red {
background: govuk-colour("red");
}


.govuk-tag {
display: inline-block;
outline: 2px solid transparent;
Expand Down
51 changes: 51 additions & 0 deletions integration_tests/e2e/compliance.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Page from '../pages/page'
import CompliancePage from '../pages/compliance'

context('Compliance', () => {
it('Compliance page is rendered', () => {
cy.visit('/case/X000001/compliance')
const page = Page.verifyOnPage(CompliancePage)
page.getCardHeader('sentence1').should('contain.text', 'Sentence (3)')
page
.getRowData('sentence1', 'mainOffenceDescription', 'Value')
.should('contain.text', 'Having possession a picklock')
page.getRowData('sentence1', 'orderDescription', 'Value').should('contain.text', 'ORA Community Order')
page.getRowData('sentence1', 'startDate', 'Value').should('contain.text', '2 March 2020')
page.getRowData('sentence1', 'breach', 'Value').should('contain.text', 'A breach is in progress')

page.getCardHeader('breach1').should('contain.text', 'Breach details')
page.getRowData('breach1', 'startDate', 'Value').should('contain.text', '2 March 2020')
page.getRowData('breach1', 'status', 'Value').should('contain.text', 'An active breach status')

page.getCardHeader('activity1').should('contain.text', '10 days RAR, 9 completed')
page.getRowData('activity1', 'appointments', 'Value').should('contain.text', '1 national standard appointments')
page.getRowData('activity1', 'withoutOutcome', 'Value').should('contain.text', '3 without a recorded outcome')

page.getRowData('activity1', 'waitingForEvidence', 'Value').should('contain.text', '1 absence waiting for evidence')
page.getRowData('activity1', 'complied', 'Value').should('contain.text', '2 complied')
page.getRowData('activity1', 'failureToComply', 'Value').should('contain.text', '2 failures to comply')

page.getRowData('activity1', 'warningLetter', 'Value').should('contain.text', 'First warning letter sent')
page.getRowData('activity1', 'acceptableAbsences', 'Value').should('contain.text', '1 acceptable absences')
page.getRowData('activity1', 'rescheduled', 'Value').should('contain.text', '1 rescheduled')

page.getCardHeader('sentence2').should('contain.text', 'Sentence (1)')
page
.getRowData('sentence2', 'mainOffenceDescription', 'Value')
.should('contain.text', 'Another main offence - 18502')
page.getRowData('sentence2', 'orderDescription', 'Value').should('contain.text', 'ORA Community Order')
page.getRowData('sentence2', 'startDate', 'Value').should('contain.text', '2 March 2020')
page.getRowData('sentence2', 'breach', 'Value').should('contain.text', 'No breaches on current order')

page.getCardHeader('previousOrder1').should('contain.text', '12 month Community Order')
page
.getRowData('previousOrder1', 'mainOffenceDescription', 'Value')
.should('contain.text', 'Common Assault and Battery')
page
.getRowData('previousOrder1', 'status', 'Value')
.should('contain.text', 'Completed - Sentence/ PSS Expiry Reached')
page.getRowData('previousOrder1', 'startDate', 'Value').should('contain.text', '12 December 1990')
page.getRowData('previousOrder1', 'endDate', 'Value').should('contain.text', '12 December 1991')
page.getRowData('previousOrder1', 'breaches', 'Value').should('contain.text', '2')
})
})
4 changes: 2 additions & 2 deletions integration_tests/e2e/overview.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ context('Overview', () => {
.should('contain.text', '1 previous orders (No breaches on previous orders)')
page
.getRowData('activityAndCompliance', 'compliance', 'Value')
.should('contain.text', '2 failure to comply within 12 months')
.should('contain.text', '2 without a recorded outcome')
page
.getRowData('activityAndCompliance', 'activityLog', 'Value')
.should('contain.text', '5 national standard appointments')
.should('contain.text', '2 national standard appointments')
page.getRowData('risk', 'rosh', 'Value').should('contain.text', 'HIGH')
page
.getRowData('risk', 'harmToSelf', 'Value')
Expand Down
7 changes: 7 additions & 0 deletions integration_tests/pages/compliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Page from './page'

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

export default class MasApiClient extends RestClient {
constructor(token: string) {
Expand Down Expand Up @@ -78,4 +79,8 @@ export default class MasApiClient extends RestClient {
async getPersonRiskFlag(crn: string, id: string): Promise<PersonRiskFlag> {
return this.get({ path: `/risk-flags/${crn}/${id}`, handle404: false })
}

async getPersonCompliance(crn: string): Promise<PersonCompliance> {
return this.get({ path: `/compliance/${crn}`, handle404: false })
}
}
22 changes: 22 additions & 0 deletions server/data/model/compliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PersonSummary } from './common'
import { ActivityCount, Compliance, Offence, Order, PreviousOrders, Rar } from './overview'

export interface PersonCompliance {
personSummary: PersonSummary
previousOrders: PreviousOrders
currentSentences: SentenceCompliance[]
}

export interface SentenceCompliance {
activity: ActivityCount
compliance: Compliance
mainOffence: Offence
order: Order
activeBreach?: Breach
rar?: Rar
}

export interface Breach {
startDate: string
status: string
}
27 changes: 21 additions & 6 deletions server/data/model/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Name, PersonalCircumstance } from './common'
export interface Overview {
appointmentsWithoutOutcome: number
absencesWithoutEvidence: number
activity?: Activity
activity?: ActivityCount
compliance?: Compliance
personalDetails: PersonalDetails
previousOrders: PreviousOrders
Expand Down Expand Up @@ -31,14 +31,18 @@ export interface Rar {
}

export interface Order {
status?: string
mainOffence?: string
description: string
endDate?: string
startDate: string
breaches?: number
}

export interface PreviousOrders {
breaches: number
count: number
orders?: Order[]
}

export interface Schedule {
Expand Down Expand Up @@ -69,14 +73,25 @@ export interface Provision {
description: string
}

export interface Activity {
acceptableAbsences: number
complied: number
nationalStandardsAppointments: number
rescheduled: number
export interface ActivityCount {
unacceptableAbsenceCount: number
attendedButDidNotComplyCount: number
outcomeNotRecordedCount: number
waitingForEvidenceCount: number
rescheduledCount: number
absentCount: number
rescheduledByStaffCount: number
rescheduledByPersonOnProbationCount: number
lettersCount: number
nationalStandardAppointmentsCount: number
compliedAppointmentsCount: number
}

export interface Compliance {
currentBreaches: number
priorBreachesOnCurrentOrderCount: number
failureToComplyInLast12Months: number
breachStarted: boolean
breachesOnCurrentOrderCount: number
failureToComplyCount: number
}
32 changes: 32 additions & 0 deletions server/routes/compliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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'

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

get('/case/:crn/compliance', async (req, res, _next) => {
const { crn } = req.params
const token = await hmppsAuthClient.getSystemClientToken(res.locals.user.username)

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

const masClient = new MasApiClient(token)

const personCompliance = await masClient.getPersonCompliance(crn)
res.render('pages/compliance', {
personCompliance,
crn,
})
})
}
2 changes: 2 additions & 0 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import sentenceRoutes from './sentence'
import scheduleRoutes from './schedule'
import activityLogRoutes from './activityLog'
import risksRoutes from './risks'
import complianceRoutes from './compliance'

export default function routes(services: Services): Router {
const router = Router()
Expand All @@ -21,5 +22,6 @@ export default function routes(services: Services): Router {
scheduleRoutes(router, services)
risksRoutes(router, services)
activityLogRoutes(router, services)
complianceRoutes(router, services)
return router
}
2 changes: 2 additions & 0 deletions server/utils/nunjucksSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
deliusHomepageUrl,
fullName,
getAppointmentsToAction,
getComplianceStatus,
getCurrentRisksToThemselves,
getPreviousRisksToThemselves,
getRisksToThemselves,
Expand Down Expand Up @@ -91,6 +92,7 @@ export default function nunjucksSetup(app: express.Express, applicationInfo: App
njkEnv.addFilter('compactActivityLogDate', compactActivityLogDate)
njkEnv.addFilter('activityLogDate', activityLogDate)
njkEnv.addFilter('removeEmpty', removeEmpty)
njkEnv.addGlobal('getComplianceStatus', getComplianceStatus)
njkEnv.addGlobal('timeFromTo', timeFromTo)
njkEnv.addGlobal('getRisksWithScore', getRisksWithScore)
njkEnv.addGlobal('activityLog', activityLog)
Expand Down
18 changes: 18 additions & 0 deletions server/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
deliusHomepageUrl,
fullName,
getAppointmentsToAction,
getComplianceStatus,
getRisksToThemselves,
getRisksWithScore,
getTagClass,
Expand Down Expand Up @@ -372,3 +373,20 @@ describe('renders time from and to', () => {
expect(timeFromTo(a, b)).toEqual(expected)
})
})

describe('Gets compliance status', () => {
it.each([
['Returns breach in progress', 2, true, { text: 'Breach in progress', panelClass: 'app-compliance-panel--red' }],
[
'Returns failure to comply',
2,
false,
{
text: '2 failures to comply within 12 months. No breach in progress yet.',
panelClass: 'app-compliance-panel--red',
},
],
])('%s timeFromTo(%s, %s)', (_: string, a: number, b: boolean, expected: { text: string; panelClass: string }) => {
expect(getComplianceStatus(a, b)).toEqual(expected)
})
})
29 changes: 29 additions & 0 deletions server/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,32 @@ export const timeFromTo = (from: string, to: string) => {
}
return `${timeFrom} to ${timeTo}`
}

export const getComplianceStatus = (failureToComplyCount: number, breachStarted: boolean) => {
const status: { text: string; panelClass: string } = {
text: '',
panelClass: '',
}

if (breachStarted) {
status.text = 'Breach in progress'
status.panelClass = 'app-compliance-panel--red'
} else {
switch (failureToComplyCount) {
case 0:
status.text = 'No failures to comply within 12 months'
status.panelClass = 'app-compliance-panel--green'
break
case 1:
status.text = '1 failure to comply within 12 months'
status.panelClass = ''
break
default:
status.text = `${failureToComplyCount} failures to comply within 12 months. No breach in progress yet.`
status.panelClass = 'app-compliance-panel--red'
break
}
}

return status
}
57 changes: 57 additions & 0 deletions server/views/pages/compliance.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{% extends "../partials/case.njk" %}
{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %}
{% set pageTitle = applicationName + " - Compliance" %}
{% set currentSectionName = 'Compliance' %}
{% set title = 'Compliance' %}
{% set currentNavSection = 'compliance' %}
{% set headerPersonName = personCompliance.personSummary.name | fullName %}
{% set headerCRN = personCompliance.personSummary.crn %}
{% block pageTitle %}{{ title }}{% endblock %}

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

{% block pageContent %}

{% set showWarning = false %}
{% for sentence in personCompliance.currentSentences %}
{% if sentence.compliance.currentBreaches > 0 %}
{% set showWarning = true %}
{% endif %}
{% endfor %}

{% if showWarning === true %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
{{ govukWarningText({
html: 'There are multiple breach NSIs in progress on Delius.<br />Use Delius to check and correct any problems.<div class="govuk-!-margin-top-1"><a href="/case/' + crn + '/handoff/delius">Go to Delius</a></div>',
iconFallbackText: 'Warning'
}) }}
</div>
</div>
{% endif %}

<p class="govuk-!-margin-bottom-6">
<a href="/case/{{ crn }}/handoff/delius">Use Delius to start a breach</a>
</p>

{% include './compliance/_compliance-current-order.njk' %}
{% include './compliance/_compliance-previous-orders.njk' %}


{% endblock %}
Loading

0 comments on commit 17f6db6

Please sign in to comment.