From e653a0ab67f9c973758c21b181721664ef4b42c4 Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:06:39 +0100 Subject: [PATCH 001/211] chore(cb2-0000): basic form --- .../tech-record-summary.component.html | 241 ++-- .../tech-record-summary.component.ts | 447 ++++--- .../radio-group/radio-group.component.ts | 46 +- .../__tests__/adr-section.component.spec.ts | 0 .../adr-section/adr-section.component.html | 1055 +++++++++++++++++ .../adr-section/adr-section.component.scss | 0 .../adr-section/adr-section.component.ts | 375 ++++++ src/app/forms/dynamic-forms.module.ts | 231 ++-- src/assets/featureToggle.int.json | 11 +- src/assets/featureToggle.json | 3 +- src/assets/featureToggle.preprod.json | 11 +- src/assets/featureToggle.prod.json | 3 +- 12 files changed, 1930 insertions(+), 493 deletions(-) create mode 100644 src/app/forms/custom-sections/adr-section/__tests__/adr-section.component.spec.ts create mode 100644 src/app/forms/custom-sections/adr-section/adr-section.component.html create mode 100644 src/app/forms/custom-sections/adr-section/adr-section.component.scss create mode 100644 src/app/forms/custom-sections/adr-section/adr-section.component.ts diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html index 6b39cc00c5..8dd9103203 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html @@ -1,5 +1,5 @@
-
{{ hint }}
+
Complete all required fields to create a testable record
@@ -19,133 +19,144 @@
- - - - - - + + + - + > - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + - - - - + + + + + + + + + + + + + + + + + + + - - + + - + diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts index f3f79069b9..c8b0617bef 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts @@ -1,15 +1,17 @@ import { ViewportScroller } from '@angular/common'; import { - ChangeDetectionStrategy, - Component, - EventEmitter, - OnDestroy, - OnInit, - Output, - QueryList, - ViewChild, - ViewChildren, + ChangeDetectionStrategy, + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, + QueryList, + ViewChild, + ViewChildren, + inject, } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { GlobalError } from '@core/components/global-error/global-error.interface'; import { GlobalErrorService } from '@core/components/global-error/global-error.service'; @@ -42,226 +44,213 @@ import { cloneDeep, mergeWith } from 'lodash'; import { Observable, Subject, debounceTime, map, take, takeUntil } from 'rxjs'; @Component({ - selector: 'app-tech-record-summary', - templateUrl: './tech-record-summary.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - styleUrls: ['./tech-record-summary.component.scss'], + selector: 'app-tech-record-summary', + templateUrl: './tech-record-summary.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./tech-record-summary.component.scss'], }) export class TechRecordSummaryComponent implements OnInit, OnDestroy { - @ViewChildren(DynamicFormGroupComponent) sections!: QueryList; - @ViewChild(BodyComponent) body!: BodyComponent; - @ViewChild(DimensionsComponent) dimensions!: DimensionsComponent; - @ViewChild(PsvBrakesComponent) psvBrakes!: PsvBrakesComponent; - @ViewChild(TrlBrakesComponent) trlBrakes!: TrlBrakesComponent; - @ViewChild(TyresComponent) tyres!: TyresComponent; - @ViewChild(WeightsComponent) weights!: WeightsComponent; - @ViewChild(LettersComponent) letters!: LettersComponent; - @ViewChild(ApprovalTypeComponent) approvalType!: ApprovalTypeComponent; - @ViewChild(AdrComponent) adr!: AdrComponent; - - @Output() isFormDirty = new EventEmitter(); - @Output() isFormInvalid = new EventEmitter(); - - techRecordCalculated?: V3TechRecordModel; - sectionTemplates: Array = []; - middleIndex = 0; - isEditing = false; - scrollPosition: [number, number] = [0, 0]; - isADREnabled = false; - isADRCertGenEnabled = false; - - private destroy$ = new Subject(); - - constructor( - private axlesService: AxlesService, - private errorService: GlobalErrorService, - private warningService: GlobalWarningService, - private referenceDataService: ReferenceDataService, - private technicalRecordService: TechnicalRecordService, - private routerService: RouterService, - private activatedRoute: ActivatedRoute, - private viewportScroller: ViewportScroller, - private store: Store, - private loading: LoadingService, - private featureToggleService: FeatureToggleService - ) {} - - ngOnInit(): void { - this.isADREnabled = this.featureToggleService.isFeatureEnabled('adrToggle'); - this.isADRCertGenEnabled = this.featureToggleService.isFeatureEnabled('adrCertToggle'); - this.technicalRecordService.techRecord$ - .pipe( - map((record) => { - if (!record) { - return; - } - - let techRecord = cloneDeep(record); - techRecord = this.normaliseAxles(record); - - return techRecord; - }), - takeUntil(this.destroy$) - ) - .subscribe((techRecord) => { - if (techRecord) { - this.techRecordCalculated = techRecord; - } - this.referenceDataService.removeTyreSearch(); - this.sectionTemplates = this.vehicleTemplates; - this.middleIndex = Math.floor(this.sectionTemplates.length / 2); - }); - - const editingReason = this.activatedRoute.snapshot.data['reason']; - if (this.isEditing) { - this.technicalRecordService.clearReasonForCreation(); - this.technicalRecordService.techRecord$.pipe(takeUntil(this.destroy$), take(1)).subscribe((techRecord) => { - if (techRecord) { - if (editingReason === ReasonForEditing.NOTIFIABLE_ALTERATION_NEEDED) { - this.technicalRecordService.updateEditingTechRecord({ - ...(techRecord as TechRecordType<'put'>), - techRecord_statusCode: StatusCodes.PROVISIONAL, - }); - } - - if (techRecord?.vin?.match('([IOQ])a*')) { - const warnings: GlobalWarning[] = []; - warnings.push({ warning: 'VIN should not contain I, O or Q', anchorLink: 'vin' }); - this.warningService.setWarnings(warnings); - } - } - }); - } else if (!this.isEditing) { - this.warningService.clearWarnings(); - } - - this.store - .select(selectScrollPosition) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((position) => { - this.scrollPosition = position; - }); - - this.loading.showSpinner$.pipe(takeUntil(this.destroy$), debounceTime(10)).subscribe((loading) => { - if (!loading) { - this.viewportScroller.scrollToPosition(this.scrollPosition); - } - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get vehicleType() { - return this.techRecordCalculated - ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordCalculated) - : undefined; - } - - get vehicleTemplates(): Array { - this.isEditing$.pipe(takeUntil(this.destroy$)).subscribe((editing) => { - this.isEditing = editing; - }); - if (!this.vehicleType) { - return []; - } - return ( - vehicleTemplateMap - .get(this.vehicleType) - ?.filter((template) => template.name !== (this.isEditing ? 'audit' : 'reasonForCreationSection')) - .filter((template) => template.name !== (this.isADREnabled ? '' : 'adrSection')) - .filter((template) => template.name !== (this.isADRCertGenEnabled ? '' : 'adrCertificateSection')) ?? [] - ); - } - - get sectionTemplatesState$() { - return this.technicalRecordService.sectionStates$; - } - - isSectionExpanded$(sectionName: string | number) { - return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); - } - - get isEditing$(): Observable { - return this.routerService.getRouteDataProperty$('isEditing').pipe(map((isEditing) => !!isEditing)); - } - - get hint(): string { - return 'Complete all required fields to create a testable record'; - } - - get customSectionForms(): Array { - const commonCustomSections = [ - this.body?.form, - this.dimensions?.form, - this.tyres?.form, - this.weights?.form, - this.approvalType?.form, - ]; - - switch (this.vehicleType) { - case VehicleTypes.PSV: - return [...commonCustomSections, this.psvBrakes.form]; - case VehicleTypes.HGV: - return this.isADREnabled ? [...commonCustomSections, this.adr.form] : commonCustomSections; - case VehicleTypes.TRL: - return this.isADREnabled - ? [...commonCustomSections, this.trlBrakes.form, this.letters.form, this.adr.form] - : [...commonCustomSections, this.trlBrakes.form, this.letters.form]; - case VehicleTypes.LGV: - return this.isADREnabled ? [this.adr.form] : []; - default: - return []; - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handleFormState(event: any): void { - const isPrimitiveArray = (a: unknown, b: unknown) => - Array.isArray(a) && !a.some((i) => typeof i === 'object') ? b : undefined; - - this.techRecordCalculated = mergeWith(cloneDeep(this.techRecordCalculated), event, isPrimitiveArray); - this.technicalRecordService.updateEditingTechRecord(this.techRecordCalculated as TechRecordType<'put'>); - } - - checkForms(): void { - const forms = this.sections?.map((section) => section.form).concat(this.customSectionForms); - - this.isFormDirty.emit(forms.some((form) => form.dirty)); - - this.setErrors(forms); - - this.isFormInvalid.emit(forms.some((form) => form.invalid)); - } - - setErrors(forms: Array): void { - const errors: GlobalError[] = []; - - forms.forEach((form) => DynamicFormService.validate(form, errors)); - - if (errors.length) { - this.errorService.setErrors(errors); - } else { - this.errorService.clearErrors(); - } - } - - private normaliseAxles(record: V3TechRecordModel): V3TechRecordModel { - const type = record.techRecord_vehicleType; - const category = record.techRecord_euVehicleCategory; - - if (type === VehicleTypes.HGV || (type === VehicleTypes.TRL && category !== 'o1' && category !== 'o2')) { - const [axles, axleSpacing] = this.axlesService.normaliseAxles( - record.techRecord_axles ?? [], - record.techRecord_dimensions_axleSpacing - ); - - record.techRecord_dimensions_axleSpacing = axleSpacing; - record.techRecord_axles = axles; - } - - return record; - } + @ViewChildren(DynamicFormGroupComponent) sections!: QueryList; + @ViewChild(BodyComponent) body!: BodyComponent; + @ViewChild(DimensionsComponent) dimensions!: DimensionsComponent; + @ViewChild(PsvBrakesComponent) psvBrakes!: PsvBrakesComponent; + @ViewChild(TrlBrakesComponent) trlBrakes!: TrlBrakesComponent; + @ViewChild(TyresComponent) tyres!: TyresComponent; + @ViewChild(WeightsComponent) weights!: WeightsComponent; + @ViewChild(LettersComponent) letters!: LettersComponent; + @ViewChild(ApprovalTypeComponent) approvalType!: ApprovalTypeComponent; + @ViewChild(AdrComponent) adr!: AdrComponent; + + @Output() isFormDirty = new EventEmitter(); + @Output() isFormInvalid = new EventEmitter(); + + techRecordCalculated?: V3TechRecordModel; + sectionTemplates: Array = []; + middleIndex = 0; + isEditing = false; + scrollPosition: [number, number] = [0, 0]; + isADRCertGenEnabled = false; + isDFSEnabled = false; + + private axlesService = inject(AxlesService); + private errorService = inject(GlobalErrorService); + private warningService = inject(GlobalWarningService); + private referenceDataService = inject(ReferenceDataService); + private technicalRecordService = inject(TechnicalRecordService); + private routerService = inject(RouterService); + private activatedRoute = inject(ActivatedRoute); + private viewportScroller = inject(ViewportScroller); + private store = inject(Store); + private loading = inject(LoadingService); + + fb = inject(FormBuilder); + featureToggleService = inject(FeatureToggleService); + + private destroy$ = new Subject(); + + form = this.fb.group({}); + + ngOnInit(): void { + this.isADRCertGenEnabled = this.featureToggleService.isFeatureEnabled('adrCertToggle'); + this.isDFSEnabled = this.featureToggleService.isFeatureEnabled('dfs'); + this.technicalRecordService.techRecord$ + .pipe( + map((record) => { + if (!record) { + return; + } + + let techRecord = cloneDeep(record); + techRecord = this.normaliseAxles(record); + + return techRecord; + }), + takeUntil(this.destroy$) + ) + .subscribe((techRecord) => { + if (techRecord) { + this.techRecordCalculated = techRecord; + } + this.referenceDataService.removeTyreSearch(); + this.sectionTemplates = this.vehicleTemplates; + this.middleIndex = Math.floor(this.sectionTemplates.length / 2); + }); + + const editingReason = this.activatedRoute.snapshot.data['reason']; + if (this.isEditing) { + this.technicalRecordService.clearReasonForCreation(); + this.technicalRecordService.techRecord$.pipe(takeUntil(this.destroy$), take(1)).subscribe((techRecord) => { + if (techRecord) { + if (editingReason === ReasonForEditing.NOTIFIABLE_ALTERATION_NEEDED) { + this.technicalRecordService.updateEditingTechRecord({ + ...(techRecord as TechRecordType<'put'>), + techRecord_statusCode: StatusCodes.PROVISIONAL, + }); + } + + if (techRecord?.vin?.match('([IOQ])a*')) { + const warnings: GlobalWarning[] = []; + warnings.push({ warning: 'VIN should not contain I, O or Q', anchorLink: 'vin' }); + this.warningService.setWarnings(warnings); + } + } + }); + } else if (!this.isEditing) { + this.warningService.clearWarnings(); + } + + this.store + .select(selectScrollPosition) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((position) => { + this.scrollPosition = position; + }); + + this.loading.showSpinner$.pipe(takeUntil(this.destroy$), debounceTime(10)).subscribe((loading) => { + if (!loading) { + this.viewportScroller.scrollToPosition(this.scrollPosition); + } + }); + + this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => this.handleFormState(this.form.value)); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + get vehicleType() { + return this.techRecordCalculated ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordCalculated) : undefined; + } + + get vehicleTemplates(): Array { + this.isEditing$.pipe(takeUntil(this.destroy$)).subscribe((editing) => { + this.isEditing = editing; + }); + if (!this.vehicleType) { + return []; + } + return ( + vehicleTemplateMap + .get(this.vehicleType) + ?.filter((template) => template.name !== (this.isEditing ? 'audit' : 'reasonForCreationSection')) + .filter((template) => template.name !== (this.isADRCertGenEnabled ? '' : 'adrCertificateSection')) ?? [] + ); + } + + get sectionTemplatesState$() { + return this.technicalRecordService.sectionStates$; + } + + isSectionExpanded$(sectionName: string | number) { + return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); + } + + get isEditing$(): Observable { + return this.routerService.getRouteDataProperty$('isEditing').pipe(map((isEditing) => !!isEditing)); + } + + get customSectionForms(): Array { + const commonCustomSections = [this.body?.form, this.dimensions?.form, this.tyres?.form, this.weights?.form, this.approvalType?.form]; + + switch (this.vehicleType) { + case VehicleTypes.PSV: + return [...commonCustomSections, this.psvBrakes.form]; + case VehicleTypes.HGV: + return !this.isDFSEnabled ? [...commonCustomSections, this.adr.form] : commonCustomSections; + case VehicleTypes.TRL: + return !this.isDFSEnabled + ? [...commonCustomSections, this.trlBrakes.form, this.letters.form, this.adr.form] + : [...commonCustomSections, this.trlBrakes.form, this.letters.form]; + case VehicleTypes.LGV: + return !this.isDFSEnabled ? [this.adr.form] : []; + default: + return []; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handleFormState(event: any): void { + const isPrimitiveArray = (a: unknown, b: unknown) => (Array.isArray(a) && !a.some((i) => typeof i === 'object') ? b : undefined); + + this.techRecordCalculated = mergeWith(cloneDeep(this.techRecordCalculated), event, isPrimitiveArray); + this.technicalRecordService.updateEditingTechRecord(this.techRecordCalculated as TechRecordType<'put'>); + } + + checkForms(): void { + const forms = this.sections?.map((section) => section.form).concat(this.customSectionForms); + + this.isFormDirty.emit(forms.some((form) => form.dirty)); + + this.setErrors(forms); + + this.isFormInvalid.emit(forms.some((form) => form.invalid)); + } + + setErrors(forms: Array): void { + const errors: GlobalError[] = []; + + forms.forEach((form) => DynamicFormService.validate(form, errors)); + + if (errors.length) { + this.errorService.setErrors(errors); + } else { + this.errorService.clearErrors(); + } + } + + private normaliseAxles(record: V3TechRecordModel): V3TechRecordModel { + const type = record.techRecord_vehicleType; + const category = record.techRecord_euVehicleCategory; + + if (type === VehicleTypes.HGV || (type === VehicleTypes.TRL && category !== 'o1' && category !== 'o2')) { + const [axles, axleSpacing] = this.axlesService.normaliseAxles(record.techRecord_axles ?? [], record.techRecord_dimensions_axleSpacing); + + record.techRecord_dimensions_axleSpacing = axleSpacing; + record.techRecord_axles = axles; + } + + return record; + } } diff --git a/src/app/forms/components/radio-group/radio-group.component.ts b/src/app/forms/components/radio-group/radio-group.component.ts index 4c901b2ef9..436e4a7ed7 100644 --- a/src/app/forms/components/radio-group/radio-group.component.ts +++ b/src/app/forms/components/radio-group/radio-group.component.ts @@ -4,32 +4,32 @@ import { FormNodeOption } from '@services/dynamic-forms/dynamic-form.types'; import { BaseControlComponent } from '../base-control/base-control.component'; @Component({ - selector: 'app-radio-group', - templateUrl: './radio-group.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: RadioGroupComponent, - multi: true, - }, - ], - styleUrls: ['./radio-group.component.scss'], + selector: 'app-radio-group', + templateUrl: './radio-group.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: RadioGroupComponent, + multi: true, + }, + ], + styleUrls: ['./radio-group.component.scss'], }) export class RadioGroupComponent extends BaseControlComponent { - @Input() options: FormNodeOption[] = []; - @Input() inline = false; + @Input() options: FormNodeOption[] = []; + @Input() inline = false; - getLabel(value: string | number | boolean | null): string | undefined { - return this.options.find((option) => option.value === value)?.label; - } + getLabel(value: string | number | boolean | null): string | undefined { + return this.options.find((option) => option.value === value)?.label; + } - trackByFn = (index: number): number => index; + trackByFn = (index: number): number => index; - getId(value: string | number | boolean | null, name: string) { - const id = `${name}-${value}-radio`; - if (this.control) { - this.control.meta.customId = id; - } - return id; - } + getId(value: string | number | boolean | null, name: string) { + const id = `${name}-${value}-radio`; + if (this.control?.meta) { + this.control.meta.customId = id; + } + return id; + } } diff --git a/src/app/forms/custom-sections/adr-section/__tests__/adr-section.component.spec.ts b/src/app/forms/custom-sections/adr-section/__tests__/adr-section.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.html b/src/app/forms/custom-sections/adr-section/adr-section.component.html new file mode 100644 index 0000000000..1406da861d --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.html @@ -0,0 +1,1055 @@ +
+
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + + + + + Applicant Details + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+
+ + + + ADR Details + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + +
+
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + +
+
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+
+ + + + + + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + + {{ error }} + +

+
+
+ + +
+
+
+ + + + +
+

+ +

+

+ Error: + + {{ error }} + +

+
+
+ + +
+
+
+ + + + +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + +

+ +
+
+ + + + +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + +

+ +
+ + +
Un Numbers
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ You have {{ 500 - (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 500) - 500 }} too many +
+
+ + + + Tank Inspections + + +
+

+ +

+

+ Error: + {{ + error + }} + {{ + error + }} +

+ +
+ + +
+ + +
+
+ +

Memo 07/09 (3 month extension) can be applied

+
+
Only applicable for vehicles used on national journeys
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+
+ +

M145

+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+
+ + + + +
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+
+
+ + + Declarations seen + + +
+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ You have {{ 500 - (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 500) - 500 }} too many +
+
+ + +
+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+
+ + +
+
+ + +
+
+ +

Owner/operator declaration

+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+
+ +

New certificate required

+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+
Will not be present on the ADR certificate
+

+ Error: + {{ error }} +

+ +
+ You have {{ 1024 - (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 1024) - 1024 }} too many +
+
+ + + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ You have {{ 1500 - (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 1500) - 1500 }} too many +
+
+
+
diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.scss b/src/app/forms/custom-sections/adr-section/adr-section.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.ts b/src/app/forms/custom-sections/adr-section/adr-section.component.ts new file mode 100644 index 0000000000..84c8748e12 --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.ts @@ -0,0 +1,375 @@ +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { ControlContainer, FormArray, FormBuilder, FormGroup, ValidatorFn } from '@angular/forms'; +import { ADRAdditionalNotesNumber } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrAdditionalNotesNumber.enum.js'; +import { ADRBodyType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrBodyType.enum.js'; +import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum.js'; +import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum.js'; +import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankStatementSubstancePermitted.js'; +import { getOptionsFromEnum } from '@forms/utils/enum-map'; +import { TC2Types } from '@models/adr.enum'; + +@Component({ + selector: 'app-adr-section', + templateUrl: './adr-section.component.html', + styleUrls: ['./adr-section.component.scss'], +}) +export class AdrSectionComponent implements OnInit, OnDestroy { + fb = inject(FormBuilder); + controlContainer = inject(ControlContainer); + + form = this.fb.group({ + techRecord_adrDetails_dangerousGoods: this.fb.control(false), + + // Applicant Details + techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), + techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [this.maxLength(150, 'Street')]), + techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), + techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), + techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [this.maxLength(25, 'Postcode')]), + + // ADR Details + techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [this.requiredWithDangerousGoods('ADR body type')]), + techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null, [ + this.requiredWithDangerousGoods('Used on international journeys'), + ]), + techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null), + techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ + this.requiredWithDangerousGoods('Permitted dangerous goods'), + ]), + techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [this.requiredWithExplosives('Compatibility Group J')]), + techRecord_adrDetails_additionalNotes_number: this.fb.control(null, [this.requiredWithDangerousGoods('Guidance notes')]), + techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [this.maxLength(40, 'ADR type approval number')]), + + // Tank Details + techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Make'), + this.maxLength(70, 'Tank Make'), + ]), + techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null), + techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Manufacturer serial number'), + this.maxLength(70, 'Manufacturer serial number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank type approval number'), + this.maxLength(65, 'Tank type approval number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ + this.requiredWithTankOrBattery('Code'), + this.maxLength(30, 'Code'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ + this.requiredWithTankOrBattery('Substances permitted'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control(null, this.requiredWithTankStatement('Select')), + techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ + this.requiredWithUNNumber('Reference number'), + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.control(null, [ + this.requiredWithProductList('Reference number'), + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, [ + this.requiredWithProductList('Product list'), + ]), + techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [this.maxLength(1500, 'Special provisions')]), + + // Tank Details > Tank Inspections + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('TC2: Certificate Number'), + this.maxLength(70, 'TC2: Certificate Number'), + ]), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control(null, [ + this.requiredWithTankOrBattery('TC2: Expiry Date'), + ]), + techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), + + // Miscellaneous + techRecord_adrDetails_memosApply: this.fb.control(null), + techRecord_adrDetails_m145Statement: this.fb.control(null), + + // Battery List + techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [this.requiredWithBattery('Battery list applicable')]), + techRecord_adrDetails_batteryListNumber: this.fb.control(null, [this.requiredWithBatteryListApplicable('Reference Number')]), + + // Brake declaration + techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), + techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null), + techRecord_adrDetails_brakeEndurance: this.fb.control(false), + techRecord_adrDetails_weight: this.fb.control(null, [ + this.max(99999999, 'Weight (tonnes)'), + this.requiredWithBrakeEndurance('Weight (tonnes)'), + this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), + ]), + + // Other declarations + techRecord_adrDetails_declarationsSeen: this.fb.control(false), + + // Miscellaneous + techRecord_adrDetails_newCertificateRequested: this.fb.control(false), + techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), + techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), + techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [this.maxLength(1500, 'ADR Certificate Notes')]), + }); + + // Option lists + dangerousGoodsOptions = [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ]; + + adrBodyTypesOptions = getOptionsFromEnum(ADRBodyType); + + usedOnInternationJourneysOptions = [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'n/a', label: 'Not applicable' }, + ]; + + permittedDangerousGoodsOptions = getOptionsFromEnum(ADRDangerousGood); + + guidanceNotesOptions = getOptionsFromEnum(ADRAdditionalNotesNumber); + + compatibilityGroupJOptions = [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ]; + + tankStatementSubstancePermittedOptions = getOptionsFromEnum(ADRTankStatementSubstancePermitted); + + tankStatementSelectOptions = getOptionsFromEnum(ADRTankDetailsTankStatementSelect); + + batteryListApplicableOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ]; + + isInvalid(formControlName: string) { + const control = this.form.get(formControlName); + return control?.invalid && control?.touched; + } + + toggle(formControlName: string, value: string) { + const control = this.form.get(formControlName); + if (!control) return; + + // If this is the first checkbox, set the value to an array + if (control.value === null) { + return control.setValue([value]); + } + + // If the value is already an array, toggle the value - if the array is then empty, set the value to null + if (Array.isArray(control.value)) { + control.value.includes(value) ? control.value.splice(control.value.indexOf(value), 1) : control.value.push(value); + if (control.value.length === 0) { + control.setValue(null); + } + } + } + + containsExplosives(arr: string[]) { + return arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_2) || arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_3); + } + + containsTankOrBattery(bodyType: string) { + return bodyType.toLowerCase().includes('tank') || bodyType.toLowerCase().includes('battery'); + } + + canDisplayDangerousGoodsSection(form: FormGroup | FormArray = this.form) { + const dangerousGoods = form.get('techRecord_adrDetails_dangerousGoods')?.value; + return dangerousGoods === true; + } + + canDisplayCompatibilityGroupJSection(form: FormGroup | FormArray = this.form) { + const permittedDangerousGoods = form.get('techRecord_adrDetails_permittedDangerousGoods')?.value; + const containsExplosives = Array.isArray(permittedDangerousGoods) && this.containsExplosives(permittedDangerousGoods); + return this.canDisplayDangerousGoodsSection(form) && containsExplosives; + } + + canDisplayBatterySection(form: FormGroup | FormArray = this.form) { + const adrBodyType = form.get('techRecord_adrDetails_vehicleDetails_type')?.value; + return typeof adrBodyType === 'string' && adrBodyType.toLowerCase().includes('battery'); + } + + canDisplayTankOrBatterySection(form: FormGroup | FormArray = this.form) { + const adrBodyType = form.get('techRecord_adrDetails_vehicleDetails_type')?.value; + const containsTankOrBattery = typeof adrBodyType === 'string' && this.containsTankOrBattery(adrBodyType); + return this.canDisplayDangerousGoodsSection(form) && containsTankOrBattery; + } + + canDisplayTankStatementSelectSection(form: FormGroup | FormArray = this.form) { + const tankStatementSubstancesPermitted = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted')?.value; + const underUNNumber = tankStatementSubstancesPermitted === ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER; + return this.canDisplayTankOrBatterySection(form) && underUNNumber; + } + + canDisplayTankStatementStatementSection(form: FormGroup | FormArray = this.form) { + const tankStatementSelect = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_select')?.value; + const underStatement = tankStatementSelect === ADRTankDetailsTankStatementSelect.STATEMENT; + return this.canDisplayTankStatementSelectSection(form) && underStatement; + } + + canDisplayTankStatementProductListSection(form: FormGroup | FormArray = this.form) { + const tankStatementSelect = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_select')?.value; + const underProductList = tankStatementSelect === ADRTankDetailsTankStatementSelect.PRODUCT_LIST; + return this.canDisplayTankStatementSelectSection(form) && underProductList; + } + + canDisplayWeightSection(form: FormGroup | FormArray = this.form) { + const brakeEndurance = form.get('techRecord_adrDetails_brakeEndurance')?.value; + return brakeEndurance === true; + } + + canDisplayBatteryListNumber(form: FormGroup | FormArray = this.form) { + const batteryListApplicable = form.get('techRecord_adrDetails_listStatementApplicable')?.value; + return this.canDisplayBatterySection(form) && batteryListApplicable === true; + } + + ngOnInit(): void { + // Attatch all form controls to parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + Object.entries(this.form.controls).forEach(([key, control]) => parent.addControl(key, control)); + } + } + + ngOnDestroy(): void { + // Detatch all form controls from parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + Object.keys(this.form.controls).forEach((key) => parent.removeControl(key)); + } + } + + // Custom validators + requiredWithDangerousGoods(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayDangerousGoodsSection(control.parent)) { + return { required: `${label} is required when dangerous goods are present` }; + } + + return null; + }; + } + + requiredWithExplosives(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayCompatibilityGroupJSection(control.parent)) { + return { required: `${label} is required when Explosives Type 2 or Explosive Type 3` }; + } + + return null; + }; + } + + requiredWithBattery(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayBatterySection(control.parent)) { + return { required: `${label} is required when ADR body type is of type 'battery'` }; + } + + return null; + }; + } + + requiredWithTankOrBattery(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayTankOrBatterySection(control.parent)) { + return { required: `${label} is required when ADR body type is of type 'tank' or 'battery'` }; + } + + return null; + }; + } + + requiredWithTankStatement(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayTankStatementSelectSection(control.parent)) { + return { required: `${label} is required with substances permitted` }; + } + + return null; + }; + } + + requiredWithUNNumber(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayTankStatementStatementSection(control.parent)) { + return { required: `${label} is required when under UN number` }; + } + + return null; + }; + } + + requiredWithProductList(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayTankStatementProductListSection(control.parent)) { + return { required: `${label} is required when under product list` }; + } + + return null; + }; + } + + requiredWithBrakeEndurance(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayWeightSection(control.parent)) { + return { required: `${label} is required when brake endurance is checked` }; + } + + return null; + }; + } + + requiredWithBatteryListApplicable(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.canDisplayBatteryListNumber(control.parent)) { + return { required: `${label} is required when battery list is applicable` }; + } + + return null; + }; + } + + max(size: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value > size) { + return { max: `${label} must be less than or equal to ${size}` }; + } + + return null; + }; + } + + maxLength(length: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value.length > length) { + return { maxLength: `${label} must be less than or equal to ${length} characters` }; + } + + return null; + }; + } + + pattern(pattern: string | RegExp, label: string): ValidatorFn { + return (control) => { + if (control.value && !new RegExp(pattern).test(control.value)) { + return { pattern: `${label} is invalid` }; + } + + return null; + }; + } + + // Dynamically add/remove controls + addTC3TankInspection() {} + + removeTC3TankInspection(index: number) {} + + addUNNumber() {} + + removeUNNumber(index: number) {} +} diff --git a/src/app/forms/dynamic-forms.module.ts b/src/app/forms/dynamic-forms.module.ts index c4215c737c..4baa7dc690 100644 --- a/src/app/forms/dynamic-forms.module.ts +++ b/src/app/forms/dynamic-forms.module.ts @@ -41,6 +41,7 @@ import { ViewListItemComponent } from './components/view-list-item/view-list-ite import { AbandonDialogComponent } from './custom-sections/abandon-dialog/abandon-dialog.component'; import { AdrExaminerNotesHistoryViewComponent } from './custom-sections/adr-examiner-notes-history-view/adr-examiner-notes-history-view.component'; import { AdrNewCertificateRequiredViewComponent } from './custom-sections/adr-new-certificate-required-view/adr-new-certificate-required-view.component'; +import { AdrSectionComponent } from './custom-sections/adr-section/adr-section.component'; import { AdrTankDetailsInitialInspectionViewComponent } from './custom-sections/adr-tank-details-initial-inspection-view/adr-tank-details-initial-inspection-view.component'; import { AdrTankDetailsM145ViewComponent } from './custom-sections/adr-tank-details-m145-view/adr-tank-details-m145-view.component'; import { AdrTankDetailsSubsequentInspectionsEditComponent } from './custom-sections/adr-tank-details-subsequent-inspections-edit/adr-tank-details-subsequent-inspections-edit.component'; @@ -66,119 +67,121 @@ import { TyresComponent } from './custom-sections/tyres/tyres.component'; import { WeightsComponent } from './custom-sections/weights/weights.component'; @NgModule({ - declarations: [ - BaseControlComponent, - TextInputComponent, - ViewListItemComponent, - DynamicFormGroupComponent, - ViewCombinationComponent, - CheckboxGroupComponent, - RadioGroupComponent, - DefectComponent, - DefectsComponent, - AutocompleteComponent, - NumberInputComponent, - TextAreaComponent, - NumberOnlyDirective, - ToUppercaseDirective, - NoSpaceDirective, - TrimWhitespaceDirective, - DateComponent, - SelectComponent, - DynamicFormFieldComponent, - FieldErrorMessageComponent, - DefectSelectComponent, - RequiredStandardSelectComponent, - DateFocusNextDirective, - TruncatePipe, - WeightsComponent, - LettersComponent, - PlatesComponent, - DimensionsComponent, - TrlBrakesComponent, - ReadOnlyComponent, - CustomDefectsComponent, - RequiredStandardComponent, - RequiredStandardsComponent, - CustomDefectComponent, - SwitchableInputComponent, - ReadOnlyComponent, - SuffixDirective, - AbandonDialogComponent, - BodyComponent, - TyresComponent, - PsvBrakesComponent, - PrefixDirective, - SuggestiveInputComponent, - CheckboxComponent, - ApprovalTypeComponent, - ApprovalTypeInputComponent, - ApprovalTypeFocusNextDirective, - ModifiedWeightsComponent, - FieldWarningMessageComponent, - AdrComponent, - AdrTankDetailsSubsequentInspectionsEditComponent, - AdrTankStatementUnNumberEditComponent, - CustomFormControlComponent, - AdrExaminerNotesHistoryEditComponent, - AdrExaminerNotesHistoryViewComponent, - AdrTankDetailsSubsequentInspectionsViewComponent, - AdrTankDetailsInitialInspectionViewComponent, - AdrTankStatementUnNumberViewComponent, - AdrCertificateHistoryComponent, - AdrTankDetailsM145ViewComponent, - ContingencyAdrGenerateCertComponent, - AdrNewCertificateRequiredViewComponent, - ], - imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], - exports: [ - TextInputComponent, - ViewListItemComponent, - DynamicFormGroupComponent, - ViewCombinationComponent, - CheckboxGroupComponent, - RadioGroupComponent, - DefectComponent, - DefectsComponent, - AutocompleteComponent, - NumberInputComponent, - TextAreaComponent, - DateComponent, - SelectComponent, - DynamicFormFieldComponent, - FieldErrorMessageComponent, - DefectSelectComponent, - RequiredStandardSelectComponent, - WeightsComponent, - LettersComponent, - PlatesComponent, - TyresComponent, - DimensionsComponent, - TrlBrakesComponent, - ReadOnlyComponent, - RequiredStandardComponent, - RequiredStandardsComponent, - CustomDefectsComponent, - CustomDefectComponent, - SwitchableInputComponent, - SuffixDirective, - ReadOnlyComponent, - AbandonDialogComponent, - BodyComponent, - PsvBrakesComponent, - PrefixDirective, - SuggestiveInputComponent, - CheckboxComponent, - ToUppercaseDirective, - NoSpaceDirective, - TrimWhitespaceDirective, - ApprovalTypeComponent, - ApprovalTypeInputComponent, - ApprovalTypeFocusNextDirective, - ModifiedWeightsComponent, - AdrComponent, - AdrCertificateHistoryComponent, - FieldWarningMessageComponent, - ], + declarations: [ + BaseControlComponent, + TextInputComponent, + ViewListItemComponent, + DynamicFormGroupComponent, + ViewCombinationComponent, + CheckboxGroupComponent, + RadioGroupComponent, + DefectComponent, + DefectsComponent, + AutocompleteComponent, + NumberInputComponent, + TextAreaComponent, + NumberOnlyDirective, + ToUppercaseDirective, + NoSpaceDirective, + TrimWhitespaceDirective, + DateComponent, + SelectComponent, + DynamicFormFieldComponent, + FieldErrorMessageComponent, + DefectSelectComponent, + RequiredStandardSelectComponent, + DateFocusNextDirective, + TruncatePipe, + WeightsComponent, + LettersComponent, + PlatesComponent, + DimensionsComponent, + TrlBrakesComponent, + ReadOnlyComponent, + CustomDefectsComponent, + RequiredStandardComponent, + RequiredStandardsComponent, + CustomDefectComponent, + SwitchableInputComponent, + ReadOnlyComponent, + SuffixDirective, + AbandonDialogComponent, + BodyComponent, + TyresComponent, + PsvBrakesComponent, + PrefixDirective, + SuggestiveInputComponent, + CheckboxComponent, + ApprovalTypeComponent, + ApprovalTypeInputComponent, + ApprovalTypeFocusNextDirective, + ModifiedWeightsComponent, + FieldWarningMessageComponent, + AdrComponent, + AdrTankDetailsSubsequentInspectionsEditComponent, + AdrTankStatementUnNumberEditComponent, + CustomFormControlComponent, + AdrExaminerNotesHistoryEditComponent, + AdrExaminerNotesHistoryViewComponent, + AdrTankDetailsSubsequentInspectionsViewComponent, + AdrTankDetailsInitialInspectionViewComponent, + AdrTankStatementUnNumberViewComponent, + AdrCertificateHistoryComponent, + AdrTankDetailsM145ViewComponent, + ContingencyAdrGenerateCertComponent, + AdrNewCertificateRequiredViewComponent, + AdrSectionComponent, + ], + imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], + exports: [ + TextInputComponent, + ViewListItemComponent, + DynamicFormGroupComponent, + ViewCombinationComponent, + CheckboxGroupComponent, + RadioGroupComponent, + DefectComponent, + DefectsComponent, + AutocompleteComponent, + NumberInputComponent, + TextAreaComponent, + DateComponent, + SelectComponent, + DynamicFormFieldComponent, + FieldErrorMessageComponent, + DefectSelectComponent, + RequiredStandardSelectComponent, + WeightsComponent, + LettersComponent, + PlatesComponent, + TyresComponent, + DimensionsComponent, + TrlBrakesComponent, + ReadOnlyComponent, + RequiredStandardComponent, + RequiredStandardsComponent, + CustomDefectsComponent, + CustomDefectComponent, + SwitchableInputComponent, + SuffixDirective, + ReadOnlyComponent, + AbandonDialogComponent, + BodyComponent, + PsvBrakesComponent, + PrefixDirective, + SuggestiveInputComponent, + CheckboxComponent, + ToUppercaseDirective, + NoSpaceDirective, + TrimWhitespaceDirective, + ApprovalTypeComponent, + ApprovalTypeInputComponent, + ApprovalTypeFocusNextDirective, + ModifiedWeightsComponent, + AdrComponent, + AdrCertificateHistoryComponent, + FieldWarningMessageComponent, + AdrSectionComponent, + ], }) export class DynamicFormsModule {} diff --git a/src/assets/featureToggle.int.json b/src/assets/featureToggle.int.json index 14c9ac4699..902e7d02b2 100644 --- a/src/assets/featureToggle.int.json +++ b/src/assets/featureToggle.int.json @@ -1,6 +1,7 @@ { - "testToggle": false, - "adrToggle": true, - "adrCertToggle": true, - "requiredStandards": true - } + "testToggle": false, + "adrToggle": true, + "adrCertToggle": true, + "requiredStandards": true, + "dfs": false +} diff --git a/src/assets/featureToggle.json b/src/assets/featureToggle.json index 576c7258b1..d282298b11 100644 --- a/src/assets/featureToggle.json +++ b/src/assets/featureToggle.json @@ -2,5 +2,6 @@ "testToggle": true, "adrToggle": true, "adrCertToggle": true, - "requiredStandards": true + "requiredStandards": true, + "dfs": true } diff --git a/src/assets/featureToggle.preprod.json b/src/assets/featureToggle.preprod.json index 14c9ac4699..902e7d02b2 100644 --- a/src/assets/featureToggle.preprod.json +++ b/src/assets/featureToggle.preprod.json @@ -1,6 +1,7 @@ { - "testToggle": false, - "adrToggle": true, - "adrCertToggle": true, - "requiredStandards": true - } + "testToggle": false, + "adrToggle": true, + "adrCertToggle": true, + "requiredStandards": true, + "dfs": false +} diff --git a/src/assets/featureToggle.prod.json b/src/assets/featureToggle.prod.json index df58ab6e4b..902e7d02b2 100644 --- a/src/assets/featureToggle.prod.json +++ b/src/assets/featureToggle.prod.json @@ -2,5 +2,6 @@ "testToggle": false, "adrToggle": true, "adrCertToggle": true, - "requiredStandards": true + "requiredStandards": true, + "dfs": false } From 71fd8408c8d0fff5b21ff5860a90bf57bdece0ff Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:04:34 +0100 Subject: [PATCH 002/211] chore(cb2-0000): changes --- .../tech-record-summary.component.ts | 24 +- .../vehicle-technical-record.component.ts | 292 ++++++----- .../adr-section/adr-section.component.html | 94 +++- .../adr-section/adr-section.component.ts | 297 ++++++----- .../dynamic-forms/dynamic-form.service.ts | 462 ++++++++---------- 5 files changed, 649 insertions(+), 520 deletions(-) diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts index c8b0617bef..4a4176970d 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts @@ -11,7 +11,7 @@ import { ViewChildren, inject, } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { GlobalError } from '@core/components/global-error/global-error.interface'; import { GlobalErrorService } from '@core/components/global-error/global-error.service'; @@ -219,7 +219,7 @@ export class TechRecordSummaryComponent implements OnInit, OnDestroy { } checkForms(): void { - const forms = this.sections?.map((section) => section.form).concat(this.customSectionForms); + const forms: Array = this.sections?.map((section) => section.form).concat(this.customSectionForms); this.isFormDirty.emit(forms.some((form) => form.dirty)); @@ -228,11 +228,29 @@ export class TechRecordSummaryComponent implements OnInit, OnDestroy { this.isFormInvalid.emit(forms.some((form) => form.invalid)); } - setErrors(forms: Array): void { + setErrors(forms: Array): void { const errors: GlobalError[] = []; forms.forEach((form) => DynamicFormService.validate(form, errors)); + this.form.markAllAsTouched(); + this.form.updateValueAndValidity(); + + function extractErrors(form: FormGroup | FormArray) { + Object.entries(form.controls).forEach(([key, control]) => { + if (control instanceof FormGroup || control instanceof FormArray) { + extractErrors(control); + } else if (control.invalid && control.errors) { + Object.values(control.errors).forEach((error) => { + errors.push({ error, anchorLink: key }); + }); + } + }); + } + + extractErrors(this.form); + console.log(errors, this.form); + if (errors.length) { this.errorService.setErrors(errors); } else { diff --git a/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts b/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts index 286b2f24cd..0360b4d445 100644 --- a/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts +++ b/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts @@ -7,13 +7,7 @@ import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/ import { Roles } from '@models/roles.enum'; import { TechRecordActions } from '@models/tech-record/tech-record-actions.enum'; import { TestResultModel } from '@models/test-results/test-result.model'; -import { - ReasonForEditing, - StatusCodes, - TechRecordModel, - V3TechRecordModel, - VehicleTypes, -} from '@models/vehicle-tech-record.model'; +import { ReasonForEditing, StatusCodes, TechRecordModel, V3TechRecordModel, VehicleTypes } from '@models/vehicle-tech-record.model'; import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { AdrService } from '@services/adr/adr.service'; @@ -26,148 +20,148 @@ import { Observable, Subject, take, takeUntil } from 'rxjs'; import { TechRecordSummaryComponent } from '../tech-record-summary/tech-record-summary.component'; @Component({ - selector: 'app-vehicle-technical-record', - templateUrl: './vehicle-technical-record.component.html', - styleUrls: ['./vehicle-technical-record.component.scss'], + selector: 'app-vehicle-technical-record', + templateUrl: './vehicle-technical-record.component.html', + styleUrls: ['./vehicle-technical-record.component.scss'], }) export class VehicleTechnicalRecordComponent implements OnInit, OnDestroy { - @ViewChild(TechRecordSummaryComponent) summary!: TechRecordSummaryComponent; - @Input() techRecord?: V3TechRecordModel; - - testResults$: Observable; - editingReason?: ReasonForEditing; - recordHistory?: TechRecordSearchSchema[]; - - isCurrent = false; - isArchived = false; - isEditing = false; - isDirty = false; - isInvalid = false; - - private destroy$ = new Subject(); - hasTestResultAmend: boolean | undefined = false; - - constructor( - public globalErrorService: GlobalErrorService, - public userService: UserService, - testRecordService: TestRecordsService, - private activatedRoute: ActivatedRoute, - private route: ActivatedRoute, - private router: Router, - private store: Store, - private actions$: Actions, - private viewportScroller: ViewportScroller, - private featureToggleService: FeatureToggleService, - public adrService: AdrService - ) { - this.testResults$ = testRecordService.testRecords$; - this.isEditing = this.activatedRoute.snapshot.data['isEditing'] ?? false; - this.editingReason = this.activatedRoute.snapshot.data['reason']; - } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - ngOnInit(): void { - this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { - void this.router.navigate([ - `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, - ]); - }); - this.isArchived = this.techRecord?.techRecord_statusCode === StatusCodes.ARCHIVED; - this.isCurrent = this.techRecord?.techRecord_statusCode === StatusCodes.CURRENT; - - this.userService.roles$.pipe(take(1)).subscribe((storedRoles) => { - this.hasTestResultAmend = storedRoles?.some((role) => { - return Roles.TestResultAmend.split(',').includes(role); - }); - }); - } - - get currentVrm(): string | undefined { - return this.techRecord?.techRecord_vehicleType !== 'trl' ? this.techRecord?.primaryVrm ?? '' : undefined; - } - - get roles(): typeof Roles { - return Roles; - } - - get vehicleTypes(): typeof VehicleTypes { - return VehicleTypes; - } - - get statusCodes(): typeof StatusCodes { - return StatusCodes; - } - - hasPlates(techRecord: TechRecordModel) { - return (techRecord.plates?.length ?? 0) > 0; - } - - getActions(techRecord?: V3TechRecordModel): TechRecordActions { - switch (techRecord?.techRecord_statusCode) { - case StatusCodes.CURRENT: - return TechRecordActions.CURRENT; - case StatusCodes.PROVISIONAL: - return TechRecordActions.PROVISIONAL; - case StatusCodes.ARCHIVED: - return TechRecordActions.ARCHIVED; - default: - return TechRecordActions.NONE; - } - } - - getVehicleDescription(techRecord: TechRecordModel, vehicleType: VehicleTypes | undefined): string { - switch (vehicleType) { - case VehicleTypes.TRL: - return techRecord.vehicleConfiguration ?? ''; - case VehicleTypes.PSV: - return techRecord.bodyMake && techRecord.bodyModel ? `${techRecord.bodyMake}-${techRecord.bodyModel}` : ''; - case VehicleTypes.HGV: - return techRecord.make && techRecord.model ? `${techRecord.make}-${techRecord.model}` : ''; - default: - return 'Unknown Vehicle Type'; - } - } - - showCreateTestButton(): boolean { - return !this.isArchived && !this.isEditing; - } - - async createTest(techRecord?: V3TechRecordModel): Promise { - this.store.dispatch(clearScrollPosition()); - if ( - (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'complete' || - (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'testable' - ) { - await this.router.navigate(['test-records/create-test/type'], { relativeTo: this.route }); - } else { - this.globalErrorService.setErrors([ - { - error: this.getCreateTestErrorMessage(techRecord?.techRecord_hiddenInVta ?? false), - anchorLink: 'create-test', - }, - ]); - - this.viewportScroller.scrollToPosition([0, 0]); - } - } - - async handleSubmit(): Promise { - this.summary.checkForms(); - if (this.isInvalid) return; - - await this.router.navigate(['change-summary'], { relativeTo: this.route }); - } - - private getCreateTestErrorMessage(hiddenInVta: boolean | undefined): string { - if (hiddenInVta) { - return 'Vehicle record is hidden in VTA. Show the vehicle record in VTA to start recording tests against it.'; - } - - return this.hasTestResultAmend - ? 'This vehicle does not have enough information to be tested. Please complete this record so tests can be recorded against it.' - : 'This vehicle does not have enough information to be tested.' + - ' Call the Contact Centre to complete this record so tests can be recorded against it.'; - } + @ViewChild(TechRecordSummaryComponent) summary!: TechRecordSummaryComponent; + @Input() techRecord?: V3TechRecordModel; + + testResults$: Observable; + editingReason?: ReasonForEditing; + recordHistory?: TechRecordSearchSchema[]; + + isCurrent = false; + isArchived = false; + isEditing = false; + isDirty = false; + isInvalid = false; + + private destroy$ = new Subject(); + hasTestResultAmend: boolean | undefined = false; + + constructor( + public globalErrorService: GlobalErrorService, + public userService: UserService, + testRecordService: TestRecordsService, + private activatedRoute: ActivatedRoute, + private route: ActivatedRoute, + private router: Router, + private store: Store, + private actions$: Actions, + private viewportScroller: ViewportScroller, + private featureToggleService: FeatureToggleService, + public adrService: AdrService + ) { + this.testResults$ = testRecordService.testRecords$; + this.isEditing = this.activatedRoute.snapshot.data['isEditing'] ?? false; + this.editingReason = this.activatedRoute.snapshot.data['reason']; + } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + ngOnInit(): void { + this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { + void this.router.navigate([ + `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, + ]); + }); + this.isArchived = this.techRecord?.techRecord_statusCode === StatusCodes.ARCHIVED; + this.isCurrent = this.techRecord?.techRecord_statusCode === StatusCodes.CURRENT; + + this.userService.roles$.pipe(take(1)).subscribe((storedRoles) => { + this.hasTestResultAmend = storedRoles?.some((role) => { + return Roles.TestResultAmend.split(',').includes(role); + }); + }); + } + + get currentVrm(): string | undefined { + return this.techRecord?.techRecord_vehicleType !== 'trl' ? this.techRecord?.primaryVrm ?? '' : undefined; + } + + get roles(): typeof Roles { + return Roles; + } + + get vehicleTypes(): typeof VehicleTypes { + return VehicleTypes; + } + + get statusCodes(): typeof StatusCodes { + return StatusCodes; + } + + hasPlates(techRecord: TechRecordModel) { + return (techRecord.plates?.length ?? 0) > 0; + } + + getActions(techRecord?: V3TechRecordModel): TechRecordActions { + switch (techRecord?.techRecord_statusCode) { + case StatusCodes.CURRENT: + return TechRecordActions.CURRENT; + case StatusCodes.PROVISIONAL: + return TechRecordActions.PROVISIONAL; + case StatusCodes.ARCHIVED: + return TechRecordActions.ARCHIVED; + default: + return TechRecordActions.NONE; + } + } + + getVehicleDescription(techRecord: TechRecordModel, vehicleType: VehicleTypes | undefined): string { + switch (vehicleType) { + case VehicleTypes.TRL: + return techRecord.vehicleConfiguration ?? ''; + case VehicleTypes.PSV: + return techRecord.bodyMake && techRecord.bodyModel ? `${techRecord.bodyMake}-${techRecord.bodyModel}` : ''; + case VehicleTypes.HGV: + return techRecord.make && techRecord.model ? `${techRecord.make}-${techRecord.model}` : ''; + default: + return 'Unknown Vehicle Type'; + } + } + + showCreateTestButton(): boolean { + return !this.isArchived && !this.isEditing; + } + + async createTest(techRecord?: V3TechRecordModel): Promise { + this.store.dispatch(clearScrollPosition()); + if ( + (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'complete' || + (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'testable' + ) { + await this.router.navigate(['test-records/create-test/type'], { relativeTo: this.route }); + } else { + this.globalErrorService.setErrors([ + { + error: this.getCreateTestErrorMessage(techRecord?.techRecord_hiddenInVta ?? false), + anchorLink: 'create-test', + }, + ]); + + this.viewportScroller.scrollToPosition([0, 0]); + } + } + + async handleSubmit(): Promise { + this.summary.checkForms(); + if (this.isInvalid) return; + + await this.router.navigate(['change-summary'], { relativeTo: this.route }); + } + + private getCreateTestErrorMessage(hiddenInVta: boolean | undefined): string { + if (hiddenInVta) { + return 'Vehicle record is hidden in VTA. Show the vehicle record in VTA to start recording tests against it.'; + } + + return this.hasTestResultAmend + ? 'This vehicle does not have enough information to be tested. Please complete this record so tests can be recorded against it.' + : 'This vehicle does not have enough information to be tested.' + + ' Call the Contact Centre to complete this record so tests can be recorded against it.'; + } } diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.html b/src/app/forms/custom-sections/adr-section/adr-section.component.html index 1406da861d..16b3ffd895 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section.component.html +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.html @@ -212,7 +212,7 @@

- +

- +

- +

-
Un Numbers
+
+
+
+

+ +

+ + Remove +
+
+ Add UN Number +
@@ -678,7 +701,54 @@

/>

+
+ + + +
+
+ +
+

+ +

+ +
+ + +
+

+ +

+ +
+ + + + +

+ Remove +

+
+
+
+ +

+ Add subsequent inspection +

@@ -708,13 +778,13 @@

Memo 07/09 (3 month extension) can be applie

- -
+ +

M145

-

+

Error: {{ error }}

@@ -767,7 +837,7 @@

- +

@@ -829,7 +899,7 @@

-
+

diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.ts b/src/app/forms/custom-sections/adr-section/adr-section.component.ts index 84c8748e12..595b679501 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.ts @@ -6,7 +6,10 @@ import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-recor import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum.js'; import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankStatementSubstancePermitted.js'; import { getOptionsFromEnum } from '@forms/utils/enum-map'; -import { TC2Types } from '@models/adr.enum'; +import { TC2Types, TC3Types } from '@models/adr.enum.js'; +import { Store } from '@ngrx/store'; +import { selectTechRecord } from '@store/technical-records'; +import { ReplaySubject, takeUntil } from 'rxjs'; @Component({ selector: 'app-adr-section', @@ -15,104 +18,113 @@ import { TC2Types } from '@models/adr.enum'; }) export class AdrSectionComponent implements OnInit, OnDestroy { fb = inject(FormBuilder); + store = inject(Store); controlContainer = inject(ControlContainer); - form = this.fb.group({ - techRecord_adrDetails_dangerousGoods: this.fb.control(false), - - // Applicant Details - techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), - techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [this.maxLength(150, 'Street')]), - techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), - techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), - techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [this.maxLength(25, 'Postcode')]), - - // ADR Details - techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [this.requiredWithDangerousGoods('ADR body type')]), - techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null, [ - this.requiredWithDangerousGoods('Used on international journeys'), - ]), - techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null), - techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ - this.requiredWithDangerousGoods('Permitted dangerous goods'), - ]), - techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [this.requiredWithExplosives('Compatibility Group J')]), - techRecord_adrDetails_additionalNotes_number: this.fb.control(null, [this.requiredWithDangerousGoods('Guidance notes')]), - techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [this.maxLength(40, 'ADR type approval number')]), - - // Tank Details - techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank Make'), - this.maxLength(70, 'Tank Make'), - ]), - techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null), - techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('Manufacturer serial number'), - this.maxLength(70, 'Manufacturer serial number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank type approval number'), - this.maxLength(65, 'Tank type approval number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ - this.requiredWithTankOrBattery('Code'), - this.maxLength(30, 'Code'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ - this.requiredWithTankOrBattery('Substances permitted'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control(null, this.requiredWithTankStatement('Select')), - techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ - this.requiredWithUNNumber('Reference number'), - this.maxLength(1500, 'Reference number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.control(null, [ - this.requiredWithProductList('Reference number'), - this.maxLength(1500, 'Reference number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, [ - this.requiredWithProductList('Product list'), - ]), - techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [this.maxLength(1500, 'Special provisions')]), - - // Tank Details > Tank Inspections - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('TC2: Certificate Number'), - this.maxLength(70, 'TC2: Certificate Number'), - ]), - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control(null, [ - this.requiredWithTankOrBattery('TC2: Expiry Date'), - ]), - techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), - - // Miscellaneous - techRecord_adrDetails_memosApply: this.fb.control(null), - techRecord_adrDetails_m145Statement: this.fb.control(null), - - // Battery List - techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [this.requiredWithBattery('Battery list applicable')]), - techRecord_adrDetails_batteryListNumber: this.fb.control(null, [this.requiredWithBatteryListApplicable('Reference Number')]), - - // Brake declaration - techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), - techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null), - techRecord_adrDetails_brakeEndurance: this.fb.control(false), - techRecord_adrDetails_weight: this.fb.control(null, [ - this.max(99999999, 'Weight (tonnes)'), - this.requiredWithBrakeEndurance('Weight (tonnes)'), - this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), - ]), - - // Other declarations - techRecord_adrDetails_declarationsSeen: this.fb.control(false), - - // Miscellaneous - techRecord_adrDetails_newCertificateRequested: this.fb.control(false), - techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), - techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), - techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [this.maxLength(1500, 'ADR Certificate Notes')]), - }); + destroy$ = new ReplaySubject(1); + + form = this.fb.group( + { + techRecord_adrDetails_dangerousGoods: this.fb.control(false), + + // Applicant Details + techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), + techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [this.maxLength(150, 'Street')]), + techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), + techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), + techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [this.maxLength(25, 'Postcode')]), + + // ADR Details + techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [this.requiredWithDangerousGoods('ADR body type')]), + techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null, [ + this.requiredWithDangerousGoods('Used on international journeys'), + ]), + techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null), + techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ + this.requiredWithDangerousGoods('Permitted dangerous goods'), + ]), + techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [this.requiredWithExplosives('Compatibility Group J')]), + techRecord_adrDetails_additionalNotes_number: this.fb.control([], [this.requiredWithDangerousGoods('Guidance notes')]), + techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [this.maxLength(40, 'ADR type approval number')]), + + // Tank Details + techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Make'), + this.maxLength(70, 'Tank Make'), + ]), + techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Year of manufacture'), + this.maxLength(70, 'Tank Year of manufacture'), + ]), + techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Manufacturer serial number'), + this.maxLength(70, 'Manufacturer serial number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank type approval number'), + this.maxLength(65, 'Tank type approval number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ + this.requiredWithTankOrBattery('Code'), + this.maxLength(30, 'Code'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ + this.requiredWithTankOrBattery('Substances permitted'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control(null, this.requiredWithTankStatement('Select')), + techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ + this.requiredWithUNNumber('Reference number'), + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: this.fb.control(null, [ + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array([ + this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, []), + techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [this.maxLength(1500, 'Special provisions')]), + + // Tank Details > Tank Inspections + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('TC2: Certificate Number'), + this.maxLength(70, 'TC2: Certificate Number'), + ]), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control(null, [ + this.requiredWithTankOrBattery('TC2: Expiry Date'), + ]), + techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), + + // Miscellaneous + techRecord_adrDetails_memosApply: this.fb.control(null), + techRecord_adrDetails_m145Statement: this.fb.control(null), + + // Battery List + techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [this.requiredWithBattery('Battery list applicable')]), + techRecord_adrDetails_batteryListNumber: this.fb.control(null, [this.requiredWithBatteryListApplicable('Reference Number')]), + + // Brake declaration + techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), + techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null, [this.maxLength(500, 'Issuer')]), + techRecord_adrDetails_brakeEndurance: this.fb.control(false), + techRecord_adrDetails_weight: this.fb.control(null, [ + this.max(99999999, 'Weight (tonnes)'), + this.requiredWithBrakeEndurance('Weight (tonnes)'), + this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), + ]), + + // Other declarations + techRecord_adrDetails_declarationsSeen: this.fb.control(false), + + // Miscellaneous + techRecord_adrDetails_newCertificateRequested: this.fb.control(false), + techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), + techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), + techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [this.maxLength(1500, 'ADR Certificate Notes')]), + }, + { validators: [this.requiresReferenceNumberOrUNNumber()] } + ); // Option lists dangerousGoodsOptions = [ @@ -146,6 +158,8 @@ export class AdrSectionComponent implements OnInit, OnDestroy { { value: false, label: 'No' }, ]; + tc3InspectionOptions = getOptionsFromEnum(TC3Types); + isInvalid(formControlName: string) { const control = this.form.get(formControlName); return control?.invalid && control?.touched; @@ -222,17 +236,30 @@ export class AdrSectionComponent implements OnInit, OnDestroy { return brakeEndurance === true; } - canDisplayBatteryListNumber(form: FormGroup | FormArray = this.form) { + canDisplayBatteryListNumberSection(form: FormGroup | FormArray = this.form) { const batteryListApplicable = form.get('techRecord_adrDetails_listStatementApplicable')?.value; return this.canDisplayBatterySection(form) && batteryListApplicable === true; } + canDisplayIssueSection() { + const brakeDeclarationsSeen = this.form.get('techRecord_adrDetails_brakeDeclarationsSeen')?.value; + return brakeDeclarationsSeen === true; + } + ngOnInit(): void { // Attatch all form controls to parent const parent = this.controlContainer.control; if (parent instanceof FormGroup) { Object.entries(this.form.controls).forEach(([key, control]) => parent.addControl(key, control)); } + + // Listen for tech record changes, and update the form accordingly + this.store + .select(selectTechRecord) + .pipe(takeUntil(this.destroy$)) + .subscribe((techRecord) => { + if (techRecord) this.form.patchValue(techRecord as any); + }); } ngOnDestroy(): void { @@ -241,6 +268,10 @@ export class AdrSectionComponent implements OnInit, OnDestroy { if (parent instanceof FormGroup) { Object.keys(this.form.controls).forEach((key) => parent.removeControl(key)); } + + // Clear subscriptions + this.destroy$.next(true); + this.destroy$.complete(); } // Custom validators @@ -304,30 +335,46 @@ export class AdrSectionComponent implements OnInit, OnDestroy { }; } - requiredWithProductList(label: string): ValidatorFn { + requiredWithBrakeEndurance(label: string): ValidatorFn { return (control) => { - if (control.parent && !control.value && this.canDisplayTankStatementProductListSection(control.parent)) { - return { required: `${label} is required when under product list` }; + if (control.parent && !control.value && this.canDisplayWeightSection(control.parent)) { + return { required: `${label} is required when brake endurance is checked` }; } return null; }; } - requiredWithBrakeEndurance(label: string): ValidatorFn { + requiredWithBatteryListApplicable(label: string): ValidatorFn { return (control) => { - if (control.parent && !control.value && this.canDisplayWeightSection(control.parent)) { - return { required: `${label} is required when brake endurance is checked` }; + if (control.parent && !control.value && this.canDisplayBatteryListNumberSection(control.parent)) { + return { required: `${label} is required when battery list is applicable` }; } return null; }; } - requiredWithBatteryListApplicable(label: string): ValidatorFn { + requiredWithTankStatementProductList(label: string): ValidatorFn { return (control) => { - if (control.parent && !control.value && this.canDisplayBatteryListNumber(control.parent)) { - return { required: `${label} is required when battery list is applicable` }; + const visible = control.parent && this.canDisplayTankStatementProductListSection(control.parent); + if (visible && !control.value) { + return { required: `${label} is required when under product list` }; + } + + return null; + }; + } + + requiresReferenceNumberOrUNNumber(): ValidatorFn { + return (control) => { + const referenceNumber = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_statement')?.value; + const unNumbers = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo')?.value; + const visible = control.parent && this.canDisplayTankStatementProductListSection(control.parent); + const unNumberPopulated = Array.isArray(unNumbers) && unNumbers.some((un) => un !== null); + + if (visible && (!referenceNumber || !unNumberPopulated)) { + return { required: 'Either reference number or UN number is required' }; } return null; @@ -365,11 +412,39 @@ export class AdrSectionComponent implements OnInit, OnDestroy { } // Dynamically add/remove controls - addTC3TankInspection() {} + addTC3TankInspection() { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); + if (formArray instanceof FormArray) { + formArray.push( + this.fb.group({ + tc3Type: this.fb.control(null), + tc3PeriodicNumber: this.fb.control(null), + tc3PeriodicExpiryDate: this.fb.control(null), + }) + ); + } + } - removeTC3TankInspection(index: number) {} + removeTC3TankInspection(index: number) { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); + if (formArray instanceof FormArray) { + formArray.removeAt(index); + } + } - addUNNumber() {} + addUNNumber() { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (formArray instanceof FormArray) { + formArray.push( + this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]) + ); + } + } - removeUNNumber(index: number) {} + removeUNNumber(index: number) { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (formArray instanceof FormArray) { + formArray.removeAt(index); + } + } } diff --git a/src/app/services/dynamic-forms/dynamic-form.service.ts b/src/app/services/dynamic-forms/dynamic-form.service.ts index 7419fcf958..b85a33577c 100644 --- a/src/app/services/dynamic-forms/dynamic-form.service.ts +++ b/src/app/services/dynamic-forms/dynamic-form.service.ts @@ -4,11 +4,7 @@ import { GlobalError } from '@core/components/global-error/global-error.interfac import { ErrorMessageMap } from '@forms/utils/error-message-map'; // eslint-disable-next-line import/no-cycle import { CustomAsyncValidators } from '@forms/validators/custom-async-validator/custom-async-validators'; -import { - CustomValidators, - EnumValidatorOptions, - IsArrayValidatorOptions, -} from '@forms/validators/custom-validators/custom-validators'; +import { CustomValidators, EnumValidatorOptions, IsArrayValidatorOptions } from '@forms/validators/custom-validators/custom-validators'; import { DefectValidators } from '@forms/validators/defects/defect.validators'; import { AsyncValidatorNames } from '@models/async-validators.enum'; import { Condition } from '@models/condition.model'; @@ -21,246 +17,222 @@ import { CustomFormArray, CustomFormControl, CustomFormGroup, FormNode, FormNode type CustomFormFields = CustomFormControl | CustomFormArray | CustomFormGroup; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class DynamicFormService { - constructor(private store: Store) {} - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - validatorMap: Record ValidatorFn> = { - [ValidatorNames.AheadOfDate]: (arg: string) => CustomValidators.aheadOfDate(arg), - [ValidatorNames.Alphanumeric]: () => CustomValidators.alphanumeric(), - [ValidatorNames.Email]: () => CustomValidators.email(), - [ValidatorNames.CopyValueToRootControl]: (arg: string) => CustomValidators.copyValueToRootControl(arg), - [ValidatorNames.CustomPattern]: (args: string[]) => CustomValidators.customPattern([...args]), - [ValidatorNames.DateNotExceed]: (args: { sibling: string; months: number }) => - CustomValidators.dateNotExceed(args.sibling, args.months), - [ValidatorNames.Defined]: () => CustomValidators.defined(), - [ValidatorNames.DisableIfEquals]: (args: { sibling: string; value: unknown }) => - CustomValidators.disableIfEquals(args.sibling, args.value), - [ValidatorNames.EnableIfEquals]: (args: { sibling: string; value: unknown }) => - CustomValidators.enableIfEquals(args.sibling, args.value), - [ValidatorNames.FutureDate]: () => CustomValidators.futureDate, - [ValidatorNames.PastYear]: () => CustomValidators.pastYear, - [ValidatorNames.HideIfEmpty]: (args: string) => CustomValidators.hideIfEmpty(args), - [ValidatorNames.HideIfNotEqual]: (args: { sibling: string; value: unknown }) => - CustomValidators.hideIfNotEqual(args.sibling, args.value), - [ValidatorNames.HideIfParentSiblingEqual]: (args: { sibling: string; value: unknown }) => - CustomValidators.hideIfParentSiblingEquals(args.sibling, args.value), - [ValidatorNames.HideIfParentSiblingNotEqual]: (args: { sibling: string; value: unknown }) => - CustomValidators.hideIfParentSiblingNotEqual(args.sibling, args.value), - [ValidatorNames.Max]: (args: number) => Validators.max(args), - [ValidatorNames.MaxLength]: (args: number) => Validators.maxLength(args), - [ValidatorNames.Min]: (args: number) => Validators.min(args), - [ValidatorNames.MinLength]: (args: number) => Validators.minLength(args), - [ValidatorNames.NotZNumber]: () => CustomValidators.notZNumber, - [ValidatorNames.Numeric]: () => CustomValidators.numeric(), - [ValidatorNames.PastDate]: () => CustomValidators.pastDate, - [ValidatorNames.Pattern]: (args: string) => Validators.pattern(args), - [ValidatorNames.Required]: () => Validators.required, - [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), - [ValidatorNames.RequiredIfNotEquals]: (args: { sibling: string; value: unknown[] }) => - CustomValidators.requiredIfNotEquals(args.sibling, args.value), - [ValidatorNames.ValidateVRMTrailerIdLength]: (args: { sibling: string }) => - CustomValidators.validateVRMTrailerIdLength(args.sibling), - [ValidatorNames.ValidateDefectNotes]: () => DefectValidators.validateDefectNotes, - [ValidatorNames.ValidateProhibitionIssued]: () => DefectValidators.validateProhibitionIssued, - [ValidatorNames.MustEqualSibling]: (args: { sibling: string }) => CustomValidators.mustEqualSibling(args.sibling), - [ValidatorNames.HandlePsvPassengersChange]: (args: { passengersOne: string; passengersTwo: string }) => - CustomValidators.handlePsvPassengersChange(args.passengersOne, args.passengersTwo), - [ValidatorNames.IsMemberOfEnum]: (args: { - enum: Record; - options?: Partial; - }) => CustomValidators.isMemberOfEnum(args.enum, args.options), - [ValidatorNames.UpdateFunctionCode]: () => CustomValidators.updateFunctionCode(), - [ValidatorNames.ShowGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.showGroupsWhenEqualTo(args.values, args.groups), - [ValidatorNames.HideGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.hideGroupsWhenEqualTo(args.values, args.groups), - [ValidatorNames.ShowGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.showGroupsWhenIncludes(args.values, args.groups), - [ValidatorNames.HideGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.hideGroupsWhenIncludes(args.values, args.groups), - [ValidatorNames.ShowGroupsWhenExcludes]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.showGroupsWhenExcludes(args.values, args.groups), - [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), - [ValidatorNames.Custom]: (...args) => CustomValidators.custom(...args), - [ValidatorNames.Tc3TestValidator]: (args: { inspectionNumber: number }) => CustomValidators.tc3TestValidator(args), - [ValidatorNames.RequiredIfNotHidden]: () => CustomValidators.requiredIfNotHidden(), - [ValidatorNames.DateIsInvalid]: () => CustomValidators.dateIsInvalid, - [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 - asyncValidatorMap: Record AsyncValidatorFn> = { - [AsyncValidatorNames.HideIfEqualsWithCondition]: (args: { - sibling: string; - value: string; - conditions: Condition | Condition[]; - }) => CustomAsyncValidators.hideIfEqualsWithCondition(this.store, args.sibling, args.value, args.conditions), - [AsyncValidatorNames.PassResultDependantOnCustomDefects]: () => - CustomAsyncValidators.passResultDependantOnCustomDefects(this.store), - [AsyncValidatorNames.RequiredIfNotAbandoned]: () => CustomAsyncValidators.requiredIfNotAbandoned(this.store), - [AsyncValidatorNames.RequiredIfNotFail]: () => CustomAsyncValidators.requiredIfNotFail(this.store), - [AsyncValidatorNames.RequiredIfNotResult]: (args: { testResult: resultOfTestEnum | resultOfTestEnum[] }) => - CustomAsyncValidators.requiredIfNotResult(this.store, args.testResult), - [AsyncValidatorNames.RequiredIfNotResultAndSiblingEquals]: (args: { - 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 - createForm(formNode: FormNode, data?: any): CustomFormGroup | CustomFormArray { - if (!formNode) { - return new CustomFormGroup(formNode, {}); - } - - const form: CustomFormGroup | CustomFormArray = - formNode.type === FormNodeTypes.ARRAY - ? new CustomFormArray(formNode, [], this.store) - : new CustomFormGroup(formNode, {}); - - data = data ?? (formNode.type === FormNodeTypes.ARRAY ? [] : {}); - - formNode.children?.forEach((child) => { - const { name, type, value, validators, asyncValidators, disabled } = child; - - const control = - FormNodeTypes.CONTROL === type - ? new CustomFormControl({ ...child }, { value, disabled: !!disabled }) - : this.createForm(child, data[`${name}`]); - - if (validators?.length) { - this.addValidators(control, validators); - } - - if (asyncValidators?.length) { - this.addAsyncValidators(control, asyncValidators); - } - - if (form instanceof FormGroup) { - form.addControl(name, control); - } else if (form instanceof FormArray) { - this.createControls(child, data).forEach((element) => form.push(element)); - } - }); - - if (data) { - form.patchValue(data); - } - - return form; - } - - createControls(child: FormNode, data: unknown): CustomFormFields[] { - // Note: There's a quirk here when dealing with arrays where if - // `data` is an array then `child.name` should be a correct index so - // make sure the template has the correct name to the node. - return Array.isArray(data) - ? data.map(() => - FormNodeTypes.CONTROL !== child.type - ? this.createForm(child, data[Number(child.name)]) - : new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled }) - ) - : [new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled })]; - } - - addValidators(control: CustomFormFields, validators: Array<{ name: ValidatorNames; args?: unknown }> = []) { - validators.forEach((v) => control.addValidators(this.validatorMap[v.name](v.args))); - } - - addAsyncValidators(control: CustomFormFields, validators: Array<{ name: AsyncValidatorNames; args?: unknown }> = []) { - validators.forEach((v) => control.addAsyncValidators(this.asyncValidatorMap[v.name](v.args))); - } - - static validate( - form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, - errors: GlobalError[], - updateValidity = true - ) { - this.getFormLevelErrors(form, errors); - Object.entries(form.controls).forEach(([, value]) => { - if (!(value instanceof FormControl || value instanceof CustomFormControl)) { - this.validate(value as CustomFormGroup | CustomFormArray, errors, updateValidity); - } else { - value.markAsTouched(); - if (updateValidity) { - value.updateValueAndValidity(); - } - (value as CustomFormControl).meta?.changeDetection?.detectChanges(); - this.getControlErrors(value, errors); - } - }); - } - - static getFormLevelErrors(form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, errors: GlobalError[]) { - if (!(form instanceof CustomFormGroup || form instanceof CustomFormArray)) { - return; - } - if (form.errors) { - Object.entries(form.errors).forEach(([key, error]) => { - // If an anchor link is provided, use that, otherwise determine target element from customId or name - const anchorLink = form.meta?.customId ?? form.meta?.name; - errors.push({ - error: ErrorMessageMap[`${key}`](error), - anchorLink, - }); - }); - } - } - - static validateControl(control: FormControl | CustomFormControl, errors: GlobalError[]) { - control.markAsTouched(); - (control as CustomFormControl).meta?.changeDetection?.detectChanges(); - this.getControlErrors(control, errors); - } - - private static getControlErrors(control: FormControl | CustomFormControl, validationErrorList: GlobalError[]) { - const { errors } = control; - const meta = (control as CustomFormControl).meta as FormNode | undefined; - - if (errors) { - if (meta?.hide) return; - 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; - - // 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, - }, - ]; - - validationErrorList.push(...globalErrors); - }); - } - } + constructor(private store: Store) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validatorMap: Record ValidatorFn> = { + [ValidatorNames.AheadOfDate]: (arg: string) => CustomValidators.aheadOfDate(arg), + [ValidatorNames.Alphanumeric]: () => CustomValidators.alphanumeric(), + [ValidatorNames.Email]: () => CustomValidators.email(), + [ValidatorNames.CopyValueToRootControl]: (arg: string) => CustomValidators.copyValueToRootControl(arg), + [ValidatorNames.CustomPattern]: (args: string[]) => CustomValidators.customPattern([...args]), + [ValidatorNames.DateNotExceed]: (args: { sibling: string; months: number }) => CustomValidators.dateNotExceed(args.sibling, args.months), + [ValidatorNames.Defined]: () => CustomValidators.defined(), + [ValidatorNames.DisableIfEquals]: (args: { sibling: string; value: unknown }) => CustomValidators.disableIfEquals(args.sibling, args.value), + [ValidatorNames.EnableIfEquals]: (args: { sibling: string; value: unknown }) => CustomValidators.enableIfEquals(args.sibling, args.value), + [ValidatorNames.FutureDate]: () => CustomValidators.futureDate, + [ValidatorNames.PastYear]: () => CustomValidators.pastYear, + [ValidatorNames.HideIfEmpty]: (args: string) => CustomValidators.hideIfEmpty(args), + [ValidatorNames.HideIfNotEqual]: (args: { sibling: string; value: unknown }) => CustomValidators.hideIfNotEqual(args.sibling, args.value), + [ValidatorNames.HideIfParentSiblingEqual]: (args: { sibling: string; value: unknown }) => + CustomValidators.hideIfParentSiblingEquals(args.sibling, args.value), + [ValidatorNames.HideIfParentSiblingNotEqual]: (args: { sibling: string; value: unknown }) => + CustomValidators.hideIfParentSiblingNotEqual(args.sibling, args.value), + [ValidatorNames.Max]: (args: number) => Validators.max(args), + [ValidatorNames.MaxLength]: (args: number) => Validators.maxLength(args), + [ValidatorNames.Min]: (args: number) => Validators.min(args), + [ValidatorNames.MinLength]: (args: number) => Validators.minLength(args), + [ValidatorNames.NotZNumber]: () => CustomValidators.notZNumber, + [ValidatorNames.Numeric]: () => CustomValidators.numeric(), + [ValidatorNames.PastDate]: () => CustomValidators.pastDate, + [ValidatorNames.Pattern]: (args: string) => Validators.pattern(args), + [ValidatorNames.Required]: () => Validators.required, + [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), + [ValidatorNames.RequiredIfNotEquals]: (args: { sibling: string; value: unknown[] }) => + CustomValidators.requiredIfNotEquals(args.sibling, args.value), + [ValidatorNames.ValidateVRMTrailerIdLength]: (args: { sibling: string }) => CustomValidators.validateVRMTrailerIdLength(args.sibling), + [ValidatorNames.ValidateDefectNotes]: () => DefectValidators.validateDefectNotes, + [ValidatorNames.ValidateProhibitionIssued]: () => DefectValidators.validateProhibitionIssued, + [ValidatorNames.MustEqualSibling]: (args: { sibling: string }) => CustomValidators.mustEqualSibling(args.sibling), + [ValidatorNames.HandlePsvPassengersChange]: (args: { passengersOne: string; passengersTwo: string }) => + CustomValidators.handlePsvPassengersChange(args.passengersOne, args.passengersTwo), + [ValidatorNames.IsMemberOfEnum]: (args: { enum: Record; options?: Partial }) => + CustomValidators.isMemberOfEnum(args.enum, args.options), + [ValidatorNames.UpdateFunctionCode]: () => CustomValidators.updateFunctionCode(), + [ValidatorNames.ShowGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.showGroupsWhenEqualTo(args.values, args.groups), + [ValidatorNames.HideGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.hideGroupsWhenEqualTo(args.values, args.groups), + [ValidatorNames.ShowGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.showGroupsWhenIncludes(args.values, args.groups), + [ValidatorNames.HideGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.hideGroupsWhenIncludes(args.values, args.groups), + [ValidatorNames.ShowGroupsWhenExcludes]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.showGroupsWhenExcludes(args.values, args.groups), + [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), + [ValidatorNames.Custom]: (...args) => CustomValidators.custom(...args), + [ValidatorNames.Tc3TestValidator]: (args: { inspectionNumber: number }) => CustomValidators.tc3TestValidator(args), + [ValidatorNames.RequiredIfNotHidden]: () => CustomValidators.requiredIfNotHidden(), + [ValidatorNames.DateIsInvalid]: () => CustomValidators.dateIsInvalid, + [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 + asyncValidatorMap: Record AsyncValidatorFn> = { + [AsyncValidatorNames.HideIfEqualsWithCondition]: (args: { sibling: string; value: string; conditions: Condition | Condition[] }) => + CustomAsyncValidators.hideIfEqualsWithCondition(this.store, args.sibling, args.value, args.conditions), + [AsyncValidatorNames.PassResultDependantOnCustomDefects]: () => CustomAsyncValidators.passResultDependantOnCustomDefects(this.store), + [AsyncValidatorNames.RequiredIfNotAbandoned]: () => CustomAsyncValidators.requiredIfNotAbandoned(this.store), + [AsyncValidatorNames.RequiredIfNotFail]: () => CustomAsyncValidators.requiredIfNotFail(this.store), + [AsyncValidatorNames.RequiredIfNotResult]: (args: { testResult: resultOfTestEnum | resultOfTestEnum[] }) => + CustomAsyncValidators.requiredIfNotResult(this.store, args.testResult), + [AsyncValidatorNames.RequiredIfNotResultAndSiblingEquals]: (args: { + 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 + createForm(formNode: FormNode, data?: any): CustomFormGroup | CustomFormArray { + if (!formNode) { + return new CustomFormGroup(formNode, {}); + } + + const form: CustomFormGroup | CustomFormArray = + formNode.type === FormNodeTypes.ARRAY ? new CustomFormArray(formNode, [], this.store) : new CustomFormGroup(formNode, {}); + + data = data ?? (formNode.type === FormNodeTypes.ARRAY ? [] : {}); + + formNode.children?.forEach((child) => { + const { name, type, value, validators, asyncValidators, disabled } = child; + + const control = + FormNodeTypes.CONTROL === type + ? new CustomFormControl({ ...child }, { value, disabled: !!disabled }) + : this.createForm(child, data[`${name}`]); + + if (validators?.length) { + this.addValidators(control, validators); + } + + if (asyncValidators?.length) { + this.addAsyncValidators(control, asyncValidators); + } + + if (form instanceof FormGroup) { + form.addControl(name, control); + } else if (form instanceof FormArray) { + this.createControls(child, data).forEach((element) => form.push(element)); + } + }); + + if (data) { + form.patchValue(data); + } + + return form; + } + + createControls(child: FormNode, data: unknown): CustomFormFields[] { + // Note: There's a quirk here when dealing with arrays where if + // `data` is an array then `child.name` should be a correct index so + // make sure the template has the correct name to the node. + return Array.isArray(data) + ? data.map(() => + FormNodeTypes.CONTROL !== child.type + ? this.createForm(child, data[Number(child.name)]) + : new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled }) + ) + : [new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled })]; + } + + addValidators(control: CustomFormFields, validators: Array<{ name: ValidatorNames; args?: unknown }> = []) { + validators.forEach((v) => control.addValidators(this.validatorMap[v.name](v.args))); + } + + addAsyncValidators(control: CustomFormFields, validators: Array<{ name: AsyncValidatorNames; args?: unknown }> = []) { + validators.forEach((v) => control.addAsyncValidators(this.asyncValidatorMap[v.name](v.args))); + } + + static validate(form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, errors: GlobalError[], updateValidity = true) { + this.getFormLevelErrors(form, errors); + Object.entries(form.controls).forEach(([, value]) => { + if (!(value instanceof FormControl || value instanceof CustomFormControl)) { + this.validate(value as CustomFormGroup | CustomFormArray, errors, updateValidity); + } else { + value.markAsTouched(); + if (updateValidity) { + value.updateValueAndValidity(); + } + (value as CustomFormControl).meta?.changeDetection?.detectChanges(); + this.getControlErrors(value, errors); + } + }); + } + + static getFormLevelErrors(form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, errors: GlobalError[]) { + if (!(form instanceof CustomFormGroup || form instanceof CustomFormArray)) { + return; + } + if (form.errors) { + Object.entries(form.errors).forEach(([key, error]) => { + // If an anchor link is provided, use that, otherwise determine target element from customId or name + const anchorLink = form.meta?.customId ?? form.meta?.name; + errors.push({ + error: ErrorMessageMap[`${key}`](error), + anchorLink, + }); + }); + } + } + + static validateControl(control: FormControl | CustomFormControl, errors: GlobalError[]) { + control.markAsTouched(); + (control as CustomFormControl).meta?.changeDetection?.detectChanges(); + this.getControlErrors(control, errors); + } + + private static getControlErrors(control: FormControl | CustomFormControl, validationErrorList: GlobalError[]) { + const { errors } = control; + const meta = (control as CustomFormControl).meta as FormNode | undefined; + + if (errors) { + if (meta?.hide) return; + 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; + + // 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, + }, + ]; + + validationErrorList.push(...globalErrors); + }); + } + } } From d0f4abceb3e6833782938e273170bfa42193c45f Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:56:06 +0100 Subject: [PATCH 003/211] chore(cb2-0000): add view and summary mode --- ...tech-record-summary-changes.component.html | 7 +- .../tech-record-summary-changes.component.ts | 436 +++---- .../tech-record-summary.component.html | 2 +- .../tech-record-summary.component.ts | 3 +- .../adr-section-edit.component.html | 1158 +++++++++++++++++ .../adr-section-edit.component.scss | 0 .../adr-section-edit.component.ts | 390 ++++++ .../adr-section-summary.component.html | 271 ++++ .../adr-section-summary.component.scss | 0 .../adr-section-summary.component.ts | 25 + .../adr-section-view.component.html | 259 ++++ .../adr-section-view.component.scss | 0 .../adr-section-view.component.ts | 16 + .../adr-section/adr-section.component.html | 1130 +--------------- .../adr-section/adr-section.component.ts | 448 +------ src/app/forms/dynamic-forms.module.ts | 9 + src/app/services/adr/adr.service.ts | 124 +- 17 files changed, 2454 insertions(+), 1824 deletions(-) create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.scss create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.html create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.scss create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.html create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.scss create mode 100644 src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts diff --git a/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.html b/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.html index ac2580f503..eaf4cc89dc 100644 --- a/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.html +++ b/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.html @@ -94,7 +94,12 @@ techRecordEdited.techRecord_vehicleType === 'lgv' " > - + + + + + + diff --git a/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts b/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts index 127829148c..2f85131714 100644 --- a/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts @@ -3,11 +3,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { GlobalErrorService } from '@core/components/global-error/global-error.service'; import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb'; import { - TechRecordGETCar, - TechRecordGETHGV, - TechRecordGETLGV, - TechRecordGETPSV, - TechRecordGETTRL, + TechRecordGETCar, + TechRecordGETHGV, + TechRecordGETLGV, + TechRecordGETPSV, + TechRecordGETTRL, } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb-vehicle-type'; import { VehicleSummary } from '@forms/templates/tech-records/vehicle-summary.template'; import { vehicleTemplateMap } from '@forms/utils/tech-record-constants'; @@ -15,232 +15,224 @@ import { Axles, VehicleTypes } from '@models/vehicle-tech-record.model'; import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { FormNode, FormNodeViewTypes } from '@services/dynamic-forms/dynamic-form.types'; +import { FeatureToggleService } from '@services/feature-toggle-service/feature-toggle-service'; import { RouterService } from '@services/router/router.service'; import { TechnicalRecordService } from '@services/technical-record/technical-record.service'; import { UserService } from '@services/user-service/user-service'; import { State } from '@store/index'; import { - clearADRDetailsBeforeUpdate, - clearAllSectionStates, - clearScrollPosition, - editingTechRecord, - selectTechRecordChanges, - selectTechRecordDeletions, - techRecord, - updateADRAdditionalExaminerNotes, - updateTechRecord, - updateTechRecordSuccess, + clearADRDetailsBeforeUpdate, + clearAllSectionStates, + clearScrollPosition, + editingTechRecord, + selectTechRecordChanges, + selectTechRecordDeletions, + techRecord, + updateADRAdditionalExaminerNotes, + updateTechRecord, + updateTechRecordSuccess, } from '@store/technical-records'; import { Subject, combineLatest, map, take, takeUntil } from 'rxjs'; @Component({ - selector: 'app-tech-record-summary-changes', - templateUrl: './tech-record-summary-changes.component.html', - styleUrls: ['./tech-record-summary-changes.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-tech-record-summary-changes', + templateUrl: './tech-record-summary-changes.component.html', + styleUrls: ['./tech-record-summary-changes.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TechRecordSummaryChangesComponent implements OnInit, OnDestroy { - destroy$ = new Subject(); - - techRecord?: TechRecordType<'get'>; - techRecordEdited?: TechRecordType<'put'>; - techRecordChanges?: Partial>; - techRecordDeletions?: Partial>; - techRecordChangesKeys: string[] = []; - - sectionsWhitelist: string[] = []; - username = ''; - - constructor( - public store$: Store, - public technicalRecordService: TechnicalRecordService, - public router: Router, - public globalErrorService: GlobalErrorService, - public route: ActivatedRoute, - public routerService: RouterService, - public actions$: Actions, - public userService$: UserService - ) {} - - ngOnInit(): void { - this.navigateUponSuccess(); - this.initSubscriptions(); - } - - navigateUponSuccess(): void { - this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { - this.store$.dispatch(clearAllSectionStates()); - this.store$.dispatch(clearScrollPosition()); - void this.router.navigate([ - `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, - ]); - }); - } - - initSubscriptions(): void { - this.userService$.name$.pipe(takeUntil(this.destroy$)).subscribe((name) => { - this.username = name; - }); - this.store$ - .select(techRecord) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((data) => { - if (!data) this.cancel(); - this.techRecord = data; - }); - - this.store$ - .select(editingTechRecord) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((data) => { - if (!data) this.cancel(); - this.techRecordEdited = data; - }); - - this.store$ - .select(selectTechRecordChanges) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((changes) => { - this.techRecordChanges = changes; - if (this.vehicleType === VehicleTypes.PSV || this.vehicleType === VehicleTypes.HGV) { - delete (this.techRecordChanges as Partial) - .techRecord_numberOfWheelsDriven; - } - if ( - (this.vehicleType === VehicleTypes.CAR || this.vehicleType === VehicleTypes.LGV) && - (changes as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass - ) { - (this.techRecordChanges as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass = ( - this.techRecordEdited as TechRecordGETCar | TechRecordGETLGV - ).techRecord_vehicleSubclass; - } - this.techRecordChangesKeys = this.getTechRecordChangesKeys(); - this.sectionsWhitelist = this.getSectionsWhitelist(); - }); - - this.store$ - .select(selectTechRecordDeletions) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((deletions) => { - this.techRecordDeletions = deletions; - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get vehicleType() { - return this.techRecordEdited - ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordEdited) - : undefined; - } - - get vehicleSummary(): FormNode { - return VehicleSummary; - } - - get deletedAxles(): Axles { - if (this.techRecordEdited?.techRecord_vehicleType === 'hgv' && this.techRecordDeletions) { - return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); - } - - if (this.techRecordEdited?.techRecord_vehicleType === 'trl' && this.techRecordDeletions) { - return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); - } - - if (this.techRecordEdited?.techRecord_vehicleType === 'psv' && this.techRecordDeletions) { - return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); - } - - return []; - } - - get sectionTemplatesState$() { - return this.technicalRecordService.sectionStates$; - } - - isSectionExpanded$(sectionName: string | number) { - return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); - } - - submit() { - combineLatest([ - this.routerService.getRouteNestedParam$('systemNumber'), - this.routerService.getRouteNestedParam$('createdTimestamp'), - ]) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe(([systemNumber, createdTimestamp]) => { - if (systemNumber && createdTimestamp) { - this.store$.dispatch(updateADRAdditionalExaminerNotes({ username: this.username })); - this.store$.dispatch(clearADRDetailsBeforeUpdate()); - this.store$.dispatch(updateTechRecord({ systemNumber, createdTimestamp })); - } - }); - } - - cancel() { - this.globalErrorService.clearErrors(); - void this.router.navigate(['..'], { relativeTo: this.route }); - } - - getTechRecordChangesKeys(): string[] { - const entries = Object.entries(this.techRecordChanges ?? {}); - const filter = entries.filter(([, value]) => this.isNotEmpty(value)); - const changeMap = filter.map(([key]) => key); - return changeMap; - } - - getSectionsWhitelist() { - const whitelist: string[] = []; - if (this.vehicleType == null) return whitelist; - if (this.techRecordChanges == null) return whitelist; - if (this.technicalRecordService.haveAxlesChanged(this.vehicleType, this.techRecordChanges)) { - whitelist.push('weightsSection'); - } - - return whitelist; - } - - get changesForWeights() { - if (this.techRecordEdited == null) return undefined; - - return ['hgv', 'trl', 'psv'].includes(this.techRecordEdited.techRecord_vehicleType) - ? (this.techRecordChanges as Partial) - : undefined; - } - - get vehicleTemplates() { - return vehicleTemplateMap - .get(this.techRecordEdited?.techRecord_vehicleType as VehicleTypes) - ?.filter((template) => template.name !== 'technicalRecordSummary'); - } - - get customVehicleTemplate() { - return this.vehicleTemplates - ?.map((vehicleTemplate) => ({ - ...this.toVisibleFormNode(vehicleTemplate), - children: vehicleTemplate.children - ?.filter((child) => { - return this.techRecordChangesKeys.includes(child.name); - }) - .map((child) => this.toVisibleFormNode(child)), - })) - .filter( - (section) => - Boolean(section && section.children && section.children.length > 0) || - this.sectionsWhitelist.includes(section.name) - ); - } - - toVisibleFormNode(node: FormNode): FormNode { - return { ...node, viewType: node.viewType === FormNodeViewTypes.HIDDEN ? FormNodeViewTypes.STRING : node.viewType }; - } - - isNotEmpty(value: unknown): boolean { - if (value === '' || value === undefined) return false; - if (typeof value === 'object' && value !== null) return Object.values(value).length > 0; - return true; - } + destroy$ = new Subject(); + + techRecord?: TechRecordType<'get'>; + techRecordEdited?: TechRecordType<'put'>; + techRecordChanges?: Partial>; + techRecordDeletions?: Partial>; + techRecordChangesKeys: string[] = []; + + sectionsWhitelist: string[] = []; + username = ''; + + constructor( + public store$: Store, + public technicalRecordService: TechnicalRecordService, + public router: Router, + public globalErrorService: GlobalErrorService, + public route: ActivatedRoute, + public routerService: RouterService, + public actions$: Actions, + public userService$: UserService, + public featureToggleService: FeatureToggleService + ) {} + + ngOnInit(): void { + this.navigateUponSuccess(); + this.initSubscriptions(); + } + + navigateUponSuccess(): void { + this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { + this.store$.dispatch(clearAllSectionStates()); + this.store$.dispatch(clearScrollPosition()); + void this.router.navigate([ + `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, + ]); + }); + } + + initSubscriptions(): void { + this.userService$.name$.pipe(takeUntil(this.destroy$)).subscribe((name) => { + this.username = name; + }); + this.store$ + .select(techRecord) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((data) => { + if (!data) this.cancel(); + this.techRecord = data; + }); + + this.store$ + .select(editingTechRecord) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((data) => { + if (!data) this.cancel(); + this.techRecordEdited = data; + }); + + this.store$ + .select(selectTechRecordChanges) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((changes) => { + this.techRecordChanges = changes; + if (this.vehicleType === VehicleTypes.PSV || this.vehicleType === VehicleTypes.HGV) { + delete (this.techRecordChanges as Partial).techRecord_numberOfWheelsDriven; + } + if ( + (this.vehicleType === VehicleTypes.CAR || this.vehicleType === VehicleTypes.LGV) && + (changes as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass + ) { + (this.techRecordChanges as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass = ( + this.techRecordEdited as TechRecordGETCar | TechRecordGETLGV + ).techRecord_vehicleSubclass; + } + this.techRecordChangesKeys = this.getTechRecordChangesKeys(); + this.sectionsWhitelist = this.getSectionsWhitelist(); + }); + + this.store$ + .select(selectTechRecordDeletions) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((deletions) => { + this.techRecordDeletions = deletions; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + get vehicleType() { + return this.techRecordEdited ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordEdited) : undefined; + } + + get vehicleSummary(): FormNode { + return VehicleSummary; + } + + get deletedAxles(): Axles { + if (this.techRecordEdited?.techRecord_vehicleType === 'hgv' && this.techRecordDeletions) { + return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); + } + + if (this.techRecordEdited?.techRecord_vehicleType === 'trl' && this.techRecordDeletions) { + return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); + } + + if (this.techRecordEdited?.techRecord_vehicleType === 'psv' && this.techRecordDeletions) { + return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); + } + + return []; + } + + get sectionTemplatesState$() { + return this.technicalRecordService.sectionStates$; + } + + isSectionExpanded$(sectionName: string | number) { + return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); + } + + submit() { + combineLatest([this.routerService.getRouteNestedParam$('systemNumber'), this.routerService.getRouteNestedParam$('createdTimestamp')]) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe(([systemNumber, createdTimestamp]) => { + if (systemNumber && createdTimestamp) { + this.store$.dispatch(updateADRAdditionalExaminerNotes({ username: this.username })); + this.store$.dispatch(clearADRDetailsBeforeUpdate()); + this.store$.dispatch(updateTechRecord({ systemNumber, createdTimestamp })); + } + }); + } + + cancel() { + this.globalErrorService.clearErrors(); + void this.router.navigate(['..'], { relativeTo: this.route }); + } + + getTechRecordChangesKeys(): string[] { + const entries = Object.entries(this.techRecordChanges ?? {}); + const filter = entries.filter(([, value]) => this.isNotEmpty(value)); + const changeMap = filter.map(([key]) => key); + return changeMap; + } + + getSectionsWhitelist() { + const whitelist: string[] = []; + if (this.vehicleType == null) return whitelist; + if (this.techRecordChanges == null) return whitelist; + if (this.technicalRecordService.haveAxlesChanged(this.vehicleType, this.techRecordChanges)) { + whitelist.push('weightsSection'); + } + + return whitelist; + } + + get changesForWeights() { + if (this.techRecordEdited == null) return undefined; + + return ['hgv', 'trl', 'psv'].includes(this.techRecordEdited.techRecord_vehicleType) + ? (this.techRecordChanges as Partial) + : undefined; + } + + get vehicleTemplates() { + return vehicleTemplateMap + .get(this.techRecordEdited?.techRecord_vehicleType as VehicleTypes) + ?.filter((template) => template.name !== 'technicalRecordSummary'); + } + + get customVehicleTemplate() { + return this.vehicleTemplates + ?.map((vehicleTemplate) => ({ + ...this.toVisibleFormNode(vehicleTemplate), + children: vehicleTemplate.children + ?.filter((child) => { + return this.techRecordChangesKeys.includes(child.name); + }) + .map((child) => this.toVisibleFormNode(child)), + })) + .filter((section) => Boolean(section && section.children && section.children.length > 0) || this.sectionsWhitelist.includes(section.name)); + } + + toVisibleFormNode(node: FormNode): FormNode { + return { ...node, viewType: node.viewType === FormNodeViewTypes.HIDDEN ? FormNodeViewTypes.STRING : node.viewType }; + } + + isNotEmpty(value: unknown): boolean { + if (value === '' || value === undefined) return false; + if (typeof value === 'object' && value !== null) return Object.values(value).length > 0; + return true; + } } diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html index 8dd9103203..e4bb51c236 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html @@ -127,7 +127,7 @@ > - + diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts index 4a4176970d..93396dd164 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts @@ -152,7 +152,7 @@ export class TechRecordSummaryComponent implements OnInit, OnDestroy { } }); - this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => this.handleFormState(this.form.value)); + this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((changes) => this.handleFormState(changes)); } ngOnDestroy(): void { @@ -249,7 +249,6 @@ export class TechRecordSummaryComponent implements OnInit, OnDestroy { } extractErrors(this.form); - console.log(errors, this.form); if (errors.length) { this.errorService.setErrors(errors); diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html new file mode 100644 index 0000000000..df56a08e3d --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html @@ -0,0 +1,1158 @@ +
+
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + + + + + Applicant Details + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+
+ + + + ADR Details + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + +
+
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + +
+
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+
+ + + + + + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ + +
+

+ +

+

+ Error: + + {{ error }} + +

+
+
+ + +
+
+
+ + + + +
+

+ +

+

+ Error: + + {{ error }} + +

+
+
+ + +
+
+
+ + + + +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + +

+ +
+
+ + + + +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + +

+ +
+ + +
+
+
+

+ +

+ + Remove +
+
+ Add UN Number +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ You have {{ 500 - (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 500) - 500 }} too many +
+
+ + + + Tank Inspections + + +
+

+ +

+

+ Error: + {{ + error + }} + {{ + error + }} +

+ +
+ +
+ + + + +
+
+ +
+

+ +

+ +
+ + +
+

+ +

+ +
+ + + + +

+ Remove +

+
+
+
+ +

+ Add subsequent inspection +

+
+ + +
+
+ +

Memo 07/09 (3 month extension) can be applied

+
+
Only applicable for vehicles used on national journeys
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+
+ +

M145

+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+
+ + + + +
+

+ +

+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+ + + + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+
+
+ + + Declarations seen + + +
+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ You have {{ 500 - (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 500) - 500 }} too many +
+
+ + +
+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+
+ + +
+
+ + +
+
+ +

Owner/operator declaration

+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+
+ +

New certificate required

+
+

+ Error: + {{ error }} +

+
+
+ + +
+
+
+
+ + +
+

+ +

+
Will not be present on the ADR certificate
+

+ Error: + {{ error }} +

+ +
+ You have {{ 1024 - (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 1024) - 1024 }} too many +
+
+ + +

Additional Examiner Notes History

+ + + + + + + + + + + + + + + + + + + +
NotesCreated ByDate
{{ note.note | defaultNullOrEmpty }}{{ note.lastUpdatedBy | defaultNullOrEmpty }}{{ note.createdAtDate | date : 'dd/MM/yyyy HH:mm' | defaultNullOrEmpty }} + Edit +
+ +
+
+ +
+
+
No additional examiner notes history available
+
+
+
+ + +
+

+ +

+

+ Error: + {{ error }} +

+ +
+ You have {{ 1500 - (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 1500) - 1500 }} too many +
+
+
+
diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.scss b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts new file mode 100644 index 0000000000..f471a1fd9d --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts @@ -0,0 +1,390 @@ +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { ControlContainer, FormArray, FormBuilder, FormGroup, ValidatorFn } from '@angular/forms'; +import { ADRAdditionalNotesNumber } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrAdditionalNotesNumber.enum'; +import { ADRBodyType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrBodyType.enum'; +import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum'; +import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum'; +import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankStatementSubstancePermitted'; +import { TC3Types } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/tc3Types.enum'; +import { AdditionalExaminerNotes } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete'; +import { getOptionsFromEnum } from '@forms/utils/enum-map'; +import { TC2Types } from '@models/adr.enum'; +import { Store } from '@ngrx/store'; +import { AdrService } from '@services/adr/adr.service'; +import { techRecord } from '@store/technical-records'; +import { ReplaySubject, take } from 'rxjs'; + +@Component({ + selector: 'app-adr-section-edit', + templateUrl: './adr-section-edit.component.html', + styleUrls: ['./adr-section-edit.component.scss'], +}) +export class AdrSectionEditComponent implements OnInit, OnDestroy { + fb = inject(FormBuilder); + store = inject(Store); + adrService = inject(AdrService); + controlContainer = inject(ControlContainer); + + destroy$ = new ReplaySubject(1); + + form = this.fb.group( + { + techRecord_adrDetails_dangerousGoods: this.fb.control(false), + + // Applicant Details + techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), + techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [this.maxLength(150, 'Street')]), + techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), + techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), + techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [this.maxLength(25, 'Postcode')]), + + // ADR Details + techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [this.requiredWithDangerousGoods('ADR body type')]), + techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null, [ + this.requiredWithDangerousGoods('Used on international journeys'), + ]), + techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null), + techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ + this.requiredWithDangerousGoods('Permitted dangerous goods'), + ]), + techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [this.requiredWithExplosives('Compatibility Group J')]), + techRecord_adrDetails_additionalNotes_number: this.fb.control([], [this.requiredWithDangerousGoods('Guidance notes')]), + techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [this.maxLength(40, 'ADR type approval number')]), + + // Tank Details + techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Make'), + this.maxLength(70, 'Tank Make'), + ]), + techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Year of manufacture'), + this.maxLength(70, 'Tank Year of manufacture'), + ]), + techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Manufacturer serial number'), + this.maxLength(70, 'Manufacturer serial number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank type approval number'), + this.maxLength(65, 'Tank type approval number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ + this.requiredWithTankOrBattery('Code'), + this.maxLength(30, 'Code'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ + this.requiredWithTankOrBattery('Substances permitted'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control(null, this.requiredWithTankStatement('Select')), + techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ + this.requiredWithUNNumber('Reference number'), + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: this.fb.control(null, [ + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array([ + this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, []), + techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [this.maxLength(1500, 'Special provisions')]), + + // Tank Details > Tank Inspections + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('TC2: Certificate Number'), + this.maxLength(70, 'TC2: Certificate Number'), + ]), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control(null, [ + this.requiredWithTankOrBattery('TC2: Expiry Date'), + ]), + techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), + + // Miscellaneous + techRecord_adrDetails_memosApply: this.fb.control(null), + techRecord_adrDetails_m145Statement: this.fb.control(null), + + // Battery List + techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [this.requiredWithBattery('Battery list applicable')]), + techRecord_adrDetails_batteryListNumber: this.fb.control(null, [this.requiredWithBatteryListApplicable('Reference Number')]), + + // Brake declaration + techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), + techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null, [this.maxLength(500, 'Issuer')]), + techRecord_adrDetails_brakeEndurance: this.fb.control(false), + techRecord_adrDetails_weight: this.fb.control(null, [ + this.max(99999999, 'Weight (tonnes)'), + this.requiredWithBrakeEndurance('Weight (tonnes)'), + this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), + ]), + + // Other declarations + techRecord_adrDetails_declarationsSeen: this.fb.control(false), + + // Miscellaneous + techRecord_adrDetails_newCertificateRequested: this.fb.control(false), + techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), + techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), + techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [this.maxLength(1500, 'ADR Certificate Notes')]), + }, + { validators: [this.requiresReferenceNumberOrUNNumber()] } + ); + + // Option lists + dangerousGoodsOptions = [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ]; + + adrBodyTypesOptions = getOptionsFromEnum(ADRBodyType); + + usedOnInternationJourneysOptions = [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'n/a', label: 'Not applicable' }, + ]; + + permittedDangerousGoodsOptions = getOptionsFromEnum(ADRDangerousGood); + + guidanceNotesOptions = getOptionsFromEnum(ADRAdditionalNotesNumber); + + compatibilityGroupJOptions = [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ]; + + tankStatementSubstancePermittedOptions = getOptionsFromEnum(ADRTankStatementSubstancePermitted); + + tankStatementSelectOptions = getOptionsFromEnum(ADRTankDetailsTankStatementSelect); + + batteryListApplicableOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ]; + + tc3InspectionOptions = getOptionsFromEnum(TC3Types); + + isInvalid(formControlName: string) { + const control = this.form.get(formControlName); + return control?.invalid && control?.touched; + } + + toggle(formControlName: string, value: string) { + const control = this.form.get(formControlName); + if (!control) return; + + // If this is the first checkbox, set the value to an array + if (control.value === null) { + return control.setValue([value]); + } + + // If the value is already an array, toggle the value - if the array is then empty, set the value to null + if (Array.isArray(control.value)) { + control.value.includes(value) ? control.value.splice(control.value.indexOf(value), 1) : control.value.push(value); + if (control.value.length === 0) { + control.setValue(null); + } + } + } + + ngOnInit(): void { + // Attatch all form controls to parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + Object.entries(this.form.controls).forEach(([key, control]) => parent.addControl(key, control)); + } + + this.store + .select(techRecord) + .pipe(take(1)) + .subscribe((techRecord) => { + if (techRecord) this.form.patchValue(techRecord as any); + }); + } + + ngOnDestroy(): void { + // Detatch all form controls from parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + Object.keys(this.form.controls).forEach((key) => parent.removeControl(key)); + } + + // Clear subscriptions + this.destroy$.next(true); + this.destroy$.complete(); + } + + // Custom validators + requiredWithDangerousGoods(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayDangerousGoodsSection(control.parent.value)) { + return { required: `${label} is required when dangerous goods are present` }; + } + + return null; + }; + } + + requiredWithExplosives(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayCompatibilityGroupJSection(control.parent.value)) { + return { required: `${label} is required when Explosives Type 2 or Explosive Type 3` }; + } + + return null; + }; + } + + requiredWithBattery(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayBatterySection(control.parent.value)) { + return { required: `${label} is required when ADR body type is of type 'battery'` }; + } + + return null; + }; + } + + requiredWithTankOrBattery(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayTankOrBatterySection(control.parent.value)) { + return { required: `${label} is required when ADR body type is of type 'tank' or 'battery'` }; + } + + return null; + }; + } + + requiredWithTankStatement(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayTankStatementSelectSection(control.parent.value)) { + return { required: `${label} is required with substances permitted` }; + } + + return null; + }; + } + + requiredWithUNNumber(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayTankStatementStatementSection(control.parent.value)) { + return { required: `${label} is required when under UN number` }; + } + + return null; + }; + } + + requiredWithBrakeEndurance(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayWeightSection(control.parent.value)) { + return { required: `${label} is required when brake endurance is checked` }; + } + + return null; + }; + } + + requiredWithBatteryListApplicable(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayBatteryListNumberSection(control.parent.value)) { + return { required: `${label} is required when battery list is applicable` }; + } + + return null; + }; + } + + requiredWithTankStatementProductList(label: string): ValidatorFn { + return (control) => { + const visible = control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value); + if (visible && !control.value) { + return { required: `${label} is required when under product list` }; + } + + return null; + }; + } + + requiresReferenceNumberOrUNNumber(): ValidatorFn { + return (control) => { + const referenceNumber = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_statement')?.value; + const unNumbers = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo')?.value; + const visible = control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value); + const unNumberPopulated = Array.isArray(unNumbers) && unNumbers.some((un) => un !== null); + + if (visible && (!referenceNumber || !unNumberPopulated)) { + return { required: 'Either reference number or UN number is required' }; + } + + return null; + }; + } + + max(size: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value > size) { + return { max: `${label} must be less than or equal to ${size}` }; + } + + return null; + }; + } + + maxLength(length: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value.length > length) { + return { maxLength: `${label} must be less than or equal to ${length} characters` }; + } + + return null; + }; + } + + pattern(pattern: string | RegExp, label: string): ValidatorFn { + return (control) => { + if (control.value && !new RegExp(pattern).test(control.value)) { + return { pattern: `${label} is invalid` }; + } + + return null; + }; + } + + // Dynamically add/remove controls + addTC3TankInspection() { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); + if (formArray instanceof FormArray) { + formArray.push( + this.fb.group({ + tc3Type: this.fb.control(null), + tc3PeriodicNumber: this.fb.control(null), + tc3PeriodicExpiryDate: this.fb.control(null), + }) + ); + } + } + + removeTC3TankInspection(index: number) { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); + if (formArray instanceof FormArray) { + formArray.removeAt(index); + } + } + + addUNNumber() { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (formArray instanceof FormArray) { + formArray.push( + this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]) + ); + } + } + + removeUNNumber(index: number) { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (formArray instanceof FormArray) { + formArray.removeAt(index); + } + } +} diff --git a/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.html b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.html new file mode 100644 index 0000000000..0fa857c5e8 --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.html @@ -0,0 +1,271 @@ + + + +
+
+
Carries dangerous goods
+
{{ amended.techRecord_adrDetails_dangerousGoods | defaultNullOrEmpty }}
+
+
+ + +

Applicant Details

+
+
+
Name
+
{{ amended.techRecord_adrDetails_applicantDetails_name | defaultNullOrEmpty }}
+
+
+
Street
+
{{ amended.techRecord_adrDetails_applicantDetails_street | defaultNullOrEmpty }}
+
+
+
Town
+
{{ amended.techRecord_adrDetails_applicantDetails_town | defaultNullOrEmpty }}
+
+
+
City
+
{{ amended.techRecord_adrDetails_applicantDetails_city | defaultNullOrEmpty }}
+
+
+
Postcode
+
{{ amended.techRecord_adrDetails_applicantDetails_postcode | defaultNullOrEmpty }}
+
+
+ + +

ADR Details

+
+
+
ADR body type
+
{{ amended.techRecord_adrDetails_vehicleDetails_type | defaultNullOrEmpty }}
+
+
+
Vehicle used on international journeys
+
+ {{ amended.techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys | defaultNullOrEmpty }} +
+
+
+
Date processed
+
{{ amended.techRecord_adrDetails_vehicleDetails_approvalDate | defaultNullOrEmpty }}
+
+
+
Permitted dangerous goods
+
{{ amended.techRecord_adrDetails_permittedDangerousGoods | defaultNullOrEmpty }}
+
+
+
Compatibility Group J
+
{{ amended.techRecord_adrDetails_compatibilityGroupJ | defaultNullOrEmpty }}
+
+
+
Guidance notes
+
{{ amended.techRecord_adrDetails_additionalNotes_number | defaultNullOrEmpty }}
+
+
+
ADR type approval number
+
{{ amended.techRecord_adrDetails_adrTypeApprovalNo | defaultNullOrEmpty }}
+
+
+ + + +

Tank Details

+
+
+
Tank Make
+
{{ amended.techRecord_adrDetails_tank_tankDetails_tankManufacturer | defaultNullOrEmpty }}
+
+
+
Tank Year of manufacture
+
{{ amended.techRecord_adrDetails_tank_tankDetails_yearOfManufacture | defaultNullOrEmpty }}
+
+
+
Manufacturer serial number
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo | defaultNullOrEmpty }} +
+
+
+
Tank type approval number
+
{{ amended.techRecord_adrDetails_tank_tankDetails_tankTypeAppNo | defaultNullOrEmpty }}
+
+
+
Code
+
{{ amended.techRecord_adrDetails_tank_tankDetails_tankCode | defaultNullOrEmpty }}
+
+
+
Substances permitted
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted | defaultNullOrEmpty }} +
+
+
+
Reference number
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tankStatement_statement | defaultNullOrEmpty }} +
+
+
+
Reference number
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo | defaultNullOrEmpty }} +
+
+
+
Additional Details
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tankStatement_productList | defaultNullOrEmpty }} +
+
+
+
Special provisions
+
{{ amended.techRecord_adrDetails_tank_tankDetails_specialProvisions | defaultNullOrEmpty }}
+
+
+ + +

Tank Inspections

+ + +
+

Initial

+
+
Certificate number
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo | defaultNullOrEmpty }} +
+
+
+
Expiry date
+
+ {{ amended.techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate | defaultNullOrEmpty }} +
+
+
+ + +
+ +

Subsequent {{ i + 1 }}

+
+
Inspection type
+
{{ inspection.tc3Type | defaultNullOrEmpty }}
+
+
+
Certificate number
+
{{ inspection.tc3PeriodicNumber | defaultNullOrEmpty }}
+
+
+
Expiry date
+
{{ inspection.tc3PeriodicExpiryDate | defaultNullOrEmpty }}
+
+
+
+ + +
+
+
Memo 07/09 (3 month extension) can be applied
+
{{ amended.techRecord_adrDetails_memosApply | defaultNullOrEmpty }}
+
+
+
M145
+
{{ amended.techRecord_adrDetails_m145Statement | defaultNullOrEmpty }}
+
+
+
+ + + +

Battery List Applicable

+
+
+
Battery List Applicable
+
{{ amended.techRecord_adrDetails_listStatementApplicable | defaultNullOrEmpty }}
+
+
+
Reference number
+
{{ amended.techRecord_adrDetails_batteryListNumber | defaultNullOrEmpty }}
+
+
+
+ + +
+

Declarations seen

+
+
+
Manufacturer brake declaration
+
{{ amended.techRecord_adrDetails_brakeDeclarationsSeen | defaultNullOrEmpty }}
+
+
+
Issuer
+
{{ amended.techRecord_adrDetails_brakeDeclarationIssuer | defaultNullOrEmpty }}
+
+
+
Brake endurance
+
{{ amended.techRecord_adrDetails_brakeEndurance | defaultNullOrEmpty }}
+
+
+
Weight
+
{{ amended.techRecord_adrDetails_weight | defaultNullOrEmpty }}
+
+
+
Owner/operator declaration
+
{{ amended.techRecord_adrDetails_declarationsSeen | defaultNullOrEmpty }}
+
+
+
+ +

New certificate requested

+
+
+
New certificate requested
+
{{ amended.techRecord_adrDetails_newCertificateRequested | defaultNullOrEmpty }}
+
+
+ +

Additional Examiner Notes History

+ + + + + + + + + + + + + + + + + +
NotesCreated ByDate
{{ note.note | defaultNullOrEmpty }}{{ note.lastUpdatedBy | defaultNullOrEmpty }}{{ note.createdAtDate | date : 'dd/MM/yyyy HH:mm' | defaultNullOrEmpty }}
+ +
+
+ +
+
+
No additional examiner notes history available
+
+
+
+ +

ADR Certificate Notes

+
+
+
ADR Certificate Notes
+
{{ amended.techRecord_adrDetails_adrCertificateNotes | defaultNullOrEmpty }}
+
+
+
+
+
+
diff --git a/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.scss b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts new file mode 100644 index 0000000000..1f77773e70 --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts @@ -0,0 +1,25 @@ +import { Component, inject } from '@angular/core'; +import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; +import { Store } from '@ngrx/store'; +import { AdrService } from '@services/adr/adr.service'; +import { editingTechRecord, techRecord } from '@store/technical-records'; + +@Component({ + selector: 'app-adr-section-summary', + templateUrl: './adr-section-summary.component.html', + styleUrls: ['./adr-section-summary.component.scss'], +}) +export class AdrSectionSummaryComponent { + store = inject(Store); + adrService = inject(AdrService); + + currentTechRecord = this.store.selectSignal(techRecord); + amendedTechRecord = this.store.selectSignal(editingTechRecord); + + hasChanged(property: keyof TechRecordType<'hgv' | 'trl' | 'lgv'>) { + const current = this.currentTechRecord() as TechRecordType<'hgv' | 'trl' | 'lgv'>; + const amended = this.amendedTechRecord() as TechRecordType<'hgv' | 'trl' | 'lgv'>; + if (!current || !amended) return true; + return current[property] !== amended[property]; + } +} diff --git a/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.html b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.html new file mode 100644 index 0000000000..731c504a30 --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.html @@ -0,0 +1,259 @@ + + +
+
+
Carries dangerous goods
+
{{ vm.techRecord_adrDetails_dangerousGoods | defaultNullOrEmpty }}
+
+
+ + +

Applicant Details

+
+
+
Name
+
{{ vm.techRecord_adrDetails_applicantDetails_name | defaultNullOrEmpty }}
+
+
+
Street
+
{{ vm.techRecord_adrDetails_applicantDetails_street | defaultNullOrEmpty }}
+
+
+
Town
+
{{ vm.techRecord_adrDetails_applicantDetails_town | defaultNullOrEmpty }}
+
+
+
City
+
{{ vm.techRecord_adrDetails_applicantDetails_city | defaultNullOrEmpty }}
+
+
+
Postcode
+
{{ vm.techRecord_adrDetails_applicantDetails_postcode | defaultNullOrEmpty }}
+
+
+ + +

ADR Details

+
+
+
ADR body type
+
{{ vm.techRecord_adrDetails_vehicleDetails_type | defaultNullOrEmpty }}
+
+
+
Vehicle used on international journeys
+
{{ vm.techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys | defaultNullOrEmpty }}
+
+
+
Date processed
+
{{ vm.techRecord_adrDetails_vehicleDetails_approvalDate | defaultNullOrEmpty }}
+
+
+
Permitted dangerous goods
+
{{ vm.techRecord_adrDetails_permittedDangerousGoods | defaultNullOrEmpty }}
+
+
+
Compatibility Group J
+
{{ vm.techRecord_adrDetails_compatibilityGroupJ | defaultNullOrEmpty }}
+
+
+
Guidance notes
+
{{ vm.techRecord_adrDetails_additionalNotes_number | defaultNullOrEmpty }}
+
+
+
ADR type approval number
+
{{ vm.techRecord_adrDetails_adrTypeApprovalNo | defaultNullOrEmpty }}
+
+
+ + + +

Tank Details

+
+
+
Tank Make
+
{{ vm.techRecord_adrDetails_tank_tankDetails_tankManufacturer | defaultNullOrEmpty }}
+
+
+
Tank Year of manufacture
+
{{ vm.techRecord_adrDetails_tank_tankDetails_yearOfManufacture | defaultNullOrEmpty }}
+
+
+
Manufacturer serial number
+
{{ vm.techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo | defaultNullOrEmpty }}
+
+
+
Tank type approval number
+
{{ vm.techRecord_adrDetails_tank_tankDetails_tankTypeAppNo | defaultNullOrEmpty }}
+
+
+
Code
+
{{ vm.techRecord_adrDetails_tank_tankDetails_tankCode | defaultNullOrEmpty }}
+
+
+
Substances permitted
+
+ {{ vm.techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted | defaultNullOrEmpty }} +
+
+
+
Reference number
+
{{ vm.techRecord_adrDetails_tank_tankDetails_tankStatement_statement | defaultNullOrEmpty }}
+
+
+
Reference number
+
+ {{ vm.techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo | defaultNullOrEmpty }} +
+
+
+
Additional Details
+
{{ vm.techRecord_adrDetails_tank_tankDetails_tankStatement_productList | defaultNullOrEmpty }}
+
+
+
Special provisions
+
{{ vm.techRecord_adrDetails_tank_tankDetails_specialProvisions | defaultNullOrEmpty }}
+
+
+ + +

Tank Inspections

+ + +
+

Initial

+
+
Certificate number
+
+ {{ vm.techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo | defaultNullOrEmpty }} +
+
+
+
Expiry date
+
+ {{ vm.techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate | defaultNullOrEmpty }} +
+
+
+ + +
+ +

Subsequent {{ i + 1 }}

+
+
Inspection type
+
{{ inspection.tc3Type | defaultNullOrEmpty }}
+
+
+
Certificate number
+
{{ inspection.tc3PeriodicNumber | defaultNullOrEmpty }}
+
+
+
Expiry date
+
{{ inspection.tc3PeriodicExpiryDate | defaultNullOrEmpty }}
+
+
+
+ + +
+
+
Memo 07/09 (3 month extension) can be applied
+
{{ vm.techRecord_adrDetails_memosApply | defaultNullOrEmpty }}
+
+
+
M145
+
{{ vm.techRecord_adrDetails_m145Statement | defaultNullOrEmpty }}
+
+
+
+ + + +

Battery List Applicable

+
+
+
Battery List Applicable
+
{{ vm.techRecord_adrDetails_listStatementApplicable | defaultNullOrEmpty }}
+
+
+
Reference number
+
{{ vm.techRecord_adrDetails_batteryListNumber | defaultNullOrEmpty }}
+
+
+
+ + +
+

Declarations seen

+
+
+
Manufacturer brake declaration
+
{{ vm.techRecord_adrDetails_brakeDeclarationsSeen | defaultNullOrEmpty }}
+
+
+
Issuer
+
{{ vm.techRecord_adrDetails_brakeDeclarationIssuer | defaultNullOrEmpty }}
+
+
+
Brake endurance
+
{{ vm.techRecord_adrDetails_brakeEndurance | defaultNullOrEmpty }}
+
+
+
Weight
+
{{ vm.techRecord_adrDetails_weight | defaultNullOrEmpty }}
+
+
+
Owner/operator declaration
+
{{ vm.techRecord_adrDetails_declarationsSeen | defaultNullOrEmpty }}
+
+
+
+ +

New certificate requested

+
+
+
New certificate requested
+
{{ vm.techRecord_adrDetails_newCertificateRequested | defaultNullOrEmpty }}
+
+
+ +

Additional Examiner Notes History

+ + + + + + + + + + + + + + + + + +
NotesCreated ByDate
{{ note.note | defaultNullOrEmpty }}{{ note.lastUpdatedBy | defaultNullOrEmpty }}{{ note.createdAtDate | date : 'dd/MM/yyyy HH:mm' | defaultNullOrEmpty }}
+ +
+
+ +
+
+
No additional examiner notes history available
+
+
+
+ +

ADR Certificate Notes

+
+
+
ADR Certificate Notes
+
{{ vm.techRecord_adrDetails_adrCertificateNotes | defaultNullOrEmpty }}
+
+
+
+
+
diff --git a/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.scss b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts new file mode 100644 index 0000000000..9f71c851c0 --- /dev/null +++ b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts @@ -0,0 +1,16 @@ +import { Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AdrService } from '@services/adr/adr.service'; +import { techRecord } from '@store/technical-records'; + +@Component({ + selector: 'app-adr-section-view', + templateUrl: './adr-section-view.component.html', + styleUrls: ['./adr-section-view.component.scss'], +}) +export class AdrSectionViewComponent { + store = inject(Store); + adrService = inject(AdrService); + + techRecord = this.store.selectSignal(techRecord); +} diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.html b/src/app/forms/custom-sections/adr-section/adr-section.component.html index 16b3ffd895..d864ee9729 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section.component.html +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.html @@ -1,1125 +1,5 @@ -
-
-

- -

-

- Error: - {{ error }} -

-
-
- - -
-
-
- - - - - - Applicant Details - - -
-

- -

-

- Error: - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} -

- -
-
- - - - ADR Details - - -
-

- -

-

- Error: - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} -

-
-
- - -
-
-
- - -
-
-

- -

-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-

- -

-

- Error: - {{ error }} -

-
-
- - -
-
-
- - -
-
-

- -

-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-

- -

-

- Error: - {{ error }} -

- -
-
- - - - - - -
-

- -

-

- Error: - {{ error }} - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} - {{ error }} -

- -
- - -
-

- -

-

- Error: - - {{ error }} - - - {{ error }} - -

- -
- - -
-

- -

-

- Error: - {{ error }} - {{ error }} -

- -
- - -
-

- -

-

- Error: - {{ error }} - {{ error }} -

- -
- - -
-

- -

-

- Error: - - {{ error }} - -

-
-
- - -
-
-
- - - - -
-

- -

-

- Error: - - {{ error }} - -

-
-
- - -
-
-
- - - - -
-

- -

-

- Error: - - {{ error }} - - - {{ error }} - -

- -
-
- - - - -
-

- -

-

- Error: - - {{ error }} - - - {{ error }} - -

- -
- - -
-
-
-

- -

- - Remove -
-
- Add UN Number -
-
-
-
- - -
-

- -

-

- Error: - {{ error }} -

- -
- You have {{ 500 - (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 500) - 500 }} too many -
-
- - - - Tank Inspections - - -
-

- -

-

- Error: - {{ - error - }} - {{ - error - }} -

- -
- -
- - - - -
-
- -
-

- -

- -
- - -
-

- -

- -
- - - - -

- Remove -

-
-
-
- -

- Add subsequent inspection -

-
- - -
-
- -

Memo 07/09 (3 month extension) can be applied

-
-
Only applicable for vehicles used on national journeys
-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-
- -

M145

-
-

- Error: - {{ error }} -

-
-
- - -
-
-
-
-
- - - - -
-

- -

-

- Error: - {{ error }} -

-
-
- - -
-
-
- - - - -
-

- -

-

- Error: - {{ error }} -

- -
-
-
- - - Declarations seen - - -
-
-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-

- -

-

- Error: - {{ error }} -

- -
- You have {{ 500 - (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 500) - 500 }} too many -
-
- - -
-
-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-

- -

-

- Error: - {{ error }} -

-
- - -
-
- - -
-
- -

Owner/operator declaration

-
-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-
- -

New certificate required

-
-

- Error: - {{ error }} -

-
-
- - -
-
-
-
- - -
-

- -

-
Will not be present on the ADR certificate
-

- Error: - {{ error }} -

- -
- You have {{ 1024 - (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 1024) - 1024 }} too many -
-
- - - - -
-

- -

-

- Error: - {{ error }} -

- -
- You have {{ 1500 - (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 1500) - 1500 }} too many -
-
-
-
+ + + + + diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.ts b/src/app/forms/custom-sections/adr-section/adr-section.component.ts index 595b679501..d69ddffaba 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.ts @@ -1,450 +1,12 @@ -import { Component, OnDestroy, OnInit, inject } from '@angular/core'; -import { ControlContainer, FormArray, FormBuilder, FormGroup, ValidatorFn } from '@angular/forms'; -import { ADRAdditionalNotesNumber } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrAdditionalNotesNumber.enum.js'; -import { ADRBodyType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrBodyType.enum.js'; -import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum.js'; -import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum.js'; -import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankStatementSubstancePermitted.js'; -import { getOptionsFromEnum } from '@forms/utils/enum-map'; -import { TC2Types, TC3Types } from '@models/adr.enum.js'; -import { Store } from '@ngrx/store'; -import { selectTechRecord } from '@store/technical-records'; -import { ReplaySubject, takeUntil } from 'rxjs'; +import { Component, input } from '@angular/core'; @Component({ selector: 'app-adr-section', templateUrl: './adr-section.component.html', styleUrls: ['./adr-section.component.scss'], }) -export class AdrSectionComponent implements OnInit, OnDestroy { - fb = inject(FormBuilder); - store = inject(Store); - controlContainer = inject(ControlContainer); - - destroy$ = new ReplaySubject(1); - - form = this.fb.group( - { - techRecord_adrDetails_dangerousGoods: this.fb.control(false), - - // Applicant Details - techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), - techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [this.maxLength(150, 'Street')]), - techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), - techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), - techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [this.maxLength(25, 'Postcode')]), - - // ADR Details - techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [this.requiredWithDangerousGoods('ADR body type')]), - techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null, [ - this.requiredWithDangerousGoods('Used on international journeys'), - ]), - techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null), - techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ - this.requiredWithDangerousGoods('Permitted dangerous goods'), - ]), - techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [this.requiredWithExplosives('Compatibility Group J')]), - techRecord_adrDetails_additionalNotes_number: this.fb.control([], [this.requiredWithDangerousGoods('Guidance notes')]), - techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [this.maxLength(40, 'ADR type approval number')]), - - // Tank Details - techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank Make'), - this.maxLength(70, 'Tank Make'), - ]), - techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank Year of manufacture'), - this.maxLength(70, 'Tank Year of manufacture'), - ]), - techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('Manufacturer serial number'), - this.maxLength(70, 'Manufacturer serial number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank type approval number'), - this.maxLength(65, 'Tank type approval number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ - this.requiredWithTankOrBattery('Code'), - this.maxLength(30, 'Code'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ - this.requiredWithTankOrBattery('Substances permitted'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control(null, this.requiredWithTankStatement('Select')), - techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ - this.requiredWithUNNumber('Reference number'), - this.maxLength(1500, 'Reference number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: this.fb.control(null, [ - this.maxLength(1500, 'Reference number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array([ - this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, []), - techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [this.maxLength(1500, 'Special provisions')]), - - // Tank Details > Tank Inspections - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('TC2: Certificate Number'), - this.maxLength(70, 'TC2: Certificate Number'), - ]), - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control(null, [ - this.requiredWithTankOrBattery('TC2: Expiry Date'), - ]), - techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), - - // Miscellaneous - techRecord_adrDetails_memosApply: this.fb.control(null), - techRecord_adrDetails_m145Statement: this.fb.control(null), - - // Battery List - techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [this.requiredWithBattery('Battery list applicable')]), - techRecord_adrDetails_batteryListNumber: this.fb.control(null, [this.requiredWithBatteryListApplicable('Reference Number')]), - - // Brake declaration - techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), - techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null, [this.maxLength(500, 'Issuer')]), - techRecord_adrDetails_brakeEndurance: this.fb.control(false), - techRecord_adrDetails_weight: this.fb.control(null, [ - this.max(99999999, 'Weight (tonnes)'), - this.requiredWithBrakeEndurance('Weight (tonnes)'), - this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), - ]), - - // Other declarations - techRecord_adrDetails_declarationsSeen: this.fb.control(false), - - // Miscellaneous - techRecord_adrDetails_newCertificateRequested: this.fb.control(false), - techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), - techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), - techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [this.maxLength(1500, 'ADR Certificate Notes')]), - }, - { validators: [this.requiresReferenceNumberOrUNNumber()] } - ); - - // Option lists - dangerousGoodsOptions = [ - { label: 'Yes', value: true }, - { label: 'No', value: false }, - ]; - - adrBodyTypesOptions = getOptionsFromEnum(ADRBodyType); - - usedOnInternationJourneysOptions = [ - { value: 'yes', label: 'Yes' }, - { value: 'no', label: 'No' }, - { value: 'n/a', label: 'Not applicable' }, - ]; - - permittedDangerousGoodsOptions = getOptionsFromEnum(ADRDangerousGood); - - guidanceNotesOptions = getOptionsFromEnum(ADRAdditionalNotesNumber); - - compatibilityGroupJOptions = [ - { label: 'Yes', value: true }, - { label: 'No', value: false }, - ]; - - tankStatementSubstancePermittedOptions = getOptionsFromEnum(ADRTankStatementSubstancePermitted); - - tankStatementSelectOptions = getOptionsFromEnum(ADRTankDetailsTankStatementSelect); - - batteryListApplicableOptions = [ - { value: true, label: 'Yes' }, - { value: false, label: 'No' }, - ]; - - tc3InspectionOptions = getOptionsFromEnum(TC3Types); - - isInvalid(formControlName: string) { - const control = this.form.get(formControlName); - return control?.invalid && control?.touched; - } - - toggle(formControlName: string, value: string) { - const control = this.form.get(formControlName); - if (!control) return; - - // If this is the first checkbox, set the value to an array - if (control.value === null) { - return control.setValue([value]); - } - - // If the value is already an array, toggle the value - if the array is then empty, set the value to null - if (Array.isArray(control.value)) { - control.value.includes(value) ? control.value.splice(control.value.indexOf(value), 1) : control.value.push(value); - if (control.value.length === 0) { - control.setValue(null); - } - } - } - - containsExplosives(arr: string[]) { - return arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_2) || arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_3); - } - - containsTankOrBattery(bodyType: string) { - return bodyType.toLowerCase().includes('tank') || bodyType.toLowerCase().includes('battery'); - } - - canDisplayDangerousGoodsSection(form: FormGroup | FormArray = this.form) { - const dangerousGoods = form.get('techRecord_adrDetails_dangerousGoods')?.value; - return dangerousGoods === true; - } - - canDisplayCompatibilityGroupJSection(form: FormGroup | FormArray = this.form) { - const permittedDangerousGoods = form.get('techRecord_adrDetails_permittedDangerousGoods')?.value; - const containsExplosives = Array.isArray(permittedDangerousGoods) && this.containsExplosives(permittedDangerousGoods); - return this.canDisplayDangerousGoodsSection(form) && containsExplosives; - } - - canDisplayBatterySection(form: FormGroup | FormArray = this.form) { - const adrBodyType = form.get('techRecord_adrDetails_vehicleDetails_type')?.value; - return typeof adrBodyType === 'string' && adrBodyType.toLowerCase().includes('battery'); - } - - canDisplayTankOrBatterySection(form: FormGroup | FormArray = this.form) { - const adrBodyType = form.get('techRecord_adrDetails_vehicleDetails_type')?.value; - const containsTankOrBattery = typeof adrBodyType === 'string' && this.containsTankOrBattery(adrBodyType); - return this.canDisplayDangerousGoodsSection(form) && containsTankOrBattery; - } - - canDisplayTankStatementSelectSection(form: FormGroup | FormArray = this.form) { - const tankStatementSubstancesPermitted = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted')?.value; - const underUNNumber = tankStatementSubstancesPermitted === ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER; - return this.canDisplayTankOrBatterySection(form) && underUNNumber; - } - - canDisplayTankStatementStatementSection(form: FormGroup | FormArray = this.form) { - const tankStatementSelect = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_select')?.value; - const underStatement = tankStatementSelect === ADRTankDetailsTankStatementSelect.STATEMENT; - return this.canDisplayTankStatementSelectSection(form) && underStatement; - } - - canDisplayTankStatementProductListSection(form: FormGroup | FormArray = this.form) { - const tankStatementSelect = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_select')?.value; - const underProductList = tankStatementSelect === ADRTankDetailsTankStatementSelect.PRODUCT_LIST; - return this.canDisplayTankStatementSelectSection(form) && underProductList; - } - - canDisplayWeightSection(form: FormGroup | FormArray = this.form) { - const brakeEndurance = form.get('techRecord_adrDetails_brakeEndurance')?.value; - return brakeEndurance === true; - } - - canDisplayBatteryListNumberSection(form: FormGroup | FormArray = this.form) { - const batteryListApplicable = form.get('techRecord_adrDetails_listStatementApplicable')?.value; - return this.canDisplayBatterySection(form) && batteryListApplicable === true; - } - - canDisplayIssueSection() { - const brakeDeclarationsSeen = this.form.get('techRecord_adrDetails_brakeDeclarationsSeen')?.value; - return brakeDeclarationsSeen === true; - } - - ngOnInit(): void { - // Attatch all form controls to parent - const parent = this.controlContainer.control; - if (parent instanceof FormGroup) { - Object.entries(this.form.controls).forEach(([key, control]) => parent.addControl(key, control)); - } - - // Listen for tech record changes, and update the form accordingly - this.store - .select(selectTechRecord) - .pipe(takeUntil(this.destroy$)) - .subscribe((techRecord) => { - if (techRecord) this.form.patchValue(techRecord as any); - }); - } - - ngOnDestroy(): void { - // Detatch all form controls from parent - const parent = this.controlContainer.control; - if (parent instanceof FormGroup) { - Object.keys(this.form.controls).forEach((key) => parent.removeControl(key)); - } - - // Clear subscriptions - this.destroy$.next(true); - this.destroy$.complete(); - } - - // Custom validators - requiredWithDangerousGoods(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayDangerousGoodsSection(control.parent)) { - return { required: `${label} is required when dangerous goods are present` }; - } - - return null; - }; - } - - requiredWithExplosives(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayCompatibilityGroupJSection(control.parent)) { - return { required: `${label} is required when Explosives Type 2 or Explosive Type 3` }; - } - - return null; - }; - } - - requiredWithBattery(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayBatterySection(control.parent)) { - return { required: `${label} is required when ADR body type is of type 'battery'` }; - } - - return null; - }; - } - - requiredWithTankOrBattery(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayTankOrBatterySection(control.parent)) { - return { required: `${label} is required when ADR body type is of type 'tank' or 'battery'` }; - } - - return null; - }; - } - - requiredWithTankStatement(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayTankStatementSelectSection(control.parent)) { - return { required: `${label} is required with substances permitted` }; - } - - return null; - }; - } - - requiredWithUNNumber(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayTankStatementStatementSection(control.parent)) { - return { required: `${label} is required when under UN number` }; - } - - return null; - }; - } - - requiredWithBrakeEndurance(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayWeightSection(control.parent)) { - return { required: `${label} is required when brake endurance is checked` }; - } - - return null; - }; - } - - requiredWithBatteryListApplicable(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.canDisplayBatteryListNumberSection(control.parent)) { - return { required: `${label} is required when battery list is applicable` }; - } - - return null; - }; - } - - requiredWithTankStatementProductList(label: string): ValidatorFn { - return (control) => { - const visible = control.parent && this.canDisplayTankStatementProductListSection(control.parent); - if (visible && !control.value) { - return { required: `${label} is required when under product list` }; - } - - return null; - }; - } - - requiresReferenceNumberOrUNNumber(): ValidatorFn { - return (control) => { - const referenceNumber = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_statement')?.value; - const unNumbers = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo')?.value; - const visible = control.parent && this.canDisplayTankStatementProductListSection(control.parent); - const unNumberPopulated = Array.isArray(unNumbers) && unNumbers.some((un) => un !== null); - - if (visible && (!referenceNumber || !unNumberPopulated)) { - return { required: 'Either reference number or UN number is required' }; - } - - return null; - }; - } - - max(size: number, label: string): ValidatorFn { - return (control) => { - if (control.value && control.value > size) { - return { max: `${label} must be less than or equal to ${size}` }; - } - - return null; - }; - } - - maxLength(length: number, label: string): ValidatorFn { - return (control) => { - if (control.value && control.value.length > length) { - return { maxLength: `${label} must be less than or equal to ${length} characters` }; - } - - return null; - }; - } - - pattern(pattern: string | RegExp, label: string): ValidatorFn { - return (control) => { - if (control.value && !new RegExp(pattern).test(control.value)) { - return { pattern: `${label} is invalid` }; - } - - return null; - }; - } - - // Dynamically add/remove controls - addTC3TankInspection() { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); - if (formArray instanceof FormArray) { - formArray.push( - this.fb.group({ - tc3Type: this.fb.control(null), - tc3PeriodicNumber: this.fb.control(null), - tc3PeriodicExpiryDate: this.fb.control(null), - }) - ); - } - } - - removeTC3TankInspection(index: number) { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); - if (formArray instanceof FormArray) { - formArray.removeAt(index); - } - } - - addUNNumber() { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); - if (formArray instanceof FormArray) { - formArray.push( - this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]) - ); - } - } - - removeUNNumber(index: number) { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); - if (formArray instanceof FormArray) { - formArray.removeAt(index); - } - } +export class AdrSectionComponent { + mode = input('edit'); } + +type Mode = 'view' | 'edit' | 'summary'; diff --git a/src/app/forms/dynamic-forms.module.ts b/src/app/forms/dynamic-forms.module.ts index 4baa7dc690..a1a1d44adf 100644 --- a/src/app/forms/dynamic-forms.module.ts +++ b/src/app/forms/dynamic-forms.module.ts @@ -41,6 +41,9 @@ import { ViewListItemComponent } from './components/view-list-item/view-list-ite import { AbandonDialogComponent } from './custom-sections/abandon-dialog/abandon-dialog.component'; import { AdrExaminerNotesHistoryViewComponent } from './custom-sections/adr-examiner-notes-history-view/adr-examiner-notes-history-view.component'; import { AdrNewCertificateRequiredViewComponent } from './custom-sections/adr-new-certificate-required-view/adr-new-certificate-required-view.component'; +import { AdrSectionEditComponent } from './custom-sections/adr-section/adr-section-edit/adr-section-edit.component'; +import { AdrSectionSummaryComponent } from './custom-sections/adr-section/adr-section-summary/adr-section-summary.component'; +import { AdrSectionViewComponent } from './custom-sections/adr-section/adr-section-view/adr-section-view.component'; import { AdrSectionComponent } from './custom-sections/adr-section/adr-section.component'; import { AdrTankDetailsInitialInspectionViewComponent } from './custom-sections/adr-tank-details-initial-inspection-view/adr-tank-details-initial-inspection-view.component'; import { AdrTankDetailsM145ViewComponent } from './custom-sections/adr-tank-details-m145-view/adr-tank-details-m145-view.component'; @@ -131,6 +134,9 @@ import { WeightsComponent } from './custom-sections/weights/weights.component'; ContingencyAdrGenerateCertComponent, AdrNewCertificateRequiredViewComponent, AdrSectionComponent, + AdrSectionEditComponent, + AdrSectionViewComponent, + AdrSectionSummaryComponent, ], imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], exports: [ @@ -182,6 +188,9 @@ import { WeightsComponent } from './custom-sections/weights/weights.component'; AdrCertificateHistoryComponent, FieldWarningMessageComponent, AdrSectionComponent, + AdrSectionEditComponent, + AdrSectionViewComponent, + AdrSectionSummaryComponent, ], }) export class DynamicFormsModule {} diff --git a/src/app/services/adr/adr.service.ts b/src/app/services/adr/adr.service.ts index a1d6e33057..c30a680725 100644 --- a/src/app/services/adr/adr.service.ts +++ b/src/app/services/adr/adr.service.ts @@ -1,38 +1,102 @@ import { Injectable } from '@angular/core'; +import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum.js'; import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum.js'; +import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankStatementSubstancePermitted'; import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class AdrService { - determineTankStatementSelect(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const { - techRecord_adrDetails_tank_tankDetails_tankStatement_statement: statement, - techRecord_adrDetails_tank_tankDetails_tankStatement_productList: productList, - techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: productListUnNo, - techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: productListRefNo, - } = techRecord; - - if (statement) return ADRTankDetailsTankStatementSelect.STATEMENT; - if (productList || productListRefNo || (productListUnNo && productListUnNo.length > 0)) - return ADRTankDetailsTankStatementSelect.PRODUCT_LIST; - - return null; - } - - carriesDangerousGoods(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - return ( - techRecord.techRecord_adrDetails_dangerousGoods || - (techRecord.techRecord_adrDetails_dangerousGoods !== false && - Boolean( - Object.keys(techRecord).find( - (key) => - key !== 'techRecord_adrDetails_dangerousGoods' && - key.includes('adrDetails') && - techRecord[key as keyof TechRecordType<'hgv' | 'lgv' | 'trl'>] != null - ) - )) - ); - } + determineTankStatementSelect(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const { + techRecord_adrDetails_tank_tankDetails_tankStatement_statement: statement, + techRecord_adrDetails_tank_tankDetails_tankStatement_productList: productList, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: productListUnNo, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: productListRefNo, + } = techRecord; + + if (statement) return ADRTankDetailsTankStatementSelect.STATEMENT; + if (productList || productListRefNo || (productListUnNo && productListUnNo.length > 0)) return ADRTankDetailsTankStatementSelect.PRODUCT_LIST; + + return null; + } + + carriesDangerousGoods(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + return ( + techRecord.techRecord_adrDetails_dangerousGoods || + (techRecord.techRecord_adrDetails_dangerousGoods !== false && + Boolean( + Object.keys(techRecord).find( + (key) => + key !== 'techRecord_adrDetails_dangerousGoods' && + key.includes('adrDetails') && + techRecord[key as keyof TechRecordType<'hgv' | 'lgv' | 'trl'>] != null + ) + )) + ); + } + + containsExplosives(arr: string[]) { + return arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_2) || arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_3); + } + + containsTankOrBattery(bodyType: string) { + return bodyType.toLowerCase().includes('tank') || bodyType.toLowerCase().includes('battery'); + } + + canDisplayDangerousGoodsSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const dangerousGoods = techRecord.techRecord_adrDetails_dangerousGoods; + return dangerousGoods === true; + } + + canDisplayCompatibilityGroupJSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const permittedDangerousGoods = techRecord.techRecord_adrDetails_permittedDangerousGoods; + const containsExplosives = Array.isArray(permittedDangerousGoods) && this.containsExplosives(permittedDangerousGoods); + return this.canDisplayDangerousGoodsSection(techRecord) && containsExplosives; + } + + canDisplayBatterySection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const adrBodyType = techRecord.techRecord_adrDetails_vehicleDetails_type; + return typeof adrBodyType === 'string' && adrBodyType.toLowerCase().includes('battery'); + } + + canDisplayTankOrBatterySection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const adrBodyType = techRecord.techRecord_adrDetails_vehicleDetails_type; + const containsTankOrBattery = typeof adrBodyType === 'string' && this.containsTankOrBattery(adrBodyType); + return this.canDisplayDangerousGoodsSection(techRecord) && containsTankOrBattery; + } + + canDisplayTankStatementSelectSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const tankStatementSubstancesPermitted = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted; + const underUNNumber = tankStatementSubstancesPermitted === ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER; + return this.canDisplayTankOrBatterySection(techRecord) && underUNNumber; + } + + canDisplayTankStatementStatementSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const tankStatementSelect = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_select; + const underStatement = tankStatementSelect === ADRTankDetailsTankStatementSelect.STATEMENT; + return this.canDisplayTankStatementSelectSection(techRecord) && underStatement; + } + + canDisplayTankStatementProductListSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const tankStatementSelect = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_select; + const underProductList = tankStatementSelect === ADRTankDetailsTankStatementSelect.PRODUCT_LIST; + return this.canDisplayTankStatementSelectSection(techRecord) && underProductList; + } + + canDisplayWeightSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const brakeEndurance = techRecord.techRecord_adrDetails_brakeEndurance; + return brakeEndurance === true; + } + + canDisplayBatteryListNumberSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const batteryListApplicable = techRecord.techRecord_adrDetails_listStatementApplicable; + return this.canDisplayBatterySection(techRecord) && batteryListApplicable === true; + } + + canDisplayIssueSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const brakeDeclarationsSeen = techRecord.techRecord_adrDetails_brakeDeclarationsSeen; + return brakeDeclarationsSeen === true; + } } From f28a586f8eca024a8ef662fcf783e281d17aecbc Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:04:54 +0100 Subject: [PATCH 004/211] chore(cb2-0000): add example directive --- .../govuk-input/govuk-input.directive.ts | 42 +++++++ .../adr-section-edit.component.html | 9 +- src/app/shared/shared.module.ts | 113 +++++++++--------- 3 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 src/app/directives/govuk-input/govuk-input.directive.ts diff --git a/src/app/directives/govuk-input/govuk-input.directive.ts b/src/app/directives/govuk-input/govuk-input.directive.ts new file mode 100644 index 0000000000..d5fc902a03 --- /dev/null +++ b/src/app/directives/govuk-input/govuk-input.directive.ts @@ -0,0 +1,42 @@ +import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core'; +import { ControlContainer } from '@angular/forms'; +import { ReplaySubject, takeUntil } from 'rxjs'; + +@Directive({ + selector: '[govukInput]', +}) +export class GovukInputDirective implements OnInit, OnDestroy { + elementRef = inject>(ElementRef); + controlContainer = inject(ControlContainer); + + formControlName = input.required(); + + destroy$ = new ReplaySubject(1); + + ngOnInit(): void { + const formControlName = this.formControlName(); + const control = this.controlContainer.control?.get(formControlName); + if (control) { + this.elementRef.nativeElement.setAttribute('id', formControlName); + this.elementRef.nativeElement.setAttribute('name', formControlName); + this.elementRef.nativeElement.classList.add('govuk-input'); + + control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { + if (statusChange === 'INVALID' && control.touched) { + this.elementRef.nativeElement.classList.add('govuk-input--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); + } + + if (statusChange === 'VALID') { + this.elementRef.nativeElement.classList.remove('govuk-input--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', ''); + } + }); + } + } + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } +} diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html index df56a08e3d..e37d79af7e 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html @@ -42,14 +42,7 @@

Error: {{ error }}

- +

diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index f3bb9ca17f..7a082b6c49 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { RoleRequiredDirective } from '@directives/app-role-required/app-role-required.directive'; import { FeatureToggleDirective } from '@directives/feature-toggle/feature-toggle.directive'; +import { GovukInputDirective } from '@directives/govuk-input/govuk-input.directive'; import { PreventDoubleClickDirective } from '@directives/prevent-double-click/prevent-double-click.directive'; import { RetrieveDocumentDirective } from '@directives/retrieve-document/retrieve-document.directive'; import { AccordionControlComponent } from '../components/accordion-control/accordion-control.component'; @@ -28,60 +29,62 @@ import { TestTypeNamePipe } from '../pipes/test-type-name/test-type-name.pipe'; import { TyreAxleLoadPipe } from '../pipes/tyre-axle-load/tyre-axle-load.pipe'; @NgModule({ - declarations: [ - DefaultNullOrEmpty, - ButtonGroupComponent, - ButtonComponent, - BannerComponent, - RoleRequiredDirective, - FeatureToggleDirective, - FeatureToggleDirective, - TagComponent, - NumberPlateComponent, - IconComponent, - TestTypeNamePipe, - AccordionComponent, - AccordionControlComponent, - PaginationComponent, - TestCertificateComponent, - PreventDoubleClickDirective, - BaseDialogComponent, - DigitGroupSeparatorPipe, - RefDataDecodePipe, - RetrieveDocumentDirective, - InputSpinnerComponent, - RouterOutletComponent, - TyreAxleLoadPipe, - GetControlLabelPipe, - FormatVehicleTypePipe, - CollapsibleTextComponent, - ], - imports: [CommonModule, RouterModule], - exports: [ - DefaultNullOrEmpty, - ButtonGroupComponent, - ButtonComponent, - BannerComponent, - RoleRequiredDirective, - FeatureToggleDirective, - TagComponent, - NumberPlateComponent, - IconComponent, - TestTypeNamePipe, - AccordionComponent, - AccordionControlComponent, - PaginationComponent, - TestCertificateComponent, - BaseDialogComponent, - DigitGroupSeparatorPipe, - RefDataDecodePipe, - RetrieveDocumentDirective, - InputSpinnerComponent, - RouterOutletComponent, - TyreAxleLoadPipe, - GetControlLabelPipe, - FormatVehicleTypePipe, - CollapsibleTextComponent, - ], + declarations: [ + DefaultNullOrEmpty, + ButtonGroupComponent, + ButtonComponent, + BannerComponent, + RoleRequiredDirective, + FeatureToggleDirective, + FeatureToggleDirective, + TagComponent, + NumberPlateComponent, + IconComponent, + TestTypeNamePipe, + AccordionComponent, + AccordionControlComponent, + PaginationComponent, + TestCertificateComponent, + PreventDoubleClickDirective, + BaseDialogComponent, + DigitGroupSeparatorPipe, + RefDataDecodePipe, + RetrieveDocumentDirective, + InputSpinnerComponent, + RouterOutletComponent, + TyreAxleLoadPipe, + GetControlLabelPipe, + FormatVehicleTypePipe, + CollapsibleTextComponent, + GovukInputDirective, + ], + imports: [CommonModule, RouterModule], + exports: [ + DefaultNullOrEmpty, + ButtonGroupComponent, + ButtonComponent, + BannerComponent, + RoleRequiredDirective, + FeatureToggleDirective, + TagComponent, + NumberPlateComponent, + IconComponent, + TestTypeNamePipe, + AccordionComponent, + AccordionControlComponent, + PaginationComponent, + TestCertificateComponent, + BaseDialogComponent, + DigitGroupSeparatorPipe, + RefDataDecodePipe, + RetrieveDocumentDirective, + InputSpinnerComponent, + RouterOutletComponent, + TyreAxleLoadPipe, + GetControlLabelPipe, + FormatVehicleTypePipe, + CollapsibleTextComponent, + GovukInputDirective, + ], }) export class SharedModule {} From 153e6716de0775e7ba4bdbc47ac10a0ce1b83b0c Mon Sep 17 00:00:00 2001 From: Brandon Thomas-Davies <87308252+BrandonT95@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:11:23 +0100 Subject: [PATCH 005/211] chore(cb2-0000): initial implementation of vehicle summary section --- .../govuk-input/govuk-input.directive.ts | 9 ++ .../tech-record-summary.component.html | 18 +++ .../vehicle-section-edit.component.html | 39 +++++ .../vehicle-section-edit.component.scss | 11 ++ .../vehicle-section-edit.component.ts | 149 ++++++++++++++++++ .../vehicle-section-summary.component.html | 1 + .../vehicle-section-summary.component.scss | 0 .../vehicle-section-summary.component.ts | 14 ++ .../vehicle-section-view.component.html | 3 + .../vehicle-section-view.component.scss | 0 .../vehicle-section-view.component.ts | 14 ++ .../vehicle-section.component.html | 5 + .../vehicle-section.component.scss | 0 .../vehicle-section.component.ts | 12 ++ src/app/forms/dynamic-forms.module.ts | 19 ++- 15 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.scss create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.html create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.scss create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.html create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.scss create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section.component.html create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section.component.scss create mode 100644 src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts diff --git a/src/app/directives/govuk-input/govuk-input.directive.ts b/src/app/directives/govuk-input/govuk-input.directive.ts index d5fc902a03..ec9e189a07 100644 --- a/src/app/directives/govuk-input/govuk-input.directive.ts +++ b/src/app/directives/govuk-input/govuk-input.directive.ts @@ -1,6 +1,7 @@ import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core'; import { ControlContainer } from '@angular/forms'; import { ReplaySubject, takeUntil } from 'rxjs'; +import { FormNodeWidth } from '@services/dynamic-forms/dynamic-form.types'; @Directive({ selector: '[govukInput]', @@ -10,6 +11,7 @@ export class GovukInputDirective implements OnInit, OnDestroy { controlContainer = inject(ControlContainer); formControlName = input.required(); + width = input(); destroy$ = new ReplaySubject(1); @@ -19,6 +21,9 @@ export class GovukInputDirective implements OnInit, OnDestroy { if (control) { this.elementRef.nativeElement.setAttribute('id', formControlName); this.elementRef.nativeElement.setAttribute('name', formControlName); + if (this.width()) { + this.elementRef.nativeElement.classList.add(`govuk-input--width-${this.width()}`); + } this.elementRef.nativeElement.classList.add('govuk-input'); control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { @@ -39,4 +44,8 @@ export class GovukInputDirective implements OnInit, OnDestroy { this.destroy$.next(true); this.destroy$.complete(); } + + get style(): string { + return `govuk-input ${this.width() ? `govuk-input--width-${this.width()}` : ''}`; + } } diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html index e4bb51c236..9eddcc53d7 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.html @@ -22,6 +22,24 @@
+ + + + + + + + + + + + + +
+

+ +

+ +
+ + + +
+

+ +

+

+ Error: + {{ error }} + {{ error }} +

+ +
+ +
+

+ +

+ +
+
diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.scss b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.scss new file mode 100644 index 0000000000..d22c223277 --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.scss @@ -0,0 +1,11 @@ +.flex { + display: flex; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.gap-2 { + gap: 0.5rem; +} diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts new file mode 100644 index 0000000000..e82394b30c --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts @@ -0,0 +1,149 @@ +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { + ControlContainer, + FormBuilder, + FormControl, + FormGroup, + ValidatorFn, +} from '@angular/forms'; +import { VehicleTypes } from '@models/vehicle-tech-record.model'; +import { FuelPropulsionSystem } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete'; +import { VehicleConfiguration } from '@models/vehicle-configuration.enum'; +import { techRecord } from '@store/technical-records'; +import { ReplaySubject, take } from 'rxjs'; +import { FormNodeWidth, TagTypeLabels } from '@services/dynamic-forms/dynamic-form.types'; +import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; +import { TagType } from '@components/tag/tag.component'; + +type VehicleSectionForm = Partial, FormControl>>; + +@Component({ + selector: 'app-vehicle-section-edit', + templateUrl: './vehicle-section-edit.component.html', + styleUrls: ['./vehicle-section-edit.component.scss'], +}) + +export class VehicleSectionEditComponent implements OnInit, OnDestroy { + protected readonly FormNodeWidth = FormNodeWidth; + protected readonly TagType = TagType; + protected readonly TagTypeLabels = TagTypeLabels; + fb = inject(FormBuilder); + store = inject(Store); + controlContainer = inject(ControlContainer); + + destroy$ = new ReplaySubject(1); + + form = this.fb.group({ + // values from hgv-tech-record template file + techRecord_alterationMarker: this.fb.control(null), + techRecord_departmentalVehicleMarker: this.fb.control(null), + techRecord_drawbarCouplingFitted: this.fb.control(null), + techRecord_emissionsLimit: this.fb.control(null), + techRecord_euVehicleCategory: this.fb.control(null), + techRecord_euroStandard: this.fb.control(null), + techRecord_fuelPropulsionSystem: this.fb.control(null), + techRecord_functionCode: this.fb.control(null), + techRecord_manufactureYear: this.fb.control(null, [this.min(1000, 'Year of manufacture'), this.pastYear('Year of manufacture')]), + techRecord_noOfAxles: this.fb.control({ value: null, disabled: true }), + techRecord_offRoad: this.fb.control(null), + techRecord_regnDate: this.fb.control(null), + techRecord_roadFriendly: this.fb.control(null), + techRecord_speedLimiterMrk: this.fb.control(null), + techRecord_statusCode: this.fb.control(null), + techRecord_tachoExemptMrk: this.fb.control(null), + techRecord_vehicleClass_description: this.fb.control(null), + techRecord_vehicleConfiguration: this.fb.control(null), + techRecord_vehicleType: this.fb.control({ value: null, disabled: true }), + }, { validators: []}); + + + ngOnInit(): void { + // Attach all form controls to parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + for (const [key, control] of Object.entries(this.form.controls)) { + parent.addControl(key, control); + } + } + + this.store + .select(techRecord) + .pipe(take(1)) + .subscribe((techRecord) => { + if (techRecord) this.form.patchValue(techRecord as any); + }); + } + + ngOnDestroy(): void { + // Detach all form controls from parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + for (const key of Object.keys(this.form.controls)) { + parent.removeControl(key); + } + } + + // Clear subscriptions + this.destroy$.next(true); + this.destroy$.complete(); + } + + // Potential to have a getter for each vehicle type as some vehicle types + // have different fields to others, this would allow for a more dynamic form + // could have an overall getter that sets the form with controls that belong + // to all vehicle types and then a getter for each vehicle type that adds the + // specific controls that only belong to that vehicle type? + // get hgvControls(): Record { + // return { + // techRecord_alterationMarker: this.fb.control(null), + // techRecord_departmentalVehicleMarker: this.fb.control(null), + // techRecord_drawbarCouplingFitted: this.fb.control(null), + // techRecord_emissionsLimit: this.fb.control(null), + // techRecord_euVehicleCategory: this.fb.control(null), + // techRecord_euroStandard: this.fb.control(null), + // techRecord_fuelPropulsionSystem: this.fb.control(null), + // techRecord_functionCode: this.fb.control(null), + // techRecord_manufactureYear: this.fb.control(null), + // techRecord_noOfAxles: this.fb.control(null), + // techRecord_numberOfWheelsDriven: this.fb.control(null), + // techRecord_offRoad: this.fb.control(null), + // techRecord_regnDate: this.fb.control(null), + // techRecord_roadFriendly: this.fb.control(null), + // techRecord_speedLimiterMrk: this.fb.control(null), + // techRecord_statusCode: this.fb.control(null), + // techRecord_tachoExemptMrk: this.fb.control(null), + // techRecord_vehicleClass_description: this.fb.control(null), + // techRecord_vehicleConfiguration: this.fb.control(null), + // techRecord_vehicleType: this.fb.control(null) + // } + // } + + isInvalid(formControlName: string) { + const control = this.form.get(formControlName); + return control?.invalid && control?.touched; + } + + min(size: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value < size) { + return { min: `${label} must be greater than or equal to ${size}` }; + } + + return null; + }; + } + + pastYear(label: string): ValidatorFn { + return (control) => { + if (control.value) { + const currentYear = new Date().getFullYear(); + const inputYear = control.value; + if (inputYear && inputYear > currentYear) { + return { pastYear: `${label} must be the current or a past year` }; + } + } + return null; + } + }; +} diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.html new file mode 100644 index 0000000000..1c6c8296ee --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.html @@ -0,0 +1 @@ +

vehicle summary

diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.scss b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts new file mode 100644 index 0000000000..bee913d9c0 --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts @@ -0,0 +1,14 @@ +import { Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { techRecord } from '@store/technical-records'; + +@Component({ + selector: 'app-vehicle-section-summary', + templateUrl: './vehicle-section-summary.component.html', + styleUrls: ['./vehicle-section-summary.component.scss'], +}) +export class VehicleSectionSummaryComponent { + store = inject(Store); + + techRecord = this.store.selectSignal(techRecord); +} diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.html new file mode 100644 index 0000000000..1fa04d24d2 --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.html @@ -0,0 +1,3 @@ + +

vehicle view

+
diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.scss b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts new file mode 100644 index 0000000000..10f6625b61 --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts @@ -0,0 +1,14 @@ +import { Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { techRecord } from '@store/technical-records'; + +@Component({ + selector: 'app-vehicle-section-view', + templateUrl: './vehicle-section-view.component.html', + styleUrls: ['./vehicle-section-view.component.scss'], +}) +export class VehicleSectionViewComponent { + store = inject(Store); + + techRecord = this.store.selectSignal(techRecord); +} diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.html new file mode 100644 index 0000000000..e2275004b8 --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.scss b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts new file mode 100644 index 0000000000..42c89e5c2f --- /dev/null +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-vehicle-section', + templateUrl: './vehicle-section.component.html', + styleUrls: ['./vehicle-section.component.scss'], +}) +export class VehicleSectionComponent { + mode = input('edit'); +} + +type Mode = 'view' | 'edit' | 'summary'; diff --git a/src/app/forms/dynamic-forms.module.ts b/src/app/forms/dynamic-forms.module.ts index a1a1d44adf..d81db758f6 100644 --- a/src/app/forms/dynamic-forms.module.ts +++ b/src/app/forms/dynamic-forms.module.ts @@ -15,7 +15,7 @@ import { AdrCertificateHistoryComponent } from '@forms/custom-sections/adr-certi import { AdrExaminerNotesHistoryEditComponent } from '@forms/custom-sections/adr-examiner-notes-history-edit/adr-examiner-notes-history.component-edit'; import { ApprovalTypeComponent } from '@forms/custom-sections/approval-type/approval-type.component'; import { SharedModule } from '@shared/shared.module'; -import { TruncatePipe } from '../pipes/truncate/truncate.pipe'; +import { TruncatePipe } from '@pipes/truncate/truncate.pipe'; import { AutocompleteComponent } from './components/autocomplete/autocomplete.component'; import { BaseControlComponent } from './components/base-control/base-control.component'; import { CheckboxGroupComponent } from './components/checkbox-group/checkbox-group.component'; @@ -68,6 +68,18 @@ import { RequiredStandardsComponent } from './custom-sections/required-standards import { TrlBrakesComponent } from './custom-sections/trl-brakes/trl-brakes.component'; import { TyresComponent } from './custom-sections/tyres/tyres.component'; import { WeightsComponent } from './custom-sections/weights/weights.component'; +import { + VehicleSectionComponent +} from '@forms/custom-sections/vehicle-section/vehicle-section.component'; +import { + VehicleSectionSummaryComponent +} from '@forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component'; +import { + VehicleSectionViewComponent +} from '@forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component'; +import { + VehicleSectionEditComponent +} from '@forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component'; @NgModule({ declarations: [ @@ -137,6 +149,10 @@ import { WeightsComponent } from './custom-sections/weights/weights.component'; AdrSectionEditComponent, AdrSectionViewComponent, AdrSectionSummaryComponent, + VehicleSectionComponent, + VehicleSectionSummaryComponent, + VehicleSectionViewComponent, + VehicleSectionEditComponent, ], imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], exports: [ @@ -191,6 +207,7 @@ import { WeightsComponent } from './custom-sections/weights/weights.component'; AdrSectionEditComponent, AdrSectionViewComponent, AdrSectionSummaryComponent, + VehicleSectionComponent ], }) export class DynamicFormsModule {} From 47b39516cae2fcb1190257e2a73484f197fcd09f Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Mon, 23 Sep 2024 08:32:49 +0100 Subject: [PATCH 006/211] chore(cb2-0000): add dates and directives --- .../global-error/global-error.service.ts | 31 + .../govuk-input/govuk-input.directive.ts | 56 +- .../govuk-select/govuk-select.directive.ts | 42 + .../govuk-textarea.directive.ts | 44 + .../tech-record-summary-changes.component.ts | 436 ++++----- .../tech-record-summary.component.ts | 472 +++++----- .../vehicle-technical-record.component.ts | 292 ++++--- .../character-count.component.html | 10 + .../character-count.component.ts | 13 + .../control-errors.component.html | 6 + .../control-errors.component.ts | 13 + .../date-controls.component.html | 1 + .../date-controls.component.scss | 0 .../date-controls/date-controls.component.ts | 132 +++ .../radio-group/radio-group.component.ts | 46 +- .../adr-section-edit.component.html | 827 +++++++----------- .../adr-section-edit.component.ts | 811 +++++++++-------- .../adr-section-summary.component.ts | 26 +- .../adr-section-view.component.ts | 12 +- .../adr-section/adr-section.component.ts | 8 +- src/app/forms/dynamic-forms.module.ts | 253 +++--- src/app/services/adr/adr.service.ts | 187 ++-- .../dynamic-forms/dynamic-form.service.ts | 462 +++++----- src/app/shared/shared.module.ts | 120 +-- 24 files changed, 2265 insertions(+), 2035 deletions(-) create mode 100644 src/app/directives/govuk-select/govuk-select.directive.ts create mode 100644 src/app/directives/govuk-textarea/govuk-textarea.directive.ts create mode 100644 src/app/forms/components/character-count/character-count.component.html create mode 100644 src/app/forms/components/character-count/character-count.component.ts create mode 100644 src/app/forms/components/control-errors/control-errors.component.html create mode 100644 src/app/forms/components/control-errors/control-errors.component.ts create mode 100644 src/app/forms/components/date-controls/date-controls.component.html create mode 100644 src/app/forms/components/date-controls/date-controls.component.scss create mode 100644 src/app/forms/components/date-controls/date-controls.component.ts diff --git a/src/app/core/components/global-error/global-error.service.ts b/src/app/core/components/global-error/global-error.service.ts index 5ecd2f92ac..b54608384b 100644 --- a/src/app/core/components/global-error/global-error.service.ts +++ b/src/app/core/components/global-error/global-error.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { FormArray, FormGroup, ValidationErrors } from '@angular/forms'; import { Store, select } from '@ngrx/store'; import { State } from '@store/.'; import { globalErrorState } from '@store/global-error/global-error-service.reducer'; @@ -56,4 +57,34 @@ export class GlobalErrorService { } }); } + + extractErrors(form: FormGroup | FormArray) { + const errors: ValidationErrors = {}; + Object.values(form.controls).forEach((control) => { + if (control instanceof FormGroup || control instanceof FormArray) { + this.extractErrors(control); + } else if (control.invalid && control.errors) { + Object.entries(control.errors).forEach(([key, error]) => { + errors[key] = error; + }); + } + }); + + return errors; + } + + extractGlobalErrors(form: FormGroup | FormArray) { + const errors: GlobalError[] = []; + Object.entries(form.controls).forEach(([key, control]) => { + if (control instanceof FormGroup || control instanceof FormArray) { + this.extractErrors(control); + } else if (control.invalid && control.errors) { + Object.values(control.errors).forEach((error) => { + errors.push({ error, anchorLink: key }); + }); + } + }); + + return errors; + } } diff --git a/src/app/directives/govuk-input/govuk-input.directive.ts b/src/app/directives/govuk-input/govuk-input.directive.ts index d5fc902a03..a0ed919e92 100644 --- a/src/app/directives/govuk-input/govuk-input.directive.ts +++ b/src/app/directives/govuk-input/govuk-input.directive.ts @@ -3,40 +3,40 @@ import { ControlContainer } from '@angular/forms'; import { ReplaySubject, takeUntil } from 'rxjs'; @Directive({ - selector: '[govukInput]', + selector: '[govukInput]', }) export class GovukInputDirective implements OnInit, OnDestroy { - elementRef = inject>(ElementRef); - controlContainer = inject(ControlContainer); + elementRef = inject>(ElementRef); + controlContainer = inject(ControlContainer); - formControlName = input.required(); + formControlName = input.required(); - destroy$ = new ReplaySubject(1); + destroy$ = new ReplaySubject(1); - ngOnInit(): void { - const formControlName = this.formControlName(); - const control = this.controlContainer.control?.get(formControlName); - if (control) { - this.elementRef.nativeElement.setAttribute('id', formControlName); - this.elementRef.nativeElement.setAttribute('name', formControlName); - this.elementRef.nativeElement.classList.add('govuk-input'); + ngOnInit(): void { + const formControlName = this.formControlName(); + const control = this.controlContainer.control?.get(formControlName); + if (control) { + this.elementRef.nativeElement.setAttribute('id', formControlName); + this.elementRef.nativeElement.setAttribute('name', formControlName); + this.elementRef.nativeElement.classList.add('govuk-input'); - control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { - if (statusChange === 'INVALID' && control.touched) { - this.elementRef.nativeElement.classList.add('govuk-input--error'); - this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); - } + control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { + if (statusChange === 'INVALID' && control.touched) { + this.elementRef.nativeElement.classList.add('govuk-input--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); + } - if (statusChange === 'VALID') { - this.elementRef.nativeElement.classList.remove('govuk-input--error'); - this.elementRef.nativeElement.setAttribute('aria-describedby', ''); - } - }); - } - } + if (statusChange === 'VALID') { + this.elementRef.nativeElement.classList.remove('govuk-input--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', ''); + } + }); + } + } - ngOnDestroy(): void { - this.destroy$.next(true); - this.destroy$.complete(); - } + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } } diff --git a/src/app/directives/govuk-select/govuk-select.directive.ts b/src/app/directives/govuk-select/govuk-select.directive.ts new file mode 100644 index 0000000000..c35d9d0c1d --- /dev/null +++ b/src/app/directives/govuk-select/govuk-select.directive.ts @@ -0,0 +1,42 @@ +import { Directive, ElementRef, inject, input } from '@angular/core'; +import { ControlContainer } from '@angular/forms'; +import { ReplaySubject, takeUntil } from 'rxjs'; + +@Directive({ + selector: '[govukSelect]', +}) +export class GovukSelectDirective { + elementRef = inject>(ElementRef); + controlContainer = inject(ControlContainer); + + formControlName = input.required(); + + destroy$ = new ReplaySubject(1); + + ngOnInit(): void { + const formControlName = this.formControlName(); + const control = this.controlContainer.control?.get(formControlName); + if (control) { + this.elementRef.nativeElement.setAttribute('id', formControlName); + this.elementRef.nativeElement.setAttribute('name', formControlName); + this.elementRef.nativeElement.classList.add('govuk-select'); + + control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { + if (statusChange === 'INVALID' && control.touched) { + this.elementRef.nativeElement.classList.add('govuk-select--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); + } + + if (statusChange === 'VALID') { + this.elementRef.nativeElement.classList.remove('govuk-select--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', ''); + } + }); + } + } + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } +} diff --git a/src/app/directives/govuk-textarea/govuk-textarea.directive.ts b/src/app/directives/govuk-textarea/govuk-textarea.directive.ts new file mode 100644 index 0000000000..d80ae1de4a --- /dev/null +++ b/src/app/directives/govuk-textarea/govuk-textarea.directive.ts @@ -0,0 +1,44 @@ +import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core'; +import { ControlContainer } from '@angular/forms'; +import { ReplaySubject, takeUntil } from 'rxjs'; + +@Directive({ + selector: '[govukTextarea]', +}) +export class GovukTextareaDirective implements OnInit, OnDestroy { + elementRef = inject>(ElementRef); + controlContainer = inject(ControlContainer); + + formControlName = input.required(); + + destroy$ = new ReplaySubject(1); + + ngOnInit(): void { + const formControlName = this.formControlName(); + const control = this.controlContainer.control?.get(formControlName); + if (control) { + this.elementRef.nativeElement.setAttribute('rows', '5'); + this.elementRef.nativeElement.setAttribute('id', formControlName); + this.elementRef.nativeElement.setAttribute('name', formControlName); + this.elementRef.nativeElement.classList.add('govuk-textarea'); + this.elementRef.nativeElement.classList.add('govuk-js-character-count'); + + control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { + if (statusChange === 'INVALID' && control.touched) { + this.elementRef.nativeElement.classList.add('govuk-textarea-error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); + } + + if (statusChange === 'VALID') { + this.elementRef.nativeElement.classList.remove('govuk-textarea-error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', ''); + } + }); + } + } + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } +} diff --git a/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts b/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts index 2f85131714..b9693867a3 100644 --- a/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary-changes/tech-record-summary-changes.component.ts @@ -3,11 +3,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { GlobalErrorService } from '@core/components/global-error/global-error.service'; import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb'; import { - TechRecordGETCar, - TechRecordGETHGV, - TechRecordGETLGV, - TechRecordGETPSV, - TechRecordGETTRL, + TechRecordGETCar, + TechRecordGETHGV, + TechRecordGETLGV, + TechRecordGETPSV, + TechRecordGETTRL, } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb-vehicle-type'; import { VehicleSummary } from '@forms/templates/tech-records/vehicle-summary.template'; import { vehicleTemplateMap } from '@forms/utils/tech-record-constants'; @@ -21,218 +21,228 @@ import { TechnicalRecordService } from '@services/technical-record/technical-rec import { UserService } from '@services/user-service/user-service'; import { State } from '@store/index'; import { - clearADRDetailsBeforeUpdate, - clearAllSectionStates, - clearScrollPosition, - editingTechRecord, - selectTechRecordChanges, - selectTechRecordDeletions, - techRecord, - updateADRAdditionalExaminerNotes, - updateTechRecord, - updateTechRecordSuccess, + clearADRDetailsBeforeUpdate, + clearAllSectionStates, + clearScrollPosition, + editingTechRecord, + selectTechRecordChanges, + selectTechRecordDeletions, + techRecord, + updateADRAdditionalExaminerNotes, + updateTechRecord, + updateTechRecordSuccess, } from '@store/technical-records'; import { Subject, combineLatest, map, take, takeUntil } from 'rxjs'; @Component({ - selector: 'app-tech-record-summary-changes', - templateUrl: './tech-record-summary-changes.component.html', - styleUrls: ['./tech-record-summary-changes.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-tech-record-summary-changes', + templateUrl: './tech-record-summary-changes.component.html', + styleUrls: ['./tech-record-summary-changes.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TechRecordSummaryChangesComponent implements OnInit, OnDestroy { - destroy$ = new Subject(); - - techRecord?: TechRecordType<'get'>; - techRecordEdited?: TechRecordType<'put'>; - techRecordChanges?: Partial>; - techRecordDeletions?: Partial>; - techRecordChangesKeys: string[] = []; - - sectionsWhitelist: string[] = []; - username = ''; - - constructor( - public store$: Store, - public technicalRecordService: TechnicalRecordService, - public router: Router, - public globalErrorService: GlobalErrorService, - public route: ActivatedRoute, - public routerService: RouterService, - public actions$: Actions, - public userService$: UserService, - public featureToggleService: FeatureToggleService - ) {} - - ngOnInit(): void { - this.navigateUponSuccess(); - this.initSubscriptions(); - } - - navigateUponSuccess(): void { - this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { - this.store$.dispatch(clearAllSectionStates()); - this.store$.dispatch(clearScrollPosition()); - void this.router.navigate([ - `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, - ]); - }); - } - - initSubscriptions(): void { - this.userService$.name$.pipe(takeUntil(this.destroy$)).subscribe((name) => { - this.username = name; - }); - this.store$ - .select(techRecord) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((data) => { - if (!data) this.cancel(); - this.techRecord = data; - }); - - this.store$ - .select(editingTechRecord) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((data) => { - if (!data) this.cancel(); - this.techRecordEdited = data; - }); - - this.store$ - .select(selectTechRecordChanges) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((changes) => { - this.techRecordChanges = changes; - if (this.vehicleType === VehicleTypes.PSV || this.vehicleType === VehicleTypes.HGV) { - delete (this.techRecordChanges as Partial).techRecord_numberOfWheelsDriven; - } - if ( - (this.vehicleType === VehicleTypes.CAR || this.vehicleType === VehicleTypes.LGV) && - (changes as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass - ) { - (this.techRecordChanges as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass = ( - this.techRecordEdited as TechRecordGETCar | TechRecordGETLGV - ).techRecord_vehicleSubclass; - } - this.techRecordChangesKeys = this.getTechRecordChangesKeys(); - this.sectionsWhitelist = this.getSectionsWhitelist(); - }); - - this.store$ - .select(selectTechRecordDeletions) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((deletions) => { - this.techRecordDeletions = deletions; - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get vehicleType() { - return this.techRecordEdited ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordEdited) : undefined; - } - - get vehicleSummary(): FormNode { - return VehicleSummary; - } - - get deletedAxles(): Axles { - if (this.techRecordEdited?.techRecord_vehicleType === 'hgv' && this.techRecordDeletions) { - return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); - } - - if (this.techRecordEdited?.techRecord_vehicleType === 'trl' && this.techRecordDeletions) { - return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); - } - - if (this.techRecordEdited?.techRecord_vehicleType === 'psv' && this.techRecordDeletions) { - return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); - } - - return []; - } - - get sectionTemplatesState$() { - return this.technicalRecordService.sectionStates$; - } - - isSectionExpanded$(sectionName: string | number) { - return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); - } - - submit() { - combineLatest([this.routerService.getRouteNestedParam$('systemNumber'), this.routerService.getRouteNestedParam$('createdTimestamp')]) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe(([systemNumber, createdTimestamp]) => { - if (systemNumber && createdTimestamp) { - this.store$.dispatch(updateADRAdditionalExaminerNotes({ username: this.username })); - this.store$.dispatch(clearADRDetailsBeforeUpdate()); - this.store$.dispatch(updateTechRecord({ systemNumber, createdTimestamp })); - } - }); - } - - cancel() { - this.globalErrorService.clearErrors(); - void this.router.navigate(['..'], { relativeTo: this.route }); - } - - getTechRecordChangesKeys(): string[] { - const entries = Object.entries(this.techRecordChanges ?? {}); - const filter = entries.filter(([, value]) => this.isNotEmpty(value)); - const changeMap = filter.map(([key]) => key); - return changeMap; - } - - getSectionsWhitelist() { - const whitelist: string[] = []; - if (this.vehicleType == null) return whitelist; - if (this.techRecordChanges == null) return whitelist; - if (this.technicalRecordService.haveAxlesChanged(this.vehicleType, this.techRecordChanges)) { - whitelist.push('weightsSection'); - } - - return whitelist; - } - - get changesForWeights() { - if (this.techRecordEdited == null) return undefined; - - return ['hgv', 'trl', 'psv'].includes(this.techRecordEdited.techRecord_vehicleType) - ? (this.techRecordChanges as Partial) - : undefined; - } - - get vehicleTemplates() { - return vehicleTemplateMap - .get(this.techRecordEdited?.techRecord_vehicleType as VehicleTypes) - ?.filter((template) => template.name !== 'technicalRecordSummary'); - } - - get customVehicleTemplate() { - return this.vehicleTemplates - ?.map((vehicleTemplate) => ({ - ...this.toVisibleFormNode(vehicleTemplate), - children: vehicleTemplate.children - ?.filter((child) => { - return this.techRecordChangesKeys.includes(child.name); - }) - .map((child) => this.toVisibleFormNode(child)), - })) - .filter((section) => Boolean(section && section.children && section.children.length > 0) || this.sectionsWhitelist.includes(section.name)); - } - - toVisibleFormNode(node: FormNode): FormNode { - return { ...node, viewType: node.viewType === FormNodeViewTypes.HIDDEN ? FormNodeViewTypes.STRING : node.viewType }; - } - - isNotEmpty(value: unknown): boolean { - if (value === '' || value === undefined) return false; - if (typeof value === 'object' && value !== null) return Object.values(value).length > 0; - return true; - } + destroy$ = new Subject(); + + techRecord?: TechRecordType<'get'>; + techRecordEdited?: TechRecordType<'put'>; + techRecordChanges?: Partial>; + techRecordDeletions?: Partial>; + techRecordChangesKeys: string[] = []; + + sectionsWhitelist: string[] = []; + username = ''; + + constructor( + public store$: Store, + public technicalRecordService: TechnicalRecordService, + public router: Router, + public globalErrorService: GlobalErrorService, + public route: ActivatedRoute, + public routerService: RouterService, + public actions$: Actions, + public userService$: UserService, + public featureToggleService: FeatureToggleService + ) {} + + ngOnInit(): void { + this.navigateUponSuccess(); + this.initSubscriptions(); + } + + navigateUponSuccess(): void { + this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { + this.store$.dispatch(clearAllSectionStates()); + this.store$.dispatch(clearScrollPosition()); + void this.router.navigate([ + `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, + ]); + }); + } + + initSubscriptions(): void { + this.userService$.name$.pipe(takeUntil(this.destroy$)).subscribe((name) => { + this.username = name; + }); + this.store$ + .select(techRecord) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((data) => { + if (!data) this.cancel(); + this.techRecord = data; + }); + + this.store$ + .select(editingTechRecord) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((data) => { + if (!data) this.cancel(); + this.techRecordEdited = data; + }); + + this.store$ + .select(selectTechRecordChanges) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((changes) => { + this.techRecordChanges = changes; + if (this.vehicleType === VehicleTypes.PSV || this.vehicleType === VehicleTypes.HGV) { + delete (this.techRecordChanges as Partial) + .techRecord_numberOfWheelsDriven; + } + if ( + (this.vehicleType === VehicleTypes.CAR || this.vehicleType === VehicleTypes.LGV) && + (changes as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass + ) { + (this.techRecordChanges as TechRecordGETCar | TechRecordGETLGV).techRecord_vehicleSubclass = ( + this.techRecordEdited as TechRecordGETCar | TechRecordGETLGV + ).techRecord_vehicleSubclass; + } + this.techRecordChangesKeys = this.getTechRecordChangesKeys(); + this.sectionsWhitelist = this.getSectionsWhitelist(); + }); + + this.store$ + .select(selectTechRecordDeletions) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((deletions) => { + this.techRecordDeletions = deletions; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + get vehicleType() { + return this.techRecordEdited + ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordEdited) + : undefined; + } + + get vehicleSummary(): FormNode { + return VehicleSummary; + } + + get deletedAxles(): Axles { + if (this.techRecordEdited?.techRecord_vehicleType === 'hgv' && this.techRecordDeletions) { + return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); + } + + if (this.techRecordEdited?.techRecord_vehicleType === 'trl' && this.techRecordDeletions) { + return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); + } + + if (this.techRecordEdited?.techRecord_vehicleType === 'psv' && this.techRecordDeletions) { + return Object.values((this.techRecordDeletions as Partial).techRecord_axles ?? {}); + } + + return []; + } + + get sectionTemplatesState$() { + return this.technicalRecordService.sectionStates$; + } + + isSectionExpanded$(sectionName: string | number) { + return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); + } + + submit() { + combineLatest([ + this.routerService.getRouteNestedParam$('systemNumber'), + this.routerService.getRouteNestedParam$('createdTimestamp'), + ]) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe(([systemNumber, createdTimestamp]) => { + if (systemNumber && createdTimestamp) { + this.store$.dispatch(updateADRAdditionalExaminerNotes({ username: this.username })); + this.store$.dispatch(clearADRDetailsBeforeUpdate()); + this.store$.dispatch(updateTechRecord({ systemNumber, createdTimestamp })); + } + }); + } + + cancel() { + this.globalErrorService.clearErrors(); + void this.router.navigate(['..'], { relativeTo: this.route }); + } + + getTechRecordChangesKeys(): string[] { + const entries = Object.entries(this.techRecordChanges ?? {}); + const filter = entries.filter(([, value]) => this.isNotEmpty(value)); + const changeMap = filter.map(([key]) => key); + return changeMap; + } + + getSectionsWhitelist() { + const whitelist: string[] = []; + if (this.vehicleType == null) return whitelist; + if (this.techRecordChanges == null) return whitelist; + if (this.technicalRecordService.haveAxlesChanged(this.vehicleType, this.techRecordChanges)) { + whitelist.push('weightsSection'); + } + + return whitelist; + } + + get changesForWeights() { + if (this.techRecordEdited == null) return undefined; + + return ['hgv', 'trl', 'psv'].includes(this.techRecordEdited.techRecord_vehicleType) + ? (this.techRecordChanges as Partial) + : undefined; + } + + get vehicleTemplates() { + return vehicleTemplateMap + .get(this.techRecordEdited?.techRecord_vehicleType as VehicleTypes) + ?.filter((template) => template.name !== 'technicalRecordSummary'); + } + + get customVehicleTemplate() { + return this.vehicleTemplates + ?.map((vehicleTemplate) => ({ + ...this.toVisibleFormNode(vehicleTemplate), + children: vehicleTemplate.children + ?.filter((child) => { + return this.techRecordChangesKeys.includes(child.name); + }) + .map((child) => this.toVisibleFormNode(child)), + })) + .filter( + (section) => + Boolean(section && section.children && section.children.length > 0) || + this.sectionsWhitelist.includes(section.name) + ); + } + + toVisibleFormNode(node: FormNode): FormNode { + return { ...node, viewType: node.viewType === FormNodeViewTypes.HIDDEN ? FormNodeViewTypes.STRING : node.viewType }; + } + + isNotEmpty(value: unknown): boolean { + if (value === '' || value === undefined) return false; + if (typeof value === 'object' && value !== null) return Object.values(value).length > 0; + return true; + } } diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts index 93396dd164..0143805374 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts @@ -1,17 +1,17 @@ import { ViewportScroller } from '@angular/common'; import { - ChangeDetectionStrategy, - Component, - EventEmitter, - OnDestroy, - OnInit, - Output, - QueryList, - ViewChild, - ViewChildren, - inject, + ChangeDetectionStrategy, + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, + QueryList, + ViewChild, + ViewChildren, + inject, } from '@angular/core'; -import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { GlobalError } from '@core/components/global-error/global-error.interface'; import { GlobalErrorService } from '@core/components/global-error/global-error.service'; @@ -44,230 +44,232 @@ import { cloneDeep, mergeWith } from 'lodash'; import { Observable, Subject, debounceTime, map, take, takeUntil } from 'rxjs'; @Component({ - selector: 'app-tech-record-summary', - templateUrl: './tech-record-summary.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - styleUrls: ['./tech-record-summary.component.scss'], + selector: 'app-tech-record-summary', + templateUrl: './tech-record-summary.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./tech-record-summary.component.scss'], }) export class TechRecordSummaryComponent implements OnInit, OnDestroy { - @ViewChildren(DynamicFormGroupComponent) sections!: QueryList; - @ViewChild(BodyComponent) body!: BodyComponent; - @ViewChild(DimensionsComponent) dimensions!: DimensionsComponent; - @ViewChild(PsvBrakesComponent) psvBrakes!: PsvBrakesComponent; - @ViewChild(TrlBrakesComponent) trlBrakes!: TrlBrakesComponent; - @ViewChild(TyresComponent) tyres!: TyresComponent; - @ViewChild(WeightsComponent) weights!: WeightsComponent; - @ViewChild(LettersComponent) letters!: LettersComponent; - @ViewChild(ApprovalTypeComponent) approvalType!: ApprovalTypeComponent; - @ViewChild(AdrComponent) adr!: AdrComponent; - - @Output() isFormDirty = new EventEmitter(); - @Output() isFormInvalid = new EventEmitter(); - - techRecordCalculated?: V3TechRecordModel; - sectionTemplates: Array = []; - middleIndex = 0; - isEditing = false; - scrollPosition: [number, number] = [0, 0]; - isADRCertGenEnabled = false; - isDFSEnabled = false; - - private axlesService = inject(AxlesService); - private errorService = inject(GlobalErrorService); - private warningService = inject(GlobalWarningService); - private referenceDataService = inject(ReferenceDataService); - private technicalRecordService = inject(TechnicalRecordService); - private routerService = inject(RouterService); - private activatedRoute = inject(ActivatedRoute); - private viewportScroller = inject(ViewportScroller); - private store = inject(Store); - private loading = inject(LoadingService); - - fb = inject(FormBuilder); - featureToggleService = inject(FeatureToggleService); - - private destroy$ = new Subject(); - - form = this.fb.group({}); - - ngOnInit(): void { - this.isADRCertGenEnabled = this.featureToggleService.isFeatureEnabled('adrCertToggle'); - this.isDFSEnabled = this.featureToggleService.isFeatureEnabled('dfs'); - this.technicalRecordService.techRecord$ - .pipe( - map((record) => { - if (!record) { - return; - } - - let techRecord = cloneDeep(record); - techRecord = this.normaliseAxles(record); - - return techRecord; - }), - takeUntil(this.destroy$) - ) - .subscribe((techRecord) => { - if (techRecord) { - this.techRecordCalculated = techRecord; - } - this.referenceDataService.removeTyreSearch(); - this.sectionTemplates = this.vehicleTemplates; - this.middleIndex = Math.floor(this.sectionTemplates.length / 2); - }); - - const editingReason = this.activatedRoute.snapshot.data['reason']; - if (this.isEditing) { - this.technicalRecordService.clearReasonForCreation(); - this.technicalRecordService.techRecord$.pipe(takeUntil(this.destroy$), take(1)).subscribe((techRecord) => { - if (techRecord) { - if (editingReason === ReasonForEditing.NOTIFIABLE_ALTERATION_NEEDED) { - this.technicalRecordService.updateEditingTechRecord({ - ...(techRecord as TechRecordType<'put'>), - techRecord_statusCode: StatusCodes.PROVISIONAL, - }); - } - - if (techRecord?.vin?.match('([IOQ])a*')) { - const warnings: GlobalWarning[] = []; - warnings.push({ warning: 'VIN should not contain I, O or Q', anchorLink: 'vin' }); - this.warningService.setWarnings(warnings); - } - } - }); - } else if (!this.isEditing) { - this.warningService.clearWarnings(); - } - - this.store - .select(selectScrollPosition) - .pipe(take(1), takeUntil(this.destroy$)) - .subscribe((position) => { - this.scrollPosition = position; - }); - - this.loading.showSpinner$.pipe(takeUntil(this.destroy$), debounceTime(10)).subscribe((loading) => { - if (!loading) { - this.viewportScroller.scrollToPosition(this.scrollPosition); - } - }); - - this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((changes) => this.handleFormState(changes)); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - get vehicleType() { - return this.techRecordCalculated ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordCalculated) : undefined; - } - - get vehicleTemplates(): Array { - this.isEditing$.pipe(takeUntil(this.destroy$)).subscribe((editing) => { - this.isEditing = editing; - }); - if (!this.vehicleType) { - return []; - } - return ( - vehicleTemplateMap - .get(this.vehicleType) - ?.filter((template) => template.name !== (this.isEditing ? 'audit' : 'reasonForCreationSection')) - .filter((template) => template.name !== (this.isADRCertGenEnabled ? '' : 'adrCertificateSection')) ?? [] - ); - } - - get sectionTemplatesState$() { - return this.technicalRecordService.sectionStates$; - } - - isSectionExpanded$(sectionName: string | number) { - return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); - } - - get isEditing$(): Observable { - return this.routerService.getRouteDataProperty$('isEditing').pipe(map((isEditing) => !!isEditing)); - } - - get customSectionForms(): Array { - const commonCustomSections = [this.body?.form, this.dimensions?.form, this.tyres?.form, this.weights?.form, this.approvalType?.form]; - - switch (this.vehicleType) { - case VehicleTypes.PSV: - return [...commonCustomSections, this.psvBrakes.form]; - case VehicleTypes.HGV: - return !this.isDFSEnabled ? [...commonCustomSections, this.adr.form] : commonCustomSections; - case VehicleTypes.TRL: - return !this.isDFSEnabled - ? [...commonCustomSections, this.trlBrakes.form, this.letters.form, this.adr.form] - : [...commonCustomSections, this.trlBrakes.form, this.letters.form]; - case VehicleTypes.LGV: - return !this.isDFSEnabled ? [this.adr.form] : []; - default: - return []; - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handleFormState(event: any): void { - const isPrimitiveArray = (a: unknown, b: unknown) => (Array.isArray(a) && !a.some((i) => typeof i === 'object') ? b : undefined); - - this.techRecordCalculated = mergeWith(cloneDeep(this.techRecordCalculated), event, isPrimitiveArray); - this.technicalRecordService.updateEditingTechRecord(this.techRecordCalculated as TechRecordType<'put'>); - } - - checkForms(): void { - const forms: Array = this.sections?.map((section) => section.form).concat(this.customSectionForms); - - this.isFormDirty.emit(forms.some((form) => form.dirty)); - - this.setErrors(forms); - - this.isFormInvalid.emit(forms.some((form) => form.invalid)); - } - - setErrors(forms: Array): void { - const errors: GlobalError[] = []; - - forms.forEach((form) => DynamicFormService.validate(form, errors)); - - this.form.markAllAsTouched(); - this.form.updateValueAndValidity(); - - function extractErrors(form: FormGroup | FormArray) { - Object.entries(form.controls).forEach(([key, control]) => { - if (control instanceof FormGroup || control instanceof FormArray) { - extractErrors(control); - } else if (control.invalid && control.errors) { - Object.values(control.errors).forEach((error) => { - errors.push({ error, anchorLink: key }); - }); - } - }); - } - - extractErrors(this.form); - - if (errors.length) { - this.errorService.setErrors(errors); - } else { - this.errorService.clearErrors(); - } - } - - private normaliseAxles(record: V3TechRecordModel): V3TechRecordModel { - const type = record.techRecord_vehicleType; - const category = record.techRecord_euVehicleCategory; - - if (type === VehicleTypes.HGV || (type === VehicleTypes.TRL && category !== 'o1' && category !== 'o2')) { - const [axles, axleSpacing] = this.axlesService.normaliseAxles(record.techRecord_axles ?? [], record.techRecord_dimensions_axleSpacing); - - record.techRecord_dimensions_axleSpacing = axleSpacing; - record.techRecord_axles = axles; - } - - return record; - } + @ViewChildren(DynamicFormGroupComponent) sections!: QueryList; + @ViewChild(BodyComponent) body!: BodyComponent; + @ViewChild(DimensionsComponent) dimensions!: DimensionsComponent; + @ViewChild(PsvBrakesComponent) psvBrakes!: PsvBrakesComponent; + @ViewChild(TrlBrakesComponent) trlBrakes!: TrlBrakesComponent; + @ViewChild(TyresComponent) tyres!: TyresComponent; + @ViewChild(WeightsComponent) weights!: WeightsComponent; + @ViewChild(LettersComponent) letters!: LettersComponent; + @ViewChild(ApprovalTypeComponent) approvalType!: ApprovalTypeComponent; + @ViewChild(AdrComponent) adr!: AdrComponent; + + @Output() isFormDirty = new EventEmitter(); + @Output() isFormInvalid = new EventEmitter(); + + techRecordCalculated?: V3TechRecordModel; + sectionTemplates: Array = []; + middleIndex = 0; + isEditing = false; + scrollPosition: [number, number] = [0, 0]; + isADRCertGenEnabled = false; + isDFSEnabled = false; + + private axlesService = inject(AxlesService); + private errorService = inject(GlobalErrorService); + private warningService = inject(GlobalWarningService); + private referenceDataService = inject(ReferenceDataService); + private technicalRecordService = inject(TechnicalRecordService); + private routerService = inject(RouterService); + private activatedRoute = inject(ActivatedRoute); + private viewportScroller = inject(ViewportScroller); + private store = inject(Store); + private loading = inject(LoadingService); + + fb = inject(FormBuilder); + featureToggleService = inject(FeatureToggleService); + globalErrorService = inject(GlobalErrorService); + + private destroy$ = new Subject(); + + form = this.fb.group({}); + + ngOnInit(): void { + this.isADRCertGenEnabled = this.featureToggleService.isFeatureEnabled('adrCertToggle'); + this.isDFSEnabled = this.featureToggleService.isFeatureEnabled('dfs'); + this.technicalRecordService.techRecord$ + .pipe( + map((record) => { + if (!record) { + return; + } + + let techRecord = cloneDeep(record); + techRecord = this.normaliseAxles(record); + + return techRecord; + }), + takeUntil(this.destroy$) + ) + .subscribe((techRecord) => { + if (techRecord) { + this.techRecordCalculated = techRecord; + } + this.referenceDataService.removeTyreSearch(); + this.sectionTemplates = this.vehicleTemplates; + this.middleIndex = Math.floor(this.sectionTemplates.length / 2); + }); + + const editingReason = this.activatedRoute.snapshot.data['reason']; + if (this.isEditing) { + this.technicalRecordService.clearReasonForCreation(); + this.technicalRecordService.techRecord$.pipe(takeUntil(this.destroy$), take(1)).subscribe((techRecord) => { + if (techRecord) { + if (editingReason === ReasonForEditing.NOTIFIABLE_ALTERATION_NEEDED) { + this.technicalRecordService.updateEditingTechRecord({ + ...(techRecord as TechRecordType<'put'>), + techRecord_statusCode: StatusCodes.PROVISIONAL, + }); + } + + if (techRecord?.vin?.match('([IOQ])a*')) { + const warnings: GlobalWarning[] = []; + warnings.push({ warning: 'VIN should not contain I, O or Q', anchorLink: 'vin' }); + this.warningService.setWarnings(warnings); + } + } + }); + } else if (!this.isEditing) { + this.warningService.clearWarnings(); + } + + this.store + .select(selectScrollPosition) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe((position) => { + this.scrollPosition = position; + }); + + this.loading.showSpinner$.pipe(takeUntil(this.destroy$), debounceTime(10)).subscribe((loading) => { + if (!loading) { + this.viewportScroller.scrollToPosition(this.scrollPosition); + } + }); + + this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((changes) => this.handleFormState(changes)); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + get vehicleType() { + return this.techRecordCalculated + ? this.technicalRecordService.getVehicleTypeWithSmallTrl(this.techRecordCalculated) + : undefined; + } + + get vehicleTemplates(): Array { + this.isEditing$.pipe(takeUntil(this.destroy$)).subscribe((editing) => { + this.isEditing = editing; + }); + if (!this.vehicleType) { + return []; + } + return ( + vehicleTemplateMap + .get(this.vehicleType) + ?.filter((template) => template.name !== (this.isEditing ? 'audit' : 'reasonForCreationSection')) + .filter((template) => template.name !== (this.isADRCertGenEnabled ? '' : 'adrCertificateSection')) ?? [] + ); + } + + get sectionTemplatesState$() { + return this.technicalRecordService.sectionStates$; + } + + isSectionExpanded$(sectionName: string | number) { + return this.sectionTemplatesState$?.pipe(map((sections) => sections?.includes(sectionName))); + } + + get isEditing$(): Observable { + return this.routerService.getRouteDataProperty$('isEditing').pipe(map((isEditing) => !!isEditing)); + } + + get customSectionForms(): Array { + const commonCustomSections = [ + this.body?.form, + this.dimensions?.form, + this.tyres?.form, + this.weights?.form, + this.approvalType?.form, + ]; + + switch (this.vehicleType) { + case VehicleTypes.PSV: + return [...commonCustomSections, this.psvBrakes.form]; + case VehicleTypes.HGV: + return !this.isDFSEnabled ? [...commonCustomSections, this.adr.form] : commonCustomSections; + case VehicleTypes.TRL: + return !this.isDFSEnabled + ? [...commonCustomSections, this.trlBrakes.form, this.letters.form, this.adr.form] + : [...commonCustomSections, this.trlBrakes.form, this.letters.form]; + case VehicleTypes.LGV: + return !this.isDFSEnabled ? [this.adr.form] : []; + default: + return []; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handleFormState(event: any): void { + const isPrimitiveArray = (a: unknown, b: unknown) => + Array.isArray(a) && !a.some((i) => typeof i === 'object') ? b : undefined; + + this.techRecordCalculated = mergeWith(cloneDeep(this.techRecordCalculated), event, isPrimitiveArray); + this.technicalRecordService.updateEditingTechRecord(this.techRecordCalculated as TechRecordType<'put'>); + } + + checkForms(): void { + const forms: Array = this.sections + ?.map((section) => section.form) + .concat(this.customSectionForms); + + this.isFormDirty.emit(forms.some((form) => form.dirty)); + + this.setErrors(forms); + + this.isFormInvalid.emit(forms.some((form) => form.invalid)); + } + + setErrors(forms: Array): void { + const errors: GlobalError[] = []; + + forms.forEach((form) => DynamicFormService.validate(form, errors)); + + this.form.markAllAsTouched(); + this.form.updateValueAndValidity(); + errors.push(...this.globalErrorService.extractGlobalErrors(this.form)); + + if (errors.length) { + this.errorService.setErrors(errors); + } else { + this.errorService.clearErrors(); + } + } + + private normaliseAxles(record: V3TechRecordModel): V3TechRecordModel { + const type = record.techRecord_vehicleType; + const category = record.techRecord_euVehicleCategory; + + if (type === VehicleTypes.HGV || (type === VehicleTypes.TRL && category !== 'o1' && category !== 'o2')) { + const [axles, axleSpacing] = this.axlesService.normaliseAxles( + record.techRecord_axles ?? [], + record.techRecord_dimensions_axleSpacing + ); + + record.techRecord_dimensions_axleSpacing = axleSpacing; + record.techRecord_axles = axles; + } + + return record; + } } diff --git a/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts b/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts index 0360b4d445..286b2f24cd 100644 --- a/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts +++ b/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts @@ -7,7 +7,13 @@ import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/ import { Roles } from '@models/roles.enum'; import { TechRecordActions } from '@models/tech-record/tech-record-actions.enum'; import { TestResultModel } from '@models/test-results/test-result.model'; -import { ReasonForEditing, StatusCodes, TechRecordModel, V3TechRecordModel, VehicleTypes } from '@models/vehicle-tech-record.model'; +import { + ReasonForEditing, + StatusCodes, + TechRecordModel, + V3TechRecordModel, + VehicleTypes, +} from '@models/vehicle-tech-record.model'; import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { AdrService } from '@services/adr/adr.service'; @@ -20,148 +26,148 @@ import { Observable, Subject, take, takeUntil } from 'rxjs'; import { TechRecordSummaryComponent } from '../tech-record-summary/tech-record-summary.component'; @Component({ - selector: 'app-vehicle-technical-record', - templateUrl: './vehicle-technical-record.component.html', - styleUrls: ['./vehicle-technical-record.component.scss'], + selector: 'app-vehicle-technical-record', + templateUrl: './vehicle-technical-record.component.html', + styleUrls: ['./vehicle-technical-record.component.scss'], }) export class VehicleTechnicalRecordComponent implements OnInit, OnDestroy { - @ViewChild(TechRecordSummaryComponent) summary!: TechRecordSummaryComponent; - @Input() techRecord?: V3TechRecordModel; - - testResults$: Observable; - editingReason?: ReasonForEditing; - recordHistory?: TechRecordSearchSchema[]; - - isCurrent = false; - isArchived = false; - isEditing = false; - isDirty = false; - isInvalid = false; - - private destroy$ = new Subject(); - hasTestResultAmend: boolean | undefined = false; - - constructor( - public globalErrorService: GlobalErrorService, - public userService: UserService, - testRecordService: TestRecordsService, - private activatedRoute: ActivatedRoute, - private route: ActivatedRoute, - private router: Router, - private store: Store, - private actions$: Actions, - private viewportScroller: ViewportScroller, - private featureToggleService: FeatureToggleService, - public adrService: AdrService - ) { - this.testResults$ = testRecordService.testRecords$; - this.isEditing = this.activatedRoute.snapshot.data['isEditing'] ?? false; - this.editingReason = this.activatedRoute.snapshot.data['reason']; - } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - ngOnInit(): void { - this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { - void this.router.navigate([ - `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, - ]); - }); - this.isArchived = this.techRecord?.techRecord_statusCode === StatusCodes.ARCHIVED; - this.isCurrent = this.techRecord?.techRecord_statusCode === StatusCodes.CURRENT; - - this.userService.roles$.pipe(take(1)).subscribe((storedRoles) => { - this.hasTestResultAmend = storedRoles?.some((role) => { - return Roles.TestResultAmend.split(',').includes(role); - }); - }); - } - - get currentVrm(): string | undefined { - return this.techRecord?.techRecord_vehicleType !== 'trl' ? this.techRecord?.primaryVrm ?? '' : undefined; - } - - get roles(): typeof Roles { - return Roles; - } - - get vehicleTypes(): typeof VehicleTypes { - return VehicleTypes; - } - - get statusCodes(): typeof StatusCodes { - return StatusCodes; - } - - hasPlates(techRecord: TechRecordModel) { - return (techRecord.plates?.length ?? 0) > 0; - } - - getActions(techRecord?: V3TechRecordModel): TechRecordActions { - switch (techRecord?.techRecord_statusCode) { - case StatusCodes.CURRENT: - return TechRecordActions.CURRENT; - case StatusCodes.PROVISIONAL: - return TechRecordActions.PROVISIONAL; - case StatusCodes.ARCHIVED: - return TechRecordActions.ARCHIVED; - default: - return TechRecordActions.NONE; - } - } - - getVehicleDescription(techRecord: TechRecordModel, vehicleType: VehicleTypes | undefined): string { - switch (vehicleType) { - case VehicleTypes.TRL: - return techRecord.vehicleConfiguration ?? ''; - case VehicleTypes.PSV: - return techRecord.bodyMake && techRecord.bodyModel ? `${techRecord.bodyMake}-${techRecord.bodyModel}` : ''; - case VehicleTypes.HGV: - return techRecord.make && techRecord.model ? `${techRecord.make}-${techRecord.model}` : ''; - default: - return 'Unknown Vehicle Type'; - } - } - - showCreateTestButton(): boolean { - return !this.isArchived && !this.isEditing; - } - - async createTest(techRecord?: V3TechRecordModel): Promise { - this.store.dispatch(clearScrollPosition()); - if ( - (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'complete' || - (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'testable' - ) { - await this.router.navigate(['test-records/create-test/type'], { relativeTo: this.route }); - } else { - this.globalErrorService.setErrors([ - { - error: this.getCreateTestErrorMessage(techRecord?.techRecord_hiddenInVta ?? false), - anchorLink: 'create-test', - }, - ]); - - this.viewportScroller.scrollToPosition([0, 0]); - } - } - - async handleSubmit(): Promise { - this.summary.checkForms(); - if (this.isInvalid) return; - - await this.router.navigate(['change-summary'], { relativeTo: this.route }); - } - - private getCreateTestErrorMessage(hiddenInVta: boolean | undefined): string { - if (hiddenInVta) { - return 'Vehicle record is hidden in VTA. Show the vehicle record in VTA to start recording tests against it.'; - } - - return this.hasTestResultAmend - ? 'This vehicle does not have enough information to be tested. Please complete this record so tests can be recorded against it.' - : 'This vehicle does not have enough information to be tested.' + - ' Call the Contact Centre to complete this record so tests can be recorded against it.'; - } + @ViewChild(TechRecordSummaryComponent) summary!: TechRecordSummaryComponent; + @Input() techRecord?: V3TechRecordModel; + + testResults$: Observable; + editingReason?: ReasonForEditing; + recordHistory?: TechRecordSearchSchema[]; + + isCurrent = false; + isArchived = false; + isEditing = false; + isDirty = false; + isInvalid = false; + + private destroy$ = new Subject(); + hasTestResultAmend: boolean | undefined = false; + + constructor( + public globalErrorService: GlobalErrorService, + public userService: UserService, + testRecordService: TestRecordsService, + private activatedRoute: ActivatedRoute, + private route: ActivatedRoute, + private router: Router, + private store: Store, + private actions$: Actions, + private viewportScroller: ViewportScroller, + private featureToggleService: FeatureToggleService, + public adrService: AdrService + ) { + this.testResults$ = testRecordService.testRecords$; + this.isEditing = this.activatedRoute.snapshot.data['isEditing'] ?? false; + this.editingReason = this.activatedRoute.snapshot.data['reason']; + } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + ngOnInit(): void { + this.actions$.pipe(ofType(updateTechRecordSuccess), takeUntil(this.destroy$)).subscribe((vehicleTechRecord) => { + void this.router.navigate([ + `/tech-records/${vehicleTechRecord.vehicleTechRecord.systemNumber}/${vehicleTechRecord.vehicleTechRecord.createdTimestamp}`, + ]); + }); + this.isArchived = this.techRecord?.techRecord_statusCode === StatusCodes.ARCHIVED; + this.isCurrent = this.techRecord?.techRecord_statusCode === StatusCodes.CURRENT; + + this.userService.roles$.pipe(take(1)).subscribe((storedRoles) => { + this.hasTestResultAmend = storedRoles?.some((role) => { + return Roles.TestResultAmend.split(',').includes(role); + }); + }); + } + + get currentVrm(): string | undefined { + return this.techRecord?.techRecord_vehicleType !== 'trl' ? this.techRecord?.primaryVrm ?? '' : undefined; + } + + get roles(): typeof Roles { + return Roles; + } + + get vehicleTypes(): typeof VehicleTypes { + return VehicleTypes; + } + + get statusCodes(): typeof StatusCodes { + return StatusCodes; + } + + hasPlates(techRecord: TechRecordModel) { + return (techRecord.plates?.length ?? 0) > 0; + } + + getActions(techRecord?: V3TechRecordModel): TechRecordActions { + switch (techRecord?.techRecord_statusCode) { + case StatusCodes.CURRENT: + return TechRecordActions.CURRENT; + case StatusCodes.PROVISIONAL: + return TechRecordActions.PROVISIONAL; + case StatusCodes.ARCHIVED: + return TechRecordActions.ARCHIVED; + default: + return TechRecordActions.NONE; + } + } + + getVehicleDescription(techRecord: TechRecordModel, vehicleType: VehicleTypes | undefined): string { + switch (vehicleType) { + case VehicleTypes.TRL: + return techRecord.vehicleConfiguration ?? ''; + case VehicleTypes.PSV: + return techRecord.bodyMake && techRecord.bodyModel ? `${techRecord.bodyMake}-${techRecord.bodyModel}` : ''; + case VehicleTypes.HGV: + return techRecord.make && techRecord.model ? `${techRecord.make}-${techRecord.model}` : ''; + default: + return 'Unknown Vehicle Type'; + } + } + + showCreateTestButton(): boolean { + return !this.isArchived && !this.isEditing; + } + + async createTest(techRecord?: V3TechRecordModel): Promise { + this.store.dispatch(clearScrollPosition()); + if ( + (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'complete' || + (techRecord as TechRecordType<'get'>)?.techRecord_recordCompleteness === 'testable' + ) { + await this.router.navigate(['test-records/create-test/type'], { relativeTo: this.route }); + } else { + this.globalErrorService.setErrors([ + { + error: this.getCreateTestErrorMessage(techRecord?.techRecord_hiddenInVta ?? false), + anchorLink: 'create-test', + }, + ]); + + this.viewportScroller.scrollToPosition([0, 0]); + } + } + + async handleSubmit(): Promise { + this.summary.checkForms(); + if (this.isInvalid) return; + + await this.router.navigate(['change-summary'], { relativeTo: this.route }); + } + + private getCreateTestErrorMessage(hiddenInVta: boolean | undefined): string { + if (hiddenInVta) { + return 'Vehicle record is hidden in VTA. Show the vehicle record in VTA to start recording tests against it.'; + } + + return this.hasTestResultAmend + ? 'This vehicle does not have enough information to be tested. Please complete this record so tests can be recorded against it.' + : 'This vehicle does not have enough information to be tested.' + + ' Call the Contact Centre to complete this record so tests can be recorded against it.'; + } } diff --git a/src/app/forms/components/character-count/character-count.component.html b/src/app/forms/components/character-count/character-count.component.html new file mode 100644 index 0000000000..4b894037df --- /dev/null +++ b/src/app/forms/components/character-count/character-count.component.html @@ -0,0 +1,10 @@ +
+ You have {{ limit() - (control()?.value?.length ?? 0) }} characters remaining +
+
+ You have {{ (control()?.value?.length ?? limit()) - limit() }} too many +
diff --git a/src/app/forms/components/character-count/character-count.component.ts b/src/app/forms/components/character-count/character-count.component.ts new file mode 100644 index 0000000000..1e90cf35d2 --- /dev/null +++ b/src/app/forms/components/character-count/character-count.component.ts @@ -0,0 +1,13 @@ +import { Component, computed, inject, input } from '@angular/core'; +import { ControlContainer } from '@angular/forms'; + +@Component({ + selector: 'app-character-count', + templateUrl: './character-count.component.html', +}) +export class CharacterCountComponent { + for = input.required(); + limit = input.required(); + controlContainer = inject(ControlContainer); + control = computed(() => this.controlContainer.control?.get(this.for())); +} diff --git a/src/app/forms/components/control-errors/control-errors.component.html b/src/app/forms/components/control-errors/control-errors.component.html new file mode 100644 index 0000000000..c91bbe04e5 --- /dev/null +++ b/src/app/forms/components/control-errors/control-errors.component.html @@ -0,0 +1,6 @@ + +

+ Error: + {{ error() }} +

+
diff --git a/src/app/forms/components/control-errors/control-errors.component.ts b/src/app/forms/components/control-errors/control-errors.component.ts new file mode 100644 index 0000000000..a77b742811 --- /dev/null +++ b/src/app/forms/components/control-errors/control-errors.component.ts @@ -0,0 +1,13 @@ +import { Component, computed, inject, input } from '@angular/core'; +import { ControlContainer } from '@angular/forms'; + +@Component({ + selector: 'app-control-errors', + templateUrl: './control-errors.component.html', +}) +export class ControlErrorsComponent { + for = input.required(); + controlContainer = inject(ControlContainer); + control = computed(() => this.controlContainer.control?.get(this.for())); + error = computed(() => Object.values(this.control()?.errors || {})[0]); +} diff --git a/src/app/forms/components/date-controls/date-controls.component.html b/src/app/forms/components/date-controls/date-controls.component.html new file mode 100644 index 0000000000..4921d89b42 --- /dev/null +++ b/src/app/forms/components/date-controls/date-controls.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/forms/components/date-controls/date-controls.component.scss b/src/app/forms/components/date-controls/date-controls.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/forms/components/date-controls/date-controls.component.ts b/src/app/forms/components/date-controls/date-controls.component.ts new file mode 100644 index 0000000000..fe26bbc375 --- /dev/null +++ b/src/app/forms/components/date-controls/date-controls.component.ts @@ -0,0 +1,132 @@ +import { Component, OnDestroy, OnInit, inject, input } from '@angular/core'; +import { + ControlContainer, + ControlValueAccessor, + FormBuilder, + NG_VALUE_ACCESSOR, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { GlobalErrorService } from '@core/components/global-error/global-error.service'; +import { ReplaySubject, takeUntil } from 'rxjs'; + +@Component({ + selector: 'app-date-controls', + templateUrl: './date-controls.component.html', + styleUrls: ['./date-controls.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: DateControlsComponent, + }, + ], +}) +export class DateControlsComponent implements ControlValueAccessor, OnInit, OnDestroy { + fb = inject(FormBuilder); + controlContainer = inject(ControlContainer); + globalErrorService = inject(GlobalErrorService); + + formControlName = input.required(); + + form = this.fb.group({ + year: this.fb.nonNullable.control(null, [this.min(1000, 'Year'), this.max(9999, 'Year')]), + month: this.fb.nonNullable.control(null, [this.min(1, 'Month'), this.max(12, 'Month')]), + day: this.fb.nonNullable.control(null, [this.min(1, 'Day'), this.max(31, 'Day')]), + hours: this.fb.nonNullable.control(null), + minutes: this.fb.nonNullable.control(null), + seconds: this.fb.nonNullable.control(null), + }); + + destroy = new ReplaySubject(1); + + onTouch: () => void = () => {}; + onChange: (value: any) => void = () => {}; + + writeValue(obj: any): void { + if (typeof obj === 'string') { + this.writeDate(new Date(obj)); + } + + if (typeof obj === 'object' && obj instanceof Date) { + this.writeDate(obj); + } + } + + writeDate(date: Date) { + this.form.setValue({ + year: date.getFullYear(), + month: date.getMonth(), + day: date.getDate(), + hours: date.getHours(), + minutes: date.getMinutes(), + seconds: date.getSeconds(), + }); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouch = fn; + } + + setDisabledState?(isDisabled: boolean): void { + isDisabled ? this.form.disable() : this.form.enable(); + } + + ngOnInit(): void { + // Ensure events of children are propagated to the parent + this.form.events.pipe(takeUntil(this.destroy)).subscribe((value) => { + if ('touched' in value && value.touched) { + this.onTouch(); + } + }); + + // Ensure form errors are propagated to the parent + this.form.statusChanges.pipe(takeUntil(this.destroy)).subscribe(() => { + const control = this.controlContainer.control?.get(this.formControlName()); + if (control) { + this.form.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + const errors = this.globalErrorService.extractErrors(this.form); + control.setErrors({ ...control.errors, ...errors }); + } + }); + + // Map the seperate form controls to a single date string + this.form.valueChanges.pipe(takeUntil(this.destroy)).subscribe(() => { + const { year, month, day, hours, minutes, seconds } = this.form.value; + + if (year === null && month === null && day === null) { + this.onChange(null); + return; + } + + const monthStr = month?.toString().padStart(2, '0'); + const dayStr = day?.toString().padStart(2, '0'); + const hoursStr = hours?.toString().padStart(2, '0'); + const minutesStr = minutes?.toString().padStart(2, '0'); + const secondsStr = seconds?.toString().padStart(2, '0'); + + this.onChange(`${year}-${monthStr}-${dayStr}T${hoursStr || '00'}:${minutesStr || '00'}:${secondsStr || '00'}`); + }); + } + + ngOnDestroy(): void { + this.destroy.next(true); + this.destroy.complete(); + } + + min(min: number, label: string): ValidatorFn { + return (control) => { + return Validators.min(min)(control) ? { min: `${label} must be greater than or equal to ${min}` } : null; + }; + } + + max(max: number, label: string): ValidatorFn { + return (control) => { + return Validators.max(max)(control) ? { max: `${label} must be less than or equal to ${max}` } : null; + }; + } +} diff --git a/src/app/forms/components/radio-group/radio-group.component.ts b/src/app/forms/components/radio-group/radio-group.component.ts index 436e4a7ed7..a29dd2a416 100644 --- a/src/app/forms/components/radio-group/radio-group.component.ts +++ b/src/app/forms/components/radio-group/radio-group.component.ts @@ -4,32 +4,32 @@ import { FormNodeOption } from '@services/dynamic-forms/dynamic-form.types'; import { BaseControlComponent } from '../base-control/base-control.component'; @Component({ - selector: 'app-radio-group', - templateUrl: './radio-group.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: RadioGroupComponent, - multi: true, - }, - ], - styleUrls: ['./radio-group.component.scss'], + selector: 'app-radio-group', + templateUrl: './radio-group.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: RadioGroupComponent, + multi: true, + }, + ], + styleUrls: ['./radio-group.component.scss'], }) export class RadioGroupComponent extends BaseControlComponent { - @Input() options: FormNodeOption[] = []; - @Input() inline = false; + @Input() options: FormNodeOption[] = []; + @Input() inline = false; - getLabel(value: string | number | boolean | null): string | undefined { - return this.options.find((option) => option.value === value)?.label; - } + getLabel(value: string | number | boolean | null): string | undefined { + return this.options.find((option) => option.value === value)?.label; + } - trackByFn = (index: number): number => index; + trackByFn = (index: number): number => index; - getId(value: string | number | boolean | null, name: string) { - const id = `${name}-${value}-radio`; - if (this.control?.meta) { - this.control.meta.customId = id; - } - return id; - } + getId(value: string | number | boolean | null, name: string) { + const id = `${name}-${value}-radio`; + if (this.control?.meta) { + this.control.meta.customId = id; + } + return id; + } } diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html index e37d79af7e..c5da6b7ade 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html @@ -3,10 +3,6 @@

-

- Error: - {{ error }} -

- + Applicant Details @@ -34,14 +30,7 @@

-

- Error: - {{ error }} -

+
@@ -50,22 +39,8 @@

-

- Error: - {{ error }} -

- + +
@@ -73,22 +48,8 @@

-

- Error: - {{ error }} -

- + + @@ -96,22 +57,8 @@

-

- Error: - {{ error }} -

- + + @@ -119,22 +66,8 @@

-

- Error: - {{ error }} -

- + + @@ -147,22 +80,8 @@

-

- Error: - {{ error }} -

- @@ -174,14 +93,6 @@

Vehicle used on international journeys

-

- Error: - {{ error }} -

- -
-
+ + +

- +

Error: - {{ error }} + {{ error }} + {{ error }} + {{ error }} + + Day must between 1 and 31 + + + Month must between 1 and 12 + + + Year must between 1000 and 9999 +

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+

+ +

+
-
+

-

- Error: - {{ error }} -

+

-

- Error: - {{ error }} -

+

-

- Error: - {{ error }} -

- + +
- + @@ -331,23 +285,8 @@

-

- Error: - {{ error }} - {{ error }} -

- + +
@@ -357,23 +296,8 @@

Tank year of manufacture

-

- Error: - {{ error }} - {{ error }} -

- + +
@@ -383,27 +307,8 @@

Manufacturer serial number

-

- Error: - - {{ error }} - - - {{ error }} - -

- + +
@@ -411,23 +316,8 @@

-

- Error: - {{ error }} - {{ error }} -

- + +
@@ -435,23 +325,8 @@

-

- Error: - {{ error }} - {{ error }} -

- + +
@@ -461,16 +336,7 @@

Substances permitted

-

- Error: - - {{ error }} - -

+
- +

-

- Error: - - {{ error }} - -

+
- +

@@ -535,32 +392,13 @@

Reference Number

-

- Error: - - {{ error }} - - - {{ error }} - -

- + +
- +

@@ -568,27 +406,8 @@

Reference number

-

- Error: - - {{ error }} - - - {{ error }} - -

- + +
@@ -624,40 +443,9 @@

-

- Error: - {{ error }} -

- -
- You have {{ 500 - (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_tank_tankDetails_specialProvisions')?.value?.length ?? 500) - 500 }} too many -
+ + +
@@ -671,32 +459,109 @@

TC2: Certificate Number

-

- Error: - {{ - error - }} - {{ - error - }} -

- + +
+ +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + + + Day must between 1 and 31 + + + Month must between 1 and 12 + + + Year must between 1000 and 9999 + +

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
@@ -710,7 +575,7 @@

-
@@ -730,6 +595,81 @@

+ +
+

+ +

+

+ Error: + + {{ error }} + + + {{ error }} + + + Day must between 1 and 31 + + + Month must between 1 and 12 + + + Year must between 1000 and 9999 + +

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+

@@ -751,10 +691,7 @@

Memo 07/09 (3 month extension) can be applied

Only applicable for vehicles used on national journeys
-

- Error: - {{ error }} -

+
Memo 07/09 (3 month extension) can be applie

M145

-

- Error: - {{ error }} -

+
M145 - +

-

- Error: - {{ error }} -

+
- +

-

- Error: - {{ error }} -

+
-

- Error: - {{ error }} -

+
-
+

-

- Error: - {{ error }} -

- -
- You have {{ 500 - (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_brakeDeclarationIssuer')?.value?.length ?? 500) - 500 }} too many -
+ + +
-

- Error: - {{ error }} -

+
-
+

-

- Error: - {{ error }} -

+
- +
@@ -984,10 +853,7 @@

Owner/operator declaration

-

- Error: - {{ error }} -

+
Owner/operator declaration

New certificate required

-

- Error: - {{ error }} -

+
Will not be present on the ADR certificate
-

- Error: - {{ error }} -

- -
- You have {{ 1024 - (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_additionalExaminerNotes_note')?.value?.length ?? 1024) - 1024 }} too many -
+ + +
@@ -1116,36 +946,9 @@

Additional Examiner Notes History

-

- Error: - {{ error }} -

- -
- You have {{ 1500 - (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 0) }} characters remaining -
-
- You have {{ (form.get('techRecord_adrDetails_adrCertificateNotes')?.value?.length ?? 1500) - 1500 }} too many -
+ + +
diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts index f471a1fd9d..809c0ab3b6 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts @@ -15,376 +15,447 @@ import { techRecord } from '@store/technical-records'; import { ReplaySubject, take } from 'rxjs'; @Component({ - selector: 'app-adr-section-edit', - templateUrl: './adr-section-edit.component.html', - styleUrls: ['./adr-section-edit.component.scss'], + selector: 'app-adr-section-edit', + templateUrl: './adr-section-edit.component.html', + styleUrls: ['./adr-section-edit.component.scss'], }) export class AdrSectionEditComponent implements OnInit, OnDestroy { - fb = inject(FormBuilder); - store = inject(Store); - adrService = inject(AdrService); - controlContainer = inject(ControlContainer); - - destroy$ = new ReplaySubject(1); - - form = this.fb.group( - { - techRecord_adrDetails_dangerousGoods: this.fb.control(false), - - // Applicant Details - techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), - techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [this.maxLength(150, 'Street')]), - techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), - techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), - techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [this.maxLength(25, 'Postcode')]), - - // ADR Details - techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [this.requiredWithDangerousGoods('ADR body type')]), - techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null, [ - this.requiredWithDangerousGoods('Used on international journeys'), - ]), - techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null), - techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ - this.requiredWithDangerousGoods('Permitted dangerous goods'), - ]), - techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [this.requiredWithExplosives('Compatibility Group J')]), - techRecord_adrDetails_additionalNotes_number: this.fb.control([], [this.requiredWithDangerousGoods('Guidance notes')]), - techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [this.maxLength(40, 'ADR type approval number')]), - - // Tank Details - techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank Make'), - this.maxLength(70, 'Tank Make'), - ]), - techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank Year of manufacture'), - this.maxLength(70, 'Tank Year of manufacture'), - ]), - techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('Manufacturer serial number'), - this.maxLength(70, 'Manufacturer serial number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('Tank type approval number'), - this.maxLength(65, 'Tank type approval number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ - this.requiredWithTankOrBattery('Code'), - this.maxLength(30, 'Code'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ - this.requiredWithTankOrBattery('Substances permitted'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control(null, this.requiredWithTankStatement('Select')), - techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ - this.requiredWithUNNumber('Reference number'), - this.maxLength(1500, 'Reference number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: this.fb.control(null, [ - this.maxLength(1500, 'Reference number'), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array([ - this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]), - ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, []), - techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [this.maxLength(1500, 'Special provisions')]), - - // Tank Details > Tank Inspections - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control(null, [ - this.requiredWithTankOrBattery('TC2: Certificate Number'), - this.maxLength(70, 'TC2: Certificate Number'), - ]), - techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control(null, [ - this.requiredWithTankOrBattery('TC2: Expiry Date'), - ]), - techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), - - // Miscellaneous - techRecord_adrDetails_memosApply: this.fb.control(null), - techRecord_adrDetails_m145Statement: this.fb.control(null), - - // Battery List - techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [this.requiredWithBattery('Battery list applicable')]), - techRecord_adrDetails_batteryListNumber: this.fb.control(null, [this.requiredWithBatteryListApplicable('Reference Number')]), - - // Brake declaration - techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), - techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null, [this.maxLength(500, 'Issuer')]), - techRecord_adrDetails_brakeEndurance: this.fb.control(false), - techRecord_adrDetails_weight: this.fb.control(null, [ - this.max(99999999, 'Weight (tonnes)'), - this.requiredWithBrakeEndurance('Weight (tonnes)'), - this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), - ]), - - // Other declarations - techRecord_adrDetails_declarationsSeen: this.fb.control(false), - - // Miscellaneous - techRecord_adrDetails_newCertificateRequested: this.fb.control(false), - techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), - techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), - techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [this.maxLength(1500, 'ADR Certificate Notes')]), - }, - { validators: [this.requiresReferenceNumberOrUNNumber()] } - ); - - // Option lists - dangerousGoodsOptions = [ - { label: 'Yes', value: true }, - { label: 'No', value: false }, - ]; - - adrBodyTypesOptions = getOptionsFromEnum(ADRBodyType); - - usedOnInternationJourneysOptions = [ - { value: 'yes', label: 'Yes' }, - { value: 'no', label: 'No' }, - { value: 'n/a', label: 'Not applicable' }, - ]; - - permittedDangerousGoodsOptions = getOptionsFromEnum(ADRDangerousGood); - - guidanceNotesOptions = getOptionsFromEnum(ADRAdditionalNotesNumber); - - compatibilityGroupJOptions = [ - { label: 'Yes', value: true }, - { label: 'No', value: false }, - ]; - - tankStatementSubstancePermittedOptions = getOptionsFromEnum(ADRTankStatementSubstancePermitted); - - tankStatementSelectOptions = getOptionsFromEnum(ADRTankDetailsTankStatementSelect); - - batteryListApplicableOptions = [ - { value: true, label: 'Yes' }, - { value: false, label: 'No' }, - ]; - - tc3InspectionOptions = getOptionsFromEnum(TC3Types); - - isInvalid(formControlName: string) { - const control = this.form.get(formControlName); - return control?.invalid && control?.touched; - } - - toggle(formControlName: string, value: string) { - const control = this.form.get(formControlName); - if (!control) return; - - // If this is the first checkbox, set the value to an array - if (control.value === null) { - return control.setValue([value]); - } - - // If the value is already an array, toggle the value - if the array is then empty, set the value to null - if (Array.isArray(control.value)) { - control.value.includes(value) ? control.value.splice(control.value.indexOf(value), 1) : control.value.push(value); - if (control.value.length === 0) { - control.setValue(null); - } - } - } - - ngOnInit(): void { - // Attatch all form controls to parent - const parent = this.controlContainer.control; - if (parent instanceof FormGroup) { - Object.entries(this.form.controls).forEach(([key, control]) => parent.addControl(key, control)); - } - - this.store - .select(techRecord) - .pipe(take(1)) - .subscribe((techRecord) => { - if (techRecord) this.form.patchValue(techRecord as any); - }); - } - - ngOnDestroy(): void { - // Detatch all form controls from parent - const parent = this.controlContainer.control; - if (parent instanceof FormGroup) { - Object.keys(this.form.controls).forEach((key) => parent.removeControl(key)); - } - - // Clear subscriptions - this.destroy$.next(true); - this.destroy$.complete(); - } - - // Custom validators - requiredWithDangerousGoods(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayDangerousGoodsSection(control.parent.value)) { - return { required: `${label} is required when dangerous goods are present` }; - } - - return null; - }; - } - - requiredWithExplosives(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayCompatibilityGroupJSection(control.parent.value)) { - return { required: `${label} is required when Explosives Type 2 or Explosive Type 3` }; - } - - return null; - }; - } - - requiredWithBattery(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayBatterySection(control.parent.value)) { - return { required: `${label} is required when ADR body type is of type 'battery'` }; - } - - return null; - }; - } - - requiredWithTankOrBattery(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayTankOrBatterySection(control.parent.value)) { - return { required: `${label} is required when ADR body type is of type 'tank' or 'battery'` }; - } - - return null; - }; - } - - requiredWithTankStatement(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayTankStatementSelectSection(control.parent.value)) { - return { required: `${label} is required with substances permitted` }; - } - - return null; - }; - } - - requiredWithUNNumber(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayTankStatementStatementSection(control.parent.value)) { - return { required: `${label} is required when under UN number` }; - } - - return null; - }; - } - - requiredWithBrakeEndurance(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayWeightSection(control.parent.value)) { - return { required: `${label} is required when brake endurance is checked` }; - } - - return null; - }; - } - - requiredWithBatteryListApplicable(label: string): ValidatorFn { - return (control) => { - if (control.parent && !control.value && this.adrService.canDisplayBatteryListNumberSection(control.parent.value)) { - return { required: `${label} is required when battery list is applicable` }; - } - - return null; - }; - } - - requiredWithTankStatementProductList(label: string): ValidatorFn { - return (control) => { - const visible = control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value); - if (visible && !control.value) { - return { required: `${label} is required when under product list` }; - } - - return null; - }; - } - - requiresReferenceNumberOrUNNumber(): ValidatorFn { - return (control) => { - const referenceNumber = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_statement')?.value; - const unNumbers = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo')?.value; - const visible = control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value); - const unNumberPopulated = Array.isArray(unNumbers) && unNumbers.some((un) => un !== null); - - if (visible && (!referenceNumber || !unNumberPopulated)) { - return { required: 'Either reference number or UN number is required' }; - } - - return null; - }; - } - - max(size: number, label: string): ValidatorFn { - return (control) => { - if (control.value && control.value > size) { - return { max: `${label} must be less than or equal to ${size}` }; - } - - return null; - }; - } - - maxLength(length: number, label: string): ValidatorFn { - return (control) => { - if (control.value && control.value.length > length) { - return { maxLength: `${label} must be less than or equal to ${length} characters` }; - } - - return null; - }; - } - - pattern(pattern: string | RegExp, label: string): ValidatorFn { - return (control) => { - if (control.value && !new RegExp(pattern).test(control.value)) { - return { pattern: `${label} is invalid` }; - } - - return null; - }; - } - - // Dynamically add/remove controls - addTC3TankInspection() { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); - if (formArray instanceof FormArray) { - formArray.push( - this.fb.group({ - tc3Type: this.fb.control(null), - tc3PeriodicNumber: this.fb.control(null), - tc3PeriodicExpiryDate: this.fb.control(null), - }) - ); - } - } - - removeTC3TankInspection(index: number) { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); - if (formArray instanceof FormArray) { - formArray.removeAt(index); - } - } - - addUNNumber() { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); - if (formArray instanceof FormArray) { - formArray.push( - this.fb.control(null, [this.requiredWithTankStatementProductList('UN Number'), this.maxLength(1500, 'UN number')]) - ); - } - } - - removeUNNumber(index: number) { - const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); - if (formArray instanceof FormArray) { - formArray.removeAt(index); - } - } + fb = inject(FormBuilder); + store = inject(Store); + adrService = inject(AdrService); + controlContainer = inject(ControlContainer); + + destroy$ = new ReplaySubject(1); + + form = this.fb.group( + { + techRecord_adrDetails_dangerousGoods: this.fb.control(false), + + // Applicant Details + techRecord_adrDetails_applicantDetails_name: this.fb.control(null, [this.maxLength(150, 'Name')]), + techRecord_adrDetails_applicantDetails_street: this.fb.control(null, [ + this.maxLength(150, 'Street'), + ]), + techRecord_adrDetails_applicantDetails_town: this.fb.control(null, [this.maxLength(100, 'Town')]), + techRecord_adrDetails_applicantDetails_city: this.fb.control(null, [this.maxLength(100, 'City')]), + techRecord_adrDetails_applicantDetails_postcode: this.fb.control(null, [ + this.maxLength(25, 'Postcode'), + ]), + + // ADR Details + techRecord_adrDetails_vehicleDetails_type: this.fb.control(null, [ + this.requiredWithDangerousGoods('ADR body type'), + ]), + techRecord_adrDetails_vehicleDetails_usedOnInternationalJourneys: this.fb.control(null), + techRecord_adrDetails_vehicleDetails_approvalDate: this.fb.control(null, [ + //this.invalidDate('Date processed'), + this.pastDate('Date processed'), + this.requiredWithDangerousGoods('Date processed'), + ]), + techRecord_adrDetails_permittedDangerousGoods: this.fb.control(null, [ + this.requiredWithDangerousGoods('Permitted dangerous goods'), + ]), + techRecord_adrDetails_compatibilityGroupJ: this.fb.control(null, [ + this.requiredWithExplosives('Compatibility Group J'), + ]), + techRecord_adrDetails_additionalNotes_number: this.fb.control( + [], + [this.requiredWithDangerousGoods('Guidance notes')] + ), + techRecord_adrDetails_adrTypeApprovalNo: this.fb.control(null, [ + this.maxLength(40, 'ADR type approval number'), + ]), + + // Tank Details + techRecord_adrDetails_tank_tankDetails_tankManufacturer: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Make'), + this.maxLength(70, 'Tank Make'), + ]), + techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank Year of manufacture'), + this.maxLength(70, 'Tank Year of manufacture'), + ]), + techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Manufacturer serial number'), + this.maxLength(70, 'Manufacturer serial number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankTypeAppNo: this.fb.control(null, [ + this.requiredWithTankOrBattery('Tank type approval number'), + this.maxLength(65, 'Tank type approval number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankCode: this.fb.control(null, [ + this.requiredWithTankOrBattery('Code'), + this.maxLength(30, 'Code'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: this.fb.control(null, [ + this.requiredWithTankOrBattery('Substances permitted'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_select: this.fb.control( + null, + this.requiredWithTankStatement('Select') + ), + techRecord_adrDetails_tank_tankDetails_tankStatement_statement: this.fb.control(null, [ + this.requiredWithUNNumber('Reference number'), + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: this.fb.control(null, [ + this.maxLength(1500, 'Reference number'), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array([ + this.fb.control(null, [ + this.requiredWithTankStatementProductList('UN Number'), + this.maxLength(1500, 'UN number'), + ]), + ]), + techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, []), + techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [ + this.maxLength(1500, 'Special provisions'), + ]), + + // Tank Details > Tank Inspections + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2Type: this.fb.control(TC2Types.INITIAL), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateApprovalNo: this.fb.control( + null, + [this.requiredWithTankOrBattery('TC2: Certificate Number'), this.maxLength(70, 'TC2: Certificate Number')] + ), + techRecord_adrDetails_tank_tankDetails_tc2Details_tc2IntermediateExpiryDate: this.fb.control( + null, + [this.requiredWithTankOrBattery('TC2: Expiry Date')] + ), + techRecord_adrDetails_tank_tankDetails_tc3Details: this.fb.array([]), + + // Miscellaneous + techRecord_adrDetails_memosApply: this.fb.control(null), + techRecord_adrDetails_m145Statement: this.fb.control(null), + + // Battery List + techRecord_adrDetails_listStatementApplicable: this.fb.control(null, [ + this.requiredWithBattery('Battery list applicable'), + ]), + techRecord_adrDetails_batteryListNumber: this.fb.control(null, [ + this.requiredWithBatteryListApplicable('Reference Number'), + ]), + + // Brake declaration + techRecord_adrDetails_brakeDeclarationsSeen: this.fb.control(false), + techRecord_adrDetails_brakeDeclarationIssuer: this.fb.control(null, [ + this.maxLength(500, 'Issuer'), + ]), + techRecord_adrDetails_brakeEndurance: this.fb.control(false), + techRecord_adrDetails_weight: this.fb.control(null, [ + this.max(99999999, 'Weight (tonnes)'), + this.requiredWithBrakeEndurance('Weight (tonnes)'), + this.pattern('^\\d*(\\.\\d{0,2})?$', 'Weight (tonnes)'), + ]), + + // Other declarations + techRecord_adrDetails_declarationsSeen: this.fb.control(false), + + // Miscellaneous + techRecord_adrDetails_newCertificateRequested: this.fb.control(false), + techRecord_adrDetails_additionalExaminerNotes_note: this.fb.control(null), + techRecord_adrDetails_additionalExaminerNotes: this.fb.control(null), + techRecord_adrDetails_adrCertificateNotes: this.fb.control(null, [ + this.maxLength(1500, 'ADR Certificate Notes'), + ]), + }, + { validators: [this.requiresReferenceNumberOrUNNumber()] } + ); + + // Option lists + dangerousGoodsOptions = [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ]; + + adrBodyTypesOptions = getOptionsFromEnum(ADRBodyType); + + usedOnInternationJourneysOptions = [ + { value: 'yes', label: 'Yes' }, + { value: 'no', label: 'No' }, + { value: 'n/a', label: 'Not applicable' }, + ]; + + permittedDangerousGoodsOptions = getOptionsFromEnum(ADRDangerousGood); + + guidanceNotesOptions = getOptionsFromEnum(ADRAdditionalNotesNumber); + + compatibilityGroupJOptions = [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ]; + + tankStatementSubstancePermittedOptions = getOptionsFromEnum(ADRTankStatementSubstancePermitted); + + tankStatementSelectOptions = getOptionsFromEnum(ADRTankDetailsTankStatementSelect); + + batteryListApplicableOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ]; + + tc3InspectionOptions = getOptionsFromEnum(TC3Types); + + isInvalid(formControlName: string) { + const control = this.form.get(formControlName); + return control?.invalid && control?.touched; + } + + toggle(formControlName: string, value: string) { + const control = this.form.get(formControlName); + if (!control) return; + + // If this is the first checkbox, set the value to an array + if (control.value === null) { + return control.setValue([value]); + } + + // If the value is already an array, toggle the value - if the array is then empty, set the value to null + if (Array.isArray(control.value)) { + control.value.includes(value) ? control.value.splice(control.value.indexOf(value), 1) : control.value.push(value); + if (control.value.length === 0) { + control.setValue(null); + } + } + } + + ngOnInit(): void { + // Attatch all form controls to parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + Object.entries(this.form.controls).forEach(([key, control]) => parent.addControl(key, control)); + } + + this.store + .select(techRecord) + .pipe(take(1)) + .subscribe((techRecord) => { + if (techRecord) this.form.patchValue(techRecord as any); + }); + } + + ngOnDestroy(): void { + // Detatch all form controls from parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + Object.keys(this.form.controls).forEach((key) => parent.removeControl(key)); + } + + // Clear subscriptions + this.destroy$.next(true); + this.destroy$.complete(); + } + + // Custom validators + requiredWithDangerousGoods(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayDangerousGoodsSection(control.parent.value)) { + return { required: `${label} is required when dangerous goods are present` }; + } + + return null; + }; + } + + requiredWithExplosives(label: string): ValidatorFn { + return (control) => { + if ( + control.parent && + !control.value && + this.adrService.canDisplayCompatibilityGroupJSection(control.parent.value) + ) { + return { required: `${label} is required when Explosives Type 2 or Explosive Type 3` }; + } + + return null; + }; + } + + requiredWithBattery(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayBatterySection(control.parent.value)) { + return { required: `${label} is required when ADR body type is of type 'battery'` }; + } + + return null; + }; + } + + requiredWithTankOrBattery(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayTankOrBatterySection(control.parent.value)) { + return { required: `${label} is required when ADR body type is of type 'tank' or 'battery'` }; + } + + return null; + }; + } + + requiredWithTankStatement(label: string): ValidatorFn { + return (control) => { + if ( + control.parent && + !control.value && + this.adrService.canDisplayTankStatementSelectSection(control.parent.value) + ) { + return { required: `${label} is required with substances permitted` }; + } + + return null; + }; + } + + requiredWithUNNumber(label: string): ValidatorFn { + return (control) => { + if ( + control.parent && + !control.value && + this.adrService.canDisplayTankStatementStatementSection(control.parent.value) + ) { + return { required: `${label} is required when under UN number` }; + } + + return null; + }; + } + + requiredWithBrakeEndurance(label: string): ValidatorFn { + return (control) => { + if (control.parent && !control.value && this.adrService.canDisplayWeightSection(control.parent.value)) { + return { required: `${label} is required when brake endurance is checked` }; + } + + return null; + }; + } + + requiredWithBatteryListApplicable(label: string): ValidatorFn { + return (control) => { + if ( + control.parent && + !control.value && + this.adrService.canDisplayBatteryListNumberSection(control.parent.value) + ) { + return { required: `${label} is required when battery list is applicable` }; + } + + return null; + }; + } + + requiredWithTankStatementProductList(label: string): ValidatorFn { + return (control) => { + const visible = control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value); + if (visible && !control.value) { + return { required: `${label} is required when under product list` }; + } + + return null; + }; + } + + requiresReferenceNumberOrUNNumber(): ValidatorFn { + return (control) => { + const referenceNumber = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_statement')?.value; + const unNumbers = control.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo')?.value; + const visible = control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value); + const unNumberPopulated = Array.isArray(unNumbers) && unNumbers.some((un) => un !== null); + + if (visible && (!referenceNumber || !unNumberPopulated)) { + return { required: 'Either reference number or UN number is required' }; + } + + return null; + }; + } + + max(size: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value > size) { + return { max: `${label} must be less than or equal to ${size}` }; + } + + return null; + }; + } + + maxLength(length: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value.length > length) { + return { maxLength: `${label} must be less than or equal to ${length} characters` }; + } + + return null; + }; + } + + pattern(pattern: string | RegExp, label: string): ValidatorFn { + return (control) => { + if (control.value && !new RegExp(pattern).test(control.value)) { + return { pattern: `${label} is invalid` }; + } + + return null; + }; + } + + pastDate(label: string): ValidatorFn { + return (control) => { + if (control.value && new Date(control.value) > new Date()) { + return { pastDate: `${label} must be in the past` }; + } + + return null; + }; + } + + invalidDate(label: string): ValidatorFn { + return (control) => { + if (control.value && Number.isNaN(Date.parse(control.value))) { + return { invalidDate: `${label} is not a valid date` }; + } + + return null; + }; + } + + // Dynamically add/remove controls + addTC3TankInspection() { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); + if (formArray instanceof FormArray) { + formArray.push( + this.fb.group({ + tc3Type: this.fb.control(null), + tc3PeriodicNumber: this.fb.control(null), + tc3PeriodicExpiryDate: this.fb.control(null), + }) + ); + } + } + + removeTC3TankInspection(index: number) { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tc3Details'); + if (formArray instanceof FormArray) { + formArray.removeAt(index); + } + } + + addUNNumber() { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (formArray instanceof FormArray) { + formArray.push( + this.fb.control(null, [ + this.requiredWithTankStatementProductList('UN Number'), + this.maxLength(1500, 'UN number'), + ]) + ); + } + } + + removeUNNumber(index: number) { + const formArray = this.form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (formArray instanceof FormArray) { + formArray.removeAt(index); + } + } } diff --git a/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts index 1f77773e70..83e21a7501 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section-summary/adr-section-summary.component.ts @@ -5,21 +5,21 @@ import { AdrService } from '@services/adr/adr.service'; import { editingTechRecord, techRecord } from '@store/technical-records'; @Component({ - selector: 'app-adr-section-summary', - templateUrl: './adr-section-summary.component.html', - styleUrls: ['./adr-section-summary.component.scss'], + selector: 'app-adr-section-summary', + templateUrl: './adr-section-summary.component.html', + styleUrls: ['./adr-section-summary.component.scss'], }) export class AdrSectionSummaryComponent { - store = inject(Store); - adrService = inject(AdrService); + store = inject(Store); + adrService = inject(AdrService); - currentTechRecord = this.store.selectSignal(techRecord); - amendedTechRecord = this.store.selectSignal(editingTechRecord); + currentTechRecord = this.store.selectSignal(techRecord); + amendedTechRecord = this.store.selectSignal(editingTechRecord); - hasChanged(property: keyof TechRecordType<'hgv' | 'trl' | 'lgv'>) { - const current = this.currentTechRecord() as TechRecordType<'hgv' | 'trl' | 'lgv'>; - const amended = this.amendedTechRecord() as TechRecordType<'hgv' | 'trl' | 'lgv'>; - if (!current || !amended) return true; - return current[property] !== amended[property]; - } + hasChanged(property: keyof TechRecordType<'hgv' | 'trl' | 'lgv'>) { + const current = this.currentTechRecord() as TechRecordType<'hgv' | 'trl' | 'lgv'>; + const amended = this.amendedTechRecord() as TechRecordType<'hgv' | 'trl' | 'lgv'>; + if (!current || !amended) return true; + return current[property] !== amended[property]; + } } diff --git a/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts index 9f71c851c0..8b8131bf5a 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section-view/adr-section-view.component.ts @@ -4,13 +4,13 @@ import { AdrService } from '@services/adr/adr.service'; import { techRecord } from '@store/technical-records'; @Component({ - selector: 'app-adr-section-view', - templateUrl: './adr-section-view.component.html', - styleUrls: ['./adr-section-view.component.scss'], + selector: 'app-adr-section-view', + templateUrl: './adr-section-view.component.html', + styleUrls: ['./adr-section-view.component.scss'], }) export class AdrSectionViewComponent { - store = inject(Store); - adrService = inject(AdrService); + store = inject(Store); + adrService = inject(AdrService); - techRecord = this.store.selectSignal(techRecord); + techRecord = this.store.selectSignal(techRecord); } diff --git a/src/app/forms/custom-sections/adr-section/adr-section.component.ts b/src/app/forms/custom-sections/adr-section/adr-section.component.ts index d69ddffaba..5870d1633b 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section.component.ts @@ -1,12 +1,12 @@ import { Component, input } from '@angular/core'; @Component({ - selector: 'app-adr-section', - templateUrl: './adr-section.component.html', - styleUrls: ['./adr-section.component.scss'], + selector: 'app-adr-section', + templateUrl: './adr-section.component.html', + styleUrls: ['./adr-section.component.scss'], }) export class AdrSectionComponent { - mode = input('edit'); + mode = input('edit'); } type Mode = 'view' | 'edit' | 'summary'; diff --git a/src/app/forms/dynamic-forms.module.ts b/src/app/forms/dynamic-forms.module.ts index a1a1d44adf..b317789209 100644 --- a/src/app/forms/dynamic-forms.module.ts +++ b/src/app/forms/dynamic-forms.module.ts @@ -18,9 +18,12 @@ import { SharedModule } from '@shared/shared.module'; import { TruncatePipe } from '../pipes/truncate/truncate.pipe'; import { AutocompleteComponent } from './components/autocomplete/autocomplete.component'; import { BaseControlComponent } from './components/base-control/base-control.component'; +import { CharacterCountComponent } from './components/character-count/character-count.component'; import { CheckboxGroupComponent } from './components/checkbox-group/checkbox-group.component'; import { CheckboxComponent } from './components/checkbox/checkbox.component'; import { ContingencyAdrGenerateCertComponent } from './components/contingency-adr-generate-cert/contingency-adr-generate-cert.component'; +import { ControlErrorsComponent } from './components/control-errors/control-errors.component'; +import { DateControlsComponent } from './components/date-controls/date-controls.component'; import { DateComponent } from './components/date/date.component'; import { DefectSelectComponent } from './components/defect-select/defect-select.component'; import { DynamicFormFieldComponent } from './components/dynamic-form-field/dynamic-form-field.component'; @@ -70,127 +73,133 @@ import { TyresComponent } from './custom-sections/tyres/tyres.component'; import { WeightsComponent } from './custom-sections/weights/weights.component'; @NgModule({ - declarations: [ - BaseControlComponent, - TextInputComponent, - ViewListItemComponent, - DynamicFormGroupComponent, - ViewCombinationComponent, - CheckboxGroupComponent, - RadioGroupComponent, - DefectComponent, - DefectsComponent, - AutocompleteComponent, - NumberInputComponent, - TextAreaComponent, - NumberOnlyDirective, - ToUppercaseDirective, - NoSpaceDirective, - TrimWhitespaceDirective, - DateComponent, - SelectComponent, - DynamicFormFieldComponent, - FieldErrorMessageComponent, - DefectSelectComponent, - RequiredStandardSelectComponent, - DateFocusNextDirective, - TruncatePipe, - WeightsComponent, - LettersComponent, - PlatesComponent, - DimensionsComponent, - TrlBrakesComponent, - ReadOnlyComponent, - CustomDefectsComponent, - RequiredStandardComponent, - RequiredStandardsComponent, - CustomDefectComponent, - SwitchableInputComponent, - ReadOnlyComponent, - SuffixDirective, - AbandonDialogComponent, - BodyComponent, - TyresComponent, - PsvBrakesComponent, - PrefixDirective, - SuggestiveInputComponent, - CheckboxComponent, - ApprovalTypeComponent, - ApprovalTypeInputComponent, - ApprovalTypeFocusNextDirective, - ModifiedWeightsComponent, - FieldWarningMessageComponent, - AdrComponent, - AdrTankDetailsSubsequentInspectionsEditComponent, - AdrTankStatementUnNumberEditComponent, - CustomFormControlComponent, - AdrExaminerNotesHistoryEditComponent, - AdrExaminerNotesHistoryViewComponent, - AdrTankDetailsSubsequentInspectionsViewComponent, - AdrTankDetailsInitialInspectionViewComponent, - AdrTankStatementUnNumberViewComponent, - AdrCertificateHistoryComponent, - AdrTankDetailsM145ViewComponent, - ContingencyAdrGenerateCertComponent, - AdrNewCertificateRequiredViewComponent, - AdrSectionComponent, - AdrSectionEditComponent, - AdrSectionViewComponent, - AdrSectionSummaryComponent, - ], - imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], - exports: [ - TextInputComponent, - ViewListItemComponent, - DynamicFormGroupComponent, - ViewCombinationComponent, - CheckboxGroupComponent, - RadioGroupComponent, - DefectComponent, - DefectsComponent, - AutocompleteComponent, - NumberInputComponent, - TextAreaComponent, - DateComponent, - SelectComponent, - DynamicFormFieldComponent, - FieldErrorMessageComponent, - DefectSelectComponent, - RequiredStandardSelectComponent, - WeightsComponent, - LettersComponent, - PlatesComponent, - TyresComponent, - DimensionsComponent, - TrlBrakesComponent, - ReadOnlyComponent, - RequiredStandardComponent, - RequiredStandardsComponent, - CustomDefectsComponent, - CustomDefectComponent, - SwitchableInputComponent, - SuffixDirective, - ReadOnlyComponent, - AbandonDialogComponent, - BodyComponent, - PsvBrakesComponent, - PrefixDirective, - SuggestiveInputComponent, - CheckboxComponent, - ToUppercaseDirective, - NoSpaceDirective, - TrimWhitespaceDirective, - ApprovalTypeComponent, - ApprovalTypeInputComponent, - ApprovalTypeFocusNextDirective, - ModifiedWeightsComponent, - AdrComponent, - AdrCertificateHistoryComponent, - FieldWarningMessageComponent, - AdrSectionComponent, - AdrSectionEditComponent, - AdrSectionViewComponent, - AdrSectionSummaryComponent, - ], + declarations: [ + BaseControlComponent, + TextInputComponent, + ViewListItemComponent, + DynamicFormGroupComponent, + ViewCombinationComponent, + CheckboxGroupComponent, + RadioGroupComponent, + DefectComponent, + DefectsComponent, + AutocompleteComponent, + NumberInputComponent, + TextAreaComponent, + NumberOnlyDirective, + ToUppercaseDirective, + NoSpaceDirective, + TrimWhitespaceDirective, + DateComponent, + DateControlsComponent, + CharacterCountComponent, + ControlErrorsComponent, + SelectComponent, + DynamicFormFieldComponent, + FieldErrorMessageComponent, + DefectSelectComponent, + RequiredStandardSelectComponent, + DateFocusNextDirective, + TruncatePipe, + WeightsComponent, + LettersComponent, + PlatesComponent, + DimensionsComponent, + TrlBrakesComponent, + ReadOnlyComponent, + CustomDefectsComponent, + RequiredStandardComponent, + RequiredStandardsComponent, + CustomDefectComponent, + SwitchableInputComponent, + ReadOnlyComponent, + SuffixDirective, + AbandonDialogComponent, + BodyComponent, + TyresComponent, + PsvBrakesComponent, + PrefixDirective, + SuggestiveInputComponent, + CheckboxComponent, + ApprovalTypeComponent, + ApprovalTypeInputComponent, + ApprovalTypeFocusNextDirective, + ModifiedWeightsComponent, + FieldWarningMessageComponent, + AdrComponent, + AdrTankDetailsSubsequentInspectionsEditComponent, + AdrTankStatementUnNumberEditComponent, + CustomFormControlComponent, + AdrExaminerNotesHistoryEditComponent, + AdrExaminerNotesHistoryViewComponent, + AdrTankDetailsSubsequentInspectionsViewComponent, + AdrTankDetailsInitialInspectionViewComponent, + AdrTankStatementUnNumberViewComponent, + AdrCertificateHistoryComponent, + AdrTankDetailsM145ViewComponent, + ContingencyAdrGenerateCertComponent, + AdrNewCertificateRequiredViewComponent, + AdrSectionComponent, + AdrSectionEditComponent, + AdrSectionViewComponent, + AdrSectionSummaryComponent, + ], + imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], + exports: [ + TextInputComponent, + ViewListItemComponent, + DynamicFormGroupComponent, + ViewCombinationComponent, + CheckboxGroupComponent, + RadioGroupComponent, + DefectComponent, + DefectsComponent, + AutocompleteComponent, + NumberInputComponent, + TextAreaComponent, + DateComponent, + DateControlsComponent, + CharacterCountComponent, + ControlErrorsComponent, + SelectComponent, + DynamicFormFieldComponent, + FieldErrorMessageComponent, + DefectSelectComponent, + RequiredStandardSelectComponent, + WeightsComponent, + LettersComponent, + PlatesComponent, + TyresComponent, + DimensionsComponent, + TrlBrakesComponent, + ReadOnlyComponent, + RequiredStandardComponent, + RequiredStandardsComponent, + CustomDefectsComponent, + CustomDefectComponent, + SwitchableInputComponent, + SuffixDirective, + ReadOnlyComponent, + AbandonDialogComponent, + BodyComponent, + PsvBrakesComponent, + PrefixDirective, + SuggestiveInputComponent, + CheckboxComponent, + ToUppercaseDirective, + NoSpaceDirective, + TrimWhitespaceDirective, + ApprovalTypeComponent, + ApprovalTypeInputComponent, + ApprovalTypeFocusNextDirective, + ModifiedWeightsComponent, + AdrComponent, + AdrCertificateHistoryComponent, + FieldWarningMessageComponent, + AdrSectionComponent, + AdrSectionEditComponent, + AdrSectionViewComponent, + AdrSectionSummaryComponent, + ], }) export class DynamicFormsModule {} diff --git a/src/app/services/adr/adr.service.ts b/src/app/services/adr/adr.service.ts index c30a680725..df5de53c60 100644 --- a/src/app/services/adr/adr.service.ts +++ b/src/app/services/adr/adr.service.ts @@ -5,98 +5,101 @@ import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/t import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class AdrService { - determineTankStatementSelect(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const { - techRecord_adrDetails_tank_tankDetails_tankStatement_statement: statement, - techRecord_adrDetails_tank_tankDetails_tankStatement_productList: productList, - techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: productListUnNo, - techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: productListRefNo, - } = techRecord; - - if (statement) return ADRTankDetailsTankStatementSelect.STATEMENT; - if (productList || productListRefNo || (productListUnNo && productListUnNo.length > 0)) return ADRTankDetailsTankStatementSelect.PRODUCT_LIST; - - return null; - } - - carriesDangerousGoods(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - return ( - techRecord.techRecord_adrDetails_dangerousGoods || - (techRecord.techRecord_adrDetails_dangerousGoods !== false && - Boolean( - Object.keys(techRecord).find( - (key) => - key !== 'techRecord_adrDetails_dangerousGoods' && - key.includes('adrDetails') && - techRecord[key as keyof TechRecordType<'hgv' | 'lgv' | 'trl'>] != null - ) - )) - ); - } - - containsExplosives(arr: string[]) { - return arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_2) || arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_3); - } - - containsTankOrBattery(bodyType: string) { - return bodyType.toLowerCase().includes('tank') || bodyType.toLowerCase().includes('battery'); - } - - canDisplayDangerousGoodsSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const dangerousGoods = techRecord.techRecord_adrDetails_dangerousGoods; - return dangerousGoods === true; - } - - canDisplayCompatibilityGroupJSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const permittedDangerousGoods = techRecord.techRecord_adrDetails_permittedDangerousGoods; - const containsExplosives = Array.isArray(permittedDangerousGoods) && this.containsExplosives(permittedDangerousGoods); - return this.canDisplayDangerousGoodsSection(techRecord) && containsExplosives; - } - - canDisplayBatterySection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const adrBodyType = techRecord.techRecord_adrDetails_vehicleDetails_type; - return typeof adrBodyType === 'string' && adrBodyType.toLowerCase().includes('battery'); - } - - canDisplayTankOrBatterySection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const adrBodyType = techRecord.techRecord_adrDetails_vehicleDetails_type; - const containsTankOrBattery = typeof adrBodyType === 'string' && this.containsTankOrBattery(adrBodyType); - return this.canDisplayDangerousGoodsSection(techRecord) && containsTankOrBattery; - } - - canDisplayTankStatementSelectSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const tankStatementSubstancesPermitted = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted; - const underUNNumber = tankStatementSubstancesPermitted === ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER; - return this.canDisplayTankOrBatterySection(techRecord) && underUNNumber; - } - - canDisplayTankStatementStatementSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const tankStatementSelect = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_select; - const underStatement = tankStatementSelect === ADRTankDetailsTankStatementSelect.STATEMENT; - return this.canDisplayTankStatementSelectSection(techRecord) && underStatement; - } - - canDisplayTankStatementProductListSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const tankStatementSelect = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_select; - const underProductList = tankStatementSelect === ADRTankDetailsTankStatementSelect.PRODUCT_LIST; - return this.canDisplayTankStatementSelectSection(techRecord) && underProductList; - } - - canDisplayWeightSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const brakeEndurance = techRecord.techRecord_adrDetails_brakeEndurance; - return brakeEndurance === true; - } - - canDisplayBatteryListNumberSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const batteryListApplicable = techRecord.techRecord_adrDetails_listStatementApplicable; - return this.canDisplayBatterySection(techRecord) && batteryListApplicable === true; - } - - canDisplayIssueSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { - const brakeDeclarationsSeen = techRecord.techRecord_adrDetails_brakeDeclarationsSeen; - return brakeDeclarationsSeen === true; - } + determineTankStatementSelect(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const { + techRecord_adrDetails_tank_tankDetails_tankStatement_statement: statement, + techRecord_adrDetails_tank_tankDetails_tankStatement_productList: productList, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: productListUnNo, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: productListRefNo, + } = techRecord; + + if (statement) return ADRTankDetailsTankStatementSelect.STATEMENT; + if (productList || productListRefNo || (productListUnNo && productListUnNo.length > 0)) + return ADRTankDetailsTankStatementSelect.PRODUCT_LIST; + + return null; + } + + carriesDangerousGoods(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + return ( + techRecord.techRecord_adrDetails_dangerousGoods || + (techRecord.techRecord_adrDetails_dangerousGoods !== false && + Boolean( + Object.keys(techRecord).find( + (key) => + key !== 'techRecord_adrDetails_dangerousGoods' && + key.includes('adrDetails') && + techRecord[key as keyof TechRecordType<'hgv' | 'lgv' | 'trl'>] != null + ) + )) + ); + } + + containsExplosives(arr: string[]) { + return arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_2) || arr.includes(ADRDangerousGood.EXPLOSIVES_TYPE_3); + } + + containsTankOrBattery(bodyType: string) { + return bodyType.toLowerCase().includes('tank') || bodyType.toLowerCase().includes('battery'); + } + + canDisplayDangerousGoodsSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const dangerousGoods = techRecord.techRecord_adrDetails_dangerousGoods; + return dangerousGoods === true; + } + + canDisplayCompatibilityGroupJSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const permittedDangerousGoods = techRecord.techRecord_adrDetails_permittedDangerousGoods; + const containsExplosives = + Array.isArray(permittedDangerousGoods) && this.containsExplosives(permittedDangerousGoods); + return this.canDisplayDangerousGoodsSection(techRecord) && containsExplosives; + } + + canDisplayBatterySection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const adrBodyType = techRecord.techRecord_adrDetails_vehicleDetails_type; + return typeof adrBodyType === 'string' && adrBodyType.toLowerCase().includes('battery'); + } + + canDisplayTankOrBatterySection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const adrBodyType = techRecord.techRecord_adrDetails_vehicleDetails_type; + const containsTankOrBattery = typeof adrBodyType === 'string' && this.containsTankOrBattery(adrBodyType); + return this.canDisplayDangerousGoodsSection(techRecord) && containsTankOrBattery; + } + + canDisplayTankStatementSelectSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const tankStatementSubstancesPermitted = + techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted; + const underUNNumber = tankStatementSubstancesPermitted === ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER; + return this.canDisplayTankOrBatterySection(techRecord) && underUNNumber; + } + + canDisplayTankStatementStatementSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const tankStatementSelect = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_select; + const underStatement = tankStatementSelect === ADRTankDetailsTankStatementSelect.STATEMENT; + return this.canDisplayTankStatementSelectSection(techRecord) && underStatement; + } + + canDisplayTankStatementProductListSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const tankStatementSelect = techRecord.techRecord_adrDetails_tank_tankDetails_tankStatement_select; + const underProductList = tankStatementSelect === ADRTankDetailsTankStatementSelect.PRODUCT_LIST; + return this.canDisplayTankStatementSelectSection(techRecord) && underProductList; + } + + canDisplayWeightSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const brakeEndurance = techRecord.techRecord_adrDetails_brakeEndurance; + return brakeEndurance === true; + } + + canDisplayBatteryListNumberSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const batteryListApplicable = techRecord.techRecord_adrDetails_listStatementApplicable; + return this.canDisplayBatterySection(techRecord) && batteryListApplicable === true; + } + + canDisplayIssueSection(techRecord: TechRecordType<'hgv' | 'lgv' | 'trl'>) { + const brakeDeclarationsSeen = techRecord.techRecord_adrDetails_brakeDeclarationsSeen; + return brakeDeclarationsSeen === true; + } } diff --git a/src/app/services/dynamic-forms/dynamic-form.service.ts b/src/app/services/dynamic-forms/dynamic-form.service.ts index b85a33577c..7419fcf958 100644 --- a/src/app/services/dynamic-forms/dynamic-form.service.ts +++ b/src/app/services/dynamic-forms/dynamic-form.service.ts @@ -4,7 +4,11 @@ import { GlobalError } from '@core/components/global-error/global-error.interfac import { ErrorMessageMap } from '@forms/utils/error-message-map'; // eslint-disable-next-line import/no-cycle import { CustomAsyncValidators } from '@forms/validators/custom-async-validator/custom-async-validators'; -import { CustomValidators, EnumValidatorOptions, IsArrayValidatorOptions } from '@forms/validators/custom-validators/custom-validators'; +import { + CustomValidators, + EnumValidatorOptions, + IsArrayValidatorOptions, +} from '@forms/validators/custom-validators/custom-validators'; import { DefectValidators } from '@forms/validators/defects/defect.validators'; import { AsyncValidatorNames } from '@models/async-validators.enum'; import { Condition } from '@models/condition.model'; @@ -17,222 +21,246 @@ import { CustomFormArray, CustomFormControl, CustomFormGroup, FormNode, FormNode type CustomFormFields = CustomFormControl | CustomFormArray | CustomFormGroup; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class DynamicFormService { - constructor(private store: Store) {} - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - validatorMap: Record ValidatorFn> = { - [ValidatorNames.AheadOfDate]: (arg: string) => CustomValidators.aheadOfDate(arg), - [ValidatorNames.Alphanumeric]: () => CustomValidators.alphanumeric(), - [ValidatorNames.Email]: () => CustomValidators.email(), - [ValidatorNames.CopyValueToRootControl]: (arg: string) => CustomValidators.copyValueToRootControl(arg), - [ValidatorNames.CustomPattern]: (args: string[]) => CustomValidators.customPattern([...args]), - [ValidatorNames.DateNotExceed]: (args: { sibling: string; months: number }) => CustomValidators.dateNotExceed(args.sibling, args.months), - [ValidatorNames.Defined]: () => CustomValidators.defined(), - [ValidatorNames.DisableIfEquals]: (args: { sibling: string; value: unknown }) => CustomValidators.disableIfEquals(args.sibling, args.value), - [ValidatorNames.EnableIfEquals]: (args: { sibling: string; value: unknown }) => CustomValidators.enableIfEquals(args.sibling, args.value), - [ValidatorNames.FutureDate]: () => CustomValidators.futureDate, - [ValidatorNames.PastYear]: () => CustomValidators.pastYear, - [ValidatorNames.HideIfEmpty]: (args: string) => CustomValidators.hideIfEmpty(args), - [ValidatorNames.HideIfNotEqual]: (args: { sibling: string; value: unknown }) => CustomValidators.hideIfNotEqual(args.sibling, args.value), - [ValidatorNames.HideIfParentSiblingEqual]: (args: { sibling: string; value: unknown }) => - CustomValidators.hideIfParentSiblingEquals(args.sibling, args.value), - [ValidatorNames.HideIfParentSiblingNotEqual]: (args: { sibling: string; value: unknown }) => - CustomValidators.hideIfParentSiblingNotEqual(args.sibling, args.value), - [ValidatorNames.Max]: (args: number) => Validators.max(args), - [ValidatorNames.MaxLength]: (args: number) => Validators.maxLength(args), - [ValidatorNames.Min]: (args: number) => Validators.min(args), - [ValidatorNames.MinLength]: (args: number) => Validators.minLength(args), - [ValidatorNames.NotZNumber]: () => CustomValidators.notZNumber, - [ValidatorNames.Numeric]: () => CustomValidators.numeric(), - [ValidatorNames.PastDate]: () => CustomValidators.pastDate, - [ValidatorNames.Pattern]: (args: string) => Validators.pattern(args), - [ValidatorNames.Required]: () => Validators.required, - [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), - [ValidatorNames.RequiredIfNotEquals]: (args: { sibling: string; value: unknown[] }) => - CustomValidators.requiredIfNotEquals(args.sibling, args.value), - [ValidatorNames.ValidateVRMTrailerIdLength]: (args: { sibling: string }) => CustomValidators.validateVRMTrailerIdLength(args.sibling), - [ValidatorNames.ValidateDefectNotes]: () => DefectValidators.validateDefectNotes, - [ValidatorNames.ValidateProhibitionIssued]: () => DefectValidators.validateProhibitionIssued, - [ValidatorNames.MustEqualSibling]: (args: { sibling: string }) => CustomValidators.mustEqualSibling(args.sibling), - [ValidatorNames.HandlePsvPassengersChange]: (args: { passengersOne: string; passengersTwo: string }) => - CustomValidators.handlePsvPassengersChange(args.passengersOne, args.passengersTwo), - [ValidatorNames.IsMemberOfEnum]: (args: { enum: Record; options?: Partial }) => - CustomValidators.isMemberOfEnum(args.enum, args.options), - [ValidatorNames.UpdateFunctionCode]: () => CustomValidators.updateFunctionCode(), - [ValidatorNames.ShowGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.showGroupsWhenEqualTo(args.values, args.groups), - [ValidatorNames.HideGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.hideGroupsWhenEqualTo(args.values, args.groups), - [ValidatorNames.ShowGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.showGroupsWhenIncludes(args.values, args.groups), - [ValidatorNames.HideGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.hideGroupsWhenIncludes(args.values, args.groups), - [ValidatorNames.ShowGroupsWhenExcludes]: (args: { values: unknown[]; groups: string[] }) => - CustomValidators.showGroupsWhenExcludes(args.values, args.groups), - [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), - [ValidatorNames.Custom]: (...args) => CustomValidators.custom(...args), - [ValidatorNames.Tc3TestValidator]: (args: { inspectionNumber: number }) => CustomValidators.tc3TestValidator(args), - [ValidatorNames.RequiredIfNotHidden]: () => CustomValidators.requiredIfNotHidden(), - [ValidatorNames.DateIsInvalid]: () => CustomValidators.dateIsInvalid, - [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 - asyncValidatorMap: Record AsyncValidatorFn> = { - [AsyncValidatorNames.HideIfEqualsWithCondition]: (args: { sibling: string; value: string; conditions: Condition | Condition[] }) => - CustomAsyncValidators.hideIfEqualsWithCondition(this.store, args.sibling, args.value, args.conditions), - [AsyncValidatorNames.PassResultDependantOnCustomDefects]: () => CustomAsyncValidators.passResultDependantOnCustomDefects(this.store), - [AsyncValidatorNames.RequiredIfNotAbandoned]: () => CustomAsyncValidators.requiredIfNotAbandoned(this.store), - [AsyncValidatorNames.RequiredIfNotFail]: () => CustomAsyncValidators.requiredIfNotFail(this.store), - [AsyncValidatorNames.RequiredIfNotResult]: (args: { testResult: resultOfTestEnum | resultOfTestEnum[] }) => - CustomAsyncValidators.requiredIfNotResult(this.store, args.testResult), - [AsyncValidatorNames.RequiredIfNotResultAndSiblingEquals]: (args: { - 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 - createForm(formNode: FormNode, data?: any): CustomFormGroup | CustomFormArray { - if (!formNode) { - return new CustomFormGroup(formNode, {}); - } - - const form: CustomFormGroup | CustomFormArray = - formNode.type === FormNodeTypes.ARRAY ? new CustomFormArray(formNode, [], this.store) : new CustomFormGroup(formNode, {}); - - data = data ?? (formNode.type === FormNodeTypes.ARRAY ? [] : {}); - - formNode.children?.forEach((child) => { - const { name, type, value, validators, asyncValidators, disabled } = child; - - const control = - FormNodeTypes.CONTROL === type - ? new CustomFormControl({ ...child }, { value, disabled: !!disabled }) - : this.createForm(child, data[`${name}`]); - - if (validators?.length) { - this.addValidators(control, validators); - } - - if (asyncValidators?.length) { - this.addAsyncValidators(control, asyncValidators); - } - - if (form instanceof FormGroup) { - form.addControl(name, control); - } else if (form instanceof FormArray) { - this.createControls(child, data).forEach((element) => form.push(element)); - } - }); - - if (data) { - form.patchValue(data); - } - - return form; - } - - createControls(child: FormNode, data: unknown): CustomFormFields[] { - // Note: There's a quirk here when dealing with arrays where if - // `data` is an array then `child.name` should be a correct index so - // make sure the template has the correct name to the node. - return Array.isArray(data) - ? data.map(() => - FormNodeTypes.CONTROL !== child.type - ? this.createForm(child, data[Number(child.name)]) - : new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled }) - ) - : [new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled })]; - } - - addValidators(control: CustomFormFields, validators: Array<{ name: ValidatorNames; args?: unknown }> = []) { - validators.forEach((v) => control.addValidators(this.validatorMap[v.name](v.args))); - } - - addAsyncValidators(control: CustomFormFields, validators: Array<{ name: AsyncValidatorNames; args?: unknown }> = []) { - validators.forEach((v) => control.addAsyncValidators(this.asyncValidatorMap[v.name](v.args))); - } - - static validate(form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, errors: GlobalError[], updateValidity = true) { - this.getFormLevelErrors(form, errors); - Object.entries(form.controls).forEach(([, value]) => { - if (!(value instanceof FormControl || value instanceof CustomFormControl)) { - this.validate(value as CustomFormGroup | CustomFormArray, errors, updateValidity); - } else { - value.markAsTouched(); - if (updateValidity) { - value.updateValueAndValidity(); - } - (value as CustomFormControl).meta?.changeDetection?.detectChanges(); - this.getControlErrors(value, errors); - } - }); - } - - static getFormLevelErrors(form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, errors: GlobalError[]) { - if (!(form instanceof CustomFormGroup || form instanceof CustomFormArray)) { - return; - } - if (form.errors) { - Object.entries(form.errors).forEach(([key, error]) => { - // If an anchor link is provided, use that, otherwise determine target element from customId or name - const anchorLink = form.meta?.customId ?? form.meta?.name; - errors.push({ - error: ErrorMessageMap[`${key}`](error), - anchorLink, - }); - }); - } - } - - static validateControl(control: FormControl | CustomFormControl, errors: GlobalError[]) { - control.markAsTouched(); - (control as CustomFormControl).meta?.changeDetection?.detectChanges(); - this.getControlErrors(control, errors); - } - - private static getControlErrors(control: FormControl | CustomFormControl, validationErrorList: GlobalError[]) { - const { errors } = control; - const meta = (control as CustomFormControl).meta as FormNode | undefined; - - if (errors) { - if (meta?.hide) return; - 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; - - // 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, - }, - ]; - - validationErrorList.push(...globalErrors); - }); - } - } + constructor(private store: Store) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validatorMap: Record ValidatorFn> = { + [ValidatorNames.AheadOfDate]: (arg: string) => CustomValidators.aheadOfDate(arg), + [ValidatorNames.Alphanumeric]: () => CustomValidators.alphanumeric(), + [ValidatorNames.Email]: () => CustomValidators.email(), + [ValidatorNames.CopyValueToRootControl]: (arg: string) => CustomValidators.copyValueToRootControl(arg), + [ValidatorNames.CustomPattern]: (args: string[]) => CustomValidators.customPattern([...args]), + [ValidatorNames.DateNotExceed]: (args: { sibling: string; months: number }) => + CustomValidators.dateNotExceed(args.sibling, args.months), + [ValidatorNames.Defined]: () => CustomValidators.defined(), + [ValidatorNames.DisableIfEquals]: (args: { sibling: string; value: unknown }) => + CustomValidators.disableIfEquals(args.sibling, args.value), + [ValidatorNames.EnableIfEquals]: (args: { sibling: string; value: unknown }) => + CustomValidators.enableIfEquals(args.sibling, args.value), + [ValidatorNames.FutureDate]: () => CustomValidators.futureDate, + [ValidatorNames.PastYear]: () => CustomValidators.pastYear, + [ValidatorNames.HideIfEmpty]: (args: string) => CustomValidators.hideIfEmpty(args), + [ValidatorNames.HideIfNotEqual]: (args: { sibling: string; value: unknown }) => + CustomValidators.hideIfNotEqual(args.sibling, args.value), + [ValidatorNames.HideIfParentSiblingEqual]: (args: { sibling: string; value: unknown }) => + CustomValidators.hideIfParentSiblingEquals(args.sibling, args.value), + [ValidatorNames.HideIfParentSiblingNotEqual]: (args: { sibling: string; value: unknown }) => + CustomValidators.hideIfParentSiblingNotEqual(args.sibling, args.value), + [ValidatorNames.Max]: (args: number) => Validators.max(args), + [ValidatorNames.MaxLength]: (args: number) => Validators.maxLength(args), + [ValidatorNames.Min]: (args: number) => Validators.min(args), + [ValidatorNames.MinLength]: (args: number) => Validators.minLength(args), + [ValidatorNames.NotZNumber]: () => CustomValidators.notZNumber, + [ValidatorNames.Numeric]: () => CustomValidators.numeric(), + [ValidatorNames.PastDate]: () => CustomValidators.pastDate, + [ValidatorNames.Pattern]: (args: string) => Validators.pattern(args), + [ValidatorNames.Required]: () => Validators.required, + [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), + [ValidatorNames.RequiredIfNotEquals]: (args: { sibling: string; value: unknown[] }) => + CustomValidators.requiredIfNotEquals(args.sibling, args.value), + [ValidatorNames.ValidateVRMTrailerIdLength]: (args: { sibling: string }) => + CustomValidators.validateVRMTrailerIdLength(args.sibling), + [ValidatorNames.ValidateDefectNotes]: () => DefectValidators.validateDefectNotes, + [ValidatorNames.ValidateProhibitionIssued]: () => DefectValidators.validateProhibitionIssued, + [ValidatorNames.MustEqualSibling]: (args: { sibling: string }) => CustomValidators.mustEqualSibling(args.sibling), + [ValidatorNames.HandlePsvPassengersChange]: (args: { passengersOne: string; passengersTwo: string }) => + CustomValidators.handlePsvPassengersChange(args.passengersOne, args.passengersTwo), + [ValidatorNames.IsMemberOfEnum]: (args: { + enum: Record; + options?: Partial; + }) => CustomValidators.isMemberOfEnum(args.enum, args.options), + [ValidatorNames.UpdateFunctionCode]: () => CustomValidators.updateFunctionCode(), + [ValidatorNames.ShowGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.showGroupsWhenEqualTo(args.values, args.groups), + [ValidatorNames.HideGroupsWhenEqualTo]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.hideGroupsWhenEqualTo(args.values, args.groups), + [ValidatorNames.ShowGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.showGroupsWhenIncludes(args.values, args.groups), + [ValidatorNames.HideGroupsWhenIncludes]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.hideGroupsWhenIncludes(args.values, args.groups), + [ValidatorNames.ShowGroupsWhenExcludes]: (args: { values: unknown[]; groups: string[] }) => + CustomValidators.showGroupsWhenExcludes(args.values, args.groups), + [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), + [ValidatorNames.Custom]: (...args) => CustomValidators.custom(...args), + [ValidatorNames.Tc3TestValidator]: (args: { inspectionNumber: number }) => CustomValidators.tc3TestValidator(args), + [ValidatorNames.RequiredIfNotHidden]: () => CustomValidators.requiredIfNotHidden(), + [ValidatorNames.DateIsInvalid]: () => CustomValidators.dateIsInvalid, + [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 + asyncValidatorMap: Record AsyncValidatorFn> = { + [AsyncValidatorNames.HideIfEqualsWithCondition]: (args: { + sibling: string; + value: string; + conditions: Condition | Condition[]; + }) => CustomAsyncValidators.hideIfEqualsWithCondition(this.store, args.sibling, args.value, args.conditions), + [AsyncValidatorNames.PassResultDependantOnCustomDefects]: () => + CustomAsyncValidators.passResultDependantOnCustomDefects(this.store), + [AsyncValidatorNames.RequiredIfNotAbandoned]: () => CustomAsyncValidators.requiredIfNotAbandoned(this.store), + [AsyncValidatorNames.RequiredIfNotFail]: () => CustomAsyncValidators.requiredIfNotFail(this.store), + [AsyncValidatorNames.RequiredIfNotResult]: (args: { testResult: resultOfTestEnum | resultOfTestEnum[] }) => + CustomAsyncValidators.requiredIfNotResult(this.store, args.testResult), + [AsyncValidatorNames.RequiredIfNotResultAndSiblingEquals]: (args: { + 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 + createForm(formNode: FormNode, data?: any): CustomFormGroup | CustomFormArray { + if (!formNode) { + return new CustomFormGroup(formNode, {}); + } + + const form: CustomFormGroup | CustomFormArray = + formNode.type === FormNodeTypes.ARRAY + ? new CustomFormArray(formNode, [], this.store) + : new CustomFormGroup(formNode, {}); + + data = data ?? (formNode.type === FormNodeTypes.ARRAY ? [] : {}); + + formNode.children?.forEach((child) => { + const { name, type, value, validators, asyncValidators, disabled } = child; + + const control = + FormNodeTypes.CONTROL === type + ? new CustomFormControl({ ...child }, { value, disabled: !!disabled }) + : this.createForm(child, data[`${name}`]); + + if (validators?.length) { + this.addValidators(control, validators); + } + + if (asyncValidators?.length) { + this.addAsyncValidators(control, asyncValidators); + } + + if (form instanceof FormGroup) { + form.addControl(name, control); + } else if (form instanceof FormArray) { + this.createControls(child, data).forEach((element) => form.push(element)); + } + }); + + if (data) { + form.patchValue(data); + } + + return form; + } + + createControls(child: FormNode, data: unknown): CustomFormFields[] { + // Note: There's a quirk here when dealing with arrays where if + // `data` is an array then `child.name` should be a correct index so + // make sure the template has the correct name to the node. + return Array.isArray(data) + ? data.map(() => + FormNodeTypes.CONTROL !== child.type + ? this.createForm(child, data[Number(child.name)]) + : new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled }) + ) + : [new CustomFormControl({ ...child }, { value: child.value, disabled: !!child.disabled })]; + } + + addValidators(control: CustomFormFields, validators: Array<{ name: ValidatorNames; args?: unknown }> = []) { + validators.forEach((v) => control.addValidators(this.validatorMap[v.name](v.args))); + } + + addAsyncValidators(control: CustomFormFields, validators: Array<{ name: AsyncValidatorNames; args?: unknown }> = []) { + validators.forEach((v) => control.addAsyncValidators(this.asyncValidatorMap[v.name](v.args))); + } + + static validate( + form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, + errors: GlobalError[], + updateValidity = true + ) { + this.getFormLevelErrors(form, errors); + Object.entries(form.controls).forEach(([, value]) => { + if (!(value instanceof FormControl || value instanceof CustomFormControl)) { + this.validate(value as CustomFormGroup | CustomFormArray, errors, updateValidity); + } else { + value.markAsTouched(); + if (updateValidity) { + value.updateValueAndValidity(); + } + (value as CustomFormControl).meta?.changeDetection?.detectChanges(); + this.getControlErrors(value, errors); + } + }); + } + + static getFormLevelErrors(form: CustomFormGroup | CustomFormArray | FormGroup | FormArray, errors: GlobalError[]) { + if (!(form instanceof CustomFormGroup || form instanceof CustomFormArray)) { + return; + } + if (form.errors) { + Object.entries(form.errors).forEach(([key, error]) => { + // If an anchor link is provided, use that, otherwise determine target element from customId or name + const anchorLink = form.meta?.customId ?? form.meta?.name; + errors.push({ + error: ErrorMessageMap[`${key}`](error), + anchorLink, + }); + }); + } + } + + static validateControl(control: FormControl | CustomFormControl, errors: GlobalError[]) { + control.markAsTouched(); + (control as CustomFormControl).meta?.changeDetection?.detectChanges(); + this.getControlErrors(control, errors); + } + + private static getControlErrors(control: FormControl | CustomFormControl, validationErrorList: GlobalError[]) { + const { errors } = control; + const meta = (control as CustomFormControl).meta as FormNode | undefined; + + if (errors) { + if (meta?.hide) return; + 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; + + // 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, + }, + ]; + + validationErrorList.push(...globalErrors); + }); + } + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 7a082b6c49..0634a5f4ac 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -4,6 +4,8 @@ import { RouterModule } from '@angular/router'; import { RoleRequiredDirective } from '@directives/app-role-required/app-role-required.directive'; import { FeatureToggleDirective } from '@directives/feature-toggle/feature-toggle.directive'; import { GovukInputDirective } from '@directives/govuk-input/govuk-input.directive'; +import { GovukSelectDirective } from '@directives/govuk-select/govuk-select.directive'; +import { GovukTextareaDirective } from '@directives/govuk-textarea/govuk-textarea.directive'; import { PreventDoubleClickDirective } from '@directives/prevent-double-click/prevent-double-click.directive'; import { RetrieveDocumentDirective } from '@directives/retrieve-document/retrieve-document.directive'; import { AccordionControlComponent } from '../components/accordion-control/accordion-control.component'; @@ -29,62 +31,66 @@ import { TestTypeNamePipe } from '../pipes/test-type-name/test-type-name.pipe'; import { TyreAxleLoadPipe } from '../pipes/tyre-axle-load/tyre-axle-load.pipe'; @NgModule({ - declarations: [ - DefaultNullOrEmpty, - ButtonGroupComponent, - ButtonComponent, - BannerComponent, - RoleRequiredDirective, - FeatureToggleDirective, - FeatureToggleDirective, - TagComponent, - NumberPlateComponent, - IconComponent, - TestTypeNamePipe, - AccordionComponent, - AccordionControlComponent, - PaginationComponent, - TestCertificateComponent, - PreventDoubleClickDirective, - BaseDialogComponent, - DigitGroupSeparatorPipe, - RefDataDecodePipe, - RetrieveDocumentDirective, - InputSpinnerComponent, - RouterOutletComponent, - TyreAxleLoadPipe, - GetControlLabelPipe, - FormatVehicleTypePipe, - CollapsibleTextComponent, - GovukInputDirective, - ], - imports: [CommonModule, RouterModule], - exports: [ - DefaultNullOrEmpty, - ButtonGroupComponent, - ButtonComponent, - BannerComponent, - RoleRequiredDirective, - FeatureToggleDirective, - TagComponent, - NumberPlateComponent, - IconComponent, - TestTypeNamePipe, - AccordionComponent, - AccordionControlComponent, - PaginationComponent, - TestCertificateComponent, - BaseDialogComponent, - DigitGroupSeparatorPipe, - RefDataDecodePipe, - RetrieveDocumentDirective, - InputSpinnerComponent, - RouterOutletComponent, - TyreAxleLoadPipe, - GetControlLabelPipe, - FormatVehicleTypePipe, - CollapsibleTextComponent, - GovukInputDirective, - ], + declarations: [ + DefaultNullOrEmpty, + ButtonGroupComponent, + ButtonComponent, + BannerComponent, + RoleRequiredDirective, + FeatureToggleDirective, + FeatureToggleDirective, + TagComponent, + NumberPlateComponent, + IconComponent, + TestTypeNamePipe, + AccordionComponent, + AccordionControlComponent, + PaginationComponent, + TestCertificateComponent, + PreventDoubleClickDirective, + BaseDialogComponent, + DigitGroupSeparatorPipe, + RefDataDecodePipe, + RetrieveDocumentDirective, + InputSpinnerComponent, + RouterOutletComponent, + TyreAxleLoadPipe, + GetControlLabelPipe, + FormatVehicleTypePipe, + CollapsibleTextComponent, + GovukInputDirective, + GovukTextareaDirective, + GovukSelectDirective, + ], + imports: [CommonModule, RouterModule], + exports: [ + DefaultNullOrEmpty, + ButtonGroupComponent, + ButtonComponent, + BannerComponent, + RoleRequiredDirective, + FeatureToggleDirective, + TagComponent, + NumberPlateComponent, + IconComponent, + TestTypeNamePipe, + AccordionComponent, + AccordionControlComponent, + PaginationComponent, + TestCertificateComponent, + BaseDialogComponent, + DigitGroupSeparatorPipe, + RefDataDecodePipe, + RetrieveDocumentDirective, + InputSpinnerComponent, + RouterOutletComponent, + TyreAxleLoadPipe, + GetControlLabelPipe, + FormatVehicleTypePipe, + CollapsibleTextComponent, + GovukInputDirective, + GovukTextareaDirective, + GovukSelectDirective, + ], }) export class SharedModule {} From edf63affc54247c4d7623b9ac681e3347e345e2e Mon Sep 17 00:00:00 2001 From: Brandon Thomas-Davies <87308252+BrandonT95@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:14:16 +0100 Subject: [PATCH 007/211] chore(cb2-0000): rebase and lint --- .../govuk-input/govuk-input.directive.ts | 66 ++--- .../vehicle-section-edit.component.ts | 252 +++++++++--------- .../vehicle-section-summary.component.ts | 10 +- .../vehicle-section-view.component.ts | 10 +- .../vehicle-section.component.ts | 8 +- src/app/forms/dynamic-forms.module.ts | 238 ++++++++--------- 6 files changed, 288 insertions(+), 296 deletions(-) diff --git a/src/app/directives/govuk-input/govuk-input.directive.ts b/src/app/directives/govuk-input/govuk-input.directive.ts index 5ea2064a3e..031fb47400 100644 --- a/src/app/directives/govuk-input/govuk-input.directive.ts +++ b/src/app/directives/govuk-input/govuk-input.directive.ts @@ -1,47 +1,47 @@ import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core'; import { ControlContainer } from '@angular/forms'; -import { ReplaySubject, takeUntil } from 'rxjs'; import { FormNodeWidth } from '@services/dynamic-forms/dynamic-form.types'; +import { ReplaySubject, takeUntil } from 'rxjs'; @Directive({ - selector: '[govukInput]', + selector: '[govukInput]', }) export class GovukInputDirective implements OnInit, OnDestroy { - elementRef = inject>(ElementRef); - controlContainer = inject(ControlContainer); + elementRef = inject>(ElementRef); + controlContainer = inject(ControlContainer); - formControlName = input.required(); - width = input(); + formControlName = input.required(); + width = input(); - destroy$ = new ReplaySubject(1); + destroy$ = new ReplaySubject(1); - ngOnInit(): void { - const formControlName = this.formControlName(); - const control = this.controlContainer.control?.get(formControlName); - if (control) { - this.elementRef.nativeElement.setAttribute('id', formControlName); - this.elementRef.nativeElement.setAttribute('name', formControlName); - if (this.width()) { - this.elementRef.nativeElement.classList.add(`govuk-input--width-${this.width()}`); - } - this.elementRef.nativeElement.classList.add('govuk-input'); + ngOnInit(): void { + const formControlName = this.formControlName(); + const control = this.controlContainer.control?.get(formControlName); + if (control) { + this.elementRef.nativeElement.setAttribute('id', formControlName); + this.elementRef.nativeElement.setAttribute('name', formControlName); + if (this.width()) { + this.elementRef.nativeElement.classList.add(`govuk-input--width-${this.width()}`); + } + this.elementRef.nativeElement.classList.add('govuk-input'); - control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { - if (statusChange === 'INVALID' && control.touched) { - this.elementRef.nativeElement.classList.add('govuk-input--error'); - this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); - } + control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { + if (statusChange === 'INVALID' && control.touched) { + this.elementRef.nativeElement.classList.add('govuk-input--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', `${formControlName}-error`); + } - if (statusChange === 'VALID') { - this.elementRef.nativeElement.classList.remove('govuk-input--error'); - this.elementRef.nativeElement.setAttribute('aria-describedby', ''); - } - }); - } - } + if (statusChange === 'VALID') { + this.elementRef.nativeElement.classList.remove('govuk-input--error'); + this.elementRef.nativeElement.setAttribute('aria-describedby', ''); + } + }); + } + } - ngOnDestroy(): void { - this.destroy$.next(true); - this.destroy$.complete(); - } + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } } diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts index e82394b30c..675c6ebe2e 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts @@ -1,149 +1,147 @@ -import { Component, inject, OnDestroy, OnInit } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { - ControlContainer, - FormBuilder, - FormControl, - FormGroup, - ValidatorFn, -} from '@angular/forms'; -import { VehicleTypes } from '@models/vehicle-tech-record.model'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { ControlContainer, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'; +import { TagType } from '@components/tag/tag.component'; import { FuelPropulsionSystem } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete'; +import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; import { VehicleConfiguration } from '@models/vehicle-configuration.enum'; +import { VehicleTypes } from '@models/vehicle-tech-record.model'; +import { Store } from '@ngrx/store'; +import { FormNodeWidth, TagTypeLabels } from '@services/dynamic-forms/dynamic-form.types'; import { techRecord } from '@store/technical-records'; import { ReplaySubject, take } from 'rxjs'; -import { FormNodeWidth, TagTypeLabels } from '@services/dynamic-forms/dynamic-form.types'; -import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; -import { TagType } from '@components/tag/tag.component'; type VehicleSectionForm = Partial, FormControl>>; @Component({ - selector: 'app-vehicle-section-edit', - templateUrl: './vehicle-section-edit.component.html', - styleUrls: ['./vehicle-section-edit.component.scss'], + selector: 'app-vehicle-section-edit', + templateUrl: './vehicle-section-edit.component.html', + styleUrls: ['./vehicle-section-edit.component.scss'], }) - export class VehicleSectionEditComponent implements OnInit, OnDestroy { - protected readonly FormNodeWidth = FormNodeWidth; - protected readonly TagType = TagType; - protected readonly TagTypeLabels = TagTypeLabels; - fb = inject(FormBuilder); - store = inject(Store); - controlContainer = inject(ControlContainer); - - destroy$ = new ReplaySubject(1); + protected readonly FormNodeWidth = FormNodeWidth; + protected readonly TagType = TagType; + protected readonly TagTypeLabels = TagTypeLabels; + fb = inject(FormBuilder); + store = inject(Store); + controlContainer = inject(ControlContainer); - form = this.fb.group({ - // values from hgv-tech-record template file - techRecord_alterationMarker: this.fb.control(null), - techRecord_departmentalVehicleMarker: this.fb.control(null), - techRecord_drawbarCouplingFitted: this.fb.control(null), - techRecord_emissionsLimit: this.fb.control(null), - techRecord_euVehicleCategory: this.fb.control(null), - techRecord_euroStandard: this.fb.control(null), - techRecord_fuelPropulsionSystem: this.fb.control(null), - techRecord_functionCode: this.fb.control(null), - techRecord_manufactureYear: this.fb.control(null, [this.min(1000, 'Year of manufacture'), this.pastYear('Year of manufacture')]), - techRecord_noOfAxles: this.fb.control({ value: null, disabled: true }), - techRecord_offRoad: this.fb.control(null), - techRecord_regnDate: this.fb.control(null), - techRecord_roadFriendly: this.fb.control(null), - techRecord_speedLimiterMrk: this.fb.control(null), - techRecord_statusCode: this.fb.control(null), - techRecord_tachoExemptMrk: this.fb.control(null), - techRecord_vehicleClass_description: this.fb.control(null), - techRecord_vehicleConfiguration: this.fb.control(null), - techRecord_vehicleType: this.fb.control({ value: null, disabled: true }), - }, { validators: []}); + destroy$ = new ReplaySubject(1); + form = this.fb.group( + { + // values from hgv-tech-record template file + techRecord_alterationMarker: this.fb.control(null), + techRecord_departmentalVehicleMarker: this.fb.control(null), + techRecord_drawbarCouplingFitted: this.fb.control(null), + techRecord_emissionsLimit: this.fb.control(null), + techRecord_euVehicleCategory: this.fb.control(null), + techRecord_euroStandard: this.fb.control(null), + techRecord_fuelPropulsionSystem: this.fb.control(null), + techRecord_functionCode: this.fb.control(null), + techRecord_manufactureYear: this.fb.control(null, [ + this.min(1000, 'Year of manufacture'), + this.pastYear('Year of manufacture'), + ]), + techRecord_noOfAxles: this.fb.control({ value: null, disabled: true }), + techRecord_offRoad: this.fb.control(null), + techRecord_regnDate: this.fb.control(null), + techRecord_roadFriendly: this.fb.control(null), + techRecord_speedLimiterMrk: this.fb.control(null), + techRecord_statusCode: this.fb.control(null), + techRecord_tachoExemptMrk: this.fb.control(null), + techRecord_vehicleClass_description: this.fb.control(null), + techRecord_vehicleConfiguration: this.fb.control(null), + techRecord_vehicleType: this.fb.control({ value: null, disabled: true }), + }, + { validators: [] } + ); - ngOnInit(): void { - // Attach all form controls to parent - const parent = this.controlContainer.control; - if (parent instanceof FormGroup) { - for (const [key, control] of Object.entries(this.form.controls)) { - parent.addControl(key, control); - } - } + ngOnInit(): void { + // Attach all form controls to parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + for (const [key, control] of Object.entries(this.form.controls)) { + parent.addControl(key, control); + } + } - this.store - .select(techRecord) - .pipe(take(1)) - .subscribe((techRecord) => { - if (techRecord) this.form.patchValue(techRecord as any); - }); - } + this.store + .select(techRecord) + .pipe(take(1)) + .subscribe((techRecord) => { + if (techRecord) this.form.patchValue(techRecord as any); + }); + } - ngOnDestroy(): void { - // Detach all form controls from parent - const parent = this.controlContainer.control; - if (parent instanceof FormGroup) { - for (const key of Object.keys(this.form.controls)) { - parent.removeControl(key); - } - } + ngOnDestroy(): void { + // Detach all form controls from parent + const parent = this.controlContainer.control; + if (parent instanceof FormGroup) { + for (const key of Object.keys(this.form.controls)) { + parent.removeControl(key); + } + } - // Clear subscriptions - this.destroy$.next(true); - this.destroy$.complete(); - } + // Clear subscriptions + this.destroy$.next(true); + this.destroy$.complete(); + } - // Potential to have a getter for each vehicle type as some vehicle types - // have different fields to others, this would allow for a more dynamic form - // could have an overall getter that sets the form with controls that belong - // to all vehicle types and then a getter for each vehicle type that adds the - // specific controls that only belong to that vehicle type? - // get hgvControls(): Record { - // return { - // techRecord_alterationMarker: this.fb.control(null), - // techRecord_departmentalVehicleMarker: this.fb.control(null), - // techRecord_drawbarCouplingFitted: this.fb.control(null), - // techRecord_emissionsLimit: this.fb.control(null), - // techRecord_euVehicleCategory: this.fb.control(null), - // techRecord_euroStandard: this.fb.control(null), - // techRecord_fuelPropulsionSystem: this.fb.control(null), - // techRecord_functionCode: this.fb.control(null), - // techRecord_manufactureYear: this.fb.control(null), - // techRecord_noOfAxles: this.fb.control(null), - // techRecord_numberOfWheelsDriven: this.fb.control(null), - // techRecord_offRoad: this.fb.control(null), - // techRecord_regnDate: this.fb.control(null), - // techRecord_roadFriendly: this.fb.control(null), - // techRecord_speedLimiterMrk: this.fb.control(null), - // techRecord_statusCode: this.fb.control(null), - // techRecord_tachoExemptMrk: this.fb.control(null), - // techRecord_vehicleClass_description: this.fb.control(null), - // techRecord_vehicleConfiguration: this.fb.control(null), - // techRecord_vehicleType: this.fb.control(null) - // } - // } + // Potential to have a getter for each vehicle type as some vehicle types + // have different fields to others, this would allow for a more dynamic form + // could have an overall getter that sets the form with controls that belong + // to all vehicle types and then a getter for each vehicle type that adds the + // specific controls that only belong to that vehicle type? + // get hgvControls(): Record { + // return { + // techRecord_alterationMarker: this.fb.control(null), + // techRecord_departmentalVehicleMarker: this.fb.control(null), + // techRecord_drawbarCouplingFitted: this.fb.control(null), + // techRecord_emissionsLimit: this.fb.control(null), + // techRecord_euVehicleCategory: this.fb.control(null), + // techRecord_euroStandard: this.fb.control(null), + // techRecord_fuelPropulsionSystem: this.fb.control(null), + // techRecord_functionCode: this.fb.control(null), + // techRecord_manufactureYear: this.fb.control(null), + // techRecord_noOfAxles: this.fb.control(null), + // techRecord_numberOfWheelsDriven: this.fb.control(null), + // techRecord_offRoad: this.fb.control(null), + // techRecord_regnDate: this.fb.control(null), + // techRecord_roadFriendly: this.fb.control(null), + // techRecord_speedLimiterMrk: this.fb.control(null), + // techRecord_statusCode: this.fb.control(null), + // techRecord_tachoExemptMrk: this.fb.control(null), + // techRecord_vehicleClass_description: this.fb.control(null), + // techRecord_vehicleConfiguration: this.fb.control(null), + // techRecord_vehicleType: this.fb.control(null) + // } + // } - isInvalid(formControlName: string) { - const control = this.form.get(formControlName); - return control?.invalid && control?.touched; - } + isInvalid(formControlName: string) { + const control = this.form.get(formControlName); + return control?.invalid && control?.touched; + } - min(size: number, label: string): ValidatorFn { - return (control) => { - if (control.value && control.value < size) { - return { min: `${label} must be greater than or equal to ${size}` }; - } + min(size: number, label: string): ValidatorFn { + return (control) => { + if (control.value && control.value < size) { + return { min: `${label} must be greater than or equal to ${size}` }; + } - return null; - }; - } + return null; + }; + } - pastYear(label: string): ValidatorFn { - return (control) => { - if (control.value) { - const currentYear = new Date().getFullYear(); - const inputYear = control.value; - if (inputYear && inputYear > currentYear) { - return { pastYear: `${label} must be the current or a past year` }; - } - } - return null; - } - }; + pastYear(label: string): ValidatorFn { + return (control) => { + if (control.value) { + const currentYear = new Date().getFullYear(); + const inputYear = control.value; + if (inputYear && inputYear > currentYear) { + return { pastYear: `${label} must be the current or a past year` }; + } + } + return null; + }; + } } diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts index bee913d9c0..b2b177dffe 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component.ts @@ -3,12 +3,12 @@ import { Store } from '@ngrx/store'; import { techRecord } from '@store/technical-records'; @Component({ - selector: 'app-vehicle-section-summary', - templateUrl: './vehicle-section-summary.component.html', - styleUrls: ['./vehicle-section-summary.component.scss'], + selector: 'app-vehicle-section-summary', + templateUrl: './vehicle-section-summary.component.html', + styleUrls: ['./vehicle-section-summary.component.scss'], }) export class VehicleSectionSummaryComponent { - store = inject(Store); + store = inject(Store); - techRecord = this.store.selectSignal(techRecord); + techRecord = this.store.selectSignal(techRecord); } diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts index 10f6625b61..802235e3be 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component.ts @@ -3,12 +3,12 @@ import { Store } from '@ngrx/store'; import { techRecord } from '@store/technical-records'; @Component({ - selector: 'app-vehicle-section-view', - templateUrl: './vehicle-section-view.component.html', - styleUrls: ['./vehicle-section-view.component.scss'], + selector: 'app-vehicle-section-view', + templateUrl: './vehicle-section-view.component.html', + styleUrls: ['./vehicle-section-view.component.scss'], }) export class VehicleSectionViewComponent { - store = inject(Store); + store = inject(Store); - techRecord = this.store.selectSignal(techRecord); + techRecord = this.store.selectSignal(techRecord); } diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts index 42c89e5c2f..88a3532a55 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section.component.ts @@ -1,12 +1,12 @@ import { Component, input } from '@angular/core'; @Component({ - selector: 'app-vehicle-section', - templateUrl: './vehicle-section.component.html', - styleUrls: ['./vehicle-section.component.scss'], + selector: 'app-vehicle-section', + templateUrl: './vehicle-section.component.html', + styleUrls: ['./vehicle-section.component.scss'], }) export class VehicleSectionComponent { - mode = input('edit'); + mode = input('edit'); } type Mode = 'view' | 'edit' | 'summary'; diff --git a/src/app/forms/dynamic-forms.module.ts b/src/app/forms/dynamic-forms.module.ts index 2b7e3ad9bb..48bffc6246 100644 --- a/src/app/forms/dynamic-forms.module.ts +++ b/src/app/forms/dynamic-forms.module.ts @@ -14,8 +14,12 @@ import { ApprovalTypeInputComponent } from '@forms/components/approval-type/appr import { AdrCertificateHistoryComponent } from '@forms/custom-sections/adr-certificate-history/adr-certificate-history.component'; import { AdrExaminerNotesHistoryEditComponent } from '@forms/custom-sections/adr-examiner-notes-history-edit/adr-examiner-notes-history.component-edit'; import { ApprovalTypeComponent } from '@forms/custom-sections/approval-type/approval-type.component'; -import { SharedModule } from '@shared/shared.module'; +import { VehicleSectionEditComponent } from '@forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component'; +import { VehicleSectionSummaryComponent } from '@forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component'; +import { VehicleSectionViewComponent } from '@forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component'; +import { VehicleSectionComponent } from '@forms/custom-sections/vehicle-section/vehicle-section.component'; import { TruncatePipe } from '@pipes/truncate/truncate.pipe'; +import { SharedModule } from '@shared/shared.module'; import { AutocompleteComponent } from './components/autocomplete/autocomplete.component'; import { BaseControlComponent } from './components/base-control/base-control.component'; import { CharacterCountComponent } from './components/character-count/character-count.component'; @@ -71,18 +75,6 @@ import { RequiredStandardsComponent } from './custom-sections/required-standards import { TrlBrakesComponent } from './custom-sections/trl-brakes/trl-brakes.component'; import { TyresComponent } from './custom-sections/tyres/tyres.component'; import { WeightsComponent } from './custom-sections/weights/weights.component'; -import { - VehicleSectionComponent -} from '@forms/custom-sections/vehicle-section/vehicle-section.component'; -import { - VehicleSectionSummaryComponent -} from '@forms/custom-sections/vehicle-section/vehicle-section-summary/vehicle-section-summary.component'; -import { - VehicleSectionViewComponent -} from '@forms/custom-sections/vehicle-section/vehicle-section-view/vehicle-section-view.component'; -import { - VehicleSectionEditComponent -} from '@forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component'; @NgModule({ declarations: [ @@ -105,116 +97,118 @@ import { DateComponent, DateControlsComponent, CharacterCountComponent, - ControlErrorsComponent,SelectComponent, - DynamicFormFieldComponent, - FieldErrorMessageComponent, - DefectSelectComponent, - RequiredStandardSelectComponent, - DateFocusNextDirective, - TruncatePipe, - WeightsComponent, - LettersComponent, - PlatesComponent, - DimensionsComponent, - TrlBrakesComponent, - ReadOnlyComponent, - CustomDefectsComponent, - RequiredStandardComponent, - RequiredStandardsComponent, - CustomDefectComponent, - SwitchableInputComponent, - ReadOnlyComponent, - SuffixDirective, - AbandonDialogComponent, - BodyComponent, - TyresComponent, - PsvBrakesComponent, - PrefixDirective, - SuggestiveInputComponent, - CheckboxComponent, - ApprovalTypeComponent, - ApprovalTypeInputComponent, - ApprovalTypeFocusNextDirective, - ModifiedWeightsComponent, - FieldWarningMessageComponent, - AdrComponent, - AdrTankDetailsSubsequentInspectionsEditComponent, - AdrTankStatementUnNumberEditComponent, - CustomFormControlComponent, - AdrExaminerNotesHistoryEditComponent, - AdrExaminerNotesHistoryViewComponent, - AdrTankDetailsSubsequentInspectionsViewComponent, - AdrTankDetailsInitialInspectionViewComponent, - AdrTankStatementUnNumberViewComponent, - AdrCertificateHistoryComponent, - AdrTankDetailsM145ViewComponent, - ContingencyAdrGenerateCertComponent, - AdrNewCertificateRequiredViewComponent, - AdrSectionComponent, - AdrSectionEditComponent, - AdrSectionViewComponent, - AdrSectionSummaryComponent, - VehicleSectionComponent, - VehicleSectionSummaryComponent, - VehicleSectionViewComponent, - VehicleSectionEditComponent, - ], - imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], - exports: [ - TextInputComponent, - ViewListItemComponent, - DynamicFormGroupComponent, - ViewCombinationComponent, - CheckboxGroupComponent, - RadioGroupComponent, - DefectComponent, - DefectsComponent, - AutocompleteComponent, - NumberInputComponent, - TextAreaComponent, - DateComponent,DateControlsComponent, + ControlErrorsComponent, + SelectComponent, + DynamicFormFieldComponent, + FieldErrorMessageComponent, + DefectSelectComponent, + RequiredStandardSelectComponent, + DateFocusNextDirective, + TruncatePipe, + WeightsComponent, + LettersComponent, + PlatesComponent, + DimensionsComponent, + TrlBrakesComponent, + ReadOnlyComponent, + CustomDefectsComponent, + RequiredStandardComponent, + RequiredStandardsComponent, + CustomDefectComponent, + SwitchableInputComponent, + ReadOnlyComponent, + SuffixDirective, + AbandonDialogComponent, + BodyComponent, + TyresComponent, + PsvBrakesComponent, + PrefixDirective, + SuggestiveInputComponent, + CheckboxComponent, + ApprovalTypeComponent, + ApprovalTypeInputComponent, + ApprovalTypeFocusNextDirective, + ModifiedWeightsComponent, + FieldWarningMessageComponent, + AdrComponent, + AdrTankDetailsSubsequentInspectionsEditComponent, + AdrTankStatementUnNumberEditComponent, + CustomFormControlComponent, + AdrExaminerNotesHistoryEditComponent, + AdrExaminerNotesHistoryViewComponent, + AdrTankDetailsSubsequentInspectionsViewComponent, + AdrTankDetailsInitialInspectionViewComponent, + AdrTankStatementUnNumberViewComponent, + AdrCertificateHistoryComponent, + AdrTankDetailsM145ViewComponent, + ContingencyAdrGenerateCertComponent, + AdrNewCertificateRequiredViewComponent, + AdrSectionComponent, + AdrSectionEditComponent, + AdrSectionViewComponent, + AdrSectionSummaryComponent, + VehicleSectionComponent, + VehicleSectionSummaryComponent, + VehicleSectionViewComponent, + VehicleSectionEditComponent, + ], + imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, RouterModule], + exports: [ + TextInputComponent, + ViewListItemComponent, + DynamicFormGroupComponent, + ViewCombinationComponent, + CheckboxGroupComponent, + RadioGroupComponent, + DefectComponent, + DefectsComponent, + AutocompleteComponent, + NumberInputComponent, + TextAreaComponent, + DateComponent, + DateControlsComponent, CharacterCountComponent, ControlErrorsComponent, - SelectComponent, - DynamicFormFieldComponent, - FieldErrorMessageComponent, - DefectSelectComponent, - RequiredStandardSelectComponent, - WeightsComponent, - LettersComponent, - PlatesComponent, - TyresComponent, - DimensionsComponent, - TrlBrakesComponent, - ReadOnlyComponent, - RequiredStandardComponent, - RequiredStandardsComponent, - CustomDefectsComponent, - CustomDefectComponent, - SwitchableInputComponent, - SuffixDirective, - ReadOnlyComponent, - AbandonDialogComponent, - BodyComponent, - PsvBrakesComponent, - PrefixDirective, - SuggestiveInputComponent, - CheckboxComponent, - ToUppercaseDirective, - NoSpaceDirective, - TrimWhitespaceDirective, - ApprovalTypeComponent, - ApprovalTypeInputComponent, - ApprovalTypeFocusNextDirective, - ModifiedWeightsComponent, - AdrComponent, - AdrCertificateHistoryComponent, - FieldWarningMessageComponent, - AdrSectionComponent, - AdrSectionEditComponent, - AdrSectionViewComponent, - AdrSectionSummaryComponent, - VehicleSectionComponent - ], + SelectComponent, + DynamicFormFieldComponent, + FieldErrorMessageComponent, + DefectSelectComponent, + RequiredStandardSelectComponent, + WeightsComponent, + LettersComponent, + PlatesComponent, + TyresComponent, + DimensionsComponent, + TrlBrakesComponent, + ReadOnlyComponent, + RequiredStandardComponent, + RequiredStandardsComponent, + CustomDefectsComponent, + CustomDefectComponent, + SwitchableInputComponent, + SuffixDirective, + ReadOnlyComponent, + AbandonDialogComponent, + BodyComponent, + PsvBrakesComponent, + PrefixDirective, + SuggestiveInputComponent, + CheckboxComponent, + ToUppercaseDirective, + NoSpaceDirective, + TrimWhitespaceDirective, + ApprovalTypeComponent, + ApprovalTypeInputComponent, + ApprovalTypeFocusNextDirective, + ModifiedWeightsComponent, + AdrComponent, + AdrCertificateHistoryComponent, + FieldWarningMessageComponent, + AdrSectionComponent, + AdrSectionEditComponent, + AdrSectionViewComponent, + AdrSectionSummaryComponent, + VehicleSectionComponent, + ], }) export class DynamicFormsModule {} From 604cf1079e872902a6b5eab37b0f2696644e2b3e Mon Sep 17 00:00:00 2001 From: Thomas Crawley Date: Mon, 23 Sep 2024 15:45:05 +0100 Subject: [PATCH 008/211] chore(cb2-0000): additional vehicle section properties --- .../vehicle-section-edit.component.html | 176 ++++++++++++++++++ .../vehicle-section-edit.component.ts | 38 +++- 2 files changed, 213 insertions(+), 1 deletion(-) diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html index 2e968812e7..d02611401e 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html @@ -36,4 +36,180 @@

+ + +
+

+ +

+
+
+ + +
+
+
+ + +
+

+ +

+
+
+ + +
+
+
+ + +
+

+ +

+
+
+ + +
+
+
+ + + +
+

+ +

+
+
+ + +
+
+
+ + +
+

+ +

+ + +
+ + +
+

+ +

+
+
+ + +
+
+
+ + + +
+

+ +

+
+
+ + +
+
+
+
diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts index 675c6ebe2e..bab2a22a72 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.ts @@ -4,11 +4,13 @@ import { TagType } from '@components/tag/tag.component'; import { FuelPropulsionSystem } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete'; import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; import { VehicleConfiguration } from '@models/vehicle-configuration.enum'; -import { VehicleTypes } from '@models/vehicle-tech-record.model'; +import { FuelTypes, VehicleTypes } from '@models/vehicle-tech-record.model'; import { Store } from '@ngrx/store'; import { FormNodeWidth, TagTypeLabels } from '@services/dynamic-forms/dynamic-form.types'; import { techRecord } from '@store/technical-records'; import { ReplaySubject, take } from 'rxjs'; +import { getOptionsFromEnum } from '@forms/utils/enum-map'; +import { EmissionStandard } from '@models/test-types/emissions.enum'; type VehicleSectionForm = Partial, FormControl>>; @@ -56,6 +58,40 @@ export class VehicleSectionEditComponent implements OnInit, OnDestroy { { validators: [] } ); + speedLimiterExemptOptions = [ + { value: true, label: 'Exempt' }, + { value: false, label: 'Not exempt' }, + ]; + + tachoExemptOptions = [ + { value: true, label: 'Exempt' }, + { value: false, label: 'Not exempt' }, + ]; + + roadFriendlySuspentionOptions = [ + { value: true, label: 'Exempt' }, + { value: false, label: 'Not exempt' }, + ]; + + euroStandardOptions = [ + { label: '0.10 g/kWh Euro III PM', value: '0.10 g/kWh Euro 3 PM' }, + ...getOptionsFromEnum(EmissionStandard), + ]; + + fuelPropulsionSystemOptions = [ + ...getOptionsFromEnum(FuelTypes), + ]; + + drawbarCouplingFittedOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ]; + + offRoadVehicleOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ] + ngOnInit(): void { // Attach all form controls to parent const parent = this.controlContainer.control; From 9949212a0db6a54ab1f49488bddb701d2ee2de60 Mon Sep 17 00:00:00 2001 From: Thomas Crawley Date: Thu, 26 Sep 2024 10:15:13 +0100 Subject: [PATCH 009/211] feat(cb2-0000): add additional directives --- .../govuk-form-group.directive.ts | 29 ++ .../govuk-radio/govuk-radio.directive.ts | 38 ++ src/app/directives/tag/tag.directive.ts | 21 + .../tech-record-amend-vin.component.ts | 2 +- .../tech-record-generate-letter.component.ts | 2 +- .../tech-record-search-tyres.component.ts | 2 +- .../tech-record-title.component.ts | 4 +- .../vehicle-technical-record.component.ts | 2 +- .../vehicle-section-edit.component.html | 106 +++-- .../vehicle-section-edit.component.ts | 72 ++-- .../templates/hgv/hgv-tech-record.template.ts | 2 +- .../__tests__/custom-async-validators.spec.ts | 387 ++++++++---------- .../dynamic-forms/dynamic-form.service.ts | 2 +- src/app/shared/shared.module.ts | 9 + .../technical-records/batch-create.reducer.ts | 3 +- 15 files changed, 393 insertions(+), 288 deletions(-) create mode 100644 src/app/directives/govuk-form-group/govuk-form-group.directive.ts create mode 100644 src/app/directives/govuk-radio/govuk-radio.directive.ts create mode 100644 src/app/directives/tag/tag.directive.ts diff --git a/src/app/directives/govuk-form-group/govuk-form-group.directive.ts b/src/app/directives/govuk-form-group/govuk-form-group.directive.ts new file mode 100644 index 0000000000..60f3e70c3d --- /dev/null +++ b/src/app/directives/govuk-form-group/govuk-form-group.directive.ts @@ -0,0 +1,29 @@ +import { Directive, ElementRef, OnDestroy, OnInit, inject } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; + +@Directive({ + selector: '[govukFormGroup]', +}) +export class GovukFormGroupDirective implements OnInit, OnDestroy { + elementRef = inject>(ElementRef); + destroy$ = new ReplaySubject(1); + + ngOnInit(): void { + this.elementRef.nativeElement.classList.add('govuk-form-group'); + const children = this.elementRef.nativeElement.children; + if (children && children[0].tagName === 'H1') { + const child = children[0]; + child.classList.add('govuk-label-wrapper'); + const grandchildren = child.children; + if (grandchildren && grandchildren[0].tagName === 'LABEL') { + grandchildren[0].classList.add('govuk-label'); + grandchildren[0].classList.add('govuk-label--m'); + } + } + } + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } +} diff --git a/src/app/directives/govuk-radio/govuk-radio.directive.ts b/src/app/directives/govuk-radio/govuk-radio.directive.ts new file mode 100644 index 0000000000..57daa5ab6d --- /dev/null +++ b/src/app/directives/govuk-radio/govuk-radio.directive.ts @@ -0,0 +1,38 @@ +import { Directive, ElementRef, inject, OnDestroy, OnInit } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; + +@Directive({ + selector: '[govukRadio]', +}) +export class GovukRadioDirective implements OnInit, OnDestroy { + elementRef = inject>(ElementRef); + destroy$ = new ReplaySubject(1); + + ngOnInit(): void { + this.elementRef.nativeElement.classList.add('govuk-radios'); + const children = this.elementRef.nativeElement.children; + console.log('test 0'); + console.log(children); + console.log(this.elementRef.nativeElement.children); + console.log(Array.from(this.elementRef.nativeElement.children)); + // if (children && children.length > 0) { + // console.log('test 1'); + // for (let child of children) { + // console.log('test 2'); + // child.classList.add('govuk-radios__item'); + // const grandchildren = child.children; + // if (grandchildren.length && grandchildren.length > 1) { + // console.log('test 3'); + // grandchildren[0].classList.add('govuk-radios__input'); + // grandchildren[1].classList.add('govuk-label'); + // grandchildren[1].classList.add('govuk-radios__label'); + // } + // } + // } + } + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } +} diff --git a/src/app/directives/tag/tag.directive.ts b/src/app/directives/tag/tag.directive.ts new file mode 100644 index 0000000000..c3a6dc2b7a --- /dev/null +++ b/src/app/directives/tag/tag.directive.ts @@ -0,0 +1,21 @@ +import { Directive, ElementRef, OnDestroy, OnInit, inject } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; + +@Directive({ + selector: '[tag]', +}) +export class TagDirective implements OnInit, OnDestroy { + elementRef = inject>(ElementRef); + destroy$ = new ReplaySubject(1); + + ngOnInit(): void { + this.elementRef.nativeElement.classList.add('flex'); + this.elementRef.nativeElement.classList.add('flex-wrap'); + this.elementRef.nativeElement.classList.add('gap-2'); + } + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } +} diff --git a/src/app/features/tech-record/components/tech-record-amend-vin/tech-record-amend-vin.component.ts b/src/app/features/tech-record/components/tech-record-amend-vin/tech-record-amend-vin.component.ts index b16bf62744..bb8cf26bd0 100644 --- a/src/app/features/tech-record/components/tech-record-amend-vin/tech-record-amend-vin.component.ts +++ b/src/app/features/tech-record/components/tech-record-amend-vin/tech-record-amend-vin.component.ts @@ -60,7 +60,7 @@ export class AmendVinComponent implements OnDestroy, OnInit { } get currentVrm(): string | undefined { - return this.techRecord?.techRecord_vehicleType !== 'trl' ? this.techRecord?.primaryVrm ?? '' : undefined; + return this.techRecord?.techRecord_vehicleType !== 'trl' ? (this.techRecord?.primaryVrm ?? '') : undefined; } isFormValid(): boolean { diff --git a/src/app/features/tech-record/components/tech-record-generate-letter/tech-record-generate-letter.component.ts b/src/app/features/tech-record/components/tech-record-generate-letter/tech-record-generate-letter.component.ts index 4a5fd5f34e..e9021f344b 100644 --- a/src/app/features/tech-record/components/tech-record-generate-letter/tech-record-generate-letter.component.ts +++ b/src/app/features/tech-record/components/tech-record-generate-letter/tech-record-generate-letter.component.ts @@ -80,7 +80,7 @@ export class GenerateLetterComponent implements OnInit { get emailAddress(): string | undefined { return this.techRecord?.techRecord_vehicleType === 'trl' - ? this.techRecord?.techRecord_applicantDetails_emailAddress ?? '' + ? (this.techRecord?.techRecord_applicantDetails_emailAddress ?? '') : undefined; } diff --git a/src/app/features/tech-record/components/tech-record-search-tyres/tech-record-search-tyres.component.ts b/src/app/features/tech-record/components/tech-record-search-tyres/tech-record-search-tyres.component.ts index 1159eaa375..47f8fecdbc 100644 --- a/src/app/features/tech-record/components/tech-record-search-tyres/tech-record-search-tyres.component.ts +++ b/src/app/features/tech-record/components/tech-record-search-tyres/tech-record-search-tyres.component.ts @@ -107,7 +107,7 @@ export class TechRecordSearchTyresComponent implements OnInit { } get currentVrm(): string | undefined { return this.vehicleTechRecord?.techRecord_vehicleType !== 'trl' - ? this.vehicleTechRecord?.primaryVrm ?? '' + ? (this.vehicleTechRecord?.primaryVrm ?? '') : undefined; } get paginatedFields(): ReferenceDataTyre[] { diff --git a/src/app/features/tech-record/components/tech-record-title/tech-record-title.component.ts b/src/app/features/tech-record/components/tech-record-title/tech-record-title.component.ts index 21056f613a..6200fe6935 100644 --- a/src/app/features/tech-record/components/tech-record-title/tech-record-title.component.ts +++ b/src/app/features/tech-record/components/tech-record-title/tech-record-title.component.ts @@ -47,11 +47,11 @@ export class TechRecordTitleComponent implements OnInit { } get currentVrm(): string | undefined { - return this.vehicle?.techRecord_vehicleType !== 'trl' ? this.vehicle?.primaryVrm ?? '' : undefined; + return this.vehicle?.techRecord_vehicleType !== 'trl' ? (this.vehicle?.primaryVrm ?? '') : undefined; } get otherVrms(): string[] | undefined { - return this.vehicle?.techRecord_vehicleType !== 'trl' ? this.vehicle?.secondaryVrms ?? [] : undefined; + return this.vehicle?.techRecord_vehicleType !== 'trl' ? (this.vehicle?.secondaryVrms ?? []) : undefined; } get vehicleTypes(): typeof VehicleTypes { diff --git a/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts b/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts index 286b2f24cd..2ae3b7c694 100644 --- a/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts +++ b/src/app/features/tech-record/components/vehicle-technical-record/vehicle-technical-record.component.ts @@ -85,7 +85,7 @@ export class VehicleTechnicalRecordComponent implements OnInit, OnDestroy { } get currentVrm(): string | undefined { - return this.techRecord?.techRecord_vehicleType !== 'trl' ? this.techRecord?.primaryVrm ?? '' : undefined; + return this.techRecord?.techRecord_vehicleType !== 'trl' ? (this.techRecord?.primaryVrm ?? '') : undefined; } get roles(): typeof Roles { diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html index d02611401e..294961061b 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html @@ -1,7 +1,7 @@
-
-

-

- Add UN Number +

+ Add UN Number +

@@ -467,13 +494,15 @@

-
+

-

+

+

+

@@ -598,76 +628,78 @@

-
+

-

- Error: - - {{ error }} - - - {{ error }} - - - Day must between 1 and 31 - - - Month must between 1 and 12 - - - Year must between 1000 and 9999 - -

-
-
-
- - +
+

+ Error: + + {{ error }} + + + {{ error }} + + + Day must between 1 and 31 + + + Month must between 1 and 12 + + + Year must between 1000 and 9999 + +

+
+
+
+ + +
-
-
-
- - +
+
+ + +
-
-
-
- - +
+
+ + +
diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts index 3e4bbbd372..862e952e7e 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts @@ -112,15 +112,23 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { ]), techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: this.fb.control(null, [ this.commonValidators.maxLength(1500, 'Reference number must be less than or equal to 1500 characters'), + this.adrValidators.requiresAUnNumberOrReferenceNumber( + 'Reference number or UN number 1 is required when selecting Product List' + ), ]), - techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array([ - this.fb.control(null, [ - this.commonValidators.maxLength(1500, 'UN number 1 must be less than or equal to 1500 characters'), - this.adrValidators.requiresAUNNumberOrReferenceNumber( + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: this.fb.array( + [ + this.fb.control(null, [ + this.commonValidators.maxLength(1500, 'UN number 1 must be less than or equal to 1500 characters'), + ]), + ], + [ + this.adrValidators.requiresAllUnNumbersToBePopulated(), + this.adrValidators.requiresAUnNumberOrReferenceNumber( 'Reference number or UN number 1 is required when selecting Product List' ), - ]), - ]), + ] + ), techRecord_adrDetails_tank_tankDetails_tankStatement_productList: this.fb.control(null, []), techRecord_adrDetails_tank_tankDetails_specialProvisions: this.fb.control(null, [ this.commonValidators.maxLength(1500, 'Special provisions must be less than or equal to 1500 characters'), @@ -277,17 +285,21 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { addUNNumber() { const arr = this.form.controls.techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo; - arr.push( - this.fb.control(null, [ - this.commonValidators.maxLength( - 1500, - `UN number ${arr.length + 1} must be less than or equal to 1500 characters` - ), - this.adrValidators.requiresAUNNumberOrReferenceNumber( - 'Reference number or UN number 1 is required when selecting Product List' - ), - ]) - ); + const allNumbersPopulated = arr.value.every((value: string | null) => !!value); + + if (allNumbersPopulated) { + arr.push( + this.fb.control(null, [ + this.commonValidators.maxLength( + 1500, + `UN number ${arr.length + 1} must be less than or equal to 1500 characters` + ), + ]) + ); + } + + arr.markAsTouched(); + arr.updateValueAndValidity(); } removeUNNumber(index: number) { diff --git a/src/app/forms/validators/adr-validators.service.ts b/src/app/forms/validators/adr-validators.service.ts index a9238113d0..d333149993 100644 --- a/src/app/forms/validators/adr-validators.service.ts +++ b/src/app/forms/validators/adr-validators.service.ts @@ -107,18 +107,38 @@ export class AdrValidatorsService { }; } - requiresAUNNumberOrReferenceNumber(message: string): ValidatorFn { + requiresAllUnNumbersToBePopulated(): ValidatorFn { return (control) => { - if ( - control.root?.value && - this.adrService.canDisplayTankStatementProductListSection(control.root.getRawValue()) - ) { - const refNo = control.root.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo'); - const unNos = control.root.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); - const unNosPopulated = unNos?.value && Array.isArray(unNos.value) && unNos.value.some((unNo) => !!unNo); - if (!refNo?.value && !unNosPopulated) { + if (control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value)) { + const unNumbers = control.value; + if (Array.isArray(unNumbers)) { + const index = unNumbers.findIndex((unNumber) => !unNumber); + if (index > -1) { + return { required: `UN number ${index + 1} is required or remove UN number ${index + 1}` }; + } + } + } + + return null; + }; + } + + requiresAUnNumberOrReferenceNumber(message: string): ValidatorFn { + return (control) => { + if (control.parent && this.adrService.canDisplayTankStatementProductListSection(control.parent.value)) { + const refNo = control.parent.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo'); + const unNumbers = control.parent.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo'); + if (!refNo?.value && Array.isArray(unNumbers?.value) && !unNumbers?.value[0]) { + // Set errors on both simulatenously + refNo?.setErrors({ required: message }); + unNumbers?.setErrors({ required: message }); + return { required: message }; } + + // Clear errors from both fields if either is populated + refNo?.setErrors(null); + unNumbers?.setErrors(null); } return null; From 38be6e16e9eebdaa3d740137d52ae2985818970d Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:14:36 +0100 Subject: [PATCH 014/211] chore(cb2-0000): fix tc3 validator --- src/app/forms/validators/adr-validators.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/forms/validators/adr-validators.service.ts b/src/app/forms/validators/adr-validators.service.ts index d333149993..f6d82050eb 100644 --- a/src/app/forms/validators/adr-validators.service.ts +++ b/src/app/forms/validators/adr-validators.service.ts @@ -94,7 +94,7 @@ export class AdrValidatorsService { requiresOnePopulatedTC3Field(message: string): ValidatorFn { return (control) => { - if (control.parent) { + if (control.parent && this.adrService.canDisplayTankOrBatterySection(control.root.value)) { const tc3InspectionType = control.parent.get('tc3Type'); const tc3PeriodicNumber = control.parent.get('tc3PeriodicNumber'); const tc3ExpiryDate = control.parent.get('tc3ExpiryDate'); From 6a04365cfecaaeb54bafa58f2e6c9e1104de783a Mon Sep 17 00:00:00 2001 From: Brandon Thomas-Davies <87308252+BrandonT95@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:24:39 +0100 Subject: [PATCH 015/211] chore(cb2-0000): commit progress on vehicle summary --- .../govuk-select/govuk-select.directive.ts | 5 + .../vehicle-section-edit.component.html | 218 +++++++++++++----- .../vehicle-section-edit.component.scss | 7 + .../vehicle-section-edit.component.ts | 181 ++++++++++----- .../validators/common-validators.service.ts | 23 ++ 5 files changed, 327 insertions(+), 107 deletions(-) diff --git a/src/app/directives/govuk-select/govuk-select.directive.ts b/src/app/directives/govuk-select/govuk-select.directive.ts index c35d9d0c1d..49fb68fe21 100644 --- a/src/app/directives/govuk-select/govuk-select.directive.ts +++ b/src/app/directives/govuk-select/govuk-select.directive.ts @@ -1,5 +1,6 @@ import { Directive, ElementRef, inject, input } from '@angular/core'; import { ControlContainer } from '@angular/forms'; +import { FormNodeWidth } from '@services/dynamic-forms/dynamic-form.types'; import { ReplaySubject, takeUntil } from 'rxjs'; @Directive({ @@ -10,6 +11,7 @@ export class GovukSelectDirective { controlContainer = inject(ControlContainer); formControlName = input.required(); + width = input(); destroy$ = new ReplaySubject(1); @@ -20,6 +22,9 @@ export class GovukSelectDirective { this.elementRef.nativeElement.setAttribute('id', formControlName); this.elementRef.nativeElement.setAttribute('name', formControlName); this.elementRef.nativeElement.classList.add('govuk-select'); + if (this.width()) { + this.elementRef.nativeElement.classList.add(`govuk-input--width-${this.width()}`); + } control.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((statusChange) => { if (statusChange === 'INVALID' && control.touched) { diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html index 294961061b..e546b7ff2c 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html @@ -1,4 +1,6 @@
+ +

+
+ + +
+
+ +

+ Tacho exempt +

+
+
+
+ + +
+
+
+
+
+ + +
+
+ +

+ Euro standard +

+
-
+
-
+
+
- -
-

- -

+ + +
+
+ +

+ Road friendly suspension +
+ {{ TagTypeLabels.PLATES }} +
+

+
-
+
-
-
- - -
-

- -

-
-
- - -
-
-
- - - -
-

- -

-
-
- - -
-
+
-
-

- -

+
+ - +
+
+ +

+ Drawbar coupling fitted +

+
+
+
+ + +
-
+
- -
-

- -

+ +
+
- -
-

- -

+ +
+
- -
-

- -

-
-
- - + +
+
+ +

+ Off road vehicle +

+
+
+
+ + +
-
+
diff --git a/src/global-styles.scss b/src/global-styles.scss index 7e4b252f75..1214e26963 100644 --- a/src/global-styles.scss +++ b/src/global-styles.scss @@ -20,3 +20,46 @@ gap: 15px; } } + +/** Global govuk styles */ +.govuk-form-group { + @extend .govuk-form-group; + + > h1 { + @extend .govuk-label-wrapper; + + > label { + @extend .govuk-label; + @extend .govuk-label--m; + + display: flex; + align-items: center; + column-gap: 0.5rem; + flex-wrap: wrap; + + > div { + display: flex; + align-items: center; + column-gap: 0.5rem; + flex-wrap: wrap; + } + } + } +} + +.govuk-radios { + @extend .govuk-radios; + + > div { + @extend .govuk-radios__item; + + > input { + @extend .govuk-radios__input; + } + + > label { + @extend .govuk-label; + @extend .govuk-radios__label; + } + } +} From d542534eaf1e6e943e70cbe539fb85b6264cf7c1 Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:26:19 +0100 Subject: [PATCH 017/211] chore(cb2-0000): general ADR section fixes --- .../tech-record-summary.component.ts | 2 +- .../date-controls/date-controls.component.ts | 19 +- .../adr-section-edit.component.spec.ts | 47 ++++- .../adr-section-edit.component.html | 67 ++++--- .../adr-section-edit.component.ts | 42 ++++- .../__tests__/adr-validators.service.spec.ts | 167 +++++++++++++++++- .../common-validators.service.spec.ts | 156 +++++++++++++++- .../validators/common-validators.service.ts | 10 ++ 8 files changed, 459 insertions(+), 51 deletions(-) diff --git a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts index 0143805374..8a1421f7f1 100644 --- a/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts +++ b/src/app/features/tech-record/components/tech-record-summary/tech-record-summary.component.ts @@ -237,7 +237,7 @@ export class TechRecordSummaryComponent implements OnInit, OnDestroy { this.setErrors(forms); - this.isFormInvalid.emit(forms.some((form) => form.invalid)); + this.isFormInvalid.emit(forms.some((form) => form.invalid || this.form.invalid)); } setErrors(forms: Array): void { diff --git a/src/app/forms/components/date-controls/date-controls.component.ts b/src/app/forms/components/date-controls/date-controls.component.ts index fe26bbc375..e6ccd2fc8a 100644 --- a/src/app/forms/components/date-controls/date-controls.component.ts +++ b/src/app/forms/components/date-controls/date-controls.component.ts @@ -27,6 +27,7 @@ export class DateControlsComponent implements ControlValueAccessor, OnInit, OnDe controlContainer = inject(ControlContainer); globalErrorService = inject(GlobalErrorService); + mode = input('yyyy-mm-dd'); formControlName = input.required(); form = this.fb.group({ @@ -56,7 +57,7 @@ export class DateControlsComponent implements ControlValueAccessor, OnInit, OnDe writeDate(date: Date) { this.form.setValue({ year: date.getFullYear(), - month: date.getMonth(), + month: date.getMonth() + 1, day: date.getDate(), hours: date.getHours(), minutes: date.getMinutes(), @@ -106,10 +107,16 @@ export class DateControlsComponent implements ControlValueAccessor, OnInit, OnDe const monthStr = month?.toString().padStart(2, '0'); const dayStr = day?.toString().padStart(2, '0'); const hoursStr = hours?.toString().padStart(2, '0'); - const minutesStr = minutes?.toString().padStart(2, '0'); - const secondsStr = seconds?.toString().padStart(2, '0'); - - this.onChange(`${year}-${monthStr}-${dayStr}T${hoursStr || '00'}:${minutesStr || '00'}:${secondsStr || '00'}`); + const minsStr = minutes?.toString().padStart(2, '0'); + const secsStr = seconds?.toString().padStart(2, '0'); + + switch (this.mode()) { + case 'iso': + this.onChange(`${year}-${monthStr}-${dayStr}T${hoursStr || '00'}:${minsStr || '00'}:${secsStr || '00'}`); + break; + default: + this.onChange(`${year}-${monthStr}-${dayStr}`); + } }); } @@ -130,3 +137,5 @@ export class DateControlsComponent implements ControlValueAccessor, OnInit, OnDe }; } } + +type Format = 'iso' | 'yyyy-mm-dd'; diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/__tests__/adr-section-edit.component.spec.ts b/src/app/forms/custom-sections/adr-section/adr-section-edit/__tests__/adr-section-edit.component.spec.ts index dd9429b08c..2ec2f25a68 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/__tests__/adr-section-edit.component.spec.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/__tests__/adr-section-edit.component.spec.ts @@ -3,14 +3,12 @@ import { ControlContainer, FormGroup, FormGroupDirective, FormsModule, ReactiveF import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-verb'; import { DynamicFormsModule } from '@forms/dynamic-forms.module'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { AdrService } from '@services/adr/adr.service'; import { initialAppState } from '@store/index'; import { techRecord } from '@store/technical-records'; import { AdrSectionEditComponent } from '../adr-section-edit.component'; describe('AdrSectionEditComponent', () => { let store: MockStore; - let adrService: AdrService; let controlContainer: ControlContainer; let component: AdrSectionEditComponent; let fixture: ComponentFixture; @@ -30,11 +28,11 @@ describe('AdrSectionEditComponent', () => { }).compileComponents(); store = TestBed.inject(MockStore); - adrService = TestBed.inject(AdrService); controlContainer = TestBed.inject(ControlContainer); fixture = TestBed.createComponent(AdrSectionEditComponent); component = fixture.componentInstance; + component.form.reset(); fixture.detectChanges(); }); @@ -71,4 +69,47 @@ describe('AdrSectionEditComponent', () => { expect(spy).toHaveBeenCalled(); }); }); + + describe('addTC3TankInspection', () => { + it('should add an empty TC3 tank inspection to the form array', () => { + const arr = component.form.controls.techRecord_adrDetails_tank_tankDetails_tc3Details; + const spy = jest.spyOn(arr, 'push'); + component.addTC3TankInspection(); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('removeTC3TankInspection', () => { + it('should remove the TC3 tank inspection at the specified index from the form array', () => { + const arr = component.form.controls.techRecord_adrDetails_tank_tankDetails_tc3Details; + const spy = jest.spyOn(arr, 'removeAt'); + component.removeTC3TankInspection(1); + expect(spy).toHaveBeenCalledWith(1); + }); + }); + + describe('addUNNumber', () => { + it('should not allow the adding of a UN number if the previous one is empty', () => { + const arr = component.form.controls.techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo; + const spy = jest.spyOn(arr, 'push'); + component.addUNNumber(); + expect(spy).not.toHaveBeenCalled(); + }); + it('should add an empty UN number to the form array if the all prior ones are filled in', () => { + const arr = component.form.controls.techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo; + arr.patchValue(['123']); + const spy = jest.spyOn(arr, 'push'); + component.addUNNumber(); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('removeUNNumber', () => { + it('should remove the UN number at the specified index from the form array', () => { + const arr = component.form.controls.techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo; + const spy = jest.spyOn(arr, 'removeAt'); + component.removeUNNumber(1); + expect(spy).toHaveBeenCalledWith(1); + }); + }); }); diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html index 3a30975e31..80599bb257 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.html @@ -58,7 +58,7 @@

- +
@@ -67,7 +67,7 @@

- +
@@ -81,7 +81,7 @@

-
@@ -218,7 +218,8 @@

-
+ +

@@ -238,6 +239,7 @@

+
@@ -272,7 +274,7 @@

- +
@@ -297,7 +299,7 @@

- +
@@ -308,7 +310,7 @@

- + @@ -317,7 +319,7 @@

- + @@ -326,11 +328,11 @@

- + -
+

@@ -959,7 +956,9 @@

Additional Examiner Notes History

{{ note.lastUpdatedBy | defaultNullOrEmpty }} {{ note.createdAtDate | date : 'dd/MM/yyyy HH:mm' | defaultNullOrEmpty }} - Edit + + Edit + diff --git a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts index 862e952e7e..b0fac0a138 100644 --- a/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts +++ b/src/app/forms/custom-sections/adr-section/adr-section-edit/adr-section-edit.component.ts @@ -1,20 +1,25 @@ +import { ViewportScroller } from '@angular/common'; import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { ControlContainer, FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; import { ADRAdditionalNotesNumber } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrAdditionalNotesNumber.enum.js'; import { ADRBodyType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrBodyType.enum.js'; +import { ADRCompatibilityGroupJ } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrCompatibilityGroupJ.enum.js'; import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum.js'; import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum.js'; import { ADRTankStatementSubstancePermitted } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankStatementSubstancePermitted.js'; import { TC3Types } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/tc3Types.enum.js'; import { AdditionalExaminerNotes } from '@dvsa/cvs-type-definitions/types/v3/tech-record/get/hgv/complete'; +import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type'; import { getOptionsFromEnum } from '@forms/utils/enum-map'; import { AdrValidatorsService } from '@forms/validators/adr-validators.service'; import { CommonValidatorsService } from '@forms/validators/common-validators.service'; import { TC2Types } from '@models/adr.enum'; import { Store } from '@ngrx/store'; import { AdrService } from '@services/adr/adr.service'; -import { techRecord } from '@store/technical-records'; -import { ReplaySubject, take } from 'rxjs'; +import { TechnicalRecordService } from '@services/technical-record/technical-record.service'; +import { techRecord, updateScrollPosition } from '@store/technical-records'; +import { ReplaySubject, take, takeUntil } from 'rxjs'; @Component({ selector: 'app-adr-section-edit', @@ -24,12 +29,17 @@ import { ReplaySubject, take } from 'rxjs'; export class AdrSectionEditComponent implements OnInit, OnDestroy { fb = inject(FormBuilder); store = inject(Store); + router = inject(Router); + route = inject(ActivatedRoute); adrService = inject(AdrService); adrValidators = inject(AdrValidatorsService); commonValidators = inject(CommonValidatorsService); controlContainer = inject(ControlContainer); + viewportScroller = inject(ViewportScroller); + technicalRecordService = inject(TechnicalRecordService); destroy$ = new ReplaySubject(1); + currentTechRecord?: TechRecordType<'hgv' | 'lgv' | 'trl'>; form = this.fb.group({ techRecord_adrDetails_dangerousGoods: this.fb.control(false), @@ -84,9 +94,10 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { this.adrValidators.requiredWithTankOrBattery('Tank Make is required with ADR body type'), this.commonValidators.maxLength(70, 'Tank Make must be less than or equal to 70 characters'), ]), - techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ + techRecord_adrDetails_tank_tankDetails_yearOfManufacture: this.fb.control(null, [ this.adrValidators.requiredWithTankOrBattery('Tank Year of manufacture is required with ADR body type'), - this.commonValidators.maxLength(70, 'Tank Year of manufacture must be less than or equal to 70 characters'), + this.commonValidators.min(1000, 'Tank Year of manufacture must be greater than or equal to 1000'), + this.commonValidators.max(9999, 'Tank Year of manufacture must be less than or equal to 9999'), ]), techRecord_adrDetails_tank_tankDetails_tankManufacturerSerialNo: this.fb.control(null, [ this.adrValidators.requiredWithTankOrBattery('Manufacturer serial number is required with ADR body type'), @@ -155,6 +166,7 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { ]), techRecord_adrDetails_batteryListNumber: this.fb.control(null, [ this.adrValidators.requiredWithBatteryListApplicable('Reference Number is required with Battery List Applicable'), + this.commonValidators.maxLength(8, 'Reference Number must be less than or equal to 8 characters'), ]), // Brake declaration @@ -200,8 +212,8 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { guidanceNotesOptions = getOptionsFromEnum(ADRAdditionalNotesNumber); compatibilityGroupJOptions = [ - { label: 'Yes', value: true }, - { label: 'No', value: false }, + { value: ADRCompatibilityGroupJ.I, label: 'Yes' }, + { value: ADRCompatibilityGroupJ.E, label: 'No' }, ]; tankStatementSubstancePermittedOptions = getOptionsFromEnum(ADRTankStatementSubstancePermitted); @@ -215,6 +227,8 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { tc3InspectionOptions = getOptionsFromEnum(TC3Types); + memosApplyOptions = [{ value: '07/09 3mth leak ext ', label: 'Yes' }]; + isInvalid(formControlName: string) { const control = this.form.get(formControlName); return control?.invalid && control?.touched; @@ -224,6 +238,10 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { const control = this.form.get(formControlName); if (!control) return; + if (control.value === null) { + return control.setValue([value]); + } + const arr = [...control.value]; arr.includes(value) ? arr.splice(arr.indexOf(value), 1) : arr.push(value); control.setValue(arr); @@ -242,6 +260,10 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { .subscribe((techRecord) => { if (techRecord) this.form.patchValue(techRecord as any); }); + + this.technicalRecordService.techRecord$.pipe(takeUntil(this.destroy$)).subscribe((currentTechRecord) => { + this.currentTechRecord = currentTechRecord as TechRecordType<'hgv' | 'lgv' | 'trl'>; + }); } ngOnDestroy(): void { @@ -305,4 +327,12 @@ export class AdrSectionEditComponent implements OnInit, OnDestroy { removeUNNumber(index: number) { this.form.controls.techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo.removeAt(index); } + + getEditAdditionalExaminerNotePage(examinerNoteIndex: number) { + const reason = this.route.snapshot.data['reason']; + const route = `../${reason}/edit-additional-examiner-note/${examinerNoteIndex}`; + + this.store.dispatch(updateScrollPosition({ position: this.viewportScroller.getScrollPosition() })); + this.router.navigate([route], { relativeTo: this.route, state: this.currentTechRecord }); + } } diff --git a/src/app/forms/validators/__tests__/adr-validators.service.spec.ts b/src/app/forms/validators/__tests__/adr-validators.service.spec.ts index 5329bb339d..1c7be32ff8 100644 --- a/src/app/forms/validators/__tests__/adr-validators.service.spec.ts +++ b/src/app/forms/validators/__tests__/adr-validators.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { FormControl, FormGroup } from '@angular/forms'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; import { ADRBodyType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrBodyType.enum.js'; import { ADRDangerousGood } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrDangerousGood.enum.js'; import { ADRTankDetailsTankStatementSelect } from '@dvsa/cvs-type-definitions/types/v3/tech-record/enums/adrTankDetailsTankStatementSelect.enum.js'; @@ -30,6 +30,19 @@ describe('AdrValidatorsService', () => { techRecord_adrDetails_brakeEndurance: new FormControl(null), techRecord_adrDetails_weight: new FormControl(null), techRecord_adrDetails_batteryListNumber: new FormControl(null), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: new FormControl(null), + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: new FormArray([ + new FormControl(null), + new FormControl(null), + new FormControl(null), + ]), + techRecord_adrDetails_tank_tankDetails_tc3Details: new FormArray([ + new FormGroup({ + tc3Type: new FormControl(null), + tc3PeriodicNumber: new FormControl(null), + tc3PeriodicExpiryDate: new FormControl(null), + }), + ]), }); }); @@ -276,4 +289,156 @@ describe('AdrValidatorsService', () => { expect(validator(control)).toEqual({ required: 'message' }); }); }); + + describe('requiresOnePopulatedTC3Field', () => { + it('should return null when the tank or battery section is not visible', () => { + const validator = service.requiresOnePopulatedTC3Field('message'); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: false, + techRecord_adrDetails_tank_tankDetails_tc3Details: [ + { + tc3Type: 'type', + tc3PeriodicNumber: 'number', + tc3PeriodicExpiryDate: 'expiry', + }, + ], + }); + expect(validator(control.controls[0])).toBeNull(); + }); + + it('should return null when the tank or battery section is visible and one field is populated', () => { + const validator = service.requiresOnePopulatedTC3Field('message'); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tc3Details: [ + { + tc3Type: 'type', + tc3PeriodicNumber: null, + tc3PeriodicExpiryDate: null, + }, + ], + }); + expect(validator(control.controls[0])).toEqual({ required: 'message' }); + }); + + it('should return an error when the tank or battery section is visible and no fields are populated', () => { + const validator = service.requiresOnePopulatedTC3Field('message'); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tc3Details') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tc3Details: [ + { + tc3Type: null, + tc3PeriodicNumber: null, + tc3PeriodicExpiryDate: null, + }, + ], + }); + expect(validator(control.controls[0])).toEqual({ required: 'message' }); + }); + }); + + describe('requiresAllUnNumbersToBePopulated', () => { + it('should return null when the un number section is not visible', () => { + const validator = service.requiresAllUnNumbersToBePopulated(); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: false, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: [null], + }); + expect(validator(control)).toBeNull(); + }); + + it('should return null when the un number section is visible and all un numbers are populated', () => { + const validator = service.requiresAllUnNumbersToBePopulated(); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: + ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER, + techRecord_adrDetails_tank_tankDetails_tankStatement_select: ADRTankDetailsTankStatementSelect.PRODUCT_LIST, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: ['123', '456', '789'], + }); + expect(validator(control)).toBeNull(); + }); + + it('should return an error when the un number section is visible and one un number is not populated', () => { + const validator = service.requiresAllUnNumbersToBePopulated(); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: + ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER, + techRecord_adrDetails_tank_tankDetails_tankStatement_select: ADRTankDetailsTankStatementSelect.PRODUCT_LIST, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: ['123', null, '789'], + }); + expect(validator(control)).toEqual({ required: 'UN number 2 is required or remove UN number 2' }); + }); + }); + + describe('requiresAUnNumberOrReferenceNumber', () => { + it('should return null when the un number section is not visible', () => { + const validator = service.requiresAUnNumberOrReferenceNumber('message'); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo') as FormControl; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: false, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: null, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: [null, null, null], + }); + expect(validator(control)).toBeNull(); + }); + + it('should return null when the un number section is visible and the reference number has a value', () => { + const validator = service.requiresAUnNumberOrReferenceNumber('message'); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo') as FormControl; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: + ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER, + techRecord_adrDetails_tank_tankDetails_tankStatement_select: ADRTankDetailsTankStatementSelect.PRODUCT_LIST, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: '123', + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: [null, null, null], + }); + expect(validator(control)).toBeNull(); + }); + + it('should return null when the un number section is visible and the first un number has a value', () => { + const validator = service.requiresAUnNumberOrReferenceNumber('message'); + const control = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo') as FormControl; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: + ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER, + techRecord_adrDetails_tank_tankDetails_tankStatement_select: ADRTankDetailsTankStatementSelect.PRODUCT_LIST, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: null, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: ['123', null, null], + }); + expect(validator(control)).toBeNull(); + }); + + it('should return an error when the un number section is visible and the reference number and un numbers are empty', () => { + const validator = service.requiresAUnNumberOrReferenceNumber('message'); + const control1 = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo') as FormControl; + const control2 = form.get('techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo') as FormArray; + form.patchValue({ + techRecord_adrDetails_dangerousGoods: true, + techRecord_adrDetails_vehicleDetails_type: ADRBodyType.CENTRE_AXLE_BATTERY, + techRecord_adrDetails_tank_tankDetails_tankStatement_substancesPermitted: + ADRTankStatementSubstancePermitted.UNDER_UN_NUMBER, + techRecord_adrDetails_tank_tankDetails_tankStatement_select: ADRTankDetailsTankStatementSelect.PRODUCT_LIST, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListRefNo: null, + techRecord_adrDetails_tank_tankDetails_tankStatement_productListUnNo: [null, null, null], + }); + expect(validator(control1)).toEqual({ required: 'message' }); + expect(validator(control2)).toEqual({ required: 'message' }); + }); + }); }); diff --git a/src/app/forms/validators/__tests__/common-validators.service.spec.ts b/src/app/forms/validators/__tests__/common-validators.service.spec.ts index 849df13dc1..7307939f60 100644 --- a/src/app/forms/validators/__tests__/common-validators.service.spec.ts +++ b/src/app/forms/validators/__tests__/common-validators.service.spec.ts @@ -1 +1,155 @@ -describe('CommonValidatorsService', () => {}); +import { TestBed } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; +import { CommonValidatorsService } from '../common-validators.service'; + +describe('CommonValidatorsService', () => { + let service: CommonValidatorsService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [CommonValidatorsService], + }).compileComponents(); + + service = TestBed.inject(CommonValidatorsService); + + jest.useFakeTimers().setSystemTime(new Date('2024-01-01')); + }); + + describe('max', () => { + it('should return null if the control has a value of null', () => { + const control = new FormControl(null); + const result = service.max(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value equal to max', () => { + const control = new FormControl(10); + const result = service.max(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value less than the max', () => { + const control = new FormControl(5); + const result = service.max(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return an error object if the control has a value greater than the max', () => { + const control = new FormControl(15); + const result = service.max(10, 'message')(control); + expect(result).toEqual({ max: 'message' }); + }); + }); + + describe('min', () => { + it('should return null if the control has a value of null', () => { + const control = new FormControl(null); + const result = service.min(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value equal to min', () => { + const control = new FormControl(10); + const result = service.min(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value greater than the min', () => { + const control = new FormControl(15); + const result = service.min(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return an error object if the control has a value less than the min', () => { + const control = new FormControl(5); + const result = service.min(10, 'message')(control); + expect(result).toEqual({ min: 'message' }); + }); + }); + + describe('maxLength', () => { + it('should return null if the control has a value of null', () => { + const control = new FormControl(null); + const result = service.maxLength(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value equal to max length', () => { + const control = new FormControl('1234567890'); + const result = service.maxLength(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value less than the max length', () => { + const control = new FormControl('123456'); + const result = service.maxLength(10, 'message')(control); + expect(result).toBeNull(); + }); + + it('should return an error object if the control has a value greater than the max length', () => { + const control = new FormControl('12345678901'); + const result = service.maxLength(10, 'message')(control); + expect(result).toEqual({ maxLength: 'message' }); + }); + }); + + describe('pattern', () => { + it('should return null if the control has a value of null', () => { + const control = new FormControl(null); + const result = service.pattern('^[a-zA-Z]+$', 'message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value that matches the pattern', () => { + const control = new FormControl('abc'); + const result = service.pattern('^[a-zA-Z]+$', 'message')(control); + expect(result).toBeNull(); + }); + + it('should return an error object if the control has a value that does not match the pattern', () => { + const control = new FormControl('123'); + const result = service.pattern('^[a-zA-Z]+$', 'message')(control); + expect(result).toEqual({ pattern: 'message' }); + }); + }); + + describe('pastDate', () => { + it('should return null if the control has a value of null', () => { + const control = new FormControl(null); + const result = service.pastDate('message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value that is a past date', () => { + const control = new FormControl('2021-01-01'); // current date mocked as 2024-01-01 + const result = service.pastDate('message')(control); + expect(result).toBeNull(); + }); + + it('should return an error object if the control has a value that is a future date', () => { + const control = new FormControl('2025-01-01'); // current date mocked as 2024-01-01 + const result = service.pastDate('message')(control); + expect(result).toEqual({ pastDate: 'message' }); + }); + }); + + describe('invalidDate', () => { + it('should return null if the control has a value of null', () => { + const control = new FormControl(null); + const result = service.invalidDate('message')(control); + expect(result).toBeNull(); + }); + + it('should return null if the control has a value that is a valid date', () => { + const control = new FormControl('2021-01-01'); + const result = service.invalidDate('message')(control); + expect(result).toBeNull(); + }); + + it('should return an error object if the control has a value that is not a valid date', () => { + const control = new FormControl('abc'); + const result = service.invalidDate('message')(control); + expect(result).toEqual({ invalidDate: 'message' }); + }); + }); +}); diff --git a/src/app/forms/validators/common-validators.service.ts b/src/app/forms/validators/common-validators.service.ts index d1a69b69e8..e7aeea9b96 100644 --- a/src/app/forms/validators/common-validators.service.ts +++ b/src/app/forms/validators/common-validators.service.ts @@ -13,6 +13,16 @@ export class CommonValidatorsService { }; } + min(size: number, message: string): ValidatorFn { + return (control) => { + if (control.value && control.value < size) { + return { min: message }; + } + + return null; + }; + } + maxLength(length: number, message: string): ValidatorFn { return (control) => { if (control.value && control.value.length > length) { From 422b21cc341e1c6d392c6d1a0741dd3532073a6d Mon Sep 17 00:00:00 2001 From: Brandon Thomas-Davies <87308252+BrandonT95@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:01:56 +0100 Subject: [PATCH 018/211] chore(cb2-0000): attempt at adding controls based off vehicle type --- .../vehicle-section-edit.component.html | 10 +- .../vehicle-section-edit.component.ts | 105 +++++++++--------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html index 9360c54179..740564d7f1 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html @@ -246,7 +246,7 @@

-
+

@@ -257,7 +257,7 @@

-
+

@@ -266,13 +266,13 @@

-
+

-
+

-
+

@@ -292,11 +283,25 @@

+
+ + +
+

+ +

+ + +
+ + +
+
@@ -348,4 +353,25 @@

+ + + + +
+ +
+ The Vehicle Class is calculated automatically based on the number of seats and standing capacity. + Only change the Class if you need to +
+ + +
+
From 40547fa2be43d91ac423490102d0aa4e2f2d72dd Mon Sep 17 00:00:00 2001 From: Thomas Crawley Date: Thu, 3 Oct 2024 09:54:16 +0100 Subject: [PATCH 023/211] feat(cb2-0000): styling POC --- .../vehicle-section-edit.component.html | 10 ++--- src/global-styles.scss | 44 ++++++++++++------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html index 1cf9bd2d80..5f846d01fc 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html @@ -324,14 +324,14 @@

-
-
- -

+
+
+ +

Departmental vehicle marker

-
+
div { - @extend .govuk-radios__item; - - > input { - @extend .govuk-radios__input; - } - - > label { - @extend .govuk-label; - @extend .govuk-radios__label; - } - } -} +//.govuk-form-group-radio { +// @extend .govuk-form-group; +// > fieldset { +// @extend .govuk-fieldset; +// > legend { +// @extend .govuk-fieldset__legend; +// @extend .govuk-fieldset__legend--m; +// > h1 { +// @extend .govuk-fieldset__heading; +// } +// } +// > div { +// @extend .govuk-radios; +// > div { +// @extend .govuk-radios__item; +// +// > input { +// @extend .govuk-radios__input; +// } +// +// > label { +// @extend .govuk-label; +// @extend .govuk-radios__label; +// } +// } +// } +// } +//} From c9bf763016bdbd773c4a634d654d4018052974c8 Mon Sep 17 00:00:00 2001 From: pbardy2000 <146740183+pbardy2000@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:45:41 +0100 Subject: [PATCH 024/211] chore(cb2-0000): styling fixes --- package-lock.json | 2 +- package.json | 2 +- .../govuk-radio/govuk-radio.directive.ts | 3 + .../vehicle-section-edit.component.html | 10 +-- src/global-styles.scss | 61 +++++++++---------- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index aabccdbf0b..62b8b8e943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "accessible-autocomplete": "^2.0.4", "angular-google-tag-manager": "^1.10.0", "deep-object-diff": "^1.1.9", - "govuk-frontend": "^4.7.0", + "govuk-frontend": "^4.8.0", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", diff --git a/package.json b/package.json index e0fafc0505..1dfdd648b9 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "accessible-autocomplete": "^2.0.4", "angular-google-tag-manager": "^1.10.0", "deep-object-diff": "^1.1.9", - "govuk-frontend": "^4.7.0", + "govuk-frontend": "^4.8.0", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", diff --git a/src/app/directives/govuk-radio/govuk-radio.directive.ts b/src/app/directives/govuk-radio/govuk-radio.directive.ts index ce3444be32..aa7e76dc26 100644 --- a/src/app/directives/govuk-radio/govuk-radio.directive.ts +++ b/src/app/directives/govuk-radio/govuk-radio.directive.ts @@ -16,6 +16,9 @@ export class GovukRadioDirective implements OnInit, OnDestroy { destroy$ = new ReplaySubject(1); ngOnInit(): void { + this.elementRef.nativeElement.setAttribute('type', 'radio'); + this.elementRef.nativeElement.classList.add('govuk-radios__input'); + const formControlName = this.formControlName(); const control = this.controlContainer.control?.get(formControlName); if (control) { diff --git a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html index 5f846d01fc..d807181ce3 100644 --- a/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html +++ b/src/app/forms/custom-sections/vehicle-section/vehicle-section-edit/vehicle-section-edit.component.html @@ -39,7 +39,7 @@

-
+

@@ -76,14 +76,16 @@

-
+