Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/1583 - Linked Report Field #1761

Merged
merged 13 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<h1>{{ transaction?.transactionType?.title }}</h1>
<div *ngIf="transactionType?.subTitle">{{ transactionType?.subTitle }}</div>
<app-independent-expenditure-create-f3x-input></app-independent-expenditure-create-f3x-input>
<h4 class="read-only-tag subtitle" *ngIf="!isEditable">READ ONLY</h4>
<p class="group-description">{{ transactionType?.description }}</p>
<p-accordion [activeIndex]="accordionActiveIndex">
Expand Down Expand Up @@ -66,4 +67,3 @@ <h3>{{ childTransactionType?.contactTitle }}</h3>
</div>
</p-accordionTab>
</p-accordion>

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ <h1 [ngClass]="{ 'dark-gray-text': transactionType?.showParentTransactionTitle }
{{ transactionType?.title }}
</h1>
<div *ngIf="transactionType?.subTitle">{{ transactionType?.subTitle }}</div>
<app-independent-expenditure-create-f3x-input></app-independent-expenditure-create-f3x-input>
<h4 class="read-only-tag subtitle" *ngIf="!isEditable">READ ONLY</h4>
<h4 class="read-only-tag subtitle" *ngIf="isDebtRepayment(transaction)">DEBT REPAYMENT</h4>
<h2>{{ transactionType?.contactTitle || 'Contact' }}</h2>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<h1>{{ transaction?.transactionType?.title }}</h1>
<div *ngIf="transactionType?.subTitle">{{ transactionType?.subTitle }}</div>
<app-independent-expenditure-create-f3x-input></app-independent-expenditure-create-f3x-input>
<h4 class="read-only-tag subtitle" *ngIf="!isEditable">READ ONLY</h4>
<p class="group-description">{{ transactionType?.description }}</p>
<p-accordion [activeIndex]="accordionActiveIndex">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<small class="p-error" *ngIf="control?.hasError('exclusiveMax')">{{ exclusiveMaxErrorMessage }}</small>
<small class="p-error" *ngIf="control?.hasError('exclusiveMin')">{{ exclusiveMinErrorMessage }}</small>
<small class="p-error" *ngIf="control?.hasError('invaliddate')">{{ invalidDateErrorMessage }}</small>
<small class="p-error" *ngIf="control?.hasError('noDateProvided')">{{ noDateProvidedErrorMessage }}</small>
<small class="p-error" *ngIf="control?.hasError('noCorrespondingForm3X')">{{
noCorrespondingForm3XErrorMessage
}}</small>
<small
class="p-error"
*ngIf="!control?.hasError('maxlength') && !control?.hasError('required') && control?.hasError('email')"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,30 @@ export class ErrorMessagesComponent implements OnInit {
return this.control?.errors?.['invaliddate']?.msg;
}

private _noDateProvidedErrorMessage = '';
@Input() set noDateProvidedErrorMessage(value: string) {
this._noDateProvidedErrorMessage = value;
}

get noDateProvidedErrorMessage(): string {
if (this._noDateProvidedErrorMessage) {
return this._noDateProvidedErrorMessage;
}
return 'A disbursement or dissemination date is required to provide a linked report.';
}

private _noCorrespondingForm3XErrorMessage = '';
@Input() set noCorrespondingForm3XErrorMessage(value: string) {
this._noCorrespondingForm3XErrorMessage = value;
}

get noCorrespondingForm3XErrorMessage(): string {
if (this._noCorrespondingForm3XErrorMessage) {
return this._noCorrespondingForm3XErrorMessage;
}
return 'There is no Form 3X with corresponding coverage dates currently in progress. Create a new Form 3X to save this transaction.';
}

constructor(@Inject(LOCALE_ID) private localeId: string) {}

ngOnInit(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
></app-error-messages>
</div>
</div>
<div class="col-4">
toddlees marked this conversation as resolved.
Show resolved Hide resolved
<app-linked-report-input [form]="this.form" [templateMap]="this.templateMap"></app-linked-report-input>
</div>
</div>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<ng-container *ngIf="this.activeReport?.report_type === form24ReportType">
<p class="ie-explanation">
Independent expenditures (IE) submitted on a Form 24 must also be submitted on the regularly scheduled Form 3X
report. In order to create an IE on the Form 24, the IE's date of disbursement must fall within the corresponding
Form 3X coverage dates. If date of disbursement is not available, date of dissemination will be used. IEs already
created on a Form 3X can be added to an existing Form 24.
</p>
<button pButton class="p-button-secondary ie-explanation-button-create" (click)="create()">Create Form 3X</button>
<button pButton class="p-button-secondary ie-explanation-button-cancel" (click)="cancel()">
Return to manage reports
</button>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

.ie-explanation {
margin-top: 16px;
margin-bottom: 8px;
max-width: 1000px;
}

.ie-explanation-button-create {
margin-left: 0px;
margin-right: 8px;
margin-bottom: 16px;
}

.ie-explanation-button-cancel {
margin-left: 8px;
margin-bottom: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { testMockStore } from 'app/shared/utils/unit-test.utils';
import { Form24 } from 'app/shared/models/form-24.model';
import { provideMockStore } from '@ngrx/store/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { IndependentExpenditureCreateF3xInputComponent } from './independent-expenditure-create-f3x-input.component';
import { TooltipModule } from 'primeng/tooltip';
import { RouterTestingModule } from '@angular/router/testing';

describe('IndependentExpenditureCreateF3xInputComponent', () => {
let component: IndependentExpenditureCreateF3xInputComponent;
let fixture: ComponentFixture<IndependentExpenditureCreateF3xInputComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [IndependentExpenditureCreateF3xInputComponent],
imports: [HttpClientTestingModule, TooltipModule],
providers: [provideMockStore(testMockStore), RouterTestingModule],
}).compileComponents();

fixture = TestBed.createComponent(IndependentExpenditureCreateF3xInputComponent);
component = fixture.componentInstance;
component.activeReport = Form24.fromJSON({});
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectActiveReport } from 'app/store/active-report.selectors';
import { firstValueFrom } from 'rxjs';
import { BaseInputComponent } from '../base-input.component';
import { Report, ReportTypes } from 'app/shared/models/report.model';
import { Router } from '@angular/router';

@Component({
selector: 'app-independent-expenditure-create-f3x-input',
styleUrls: ['./independent-expenditure-create-f3x-input.component.scss'],
templateUrl: './independent-expenditure-create-f3x-input.component.html',
})
export class IndependentExpenditureCreateF3xInputComponent extends BaseInputComponent implements OnInit {
activeReport?: Report;
form24ReportType = ReportTypes.F24;

constructor(
private store: Store,
private router: Router,
) {
super();
}

ngOnInit(): void {
firstValueFrom(this.store.select(selectActiveReport)).then((report) => {
this.activeReport = report;
});
}

cancel(): void {
this.router.navigateByUrl('/reports');
}

create(): void {
this.router.navigateByUrl('/reports/f3x/create/step1');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<ng-container *ngIf="this.activeReport?.report_type === form24ReportType">
<form id="form" [formGroup]="form" class="p-fluid">
<div class="field">
<label for="linkedF3x">Linked report</label>
<span
class="pi pi-info-circle"
style="font-size: 1.5rem; margin-left: 0.5rem"
pTooltip="{{ tooltipText }}"
[tooltipOptions]="{
tooltipPosition: 'top'
}"
></span>
<input type="text" class="p-disabled" pInputText id="linkedF3x" formControlName="linkedF3x" />
<app-error-messages [form]="form" fieldName="linkedF3x" [formSubmitted]="formSubmitted"></app-error-messages>
</div>
</form>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { InputTextModule } from 'primeng/inputtext';
import { ErrorMessagesComponent } from '../../error-messages/error-messages.component';
import { testMockStore, testTemplateMap } from 'app/shared/utils/unit-test.utils';
import { LinkedReportInputComponent } from './linked-report-input.component';
import { FecDatePipe } from 'app/shared/pipes/fec-date.pipe';
import { Form24 } from 'app/shared/models/form-24.model';
import { provideMockStore } from '@ngrx/store/testing';
import { ReportService } from 'app/shared/services/report.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { firstValueFrom, of } from 'rxjs';
import { Form3X } from 'app/shared/models/form-3x.model';
import { F3xReportCodes } from 'app/shared/utils/report-code.utils';

describe('LinkedReportInputComponent', () => {
let component: LinkedReportInputComponent;
let fixture: ComponentFixture<LinkedReportInputComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LinkedReportInputComponent, ErrorMessagesComponent],
imports: [HttpClientTestingModule, InputTextModule, ReactiveFormsModule, FormsModule],
providers: [ReportService, FecDatePipe, provideMockStore(testMockStore)],
}).compileComponents();

fixture = TestBed.createComponent(LinkedReportInputComponent);
component = fixture.componentInstance;
component.templateMap = Object.assign(testTemplateMap, {
date2: 'other_date',
});
component.activeReport = Form24.fromJSON({});
component.form = new FormGroup({});
component.form.addControl('other_date', new FormControl());
component.form.addControl(testTemplateMap['date'], new FormControl());
component.ngOnInit();
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should try to determine the linked F3X report when the dates change', async () => {
const spy = spyOn(component, 'getLinkedForm3X').and.returnValue(Promise.resolve(Form3X.fromJSON({})));

component.form.get('other_date')?.setValue('2025-02-12');
component.form.get(testTemplateMap['date'])?.setValue('2025-02-12');

fixture.detectChanges();

expect(spy).toHaveBeenCalledTimes(4);
});

it('should determine the correct label', () => {
const testF3X = Form3X.fromJSON({
coverage_from_date: '2020-01-15',
coverage_through_date: '2020-04-29',
report_code: F3xReportCodes.Q1,
});

component.committeeF3xReports = firstValueFrom(of([testF3X]));

component.form.get('other_date')?.setValue(new Date('2020-02-21'));
component.getLinkedForm3X(undefined, new Date('2020-02-21')).then((report) => {
expect(report).toEqual(testF3X);
expect(component.getForm3XLabel(report)).toEqual('APRIL 15 (Q1): 01/15/2020 - 04/29/2020');
});

component.form.get('other_date')?.setValue(new Date('2022-06-22'));
component.getLinkedForm3X(undefined, new Date('2022-06-22')).then((report) => {
expect(report).toEqual(undefined);
expect(component.getForm3XLabel(report)).toEqual('');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectActiveReport } from 'app/store/active-report.selectors';
import { combineLatestWith, firstValueFrom, startWith, takeUntil } from 'rxjs';
import { BaseInputComponent } from '../base-input.component';
import { Report, ReportTypes } from 'app/shared/models/report.model';
import { ReportService } from 'app/shared/services/report.service';
import { Form3X } from 'app/shared/models/form-3x.model';
import { getReportCodeLabel } from 'app/shared/utils/report-code.utils';
import { FecDatePipe } from 'app/shared/pipes/fec-date.pipe';
import { FormControl } from '@angular/forms';
import { buildCorrespondingForm3XValidator } from 'app/shared/utils/validators.utils';

@Component({
selector: 'app-linked-report-input',
templateUrl: './linked-report-input.component.html',
})
export class LinkedReportInputComponent extends BaseInputComponent implements OnInit {
activeReport?: Report;
committeeF3xReports: Promise<Report[]>;
form24ReportType = ReportTypes.F24;
linkedF3xControl = new FormControl();

tooltipText =
'Transactions created in Form 24 must be linked to a Form 3X with corresponding coverage dates. ' +
'To determine coverage dates, calculations rely on an IE’s date of disbursement. If date of disbursement is not ' +
'available, date of dissemination will be used. Before saving this transaction, create a Form 3X with ' +
'corresponding coverage dates.';

constructor(
private store: Store,
private reportService: ReportService,
private datePipe: FecDatePipe,
) {
super();
this.committeeF3xReports = this.reportService.getAllReports();
}

ngOnInit(): void {
this.form.addControl('linkedF3x', this.linkedF3xControl);
const dateControl = this.form.get(this.templateMap['date']) ?? new FormControl();
const date2Control = this.form.get(this.templateMap['date2']) ?? new FormControl();
this.linkedF3xControl.addValidators(buildCorrespondingForm3XValidator(dateControl, date2Control));

firstValueFrom(this.store.select(selectActiveReport)).then((report) => {
this.activeReport = report;
});

dateControl.valueChanges
.pipe(
startWith(dateControl.value),
combineLatestWith(date2Control.valueChanges.pipe(startWith(date2Control.value))),
takeUntil(this.destroy$),
)
.subscribe(this.setLinkedForm3X.bind(this));
}

setLinkedForm3X([disbursementDate, disseminationDate]: (Date | undefined)[]): void {
this.getLinkedForm3X(disbursementDate, disseminationDate).then((report) => {
this.form.get('linkedF3x')?.setValue(this.getForm3XLabel(report));
this.form.get('linkedF3x')?.markAsTouched();
});
}

async getLinkedForm3X(disbursementDate?: Date, disseminationDate?: Date): Promise<Form3X | undefined> {
const date = disbursementDate ?? disseminationDate;
if (date) {
const reports = await this.committeeF3xReports.then((reports) => {
return reports.filter((report) => {
return report.report_type === ReportTypes.F3X;
}) as Form3X[];
});

for (const report of reports) {
if (report.coverage_from_date && report.coverage_through_date) {
if (date >= report.coverage_from_date && date <= report.coverage_through_date) {
return report;
}
}
}
}

return undefined;
}

getForm3XLabel(report: Form3X | undefined): string {
if (!report) return '';

let label = getReportCodeLabel(report.report_code);
const stringsToRemove = [' MID-YEAR-REPORT', ' YEAR-END', ' QUARTERLY REPORT', ' MONTHLY REPORT'];
for (const string of stringsToRemove) {
label = label?.replaceAll(string, '');
}

return `${label}: ${this.datePipe.transform(report.coverage_from_date)} - ${this.datePipe.transform(
report.coverage_through_date,
)}`;
}
}
8 changes: 7 additions & 1 deletion front-end/src/app/shared/services/report.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { map, Observable, tap } from 'rxjs';
import { firstValueFrom, map, Observable, tap } from 'rxjs';
import { Store } from '@ngrx/store';
import { setActiveReportAction } from 'app/store/active-report.actions';
import { Report, ReportTypes } from '../models/report.model';
Expand Down Expand Up @@ -42,6 +42,12 @@ export class ReportService implements TableListService<Report> {
);
}

public getAllReports(): Promise<Report[]> {
return firstValueFrom(this.apiService.get<Report[]>(this.apiEndpoint + '/')).then((rawReports) => {
return rawReports.map((item) => getReportFromJSON(item));
});
}

public get(reportId: string): Observable<Report> {
return this.apiService
.get<Report>(`${this.apiEndpoint}/${reportId}`)
Expand Down
Loading