diff --git a/assets/js/deselect-children.js b/assets/js/deselect-children.js new file mode 100644 index 00000000..e1e560ac --- /dev/null +++ b/assets/js/deselect-children.js @@ -0,0 +1,9 @@ +document.querySelectorAll('[id^=parent-]').forEach(parent => { + parent.addEventListener('change', deselectChildrenOnUncheck) +}) +function deselectChildrenOnUncheck(e) { + const parent = e.srcElement + if (!parent.checked) { + document.querySelectorAll(`[id^=child-${parent.value}__]`).forEach(child => (child.checked = false)) + } +} diff --git a/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js b/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js index 11827a83..caa51fc6 100644 --- a/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js +++ b/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js @@ -35,7 +35,6 @@ context('Enter use of force details page', () => { useOfForceDetailsPage.guidingHold().check('true') useOfForceDetailsPage.guidingHoldOfficersInvolved.check('2') useOfForceDetailsPage.escortingHold().check('true') - useOfForceDetailsPage.restraint().check('true') useOfForceDetailsPage.restraintPositions.check(restraintPositions) useOfForceDetailsPage.handcuffsApplied().check('true') useOfForceDetailsPage.painInducingTechniques().check('true') @@ -63,7 +62,6 @@ context('Enter use of force details page', () => { pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, - restraint: true, restraintPositions: ['STANDING', 'ON_BACK', 'FACE_DOWN', 'KNEELING'], painInducingTechniques: true, painInducingTechniquesUsed: ['THROUGH_RIGID_BAR_CUFFS', 'THUMB_LOCK'], @@ -90,8 +88,7 @@ context('Enter use of force details page', () => { pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, - restraint: true, - restraintPositions: ['STANDING'], + restraintPositions: 'STANDING', painInducingTechniques: true, painInducingTechniquesUsed: ['THROUGH_RIGID_BAR_CUFFS', 'THUMB_LOCK'], }) @@ -116,7 +113,6 @@ context('Enter use of force details page', () => { useOfForceDetailsPage.guidingHold().should('have.value', 'true') useOfForceDetailsPage.guidingHoldOfficersInvolved.two().should('be.checked') useOfForceDetailsPage.escortingHold().should('have.value', 'true') - useOfForceDetailsPage.restraint().should('have.value', 'true') useOfForceDetailsPage.restraintPositions.standing().should('be.checked') useOfForceDetailsPage.restraintPositions.faceDown().should('not.be.checked') useOfForceDetailsPage.restraintPositions.kneeling().should('be.checked') @@ -142,7 +138,6 @@ context('Enter use of force details page', () => { useOfForceDetailsPage.guidingHold().check('true') useOfForceDetailsPage.guidingHoldOfficersInvolved.check('2') useOfForceDetailsPage.escortingHold().check('true') - useOfForceDetailsPage.restraint().check('false') useOfForceDetailsPage.handcuffsApplied().check('true') useOfForceDetailsPage.painInducingTechniques().check('true') useOfForceDetailsPage.clickSaveAndContinue() diff --git a/integration-tests/integration/seedData.js b/integration-tests/integration/seedData.js index 55cb54fc..98c58307 100644 --- a/integration-tests/integration/seedData.js +++ b/integration-tests/integration/seedData.js @@ -31,12 +31,10 @@ const expectedPayload = { batonDrawn: true, guidingHold: true, escortingHold: true, - restraint: true, handcuffsApplied: true, restraintPositions: ['STANDING', 'ON_BACK', 'FACE_DOWN', 'KNEELING'], painInducingTechniques: true, painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], - positiveCommunication: true, guidingHoldOfficersInvolved: 2, personalProtectionTechniques: true, diff --git a/integration-tests/pages/createReport/useOfForceDetailsPage.js b/integration-tests/pages/createReport/useOfForceDetailsPage.js index 604605d4..a484390b 100644 --- a/integration-tests/pages/createReport/useOfForceDetailsPage.js +++ b/integration-tests/pages/createReport/useOfForceDetailsPage.js @@ -30,9 +30,9 @@ const useOfForceDetailsPage = () => onBack: () => cy.get('#control-and-restraint [type="checkbox"][value="ON_BACK"]'), faceDown: () => cy.get('#control-and-restraint [type="checkbox"][value="FACE_DOWN"]'), kneeling: () => cy.get('#control-and-restraint [type="checkbox"][value="KNEELING"]'), + none: () => cy.get('#control-and-restraint [type="checkbox"][value="NONE"]'), }, - restraint: () => cy.get('[name="restraint"]'), handcuffsApplied: () => cy.get('[name="handcuffsApplied"]'), painInducingTechniques: () => cy.get('[name="painInducingTechniques"]'), @@ -60,7 +60,6 @@ const useOfForceDetailsPage = () => this.guidingHold().check('true') this.guidingHoldOfficersInvolved.check('2') this.escortingHold().check('true') - this.restraint().check('true') this.restraintPositions.check(['STANDING', 'ON_BACK', 'FACE_DOWN', 'KNEELING']) this.handcuffsApplied().check('true') this.painInducingTechniques().check('true') diff --git a/integration-tests/pages/sections/reportDetails.js b/integration-tests/pages/sections/reportDetails.js index d8e92c05..47fec1b5 100644 --- a/integration-tests/pages/sections/reportDetails.js +++ b/integration-tests/pages/sections/reportDetails.js @@ -63,7 +63,10 @@ module.exports = { cy.get('[data-qa="pavaDrawn"]').contains('Yes and used') cy.get('[data-qa="guidingHold"]').contains('Yes - 2 officers involved') cy.get('[data-qa="escortingHold"]').contains('Yes') - cy.get('[data-qa="restraintUsed"]').contains('Yes - standing, on back (supine), on front (prone), kneeling') + cy.get('[data-qa="restraintUsed"]').contains('Standing') + cy.get('[data-qa="restraintUsed"]').contains('On back (supine)') + cy.get('[data-qa="restraintUsed"]').contains('On front (prone)') + cy.get('[data-qa="restraintUsed"]').contains('Kneeling') handcuffsApplied().contains('Yes') painInducingTechniques().contains('Yes') cy.get('[data-qa="painInducingTechniques"]').contains('Yes - wrist flexion, thumb lock') diff --git a/server/config/forms/useOfForceDetailsForm.js b/server/config/forms/useOfForceDetailsForm.js index 57d262be..7634fe0e 100644 --- a/server/config/forms/useOfForceDetailsForm.js +++ b/server/config/forms/useOfForceDetailsForm.js @@ -71,30 +71,43 @@ const completeSchema = joi.object({ }), escortingHold: requiredBooleanMsg('Select yes if an escorting hold was used').alter(optionalForPartialValidation), - - restraint: requiredBooleanMsg('Select yes if control and restraint was used').alter(optionalForPartialValidation), - - restraintPositions: joi.when('restraint', { - is: true, - then: joi - .alternatives() - .try( - joi - .array() - .items( - requiredOneOfMsg( - 'STANDING', - 'FACE_DOWN', - 'ON_BACK', - 'KNEELING' - )('Select the control and restraint positions used').alter(optionalForPartialValidation) - ) - ) - .required() - .messages({ 'any.required': 'Select the control and restraint positions used' }) - .alter(optionalForPartialValidation), - otherwise: joi.any().strip(), - }), + restraintPositions: joi + .alternatives() + .try( + joi + .valid('STANDING', 'ON_BACK', 'FACE_DOWN', 'KNEELING', 'NONE') + .messages({ 'any.only': 'Select which control and restraint positions were used' }), + joi + .array() + .items( + requiredOneOfMsg( + 'STANDING', + 'STANDING__WRIST_WEAVE', + 'STANDING__DOUBLE_WRIST_HOLD', + 'STANDING__UNDERHOOK', + 'STANDING__WRIST_HOLD', + 'STANDING__STRAIGHT_HOLD', + 'ON_BACK', + 'ON_BACK__STRAIGHT_ARM_HOLD', + 'ON_BACK__CONVERSION_TO_RBH', + 'ON_BACK__WRIST_HOLD', + 'FACE_DOWN', + 'FACE_DOWN__BALANCE_DISPLACEMENT', + 'FACE_DOWN__STRAIGHT_ARM_HOLD', + 'FACE_DOWN__CONVERSION_TO_RBH', + 'FACE_DOWN__WRIST_HOLD', + 'KNEELING' + )('Select which control and restraint positions were used') + ) + ) + .messages({ + 'alternatives.types': 'Select which control and restraint positions were used', + }) + .required() + .messages({ + 'any.required': 'Select which control and restraint positions were used', + }) + .alter(optionalForPartialValidation), painInducingTechniques: requiredBooleanMsg('Select yes if pain inducing techniques were used').alter( optionalForPartialValidation diff --git a/server/config/forms/useOfForceDetailsValidation.test.js b/server/config/forms/useOfForceDetailsValidation.test.js index 9ce9b628..5015667d 100644 --- a/server/config/forms/useOfForceDetailsValidation.test.js +++ b/server/config/forms/useOfForceDetailsValidation.test.js @@ -20,7 +20,6 @@ beforeEach(() => { guidingHold: 'true', guidingHoldOfficersInvolved: '2', escortingHold: 'true', - restraint: 'true', restraintPositions: ['STANDING', 'FACE_DOWN'], handcuffsApplied: 'true', painInducingTechniques: 'true', @@ -48,7 +47,6 @@ describe('complete schema', () => { guidingHold: true, guidingHoldOfficersInvolved: 2, escortingHold: true, - restraint: true, restraintPositions: ['STANDING', 'FACE_DOWN'], handcuffsApplied: true, painInducingTechniques: true, @@ -90,8 +88,8 @@ describe('complete schema', () => { text: 'Select yes if an escorting hold was used', }, { - href: '#restraint', - text: 'Select yes if control and restraint was used', + href: '#restraintPositions', + text: 'Select which control and restraint positions were used', }, { href: '#painInducingTechniques', @@ -197,7 +195,6 @@ describe('complete schema', () => { pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, - restraint: true, restraintPositions: ['STANDING', 'FACE_DOWN'], }) }) @@ -224,7 +221,6 @@ describe('complete schema', () => { pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, - restraint: true, restraintPositions: ['STANDING', 'FACE_DOWN'], }) }) @@ -255,7 +251,6 @@ describe('complete schema', () => { pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, - restraint: true, restraintPositions: ['STANDING', 'FACE_DOWN'], }) }) @@ -403,24 +398,7 @@ describe('complete schema', () => { expect(formResponse.escortingHold).toEqual(undefined) }) - it("Not selecting an option for 'restraint'returns a validation error message plus 'restraint positions' is undefined", () => { - const input = { - ...validInput, - restraint: undefined, - } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([ - { - href: '#restraint', - text: 'Select yes if control and restraint was used', - }, - ]) - expect(formResponse.restraint).toBe(undefined) - expect(formResponse.restraintPositions).toBe(undefined) - }) - - it("Selecting Yes to 'restraint' but nothing for 'restraint positions' returns a validation error message", () => { + it("Selecting nothing for 'restraint positions' returns a validation error message", () => { const input = { ...validInput, restraintPositions: undefined, @@ -430,10 +408,9 @@ describe('complete schema', () => { expect(errors).toEqual([ { href: '#restraintPositions', - text: 'Select the control and restraint positions used', + text: 'Select which control and restraint positions were used', }, ]) - expect(formResponse.restraint).toEqual(true) expect(formResponse.restraintPositions).toBe(undefined) }) @@ -445,7 +422,6 @@ describe('complete schema', () => { const { errors, formResponse } = check(input) expect(errors).toEqual([]) - expect(formResponse.restraint).toBe(true) expect(formResponse.restraintPositions).toEqual(['KNEELING']) }) @@ -456,10 +432,20 @@ describe('complete schema', () => { const { errors, formResponse } = check(input) expect(errors).toEqual([]) - expect(formResponse.restraint).toEqual(true) expect(formResponse.restraintPositions).toEqual(['STANDING', 'FACE_DOWN']) }) + it("Selecting 'Standing' and child option 'Wrist hold' for 'restraint positions' returns no errors", () => { + const input = { + ...validInput, + restraintPositions: ['STANDING', 'STANDING__WRIST_HOLD'], + } + const { errors, formResponse } = check(input) + + expect(errors).toEqual([]) + expect(formResponse.restraintPositions).toEqual(['STANDING', 'STANDING__WRIST_HOLD']) + }) + it("Not selecting an option for 'handcuffs applied' returns a validation error message", () => { const input = { ...validInput, @@ -558,7 +544,6 @@ describe('partial schema', () => { guidingHold: true, guidingHoldOfficersInvolved: 2, escortingHold: true, - restraint: true, restraintPositions: ['STANDING', 'FACE_DOWN'], handcuffsApplied: true, painInducingTechniques: true, @@ -566,7 +551,7 @@ describe('partial schema', () => { }) }) - it('Should return no error massages if no input field is completed', () => { + it('Should return no error messages if no input field is completed', () => { const input = {} const { errors, formResponse } = check(input) @@ -575,14 +560,13 @@ describe('partial schema', () => { }) }) - it('Should return no error massages when dependent answers are absent', () => { + it('Should return no error messages when dependent answers are absent', () => { const { errors, formResponse } = check({ batonDrawn: 'true', pavaDrawn: 'true', guidingHold: 'true', escortingHold: 'true', - restraint: 'true', - restraintPositions: [], + restraintPositions: 'NONE', }) expect(errors).toEqual([]) @@ -591,7 +575,22 @@ describe('partial schema', () => { guidingHold: true, escortingHold: true, pavaDrawn: true, - restraint: true, + restraintPositions: 'NONE', }) }) + it('Selecting only a child control technique returns a validation error message', () => { + const input = { + ...validInput, + restraintPositions: 'STANDING__WRIST_HOLD', + } + const { errors, formResponse } = check(input) + + expect(errors).toEqual([ + { + href: '#restraintPositions', + text: 'Select which control and restraint positions were used', + }, + ]) + expect(formResponse.restraintPositions).toBe('STANDING__WRIST_HOLD') + }) }) diff --git a/server/config/types.ts b/server/config/types.ts index b6cf3213..3a7ee39c 100644 --- a/server/config/types.ts +++ b/server/config/types.ts @@ -1,4 +1,12 @@ -export type LabelledValue = { readonly value: string; readonly label: string; readonly inactive?: boolean } +export type LabelledValue = { + readonly value: string + readonly label: string + readonly inactive?: boolean + readonly sub_options_label?: string + readonly sub_options?: boolean + readonly parent?: string + readonly exclusive?: boolean +} type LabelledEnum = Record const toEnum = (value: LabelledEnum): Readonly> => Object.freeze(value) @@ -8,6 +16,11 @@ export const toLabel = (type: LabelledEnum, val: string): s return match ? type[match].label : undefined } +export const findEnum = (type: LabelledEnum, val: string): Readonly> => { + const match = Object.keys(type).find(value => value === val) + return match ? type[match] : undefined +} + export const BodyWornCameras = toEnum({ YES: { value: 'YES', label: 'Yes' }, NO: { value: 'NO', label: 'No' }, @@ -21,10 +34,58 @@ export const Cctv = toEnum({ }) export const ControlAndRestraintPosition = toEnum({ - STANDING: { value: 'STANDING', label: 'Standing' }, - ON_BACK: { value: 'ON_BACK', label: 'On back (supine)' }, - FACE_DOWN: { value: 'FACE_DOWN', label: 'On front (prone)' }, - KNEELING: { value: 'KNEELING', label: 'Kneeling' }, + STANDING: { + value: 'STANDING', + label: 'Standing', + sub_options_label: 'Standing techniques', + sub_options: true, + }, + STANDING__WRIST_WEAVE: { value: 'STANDING__WRIST_WEAVE', label: 'Wrist weave', parent: 'STANDING' }, + STANDING__DOUBLE_WRIST_HOLD: { value: 'STANDING__DOUBLE_WRIST_HOLD', label: 'Double wrist hold', parent: 'STANDING' }, + STANDING__UNDERHOOK: { value: 'STANDING__UNDERHOOK', label: 'Underhook', parent: 'STANDING' }, + STANDING__WRIST_HOLD: { value: 'STANDING__WRIST_HOLD', label: 'Wrist hold', parent: 'STANDING' }, + STANDING__STRAIGHT_ARM_HOLD: { value: 'STANDING__STRAIGHT_ARM_HOLD', label: 'Straight arm hold', parent: 'STANDING' }, + ON_BACK: { + value: 'ON_BACK', + label: 'On back (supine)', + sub_options_label: 'On back (supine) techniques', + sub_options: true, + }, + ON_BACK__STRAIGHT_ARM_HOLD: { + value: 'ON_BACK__STRAIGHT_ARM_HOLD', + label: 'Straight arm hold (left or right)', + parent: 'ON_BACK', + }, + ON_BACK__CONVERSION_TO_RBH: { + value: 'ON_BACK__CONVERSION_TO_RBH', + label: 'Conversion to apply RBH', + parent: 'ON_BACK', + }, + ON_BACK__WRIST_HOLD: { value: 'ON_BACK__WRIST_HOLD', label: 'Wrist hold', parent: 'ON_BACK' }, + FACE_DOWN: { + value: 'FACE_DOWN', + label: 'On front (prone)', + sub_options_label: 'On front (prone) techniques', + sub_options: true, + }, + FACE_DOWN__BALANCE_DISPLACEMENT: { + value: 'FACE_DOWN__BALANCE_DISPLACEMENT', + label: 'Balance displacement technique', + parent: 'FACE_DOWN', + }, + FACE_DOWN__STRAIGHT_ARM_HOLD: { + value: 'FACE_DOWN__STRAIGHT_ARM_HOLD', + label: 'Straight arm hold (left or right)', + parent: 'FACE_DOWN', + }, + FACE_DOWN__CONVERSION_TO_RBH: { + value: 'FACE_DOWN__CONVERSION_TO_RBH', + label: 'Conversion to apply RBH', + parent: 'FACE_DOWN', + }, + FACE_DOWN__WRIST_HOLD: { value: 'FACE_DOWN__WRIST_HOLD', label: 'Wrist hold', parent: 'FACE_DOWN' }, + KNEELING: { value: 'KNEELING', label: 'Kneeling', sub_options: false }, + NONE: { value: 'NONE', label: 'No control and restraint positions were used', exclusive: true, sub_options: false }, }) export const PainInducingTechniquesUsed = toEnum({ diff --git a/server/data/UseOfForceReport.ts b/server/data/UseOfForceReport.ts index bcbdc2b4..9732ae84 100644 --- a/server/data/UseOfForceReport.ts +++ b/server/data/UseOfForceReport.ts @@ -17,8 +17,8 @@ export type UseOfForceDetails = { guidingHold: boolean guidingHoldOfficersInvolved: number escortingHold?: boolean - restraint: boolean - restraintPositions: string[] + restraint?: boolean + restraintPositions: string | string[] handcuffsApplied: boolean painInducingTechniques: boolean painInducingTechniquesUsed: string[] diff --git a/server/routes/creatingReports/addInvolvedStaff.test.ts b/server/routes/creatingReports/addInvolvedStaff.test.ts index 7d05cd80..187e4249 100755 --- a/server/routes/creatingReports/addInvolvedStaff.test.ts +++ b/server/routes/creatingReports/addInvolvedStaff.test.ts @@ -138,7 +138,7 @@ describe('delete staff page', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffInvolved(REPORT_ID)) .expect(() => { - expect(draftReportService.deleteInvolvedStaff).not.toBeCalled() + expect(draftReportService.deleteInvolvedStaff).not.toHaveBeenCalled() }) }) @@ -149,7 +149,7 @@ describe('delete staff page', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffInvolved(REPORT_ID)) .expect(() => { - expect(draftReportService.deleteInvolvedStaff).toBeCalledWith(user, REPORT_ID, 'USER-1') + expect(draftReportService.deleteInvolvedStaff).toHaveBeenCalledWith(user, REPORT_ID, 'USER-1') }) }) }) @@ -195,7 +195,7 @@ describe('submit staff', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffInvolved(REPORT_ID)) .expect(() => { - expect(draftReportService.addDraftStaffByName).toBeCalledWith(user, REPORT_ID, 'Jo', 'Jones') + expect(draftReportService.addDraftStaffByName).toHaveBeenCalledWith(user, REPORT_ID, 'Jo', 'Jones') }) }) @@ -207,7 +207,7 @@ describe('submit staff', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffInvolved(REPORT_ID)) .expect(() => { - expect(draftReportService.addDraftStaffByName).toBeCalledWith(user, REPORT_ID, 'Jo', 'Jones') + expect(draftReportService.addDraftStaffByName).toHaveBeenCalledWith(user, REPORT_ID, 'Jo', 'Jones') }) }) @@ -219,7 +219,7 @@ describe('submit staff', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffInvolved(REPORT_ID)) .expect(() => { - expect(draftReportService.addDraftStaffByName).toBeCalledWith(user, REPORT_ID, 'Jo', 'Jones') + expect(draftReportService.addDraftStaffByName).toHaveBeenCalledWith(user, REPORT_ID, 'Jo', 'Jones') }) }) @@ -231,7 +231,7 @@ describe('submit staff', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffNotFound(REPORT_ID)) .expect(() => { - expect(draftReportService.addDraftStaffByName).toBeCalledWith(user, REPORT_ID, 'Jo', 'Jones') + expect(draftReportService.addDraftStaffByName).toHaveBeenCalledWith(user, REPORT_ID, 'Jo', 'Jones') }) }) }) @@ -246,7 +246,7 @@ describe('multiple results', () => { .get(paths.selectStaffMember(REPORT_ID)) .expect('Content-Type', /html/) .expect(() => { - expect(draftReportService.findUsers).toBeCalledWith('user1-system-token', 'MDI', 'Bob', 'Smith') + expect(draftReportService.findUsers).toHaveBeenCalledWith('user1-system-token', 'MDI', 'Bob', 'Smith') }) }) test('POST requires staff member to be selected', () => { @@ -264,7 +264,7 @@ describe('multiple results', () => { .expect('Content-Type', /text\/plain/) .expect('Location', paths.staffInvolved(REPORT_ID)) .expect(() => { - expect(draftReportService.addDraftStaffByUsername).toBeCalledWith(user, REPORT_ID, 'USER-2') + expect(draftReportService.addDraftStaffByUsername).toHaveBeenCalledWith(user, REPORT_ID, 'USER-2') }) }) }) diff --git a/server/routes/creatingReports/checkYourAnswers.test.ts b/server/routes/creatingReports/checkYourAnswers.test.ts index 4d10704d..420a9221 100644 --- a/server/routes/creatingReports/checkYourAnswers.test.ts +++ b/server/routes/creatingReports/checkYourAnswers.test.ts @@ -54,13 +54,13 @@ describe('GET /check-your-answers', () => { incidentDetails: {}, useOfForceDetails: { pavaDrawn: false, - restraint: false, batonDrawn: false, guidingHold: false, escortingHold: false, handcuffsApplied: false, positiveCommunication: false, personalProtectionTechniques: false, + restraintPositions: 'NONE', painInducingTechniques: undefined, }, }, @@ -82,13 +82,13 @@ describe('GET /check-your-answers', () => { incidentDetails: {}, useOfForceDetails: { pavaDrawn: false, - restraint: false, batonDrawn: false, guidingHold: false, handcuffsApplied: false, positiveCommunication: false, painInducingTechniques: true, personalProtectionTechniques: false, + restraintPositions: 'NONE', }, }, }) @@ -109,13 +109,13 @@ describe('GET /check-your-answers', () => { incidentDetails: {}, useOfForceDetails: { pavaDrawn: false, - restraint: false, batonDrawn: false, guidingHold: false, handcuffsApplied: false, positiveCommunication: false, painInducingTechniques: false, personalProtectionTechniques: false, + restraintPositions: 'NONE', }, }, }) diff --git a/server/routes/creatingReports/createReport.test.ts b/server/routes/creatingReports/createReport.test.ts index 14de8fe6..212e4c1c 100644 --- a/server/routes/creatingReports/createReport.test.ts +++ b/server/routes/creatingReports/createReport.test.ts @@ -46,9 +46,9 @@ const validUseOfForceDetailsRequest = { pavaDrawn: 'false', guidingHold: 'false', escortingHold: 'false', - restraint: 'false', painInducingTechniques: 'false', handcuffsApplied: 'false', + restraintPositions: 'NONE', submitType: 'save-and-continue', } @@ -67,7 +67,7 @@ const validUseofForceDetailUpdate = [ pavaDrawn: false, personalProtectionTechniques: false, positiveCommunication: false, - restraint: false, + restraintPositions: 'NONE', }, ] @@ -79,8 +79,8 @@ describe('POST save and continue /section/form', () => { .expect(302) .expect('Location', '/report/1/relocation-and-injuries') .expect(() => { - expect(draftReportService.process).toBeCalledTimes(1) - expect(draftReportService.process).toBeCalledWith(...validUseofForceDetailUpdate) + expect(draftReportService.process).toHaveBeenCalledTimes(1) + expect(draftReportService.process).toHaveBeenCalledWith(...validUseofForceDetailUpdate) }) }) @@ -91,7 +91,7 @@ describe('POST save and continue /section/form', () => { .expect(302) .expect('Location', '/report/1/use-of-force-details') .expect(() => { - expect(draftReportService.process).not.toBeCalled() + expect(draftReportService.process).not.toHaveBeenCalled() })) }) @@ -103,8 +103,8 @@ describe('POST save and return to tasklist', () => { .expect(302) .expect('Location', '/report/1/report-use-of-force') .expect(() => { - expect(draftReportService.process).toBeCalledTimes(1) - expect(draftReportService.process).toBeCalledWith(...validUseofForceDetailUpdate) + expect(draftReportService.process).toHaveBeenCalledTimes(1) + expect(draftReportService.process).toHaveBeenCalledWith(...validUseofForceDetailUpdate) }) }) @@ -115,8 +115,8 @@ describe('POST save and return to tasklist', () => { .expect(302) .expect('Location', '/report/1/report-use-of-force') .expect(() => { - expect(draftReportService.process).toBeCalledTimes(1) - expect(draftReportService.process).toBeCalledWith(user, 1, 'useOfForceDetails', { + expect(draftReportService.process).toHaveBeenCalledTimes(1) + expect(draftReportService.process).toHaveBeenCalledWith(user, 1, 'useOfForceDetails', { guidingHold: false, escortingHold: false, handcuffsApplied: false, @@ -124,7 +124,7 @@ describe('POST save and return to tasklist', () => { pavaDrawn: false, personalProtectionTechniques: false, positiveCommunication: false, - restraint: false, + restraintPositions: 'NONE', }) }) }) @@ -163,14 +163,13 @@ describe('POST save once complete and return to check-your-answers', () => { .post(`/report/1/use-of-force-details`) .send({ ...validUseOfForceDetailsRequest, - restraint: 'true', restraintPositions: ['not a valid value'], submitType: 'save-and-return', }) .expect(302) .expect('Location', '/report/1/use-of-force-details') .expect(() => { - expect(draftReportService.process).not.toBeCalled() + expect(draftReportService.process).not.toHaveBeenCalled() }) }) }) diff --git a/server/services/drafts/reportStatusChecker.test.ts b/server/services/drafts/reportStatusChecker.test.ts index 453c435e..e109b44c 100644 --- a/server/services/drafts/reportStatusChecker.test.ts +++ b/server/services/drafts/reportStatusChecker.test.ts @@ -14,7 +14,6 @@ describe('statusCheck', () => { reasonsForUseOfForce: { reasons: [UofReasons.FIGHT_BETWEEN_PRISONERS.value] }, useOfForceDetails: { pavaDrawn: false, - restraint: false, batonDrawn: false, guidingHold: false, escortingHold: false, @@ -22,6 +21,7 @@ describe('statusCheck', () => { positiveCommunication: false, personalProtectionTechniques: true, painInducingTechniques: false, + restraintPositions: 'NONE', bodyWornCamera: 'YES', bodyWornCameraNumbers: [{ cameraNum: '1111' }, { cameraNum: '2222' }], }, diff --git a/server/services/reportSummary.ts b/server/services/reportSummary.ts index 5e8fc8e4..92c3d09b 100644 --- a/server/services/reportSummary.ts +++ b/server/services/reportSummary.ts @@ -7,6 +7,7 @@ import { RelocationType, toLabel, UofReasons, + findEnum, } from '../config/types' import { Prison } from '../data/prisonClientTypes' import { @@ -68,9 +69,10 @@ const createUseOfForceDetails = ( value ? howManyOfficersInvolved(details.guidingHoldOfficersInvolved) : NO ), escortingHoldUsed: details.escortingHold, - controlAndRestraintUsed: whenPresent(details.restraint, value => - value === true && details.restraintPositions ? getRestraintPositions(details.restraintPositions) : NO - ), + controlAndRestraintUsed: + details.restraint === undefined || details.restraint + ? getRestraintPositions(details.restraintPositions) + : getRestraintPositions(ControlAndRestraintPosition.NONE), painInducingTechniques: getPainInducingTechniques(details), handcuffsApplied: details.handcuffsApplied, @@ -132,9 +134,44 @@ const wasWeaponUsed = weaponUsed => { } const getRestraintPositions = positions => { - return positions == null - ? '' - : `${YES} - ${positions.map(pos => toLabel(ControlAndRestraintPosition, pos)).join(', ')}` + if (positions == null) { + return Array.of[ControlAndRestraintPosition.NONE.label] + } + return toParentChild(toArray(positions)) +} + +const toParentChild = postions => { + const positionObjects = postions.map(p => findEnum(ControlAndRestraintPosition, p)) + console.log(positionObjects) + const parents: any[] = [] + const children: any[] = [] + positionObjects.forEach(obj => { + if (obj.parent == null) { + parents.push(obj) + } else { + children.push(obj) + } + }) + console.log(parents) + console.log(children) + const parentChild: string[] = [] + parents.forEach(function (p) { + const thesechildren = children + .filter(pos => pos.parent === p.value) + .map(child => child.label) + .join(', ') + if (thesechildren === '') { + parentChild.push(p.label) + } else { + parentChild.push(`${p.label}: ${thesechildren}`) + } + }) + console.log(parentChild) + return parentChild +} + +const toArray = obj => { + return Array.isArray(obj) ? obj : Array.of(obj) } const getPainInducingTechniques = (details: Partial) => { diff --git a/server/utils/nunjucksSetup.ts b/server/utils/nunjucksSetup.ts index 06b62e0a..3dc92cd9 100644 --- a/server/utils/nunjucksSetup.ts +++ b/server/utils/nunjucksSetup.ts @@ -118,10 +118,18 @@ export default function configureNunjucks(app: Express.Application): nunjucks.En return Array.isArray(value) }) + njkEnv.addFilter('toArray', value => { + return Array.isArray(value) ? value : Array.of(value) + }) + njkEnv.addFilter('toOptions', (array, valueKey, textKey) => { return array.map(item => ({ value: item[valueKey], label: item[textKey], + sub_options_label: item.sub_options_label, + sub_options: item.sub_options, + parent: item.parent, + exclusive: item.exclusive, })) }) diff --git a/server/views/formPages/incident/useOfForceDetails.html b/server/views/formPages/incident/useOfForceDetails.html index 77dd91c5..fca56701 100644 --- a/server/views/formPages/incident/useOfForceDetails.html +++ b/server/views/formPages/incident/useOfForceDetails.html @@ -40,7 +40,7 @@

{{ pageTitle }}

errorMessage: errors | findError('bodyWornCamera'), includeNotKnown: false }, - followUpQuestion:{ + followUpQuestion:{ name: "bodyWornCameraNumbers", value: data.bodyWornCameraNumbers, errorMessage: errors, @@ -140,20 +140,14 @@

{{ pageTitle }}

- {{ incidentMacro.radiosWithNestedCheckboxes({ + {{ incidentMacro.checkboxesWithNestedCheckboxes({ primaryQuestion: { - text: "Was control and restraint used?", - name: "restraint", - value: data.restraint, - options: yesNoOptions, - errorMessage: errors | findError('restraint') - }, - followUpQuestion: { - text: "What positions were used?", + text: "Which control and restraint positions were used?", + hint: "Select all that apply", name: "restraintPositions", - value: data.restraintPositions or [], - errorMessage: errors | findError('restraintPositions'), - options: data.types.ControlAndRestraintPosition | list | extractAttr('value') | toOptions('value', 'label') + value: data.restraintPositions | toArray, + options: data.types.ControlAndRestraintPosition | list | extractAttr('value') | toOptions('value', 'label'), + errorMessage: errors | findError('restraintPositions') } }) }} @@ -194,5 +188,6 @@

{{ pageTitle }}

{% endblock %} {% block script %} + {% endblock %} \ No newline at end of file diff --git a/server/views/formPages/incidentMacros.njk b/server/views/formPages/incidentMacros.njk index 3e16a7e0..518dfd9b 100644 --- a/server/views/formPages/incidentMacros.njk +++ b/server/views/formPages/incidentMacros.njk @@ -364,7 +364,78 @@ {% endmacro %} - +{% macro checkboxesWithNestedCheckboxes(question) %} + {% if question.primaryQuestion.errorMessage %} + {% set govukFormGroupErrorOuter = 'govuk-form-group--error' %} + {% set primaryErrorMessageText = question.primaryQuestion.errorMessage.text %} + {% endif %} + +
+
+ + + + {{question.primaryQuestion.text}} + + + + Error: + {{primaryErrorMessageText}} + + +
+ {{question.primaryQuestion.hint}} +
+ +
+ {% for option in question.primaryQuestion.options %} + {% if option.exclusive %}
or
{% endif %} + {% if not option.parent %} +
+ + +
+ {% if option.sub_options %} +
+ {% for subopt in question.primaryQuestion.options %} + {% if subopt.parent === option.value %} +
+ + +
+ {% endif %} + {% endfor %} +
+ {% endif %} + {% endif %} + {% endfor %} +
+
+
+{% endmacro %} + + {% macro radiosWithNestedTextbox(question) %} {% if question.primaryQuestion.errorMessage.text %} diff --git a/server/views/pages/reportDetailMacro.njk b/server/views/pages/reportDetailMacro.njk index 013ca67c..6f158b5b 100644 --- a/server/views/pages/reportDetailMacro.njk +++ b/server/views/pages/reportDetailMacro.njk @@ -234,10 +234,11 @@ {% endif %} {{ reportDetailsMacros.tableRow({ - label: 'Was control and restraint used?', + label: 'Which control and restraint techniques were used?', 'data-qa': 'restraintUsed', - dataValue: data.useOfForceDetails.controlAndRestraintUsed | capitalize, - print: print + dataValue: data.useOfForceDetails.controlAndRestraintUsed | toArray, + print: print, + type: 'parentChild' }) }} diff --git a/server/views/pages/reportDetailSectionsMacros.njk b/server/views/pages/reportDetailSectionsMacros.njk index da818421..9c5f9848 100644 --- a/server/views/pages/reportDetailSectionsMacros.njk +++ b/server/views/pages/reportDetailSectionsMacros.njk @@ -22,12 +22,19 @@ {% elif dataItem.dataValue | isArray and dataItem.type === 'itemPerLine' %} {%for item in dataItem.dataValue %} {%for value in item %} - {{value}} + {{value}} {% if not loop.last %}
{% endif %} {% endfor%} -
+
{%if item.length > 1 and not loop.last %} -
+
+ {% endif %} + {% endfor %} + {% elif dataItem.dataValue | isArray and dataItem.type === 'parentChild' %} + {%for item in dataItem.dataValue %} + {{item | capitalize}} + {%if item.length > 1 and not loop.last %} +
{% endif %} {% endfor %} {% else %}