From e5d5615fbfec5a1d154c5710fa6a3aecb558786c Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:42:00 +0100 Subject: [PATCH] feat(cb2-13176): VTM - Add guidance for handling documents issued centrally (#1519) * feat(cb2-13164): add issue documents centrally * feat(cb2-13164): issue documents centrally for all relevant templates * feat(cb2-13164): fix linting * feat(cb2-13164): change name from issueDocumentsCentrally to issueRequired to match BE * feat(cb2-13164): fix linting again * feat(cb2-13164): hide for group 1 * feat(cb2-13164): fix linting again again * feat(cb2-13164): set issueRequired to false for fail/abandon * feat(cb2-13176): conditionally show hint text on ADR tests only * feat(cb2-13176): fix linting * feat(cb2-13176): remove issue docs centrally from groups 2 3 4 and add to 15 and 16 * feat(cb2-13176): remove issue required from group 3 * feat(cb2-13176): remove validation for certificate number on groups 3 and 4 * feat(cb2-13176): fix linting * feat(cb2-13176): use nested shape * feat(cb2-13176): fix linting * feat(cb2-13176): ensure reasons for issue is always an array of strings --- src/app/forms/models/async-validators.enum.ts | 1 + src/app/forms/models/validators.enum.ts | 1 + .../forms/services/dynamic-form.service.ts | 44 +- .../test-records/create-master.template.ts | 9 +- .../notes/adr-notes-section.template.ts | 55 ++ ...ency-test-section-group15and16.template.ts | 37 +- ...ontingency-test-section-group7.template.ts | 32 +- ...test-section-specialist-group1.template.ts | 54 +- ...test-section-specialist-group2.template.ts | 8 +- ...specialist-test-section-group1.template.ts | 49 +- .../test-section-group15And16.template.ts | 33 +- .../test/test-section-group7.template.ts | 30 + .../validators/custom-async-validators.ts | 10 + .../validators/custom-validators.spec.ts | 588 +++++++++--------- src/app/forms/validators/custom-validators.ts | 41 +- src/app/models/test-types/test-type.model.ts | 9 + .../reducers/test-records.reducer.ts | 73 ++- 17 files changed, 664 insertions(+), 410 deletions(-) create mode 100644 src/app/forms/templates/test-records/section-templates/notes/adr-notes-section.template.ts diff --git a/src/app/forms/models/async-validators.enum.ts b/src/app/forms/models/async-validators.enum.ts index 4232bbe6ff..cec42b68f7 100644 --- a/src/app/forms/models/async-validators.enum.ts +++ b/src/app/forms/models/async-validators.enum.ts @@ -10,4 +10,5 @@ export enum AsyncValidatorNames { HideIfEqualsWithCondition = 'hideIfEqualsWithCondition', PassResultDependantOnCustomDefects = 'passResultDependantOnCustomDefects', RequiredWhenCarryingDangerousGoods = 'requiredWhenCarryingDangerousGoods', + Custom = 'custom', } diff --git a/src/app/forms/models/validators.enum.ts b/src/app/forms/models/validators.enum.ts index d79b068524..550695214e 100644 --- a/src/app/forms/models/validators.enum.ts +++ b/src/app/forms/models/validators.enum.ts @@ -46,4 +46,5 @@ export enum ValidatorNames { Tc3TestValidator = 'tc3TestValidator', DateIsInvalid = 'dateIsInvalid', MinArrayLengthIfNotEmpty = 'minArrayLengthIfNotEmpty', + IssueRequired = 'issueRequired', } diff --git a/src/app/forms/services/dynamic-form.service.ts b/src/app/forms/services/dynamic-form.service.ts index 61dff9f13c..67d69c5c77 100644 --- a/src/app/forms/services/dynamic-form.service.ts +++ b/src/app/forms/services/dynamic-form.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { - AsyncValidatorFn, FormArray, FormControl, FormGroup, - ValidatorFn, Validators, + AsyncValidatorFn, FormArray, FormControl, FormGroup, ValidatorFn, Validators, } from '@angular/forms'; import { GlobalError } from '@core/components/global-error/global-error.interface'; import { AsyncValidatorNames } from '@forms/models/async-validators.enum'; @@ -25,7 +24,7 @@ type CustomFormFields = CustomFormControl | CustomFormArray | CustomFormGroup; providedIn: 'root', }) export class DynamicFormService { - constructor(private store: Store) { } + constructor(private store: Store) {} // eslint-disable-next-line @typescript-eslint/no-explicit-any validatorMap: Record ValidatorFn> = { @@ -55,7 +54,7 @@ export class DynamicFormService { [ValidatorNames.PastDate]: () => CustomValidators.pastDate, [ValidatorNames.Pattern]: (args: string) => Validators.pattern(args), [ValidatorNames.Required]: () => Validators.required, - [ValidatorNames.RequiredIfEquals]: (args: { sibling: string; value: unknown[], customErrorMessage?: string }) => + [ValidatorNames.RequiredIfEquals]: (args: { sibling: string; value: unknown[]; customErrorMessage?: string }) => CustomValidators.requiredIfEquals(args.sibling, args.value, args.customErrorMessage), [ValidatorNames.requiredIfAllEquals]: (args: { sibling: string; value: unknown[] }) => CustomValidators.requiredIfAllEquals(args.sibling, args.value), @@ -70,17 +69,17 @@ export class DynamicFormService { [ValidatorNames.IsMemberOfEnum]: (args: { enum: Record; options?: Partial }) => CustomValidators.isMemberOfEnum(args.enum, args.options), [ValidatorNames.UpdateFunctionCode]: () => CustomValidators.updateFunctionCode(), - [ValidatorNames.ShowGroupsWhenEqualTo]: (args: { values: unknown[], groups: string[] }) => + [ValidatorNames.ShowGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => CustomValidators.showGroupsWhenEqualTo(args.values, args.groups), - [ValidatorNames.HideGroupsWhenEqualTo]: (args: { values: unknown[], groups: string[] }) => + [ValidatorNames.HideGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => CustomValidators.hideGroupsWhenEqualTo(args.values, args.groups), - [ValidatorNames.ShowGroupsWhenIncludes]: (args: { values: unknown[], groups: string[] }) => + [ValidatorNames.ShowGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => CustomValidators.showGroupsWhenIncludes(args.values, args.groups), - [ValidatorNames.HideGroupsWhenIncludes]: (args: { values: unknown[], groups: string[] }) => + [ValidatorNames.HideGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => CustomValidators.hideGroupsWhenIncludes(args.values, args.groups), - [ValidatorNames.ShowGroupsWhenExcludes]: (args: { values: unknown[], groups: string[] }) => + [ValidatorNames.ShowGroupsWhenExcludes]: (args: { values: unknown[]; groups: string[] }) => CustomValidators.showGroupsWhenExcludes(args.values, args.groups), - [ValidatorNames.HideGroupsWhenExcludes]: (args: { values: unknown[], groups: string[] }) => + [ValidatorNames.HideGroupsWhenExcludes]: (args: { values: unknown[]; groups: string[] }) => CustomValidators.hideGroupsWhenExcludes(args.values, args.groups), [ValidatorNames.AddWarningForAdrField]: (warning: string) => CustomValidators.addWarningForAdrField(warning), [ValidatorNames.IsArray]: (args: Partial) => CustomValidators.isArray(args), @@ -88,8 +87,9 @@ export class DynamicFormService { [ValidatorNames.Tc3TestValidator]: (args: { inspectionNumber: number }) => CustomValidators.tc3TestValidator(args), [ValidatorNames.RequiredIfNotHidden]: () => CustomValidators.requiredIfNotHidden(), [ValidatorNames.DateIsInvalid]: () => CustomValidators.dateIsInvalid, - [ValidatorNames.MinArrayLengthIfNotEmpty]: (args: { minimumLength: number, message: string }) => + [ValidatorNames.MinArrayLengthIfNotEmpty]: (args: { minimumLength: number; message: string }) => CustomValidators.minArrayLengthIfNotEmpty(args.minimumLength, args.message), + [ValidatorNames.IssueRequired]: () => CustomValidators.issueRequired(), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -102,13 +102,16 @@ export class DynamicFormService { [AsyncValidatorNames.RequiredIfNotResult]: (args: { testResult: resultOfTestEnum | resultOfTestEnum[] }) => CustomAsyncValidators.requiredIfNotResult(this.store, args.testResult), [AsyncValidatorNames.RequiredIfNotResultAndSiblingEquals]: (args: { - testResult: resultOfTestEnum | resultOfTestEnum[]; sibling: string; value: unknown + testResult: resultOfTestEnum | resultOfTestEnum[]; + sibling: string; + value: unknown; }) => CustomAsyncValidators.requiredIfNotResultAndSiblingEquals(this.store, args.testResult, args.sibling, args.value), [AsyncValidatorNames.ResultDependantOnCustomDefects]: () => CustomAsyncValidators.resultDependantOnCustomDefects(this.store), [AsyncValidatorNames.ResultDependantOnRequiredStandards]: () => CustomAsyncValidators.resultDependantOnRequiredStandards(this.store), [AsyncValidatorNames.UpdateTesterDetails]: () => CustomAsyncValidators.updateTesterDetails(this.store), [AsyncValidatorNames.UpdateTestStationDetails]: () => CustomAsyncValidators.updateTestStationDetails(this.store), [AsyncValidatorNames.RequiredWhenCarryingDangerousGoods]: () => CustomAsyncValidators.requiredWhenCarryingDangerousGoods(this.store), + [AsyncValidatorNames.Custom]: (...args) => CustomAsyncValidators.custom(this.store, ...args), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -221,19 +224,20 @@ export class DynamicFormService { Object.entries(errors).forEach(([error, data]) => { // If an anchor link is provided, use that, otherwise determine target element from customId or name const defaultAnchorLink = meta?.customId ?? meta?.name; - const anchorLink = typeof data === 'object' && data !== null - ? data.anchorLink ?? defaultAnchorLink - : defaultAnchorLink; + const anchorLink = typeof data === 'object' && data !== null ? data.anchorLink ?? defaultAnchorLink : defaultAnchorLink; // If typeof data is an array, assume we're passing the service multiple global errors - const globalErrors = Array.isArray(data) ? data : [{ - error: meta?.customErrorMessage ?? ErrorMessageMap[`${error}`](data, meta?.customValidatorErrorName ?? meta?.label), - anchorLink, - }]; + const globalErrors = Array.isArray(data) + ? data + : [ + { + error: meta?.customErrorMessage ?? ErrorMessageMap[`${error}`](data, meta?.customValidatorErrorName ?? meta?.label), + anchorLink, + }, + ]; validationErrorList.push(...globalErrors); }); } - } } diff --git a/src/app/forms/templates/test-records/create-master.template.ts b/src/app/forms/templates/test-records/create-master.template.ts index a42180ab83..5ae59ff5d3 100644 --- a/src/app/forms/templates/test-records/create-master.template.ts +++ b/src/app/forms/templates/test-records/create-master.template.ts @@ -7,6 +7,7 @@ import { AdditionalDefectsSection } from './section-templates/additionalDefects/ import { CustomDefectsSection } from './section-templates/customDefects/custom-defects-section.template'; import { DeskBasedEmissionsSection } from './section-templates/emissions/desk-based-emissions-section.template'; import { EmissionsSection } from './section-templates/emissions/emissions-section.template'; +import { AdrNotesSection } from './section-templates/notes/adr-notes-section.template'; import { NotesSection } from './section-templates/notes/notes-section.template'; import { reasonForCreationHiddenSection, reasonForCreationSection } from './section-templates/reasonForCreation/reasonForCreation.template'; import { CreateRequiredSectionHgvTrl } from './section-templates/required/contingency-required-hidden-section-hgv-trl.template'; @@ -34,6 +35,8 @@ import { ContingencyTestSectionSpecialistGroup3And4, } from './section-templates/test/contingency/contingency-test-section-specialist-group3And4.template'; import { ContingencyTestSectionSpecialistGroup5 } from './section-templates/test/contingency/contingency-test-section-specialist-group5.template'; +import { OldIVAContingencyTestSectionSpecialistGroup1 } from './section-templates/test/contingency/old-contingency-specialist-group1.template'; +import { OldIVAContingencyTestSectionSpecialistGroup5 } from './section-templates/test/contingency/old-contingency-specialist-group5.template'; import { DeskBasedTestSectionGroup1Psv } from './section-templates/test/desk-based/desk-based-test-section-group1-PSV.template'; import { DeskBasedTestSectionGroup1And4HgvTrl as DeskBasedTestSectionGroup1And4And5HgvTrl, @@ -58,8 +61,6 @@ import { DeskBasedVehicleSectionGroup5Lgv } from './section-templates/vehicle/de import { VehicleSectionGroup3 } from './section-templates/vehicle/group-3-light-vehicle-section.template'; import { ContingencyVisitSection } from './section-templates/visit/contingency-visit-section.template'; import { VisitSection } from './section-templates/visit/visit-section.template'; -import { OldIVAContingencyTestSectionSpecialistGroup1 } from './section-templates/test/contingency/old-contingency-specialist-group1.template'; -import { OldIVAContingencyTestSectionSpecialistGroup5 } from './section-templates/test/contingency/old-contingency-specialist-group5.template'; const groups1and2Template: Record = { required: CreateRequiredSection, @@ -280,7 +281,7 @@ export const contingencyTestTemplates: Record) => { + return store.pipe( + select(testResultInEdit), + take(1), + map((testResult) => testResult?.testTypes.at(0)?.centralDocs?.issueRequired), + tap((issueRequired) => { + control.meta.hint = issueRequired ? 'Enter a reason for issuing documents centrally' : ''; + }), + map(() => null), + ); + }, + }, + ], + }, + ], + }, + ], + }, + ], +}; diff --git a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group15and16.template.ts b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group15and16.template.ts index 5cfdd2b459..e297897480 100644 --- a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group15and16.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group15and16.template.ts @@ -51,10 +51,38 @@ export const ContingencyTestSectionGroup15and16: FormNode = { { value: 'pass', label: 'Pass' }, { value: 'fail', label: 'Fail' }, ], - validators: [{ name: ValidatorNames.HideIfNotEqual, args: { sibling: 'testExpiryDate', value: 'pass' } }], + validators: [ + { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'testExpiryDate', value: 'pass' } }, + { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'centralDocs', value: ['pass', 'prs'] } }, + ], asyncValidators: [{ name: AsyncValidatorNames.PassResultDependantOnCustomDefects }], type: FormNodeTypes.CONTROL, }, + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'issueRequired', + type: FormNodeTypes.CONTROL, + label: 'Issue documents centrally', + editType: FormNodeEditTypes.RADIO, + value: false, + options: [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ], + validators: [{ name: ValidatorNames.HideIfParentSiblingEqual, args: { sibling: 'certificateNumber', value: true } }], + }, + { + name: 'reasonsForIssue', + type: FormNodeTypes.CONTROL, + viewType: FormNodeViewTypes.HIDDEN, + editType: FormNodeEditTypes.HIDDEN, + value: [], + }, + ], + }, { name: 'reasonForAbandoning', type: FormNodeTypes.CONTROL, @@ -76,7 +104,12 @@ export const ContingencyTestSectionGroup15and16: FormNode = { label: 'Certificate number', type: FormNodeTypes.CONTROL, editType: FormNodeEditTypes.TEXT, - validators: [{ name: ValidatorNames.Required }, { name: ValidatorNames.Alphanumeric }], + validators: [ + { name: ValidatorNames.Required }, + { name: ValidatorNames.Alphanumeric }, + // Make required if test result is pass/prs, but issue documents centrally is false + { name: ValidatorNames.IssueRequired }, + ], viewType: FormNodeViewTypes.HIDDEN, required: true, value: null, diff --git a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group7.template.ts b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group7.template.ts index 93b7789397..a0d95a1d62 100644 --- a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group7.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-group7.template.ts @@ -56,10 +56,36 @@ export const ContingencyTestSectionGroup7: FormNode = { { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'certificateNumber', value: 'pass' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'generateCert', value: 'pass' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'testExpiryDate', value: 'pass' } }, + { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'centralDocs', value: 'pass' } }, ], asyncValidators: [{ name: AsyncValidatorNames.ResultDependantOnCustomDefects }], type: FormNodeTypes.CONTROL, }, + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'issueRequired', + type: FormNodeTypes.CONTROL, + label: 'Issue documents centrally', + editType: FormNodeEditTypes.RADIO, + value: false, + options: [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ], + validators: [{ name: ValidatorNames.HideIfParentSiblingEqual, args: { sibling: 'certificateNumber', value: true } }], + }, + { + name: 'reasonsForIssue', + type: FormNodeTypes.CONTROL, + viewType: FormNodeViewTypes.HIDDEN, + editType: FormNodeEditTypes.HIDDEN, + value: [], + }, + ], + }, { name: 'testTypeName', label: 'Description', @@ -103,10 +129,8 @@ export const ContingencyTestSectionGroup7: FormNode = { type: FormNodeTypes.CONTROL, validators: [ { name: ValidatorNames.Alphanumeric }, - { - name: ValidatorNames.RequiredIfEquals, - args: { sibling: 'testResult', value: ['pass'] }, - }, + // Make required if test result is pass/prs, but issue documents centrally is false + { name: ValidatorNames.IssueRequired }, ], required: true, value: null, diff --git a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group1.template.ts b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group1.template.ts index 1695c14877..9eb0df7116 100644 --- a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group1.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group1.template.ts @@ -53,22 +53,7 @@ export const ContingencyTestSectionSpecialistGroup1: FormNode = { { value: 'fail', label: 'Fail' }, { value: 'prs', label: 'PRS' }, ], - validators: [ - { - name: ValidatorNames.ShowGroupsWhenIncludes, - args: { - values: ['fail'], - groups: ['failOnly'], - }, - }, - { - name: ValidatorNames.HideGroupsWhenExcludes, - args: { - values: ['fail'], - groups: ['failOnly'], - }, - }, - ], + validators: [{ name: ValidatorNames.HideIfNotEqual, args: { sibling: 'centralDocs', value: ['pass', 'prs'] } }], asyncValidators: [ { name: AsyncValidatorNames.ResultDependantOnRequiredStandards }, { @@ -82,6 +67,31 @@ export const ContingencyTestSectionSpecialistGroup1: FormNode = { ], type: FormNodeTypes.CONTROL, }, + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'issueRequired', + type: FormNodeTypes.CONTROL, + label: 'Issue documents centrally', + editType: FormNodeEditTypes.RADIO, + value: false, + options: [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ], + validators: [{ name: ValidatorNames.HideIfParentSiblingEqual, args: { sibling: 'certificateNumber', value: true } }], + }, + { + name: 'reasonsForIssue', + type: FormNodeTypes.CONTROL, + viewType: FormNodeViewTypes.HIDDEN, + editType: FormNodeEditTypes.HIDDEN, + value: [], + }, + ], + }, { name: 'testTypeName', label: 'Description', @@ -114,16 +124,8 @@ export const ContingencyTestSectionSpecialistGroup1: FormNode = { editType: FormNodeEditTypes.TEXT, validators: [ { name: ValidatorNames.Alphanumeric }, - { - name: ValidatorNames.RequiredIfEquals, - args: { - sibling: 'testResult', - value: [ - 'pass', - 'prs', - ], - }, - }, + // Make required if test result is pass/prs, but issue documents centrally is false + { name: ValidatorNames.IssueRequired }, ], required: true, value: null, diff --git a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group2.template.ts b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group2.template.ts index d1b26533fc..189d9e6696 100644 --- a/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group2.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/contingency/contingency-test-section-specialist-group2.template.ts @@ -52,7 +52,12 @@ export const ContingencyTestSectionSpecialistGroup2: FormNode = { { value: 'fail', label: 'Fail' }, { value: 'prs', label: 'PRS' }, ], - validators: [{ name: ValidatorNames.HideIfNotEqual, args: { sibling: 'secondaryCertificateNumber', value: 'pass' } }], + validators: [ + { + name: ValidatorNames.HideIfNotEqual, + args: { sibling: 'secondaryCertificateNumber', value: 'pass' }, + }, + ], asyncValidators: [{ name: AsyncValidatorNames.ResultDependantOnCustomDefects }], type: FormNodeTypes.CONTROL, }, @@ -61,7 +66,6 @@ export const ContingencyTestSectionSpecialistGroup2: FormNode = { label: 'Description', value: '', disabled: true, - type: FormNodeTypes.CONTROL, }, { diff --git a/src/app/forms/templates/test-records/section-templates/test/specialist/specialist-test-section-group1.template.ts b/src/app/forms/templates/test-records/section-templates/test/specialist/specialist-test-section-group1.template.ts index f4e7e47952..eea12026f3 100644 --- a/src/app/forms/templates/test-records/section-templates/test/specialist/specialist-test-section-group1.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/specialist/specialist-test-section-group1.template.ts @@ -60,20 +60,7 @@ export const SpecialistTestSectionGroup1: FormNode = { validators: [ { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'reasonForAbandoning', value: 'abandoned' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'additionalCommentsForAbandon', value: 'abandoned' } }, - { - name: ValidatorNames.ShowGroupsWhenIncludes, - args: { - values: ['fail'], - groups: ['failOnly'], - }, - }, - { - name: ValidatorNames.HideGroupsWhenExcludes, - args: { - values: ['fail'], - groups: ['failOnly'], - }, - }, + { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'centralDocs', value: ['pass', 'prs'] } }, ], asyncValidators: [ { name: AsyncValidatorNames.ResultDependantOnRequiredStandards }, @@ -88,6 +75,31 @@ export const SpecialistTestSectionGroup1: FormNode = { ], type: FormNodeTypes.CONTROL, }, + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'issueRequired', + type: FormNodeTypes.CONTROL, + label: 'Issue documents centrally', + editType: FormNodeEditTypes.RADIO, + value: false, + options: [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ], + validators: [{ name: ValidatorNames.HideIfParentSiblingEqual, args: { sibling: 'certificateNumber', value: true } }], + }, + { + name: 'reasonsForIssue', + type: FormNodeTypes.CONTROL, + viewType: FormNodeViewTypes.HIDDEN, + editType: FormNodeEditTypes.HIDDEN, + value: [], + }, + ], + }, { name: 'reasonForAbandoning', type: FormNodeTypes.CONTROL, @@ -132,13 +144,8 @@ export const SpecialistTestSectionGroup1: FormNode = { editType: FormNodeEditTypes.TEXT, validators: [ { name: ValidatorNames.Alphanumeric }, - { - name: ValidatorNames.RequiredIfEquals, - args: { - sibling: 'testResult', - value: ['pass', 'prs'], - }, - }, + // Make required if test result is pass/prs, but issue documents centrally is false + { name: ValidatorNames.IssueRequired }, ], viewType: FormNodeViewTypes.HIDDEN, width: FormNodeWidth.L, diff --git a/src/app/forms/templates/test-records/section-templates/test/test-section-group15And16.template.ts b/src/app/forms/templates/test-records/section-templates/test/test-section-group15And16.template.ts index 537f267498..7e279c58cb 100644 --- a/src/app/forms/templates/test-records/section-templates/test/test-section-group15And16.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/test-section-group15And16.template.ts @@ -59,10 +59,36 @@ export const TestSectionGroup15And16: FormNode = { { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'reasonForAbandoning', value: 'abandoned' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'additionalCommentsForAbandon', value: 'abandoned' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'testExpiryDate', value: ['pass', 'abandoned'] } }, + { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'centralDocs', value: ['pass', 'prs'] } }, ], asyncValidators: [{ name: AsyncValidatorNames.PassResultDependantOnCustomDefects }], type: FormNodeTypes.CONTROL, }, + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'issueRequired', + type: FormNodeTypes.CONTROL, + label: 'Issue documents centrally', + editType: FormNodeEditTypes.RADIO, + value: false, + options: [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ], + validators: [{ name: ValidatorNames.HideIfParentSiblingEqual, args: { sibling: 'certificateNumber', value: true } }], + }, + { + name: 'reasonsForIssue', + type: FormNodeTypes.CONTROL, + viewType: FormNodeViewTypes.HIDDEN, + editType: FormNodeEditTypes.HIDDEN, + value: [], + }, + ], + }, { name: 'reasonForAbandoning', type: FormNodeTypes.CONTROL, @@ -105,7 +131,12 @@ export const TestSectionGroup15And16: FormNode = { label: 'Certificate number', type: FormNodeTypes.CONTROL, editType: FormNodeEditTypes.TEXT, - validators: [{ name: ValidatorNames.Required }, { name: ValidatorNames.Alphanumeric }], + validators: [ + { name: ValidatorNames.Required }, + { name: ValidatorNames.Alphanumeric }, + // Make required if test result is pass/prs, but issue documents centrally is false + { name: ValidatorNames.IssueRequired }, + ], viewType: FormNodeViewTypes.HIDDEN, required: true, value: null, diff --git a/src/app/forms/templates/test-records/section-templates/test/test-section-group7.template.ts b/src/app/forms/templates/test-records/section-templates/test/test-section-group7.template.ts index bc358b76c2..d6634552b6 100644 --- a/src/app/forms/templates/test-records/section-templates/test/test-section-group7.template.ts +++ b/src/app/forms/templates/test-records/section-templates/test/test-section-group7.template.ts @@ -60,10 +60,36 @@ export const TestSectionGroup7: FormNode = { { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'reasonForAbandoning', value: 'abandoned' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'additionalCommentsForAbandon', value: 'abandoned' } }, { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'certificateNumber', value: ['pass', 'prs', 'abandoned'] } }, + { name: ValidatorNames.HideIfNotEqual, args: { sibling: 'centralDocs', value: ['pass', 'prs'] } }, ], asyncValidators: [{ name: AsyncValidatorNames.ResultDependantOnCustomDefects }], type: FormNodeTypes.CONTROL, }, + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'issueRequired', + type: FormNodeTypes.CONTROL, + label: 'Issue documents centrally', + editType: FormNodeEditTypes.RADIO, + value: false, + options: [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ], + validators: [{ name: ValidatorNames.HideIfParentSiblingEqual, args: { sibling: 'certificateNumber', value: true } }], + }, + { + name: 'reasonsForIssue', + type: FormNodeTypes.CONTROL, + viewType: FormNodeViewTypes.HIDDEN, + editType: FormNodeEditTypes.HIDDEN, + value: [], + }, + ], + }, { name: 'testTypeName', label: 'Description', @@ -76,6 +102,10 @@ export const TestSectionGroup7: FormNode = { label: 'Certificate number', type: FormNodeTypes.CONTROL, viewType: FormNodeViewTypes.HIDDEN, + validators: [ + // Make required if test result is pass/prs, but issue documents centrally is false + { name: ValidatorNames.IssueRequired }, + ], required: true, value: null, }, diff --git a/src/app/forms/validators/custom-async-validators.ts b/src/app/forms/validators/custom-async-validators.ts index 395ed33793..6594efecbd 100644 --- a/src/app/forms/validators/custom-async-validators.ts +++ b/src/app/forms/validators/custom-async-validators.ts @@ -263,4 +263,14 @@ export class CustomAsyncValidators { return operator === operatorEnum.Equals ? isTrue : !isTrue; } + + static custom = ( + store: Store, + func: (...args: unknown[]) => Observable, + ...args: unknown[] + ) => { + return (control: AbstractControl): Observable => { + return func(control, store, ...args); + }; + }; } diff --git a/src/app/forms/validators/custom-validators.spec.ts b/src/app/forms/validators/custom-validators.spec.ts index ca5fa5b20e..64e244323f 100644 --- a/src/app/forms/validators/custom-validators.spec.ts +++ b/src/app/forms/validators/custom-validators.spec.ts @@ -1,19 +1,11 @@ import { - AbstractControl, - FormArray, - FormControl, FormGroup, + AbstractControl, FormArray, FormControl, FormGroup, } from '@angular/forms'; import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum.js'; import { ApprovalType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/approvalType.enum.js'; -import { - VehicleClassDescription, -} from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/vehicleClassDescription.enum.js'; +import { VehicleClassDescription } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/vehicleClassDescription.enum.js'; import { ValidatorNames } from '@forms/models/validators.enum'; -import { - CustomFormControl, - CustomFormGroup, - FormNodeTypes, -} from '@forms/services/dynamic-form.types'; +import { CustomFormControl, CustomFormGroup, FormNodeTypes } from '@forms/services/dynamic-form.types'; import { VehicleSizes, VehicleTypes } from '@models/vehicle-tech-record.model'; import { CustomValidators } from './custom-validators'; @@ -373,7 +365,9 @@ describe('customPattern', () => { const validation = customPattern(new FormControl(input)); expect(validation).toEqual(expected); if (validation) { - const { customPattern: { message } } = validation; + const { + customPattern: { message }, + } = validation; // eslint-disable-next-line jest/no-conditional-expect expect(message).toEqual(msg); } else { @@ -737,32 +731,31 @@ describe('showGroupsWhenEqualTo', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: false, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_applicantDetails_name', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['adr'], - }, - { - name: 'techRecord_adrDetails_applicantDetails_street', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['adr'], - }, - { - name: 'techRecord_adrDetails_applicantDetails_town', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['adr'], - }, - ], + children: [ + { + name: 'dangerousGoods', + value: false, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_applicantDetails_name', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['adr'], + }, + { + name: 'techRecord_adrDetails_applicantDetails_street', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['adr'], + }, + { + name: 'techRecord_adrDetails_applicantDetails_town', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['adr'], + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -867,32 +860,31 @@ describe('hideGroupsWhenEqualTo', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: false, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_applicantDetails_name', - type: FormNodeTypes.CONTROL, - hide: false, - groups: ['adr'], - }, - { - name: 'techRecord_adrDetails_applicantDetails_street', - type: FormNodeTypes.CONTROL, - hide: false, - groups: ['adr'], - }, - { - name: 'techRecord_adrDetails_applicantDetails_town', - type: FormNodeTypes.CONTROL, - hide: false, - groups: ['adr'], - }, - ], + children: [ + { + name: 'dangerousGoods', + value: false, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_applicantDetails_name', + type: FormNodeTypes.CONTROL, + hide: false, + groups: ['adr'], + }, + { + name: 'techRecord_adrDetails_applicantDetails_street', + type: FormNodeTypes.CONTROL, + hide: false, + groups: ['adr'], + }, + { + name: 'techRecord_adrDetails_applicantDetails_town', + type: FormNodeTypes.CONTROL, + hide: false, + groups: ['adr'], + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -997,19 +989,18 @@ describe('addWarningIfFalse', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: true, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_applicantDetails_name', - type: FormNodeTypes.CONTROL, - hide: false, - }, - ], + children: [ + { + name: 'dangerousGoods', + value: true, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_applicantDetails_name', + type: FormNodeTypes.CONTROL, + hide: false, + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -1145,20 +1136,19 @@ describe('showGroupsWhenIncludes', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: false, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_compatibilityGroupJ', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['compat', 'details'], - }, - ], + children: [ + { + name: 'dangerousGoods', + value: false, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_compatibilityGroupJ', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['compat', 'details'], + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -1263,20 +1253,19 @@ describe('hideGroupsWhenIncludes', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: false, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_compatibilityGroupJ', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['compat', 'details'], - }, - ], + children: [ + { + name: 'dangerousGoods', + value: false, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_compatibilityGroupJ', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['compat', 'details'], + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -1382,20 +1371,19 @@ describe('showGroupsWhenExcludes', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: false, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_compatibilityGroupJ', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['compat', 'details'], - }, - ], + children: [ + { + name: 'dangerousGoods', + value: false, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_compatibilityGroupJ', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['compat', 'details'], + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -1499,20 +1487,19 @@ describe('hideGroupsWhenExcludes', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'dangerousGoods', - value: false, - type: FormNodeTypes.CONTROL, - }, - { - name: 'techRecord_adrDetails_compatibilityGroupJ', - type: FormNodeTypes.CONTROL, - hide: true, - groups: ['compat', 'details'], - }, - ], + children: [ + { + name: 'dangerousGoods', + value: false, + type: FormNodeTypes.CONTROL, + }, + { + name: 'techRecord_adrDetails_compatibilityGroupJ', + type: FormNodeTypes.CONTROL, + hide: true, + groups: ['compat', 'details'], + }, + ], }, { dangerousGoods: new CustomFormControl( @@ -1619,14 +1606,13 @@ describe('isArray', () => { { name: 'form-group', type: FormNodeTypes.GROUP, - children: - [ - { - name: 'techRecord_adrDetails_additionalNotes_number', - type: FormNodeTypes.CONTROL, - value: [], - }, - ], + children: [ + { + name: 'techRecord_adrDetails_additionalNotes_number', + type: FormNodeTypes.CONTROL, + value: [], + }, + ], }, { techRecord_adrDetails_additionalNotes_number: new CustomFormControl( @@ -1704,63 +1690,61 @@ describe('tc3FieldTestValidator', () => { let form: FormGroup; beforeEach(() => { - form = new CustomFormGroup({ - name: 'group', - label: 'Subsequent', - type: FormNodeTypes.GROUP, - children: [ - { + form = new CustomFormGroup( + { + name: 'group', + label: 'Subsequent', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'tc3Type', + type: FormNodeTypes.CONTROL, + value: null, + label: 'TC3: Inspection Type', + }, + { + name: 'tc3PeriodicNumber', + label: 'TC3: Certificate Number', + value: null, + type: FormNodeTypes.CONTROL, + }, + { + name: 'tc3PeriodicExpiryDate', + label: 'TC3: Expiry Date', + type: FormNodeTypes.CONTROL, + value: null, + isoDate: false, + }, + ], + }, + { + tc3Type: new CustomFormControl({ name: 'tc3Type', type: FormNodeTypes.CONTROL, - value: null, - label: 'TC3: Inspection Type', - }, - { + }), + tc3PeriodicNumber: new CustomFormControl({ name: 'tc3PeriodicNumber', - label: 'TC3: Certificate Number', - value: null, type: FormNodeTypes.CONTROL, - }, - { + }), + tc3PeriodicExpiryDate: new CustomFormControl({ name: 'tc3PeriodicExpiryDate', - label: 'TC3: Expiry Date', type: FormNodeTypes.CONTROL, - value: null, - isoDate: false, - }, - ], - }, { - tc3Type: new CustomFormControl({ - name: 'tc3Type', - type: FormNodeTypes.CONTROL, - }), - tc3PeriodicNumber: new CustomFormControl({ - name: 'tc3PeriodicNumber', - type: FormNodeTypes.CONTROL, - }), - tc3PeriodicExpiryDate: new CustomFormControl({ - name: 'tc3PeriodicExpiryDate', - type: FormNodeTypes.CONTROL, - }), - }); + }), + }, + ); }); it('should give an error if all fields passed to the validator are null', () => { const type = form.get('tc3Type') as CustomFormControl; - const validator = CustomValidators.tc3TestValidator( - { - inspectionNumber: 1, - }, - )(type as AbstractControl); + const validator = CustomValidators.tc3TestValidator({ + inspectionNumber: 1, + })(type as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should give an error if fields passed to the validator are undefined', () => { const type = form.get('tc3Type') as CustomFormControl; @@ -1771,20 +1755,15 @@ describe('tc3FieldTestValidator', () => { date.patchValue(undefined); number.patchValue(undefined); - const validator = CustomValidators.tc3TestValidator( - { - inspectionNumber: 1, - }, - )(type as AbstractControl); + const validator = CustomValidators.tc3TestValidator({ + inspectionNumber: 1, + })(type as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should give an error if fields passed to the validator are empty strings', () => { const type = form.get('tc3Type') as CustomFormControl; @@ -1795,20 +1774,15 @@ describe('tc3FieldTestValidator', () => { date.patchValue(''); number.patchValue(''); - const validator = CustomValidators.tc3TestValidator( - { - inspectionNumber: 1, - }, - )(type as AbstractControl); + const validator = CustomValidators.tc3TestValidator({ + inspectionNumber: 1, + })(type as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should give an error if fields to the validator have a variety of empty values', () => { const type = form.get('tc3Type') as CustomFormControl; @@ -1819,20 +1793,15 @@ describe('tc3FieldTestValidator', () => { date.patchValue(null); number.patchValue(undefined); - const validator = CustomValidators.tc3TestValidator( - { - inspectionNumber: 1, - }, - )(type as AbstractControl); + const validator = CustomValidators.tc3TestValidator({ + inspectionNumber: 1, + })(type as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should return null if one field passed to the validator has a value', () => { const type = form.get('tc3Type') as CustomFormControl; @@ -1840,11 +1809,9 @@ describe('tc3FieldTestValidator', () => { number.patchValue('test'); - const validator = CustomValidators.tc3TestValidator( - { - inspectionNumber: 1, - }, - )(type as AbstractControl); + const validator = CustomValidators.tc3TestValidator({ + inspectionNumber: 1, + })(type as AbstractControl); expect(validator).toBeNull(); }); @@ -1854,23 +1821,26 @@ describe('tc3ParentValidator', () => { let form: FormGroup; beforeEach(() => { - form = new CustomFormGroup({ - name: 'group', - label: 'Subsequent', - type: FormNodeTypes.GROUP, - children: [ - { + form = new CustomFormGroup( + { + name: 'group', + label: 'Subsequent', + type: FormNodeTypes.GROUP, + children: [ + { + name: 'techRecord_adrDetails_tank_tankDetails_tc3Details', + type: FormNodeTypes.CONTROL, + value: null, + }, + ], + }, + { + techRecord_adrDetails_tank_tankDetails_tc3Details: new CustomFormControl({ name: 'techRecord_adrDetails_tank_tankDetails_tc3Details', type: FormNodeTypes.CONTROL, - value: null, - }, - ], - }, { - techRecord_adrDetails_tank_tankDetails_tc3Details: new CustomFormControl({ - name: 'techRecord_adrDetails_tank_tankDetails_tc3Details', - type: FormNodeTypes.CONTROL, - }), - }); + }), + }, + ); }); it('should give an error if value contains a test with all null values', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -1879,14 +1849,11 @@ describe('tc3ParentValidator', () => { const validator = CustomValidators.tc3TestValidator({ inspectionNumber: 0 })(details as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should give an error if fields passed to the validator are undefined', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -1895,14 +1862,11 @@ describe('tc3ParentValidator', () => { const validator = CustomValidators.tc3TestValidator({ inspectionNumber: 0 })(details as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should give an error if fields passed to the validator are empty strings', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -1911,14 +1875,11 @@ describe('tc3ParentValidator', () => { const validator = CustomValidators.tc3TestValidator({ inspectionNumber: 0 })(details as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should give an error if fields to the validator have a variety of empty values', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -1927,14 +1888,11 @@ describe('tc3ParentValidator', () => { const validator = CustomValidators.tc3TestValidator({ inspectionNumber: 0 })(details as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 1 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 1 must have at least one populated field', }, - ); + }); }); it('should tell you which test needs to be filled out', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -1947,14 +1905,11 @@ describe('tc3ParentValidator', () => { const validator = CustomValidators.tc3TestValidator({ inspectionNumber: 0 })(details as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 2 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 2 must have at least one populated field', }, - ); + }); }); it('should tell you which test needs to be filled out if there are multiple', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -1969,14 +1924,11 @@ describe('tc3ParentValidator', () => { const validator = CustomValidators.tc3TestValidator({ inspectionNumber: 0 })(details as AbstractControl); - expect(validator).toEqual( - { - tc3TestValidator: - { - message: 'TC3 Subsequent inspection 2, 4, 5 must have at least one populated field', - }, + expect(validator).toEqual({ + tc3TestValidator: { + message: 'TC3 Subsequent inspection 2, 4, 5 must have at least one populated field', }, - ); + }); }); it('should return null if one field passed to the validator has a value', () => { const details = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as CustomFormControl; @@ -2036,9 +1988,11 @@ describe('minArrayLengthIfNotEmpty', () => { }), ]); - expect(CustomValidators.minArrayLengthIfNotEmpty(2, 'Error message')(formArray)).toStrictEqual(expect.objectContaining({ - minArrayLengthIfNotEmpty: { message: 'Error message' }, - })); + expect(CustomValidators.minArrayLengthIfNotEmpty(2, 'Error message')(formArray)).toStrictEqual( + expect.objectContaining({ + minArrayLengthIfNotEmpty: { message: 'Error message' }, + }), + ); }); it('should return null if the minimum length is reached', () => { @@ -2072,3 +2026,63 @@ describe('minArrayLengthIfNotEmpty', () => { expect(CustomValidators.minArrayLengthIfNotEmpty(2, 'Error message')(formArray)).toBeNull(); }); }); + +describe('IssueRequired', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + testResult: new CustomFormControl({ type: FormNodeTypes.CONTROL, name: 'testResult' }, 'pass'), + certificateNumber: new CustomFormControl({ type: FormNodeTypes.CONTROL, name: 'certificateNumber' }, null), + centralDocs: new CustomFormGroup( + { + name: 'centralDocs', + type: FormNodeTypes.GROUP, + children: [{ type: FormNodeTypes.CONTROL, name: 'issueRequired' }], + }, + { + issueRequired: new CustomFormControl({ type: FormNodeTypes.CONTROL, name: 'issueRequired' }, true), + }, + ), + }); + }); + + describe('when issueRequired is true', () => { + beforeEach(() => { + form.get(['centralDocs', 'issueRequired'])?.patchValue(true); + }); + + it('should return null when testResult is prs', () => { + form.get('testResult')?.patchValue('prs'); + const control = form.get('certificateNumber') as FormControl; + expect(CustomValidators.issueRequired()(control)).toBeNull(); + }); + + it('should return null when testResult is pass', () => { + form.get('testResult')?.patchValue('pass'); + const control = form.get('certificateNumber') as FormControl; + expect(CustomValidators.issueRequired()(control)).toBeNull(); + }); + }); + + describe('when issueRequired is false', () => { + beforeEach(() => { + form.get(['centralDocs', 'issueRequired'])?.patchValue(false); + }); + it('should return null when testResult is pass, but the control is populated', () => { + form.get('testResult')?.patchValue('pass'); + const control = form.get('certificateNumber') as FormControl; + control.patchValue('value'); + expect(CustomValidators.issueRequired()(control)).toBeNull(); + }); + + it('should return error when testResult is pass, but the control is not populated', () => { + form.get('testResult')?.patchValue('pass'); + const control = form.get('certificateNumber') as FormControl; + control.patchValue(null); + expect(CustomValidators.issueRequired()(control)).toEqual({ + requiredIfEquals: { customErrorMessage: undefined, sibling: undefined }, + }); + }); + }); +}); diff --git a/src/app/forms/validators/custom-validators.ts b/src/app/forms/validators/custom-validators.ts index 41994a60c9..e93ffdc8bc 100644 --- a/src/app/forms/validators/custom-validators.ts +++ b/src/app/forms/validators/custom-validators.ts @@ -1,6 +1,4 @@ -import { - AbstractControl, ValidationErrors, ValidatorFn, -} from '@angular/forms'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { VehicleClassDescription } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/vehicleClassDescription.enum.js'; // eslint-disable-next-line import/no-cycle import { CustomFormControl, CustomFormGroup } from '@forms/services/dynamic-form.types'; @@ -139,18 +137,14 @@ export class CustomValidators { const isSiblingVisible = !siblingControl.meta.hide; - const isSiblingValueIncluded = Array.isArray(siblingValue) - ? siblingValue.every((val) => values.includes(val)) - : values.includes(siblingValue); + const isSiblingValueIncluded = Array.isArray(siblingValue) ? siblingValue.every((val) => values.includes(val)) : values.includes(siblingValue); const isControlValueEmpty = control.value === null || control.value === undefined || control.value === '' || (Array.isArray(control.value) && (control.value.length === 0 || control.value.every((val) => !val))); - return isSiblingValueIncluded && isControlValueEmpty && isSiblingVisible - ? { requiredIfEquals: { sibling: siblingControl.meta.label } } - : null; + return isSiblingValueIncluded && isControlValueEmpty && isSiblingVisible ? { requiredIfEquals: { sibling: siblingControl.meta.label } } : null; }; static requiredIfNotEquals = (sibling: string, value: unknown): ValidatorFn => { @@ -505,9 +499,7 @@ export class CustomValidators { const { sibling, value } = options.whenEquals; const siblingControl = control.parent?.get(sibling); const siblingValue = siblingControl?.value; - const isSiblingValueIncluded = Array.isArray(siblingValue) - ? value.some((v) => siblingValue.includes(v)) - : value.includes(siblingValue); + const isSiblingValueIncluded = Array.isArray(siblingValue) ? value.some((v) => siblingValue.includes(v)) : value.includes(siblingValue); if (!isSiblingValueIncluded) return null; } @@ -516,16 +508,12 @@ export class CustomValidators { if (options.ofType) { const index = control.value.findIndex((val) => typeof val !== options.ofType); - return index === -1 - ? null - : { isArray: { message: `${index + 1} must be of type ${options.ofType}` } }; + return index === -1 ? null : { isArray: { message: `${index + 1} must be of type ${options.ofType}` } }; } if (options.requiredIndices) { const index = control.value.findIndex((val, i) => options.requiredIndices?.includes(i) && !val); - return index === -1 - ? null - : { isArray: { message: `${index + 1} is required` } }; + return index === -1 ? null : { isArray: { message: `${index + 1} is required` } }; } return null; @@ -580,6 +568,19 @@ export class CustomValidators { return null; }; }; + + static issueRequired = (): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const isPRS = control.parent?.value.testResult === 'prs'; + const isPass = control.parent?.value.testResult === 'pass'; + const issueRequired = control.parent?.value.centralDocs?.issueRequired; + if ((isPRS || isPass) && issueRequired) { + return null; + } + + return CustomValidators.requiredIfEquals('testResult', ['pass'])(control); + }; + }; } export type EnumValidatorOptions = { @@ -589,10 +590,10 @@ export type EnumValidatorOptions = { export type IsArrayValidatorOptions = { ofType: string; requiredIndices: number[]; - whenEquals: { sibling: string, value: unknown[] } + whenEquals: { sibling: string; value: unknown[] }; }; -const areTc3FieldsEmpty = (values: { tc3Type: string, tc3PeriodicNumber: string, tc3PeriodicExpiryDate: string }[]) => { +const areTc3FieldsEmpty = (values: { tc3Type: string; tc3PeriodicNumber: string; tc3PeriodicExpiryDate: string }[]) => { const isValueEmpty: boolean[] = []; values.forEach((value) => { diff --git a/src/app/models/test-types/test-type.model.ts b/src/app/models/test-types/test-type.model.ts index e3939849ef..e058699035 100644 --- a/src/app/models/test-types/test-type.model.ts +++ b/src/app/models/test-types/test-type.model.ts @@ -39,6 +39,15 @@ export interface TestType { certificateLink?: string | null; deletionFlag?: boolean; secondaryCertificateNumber?: string | null; + reapplicationDate?: string; + + centralDocs?: CentralDocs; +} + +export interface CentralDocs { + issueRequired: boolean; + notes?: string; + reasonsForIssue?: string[]; } export interface CustomDefects { diff --git a/src/app/store/test-records/reducers/test-records.reducer.ts b/src/app/store/test-records/reducers/test-records.reducer.ts index aed052d4f7..f54cd8081b 100644 --- a/src/app/store/test-records/reducers/test-records.reducer.ts +++ b/src/app/store/test-records/reducers/test-records.reducer.ts @@ -50,7 +50,7 @@ interface Extras { sectionTemplates?: FormNode[]; } -export interface TestResultsState extends EntityState, Extras { } +export interface TestResultsState extends EntityState, Extras {} const selectTestResultId = (a: TestResultModel): string => { return a.testResultId; @@ -100,15 +100,23 @@ export const testResultsReducer = createReducer( on(updateDefect, (state, action) => ({ ...state, editingTestResult: updateDefectAtIndex(state.editingTestResult, action.defect, action.index) })), on(removeDefect, (state, action) => ({ ...state, editingTestResult: removeDefectAtIndex(state.editingTestResult, action.index) })), - on(createRequiredStandard, (state, action) => - ({ ...state, editingTestResult: createNewRequiredStandard(state.editingTestResult, action.requiredStandard) })), - on(updateRequiredStandard, (state, action) => - ({ ...state, editingTestResult: updateRequiredStandardAtIndex(state.editingTestResult, action.requiredStandard, action.index) })), - on(removeRequiredStandard, (state, action) => - ({ ...state, editingTestResult: removeRequiredStandardAtIndex(state.editingTestResult, action.index) })), + on(createRequiredStandard, (state, action) => ({ + ...state, + editingTestResult: createNewRequiredStandard(state.editingTestResult, action.requiredStandard), + })), + on(updateRequiredStandard, (state, action) => ({ + ...state, + editingTestResult: updateRequiredStandardAtIndex(state.editingTestResult, action.requiredStandard, action.index), + })), + on(removeRequiredStandard, (state, action) => ({ + ...state, + editingTestResult: removeRequiredStandardAtIndex(state.editingTestResult, action.index), + })), - on(updateResultOfTestRequiredStandards, (state) => - ({ ...state, editingTestResult: calculateTestResultRequiredStandards(state.editingTestResult) })), + on(updateResultOfTestRequiredStandards, (state) => ({ + ...state, + editingTestResult: calculateTestResultRequiredStandards(state.editingTestResult), + })), on(cleanTestResult, (state) => ({ ...state, editingTestResult: cleanTestResultPayload(state.editingTestResult) })), ); @@ -130,13 +138,38 @@ function createNewRequiredStandard(testResultState: TestResultModel | undefined, } function cleanTestResultPayload(testResult: TestResultModel | undefined) { - if (testResult?.testTypes?.at(0)) { - const { testTypeId, requiredStandards } = testResult.testTypes[0]; - if ((TEST_TYPES_GROUP1_SPEC_TEST.includes(testTypeId) || TEST_TYPES_GROUP5_SPEC_TEST.includes(testTypeId)) && !(requiredStandards ?? []).length) { - delete testResult.testTypes[0].requiredStandards; - } + if (!testResult || !testResult.testTypes) { + return testResult; } - return testResult; + + const testTypes = testResult.testTypes.map((testType, index) => { + // Remove empty requiredStandards from pass/prs non-voluntary IVA/MVSA tests + if (index === 0) { + const { testTypeId, requiredStandards } = testType; + const isGroup1SpecTest = TEST_TYPES_GROUP1_SPEC_TEST.includes(testTypeId); + const isGroup5SpecTest = TEST_TYPES_GROUP5_SPEC_TEST.includes(testTypeId); + if ((isGroup1SpecTest || isGroup5SpecTest) && !(requiredStandards ?? []).length) { + delete testType.requiredStandards; + } + } + + // If the test type is a fail/cancel/abandon, and issueRequired is true, set it to false + const isFail = testType.testResult === resultOfTestEnum.fail; + const isAbandon = testType.testResult === resultOfTestEnum.abandoned; + if ((isFail || isAbandon) && testType.centralDocs?.issueRequired) { + testType.centralDocs.issueRequired = false; + } + + // If test type has issueRequired set to true, set the certificateNumber/secondaryCertificateNumber to 000000 + if (testType.centralDocs?.issueRequired) { + testType.certificateNumber = '000000'; + testType.secondaryCertificateNumber = '000000'; + } + + return testType; + }); + + return { ...testResult, testTypes }; } function updateRequiredStandardAtIndex(testResultState: TestResultModel | undefined, requiredStandard: TestResultRequiredStandard, index: number) { @@ -223,9 +256,7 @@ function calculateTestResult(testResultState: TestResultModel | undefined): Test } const failOrPrs = testType.defects.some( - (defect) => - defect.deficiencyCategory === DeficiencyCategoryEnum.Major - || defect.deficiencyCategory === DeficiencyCategoryEnum.Dangerous, + (defect) => defect.deficiencyCategory === DeficiencyCategoryEnum.Major || defect.deficiencyCategory === DeficiencyCategoryEnum.Dangerous, ); if (!failOrPrs) { testType.testResult = resultOfTestEnum.pass; @@ -264,11 +295,7 @@ function calculateTestResultRequiredStandards(testResultState: TestResultModel | return testType; } - testType.testResult = testType.requiredStandards.every( - (rs) => rs.prs, - ) - ? resultOfTestEnum.prs - : resultOfTestEnum.fail; + testType.testResult = testType.requiredStandards.every((rs) => rs.prs) ? resultOfTestEnum.prs : resultOfTestEnum.fail; return testType; });