diff --git a/front-end/src/app/app.module.ts b/front-end/src/app/app.module.ts index bb5aeb430d..b9dfa655b7 100644 --- a/front-end/src/app/app.module.ts +++ b/front-end/src/app/app.module.ts @@ -43,6 +43,7 @@ import { TwoFactorLoginComponent } from './login/two-factor-login/two-factor-log import { ConfirmTwoFactorComponent } from './login/confirm-two-factor/confirm-two-factor.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { HttpErrorInterceptor } from './shared/interceptors/http-error.interceptor'; +import { FecDatePipe } from './shared/pipes/fec-date.pipe'; // Save ngrx store to localStorage dynamically function localStorageSyncReducer(reducer: ActionReducer): ActionReducer { @@ -96,6 +97,7 @@ const metaReducers: Array> = [localStorageSyncRedu ConfirmationService, MessageService, { provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true }, + FecDatePipe, ], bootstrap: [AppComponent], }) diff --git a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.html b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.html index 08dbfa88c7..102e9e9a3d 100644 --- a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.html +++ b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.html @@ -121,7 +121,7 @@

Covering period

name="coverage_from_date" formControlName="coverage_from_date" > - +
@@ -132,7 +132,7 @@

Covering period

name="coverage_through_date" formControlName="coverage_through_date" > - +
diff --git a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts index 3210547149..b7b139b3fc 100644 --- a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts +++ b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.spec.ts @@ -16,6 +16,7 @@ import { RadioButtonModule } from 'primeng/radiobutton'; import { SelectButtonModule } from 'primeng/selectbutton'; import { CreateF3XStep1Component, F3xReportTypeCategories } from './create-f3x-step1.component'; import { selectUserLoginData } from 'app/store/login.selectors'; +import { FecDatePipe } from 'app/shared/pipes/fec-date.pipe'; describe('CreateF3XStep1Component', () => { let component: CreateF3XStep1Component; @@ -50,6 +51,7 @@ describe('CreateF3XStep1Component', () => { F3xSummaryService, FormBuilder, MessageService, + FecDatePipe, provideMockStore({ initialState: { fecfile_online_userLoginData: userLoginData }, selectors: [{ selector: selectUserLoginData, value: userLoginData }], diff --git a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.ts b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.ts index 6247da1313..839d9655a8 100644 --- a/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.ts +++ b/front-end/src/app/reports/f3x/create-workflow/create-f3x-step1.component.ts @@ -1,6 +1,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Store } from '@ngrx/store'; import { electionReportCodes, + F3xCoverageDates, F3xFormTypes, F3xReportCode, F3xReportCodes, @@ -8,19 +12,17 @@ import { monthlyElectionYearReportCodes, monthlyNonElectionYearReportCodes, quarterlyElectionYearReportCodes, - quarterlyNonElectionYearReportCodes, + quarterlyNonElectionYearReportCodes } from 'app/shared/models/f3x-summary.model'; +import { FecDatePipe } from 'app/shared/pipes/fec-date.pipe'; +import { F3xSummaryService } from 'app/shared/services/f3x-summary.service'; +import { ValidateService } from 'app/shared/services/validate.service'; import { LabelList, LabelUtils, PrimeOptions, StatesCodeLabels } from 'app/shared/utils/label.utils'; import { selectCommitteeAccount } from 'app/store/committee-account.selectors'; -import { ValidateService } from 'app/shared/services/validate.service'; -import { schema as f3xSchema } from 'fecfile-validate/fecfile_validate_js/dist/F3X'; -import { Subject, takeUntil } from 'rxjs'; -import { Store } from '@ngrx/store'; -import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { environment } from 'environments/environment'; -import { ActivatedRoute, Router } from '@angular/router'; -import { F3xSummaryService } from 'app/shared/services/f3x-summary.service'; +import { schema as f3xSchema } from 'fecfile-validate/fecfile_validate_js/dist/F3X'; import { MessageService } from 'primeng/api'; +import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-create-f3x-step1', @@ -77,16 +79,18 @@ export class CreateF3XStep1Component implements OnInit, OnDestroy { ]; readonly F3xReportTypeCategories = F3xReportTypeCategories; + private f3xCoverageDatesList: F3xCoverageDates[] | undefined; constructor( private store: Store, private validateService: ValidateService, + private fecDatePipe: FecDatePipe, private fb: FormBuilder, private f3xSummaryService: F3xSummaryService, private messageService: MessageService, private activatedRoute: ActivatedRoute, protected router: Router - ) {} + ) { } ngOnInit(): void { const report: F3xSummary = this.activatedRoute.snapshot.data['report']; @@ -123,11 +127,59 @@ export class CreateF3XStep1Component implements OnInit, OnDestroy { }); this.stateOptions = LabelUtils.getPrimeOptions(StatesCodeLabels); + this.f3xSummaryService.getF3xCoverageDates().subscribe((dates) => { + this.f3xCoverageDatesList = dates; + }); + this.form.controls['coverage_from_date'].addValidators( + this.buildCoverageDatesValidator('coverage_from_date')); + this.form.controls['coverage_through_date'].addValidators( + this.buildCoverageDatesValidator('coverage_through_date')); + // Initialize validation tracking of current JSON schema and form data this.validateService.formValidatorSchema = f3xSchema; this.validateService.formValidatorForm = this.form; } + buildCoverageDatesValidator(valueFormControlName: string,): ValidatorFn { + return (): ValidationErrors | null => { + let result: ValidationErrors | null = null; + const formValue: Date = this.form?.get(valueFormControlName)?.value + if (this.f3xCoverageDatesList && formValue) { + const retval = this.f3xCoverageDatesList.find((f3xCoverageDate) => { + return (f3xCoverageDate && + f3xCoverageDate.coverage_from_date && + f3xCoverageDate.coverage_through_date && + (formValue >= f3xCoverageDate.coverage_from_date && + formValue <= f3xCoverageDate.coverage_through_date) + ) + }); + result = this.getCoverageDatesValidator(retval); + } + return result; + }; + } + + getCoverageDatesValidator(f3xCoverageDates?: F3xCoverageDates) { + let retval: ValidationErrors | null = null; + if (f3xCoverageDates) { + const f3xReportCodeLabel = this.f3xReportCodeCreationLabels.find( + label => label[0] === f3xCoverageDates.report_code); + const reportCodeLabel = f3xReportCodeLabel ? f3xReportCodeLabel[1] || + f3xCoverageDates.report_code?.valueOf : 'invalid name'; + const coverageFromDate = this.fecDatePipe.transform( + f3xCoverageDates.coverage_from_date); + const coverageThroughDate = this.fecDatePipe.transform( + f3xCoverageDates.coverage_through_date); + retval = {}; + retval['invaliddate'] = { + msg: `You have entered coverage dates that overlap ` + + `the coverage dates of the following report: ${reportCodeLabel} ` + + ` ${coverageFromDate} - ${coverageThroughDate}` + }; + } + return retval; + } + ngOnDestroy(): void { this.destroy$.next(true); this.destroy$.complete(); diff --git a/front-end/src/app/shared/components/error-messages/error-messages.component.html b/front-end/src/app/shared/components/error-messages/error-messages.component.html index b3df7051ca..5990960cf8 100644 --- a/front-end/src/app/shared/components/error-messages/error-messages.component.html +++ b/front-end/src/app/shared/components/error-messages/error-messages.component.html @@ -2,9 +2,10 @@ {{ requiredErrorMessage }} {{ minLengthErrorMessage }} {{ maxLengthErrorMessage }} + {{ invalidDateErrorMessage }} {{ patternErrorMessage }} diff --git a/front-end/src/app/shared/components/error-messages/error-messages.component.ts b/front-end/src/app/shared/components/error-messages/error-messages.component.ts index d617ae53a8..3c1e26501e 100644 --- a/front-end/src/app/shared/components/error-messages/error-messages.component.ts +++ b/front-end/src/app/shared/components/error-messages/error-messages.component.ts @@ -34,6 +34,17 @@ export class ErrorMessagesComponent implements OnInit { return `This field cannot contain more than ${this.control?.errors?.['maxlength']?.requiredLength} alphanumeric characters.`; } + private _invalidDateErrorMessage = ''; + @Input() set invalidDateErrorMessage(value: string) { + this._invalidDateErrorMessage = value; + } + get invalidDateErrorMessage(): string { + if (this._invalidDateErrorMessage) { + return this._invalidDateErrorMessage; + } + return this.control?.errors?.['invaliddate']?.msg; + } + control: FormGroup | null = null; ngOnInit(): void { diff --git a/front-end/src/app/shared/models/f3x-summary.model.ts b/front-end/src/app/shared/models/f3x-summary.model.ts index 119905e7dd..7f2168a014 100644 --- a/front-end/src/app/shared/models/f3x-summary.model.ts +++ b/front-end/src/app/shared/models/f3x-summary.model.ts @@ -1,7 +1,7 @@ -import { BaseModel } from './base.model'; -import { Report } from '../interfaces/report.interface'; import { plainToClass, Transform } from 'class-transformer'; +import { Report } from '../interfaces/report.interface'; import { LabelList } from '../utils/label.utils'; +import { BaseModel } from './base.model'; export enum F3xFormTypes { F3XN = 'F3XN', @@ -146,6 +146,16 @@ export const electionReportCodes: F3xReportCode[] = [ F3xReportCodes.TwelveS, ]; +export class F3xCoverageDates { + @Transform(BaseModel.dateTransform) coverage_from_date: Date | null = null; + @Transform(BaseModel.dateTransform) coverage_through_date: Date | null = null; + report_code: F3xReportCodes | null = null; + // prettier-ignore + static fromJSON(json: any): F3xCoverageDates { // eslint-disable-line @typescript-eslint/no-explicit-any + return plainToClass(F3xCoverageDates, json); + } +} + export class F3xSummary extends BaseModel implements Report { id: number | null = null; diff --git a/front-end/src/app/shared/services/f3x-summary.service.ts b/front-end/src/app/shared/services/f3x-summary.service.ts index bba4e7385a..07ffdd94b5 100644 --- a/front-end/src/app/shared/services/f3x-summary.service.ts +++ b/front-end/src/app/shared/services/f3x-summary.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { F3xCoverageDates, F3xSummary } from '../models/f3x-summary.model'; import { ApiService } from './api.service'; -import { F3xSummary } from '../models/f3x-summary.model'; @Injectable({ providedIn: 'root', @@ -10,6 +10,13 @@ import { F3xSummary } from '../models/f3x-summary.model'; export class F3xSummaryService { constructor(private apiService: ApiService) {} + public getF3xCoverageDates(): Observable { + return this.apiService + .get(`/f3x-summaries/coverage_dates`) + .pipe(map((response) => response.map(fx3CoverageDate => + F3xCoverageDates.fromJSON(fx3CoverageDate)))); + } + public get(id: number): Observable { return this.apiService .get(`/f3x-summaries/${id}`)