From d5d58d93f8fe5860be37c7e3f32ee6654650641c Mon Sep 17 00:00:00 2001 From: ldlharper Date: Fri, 24 Nov 2023 12:58:57 +0000 Subject: [PATCH] ADJUST1-272 Review remand (#101) --- server/model/remandReviewModel.ts | 78 +++++++++++++ server/model/reviewRemandForm.ts | 18 +++ server/routes/index.ts | 2 + server/routes/remandRoutes.test.ts | 79 +++++++++++++- server/routes/remandRoutes.ts | 37 ++++++- server/routes/testutils/toContainInOrder.ts | 16 +-- .../views/pages/adjustments/remand/review.njk | 103 ++++++++++++++++++ 7 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 server/model/remandReviewModel.ts create mode 100644 server/model/reviewRemandForm.ts create mode 100644 server/views/pages/adjustments/remand/review.njk diff --git a/server/model/remandReviewModel.ts b/server/model/remandReviewModel.ts new file mode 100644 index 00000000..5269ec62 --- /dev/null +++ b/server/model/remandReviewModel.ts @@ -0,0 +1,78 @@ +import dayjs from 'dayjs' +import { Adjustment } from '../@types/adjustments/adjustmentsTypes' +import { PrisonApiOffenderSentenceAndOffences, PrisonApiPrisoner } from '../@types/prisonApi/prisonClientTypes' +import { daysBetween } from '../utils/utils' +import ReviewRemandForm from './reviewRemandForm' + +export default class RemandReviewModel { + adjustmentIds: string[] + + constructor( + public prisonerDetail: PrisonApiPrisoner, + public adjustments: { string?: Adjustment }, + private sentencesAndOffences: PrisonApiOffenderSentenceAndOffences[], + public form: ReviewRemandForm, + ) { + this.adjustmentIds = Object.keys(adjustments) + } + + public totalDays(): number { + return Object.values(this.adjustments) + .map(it => daysBetween(new Date(it.fromDate), new Date(it.toDate))) + .reduce((sum, current) => sum + current, 0) + } + + public adjustmentSummary(id: string) { + const adjustment = this.adjustments[id] + const offences = this.sentencesAndOffences.flatMap(it => + it.offences.filter(off => adjustment.remand.chargeId.includes(off.offenderChargeId)), + ) + return { + rows: [ + { + key: { + text: 'Remand period', + }, + value: { + text: `${dayjs(adjustment.fromDate).format('DD MMMM YYYY')} to ${dayjs(adjustment.toDate).format( + 'DD MMMM YYYY', + )}`, + }, + }, + { + key: { + text: 'Offences', + }, + value: { + html: ``, + }, + }, + { + key: { + text: 'Days spend on remand', + }, + value: { + text: daysBetween(new Date(adjustment.fromDate), new Date(adjustment.toDate)), + }, + }, + ], + } + } + + public totalDaysSummary() { + return { + rows: [ + { + key: { + text: 'Total days', + }, + value: { + text: this.totalDays(), + }, + }, + ], + } + } +} diff --git a/server/model/reviewRemandForm.ts b/server/model/reviewRemandForm.ts new file mode 100644 index 00000000..91878039 --- /dev/null +++ b/server/model/reviewRemandForm.ts @@ -0,0 +1,18 @@ +import AbstractForm from './abstractForm' +import ValidationError from './validationError' + +export default class ReviewRemandForm extends AbstractForm { + another: 'yes' | 'no' + + async validation(): Promise { + if (!this.another) { + return [ + { + fields: ['another'], + text: 'Select an answer', + }, + ] + } + return [] + } +} diff --git a/server/routes/index.ts b/server/routes/index.ts index 5b660586..cc3a782d 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -58,6 +58,8 @@ export default function routes(service: Services): Router { post('/:nomsId/remand/dates/:addOrEdit/:id', remandRoutes.submitDates) get('/:nomsId/remand/offences/:addOrEdit/:id', remandRoutes.offences) post('/:nomsId/remand/offences/:addOrEdit/:id', remandRoutes.submitOffences) + get('/:nomsId/remand/review', remandRoutes.review) + post('/:nomsId/remand/review', remandRoutes.submitReview) get('/:nomsId/:adjustmentTypeUrl/view', adjustmentRoutes.view) get('/:nomsId/:adjustmentTypeUrl/remove/:id', adjustmentRoutes.remove) diff --git a/server/routes/remandRoutes.test.ts b/server/routes/remandRoutes.test.ts index 3fad9fbb..75675106 100644 --- a/server/routes/remandRoutes.test.ts +++ b/server/routes/remandRoutes.test.ts @@ -54,8 +54,13 @@ const stubbedSentencesAndOffences = [ sentenceSequence: 1, sentenceStatus: 'A', offences: [ - { offenceDescription: 'Doing a crime', offenceStartDate: '2021-01-04', offenceEndDate: '2021-01-05' }, - { offenceDescription: 'Doing a different crime', offenceStartDate: '2021-03-06' }, + { + offenderChargeId: 1, + offenceDescription: 'Doing a crime', + offenceStartDate: '2021-01-04', + offenceEndDate: '2021-01-05', + }, + { offenderChargeId: 2, offenceDescription: 'Doing a different crime', offenceStartDate: '2021-03-06' }, ], } as PrisonApiOffenderSentenceAndOffences, ] @@ -71,6 +76,13 @@ const adjustmentWithDates = { toDate: '2023-01-10', } as Adjustment +const adjustmentWithDatesAndCharges = { + ...adjustmentWithDates, + remand: { + chargeId: [1, 2], + }, +} as Adjustment + let app: Express beforeEach(() => { @@ -232,4 +244,67 @@ describe('Adjustment routes tests', () => { expect(res.text).toContain('Select an offence') }) }) + it('GET /{nomsId}/remand/review', () => { + const adjustments = {} + adjustments[SESSION_ID] = adjustmentWithDatesAndCharges + prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData) + prisonerService.getSentencesAndOffences.mockResolvedValue(stubbedSentencesAndOffences) + adjustmentsStoreService.getAll.mockReturnValue(adjustments) + return request(app) + .get(`/${NOMS_ID}/remand/review`) + .expect('Content-Type', /html/) + .expect(res => { + expect(res.text).toContain('Anon') + expect(res.text).toContain('Nobody') + expect(res.text).toContain('Review remand details') + expect(res.text).toContainInOrder([ + 'Remand period', + '01 January 2023 to 10 January 2023', + 'Offences', + 'Doing a crime', + 'Doing a different crime', + 'Days spend on remand', + '10', + 'Total days', + '10', + ]) + }) + }) + + it('POST /{nomsId}/remand/review yes more to add', () => { + return request(app) + .post(`/${NOMS_ID}/remand/review`) + .type('form') + .send({ + another: 'yes', + }) + .expect(302) + .expect('Location', `/${NOMS_ID}/remand/add`) + }) + + it('POST /{nomsId}/remand/review no more to add', () => { + return request(app) + .post(`/${NOMS_ID}/remand/review`) + .type('form') + .send({ + another: 'no', + }) + .expect(302) + .expect('Location', `/${NOMS_ID}/remand/save`) + }) + + it('POST /{nomsId}/remand/review nothing selected', () => { + const adjustments = {} + adjustments[SESSION_ID] = adjustmentWithDatesAndCharges + prisonerService.getPrisonerDetail.mockResolvedValue(stubbedPrisonerData) + prisonerService.getSentencesAndOffences.mockResolvedValue(stubbedSentencesAndOffences) + adjustmentsStoreService.getAll.mockReturnValue(adjustments) + return request(app) + .post(`/${NOMS_ID}/remand/review`) + .type('form') + .expect('Content-Type', /html/) + .expect(res => { + expect(res.text).toContain('Select an answer') + }) + }) }) diff --git a/server/routes/remandRoutes.ts b/server/routes/remandRoutes.ts index a1fe8834..fbc2d847 100644 --- a/server/routes/remandRoutes.ts +++ b/server/routes/remandRoutes.ts @@ -6,6 +6,8 @@ import FullPageError from '../model/FullPageError' import RemandDatesForm from '../model/remandDatesForm' import RemandOffencesForm from '../model/remandOffencesForm' import RemandSelectOffencesModel from '../model/remandSelectOffencesModel' +import RemandReviewModel from '../model/remandReviewModel' +import ReviewRemandForm from '../model/reviewRemandForm' export default class RemandRoutes { constructor( @@ -97,9 +99,9 @@ export default class RemandRoutes { await adjustmentForm.validate() const adjustment = this.adjustmentsStoreService.getById(req, nomsId, id) - const sentencesAndOffences = await this.prisonerService.getSentencesAndOffences(prisonerDetail.bookingId, token) if (adjustmentForm.errors.length) { + const sentencesAndOffences = await this.prisonerService.getSentencesAndOffences(prisonerDetail.bookingId, token) return res.render('pages/adjustments/remand/offences', { model: new RemandSelectOffencesModel(id, prisonerDetail, adjustment, adjustmentForm, sentencesAndOffences), }) @@ -109,4 +111,37 @@ export default class RemandRoutes { return res.redirect(`/${nomsId}/remand/review`) } + + public review: RequestHandler = async (req, res): Promise => { + const { caseloads, token } = res.locals.user + const { nomsId } = req.params + + const adjustments = this.adjustmentsStoreService.getAll(req, nomsId) + const prisonerDetail = await this.prisonerService.getPrisonerDetail(nomsId, caseloads, token) + const sentencesAndOffences = await this.prisonerService.getSentencesAndOffences(prisonerDetail.bookingId, token) + + return res.render('pages/adjustments/remand/review', { + model: new RemandReviewModel(prisonerDetail, adjustments, sentencesAndOffences, new ReviewRemandForm({})), + }) + } + + public submitReview: RequestHandler = async (req, res): Promise => { + const { caseloads, token } = res.locals.user + const { nomsId } = req.params + + const form = new ReviewRemandForm(req.body) + await form.validate() + if (form.errors.length) { + const adjustments = this.adjustmentsStoreService.getAll(req, nomsId) + const prisonerDetail = await this.prisonerService.getPrisonerDetail(nomsId, caseloads, token) + const sentencesAndOffences = await this.prisonerService.getSentencesAndOffences(prisonerDetail.bookingId, token) + return res.render('pages/adjustments/remand/review', { + model: new RemandReviewModel(prisonerDetail, adjustments, sentencesAndOffences, form), + }) + } + if (form.another === 'yes') { + return res.redirect(`/${nomsId}/remand/add`) + } + return res.redirect(`/${nomsId}/remand/save`) + } } diff --git a/server/routes/testutils/toContainInOrder.ts b/server/routes/testutils/toContainInOrder.ts index 56a61bf4..3ba18fda 100644 --- a/server/routes/testutils/toContainInOrder.ts +++ b/server/routes/testutils/toContainInOrder.ts @@ -16,15 +16,17 @@ expect.extend({ let match: jest.CustomMatcherResult = null let text = received all.forEach(expected => { - const result = text.indexOf(expected) - if (result === -1) { - match = { - message: () => `Remaining text didn't contain: '${expected}' within: \n ${text}`, - pass: false, + if (!match) { + const result = text.indexOf(expected) + if (result === -1) { + match = { + message: () => `Remaining text didn't contain: '${expected}' within: \n ${text}`, + pass: false, + } + return } - return + text = text.substring(result) } - text = text.substring(result) }) if (match) { return match diff --git a/server/views/pages/adjustments/remand/review.njk b/server/views/pages/adjustments/remand/review.njk new file mode 100644 index 00000000..530f3faa --- /dev/null +++ b/server/views/pages/adjustments/remand/review.njk @@ -0,0 +1,103 @@ +{% extends "../../../partials/layout.njk" %} +{% from "govuk/components/date-input/macro.njk" import govukDateInput %} +{% from "govuk/components/input/macro.njk" import govukInput %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} + +{% set pageTitle = applicationName + " - Review remand details" %} +{% set mainClasses = "app-container govuk-body" %} + +{% block beforeContent %} + {{ super() }} + +{% endblock %} + +{% block content %} + + {% if model.form.errors.length %} +
+
+ {{ govukErrorSummary({ + titleText: "There is a problem", + errorList: model.form.errorList() + }) }} +
+
+ {% endif %} +
+
+

+ Adjust release dates + Review remand details +

+
+
+
+
+

Check that the remand and offence information is correct. Once you've reviewed it, you can add more remand time if needed.

+
+
+
+
+

Remand details

+ +
+ + + {% for adjustmentId in model.adjustmentIds %} + {{ govukSummaryList(model.adjustmentSummary(adjustmentId))}} + {% endfor %} + {{ govukSummaryList(model.totalDaysSummary())}} + + {{ govukRadios({ + classes: "govuk-radios--inline", + name: "another", + fieldset: { + legend: { + text: "Do you need to add another period of remand?", + classes: "govuk-fieldset__legend--m" + } + }, + errorMessage: model.form.messageForField('another'), + items: [ + { + value: "yes", + text: "Yes" + }, + { + value: "no", + text: "No" + } + ] + }) }} + +
+ {{ govukButton({ + text: "Continue", + type: submit, + preventDoubleClick: true, + attributes: { 'data-qa': 'submit-form' } + }) }} + {{ govukButton({ + text: "Cancel", + classes: "govuk-button--secondary", + href: "/" + model.prisonerDetail.offenderNo + }) }} +
+
+
+
+{% endblock %} \ No newline at end of file