This commit is contained in:
Zeeshaun
2026-03-25 11:54:22 -05:00
parent 4d52ebc209
commit 883009770f
776 changed files with 86726 additions and 0 deletions
@@ -0,0 +1,30 @@
import { Validators } from '@angular/forms';
export class AdminGroups {
private static DATE_PATTERN = /^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/;
public static readonly FILTER = {
group: 'filterInformation',
fields: [
{field: 'startDate', validation: [Validators.required, Validators.pattern(AdminGroups.DATE_PATTERN)]},
{field: 'endDate', validation: [Validators.required, Validators.pattern(AdminGroups.DATE_PATTERN)]},
{field: 'loanType', validation: []},
{field: 'branch', validation: []},
{field: 'decisioned', validation: []},
{field: 'submitted', validation: []},
{field: 'inProgress', validation: []},
{field: 'error', validation: []},
{field: 'deleted', validation: []},
]
};
public static readonly CONFIG = {
group: 'configInformation',
fields: [
{field: 'appStatus', validation: []},
]
};
}
@@ -0,0 +1,31 @@
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { AdminComponent } from './admin.component';
import { LoanTableComponent } from './loan-table/loan-table.component';
import { LoanDataComponent } from './loan-data/loan-data.component';
import { BranchConfigComponent } from './branch-config/branch-config.component';
import { BranchNameConfigComponent } from './branch-name-config/branch-name-config.component';
import { SystemConfigComponent } from './system-config/system-config.component';
import { SchumerPreviewComponent } from './schumer-boxes/schumer-preview.component';
const routes: Routes = [
{
path: '',
component: AdminComponent,
children: [
{path: 'branch-config', component: BranchConfigComponent},
{path: 'branch-name-config', component: BranchNameConfigComponent},
{path: 'loan-table', component: LoanTableComponent},
{path: 'system-config', component: SystemConfigComponent},
{path: 'schumer-preview', component: SchumerPreviewComponent},
{path: 'loan-data/:id', component: LoanDataComponent},
{path: '', redirectTo: '/admin', pathMatch: 'prefix'},
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule {}
@@ -0,0 +1,7 @@
<div *ngIf ="router.url == '/admin'">
<lw-admin-landing></lw-admin-landing>
</div>
<div>
<router-outlet></router-outlet>
</div>
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'lw-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss']
})
export class AdminComponent {
constructor(public router: Router ) {}
}
@@ -0,0 +1,68 @@
import { NgModule } from '@angular/core';
import { MatSortModule } from '@angular/material/sort';
import { MatPaginatorModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule,
MatTooltipModule, MatSnackBarModule, MatSlideToggleModule } from '@angular/material';
import { MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { AdminComponent } from './admin.component';
import { LoanTableComponent } from './loan-table/loan-table.component';
import { LoanDataComponent } from './loan-data/loan-data.component';
import { AdminRoutingModule } from './admin-routing.module';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BranchConfigComponent } from './branch-config/branch-config.component';
import { BranchNameConfigComponent } from './branch-name-config/branch-name-config.component';
import { AdminLandingComponent } from './landing/admin-landing.component';
import { MainCardAdminComponent } from './cards/main-card/main-card.component';
import { TopNavigationComponent } from './navigation/top-navigation.component';
import { SharedModule } from '../shared/shared.module';
import { SnackBarErrorComponent } from './snack-bar-error/snack-bar-error.component';
import { SystemConfigComponent } from './system-config/system-config.component';
import { SnackBarService } from './snack-bar-error/snack-bar.service';
import { SchumerPreviewComponent } from './schumer-boxes/schumer-preview.component';
@NgModule({
imports: [
AdminRoutingModule,
CommonModule,
SharedModule,
FormsModule,
MatSortModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
MatTooltipModule,
MatDatepickerModule,
MatNativeDateModule,
MatSelectModule,
MatSnackBarModule,
MatCheckboxModule,
MatSlideToggleModule,
ReactiveFormsModule,
TranslateModule,
],
entryComponents: [
SnackBarErrorComponent
],
declarations: [
AdminComponent,
AdminLandingComponent,
BranchConfigComponent,
BranchNameConfigComponent,
LoanTableComponent,
LoanDataComponent,
SnackBarErrorComponent,
MainCardAdminComponent,
TopNavigationComponent,
SchumerPreviewComponent,
SystemConfigComponent
],
providers: [
MatDatepickerModule,
SnackBarService
],
})
export class AdminModule {}
@@ -0,0 +1,81 @@
<lw-top-navigation></lw-top-navigation>
<div class="card mat-elevation-z8">
<div class="card-header address-compare-panel-header">
<h4 translate>BRANCHMAP.TITLE</h4>
<span translate>BRANCHMAP.SUBTEXT</span>
<p *ngIf="lastUpdated">
<span translate>BRANCHMAP.DATETEXT</span><span style="font-weight: bold;">{{lastUpdated | date:'short'}}</span>
</p>
</div>
<div class="row card-body shift-right">
<div class="col-5">
<div class="icon-input-row">
<div class="btn-group btn-group-toggle row">
<div class="btn-group-child">
<div><strong translate>BRANCHMAP.DOWNLOAD</strong></div>
<button type="button" class="btn btn-success btn-less-padding" style="margin-top: 5px" (click)="commandExportCSV()">CSV Export
</button>
</div>
</div>
</div>
</div>
<div class=" btn-vertical-divider"></div>
<div class="col-5">
<div class="icon-input-row">
<div class="btn-group btn-group-toggle row">
<div class="btn-group-child" style="display: inline-block">
<div><strong translate>BRANCHMAP.UPLOAD</strong></div>
<input type="file"
#fileImportInput
name="File Upload"
id="txtFileUpload"
class="btn btn-primary"
(change)="fileChangeListener($event)"
accept=".csv"/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mat-elevation-z8" *ngIf="localState.compareResult !== null">
<div class="row card-body">
<div class="col-8">
<div *ngIf="localState.compareResult.inError === false">
<div><strong translate>Please confirm your upload</strong></div>
<ul>
<li>There are currently {{localState.compareResult.currentCount}} branch mappings.</li>
<li>There will be {{localState.compareResult.newCount}} branch mappings.</li>
<li *ngFor="let warning of localState.compareResult.warnings">{{warning}}</li>
</ul>
<button type="button" class="btn-success btn btn-less-padding" style="margin-top: 5px" (click)="commandConfirmSave()">Save New Branch Mappings
</button>
</div>
<div *ngIf="localState.compareResult.inError === true">
<div><strong translate>There was a problem with your upload, it will not be saved.</strong></div>
<ul>
<li>{{localState.compareResult.errorMessage}}</li>
<li *ngFor="let warning of localState.compareResult.warnings">{{warning}}</li>
</ul>
</div>
</div>
</div>
</div>
<div class="mat-elevation-z8" *ngIf="localState.pendingData === null">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="zipcode">
<th mat-header-cell *matHeaderCellDef mat-sort-header>ZipCode</th>
<td mat-cell *matCellDef="let loan">{{loan.zipcode}} </td>
</ng-container>
<ng-container matColumnDef="channelId">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Channel ID</th>
<td mat-cell *matCellDef="let loan">{{loan.channelId}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
@@ -0,0 +1,38 @@
table {
width: 100%;
}
.mat-form-field {
margin-right: 10px;
font-size: .875em;
width: 20%;
}
.mat-header-cell {
font-size: 1em;
font-weight: bold;
color: black;
}
.mat-checkbox {
margin-right: 10px;
:host ::ng-deep .mat-checkbox-background {
background-color: #005B9A
}
}
.btn-disabled {
margin-bottom: 0px;
margin-left: 0px;
}
.btn-less-padding {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 8px;
padding-right: 8px;
}
.shift-right {
margin-left: 5px;
}
@@ -0,0 +1,66 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { configureTestSuite } from 'ng-bullet';
import { BranchConfigComponent } from './branch-config.component';
import { MatSortModule, MatPaginatorModule, MatTableModule, MatInputModule,
MatDatepickerModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, MatSnackBar, MatSnackBarModule } from '@angular/material';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe';
import { BranchConfigService } from './branch-config.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TopNavigationComponent } from '../navigation/top-navigation.component';
import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component';
import { TranslateModule } from '@ngx-translate/core';
describe('BranchConfigComponent', () => {
let component: BranchConfigComponent;
let fixture: ComponentFixture<BranchConfigComponent>;
let service: BranchConfigService;
let snackBar: MatSnackBar;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
MatSortModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
MatSnackBarModule,
MatDatepickerModule,
MatNativeDateModule,
MatSelectModule,
MatCheckboxModule,
ReactiveFormsModule,
HttpClientTestingModule,
NoopAnimationsModule,
TranslateModule.forRoot()
],
declarations: [
SnackBarErrorComponent,
BranchConfigComponent,
LoanCodePipe,
TopNavigationComponent
],
providers: [
]
});
});
beforeEach(() => {
service = TestBed.get(BranchConfigService);
snackBar = TestBed.get(MatSnackBar);
fixture = TestBed.createComponent(BranchConfigComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
expect(service).toBeTruthy();
fixture.detectChanges();
});
});
@@ -0,0 +1,126 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { BranchConfigService } from './branch-config.service';
import { Subscription } from 'rxjs';
import { MatTableDataSource, MatSort } from '@angular/material';
import { BranchConfig } from './branch-config';
import { ServerError } from '../snack-bar-error/server-error';
import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component';
import { SnackBarService } from '../snack-bar-error/snack-bar.service';
@Component({
selector: 'lw-branch-config',
templateUrl: './branch-config.component.html',
styleUrls: ['./branch-config.component.scss']
})
export class BranchConfigComponent implements OnInit, OnDestroy {
private dataSubscription: Subscription;
public dataSource: MatTableDataSource<any>;
public lastUpdated: Date;
public displayedColumns: string[] = ['zipcode', 'channelId'];
public localState = {
uploadConfirmed: false,
compareResult: null,
pendingData: null
};
@ViewChild(MatSort) sort: MatSort;
@ViewChild('fileImportInput') fileImportInput: any;
constructor(private branchConfigService: BranchConfigService, private snackBarService: SnackBarService) {}
public ngOnInit() {
this.subscribeToSourceData();
if (!this.branchConfigService.hasBranchConfig()) {
this.loadBranchConfig();
}
}
public loadBranchConfig(): void {
this.branchConfigService.loadBranchConfig().subscribe(
(success: BranchConfig) => {
this.branchConfigService.setBranchConfig(success);
},
(error) => {
this.snackBarService.throwServerError(error, 'BRANCHMAP.LOAD_ERROR');
}
);
}
public subscribeToSourceData(): void {
this.dataSubscription = this.branchConfigService.branchConfig$.subscribe((branchConfig) => {
if (branchConfig) {
this.dataSource = new MatTableDataSource(branchConfig.branchCodesByZip);
this.lastUpdated = branchConfig.date;
this.dataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase();
this.dataSource.sort = this.sort;
}
});
}
public commandExportCSV() {
this.branchConfigService.exportCSV(this.dataSource.data);
}
public ngOnDestroy(): void {
if (this.dataSubscription) {
this.dataSubscription.unsubscribe();
}
}
public fileChangeListener($event): void {
const input = $event.target;
const currentFile = input.files[0];
if (currentFile) {
const reader = new FileReader();
reader.readAsText(currentFile);
reader.onload = (data) => {
this.localState.pendingData = reader.result;
this.localState.compareResult = this.branchConfigService.validateUpload(this.localState.pendingData);
this.fileReset();
};
reader.onerror = function () {
alert('Unable to read ' + currentFile);
};
}
}
public fileReset(): void {
this.fileImportInput.nativeElement.value = '';
}
public resetLocalState(): void {
this.localState = {
uploadConfirmed: false,
compareResult: null,
pendingData: null
};
}
public handleLocalStateError(errorResult): void {
this.localState.compareResult.warnings = [];
let field;
for (field in errorResult.error) {
if (errorResult.error.hasOwnProperty(field)) {
this.localState.compareResult.warnings.push(field + ': ' + errorResult.error[field][0]);
}
}
this.localState.compareResult.inError = true;
this.localState.compareResult.errorMessage = 'The server rejected the mapping.';
}
public commandConfirmSave(): void {
this.branchConfigService.saveNewBranchMapping(this.localState.compareResult.convertedData).subscribe(
(result) => {
this.resetLocalState();
},
(errorResult) => {
this.snackBarService.throwServerError(errorResult, 'BRANCHMAP.LOAD_ERROR');
this.handleLocalStateError(errorResult);
}
);
}
}
@@ -0,0 +1,175 @@
import { ReactiveFormsModule } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { configureTestSuite } from 'ng-bullet';
import { BranchConfigService } from './branch-config.service';
import { environment } from 'src/environments/environment';
import { FilterUtil } from '../filter/filter-util';
import { Filter } from '../filter/filter';
import { BranchConfig, BranchData } from './branch-config';
describe('BranchConfig Service', () => {
let service: BranchConfigService;
let httpTestingController: HttpTestingController;
const urlBase: string = environment.urlBase;
const baseConfig = <BranchConfig>{
id: '5',
date: new Date(),
branchCodesByZip: [
<BranchData>{
zipcode: '53207',
channelId: 9196721
},
<BranchData>{
zipcode: '53209',
channelId: 9196721
},
<BranchData>{
zipcode: '53215',
channelId: 9196722
}
]
};
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
HttpClientTestingModule
],
schemas: [
NO_ERRORS_SCHEMA
],
declarations: [],
providers: [
BranchConfigService
]
});
});
beforeEach(() => {
service = TestBed.get(BranchConfigService);
httpTestingController = TestBed.get(HttpTestingController);
});
it('should create', () => {
expect(service).toBeTruthy();
});
it('should build branch config url', () => {
const url = service.buildBranchConfigURL();
expect(url).toBe(urlBase + '/BranchConfig');
});
it('should identify if there is a branch config', () => {
const apps = service.hasBranchConfig();
expect(apps).toEqual(false);
});
it('should load branch config', () => {
const testBranchConfig = new BranchConfig();
testBranchConfig.branchCodesByZip = [];
testBranchConfig.date = new Date();
testBranchConfig.id = '555';
service.loadBranchConfig().subscribe((data) => {
expect(data).toEqual(testBranchConfig);
service.setBranchConfig(data);
}
);
const req = httpTestingController.expectOne(service.buildBranchConfigURL());
// Assert that the request is a GET.
expect(req.request.method).toEqual('GET');
// Assert that the request contains the appropriate header.
expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')));
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(testBranchConfig);
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
it('should save a branch config', () => {
const testBranchConfig = new BranchConfig();
service.submitBranchConfig(testBranchConfig).subscribe(() => {});
const req = httpTestingController.expectOne(service.buildBranchConfigURL());
expect(req.request.method).toEqual('PUT');
httpTestingController.verify();
});
it('should validate an upload.', () => {
service.setBranchConfig(baseConfig);
const newConfig =
`ZIP,ChannelId
53207,9196722
53209,9196721
53215,9196722
`;
const result = service.validateUpload(newConfig);
expect(result.inError).toEqual(false);
expect(result.currentCount).toEqual(baseConfig.branchCodesByZip.length);
expect(result.newCount).toEqual(3);
expect(result.warnings.length).toEqual(1);
});
it('should check for removed entries in an upload.', () => {
service.setBranchConfig(baseConfig);
expect(service.checkForRemovedEntries(baseConfig.branchCodesByZip, baseConfig.branchCodesByZip)).toEqual([]);
const newConfig = <BranchConfig>{
id: '5',
branchCodesByZip: [
<BranchData>{
zipcode: '53207',
channelId: 9196721
},
<BranchData>{
zipcode: '53215',
channelId: 9196722
}
]
};
expect(service.checkForRemovedEntries(baseConfig.branchCodesByZip, newConfig.branchCodesByZip)).toEqual([
'Zipcode 53209 is not in the new set.'
]);
});
it('should create branch data from a row of an upload.', () => {
service.setBranchConfig(baseConfig);
expect(service.createBranchDataFromRow('53215,9196722', 1)).toEqual(
<BranchData>{
zipcode: '53215',
channelId: 9196722
}
);
expect(() => { service.createBranchDataFromRow('53215', 0); }).toThrow();
expect(service.createBranchDataFromRow('', 1)).toBeNull();
});
it('should find existing rows given an uploaded row. ', () => {
service.setBranchConfig(baseConfig);
expect(service.findExistingRow(baseConfig.branchCodesByZip, <BranchData>{
zipcode: '53215',
channelId: 9196725
})).toEqual(<BranchData>{
zipcode: '53215',
channelId: 9196722
}
);
});
});
@@ -0,0 +1,160 @@
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { ExportService } from 'src/app/shared/export/export.service';
import { BranchConfig, BranchData } from './branch-config';
@Injectable({
providedIn: 'root'
})
export class BranchConfigService {
private branchConfigSubject: BehaviorSubject<BranchConfig> = new BehaviorSubject<BranchConfig>(null);
public branchConfig$ = this.branchConfigSubject.asObservable();
constructor(private httpClient: HttpClient, private exportService: ExportService) {}
public buildBranchConfigURL(): string {
return environment.urlBase + '/BranchConfig';
}
public buildHeader(): HttpHeaders {
return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'));
}
public hasBranchConfig(): boolean {
return this.branchConfigSubject.value !== null;
}
public loadBranchConfig(): Observable<BranchConfig> {
const headers = this.buildHeader();
return this.httpClient.get(this.buildBranchConfigURL(), {headers, withCredentials: true}).pipe(
map((success) => {
const config = new BranchConfig();
config.id = success['id'];
config.date = success['date'];
config.branchCodesByZip = success['branchCodesByZip'];
return config;
})
);
}
public setBranchConfig(branchConfig: BranchConfig): void {
this.branchConfigSubject.next(branchConfig);
}
public submitBranchConfig(branchConfig: BranchConfig): Observable<any> {
const headers = this.buildHeader();
return this.httpClient.put(this.buildBranchConfigURL(), branchConfig, {headers, withCredentials: true})
.pipe(
map((success) => {
const config = new BranchConfig();
config.id = success['id'];
config.date = success['date'];
config.branchCodesByZip = success['branchCodesByZip'];
this.setBranchConfig(config);
return config;
})
);
}
public exportCSV(dataSource: Array<any>) {
this.exportService.exportBranchConfigAsCSV(dataSource);
}
public validateUpload(rawData: string): any {
const pendingData = rawData.split('\n');
const oldData = this.branchConfigSubject.getValue().branchCodesByZip;
const compareData = {
currentCount: oldData.length,
newCount: pendingData.length - 1,
inError: false,
errorMessage: '',
convertedData: [],
warnings: []
};
if (pendingData.length <= 1) {
compareData.inError = true;
compareData.errorMessage = 'There are no rows.';
return compareData;
}
let rowData: BranchData;
let foundRow: BranchData;
try {
for (let newDataIndex = 1; newDataIndex < pendingData.length; newDataIndex++) {
rowData = this.createBranchDataFromRow(pendingData[newDataIndex], newDataIndex);
if (rowData) {
foundRow = this.findExistingRow(oldData, rowData);
if (foundRow === null) {
compareData.warnings.push('Zipcode ' + rowData.zipcode + ' is a newly added zipcode.');
} else if ((foundRow.channelId + '') !== (rowData.channelId + '')) {
compareData.warnings
.push('Zipcode ' + rowData.zipcode + ' was changed from ' + foundRow.channelId + ' to ' + rowData.channelId + '.');
}
compareData.convertedData.push(rowData);
}
}
compareData.warnings = compareData.warnings.concat(this.checkForRemovedEntries(oldData, compareData.convertedData));
compareData.newCount = compareData.convertedData.length;
} catch (error) {
compareData.inError = true;
compareData.errorMessage = error.msg;
}
return compareData;
}
public checkForRemovedEntries(oldSet: Array<BranchData>, newSet: Array<BranchData>): Array<string> {
const warnings = [];
let found = false;
for (let oldDataIndex = 0; oldDataIndex < oldSet.length; oldDataIndex++) {
for (let newDataIndex = 0; newDataIndex < newSet.length; newDataIndex++) {
if (oldSet[oldDataIndex].zipcode === newSet[newDataIndex].zipcode) {
found = true;
}
}
if (!found) {
warnings.push('Zipcode ' + oldSet[oldDataIndex].zipcode + ' is not in the new set.');
}
found = false;
}
return warnings;
}
public saveNewBranchMapping(pendingData: Array<any>): Observable<any> {
const branchConfig = {
id: this.branchConfigSubject.getValue()['id'],
date: new Date(),
branchCodesByZip: pendingData
};
return this.submitBranchConfig(branchConfig);
}
public createBranchDataFromRow(dataString: string, rowIndex: number): BranchData {
if (!dataString || !dataString.trim()) {
return null;
}
const cols = dataString.trim().split(',');
if (!cols || cols.length !== 2) {
throw new Error('Row ' + rowIndex + ' does not have correct number of columns.');
}
return <BranchData>{
zipcode: cols[0],
channelId: parseInt(cols[1], 10)
};
}
public findExistingRow(oldData: Array<BranchData>, rowData: BranchData): BranchData {
for (let oldDataIndex = 0; oldDataIndex < oldData.length; oldDataIndex++) {
if (oldData[oldDataIndex].zipcode === rowData.zipcode) {
return oldData[oldDataIndex];
}
}
return null;
}
}
@@ -0,0 +1,10 @@
export class BranchData {
zipcode: string;
channelId: number;
}
export class BranchConfig {
public id: string;
public date: Date;
public branchCodesByZip: Array<BranchData>;
}
@@ -0,0 +1,41 @@
<lw-top-navigation></lw-top-navigation>
<div class="card mat-elevation-z8">
<div class="card-header address-compare-panel-header">
<h4 translate>BRANCHNAMEMAP.TITLE</h4>
<span translate>BRANCHNAMEMAP.SUBTEXT</span>
<p *ngIf="lastUpdated">
<span translate>BRANCHNAMEMAP.DATETEXT</span><span
style="font-weight: bold;">{{lastUpdated | date:'short'}}</span>
</p>
</div>
<div class="row card-body shift-right">
<div class="col-12">
<div class="icon-input-row">
<div class="btn-group btn-group-toggle row">
<div class="btn-group-child">
<div><strong translate>BRANCHNAMEMAP.UPDATETEXT</strong></div>
<button type="button" class="btn btn-success btn-less-padding" style="margin-top: 5px"
(click)="commandUpdate()">UPDATE
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mat-elevation-z8" >
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="channelId">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Channel ID</th>
<td mat-cell *matCellDef="let loan">{{loan.channelId}} </td>
</ng-container>
<ng-container matColumnDef="branchName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Branch Name</th>
<td mat-cell *matCellDef="let loan">{{loan.branchName}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
@@ -0,0 +1,38 @@
table {
width: 100%;
}
.mat-form-field {
margin-right: 10px;
font-size: .875em;
width: 20%;
}
.mat-header-cell {
font-size: 1em;
font-weight: bold;
color: black;
}
.mat-checkbox {
margin-right: 10px;
:host ::ng-deep .mat-checkbox-background {
background-color: #005B9A
}
}
.btn-disabled {
margin-bottom: 0px;
margin-left: 0px;
}
.btn-less-padding {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 8px;
padding-right: 8px;
}
.shift-right {
margin-left: 5px;
}
@@ -0,0 +1,66 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { configureTestSuite } from 'ng-bullet';
import { BranchNameConfigComponent } from './branch-name-config.component';
import { MatSortModule, MatPaginatorModule, MatTableModule, MatInputModule,
MatDatepickerModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, MatSnackBar, MatSnackBarModule } from '@angular/material';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe';
import { BranchNameConfigService } from './branch-name-config.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TopNavigationComponent } from '../navigation/top-navigation.component';
import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component';
import { TranslateModule } from '@ngx-translate/core';
describe('BranchNameConfigComponent', () => {
let component: BranchNameConfigComponent;
let fixture: ComponentFixture<BranchNameConfigComponent>;
let service: BranchNameConfigService;
let snackBar: MatSnackBar;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
MatSortModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
MatSnackBarModule,
MatDatepickerModule,
MatNativeDateModule,
MatSelectModule,
MatCheckboxModule,
ReactiveFormsModule,
HttpClientTestingModule,
NoopAnimationsModule,
TranslateModule.forRoot()
],
declarations: [
SnackBarErrorComponent,
BranchNameConfigComponent,
LoanCodePipe,
TopNavigationComponent
],
providers: [
]
});
});
beforeEach(() => {
service = TestBed.get(BranchNameConfigService);
snackBar = TestBed.get(MatSnackBar);
fixture = TestBed.createComponent(BranchNameConfigComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
expect(service).toBeTruthy();
fixture.detectChanges();
});
});
@@ -0,0 +1,65 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { BranchNameConfigService } from './branch-name-config.service';
import { Subscription } from 'rxjs';
import { MatTableDataSource, MatSort } from '@angular/material';
import { BranchNameConfig } from './branch-name-config';
import { ServerError } from '../snack-bar-error/server-error';
import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component';
import { SnackBarService } from '../snack-bar-error/snack-bar.service';
@Component({
selector: 'lw-branch-Name-config',
templateUrl: './branch-name-config.component.html',
styleUrls: ['./branch-name-config.component.scss']
})
export class BranchNameConfigComponent implements OnInit, OnDestroy {
private dataSubscription: Subscription;
public dataSource: MatTableDataSource<any>;
public lastUpdated: Date;
public displayedColumns: string[] = ['channelId', 'branchName'];
@ViewChild(MatSort) sort: MatSort;
constructor(private branchNameConfigService: BranchNameConfigService, private snackBarService: SnackBarService) {}
public ngOnInit() {
this.subscribeToSourceData();
if (!this.branchNameConfigService.hasBranchNameConfig()) {
this.loadBranchNameConfig();
}
}
public commandUpdate() {
this.loadBranchNameConfig(true);
}
public loadBranchNameConfig(update: boolean = false): void {
this.branchNameConfigService.loadBranchNameConfig(update).subscribe(
(success: BranchNameConfig) => {
this.branchNameConfigService.setBranchNameConfig(success);
},
(error) => {
this.snackBarService.throwServerError(error, 'BRANCHNAMEMAP.LOAD_ERROR');
}
);
}
public subscribeToSourceData(): void {
this.dataSubscription = this.branchNameConfigService.branchNameConfig$.subscribe((branchNameConfig) => {
if (branchNameConfig) {
this.dataSource = new MatTableDataSource(branchNameConfig.branchNamesByChannelId);
this.lastUpdated = branchNameConfig.date;
this.dataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase();
this.dataSource.sort = this.sort;
}
});
}
public ngOnDestroy(): void {
if (this.dataSubscription) {
this.dataSubscription.unsubscribe();
}
}
}
@@ -0,0 +1,106 @@
import { ReactiveFormsModule } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { configureTestSuite } from 'ng-bullet';
import { BranchNameConfigService } from './branch-name-config.service';
import { environment } from 'src/environments/environment';
import { BranchNameConfig, BranchNameData } from './branch-name-config';
describe('BranchNameConfig Service', () => {
let service: BranchNameConfigService;
let httpTestingController: HttpTestingController;
const urlBase: string = environment.urlBase;
const baseConfig = <BranchNameConfig>{
id: '5',
date: new Date(),
branchNamesByChannelId: [
<BranchNameData>{
channelId: 9196721,
branchName: 'Watertown'
},
<BranchNameData>{
channelId: 9196721,
branchName: 'Brookfield East'
},
<BranchNameData>{
channelId: 9196722,
branchName: 'Waukesha'
}
]
};
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
HttpClientTestingModule
],
schemas: [
NO_ERRORS_SCHEMA
],
declarations: [],
providers: [
BranchNameConfigService
]
});
});
beforeEach(() => {
service = TestBed.get(BranchNameConfigService);
httpTestingController = TestBed.get(HttpTestingController);
});
it('should create', () => {
expect(service).toBeTruthy();
});
it('should build branchName config url', () => {
const url = service.buildBranchNameConfigURL();
expect(url).toBe(urlBase + '/BranchConfig/BranchNameConfig');
});
it('should build branchName config update url', () => {
const url = service.buildBranchNameConfigUpdateURL();
expect(url).toBe(urlBase + '/BranchConfig/UpdateBranchNameConfig');
});
it('should identify if there is a branchName config', () => {
const apps = service.hasBranchNameConfig();
expect(apps).toEqual(false);
});
it('should load branchName config', () => {
const testBranchNameConfig = new BranchNameConfig();
testBranchNameConfig.branchNamesByChannelId = [];
testBranchNameConfig.date = new Date();
testBranchNameConfig.id = '555';
service.loadBranchNameConfig().subscribe((data) => {
expect(data).toEqual(testBranchNameConfig);
service.setBranchNameConfig(data);
}
);
const req = httpTestingController.expectOne(service.buildBranchNameConfigURL());
// Assert that the request is a GET.
expect(req.request.method).toEqual('GET');
// Assert that the request contains the appropriate header.
expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')));
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(testBranchNameConfig);
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
});
@@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { BranchNameConfig, BranchNameData } from './branch-name-config';
@Injectable({
providedIn: 'root'
})
export class BranchNameConfigService {
private branchNameConfigSubject: BehaviorSubject<BranchNameConfig> = new BehaviorSubject<BranchNameConfig>(null);
public branchNameConfig$ = this.branchNameConfigSubject;
constructor(private httpClient: HttpClient) {}
public buildBranchNameConfigURL(): string {
return environment.urlBase + '/BranchConfig/BranchNameConfig';
}
public buildBranchNameConfigUpdateURL(): string {
return environment.urlBase + '/BranchConfig/UpdateBranchNameConfig';
}
public buildHeader(): HttpHeaders {
return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'));
}
public hasBranchNameConfig(): boolean {
return this.branchNameConfigSubject.value !== null;
}
public loadBranchNameConfig(update: boolean = false): Observable<BranchNameConfig> {
const headers = this.buildHeader();
const url = update ? this.buildBranchNameConfigUpdateURL() : this.buildBranchNameConfigURL();
return this.httpClient.get(url, {headers, withCredentials: true}).pipe(
map((success) => {
const config = new BranchNameConfig();
config.id = success['id'];
config.date = success['date'];
config.branchNamesByChannelId = success['branchNamesByChannelId'];
return config;
})
);
}
public setBranchNameConfig(branchNameConfig: BranchNameConfig): void {
this.branchNameConfigSubject.next(branchNameConfig);
}
}
@@ -0,0 +1,10 @@
export class BranchNameData {
channelId: number;
branchName: string;
}
export class BranchNameConfig {
public id: string;
public date: Date;
public branchNamesByChannelId: Array<BranchNameData>;
}
@@ -0,0 +1,71 @@
.fa-angle-right {
padding-top: 4px;
}
.main-card-text {
font-weight: bold;
font-size: 25px;
}
.main-card-footer {
margin-bottom: 10px;
}
.card {
font-family: Arial, sans-serif;
margin-bottom: 25px;
text-decoration: none;
color:#565656;
}
.row {
text-align: center;
padding-right: 0px;
padding-left: 0px;
}
.card-body {
padding-right: 0px;
padding-left: 0px;
}
.main-icon {
margin-top: 10px;
margin-bottom: 8px;
}
.footer-text {
margin-right: 8px;
}
.desktop-view {
font-size: 20px;
margin-right: 8px;
}
/*mobile*/
@media (max-width: 767px) {
.fa-angle-right {
padding-top: 0px;
}
.main-card-text {
float: left;
font-size: 17px;
}
.main-card-footer {
margin-bottom: 0px;
}
.card {
margin-bottom: 10px;
}
.footer-text {
float: right;
}
.main-icon {
margin-top: 0px;
margin-bottom: 0px;
}
.desktop-view {
display: none;
}
.mobile-view[style] {
display: inline !important;
}
.card-body {
padding-right: 15px;
padding-left: 15px;
}
}
@@ -0,0 +1,30 @@
<a [routerLink]="[cardPath]" class="card bg-light">
<div class="card-body">
<div class="row align-items-center">
<div class="col-3 col-md-12">
<div class="main-icon">
<span class="desktop-view">
<i [className]=desktopIcon></i>
</span>
<span class="mobile-view" style="display: none">
<i [className]=mobileIcon></i>
</span>
</div>
</div>
<div class="col-7 col-md-12">
<div class="main-card-text" translate>{{cardName}}</div>
</div>
<div class="col-2 col-md-12">
<div class="main-card-footer">
<span class="desktop-view">
<span class="footer-text" translate>{{footerName}}</span>
<i class="fas fa-angle-right"></i>
</span>
<span class="mobile-view" style="display: none">
<i class="fas fa-angle-right fa-2x"></i>
</span>
</div>
</div>
</div>
</div>
</a>
@@ -0,0 +1 @@
// see card-component.scss for styling
@@ -0,0 +1,45 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ReactiveFormsModule } from '@angular/forms';
import { configureTestSuite } from 'ng-bullet';
import { MainCardAdminComponent } from './main-card.component';
describe('MainCardComponent', () => {
let component: MainCardAdminComponent;
let fixture: ComponentFixture<MainCardAdminComponent>;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule
],
declarations: [
MainCardAdminComponent,
],
});
});
beforeEach(() => {
fixture = TestBed.createComponent(MainCardAdminComponent);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should create with input', () => {
fixture.detectChanges();
component.desktopIcon = 'fas fa-credit-card fa-3x';
component.cardName = 'Credit Card';
component.cardPath = 'credit-card';
fixture.detectChanges();
const icon = fixture.nativeElement.querySelectorAll('.fa-credit-card');
expect(icon).toBeDefined();
expect(icon.length).toBeGreaterThanOrEqual(1);
});
});
@@ -0,0 +1,20 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'lw-main-card',
templateUrl: './main-card.component.html',
styleUrls: ['../card.component.scss']
})
export class MainCardAdminComponent {
@Input()
public cardPath: string;
@Input()
public desktopIcon: string;
@Input()
public mobileIcon: string;
@Input()
public cardName: string;
@Input()
public footerName: string;
constructor() { }
}
@@ -0,0 +1,11 @@
import { FilterField } from './filter-field';
export class FilterApi {
public count: number;
public sortDesc: boolean;
public sortField: string;
public startDate: Date;
public endDate: Date;
public filterField?: FilterField[];
}
@@ -0,0 +1,19 @@
export class FilterField {
public field: string;
public filterValue: string;
public equal: boolean;
public notEqual: boolean;
public greaterThan: boolean;
public lessThan: boolean;
constructor (field: string, filterValue: string, equal: boolean, notEqual: boolean, greaterThan: boolean, lessThan: boolean) {
this.field = field;
this.filterValue = filterValue;
this.equal = equal;
this.notEqual = notEqual;
this.greaterThan = greaterThan;
this.lessThan = lessThan;
}
}
@@ -0,0 +1,90 @@
import { FilterField } from './filter-field';
import { FilterApi } from './filter-api';
import { DateBuilder } from 'src/app/shared/date/date-builder';
import { Status } from './status';
import { ApiStatus } from 'src/app/shared/api-models/api-status';
import { Filter } from './filter';
export class FilterUtil {
public static getDefaultFilter(): Filter {
const filter: Filter = new Filter();
filter.count = 1000;
filter.sortDesc = true;
filter.sortField = 'Date';
filter.startDate = this.buildDefaultStartDate();
filter.endDate = this.buildDefaultEndDate();
filter.status = this.getDefaultStatus();
filter.branch = 'ALL';
filter.loanType = 'ALL';
return filter;
}
public static getDefaultStatus(): Status {
const status: Status = new Status();
status.error = true;
status.deleted = true;
status.inProgress = true;
status.submitted = false;
status.decisioned = false;
return status;
}
public static translateFilter(filter: Filter): FilterApi {
const filterApi: FilterApi = new FilterApi();
filterApi.count = filter.count;
filterApi.startDate = this.buildApiStartDate(filter.startDate);
filterApi.endDate = this.buildApiEndDate(filter.endDate);
filterApi.sortDesc = filter.sortDesc;
filterApi.sortField = filter.sortField;
const filterField: FilterField [] = [] ;
if (filter.branch !== 'ALL') {
filterField.push(new FilterField('Branch', filter.branch, true, false, false, false));
}
if (filter.loanType !== 'ALL') {
filterField.push(new FilterField('LoanType', filter.loanType, true, false, false, false));
}
if (filter.status.error === false) {
filterField.push(new FilterField('Status', ApiStatus.APPLICATION_ERROR, true, true, false, false));
filterField.push(new FilterField('Status', ApiStatus.VENDOR_ERROR, false, true, false, false));
}
if (filter.status.deleted === false) {
filterField.push(new FilterField('Status', ApiStatus.DELETED, false, true, false, false));
}
if (filter.status.inProgress === false) {
filterField.push(new FilterField('Status', ApiStatus.IN_PROGRESS, false, true, false, false));
}
if (filter.status.submitted === false) {
filterField.push(new FilterField('Status', ApiStatus.SUBMITTED, false, true, false, false));
}
if (filter.status.decisioned === false) {
filterField.push(new FilterField('Status', ApiStatus.DECISIONED, false, true, false, false));
}
filterApi.filterField = filterField;
return filterApi;
}
public static buildDefaultStartDate(): string {
const startDate = DateBuilder.buildPastDate(7);
return DateBuilder.formatDateAsString(startDate);
}
public static buildDefaultEndDate(): string {
return DateBuilder.formatDateAsString(new Date());
}
public static buildApiStartDate(date: string): Date {
let startDate = DateBuilder.formatStringAsDate(date);
startDate = DateBuilder.setBeginOfDay(startDate);
return startDate;
}
public static buildApiEndDate(date: string): Date {
let endDate = DateBuilder.formatStringAsDate(date);
endDate = DateBuilder.setEndOfDay(endDate);
return endDate;
}
}
@@ -0,0 +1,13 @@
import { Status } from './status';
export class Filter {
public count: number;
public sortDesc: boolean;
public sortField: string;
public startDate: string;
public endDate: string;
public status: Status;
public branch: string;
public loanType: string;
}
@@ -0,0 +1,9 @@
export class Status {
public error: boolean;
public inProgress: boolean;
public deleted: boolean;
public submitted: boolean;
public decisioned: boolean;
}
@@ -0,0 +1,24 @@
<div class="loan-header" translate>ADMIN.LANDING_TITLE</div>
<hr class="title-hr"/>
<div class="container">
<div class="row">
<div class="col-12 col-md-6 col-lg-4">
<lw-main-card cardPath="loan-table" desktopIcon="fas fa-table fa-2x" mobileIcon="fas fa-table fa-2x" cardName="ADMIN.LOAN_TABLE" footerName="ADMIN.NAVIGATE" id="loan-table"></lw-main-card>
</div>
<div class="col-12 col-md-6 col-lg-4">
<lw-main-card cardPath="branch-config" desktopIcon="fas fa-cogs fa-2x" mobileIcon="fas fa-cogs fa-2x" cardName="ADMIN.BRANCH_CONFIG" footerName="ADMIN.NAVIGATE" id="branch-config"></lw-main-card>
</div>
<div class="col-12 col-md-6 col-lg-4">
<lw-main-card cardPath="branch-name-config" desktopIcon="fas fa-cogs fa-2x" mobileIcon="fas fa-cogs fa-2x" cardName="ADMIN.BRANCHNAME_CONFIG" footerName="ADMIN.NAVIGATE" id="branch-name-config"></lw-main-card>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 col-lg-4">
<lw-main-card cardPath="system-config" desktopIcon="fas fa-wrench fa-2x" mobileIcon="fas fa-wrench fa-2x" cardName="ADMIN.SYSTEM" footerName="ADMIN.NAVIGATE" id="system-config"></lw-main-card>
</div>
<div class="col-12 col-md-6 col-lg-4">
<lw-main-card cardPath="schumer-preview" desktopIcon="fas fa-wrench fa-2x" mobileIcon="fas fa-wrench fa-2x" cardName="ADMIN.SCHUMER" footerName="ADMIN.NAVIGATE" id="schumer-preview"></lw-main-card>
</div>
</div>
</div>
@@ -0,0 +1,22 @@
.loan-header {
font-family: 'Open Sans', sans-serif !important;
font-size:44px;
color:#012E50
}
.title-hr {
margin: 0 0 2.5em 0;
color: #B1B1B1;
height: 1px;
border: none;
background-color: #B1B1B1;
}
/*mobile*/
@media (max-width: 767px) {
.loan-header {
font-size:30px;
font-weight:400;
text-align: center;
}
}
@@ -0,0 +1,37 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ReactiveFormsModule } from '@angular/forms';
import { configureTestSuite } from 'ng-bullet';
import { AdminLandingComponent } from './admin-landing.component';
import { MainCardAdminComponent } from '../cards/main-card/main-card.component';
describe('AdminLandingComponent', () => {
let component: AdminLandingComponent;
let fixture: ComponentFixture<AdminLandingComponent>;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule
],
declarations: [
AdminLandingComponent,
MainCardAdminComponent
],
});
});
beforeEach(() => {
fixture = TestBed.createComponent(AdminLandingComponent);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'lw-admin-landing',
templateUrl: './admin-landing.component.html',
styleUrls: ['./admin-landing.component.scss']
})
export class AdminLandingComponent {
constructor() { }
}
@@ -0,0 +1,3 @@
<pre>{{container | json}}</pre>
<br>
<button type="button" class="btn btn-link btn-previous" id='buttonPrevious' (click)="commandPrevious()" translate>BUTTONS.BACK</button>
@@ -0,0 +1,39 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ReactiveFormsModule } from '@angular/forms';
import { configureTestSuite } from 'ng-bullet';
import { LoanDataComponent } from './loan-data.component';
import { LoanTableService } from '../loan-table/loan-table.service';
import { MockLoanTableService } from 'src/app/testing/shared-mocks/loan-table.mock.service';
describe('LoanDataComponent', () => {
let component: LoanDataComponent;
let fixture: ComponentFixture<LoanDataComponent>;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
],
declarations: [
LoanDataComponent
],
providers: [
{ provide: LoanTableService, useValue: MockLoanTableService }
]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(LoanDataComponent);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,37 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { LoanTableService } from '../loan-table/loan-table.service';
import { Subscription } from 'rxjs';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container';
@Component({
selector: 'lw-loan-data',
templateUrl: './loan-data.component.html',
styleUrls: ['./loan-data.component.scss']
})
export class LoanDataComponent implements OnInit, OnDestroy {
public container: ApiLoanContainer;
private subscription: Subscription;
constructor(private loanTableService: LoanTableService, private route: ActivatedRoute, private location: Location) { }
public ngOnInit() {
this.subscription = this.route.params.subscribe(params => {
if (params.id) {
this.container = this.loanTableService.getApplicationById(params.id);
}
});
}
public ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
public commandPrevious() {
this.location.back();
}
}
@@ -0,0 +1,103 @@
<lw-top-navigation></lw-top-navigation>
<form [formGroup]="pageForm" novalidate (ngSubmit)="commandFilter()">
<div [formGroup]="filterGroup">
<span matTooltip="Search the data fields listed below"><i class="search-tip fas fa-info-circle fa-2x"></i></span>
<mat-form-field>
<input matInput (keyup)="commandSearch($event.target.value)" placeholder="Search">
</mat-form-field>
<mat-form-field>
<input matInput id="startDate" formControlName="startDate" placeholder="Start Date">
<mat-error><div translate>ADMIN.DATE_ERROR</div></mat-error>
</mat-form-field>
<mat-form-field>
<input matInput id="endDate" formControlName="endDate" placeholder="End Date">
<mat-error><div translate>ADMIN.DATE_ERROR</div></mat-error>
</mat-form-field>
<br>
<mat-form-field>
<mat-select placeholder="Loan Type" formControlName="loanType">
<mat-option *ngFor="let loanType of loanTypes" [value]="loanType.value">{{loanType.desc}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="Branch" formControlName="branch">
<mat-option *ngFor="let branch of branches" [value]="branch">{{branch}}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox color="primary" formControlName="decisioned">Decisioned</mat-checkbox>
<mat-checkbox color="primary" formControlName="submitted">Submitted</mat-checkbox>
<mat-checkbox color="primary" formControlName="inProgress">In Progress</mat-checkbox>
<mat-checkbox color="primary" formControlName="error">Error</mat-checkbox>
<mat-checkbox color="primary" formControlName="deleted">Deleted</mat-checkbox>
<button type="button" class="btn btn-less-padding" [ngClass]="localState.filterDisabled ? 'btn-disabled' : 'btn-primary'" [disabled]="localState.filterDisabled" (click)="commandFilter()">Filter
<span *ngIf="localState.filterIndicator" style="margin-left: 0.33em"><i focusable='false' class="fas fa-spinner fa-pulse"></i></span>
</button>
</div>
</form>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<!-- Loan Id Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
<td mat-cell *matCellDef="let loan"><a [routerLink]="['/admin/loan-data', loan.id || all]">{{loan.id | slice:0:7}} </a></td>
</ng-container>
<!-- Loan Type Column -->
<ng-container matColumnDef="loanType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Loan Type</th>
<td mat-cell *matCellDef="let loan">{{loan.loanType | loanCode}}</td>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
<span matTooltip=
"Decisioned: Application has been decided.
Submitted: Application has been submitted.
In Progress: Application is currently in progress.
Deleted: Application has been deleted.
Application Error: Error with landmarks service.
Vendor Error: Error with vendors service."
>
<i class="status-tip fas fa-info-circle"></i>
</span>
Status
</th>
<td mat-cell *matCellDef="let loan">{{loan.status}}</td>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Date</th>
<td mat-cell *matCellDef="let loan">{{loan.date | date:'short'}}</td>
</ng-container>
<!-- Branch Column -->
<ng-container matColumnDef="branch">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Branch</th>
<td mat-cell *matCellDef="let loan"> {{loan.branch}} </td>
</ng-container>
<!-- First Name Column -->
<ng-container matColumnDef="firstName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>
<td mat-cell *matCellDef="let loan">{{loan.firstName}}</td>
</ng-container>
<!-- Last Name Column -->
<ng-container matColumnDef="lastName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>
<td mat-cell *matCellDef="let loan">{{loan.lastName}}</td>
</ng-container>
<!-- Submit Column -->
<ng-container matColumnDef="submit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let loan"><div *ngIf="loan.status == 'In_Progress' || loan.status == 'Application_Error' || loan.status == 'Vendor_Error'"><button type="button" class="btn btn-primary btn-less-padding" (click)="commandSubmit(loan.id)">Submit</button></div></td>
</ng-container>
<!-- Delete Column -->
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let loan"><div *ngIf="loan.status == 'In_Progress' || loan.status == 'Application_Error' || loan.status == 'Vendor_Error'"><button type="button" class="btn btn-danger btn-less-padding" (click)="commandDelete(loan.id)">Delete</button></div></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
<button type="button" class="btn btn-success btn-less-padding" style="margin-top: 5px" (click)="commandExportCSV()">CSV Export</button>
@@ -0,0 +1,55 @@
table {
width: 100%;
}
.mat-form-field {
margin-right: 10px;
font-size: 14px;
width: 20%;
}
.mat-header-cell {
font-size: 16px;
color: black;
}
.mat-checkbox {
margin-right: 10px;
:host ::ng-deep .mat-checkbox-background {
background-color: #005B9A
}
}
.btn-disabled {
margin-bottom: 0px;
margin-left: 0px;
}
.btn-less-padding {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 8px;
padding-right: 8px;
}
.search-tip {
color: #005B9A;
width: 16px;
padding-top: 16px;
margin-right: 4px;
}
.status-tip {
color: #005B9A;
margin-right: .3em;
}
::ng-deep .mat-tooltip {
white-space: pre-line !important;
}
.error {
font-size: 80%;
color: #dc3545;
font-style: italic;
}
@@ -0,0 +1,58 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { configureTestSuite } from 'ng-bullet';
import { LoanTableService } from '../loan-table/loan-table.service';
import { MockLoanTableService } from 'src/app/testing/shared-mocks/loan-table.mock.service';
import { LoanTableComponent } from './loan-table.component';
import { MatSortModule, MatPaginatorModule, MatTableModule, MatInputModule,
MatDatepickerModule, MatNativeDateModule, MatSelectModule, MatCheckboxModule, MatSnackBar, MatSnackBarModule } from '@angular/material';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { LoanCodePipe } from 'src/app/shared/pipes/loan-code.pipe';
import { TopNavigationComponent } from '../navigation/top-navigation.component';
import { TranslateModule } from '@ngx-translate/core';
describe('LoanTableComponent', () => {
let component: LoanTableComponent;
let fixture: ComponentFixture<LoanTableComponent>;
let service: LoanTableService;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
MatSortModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
MatDatepickerModule,
MatNativeDateModule,
MatSnackBarModule,
MatSelectModule,
MatCheckboxModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [
LoanTableComponent,
LoanCodePipe,
TopNavigationComponent
],
providers: [
{ provide: LoanTableService, useValue: MockLoanTableService }
]
});
});
beforeEach(() => {
service = TestBed.get(LoanTableService);
fixture = TestBed.createComponent(LoanTableComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,195 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { LoanTableService } from './loan-table.service';
import { Subscription } from 'rxjs';
import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container';
import { MatTableDataSource, MatPaginator, MatSort } from '@angular/material';
import { Filter } from '../filter/filter';
import { FormGroup } from '@angular/forms';
import { FilterUtil } from '../filter/filter-util';
import { BranchOptions } from 'src/app/shared/options/branch-options';
import { SelectOptions } from 'src/app/shared/options/select-options';
import { SnackBarErrorComponent } from '../snack-bar-error/snack-bar-error.component';
import { ServerError } from '../snack-bar-error/server-error';
import { SnackBarService } from '../snack-bar-error/snack-bar.service';
import { BranchNameConfig } from './loan-table';
@Component({
selector: 'lw-loan-table',
templateUrl: './loan-table.component.html',
styleUrls: ['./loan-table.component.scss']
})
export class LoanTableComponent implements OnInit, OnDestroy {
public readonly FORM_STATE_INVALID = 'INVALID';
public pageForm: FormGroup;
public filterGroup: FormGroup;
public formSubscription: Subscription;
private dataSubscription: Subscription;
public dataSource: MatTableDataSource<ApiLoanContainer>;
private branchNameConfigDataSubscription: Subscription;
public branchNameConfigDataSource: MatTableDataSource<any>;
public displayedColumns: string[] = ['id', 'loanType', 'status', 'date', 'branch', 'firstName', 'lastName', 'submit', 'delete'];
public filter: Filter;
public lastUpdated: Date;
public branches: string[] = [];
public loanTypes: Array<{value: any, desc: string}>;
public localState = {
filterDisabled: true,
filterIndicator: false,
};
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(private loanTableService: LoanTableService, private snackBarService: SnackBarService) {}
public ngOnInit() {
this.filter = this.loanTableService.getFilter();
this.loanTypes = SelectOptions.loanOptions;
this.subscribeToBranchNameSourceData();
if (!this.loanTableService.hasBranchNameConfig()) {
this.loadBranchNameConfig();
}
this.setupForm();
if (!this.loanTableService.hasApplications()) {
this.loadApplications();
}
this.subscribeToDataSource();
}
public loadBranchNameConfig(): void {
this.loanTableService.loadBranchNameConfig().subscribe(
(success: BranchNameConfig) => {
this.loanTableService.setbranchNameConfig(success);
},
(error) => {
this.snackBarService.throwServerError(error, 'BRANCHNAMEMAP.LOAD_ERROR');
}
);
}
public subscribeToBranchNameSourceData(): void {
this.dataSubscription = this.loanTableService.branchNameConfig$.subscribe((branchNameConfig) => {
if (branchNameConfig) {
this.branchNameConfigDataSource = new MatTableDataSource(branchNameConfig.branchNamesByChannelId);
this.lastUpdated = branchNameConfig.date;
this.branchNameConfigDataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase();
this.branchNameConfigDataSource.sort = this.sort;
var branchList = this.branchNameConfigDataSource.data.map(value => value.branchName);
this.branches = branchList.sort();
}
});
}
public getBranchName(item) {
var branchName = [item.branchName];
return branchName;
}
public loadApplications(): void {
this.localState.filterIndicator = true;
this.loanTableService.loadApplications(FilterUtil.translateFilter(this.filter)).subscribe(
(success: ApiLoanContainer[]) => {
this.loanTableService.setApplications(success);
this.localState.filterIndicator = false;
},
(error) => {
this.localState.filterIndicator = false;
this.snackBarService.throwServerError(error, 'ADMIN.LOAD_ERROR');
}
);
}
public setupForm(): void {
this.pageForm = this.loanTableService.createPageGroup();
this.filterGroup = <FormGroup>this.pageForm.get(this.loanTableService.FILTER_FORM_NAME);
this.formSubscription = this.pageForm.statusChanges.subscribe((newStatus) => {
this.localState.filterDisabled = (newStatus === this.FORM_STATE_INVALID);
});
this.patchForm();
}
public patchForm(): void {
this.filterGroup.patchValue({startDate: this.filter.startDate});
this.filterGroup.patchValue({endDate: this.filter.endDate});
this.filterGroup.patchValue({loanType: this.filter.loanType});
this.filterGroup.patchValue({branch: this.filter.branch});
this.filterGroup.patchValue({deleted: this.filter.status.deleted});
this.filterGroup.patchValue({error: this.filter.status.error});
this.filterGroup.patchValue({inProgress: this.filter.status.inProgress});
this.filterGroup.patchValue({submitted: this.filter.status.submitted});
this.filterGroup.patchValue({decisioned: this.filter.status.decisioned});
}
public subscribeToDataSource(): void {
this.dataSubscription = this.loanTableService.applicationSet$.subscribe((loanContainers) => {
this.dataSource = new MatTableDataSource(loanContainers);
this.dataSource.paginator = this.paginator;
this.dataSource.sortingDataAccessor = (data, sortHeaderId) => data[sortHeaderId].toLocaleLowerCase();
this.dataSource.sort = this.sort;
});
}
public commandExportCSV(): void {
this.loanTableService.exportCSV(this.dataSource.filteredData);
}
public commandSearch(searchValue: string): void {
this.dataSource.filter = searchValue.trim().toLowerCase();
if (this.dataSource.paginator) {
this.dataSource.paginator.firstPage();
}
}
public commandFilter(): void {
this.translateFormToFilter();
this.loanTableService.setFilter(this.filter);
this.loadApplications();
}
public commandDelete(loanId: string): void {
this.loanTableService.deleteApplication(loanId).subscribe(
(result) => {
this.loadApplications();
},
(error) => {
this.snackBarService.throwServerError(error, 'ADMIN.DELETE_ERROR');
}
);
}
public commandSubmit(loanId: string): void {
this.loanTableService.submitApplication(loanId).subscribe(
(result) => {
this.loadApplications();
},
(error) => {
this.snackBarService.throwServerError(error, 'ADMIN.SUBMIT_ERROR');
}
);
}
public translateFormToFilter(): void {
this.filter.startDate = this.filterGroup.value.startDate;
this.filter.endDate = this.filterGroup.value.endDate;
this.filter.loanType = this.filterGroup.value.loanType;
this.filter.branch = this.filterGroup.value.branch;
this.filter.status.deleted = this.filterGroup.value.deleted;
this.filter.status.error = this.filterGroup.value.error;
this.filter.status.inProgress = this.filterGroup.value.inProgress;
this.filter.status.submitted = this.filterGroup.value.submitted;
this.filter.status.decisioned = this.filterGroup.value.decisioned;
}
public ngOnDestroy(): void {
if (this.formSubscription) {
this.formSubscription.unsubscribe();
}
if (this.dataSubscription) {
this.dataSubscription.unsubscribe();
}
if (this.branchNameConfigDataSubscription){
this.branchNameConfigDataSubscription.unsubscribe();
}
}
}
@@ -0,0 +1,131 @@
import { ReactiveFormsModule } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpHeaders } from '@angular/common/http';
import { configureTestSuite } from 'ng-bullet';
import { LoanTableService } from './loan-table.service';
import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container';
import { TestApiLoanContainer } from 'src/app/testing/shared-mocks/test-api-loan-container';
import { environment } from 'src/environments/environment';
import { FilterUtil } from '../filter/filter-util';
import { TranslateModule } from '@ngx-translate/core';
describe('LoanTable Service', () => {
let service: LoanTableService;
const loanContainer: ApiLoanContainer = TestApiLoanContainer;
const loanContainers: ApiLoanContainer[] = [loanContainer];
let httpTestingController: HttpTestingController;
const urlBase: string = environment.urlBase;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
HttpClientTestingModule
],
schemas: [
NO_ERRORS_SCHEMA
],
declarations: [],
providers: [
LoanTableService
]
});
});
beforeEach(() => {
service = TestBed.get(LoanTableService);
httpTestingController = TestBed.get(HttpTestingController);
});
it('should create', () => {
expect(service).toBeTruthy();
});
it('should build storage url', () => {
const url = service.buildStorageFilterURL();
expect(url).toBe(urlBase + '/Storage/Filter');
});
it('should build loan application url', () => {
const url = service.buildLoanApplicationURL();
expect(url).toBe(urlBase + '/LoanApplication/Submit');
});
it('should identify if there are applications', () => {
const apps = service.hasApplications();
expect(apps).toEqual(false);
});
it('should load applications', () => {
service.loadApplications(FilterUtil.translateFilter(FilterUtil.getDefaultFilter())).subscribe((data) => {
expect(data.test).toEqual('x');
service.setApplications(data);
}
);
const req = httpTestingController.expectOne(service.buildStorageFilterURL());
// Assert that the request is a POST.
expect(req.request.method).toEqual('POST');
// Assert that the request contains the appropriate header.
expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')));
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush({'test': 'x'});
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
it('should delete application', () => {
const loanId = '1234';
service.deleteApplication(loanId).subscribe(() => {});
const req = httpTestingController.expectOne(service.buildStorageDeleteURL() + '/' + loanId);
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers).toEqual(new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken')));
httpTestingController.verify();
});
it('should submit application', () => {
const loanId = '1234';
service.submitApplication(loanId).subscribe(() => {});
const req = httpTestingController.expectOne(service.buildLoanApplicationURL() + '/' + loanId);
expect(req.request.method).toEqual('POST');
httpTestingController.verify();
});
it('should set and get loan containers', () => {
service.setApplications(loanContainers);
let subLoanApps;
service.applicationSet$.subscribe((containers) => {
subLoanApps = containers;
});
expect(subLoanApps).toEqual(loanContainers);
});
it('should get application by id', () => {
service.setApplications(loanContainers);
const loan = service.getApplicationById('12341234');
expect(loan.branch).toBe('milwaukee');
});
it('should fail with bad application id', () => {
service.setApplications(loanContainers);
expect(function() { service.getApplicationById('0'); })
.toThrow(new Error('Application not found'));
});
it('should fail with no applications loaded', () => {
expect(function() { service.getApplicationById('0'); })
.toThrow(new Error('Applications are not loaded'));
});
});
@@ -0,0 +1,135 @@
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { ApiLoanContainer } from 'src/app/shared/api-models/api-loan-container';
import { FilterApi } from '../filter/filter-api';
import { ExportService } from 'src/app/shared/export/export.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FilterUtil } from '../filter/filter-util';
import { Filter } from '../filter/filter';
import { AdminGroups } from '../admin-groups';
import { ConfigurationService } from 'src/app/shared/configuration/configuration.service';
import { BranchOptions } from 'src/app/shared/options/branch-options';
import { map } from 'rxjs/operators';
import { BranchNameConfig, BranchNameData } from './loan-table';
@Injectable({
providedIn: 'root'
})
export class LoanTableService {
private applicationSet: BehaviorSubject<Array<ApiLoanContainer>> = new BehaviorSubject<Array<ApiLoanContainer>>([]);
public applicationSet$ = this.applicationSet.asObservable();
private branchNameConfigSubject: BehaviorSubject<BranchNameConfig> = new BehaviorSubject<BranchNameConfig>(null);
public branchNameConfig$ = this.branchNameConfigSubject.asObservable();
public readonly FILTER_FORM_NAME = 'filterInformation';
private filter = FilterUtil.getDefaultFilter();
constructor(private formBuilder: FormBuilder, private httpClient: HttpClient, private exportService: ExportService, private configurationService: ConfigurationService) {}
public buildHeader(): HttpHeaders {
return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'));
}
public buildStorageDeleteURL(): string {
return environment.urlBase + '/Storage/Loan';
}
public buildStorageFilterURL(): string {
return environment.urlBase + '/Storage/Filter';
}
public buildLoanApplicationURL(): string {
return environment.urlBase + '/LoanApplication/Submit';
}
public buildBranchNameConfigURL(): string {
return environment.urlBase + '/BranchConfig/BranchNameConfig';
}
public loadApplications(filterApi: FilterApi): Observable<any> {
const headers = this.buildHeader();
return this.httpClient.post(this.buildStorageFilterURL(), filterApi, {headers, withCredentials: true});
}
public hasBranchNameConfig(): boolean {
return this.branchNameConfigSubject.value !== null;
}
public loadBranchNameConfig(): Observable<BranchNameConfig> {
const headers = this.buildHeader();
return this.httpClient.get(this.buildBranchNameConfigURL(), {headers, withCredentials: true}).pipe(
map((success) => {
const config = new BranchNameConfig();
config.id = success['id'];
config.date = success['date'];
config.branchNamesByChannelId = success['branchNamesByChannelId'];
return config;
})
);
}
public setbranchNameConfig(branchNameConfig: BranchNameConfig): void {
this.branchNameConfigSubject.next(branchNameConfig);
}
public deleteApplication(loanId: string): Observable<any> {
const headers = this.buildHeader();
return this.httpClient.delete(this.buildStorageDeleteURL() + '/' + loanId, {headers, withCredentials: true});
}
public submitApplication(loanId: string): Observable<any> {
return this.httpClient.post(this.buildLoanApplicationURL() + '/' + loanId, '');
}
public hasApplications(): boolean {
return !!(this.applicationSet.value.length);
}
public setApplications(applications: ApiLoanContainer[]): void {
this.applicationSet.next(applications);
}
public getApplicationById(loanId: string): ApiLoanContainer {
if (!this.applicationSet.value.length) {
throw new Error('Applications are not loaded');
}
const single = this.applicationSet.value.find(x => x.id === loanId);
if (!single) {
throw new Error('Application not found');
}
return single;
}
public getFilter() {
return this.filter;
}
public setFilter(filter: Filter) {
this.filter = filter;
}
public exportCSV(dataSource: ApiLoanContainer[]) {
this.exportService.exportLoanContainerAsCSV(dataSource);
}
public createPageGroup(): FormGroup {
const group = this.formBuilder.group({});
const filterGroup = this.createFilterGroup();
group.addControl(this.FILTER_FORM_NAME, filterGroup);
return group;
}
public createFilterGroup(): FormGroup {
const filterGroup = this.formBuilder.group({});
AdminGroups.FILTER.fields.forEach((eachField) => {
filterGroup.addControl(eachField.field, this.formBuilder.control(''));
filterGroup.controls[eachField.field].setValidators(eachField.validation.slice());
}, this);
return filterGroup;
}
}
@@ -0,0 +1,10 @@
export class BranchNameData {
channelId: number;
branchName: string;
}
export class BranchNameConfig {
public id: string;
public date: Date;
public branchNamesByChannelId: Array<BranchNameData>;
}
@@ -0,0 +1,2 @@
<button type="button" class="btn btn-primary btn-home" id='buttonHome' (click)="commandHome()" translate><i class="fas fa-home" style="margin-right: 0.33em"></i>ADMIN.HOME</button>
<hr class="title-hr"/>
@@ -0,0 +1,11 @@
.btn-home {
margin-bottom: 15px;
}
.title-hr {
margin: 0 0 15px 0;
color: #B1B1B1;
height: 1px;
border: none;
background-color: #B1B1B1;
}
@@ -0,0 +1,35 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ReactiveFormsModule } from '@angular/forms';
import { configureTestSuite } from 'ng-bullet';
import { TopNavigationComponent } from './top-navigation.component';
describe('TopNavigationComponent', () => {
let component: TopNavigationComponent;
let fixture: ComponentFixture<TopNavigationComponent>;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule
],
declarations: [
TopNavigationComponent,
],
});
});
beforeEach(() => {
fixture = TestBed.createComponent(TopNavigationComponent);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'lw-top-navigation',
templateUrl: './top-navigation.component.html',
styleUrls: ['./top-navigation.component.scss']
})
export class TopNavigationComponent {
constructor(private router: Router) { }
public commandHome(): void {
this.router.navigate(['/admin']);
}
}
@@ -0,0 +1,20 @@
<lw-top-navigation></lw-top-navigation>
<div class="loan-header" translate>ADMIN.SCHUMER</div>
<hr class="title-hr"/>
<div class="container">
<div class="row">
<div class="col-12 col-md-6 col-lg-4">
<strong>Select a loan type to preview the Schumer box:</strong>
</div>
<div class="col-12 col-md-6 col-lg-4">
<select id="schumer-preview-type" [(ngModel)]="schumerType" class="form-control">
<option value="CC_CashBack" >CC_CashBack</option>
<option value="CC_Rewards" >CC_Rewards</option>
<option value="CC_Student" >CC_Student</option>
<option value="CC_Secure" >CC_Secure</option>
</select>
</div>
</div>
</div>
<lw-schumer-box [showSchumer]="localState.showSchumer" [boxType]="schumerType"></lw-schumer-box>
@@ -0,0 +1,11 @@
$max-width: 770px;
@media screen and (max-width: $max-width) {
.url-long {
display: none;
}
.url-short[style] {
display: inline !important;
}
}
@@ -0,0 +1,18 @@
import {Component} from '@angular/core';
@Component({
selector: 'lw-schumer-preview',
templateUrl: './schumer-preview.component.html',
styleUrls: ['./schumer-preview.component.scss']
})
export class SchumerPreviewComponent {
public schumerType = 'CC_CashBack';
public localState = {
showSchumer: true
};
constructor() {
}
}
@@ -0,0 +1,11 @@
export class ServerError {
public statusCode: string;
public message: string;
constructor (statusCode: string, message: string) {
this.statusCode = statusCode;
this.message = message;
}
}
@@ -0,0 +1,4 @@
<span >
<div>HTTP Status: {{error.statusCode}}</div>
<div>Message: {{error.message | translate}}</div>
</span>
@@ -0,0 +1,21 @@
import { Component, Inject } from '@angular/core';
import { MAT_SNACK_BAR_DATA } from '@angular/material';
import { ServerError } from './server-error';
@Component({
selector: 'lw-snack-bar-error',
templateUrl: 'snack-bar-error.component.html',
styles: ['snack-bar-error.component.scss'],
})
export class SnackBarErrorComponent {
public error: ServerError;
constructor(@Inject(MAT_SNACK_BAR_DATA) public data: ServerError) {
this.error = data;
}
}
@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { SnackBarErrorComponent } from './snack-bar-error.component';
import { ServerError } from './server-error';
@Injectable({
providedIn: 'root'
})
export class SnackBarService {
public errorDurationInSeconds = 10;
constructor(public snackBar: MatSnackBar) {}
public throwServerError(error: any, errorMessage: string): void {
this.snackBar.openFromComponent(SnackBarErrorComponent, {
verticalPosition: 'top',
duration: this.errorDurationInSeconds * 1000,
data: new ServerError(this.getStatusFromError(error), errorMessage)
});
}
public throwSuccess(message: string): void {
this.snackBar.open(message, 'Okay', {
verticalPosition: 'top',
});
}
public getStatusFromError(error: any): string {
if (error && error['status']) {
return error['status'];
} else {
return 'Unknown';
}
}
}
@@ -0,0 +1,25 @@
<lw-top-navigation></lw-top-navigation>
<form [formGroup]="pageForm" novalidate (ngSubmit)="commandSaveSettings()">
<div [formGroup]="configGroup">
<div class="card">
<div class="card-header address-compare-panel-header">
<h4 translate>CONFIG.TITLE</h4>
<span translate>CONFIG.SUBTEXT</span>
</div>
<div class="card-body">
<div class="col-12 col-md-6 col-lg-4">
<div class="alert alert-secondary" role="alert">
<h4 class="alert-heading" translate>CONFIG.STATUS</h4>
<hr>
<span *ngIf="storedSettings.applicationDisabled" style="margin-right: 10px" translate>CONFIG.APP_DISABLED</span>
<span *ngIf="!storedSettings.applicationDisabled" style="margin-right: 10px" translate>CONFIG.APP_ENABLED</span>
<span style="float:right"><mat-slide-toggle formControlName="appStatus"></mat-slide-toggle></span>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-success btn-save" (click)="commandSaveSettings()" translate>CONFIG.SAVE
<span *ngIf="localState.saveIndicator" style="margin-left: 0.33em"><i focusable='false' class="fas fa-spinner fa-pulse"></i></span>
</button>
</div>
</form>
@@ -0,0 +1,16 @@
.btn-save {
margin-top: 15px;
}
.alert-secondary hr {
margin-top: 0px;
margin-bottom: 5px;
border-top-color: #c8cbcf;
}
.alert-secondary {
background-color: #E2E3E5;
color: #383D41;
border-color: #d6d8db;
margin-bottom: 0px;
}
@@ -0,0 +1,71 @@
import { Component, OnInit } from '@angular/core';
import { SystemConfigService } from './system-config.service';
import { Configuration } from '../../shared/configuration/configuration';
import { FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { SnackBarService } from '../snack-bar-error/snack-bar.service';
@Component({
selector: 'lw-system-config',
templateUrl: './system-config.component.html',
styleUrls: ['./system-config.component.scss']
})
export class SystemConfigComponent implements OnInit {
public pageForm: FormGroup;
public configGroup: FormGroup;
public storedSettings: Configuration = new Configuration();
public formSubscription: Subscription;
public localState = {
saveIndicator: false,
};
constructor(private systemConfigService: SystemConfigService, private snackBarService: SnackBarService) {}
public ngOnInit() {
this.setupForm();
this.loadStoredSettings();
}
public loadStoredSettings() {
this.systemConfigService.getStoredSettings().subscribe (
(success: Configuration) => {
this.storedSettings = success;
this.patchForm(this.storedSettings);
},
(error) => {
this.snackBarService.throwServerError(error, 'CONFIG.LOAD_ERROR');
}
);
}
public updateStoredSettings() {
this.localState.saveIndicator = true;
this.systemConfigService.updateStoredSettings(this.storedSettings).subscribe (
(success: Configuration) => {
this.storedSettings = success;
this.patchForm(this.storedSettings);
this.localState.saveIndicator = false;
this.snackBarService.throwSuccess('Your changes have been successfully updated and published.');
},
(error) => {
this.snackBarService.throwServerError(error, 'CONFIG.UPDATE_ERROR');
this.localState.saveIndicator = false;
}
);
}
public commandSaveSettings() {
this.storedSettings.applicationDisabled = this.configGroup.value.appStatus ? false : true;
this.updateStoredSettings();
}
public setupForm(): void {
this.pageForm = this.systemConfigService.createPageGroup();
this.configGroup = <FormGroup>this.pageForm.get(this.systemConfigService.CONFIG_FORM_NAME);
}
public patchForm(config: Configuration): void {
this.configGroup.patchValue({appStatus: config.applicationDisabled ? false : true});
}
}
@@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Configuration } from '../../shared/configuration/configuration';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AdminGroups } from '../admin-groups';
@Injectable({
providedIn: 'root'
})
export class SystemConfigService {
public readonly CONFIG_FORM_NAME = 'configInformation';
constructor(private httpClient: HttpClient, private formBuilder: FormBuilder) {}
public buildHeader(): HttpHeaders {
return new HttpHeaders().set('Authorization', 'Bearer ' + localStorage.getItem('adal.idtoken'));
}
public buildStoredSettingsURL(): string {
return environment.urlBase + '/UiSettings/StoredSettings';
}
public getStoredSettings(): Observable<any> {
const headers = this.buildHeader();
return this.httpClient.get(this.buildStoredSettingsURL(), {headers, withCredentials: true});
}
public updateStoredSettings(settings: Configuration): Observable<any> {
const headers = this.buildHeader();
return this.httpClient.put(this.buildStoredSettingsURL(), settings, {headers, withCredentials: true});
}
public createPageGroup(): FormGroup {
const group = this.formBuilder.group({});
const configGroup = this.createConfigGroup();
group.addControl(this.CONFIG_FORM_NAME, configGroup);
return group;
}
public createConfigGroup(): FormGroup {
const configGroup = this.formBuilder.group({});
AdminGroups.CONFIG.fields.forEach((eachField) => {
configGroup.addControl(eachField.field, this.formBuilder.control(''));
configGroup.controls[eachField.field].setValidators(eachField.validation.slice());
}, this);
return configGroup;
}
}
@@ -0,0 +1,125 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoanWizardComponent } from './loan-wizard/loan-wizard.component';
import { SelectLoanComponent } from './select-loan/select-loan.component';
import { AppUnavailableComponent } from './shared/app-unavailable/app-unavailable-page.component';
import { DealershipPortalComponent } from './shared/dealership-portal/dealership-portal-page.component';
import { ConfigurationResolver } from './shared/configuration/configuration.resolver';
import { ConfigurationService } from './shared/configuration/configuration.service';
import { TimeoutLandingPageComponent } from './shared/timeout/timeout-landing-page.component';
import { AppTimeoutGuard } from './shared/guards/app-timeout.guard';
import { ccRoutes } from './loan-wizard/wizards/credit-card/credit-card-routing.module';
import { eqRoutes } from './loan-wizard/wizards/equity/equity-routing.module';
import { veRoutes } from './loan-wizard/wizards/vehicle/vehicle-routing.module';
import { slRoutes } from './loan-wizard/wizards/signature/signature-routing.module';
import { SelectCreditCardComponent } from './select-loan/credit-card/select-credit-card.component';
import { SelectLoanOutletComponent } from './select-loan/outlet/select-loan-outlet.component';
import { SelectHomeEquityComponent } from './select-loan/home/select-home-equity.component';
import { SelectVehicleComponent } from './select-loan/vehicle/select-vehicle.component';
import { SelectSubVehicleComponent } from './select-loan/vehicle/sub-vehicle/select-sub-vehicle.component';
import { MemberLoginComponent } from './member/member-login/member-login.component';
import { MemberOutletComponent } from './member/member-outlet/member-outlet.component';
import { AuthenticationGuard } from 'microsoft-adal-angular6';
import { AdminRoleGuard } from './shared/guards/admin-role.guard';
import { environment } from 'src/environments/environment';
import { TestRoleGuard } from './shared/guards/test-role.guard';
import { MemberLoginGuard } from './shared/guards/member-login.guard';
import { MemberVerificationComponent } from './member/member-verification/member-verification.component';
import { MemberVerificationGuard } from './shared/guards/member-verification-guard';
import { MemberCoreResolver } from './member/member-core.resolver';
import { AppDownComponent } from './shared/app-down/app-down-page.component';
export const routes: Routes = [
{
path: 'select-loan',
component: SelectLoanOutletComponent,
resolve: {UiSettings: ConfigurationResolver},
canActivate:
environment.testGuard ? [AuthenticationGuard, TestRoleGuard, MemberVerificationGuard] : [MemberVerificationGuard],
children: [
{
path: '',
component: SelectLoanComponent
},
{
path: 'credit-card',
component: SelectCreditCardComponent
},
{
path: 'home-equity',
component: SelectHomeEquityComponent,
},
{
path: 'vehicle',
component: SelectVehicleComponent,
},
{
path: 'sub-vehicle',
component: SelectSubVehicleComponent,
}
]
},
{
path: 'member',
component: MemberOutletComponent,
canActivate: environment.testGuard ? [AuthenticationGuard] : [],
children: [
{
path: '',
component: MemberLoginComponent
},
{
path: 'verification',
component: MemberVerificationComponent
},
]
},
{
path: 'admin',
canActivate: [AuthenticationGuard, AdminRoleGuard],
loadChildren: './admin/admin.module#AdminModule'
},
{
path: 'timeout',
component: TimeoutLandingPageComponent,
canDeactivate: [AppTimeoutGuard]
},
{
path: 'error',
component: AppUnavailableComponent
},
{
path: 'dealership-portal',
resolve: {UiSettings: ConfigurationResolver},
component: DealershipPortalComponent
},
{
path: 'down',
component: AppDownComponent
},
{
path: 'loan-wizard',
component: LoanWizardComponent,
resolve: {UiSettings: ConfigurationResolver},
canActivate: [ MemberLoginGuard ],
canActivateChild:
environment.testGuard ? [AuthenticationGuard, TestRoleGuard, MemberVerificationGuard] : [MemberVerificationGuard],
children: [
ccRoutes,
eqRoutes,
veRoutes,
slRoutes,
{path: '', redirectTo: '/select-loan', pathMatch: 'prefix'},
]
},
{path: '', redirectTo: '/select-loan', pathMatch: 'full'},
{path: '**', redirectTo: '/select-loan'},
];
@NgModule({
imports: [RouterModule.forRoot(routes, {useHash: false, enableTracing: false})],
exports: [RouterModule],
providers: [ConfigurationService, ConfigurationResolver, MemberCoreResolver]
})
export class AppRoutingModule {}
@@ -0,0 +1,9 @@
<a [href]="skipLinkPath" class="sr-only">Skip To Content</a>
<lw-timeout-popup></lw-timeout-popup>
<lw-header id="header"></lw-header>
<div class="container" id="content" name="content" style="min-height: 400px;">
<router-outlet></router-outlet>
</div>
<br/>
<lw-footer></lw-footer>
@@ -0,0 +1,69 @@
import {TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {FooterComponent} from './layout/footer/footer.component';
import {HeaderComponent} from './layout/header/header.component';
import {TranslateModule} from '@ngx-translate/core';
import {TimeoutPopupComponent} from './shared/timeout/timeout-popup.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CreditApplicationDaoService } from './shared/credit-application-dao.service';
import { MockCreditApplicationDaoService } from './testing/shared-mocks/credit-application-dao.mock.service';
import { configureTestSuite } from 'ng-bullet';
describe('AppComponent', () => {
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
AppComponent,
FooterComponent,
HeaderComponent,
TimeoutPopupComponent
],
providers: [
{provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}
],
schemas: [ NO_ERRORS_SCHEMA ]
});
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it('should render a router outlet', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('router-oulet')).toBeDefined();
});
it('should include a header', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('lw-header')).toBeDefined();
});
it('should include a footer', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('lw-footer')).toBeDefined();
});
it('should include timeout modal', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('lw-timeout-popup')).toBeDefined();
});
});
@@ -0,0 +1,39 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import {TranslateService} from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { NavigationEnd, Router } from '@angular/router';
@Component({
selector: 'lw-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
private subscriptionRef: Subscription;
public skipLinkPath: string;
constructor(translate: TranslateService,
private router: Router) {
translate.setDefaultLang('en');
translate.use('en');
}
ngOnInit() {
this.subscriptionRef = this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.scrollToTop();
}
this.skipLinkPath = this.router.url.includes('#content') ? `${this.router.url}` : `${this.router.url}#content`;
});
}
ngOnDestroy() {
if (this.subscriptionRef) {
this.subscriptionRef.unsubscribe();
}
}
private scrollToTop(): void {
document.getElementById('header').scrollIntoView();
}
}
@@ -0,0 +1,281 @@
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { LoanWizardComponent } from './loan-wizard/loan-wizard.component';
import { WaypointsComponent } from './layout/waypoints/waypoints.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { InputIconComponent } from './loan-wizard/controls/input-icon/input-icon.component';
import { ErrorDisplayComponent } from './loan-wizard/controls/error-display/error-display.component';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { SelectLoanComponent } from './select-loan/select-loan.component';
import { MemberInfoFormComponent } from './loan-wizard/field-groups/member/member-info-form.component';
import { PersonalInfoFormComponent } from './loan-wizard/field-groups/personal/personal-info-form.component';
import { AddressInfoFormComponent } from './loan-wizard/field-groups/address/address-info-form.component';
import { EmploymentInfoFormComponent } from './loan-wizard/field-groups/employment/employment-info-form.component';
import { AddressInfoPageComponent } from './loan-wizard/wizards/common/address/address-info-page.component';
import { MemberInfoPageComponent } from './loan-wizard/wizards/common/member/member-info-page.component';
import { PersonalInfoPageComponent } from './loan-wizard/wizards/common/personal/personal-info-page.component';
import { EmploymentInfoPageComponent } from './loan-wizard/wizards/common/employment/employment-info-page.component';
import { FooterComponent } from './layout/footer/footer.component';
import { HeaderComponent } from './layout/header/header.component';
import { HeaderIconComponent } from './layout/header/header-icon.component';
import { PatriotActComponent } from './shared/patriot-act/patriot-act.component';
import { IsAddressSameInfoFormComponent } from './loan-wizard/field-groups/address/is-address-same-info-form.component';
import { CreditCardDisclosureFormComponent } from './loan-wizard/field-groups/disclosures/credit-card-disclosure-form.component';
import { CreditCardDisclosuresPageComponent } from './loan-wizard/wizards/credit-card/credit-card-disclosures-page.component';
import { CreditCardSchumerBoxComponent } from './loan-wizard/wizards/credit-card/credit-card-schumer-box.component';
import { SummaryPageComponent } from './loan-wizard/wizards/common/summary/summary-page.component';
import { TextMaskModule } from 'angular2-text-mask';
import { PersonalSummaryComponent } from './loan-wizard/wizards/common/summary/personal-summary.component';
import { ResidenceSummaryComponent } from './loan-wizard/wizards/common/summary/residence-summary.component';
import { ApplicationSummaryComponent } from './loan-wizard/wizards/common/summary/application-summary.component';
import { IncomeSummaryComponent } from './loan-wizard/wizards/common/summary/income-summary.component';
import { EquityMemberInfoPageComponent } from './loan-wizard/wizards/equity/equity-member-info-page.component';
import { ResidenceInfoFormComponent } from './loan-wizard/field-groups/address/residence-info-form.component';
import { DecisionPageComponent } from './loan-wizard/wizards/common/decision/decision-page.component';
import { BeforeYouBeginComponent } from './shared/before-you-begin/before-you-begin.component';
import { FileUploadComponent } from './shared/file-upload/file-upload.component';
import { SsnPipe } from './shared/pipes/ssn.pipe';
import { PhoneTypePipe } from './shared/pipes/phone-type.pipe';
import { UploadedFilesPipe } from './shared/pipes/uploaded-files.pipe';
import { PhoneNumberPipe } from './shared/pipes/phone-number.pipe';
import { StatePipe } from './shared/pipes/state.pipe';
import { AddressStatusPipe } from './shared/pipes/address-status.pipe';
import { EmploymentTypePipe } from './shared/pipes/employment-type.pipe';
import { CollateralInfoFormComponent } from './loan-wizard/field-groups/equity/collateral-info-form.component';
import { CollateralInfoPageComponent } from './loan-wizard/wizards/equity/collateral-info-page.component';
import { CollateralSummaryComponent } from './loan-wizard/wizards/common/summary/collateral-summary.component';
import { BooleanStringPipe } from './shared/pipes/boolean-string.pipe';
import { OccupancyTypePipe } from './shared/pipes/occupancy-type.pipe';
import { PropertyTypePipe, MfgHomePropertyTypePipe } from './shared/pipes/property-type.pipe';
import { GenderInfoFormComponent } from './loan-wizard/field-groups/hmda/gender-info-form.component';
import { EthnicityInfoFormComponent } from './loan-wizard/field-groups/hmda/ethnicity-info-form.component';
import { RaceInfoFormComponent } from './loan-wizard/field-groups/hmda/race-info-form.component';
import { HmdaInfoPageComponent } from './loan-wizard/wizards/common/hmda/hmda-info-page.component';
import { ElectronicDisclosuresFormComponent } from './loan-wizard/field-groups/disclosures/electronic-disclosures-form.component';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { OtherIncomeInfoFormComponent } from './loan-wizard/field-groups/employment/other-income-info-form.component';
import { AlimonyInfoFormComponent } from './loan-wizard/field-groups/employment/alimony-info-form.component';
import { EmploymentFlagFormComponent } from './loan-wizard/field-groups/employment/financial/employment-flag-form.component';
import { AlimonyFlagFormComponent } from './loan-wizard/field-groups/employment/financial/alimony-flag-form.component';
import { OtherIncomeFlagFormComponent } from './loan-wizard/field-groups/employment/financial/other-income-flag-form.component';
import { VehicleCollateralInfoPageComponent } from './loan-wizard/wizards/vehicle/vehicle-collateral-info-page.component';
import { VehicleCollateralInfoFormComponent } from './loan-wizard/field-groups/vehicle/vehicle-collateral-info-form.component';
import { AppUnavailableComponent } from './shared/app-unavailable/app-unavailable-page.component';
import { DealershipPortalComponent } from './shared/dealership-portal/dealership-portal-page.component';
import { TimeoutPopupComponent } from './shared/timeout/timeout-popup.component';
import { TimeoutLandingPageComponent } from './shared/timeout/timeout-landing-page.component';
import { TattleSpousePersonalInfoFormComponent } from './loan-wizard/field-groups/tattle-spouse/tattle-spouse-personal-info-form.component';
import { TattleSpouseInfoPageComponent } from './loan-wizard/wizards/common/tattle-spouse/tattle-spouse-info-page.component';
import { SpouseTypeInfoPageComponent } from './loan-wizard/wizards/common/spouse-type/spouse-type-info-page.component';
import { IsFullInfoSpouseInfoFormComponent } from './loan-wizard/field-groups/spouse-type/is-full-info-spouse-info-form.component';
import { IsRentedLotInfoFormComponent } from './loan-wizard/field-groups/equity/is-rented-lot-info-form.component';
import { TattleSpouseSummaryComponent } from './loan-wizard/wizards/common/summary/tattle-spouse-summary.component';
import { environment } from '../environments/environment';
import { DecisionAnimationComponent } from './layout/decision-animation/decision-animation.component';
import { JointCreditFormComponent } from './loan-wizard/field-groups/warning-modals/joint-credit-confirmation.component';
import { ProcessingPageComponent } from './loan-wizard/wizards/common/processing/processing-page.component';
import { DecisionHeroComponent } from './layout/decision-hero/decision-hero.component';
import { AutoFocusDirective } from './shared/page-focus/auto-focus.directive';
import { VehicleSummaryComponent } from './loan-wizard/wizards/common/summary/vehicle-summary.component';
import { VehicleSellerPipe } from './shared/pipes/vehicle-collateral.pipe';
import { DisclosureBoxesComponent } from './shared/legal-message/disclosure-boxes.component';
import { NoteFormComponent } from './loan-wizard/field-groups/summary/note-form.component';
import { AdditionalInfoFormComponent } from './loan-wizard/field-groups/summary/additional-info-form.component';
import { BUILD_STAMP } from '../environments/version';
import { AddressCompareInfoFormComponent } from './loan-wizard/field-groups/address-compare/address-compare-info-form.component';
import { VehicleTypePipe } from './shared/pipes/vehicle-type.pipe';
import { AuthenticationGuard, MsAdalAngular6Module } from 'microsoft-adal-angular6';
import { AppLoaderComponent } from './shared/app-loader/app-loader.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TcpaComponent } from './shared/legal-message/tcpa.component';
import { SelectCreditCardComponent } from './select-loan/credit-card/select-credit-card.component';
import { SelectHomeEquityComponent } from './select-loan/home/select-home-equity.component';
import { SelectVehicleComponent } from './select-loan/vehicle/select-vehicle.component';
import { MainCardComponent } from './select-loan/cards/main-card/main-card.component';
import { SubCardComponent } from './select-loan/cards/sub-card/sub-card.component';
import { SelectLoanOutletComponent } from './select-loan/outlet/select-loan-outlet.component';
import { SelectSubVehicleComponent } from './select-loan/vehicle/sub-vehicle/select-sub-vehicle.component';
import { MemberLoginComponent } from './member/member-login/member-login.component';
import { MemberOutletComponent } from './member/member-outlet/member-outlet.component';
import { SharedModule } from './shared/shared.module';
import { MemberVerificationComponent } from './member/member-verification/member-verification.component';
import { LoanIconPipe } from './shared/pipes/loan-icon.pipe';
import { HttpErrorReporter } from './shared/http-error-reporter.service';
import { AppDownComponent } from './shared/app-down/app-down-page.component';
function resolveRedirectUri(): string {
if (typeof window === 'undefined') {
return environment.msAuthRedirectUri;
}
const rawRedirectUri = environment.msAuthRedirectUri;
if (rawRedirectUri) {
try {
const parsed = new URL(rawRedirectUri, window.location.origin);
return `${window.location.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
} catch (error) {
// Fall back to origin if the configured redirect URI is invalid.
}
}
const msAuthRedirectUri = window.location.origin;
return window.location.pathname === '/admin' ? window.location.href : msAuthRedirectUri;
}
export function adalConfigFactory() {
return {
tenant: '63cd1554-2783-4a86-ae50-b9e6c21c9499',
clientId: '59d54f39-3ff9-4166-8b16-254c00416754',
redirectUri: resolveRedirectUri(),
endpoints: {},
navigateToLoginRequestUrl: false,
cacheLocation: 'localStorage'
};
}
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, environment.baseHref + '/assets/i18n/', '.json?cb=' + new Date().getTime());
}
@NgModule({
declarations: [
AddressCompareInfoFormComponent,
AddressInfoFormComponent,
AddressInfoPageComponent,
AddressStatusPipe,
AlimonyFlagFormComponent,
AlimonyInfoFormComponent,
AppComponent,
ApplicationSummaryComponent,
AppLoaderComponent,
AppDownComponent,
AppUnavailableComponent,
DealershipPortalComponent,
AutoFocusDirective,
BeforeYouBeginComponent,
FileUploadComponent,
BooleanStringPipe,
CollateralInfoFormComponent,
CollateralInfoPageComponent,
CollateralSummaryComponent,
CreditCardDisclosureFormComponent,
CreditCardDisclosuresPageComponent,
CreditCardSchumerBoxComponent,
DecisionAnimationComponent,
DecisionPageComponent,
DecisionHeroComponent,
DisclosureBoxesComponent,
ElectronicDisclosuresFormComponent,
EmploymentFlagFormComponent,
EmploymentInfoFormComponent,
EmploymentInfoPageComponent,
EmploymentTypePipe,
EthnicityInfoFormComponent,
EquityMemberInfoPageComponent,
ErrorDisplayComponent,
FooterComponent,
GenderInfoFormComponent,
HeaderComponent,
HeaderIconComponent,
HmdaInfoPageComponent,
IncomeSummaryComponent,
InputIconComponent,
IsAddressSameInfoFormComponent,
IsFullInfoSpouseInfoFormComponent,
IsRentedLotInfoFormComponent,
JointCreditFormComponent,
TcpaComponent,
LoanWizardComponent,
LoanIconPipe,
MainCardComponent,
MemberLoginComponent,
MemberVerificationComponent,
MemberOutletComponent,
MemberInfoFormComponent,
MemberInfoPageComponent,
MfgHomePropertyTypePipe,
NoteFormComponent,
AdditionalInfoFormComponent,
OccupancyTypePipe,
OtherIncomeFlagFormComponent,
OtherIncomeInfoFormComponent,
PatriotActComponent,
PersonalInfoFormComponent,
PersonalInfoPageComponent,
PersonalSummaryComponent,
PhoneNumberPipe,
PhoneTypePipe,
UploadedFilesPipe,
ProcessingPageComponent,
PropertyTypePipe,
RaceInfoFormComponent,
ResidenceSummaryComponent,
ResidenceInfoFormComponent,
SelectLoanOutletComponent,
SelectLoanComponent,
SelectCreditCardComponent,
SelectHomeEquityComponent,
SelectVehicleComponent,
SelectSubVehicleComponent,
SpouseTypeInfoPageComponent,
SsnPipe,
StatePipe,
SubCardComponent,
SummaryPageComponent,
TattleSpousePersonalInfoFormComponent,
TattleSpouseInfoPageComponent,
TattleSpouseSummaryComponent,
TimeoutPopupComponent,
TimeoutLandingPageComponent,
VehicleCollateralInfoPageComponent,
VehicleCollateralInfoFormComponent,
VehicleSellerPipe,
VehicleTypePipe,
VehicleSummaryComponent,
WaypointsComponent
],
imports: [
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
NgbModule.forRoot(),
SharedModule,
TextMaskModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
}
}),
ReactiveFormsModule,
MsAdalAngular6Module
],
providers: [
{
provide: 'adalConfig',
useFactory: adalConfigFactory,
},
AuthenticationGuard,
{provide: ErrorHandler, useClass: HttpErrorReporter}
],
bootstrap: [AppComponent],
entryComponents: [
AddressCompareInfoFormComponent,
AddressInfoFormComponent,
CollateralInfoFormComponent,
IsRentedLotInfoFormComponent,
ResidenceInfoFormComponent,
EmploymentInfoFormComponent,
IsAddressSameInfoFormComponent,
IsFullInfoSpouseInfoFormComponent,
MemberInfoFormComponent,
TattleSpousePersonalInfoFormComponent,
TimeoutPopupComponent
]
})
export class AppModule {}
@@ -0,0 +1,9 @@
<span *ngIf="!step?.isActive && !step?.isComplete">
<i focusable='false' class="fas fa-spinner"></i>
</span>
<span *ngIf="step?.isActive">
<i focusable='false' class="fas fa-spinner fa-pulse"></i>
</span>
<span *ngIf="step?.isComplete">
<i focusable='false' class="fas fa-check-circle"></i>
</span>
@@ -0,0 +1,67 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { configureTestSuite } from 'ng-bullet';
import { DecisionAnimationComponent } from './decision-animation.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ProcessingStep } from '../../loan-wizard/wizards/common/processing/processing-step';
describe('Decision Animation Component', () => {
let component: DecisionAnimationComponent;
let fixture: ComponentFixture<DecisionAnimationComponent>;
const preMsg = 'test_pre_msg';
const postMsg = 'test_post_msg';
const hasStatus = false;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [ NgbModule.forRoot()],
declarations: [ DecisionAnimationComponent ]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(DecisionAnimationComponent);
component = fixture.componentInstance;
component.step = new ProcessingStep( preMsg, postMsg, hasStatus);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create with correct values', () => {
expect(component.step).toBeTruthy();
expect(component.step.preMessage).toEqual(preMsg);
expect(component.step.postMessage).toEqual(postMsg);
expect(component.step.hasStatus).toEqual(hasStatus);
});
it('should set current', () => {
expect(component.step).toBeTruthy();
component.step.setCurrent(true);
expect(component.step.isActive).toEqual(true);
});
it('should set complete', () => {
component.step.setComplete(true);
expect(component.step.isActive).toEqual(false);
expect(component.step.isComplete).toEqual(true);
});
it('should get message', () => {
component.step.setComplete(false);
let message = component.step.getMessage();
expect(message).toEqual(preMsg);
component.step.setComplete(true);
message = component.step.getMessage();
expect(message).toEqual(postMsg);
});
it('should set post message', () => {
const message = 'a different test message';
expect(component.step.postMessage).toEqual(postMsg);
component.step.setPostMessage(message);
expect(component.step.postMessage).toEqual(message);
});
});
@@ -0,0 +1,15 @@
import { Component, Input } from '@angular/core';
import { ProcessingStep } from '../../loan-wizard/wizards/common/processing/processing-step';
@Component({
selector: 'lw-decision-animation',
templateUrl: './decision-animation.component.html',
styleUrls: ['./decision-animation.component.scss']
})
export class DecisionAnimationComponent {
@Input()
step: ProcessingStep;
constructor() { }
}
@@ -0,0 +1,5 @@
<div class="jumbotron landmark-hero" >
<p class="landmark-hero-status" translate [innerHTML]="localState.title"></p>
<p class="landmark-hero-copy" translate [innerHTML]="localState.text"></p>
</div>
@@ -0,0 +1,73 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {DecisionHeroComponent, HeroType} from './decision-hero.component';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {of} from 'rxjs';
import {configureTestSuite} from 'ng-bullet';
import {LoanTypeSet} from '../../shared/loan-type';
describe('Decision Hero Component', () => {
let component: DecisionHeroComponent;
let fixture: ComponentFixture<DecisionHeroComponent>;
let translateService: TranslateService;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [NgbModule.forRoot(), TranslateModule.forRoot()],
declarations: [DecisionHeroComponent]
});
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NgbModule.forRoot(), TranslateModule.forRoot()],
declarations: [DecisionHeroComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DecisionHeroComponent);
component = fixture.componentInstance;
fixture.detectChanges();
translateService = TestBed.get(TranslateService);
});
it('should create', () => {
expect(component).toBeTruthy();
component.displayName = 'John';
component.heroType = HeroType.SUCCESS;
fixture.detectChanges();
});
it('should find the right key for a type', () => {
expect(component).toBeTruthy();
expect(component.lookupKeyForType(null)).toEqual('DECISION.ERROR_SUBMIT');
expect(component.lookupKeyForType(HeroType.REVIEW)).toEqual('DECISION.REVIEW');
expect(component.lookupKeyForType(HeroType.ERROR_SUBMIT)).toEqual('DECISION.ERROR_SUBMIT');
expect(component.lookupKeyForType(HeroType.ERROR_DECISION)).toEqual('DECISION.ERROR_DECISION');
});
it('should ask the translate service for the messaging', () => {
expect(component).toBeTruthy();
spyOn(component, 'lookupKeyForType').and.returnValue('DECISION.ERROR_SUBMIT');
spyOn(translateService, 'get').and.returnValue(of('AAA'));
component.ngOnChanges(null);
expect(translateService.get).toHaveBeenCalledTimes(2);
expect(component.lookupKeyForType).toHaveBeenCalled();
});
it('should ask the translate service for the messaging', () => {
expect(component).toBeTruthy();
expect(LoanTypeSet[0].decisionReviewKey).toEqual('TEXT_CC');
component.decisionKey = LoanTypeSet[0].decisionReviewKey;
spyOn(component, 'lookupKeyForType').and.returnValue('DECISION.REVIEW');
spyOn(translateService, 'get').and.returnValue(of('AAA'));
component.ngOnChanges(null);
expect(translateService.get).toHaveBeenCalledWith('DECISION.REVIEW.TITLE', {displayName: undefined});
expect(translateService.get).toHaveBeenCalledWith('DECISION.REVIEW.TEXT_CC', {displayName: undefined});
expect(translateService.get).toHaveBeenCalledTimes(2);
expect(component.lookupKeyForType).toHaveBeenCalled();
});
});
@@ -0,0 +1,62 @@
import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
import {TranslateService} from '@ngx-translate/core';
export enum HeroType {
SUCCESS,
REVIEW,
DENIED,
ERROR_SUBMIT,
ERROR_DECISION
}
@Component({
selector: 'lw-decision-hero',
templateUrl: './decision-hero.component.html',
styleUrls: ['./decision-hero.component.scss']
})
export class DecisionHeroComponent implements OnChanges {
public localState = {
text: '',
title: ''
};
@Input()
public decisionKey: string;
@Input()
public heroType: HeroType;
@Input()
public displayName: string;
constructor(private translateService: TranslateService) {
}
public ngOnChanges(changes: SimpleChanges): void {
const key = this.lookupKeyForType(this.heroType);
const decisionKey = this.decisionKey || 'TEXT';
this.translateService.get(key + '.TITLE', {displayName: this.displayName}).subscribe((res: string) => {
this.localState.title = res;
});
this.translateService.get(key + '.' + decisionKey, {displayName: this.displayName}).subscribe((res: string) => {
this.localState.text = res;
});
}
public lookupKeyForType(heroType: HeroType) {
switch (heroType) {
case HeroType.SUCCESS:
return 'DECISION.APPROVED';
case HeroType.REVIEW:
return 'DECISION.REVIEW';
case HeroType.DENIED:
return 'DECISION.DENIED';
case HeroType.ERROR_DECISION:
return 'DECISION.ERROR_DECISION';
default:
case HeroType.ERROR_SUBMIT:
return 'DECISION.ERROR_SUBMIT';
}
}
}
@@ -0,0 +1,27 @@
<div class="muted-background">
<div class="container text-muted">
<div class="row flex-row">
<div class="row col-md-8">
<div class="p-2 col-md-5 col-sm-12 d-flex justify-content-lg-start justify-content-center">
<small translate><img src="assets/ncua.png" style="width: 40px;padding-right: .15rem;padding-bottom: .3rem;">FOOTER.NCUA
</small>
</div>
<div class=" p-2 col-md-4 col-sm-12 d-flex justify-content-lg-start justify-content-center">
<small translate><img src="assets/eqo.png" style="width: 20px;padding-right: .15rem;padding-bottom: .3rem;">FOOTER.HOUSING
</small>
</div>
<div class="p-2 col-md-2 col-sm-12 d-flex justify-content-lg-start justify-content-center">
<small translate><i focusable='false' class="fas fa-lock" style="padding-right: .15rem;"></i>FOOTER.SECURE</small>
</div>
</div>
<div class="p-2 col-md-4 col-sm-12 d-flex justify-content-lg-end justify-content-center" title="{{versionHint}}">
<small translate [translateParams]="{currentYear: currentYear}">FOOTER.COPY_RIGHT</small>
</div>
</div>
</div>
</div>
@@ -0,0 +1,29 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { configureTestSuite } from 'ng-bullet';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('Footer Component', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [ NgbModule.forRoot()],
declarations: [ FooterComponent ],
schemas: [ NO_ERRORS_SCHEMA ]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { BUILD_STAMP, VERSION } from '../../../environments/version';
@Component({
selector: 'lw-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent {
public readonly routingNumber = '275079714';
public versionHint = VERSION.version + '-' + BUILD_STAMP;
public currentYear = new Date().getFullYear();
constructor() { }
}
@@ -0,0 +1,7 @@
<span *ngIf="applicationIcon === 'credit-card'"><i focusable='false' class="fa fa-credit-card"></i></span>
<span *ngIf="applicationIcon === 'home'"><i focusable='false' class="fa fa-home"></i></span>
<span *ngIf="applicationIcon === 'car'"><i focusable='false' class="fa fa-car"></i></span>
<span *ngIf="applicationIcon === 'boat'"><i focusable='false' class="fa fa-ship"></i></span>
<span *ngIf="applicationIcon === 'pwc'"><i focusable='false' class="fa fa-motorcycle"></i></span>
<span *ngIf="applicationIcon === 'tmh'"><i focusable='false' class="fa fa-truck"></i></span>
<span *ngIf="applicationIcon === 'signature'"><i focusable='false' class="fa fa-pencil-alt"></i></span>
@@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HeaderIconComponent } from './header-icon.component';
import { configureTestSuite } from 'ng-bullet';
describe('Header Icon Component', () => {
let component: HeaderIconComponent;
let fixture: ComponentFixture<HeaderIconComponent>;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [ NgbModule.forRoot()],
declarations: [ HeaderIconComponent ]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(HeaderIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,12 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'lw-header-icon',
templateUrl: './header-icon.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderIconComponent {
@Input()
public applicationIcon: string;
constructor() { }
}
@@ -0,0 +1,34 @@
<div class="muted-background">
<div class="container">
<meta name="format-detection" content="telephone=no">
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
<a class="navbar-brand" >
<img src="assets/LM_Full-Logo_Main.png" class="logo-lmcu" alt="Landmark Credit Union" title="Landmark Credit Union">
</a>
<div class="row float-right header-information-big" style="margin-left:auto;width:50%;">
<div class="col-6">
<small class="float-right">
<lw-header-icon [applicationIcon]="applicantType?.icon" ></lw-header-icon>
{{applicantType?.headerMessageKey | translate}}
</small>
</div>
<div class="col-6">
<small class="float-right">
<i focusable='false' class="fas fa-phone"></i> {{ 'HEADER.ASSISTANCE' | translate}} <b>{{ 'HEADER.PHONE_NUM' | translate}}</b>
</small>
</div>
</div>
<div>
<a href="tel:{{'HEADER.PROPER_PHONE_NUMBER' | translate}}">
<button class="phone-button" type="button"><i focusable='false' class="fas fa-phone phone-button-icon"></i></button>
</a>
</div>
</nav>
</div>
</div>
<div class="d-flex justify-content-center">
<div class="header-information-small p-2 bd-highlight">
<lw-header-icon [applicationIcon]="applicantType?.icon" ></lw-header-icon>
{{applicantType?.headerMessageKey | translate}}
</div>
</div>
@@ -0,0 +1,53 @@
$primary: #005674;
$primaryDark: #053251;
$max-width: 770px;
.navbar-lmcu {
border-bottom: 3px solid #b3c9e4;
}
.logo-lmcu {
height: 70px;
}
.phone-button {
display: none;
}
.header-information-small {
display: none !important;
}
@media screen and (max-width: $max-width) {
.logo-lmcu {
height: 35px;
}
.phone-button {
position: absolute;
display: inline;
border: $primary;
right: -15px;
top:0;
background-color: $primary;
height: 60px;
width: 60px;
cursor:pointer;
color: white;
}
.phone-button-icon {
width: 1.5rem;
height: 1.5rem;
}
.phone-button:hover {
background-color: $primaryDark;
color:gray;
}
.header-information-big {
display: none;
}
.header-information-small {
display: inline !important;
}
}
@@ -0,0 +1,121 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { HeaderService } from './header.service';
import { RouterTestingModule } from '@angular/router/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { TextMaskModule } from 'angular2-text-mask';
import { CreditApplicationDaoService } from '../../shared/credit-application-dao.service';
import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service';
import { LoanTypeSet } from '../../shared/loan-type';
import { HeaderIconComponent } from './header-icon.component';
import { configureTestSuite } from 'ng-bullet';
describe('Header Component', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let applicationWizardService: ApplicationWizardService;
let creditApplicationDaoService: MockCreditApplicationDaoService;
let service: HeaderService;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
TextMaskModule,
TranslateModule.forRoot()],
declarations: [ HeaderComponent, HeaderIconComponent],
providers: [
{provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}
],
schemas: [ NO_ERRORS_SCHEMA ]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
service = TestBed.get(HeaderService);
applicationWizardService = TestBed.get(ApplicationWizardService);
creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should update icons/text based on loan type ( credit card ) ', () => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
// set to credit card
applicationWizardService.setLoanType(LoanTypeSet[0].id);
fixture.detectChanges();
const creditCard = compiled.querySelectorAll('.fa-credit-card');
const header = compiled.querySelectorAll('.header-information-big .col-6 small');
const home = compiled.querySelectorAll('.fa-home');
const vehicle = compiled.querySelectorAll('.fa-car');
expect(creditCard).toBeDefined();
expect(home).toBeDefined();
expect(vehicle).toBeDefined();
expect(header).toBeDefined();
expect(creditCard.length).toBeGreaterThanOrEqual(1);
expect(home.length).toEqual(0);
expect(vehicle.length).toEqual(0);
expect(header[0].innerHTML).toContain(LoanTypeSet[0].headerMessageKey);
});
it('should update icons/text based on loan type ( equity ) ', () => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
// set to equity
applicationWizardService.setLoanType(LoanTypeSet[4].id);
fixture.detectChanges();
const creditCard = compiled.querySelectorAll('.fa-credit-card');
const header = compiled.querySelectorAll('.header-information-big .col-6 small');
const home = compiled.querySelectorAll('.fa-home');
const vehicle = compiled.querySelectorAll('.fa-car');
expect(creditCard).toBeDefined();
expect(home).toBeDefined();
expect(vehicle).toBeDefined();
expect(header).toBeDefined();
expect(creditCard).toBeDefined();
expect(home).toBeDefined();
expect(vehicle).toBeDefined();
expect(creditCard.length).toEqual(0);
expect(home.length).toBeGreaterThanOrEqual(1);
expect(vehicle.length).toEqual(0);
expect(header[0].innerHTML).toContain(LoanTypeSet[4].headerMessageKey);
});
it('should update icons/text based on loan type ( vehicle ) ', () => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
// set to vehicle
applicationWizardService.setLoanType(LoanTypeSet[7].id);
fixture.detectChanges();
const creditCard = compiled.querySelectorAll('.fa-credit-card');
const header = compiled.querySelectorAll('.header-information-big .col-6 small');
const home = compiled.querySelectorAll('.fa-home');
const vehicle = compiled.querySelectorAll('.fa-car');
expect(creditCard).toBeDefined();
expect(home).toBeDefined();
expect(vehicle).toBeDefined();
expect(header).toBeDefined();
expect(creditCard.length).toEqual(0);
expect(home.length).toEqual(0);
expect(vehicle.length).toBeGreaterThanOrEqual(1);
expect(header[0].innerHTML).toContain(LoanTypeSet[7].headerMessageKey);
});
});
@@ -0,0 +1,29 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { HeaderService } from './header.service';
import { Subscription } from 'rxjs';
import { LoanType } from '../../shared/loan-type';
@Component({
selector: 'lw-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit, OnDestroy {
public applicantType: LoanType;
private subscriptionRef: Subscription;
constructor(private headerService: HeaderService) { }
ngOnInit() {
this.subscriptionRef = this.headerService.getApplicantTypeObservable().subscribe(value => {
this.applicantType = value;
});
}
ngOnDestroy() {
if (this.subscriptionRef) {
this.subscriptionRef.unsubscribe();
}
}
}
@@ -0,0 +1,61 @@
import { ReactiveFormsModule } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { HeaderService } from './header.service';
import { ApplicationWizardService } from '../../loan-wizard/application-wizard.service';
import { MockCreditApplicationDaoService } from '../../testing/shared-mocks/credit-application-dao.mock.service';
import { Router } from '@angular/router';
import { CreditApplicationDaoService } from '../../shared/credit-application-dao.service';
import { LoanTypeSet } from '../../shared/loan-type';
import { TranslateModule } from '@ngx-translate/core';
import { configureTestSuite } from 'ng-bullet';
describe('Header Service', () => {
let service: HeaderService;
let applicationWizardService: ApplicationWizardService;
let creditApplicationDaoService: MockCreditApplicationDaoService;
let router: Router;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [],
providers: [
{provide: CreditApplicationDaoService, useValue: MockCreditApplicationDaoService}
],
schemas: [
NO_ERRORS_SCHEMA
]
});
});
beforeEach(() => {
service = TestBed.get(HeaderService);
applicationWizardService = TestBed.get(ApplicationWizardService);
applicationWizardService.setLoanType(LoanTypeSet[0].id);
creditApplicationDaoService = TestBed.get(MockCreditApplicationDaoService);
router = TestBed.get(Router);
});
it('should create', () => {
expect(service).toBeTruthy();
});
it('should get applicant observable', () => {
spyOn(service, 'getApplicantTypeObservable').and.callThrough();
const observable$ = service.getApplicantTypeObservable();
expect(service.getApplicantTypeObservable).toHaveBeenCalled();
expect(observable$).toBeDefined();
});
});
@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { LoanType } from '../../shared/loan-type';
import { CreditApplicationService } from '../../shared/credit-application.service';
@Injectable({
providedIn: 'root'
})
export class HeaderService {
constructor(private creditApplicationService: CreditApplicationService) {
}
public getApplicantTypeObservable(): Observable<LoanType> {
return this.creditApplicationService.getLoanTypeObservable();
}
}
@@ -0,0 +1,106 @@
import { Waypoints, WaypointService } from './waypoint.service';
import { Waypoint } from './waypoint';
const stepIndex = 4; // start counting at 0
const amountOfSteps = 5; // number of steps in the stepper
describe('Waypoint Service', () => {
let service: WaypointService;
beforeEach(() => {
service = new WaypointService();
});
it('should create', () => {
expect(service).toBeTruthy();
});
it('should have an initial value', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
expect(testData.length).toEqual(amountOfSteps);
const waypoint = <Waypoint>testData[0];
expect(waypoint.id).toEqual(0);
expect(waypoint.isActive).toEqual(false);
});
it('should send out updated values on change', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
testData = null;
service.setStepActive(2);
expect( testData ).not.toBeNull();
});
it('should only set a single step as active.', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
expect( testData[0].isActive).toEqual(false);
expect( testData[1].isActive).toEqual(false);
service.setStepActive(0);
expect( testData[0].isActive).toEqual(true);
expect( testData[1].isActive).toEqual(false);
service.setStepActive(1);
expect( testData[0].isActive).toEqual(false);
expect( testData[1].isActive).toEqual(true);
});
it('should throw an error on invalid index.', () => {
expect( () => { service.setStepActive(-1); }).toThrowError();
expect( () => { service.setStepActive(stepIndex); }).not.toThrowError();
expect( () => { service.setStepActive(stepIndex + 1); }).toThrowError();
expect( () => { service.setStepActive(99); }).toThrowError();
});
it('should any steps as done.', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
expect( testData[Waypoints.APPLICANT].isComplete).toEqual(false);
expect( testData[Waypoints.REVIEW].isComplete).toEqual(false);
service.setStepActive(Waypoints.REVIEW);
expect( testData[Waypoints.APPLICANT].isComplete).toEqual(true);
expect( testData[Waypoints.REVIEW].isComplete).toEqual(false);
service.setStepActive(Waypoints.FINISH);
expect( testData[Waypoints.APPLICANT].isComplete).toEqual(true);
expect( testData[Waypoints.REVIEW].isComplete).toEqual(true);
});
it('should show collateral if it is required.', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
service.showCollateral(true);
expect( testData[Waypoints.COLLATERAL].shouldBeShown).toEqual(true);
service.showCollateral(false);
expect( testData[Waypoints.COLLATERAL].shouldBeShown).toEqual(false);
});
it('should set steps as complete up to the active step.', () => {
spyOn(service, 'setStepActive').and.callThrough();
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
service.setStepActive(Waypoints.REVIEW);
expect(service.setStepActive).toHaveBeenCalled();
expect(testData[Waypoints.REVIEW].isActive).toBe(true);
expect(testData[Waypoints.APPLICANT].isComplete).toBe(true);
expect(testData[Waypoints.FINISH].isComplete).toBe(false);
});
it('should set all steps as complete when finished.', () => {
spyOn(service, 'setAllComplete').and.callThrough();
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
expect(testData).not.toBeNull();
service.setAllComplete(true);
expect(service.setAllComplete).toHaveBeenCalled();
testData.forEach((waypoint) => {
expect(waypoint.isComplete).toBe(true);
});
});
});
@@ -0,0 +1,162 @@
import { Waypoint } from './waypoint';
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { environment } from 'src/environments/environment';
export enum Waypoints {
APPLICANT,
CO_APPLICANT,
COLLATERAL,
REVIEW,
FINISH
}
@Injectable({
providedIn: 'root'
})
export class WaypointService {
private readonly applicantWayPoint = <Waypoint>{
displayText: 'Applicant',
activeImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-blue-01.png`,
inactiveImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-gray-01.png`,
id: 0,
isActive: false,
isComplete: false,
shouldBeShown: true,
comment: null,
displayNumber: 1
};
private readonly coapplicantWayPoint = <Waypoint>{
displayText: 'Co-Applicant',
activeImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-blue-01.png`,
inactiveImagePath: `${environment.baseHref}/assets/waypoint/applicant-icon-gray-01.png`,
id: 1,
isActive: false,
isComplete: false,
shouldBeShown: false,
comment: null,
displayNumber: 2
};
private readonly collateralWayPoint = <Waypoint>{
displayText: 'Collateral',
// TODO ( next PR ) these images can change depending on collateral type, for now $'s are fine
activeImagePath: `${environment.baseHref}/assets/waypoint/collateral-icon-blue.png`,
inactiveImagePath: `${environment.baseHref}/assets/waypoint/collateral-icon-gray.png`,
id: 2,
isActive: false,
isComplete: false,
shouldBeShown: false,
comment: null,
displayNumber: 2
};
private readonly reviewWayPoint = <Waypoint>{
displayText: 'Review',
activeImagePath: `${environment.baseHref}/assets/waypoint/review-icon-blue.png`,
inactiveImagePath: `${environment.baseHref}/assets/waypoint/review-icon-gray.png`,
id: 3,
isActive: false,
isComplete: false,
shouldBeShown: true,
comment: null,
displayNumber: 2
};
private readonly finishWayPoint = <Waypoint>{
displayText: 'Finish',
activeImagePath: `${environment.baseHref}/assets/waypoint/5-finish-active-icon.png`,
inactiveImagePath: `${environment.baseHref}/assets/waypoint/finish-icon-gray.png`,
id: 4,
isActive: false,
isComplete: false,
shouldBeShown: true,
comment: null,
displayNumber: 3
};
private readonly defaultWayPointSet = [this.applicantWayPoint, this.coapplicantWayPoint,
this.collateralWayPoint, this.reviewWayPoint, this.finishWayPoint];
private waypointSet: BehaviorSubject<Array<Waypoint>> = new BehaviorSubject<Array<Waypoint>>(this.defaultWayPointSet);
public waypointSet$ = this.waypointSet.asObservable();
constructor() { }
private updateDisplayNumbers() {
let counter = 1;
const newWayPoints = this.waypointSet.value.splice(0);
newWayPoints.forEach((waypoint) => {
if ( waypoint.shouldBeShown ) {
waypoint.displayNumber = counter;
counter++;
}
}, this);
this.waypointSet.next(newWayPoints);
}
public handleActivePageRoute(route: string) {
if (route === 'step-1' || route === 'step-4') {
this.setStepActive(Waypoints.APPLICANT);
} else if (route.includes('co-applicant')) {
this.setStepActive(Waypoints.CO_APPLICANT);
} else if (route.includes('collateral')) {
this.setStepActive(Waypoints.COLLATERAL);
} else if (route === 'cc-disclosures' || route === 'cc-summary') {
this.setStepActive(Waypoints.REVIEW);
} else if (route === 'decision') {
this.setAllComplete(true);
}
}
public showCoApplicant(shouldShow: boolean) {
const newWayPoints = this.waypointSet.value.splice(0);
newWayPoints[Waypoints.CO_APPLICANT].shouldBeShown = shouldShow;
this.waypointSet.next(newWayPoints);
this.updateDisplayNumbers();
}
public showCollateral(shouldShow: boolean) {
const newWayPoints = this.waypointSet.value.splice(0);
newWayPoints[Waypoints.COLLATERAL].shouldBeShown = shouldShow;
this.waypointSet.next(newWayPoints);
this.updateDisplayNumbers();
}
public setStepActive(id: number): void {
if (id < 0 || id > Waypoints.FINISH) {
throw new Error('Waypoint ID must be between 0 and ' + Waypoints.FINISH);
}
const newWaypoints = this.waypointSet.value.splice(0);
newWaypoints.forEach((waypoint, index) => {
waypoint.isActive = (newWaypoints[index].id === id);
}, this);
this.waypointSet.next(newWaypoints);
this.setCompleteUpToActive();
}
private setCompleteUpToActive(): void {
let activeFlag = false;
const newWaypoints = this.waypointSet.value.splice(0);
newWaypoints.forEach((waypoint, index) => {
if (waypoint.isActive) {
activeFlag = true;
waypoint.isComplete = false;
} else {
waypoint.isComplete = !activeFlag;
}
}, this);
this.waypointSet.next(newWaypoints);
}
public setAllComplete(complete: boolean): void {
const newWaypoints = this.waypointSet.value.splice(0);
newWaypoints.forEach((waypoint) => {
waypoint.isActive = false;
waypoint.isComplete = complete;
}, this);
this.waypointSet.next(newWaypoints);
}
}
@@ -0,0 +1,11 @@
export interface Waypoint {
id: number;
displayText: string;
activeImagePath: string;
inactiveImagePath: string;
isActive: boolean;
isComplete: boolean;
shouldBeShown: boolean;
comment: string;
displayNumber: number;
}
@@ -0,0 +1,26 @@
<!-- https://codepen.io/thdeux/pen/zBGNrM -->
<!-- can do active, done, editable with the md-step class. -->
<div class="">
<div class="md-stepper-horizontal primary">
<ng-container *ngFor="let waypoint of waypointSet; let i = index">
<div *ngIf="waypoint.shouldBeShown" class="md-step" [ngClass]="{'active': waypoint.isActive, 'done': waypoint.isComplete}" style="z-index: 1;">
<!--mobile view-->
<div class="md-step-circle" *ngIf="!waypoint.isActive && !waypoint.isComplete">{{waypoint.displayNumber}}</div>
<div class="md-step-circle" *ngIf="waypoint.isComplete && ( i !== lastWaypoint)"><span> <i focusable='false' class="fa fa-check"></i></span></div>
<!--if it's the last way-point on mobile, we want to show the image with check-mark, not the circle-->
<div *ngIf="( i === lastWaypoint ) && waypoint.isComplete && !waypoint.isActive">
<img class="md-final-step-image rounded mx-auto d-block" src="assets/waypoint/completed-check-icon.png">
<div class="md-final-step-text-mobile md-step-title">{{waypoint.displayNumber}}. {{waypoint.displayText}}</div>
</div>
<!-- regular view -->
<img class="rounded mx-auto d-block" *ngIf="waypoint.isActive" src="{{waypoint.activeImagePath}}">
<img class="md-step-image rounded mx-auto d-block" *ngIf="!waypoint.isComplete && !waypoint.isActive" src="{{waypoint.inactiveImagePath}}">
<img class="md-step-image rounded mx-auto d-block" *ngIf="( i !== lastWaypoint ) && waypoint.isComplete && !waypoint.isActive" src="assets/waypoint/completed-check-icon.png">
<div class="md-step-title" [ngClass]="{'md-step-image': !waypoint.isActive, 'md-final-step-text': (i === lastWaypoint)}">{{waypoint.displayNumber}}. {{waypoint.displayText}}</div>
<!-- <div class="md-step-optional" *ngIf="waypoint.comment">{{waypoint.comment}}</div> Not Implemented Yet. -->
<hr class="md-step-bar-left" [ngClass]="{'done': waypoint.isActive || waypoint.isComplete}"/>
<hr class="md-step-bar-right" [ngClass]="{'done': waypoint.isComplete && !waypoint.isActive }"/>
</div>
</ng-container>
</div>
</div>
@@ -0,0 +1,177 @@
$primary: #0054A2;
$secondary: #FBFAF9;
$success: #7DA454;
$white: #FFFFFF;
$gray-background: #DFDFDF;
$dark-gray-background: #999999;
$max-width: 770px; //at what point does it switch to 'mobile' view
.md-stepper-horizontal {
display:table;
width:100%;
margin:0 auto;
background-color: $white;
}
.md-stepper-horizontal .md-step {
display:table-cell;
position:relative;
padding:24px;
}
.md-stepper-horizontal .md-step:active {
border-radius: 15% / 75%;
}
.md-stepper-horizontal .md-step:first-child:active {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.md-stepper-horizontal .md-step:last-child:active {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.md-stepper-horizontal .md-step:first-child .md-step-bar-left,
.md-stepper-horizontal .md-step:last-child .md-step-bar-right {
display:none;
}
.md-stepper-horizontal .md-step .md-step-circle {
display: none;
width:30px;
height:30px;
margin:0 auto;
bottom: 41px;
position: relative;
background-color: $dark-gray-background;
border-radius: 50%;
text-align: center;
line-height:30px;
font-size: 16px;
font-weight: 600;
color: $white;
}
.md-stepper-horizontal.green .md-step.active .md-step-circle {
background-color:#7ca550;
}
.md-stepper-horizontal .md-step.editable .md-step-circle {
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.md-stepper-horizontal .md-step.editable .md-step-circle:before {
font-family:'FontAwesome';
font-weight:100;
content: "\f040";
}
.md-stepper-horizontal .md-step .md-step-title {
margin-top:12px;
font-size:16px;
font-weight:600;
}
.md-stepper-horizontal .md-step .md-step-title,
.md-stepper-horizontal .md-step .md-step-optional {
text-align: center;
color:rgba(0,0,0,.26);
}
.md-stepper-horizontal .md-step.active .md-step-title {
font-weight: 600;
color:rgba(0,0,0,.87);
}
.md-stepper-horizontal .md-step.active.done .md-step-title,
.md-stepper-horizontal .md-step.active.editable .md-step-title {
font-weight:600;
}
.md-stepper-horizontal .md-step.done .md-step-circle {
background-color: #7ca550;
}
.md-stepper-horizontal .md-step .md-step-optional {
font-size:12px;
}
.md-stepper-horizontal .md-step.active .md-step-optional {
color:rgba(0,0,0,.54);
}
.md-stepper-horizontal .md-step .md-step-bar-left,
.md-stepper-horizontal .md-step .md-step-bar-right {
position:absolute;
bottom:95px;
height:1px;
border-top:3px solid #DDDDDD;
}
.md-stepper-horizontal .md-step .md-step-bar-right {
z-index: -1;
margin: 0 0 0 0;
color: $gray-background;
left: 50%;
height: 4px;
width: 90%;
top: 40%;
border: none;
background-color: $gray-background;
}
.md-stepper-horizontal .md-step .md-step-bar-left {
z-index: -1;
margin: 0 0 0 0;
color: $gray-background;
height: 4px;
width: 50%;
top: 40%;
border: none;
background-color: $gray-background;
}
.md-stepper-horizontal .md-step .md-step-bar-left.done,
.md-stepper-horizontal .md-step .md-step-bar-right.done {
background-color: $success !important;
}
.md-final-step-text-mobile {
display: none;
}
/*** Mobile view settings ***/
@media screen and (max-width: $max-width) {
.md-stepper-horizontal .md-step .md-step-bar-right {
top: 32%;
width: 60%;
}
.md-stepper-horizontal .md-step .md-step-bar-left {
top: 32%;
width: 40%;
}
.md-step-image {
display: none !important;
}
.md-stepper-horizontal .md-step{
padding: 2px;
}
.md-stepper-horizontal .md-step .md-step-circle {
display: block !important;
}
.md-stepper-horizontal .md-step.active img{
width: 50px !important;
}
.md-stepper-horizontal .md-step .md-step-title {
font-size:12px;
}
.md-final-step-image {
width: 50px !important;
}
.md-final-step-text {
display: none !important;
}
.md-final-step-text-mobile {
display: inherit !important;
}
// way-point fix for IE
@media screen and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.md-stepper-horizontal .md-step .md-step-circle {
display: -ms-flexbox !important;
bottom: 35px !important;
right: 0 !important;
position: relative;
margin-top:50px;
justify-content: center;
}
}
}
@@ -0,0 +1,72 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WaypointsComponent } from './waypoints.component';
import { Waypoints, WaypointService } from './waypoint.service';
import { configureTestSuite } from 'ng-bullet';
describe('Waypoints Component', () => {
let component: WaypointsComponent;
let fixture: ComponentFixture<WaypointsComponent>;
let service: WaypointService;
configureTestSuite(() => {
TestBed.configureTestingModule({
declarations: [WaypointsComponent]
});
});
beforeEach(() => {
service = TestBed.get(WaypointService);
fixture = TestBed.createComponent(WaypointsComponent);
fixture.detectChanges();
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
expect(service).toBeTruthy();
});
it('should render all initial provided steps ', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
const compiled = fixture.debugElement.nativeElement;
let steps = compiled.querySelectorAll('.md-step');
expect( steps).toBeDefined();
expect( steps.length).toEqual(3);
service.showCollateral(true);
fixture.detectChanges();
steps = compiled.querySelectorAll('.md-step');
expect( steps.length).toEqual(4);
service.showCoApplicant(true);
fixture.detectChanges();
steps = compiled.querySelectorAll('.md-step');
expect( steps.length).toEqual(testData.length);
});
it('should render the complete and done status of each step ', () => {
const compiled = fixture.debugElement.nativeElement;
let steps = compiled.querySelectorAll('.md-step.active');
expect( steps).toBeDefined();
expect( steps.length).toEqual(0);
service.setStepActive(Waypoints.REVIEW);
fixture.detectChanges();
steps = compiled.querySelectorAll('.active');
expect( steps).toBeDefined();
expect( steps.length).toEqual(1);
steps = compiled.querySelectorAll('.md-step.done');
expect( steps).toBeDefined();
expect( steps.length).toEqual(1);
});
it('should render correct number and title for each step ', () => {
let testData = null;
service.waypointSet$.subscribe((data) => testData = data);
const compiled = fixture.debugElement.nativeElement;
let steps = compiled.querySelectorAll('.md-step-title');
expect( steps).toBeDefined();
expect( steps[0].textContent).toEqual(testData[0].displayNumber + '. ' + testData[0].displayText );
steps = compiled.querySelectorAll('.md-step-circle span');
expect( steps).toBeDefined();
});
});
@@ -0,0 +1,30 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WaypointService } from './waypoint.service';
import { Subscription } from 'rxjs';
import { environment } from 'src/environments/environment';
@Component({
selector: 'lw-waypoints',
templateUrl: './waypoints.component.html',
styleUrls: ['./waypoints.component.scss']
})
export class WaypointsComponent implements OnInit, OnDestroy {
public waypointSet = [];
public lastWaypoint = 0;
private subscription: Subscription;
public completedCheck = environment.baseHref + '/assets/waypoint/completed-check-icon.png';
constructor(private waypointService: WaypointService) { }
public ngOnInit(): void {
this.subscription = this.waypointService.waypointSet$.subscribe((waypointSet) => {
this.waypointSet = waypointSet;
this.lastWaypoint = waypointSet.length - 1;
});
}
public ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
@@ -0,0 +1,35 @@
import { ValidatorFn } from '@angular/forms';
import { ApplicationWizardService } from './application-wizard.service';
import { CreditApplicationService } from '../shared/credit-application.service';
export interface ApplicationField {
field: string;
validation: Array<ValidatorFn>;
}
export interface ApplicationFieldGroup {
group: string;
fields: Array<ApplicationField>;
}
export class ApplicationPage {
public route: string;
public fieldgroup: Array<ApplicationFieldGroup>;
public titleKey;
public constructor(titleKey: string, route: string, formGroup: Array<ApplicationFieldGroup>) {
this.titleKey = titleKey;
this.route = route;
this.fieldgroup = formGroup;
}
public determinePageRequired(applicationWizardService: ApplicationWizardService): boolean {
return true;
}
public isValidPage(applicationWizardService: ApplicationWizardService, creditApplicationService: CreditApplicationService): Array<any> {
return ['ERROR'];
}
}
@@ -0,0 +1,236 @@
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CreditApplicationDaoService } from '../shared/credit-application-dao.service';
import { ApplicationWizardService } from './application-wizard.service';
import { MaskingService } from '../shared/masking/masking.service';
import { MemberInfoFormComponent } from './field-groups/member/member-info-form.component';
import { TextMaskModule } from 'angular2-text-mask';
import { MemberInfoPageService } from './wizards/common/member/member-info-page.service';
import { CommonGroups } from './field-groups/common-groups';
import { LoanTypeSet } from '../shared/loan-type';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CreditApplicationValues } from '../shared/credit-application-values';
import { TranslateModule } from '@ngx-translate/core';
import { BorrowerType } from '../shared/borrower-type';
import { apiTranslatorPreTranslate } from '../testing/shared-mocks/api-translator-pre-post';
import { EquityMemberInfoPageService } from './wizards/equity/equity-member-info-page.service';
import { configureTestSuite } from 'ng-bullet';
import { CreditApplicationService } from '../shared/credit-application.service';
const createTestFormGroup = (formBuilder: FormBuilder): FormGroup => {
const group = formBuilder.group({});
CommonGroups.MEMBER_INFO.fields.forEach((eachField) => {
group.addControl(eachField.field, formBuilder.control('', eachField.validation.slice()));
}, this);
return group;
};
describe('ApplicationWizardService', () => {
let applicationWizardService: ApplicationWizardService;
let creditApplicationDaoService: CreditApplicationDaoService;
let creditApplicationService: CreditApplicationService;
let component: MemberInfoFormComponent;
let fixture: ComponentFixture<MemberInfoFormComponent>;
let memberInfoPageService: MemberInfoPageService;
let equityMemberInfoPageService: EquityMemberInfoPageService;
let formBuilder: FormBuilder;
configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
ReactiveFormsModule,
TextMaskModule,
HttpClientTestingModule,
TranslateModule.forRoot()
],
declarations: [
MemberInfoFormComponent
],
schemas: [NO_ERRORS_SCHEMA],
providers: []
});
});
beforeEach(() => {
formBuilder = TestBed.get(FormBuilder);
applicationWizardService = TestBed.get(ApplicationWizardService);
applicationWizardService.setLoanType(LoanTypeSet[0].id);
creditApplicationDaoService = TestBed.get(CreditApplicationDaoService);
creditApplicationService = TestBed.get(CreditApplicationService);
memberInfoPageService = TestBed.get(MemberInfoPageService);
equityMemberInfoPageService = TestBed.get(EquityMemberInfoPageService);
fixture = TestBed.createComponent(MemberInfoFormComponent);
component = fixture.componentInstance;
component.group = createTestFormGroup(formBuilder);
});
it('should create', () => {
expect(applicationWizardService).toBeTruthy();
});
it('should determine whether or not a page can be visited', () => {
const canVisitStep1: boolean = applicationWizardService.canVisitPage('step-1');
const canVisitStep2: boolean = applicationWizardService.canVisitPage('step-2');
expect(canVisitStep1).toBe(true);
expect(canVisitStep2).toBe(false);
});
it('should determine a page can be visited because there were no errors', () => {
spyOn(applicationWizardService, 'getFieldErrors').and.callThrough().and.returnValue([]);
const noErrors: boolean = applicationWizardService.canVisitPage('step-2');
expect(noErrors).toBe(true);
});
it('should determine if there is a co-applicant', () => {
spyOn(MaskingService, 'unMaskNumber').and.callThrough().and.returnValue(1000);
spyOn(applicationWizardService, 'getFieldValue').and.callThrough();
spyOn(memberInfoPageService, 'createMemberInformationGroup').and.callThrough();
spyOn(creditApplicationService, 'saveCreditApplication').and.returnValue(of(true));
component.group.patchValue({
'applicantType': CreditApplicationValues.CHOICE_YES,
'limitRequested': '1000',
'maritalStatus': CreditApplicationValues.CHOICE_NO
});
creditApplicationService.saveMemberInformation(component.group.value);
const isCoApplicant: boolean = applicationWizardService.isCoApplicant();
expect(isCoApplicant).toBe(true);
component.group.patchValue({
'applicantType': CreditApplicationValues.CHOICE_NO,
'limitRequested': '1000',
'maritalStatus': CreditApplicationValues.CHOICE_NO
});
creditApplicationService.saveMemberInformation(component.group.value);
const changedCoApplicant: boolean = applicationWizardService.isCoApplicant();
expect(changedCoApplicant).toBe(false);
});
it('should submit an application ', () => {
const application = applicationWizardService.getFullCreditApplication();
spyOn(creditApplicationDaoService, 'submitApplication').and.returnValue(of(true));
applicationWizardService.submitCreditApplication(application);
expect(creditApplicationDaoService.submitApplication).toHaveBeenCalled();
});
it('should submit an application for decision', () => {
const application = applicationWizardService.getFullCreditApplication();
spyOn(creditApplicationDaoService, 'decideApplication').and.returnValue(of(true));
applicationWizardService.submitApplicationForDecision(application);
expect(creditApplicationDaoService.decideApplication).toHaveBeenCalled();
});
it('should update an applications decision status', () => {
spyOn(creditApplicationDaoService, 'getApplicationDecision').and.returnValue(of(true));
applicationWizardService.updateApplicationDecisionStatus();
expect(creditApplicationDaoService.getApplicationDecision).toHaveBeenCalled();
});
it('should return data for primary applicant', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
const primaryData1 = applicationWizardService.getIndividualGroupValues('employmentInformation', BorrowerType.PRIMARY_BORROWER);
expect(primaryData1).toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[0].employmentInformation);
expect(Array.isArray(primaryData1)).toBeTruthy();
});
it('should return data for secondary applicant', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
const primaryData1 = applicationWizardService.getIndividualGroupValues('otherIncomeInformation', BorrowerType.CO_BORROWER);
expect(primaryData1).toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[1].otherIncomeInformation);
});
it('should return the applicant\'s first name', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
expect(applicationWizardService.getApplicantName(BorrowerType.PRIMARY_BORROWER))
.toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[0].personalInformation.firstName);
expect(applicationWizardService.getApplicantName(BorrowerType.CO_BORROWER))
.toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[1].personalInformation.firstName);
});
it('should return a field value w/ or w/o a group', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
expect(applicationWizardService.getFieldValue('loanType')).toEqual(apiTranslatorPreTranslate.loanType);
expect(applicationWizardService.getFieldValue('estimatedValue', 'collateralInformation'))
.toBe(apiTranslatorPreTranslate.collateralInformation.estimatedValue);
});
it('should determine that a loan needs collateral information', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
// apiTranslatorPreTranslate is using Loan type 8, which does not require collateral.
// 8 used to be MANUFACTURED_HOME_LOAN which is commented out as of 2023.
expect(applicationWizardService.needsCollateral()).toBe(false);
});
it('should determine that a loan doesn\'t need collateral information', () => {
const creditCardApplication = apiTranslatorPreTranslate;
creditCardApplication.loanType = LoanTypeSet[0];
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(creditCardApplication);
expect(applicationWizardService.needsCollateral()).toBe(false);
});
it('should return true if the co-borrower is married to the primary borrower', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
expect(applicationWizardService.isCoApplicantSpouse()).toBe(true);
});
it('should return false if the co-borrower is not married to the primary borrower', () => {
const applicantsAreNotMarriedToEachOther = apiTranslatorPreTranslate;
applicantsAreNotMarriedToEachOther.hasCoApplicantSpouse = null;
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(applicantsAreNotMarriedToEachOther);
expect(applicationWizardService.isCoApplicantSpouse()).toBe(false);
});
// tslint:disable-next-line:max-line-length
it('should return false if the applicant/co-applicant does not need to provide spouse information (if primary and co-borrower are not married to each other)', () => {
apiTranslatorPreTranslate.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_YES;
apiTranslatorPreTranslate.fullInfoBorrowers[0].personalInformation.maritalStatus = CreditApplicationValues.CHOICE_YES;
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
spyOn(applicationWizardService, 'isCoApplicantSpouse').and.callThrough();
expect(applicationWizardService.isApplicantNonSpouse(BorrowerType.CO_BORROWER)).toBe(false);
});
it('should return true if the applicant/co-applicant needs to provide spouse information', () => {
apiTranslatorPreTranslate.hasCoApplicantSpouse = CreditApplicationValues.CHOICE_NO;
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
spyOn(applicationWizardService, 'isCoApplicantSpouse').and.callThrough();
expect(applicationWizardService.isApplicantNonSpouse(BorrowerType.PRIMARY_BORROWER)).toBe(true);
});
it('should return false if this is not a Manufactured Home', () => {
const mfgHomeNotOnRentedLotApplication = apiTranslatorPreTranslate;
mfgHomeNotOnRentedLotApplication.loanType = LoanTypeSet[4];
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(mfgHomeNotOnRentedLotApplication);
expect(applicationWizardService.isRentedLotInfoRequired()).toBe(false);
expect(applicationWizardService.isLienPositionInfoRequired()).toBe(true);
});
it('should return group values', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
expect(applicationWizardService.getGroupValues('collateralInformation')).toEqual(apiTranslatorPreTranslate.collateralInformation);
expect(applicationWizardService.getGroupValues('someNonExistantGroup')).toEqual({});
});
it('should return an individual from the application', () => {
spyOn(applicationWizardService, 'getFullCreditApplication').and.returnValue(apiTranslatorPreTranslate);
expect(applicationWizardService.getIndividualByBorrowerType(BorrowerType.CO_BORROWER))
.toEqual(apiTranslatorPreTranslate.fullInfoBorrowers[1]);
});
it('should correctly calculate a simple next route name', () => {
const service = applicationWizardService;
expect(service.getSimpleRouteName('https://applydev.landmarkcu.com/loan-wizard/cc/re/step-2')).toEqual('step-2');
expect(service.getSimpleRouteName('https://a.x.com/lw/cc/re/step-1?fbclid=IwAR226oQp-ISjjS9U1I440bjA')).toEqual('step-1');
});
});
@@ -0,0 +1,353 @@
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
import { CreditApplication, Individual, SpouseType } from '../shared/credit-application';
import { CreditApplicationDaoService } from '../shared/credit-application-dao.service';
import { ApplicationPage } from './application-page';
import { Injectable } from '@angular/core';
import { CreditApplicationService } from '../shared/credit-application.service';
import { CreditApplicationValues } from '../shared/credit-application-values';
import { BorrowerType } from 'src/app/shared/borrower-type';
import { WaypointService } from '../layout/waypoints/waypoint.service';
import { TranslateService } from '@ngx-translate/core';
import { LoanTitle } from '../shared/loan-type/loan-title';
import { LoanCategory } from '../shared/loan-type/loan-category';
import { LoanCode } from '../shared/loan-type/loan-code';
@Injectable({
providedIn: 'root'
})
export class ApplicationWizardService {
protected cachedFormControl = new FormControl('');
// when using inheritance, the implied private field is not clear, so I've declared it implicitly.
protected creditApplicationDaoService: CreditApplicationDaoService;
constructor(creditApplicationDaoService: CreditApplicationDaoService,
private creditApplicationService: CreditApplicationService,
private translateService: TranslateService,
private wayPointService: WaypointService) {
this.creditApplicationDaoService = creditApplicationDaoService;
}
public setLoanType(loanType: string, parsedData?: any): void {
this.creditApplicationService.setLoanType(loanType, parsedData);
}
public resetLoanType(): void {
this.creditApplicationService.resetLoanType();
}
public getApplicantName(borrowerType: BorrowerType): string {
const setIndividual = this.getIndividualByBorrowerType(borrowerType);
if (!setIndividual || !setIndividual.personalInformation.firstName) {
return 'Applicant';
}
return setIndividual.personalInformation.firstName;
}
public getBeforeYouBeginMessage(): object {
const message = this.getFullCreditApplication().loanType.beforeYouBeginKey;
if(message) {
return this.translateService.get(message);
} else {
return null;
}
}
public showProofOfIncomeUploadForm(): boolean {
const application = this.getFullCreditApplication();
if(application.loanType.apiCode === LoanCode.EQ_HELOC_Flex) {
return application.limitRequested > 100000 && application.loanType.showProofOfIncomeUploadForm === true
}
return application.loanType.showProofOfIncomeUploadForm === true;
}
public canVisitPage(pageRoute: string): boolean {
// You can't visit anything once you have a decision.
const creditApplication: CreditApplication = this.getFullCreditApplication();
if (creditApplication && creditApplication.hasBeenCompleted) {
return pageRoute === 'decision';
}
const pages = creditApplication.loanType.pages;
let currentPage = null;
for (let index = 0; index < pages.length; index++) {
currentPage = pages[index];
if (currentPage.route === pageRoute) {
return true;
} else if (currentPage.determinePageRequired(this) && currentPage.isValidPage(this, this.creditApplicationService).length > 0) {
// redirect the user.
return false;
}
}
}
public getFieldErrors(fieldName, value, validatorFnSet) {
let validationResult;
const errors = [];
validatorFnSet.forEach((validatorFn) => {
this.cachedFormControl.setValue(value);
validationResult = validatorFn(this.cachedFormControl);
if (validationResult) {
validationResult.field = fieldName;
errors.push(validationResult);
}
});
return errors;
}
public getFieldValue(field: string, group?: string) {
const creditApplication: CreditApplication = this.getFullCreditApplication();
let value;
if (group) {
value = creditApplication[group];
if (value) {
value = value[field];
}
} else {
value = creditApplication[field];
}
return value;
}
public isCoApplicant(): boolean {
return this.getFieldValue('hasCoApplicant') === CreditApplicationValues.CHOICE_YES;
}
public needsCollateral(): boolean {
return this.getFullCreditApplication().loanType.requiresCollateral;
}
public isCoApplicantSpouse(): boolean {
return this.getFullCreditApplication().hasCoApplicantSpouse === CreditApplicationValues.CHOICE_YES;
}
public isApplicantNonSpouse(individual: BorrowerType): boolean {
const setIndividual = this.getIndividualByBorrowerType(individual);
if (!setIndividual || this.isCoApplicantSpouse()) {
return false;
}
return setIndividual.addressInformation.state === CreditApplicationValues.STATE_REQUIRED_COAPP &&
setIndividual.personalInformation.maritalStatus === CreditApplicationValues.CHOICE_YES;
}
public isRentedLotInfoRequired(): boolean {
return this.getFullCreditApplication().loanType.title === LoanTitle.ManufacturedHome;
}
public isLienPositionInfoRequired(): boolean {
if (this.getFullCreditApplication().loanType.category === LoanCategory.Equity) {
return this.getFullCreditApplication().loanType.title !== LoanTitle.ManufacturedHome;
} else {
return false;
}
}
public showPayoffFirstMortgageStatementUploadForm(): boolean {
if (this.getFullCreditApplication().loanType.category === LoanCategory.Equity) {
return this.getFullCreditApplication().loanType.title === LoanTitle.HELOC || this.getFullCreditApplication().loanType.title === LoanTitle.HomeEquityFixed;
} else {
return false;
}
}
public getSpouseType(borrowerType: BorrowerType): SpouseType {
const individual = this.getIndividualByBorrowerType(borrowerType);
if (borrowerType === BorrowerType.PRIMARY_BORROWER || (borrowerType === BorrowerType.CO_BORROWER && individual)) {
return individual.spouseType;
}
return null;
}
public getGroupValues(group: string) {
if (!this.getFullCreditApplication()[group]) {
return {};
}
return this.getFullCreditApplication()[group];
}
public getIndividualGroupValues(group: string, individual: BorrowerType) {
const selectedIndividual = this.getIndividualByBorrowerType(individual);
if (selectedIndividual && selectedIndividual[group]) {
return selectedIndividual[group];
}
return {};
}
public getIndividualSpouse(individual: Individual): any {
if (individual.spouseType === SpouseType.FullInfoSpouse) {
return individual.fullInfoSpouse;
} else {
return individual.tattleSpouse;
}
}
public getIndividualByBorrowerType(individual: BorrowerType) {
const creditApplication = this.getFullCreditApplication(); // DO NOT REMOVE THIS, tests will fail.
switch (individual) {
case BorrowerType.PRIMARY_BORROWER:
return creditApplication.fullInfoBorrowers[0];
case BorrowerType.CO_BORROWER:
this.addDefaultCoApplicantIfNecessary();
return creditApplication.fullInfoBorrowers[1];
case BorrowerType.PRIMARY_BORROWER_SPOUSE:
return this.getIndividualSpouse(creditApplication.fullInfoBorrowers[0]);
case BorrowerType.CO_BORROWER_SPOUSE:
return this.getIndividualSpouse(creditApplication.fullInfoBorrowers[1]);
}
throw new Error('Invalid individual type in getIndividualByBorrowerType: ' + individual);
}
public addDefaultCoApplicantIfNecessary() {
const creditApplication = this.getFullCreditApplication();
if (creditApplication.hasCoApplicant === CreditApplicationValues.CHOICE_YES
&& creditApplication.fullInfoBorrowers.length === 1) {
creditApplication.fullInfoBorrowers.push(this.creditApplicationService.generateDefaultFullInfoBorrower());
if (creditApplication.coApplicantLivesInMaritalPropertyState) {
creditApplication.fullInfoBorrowers[1].addressInformation.state = CreditApplicationValues.STATE_REQUIRED_COAPP;
}
if (this.isCoApplicantSpouse()) {
creditApplication.fullInfoBorrowers[1].personalInformation.maritalStatus = CreditApplicationValues.CHOICE_YES;
}
}
}
public getFullCreditApplication(): CreditApplication {
return this.creditApplicationService.getFullCreditApplication();
}
public submitCreditApplication(creditApplication: CreditApplication): Observable<boolean> {
return this.creditApplicationDaoService.submitApplication(creditApplication)
.pipe(
map(result => {
if (result) {
const savedApplication = this.getFullCreditApplication();
savedApplication.loanId = result['loanApplicationId'];
return true;
}
return false;
}),
catchError((err, caught) => {
return of(false);
})
);
}
public submitApplicationForDecision(creditApplication: CreditApplication): Observable<boolean> {
return this.creditApplicationDaoService.decideApplication(creditApplication)
.pipe(
map(result => {
return true;
}),
catchError((err, caught) => {
return of(false);
})
);
}
public updateApplicationDecisionStatus(): Observable<boolean> {
const creditApplication = this.getFullCreditApplication();
return this.creditApplicationDaoService.getApplicationDecision(creditApplication)
.pipe(
map(result => {
if (result && result.state === 'Ready') {
creditApplication.decision = result;
return true;
}
return false;
}),
catchError((err, caught) => {
return of(false);
})
);
}
public getPageIndexByRouteName(routeName: string): number {
const creditApplication = this.getFullCreditApplication();
const pages = creditApplication.loanType.pages;
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
if (pages[pageIndex].route === routeName) {
return pageIndex;
}
}
throw new Error('No page found for route name: ' + routeName);
}
public getNextPageByIndex(currentIndex: number): ApplicationPage {
const creditApplication = this.getFullCreditApplication();
const pages = creditApplication.loanType.pages;
for (let pageIndex = currentIndex + 1; pageIndex < pages.length; pageIndex++) {
if (pages[pageIndex].determinePageRequired(this)) {
return pages[pageIndex];
}
}
return null;
}
public getPreviousPageByIndex(currentIndex: number): ApplicationPage {
const creditApplication = this.getFullCreditApplication();
const pages = creditApplication.loanType.pages;
for (let pageIndex = currentIndex - 1; pageIndex >= 0; pageIndex--) {
if (pages[pageIndex].determinePageRequired(this)) {
return pages[pageIndex];
}
}
return null;
}
public handlePostApplicationEvents(): void {
this.creditApplicationService.clearApplication();
}
public getImagePath(): string {
return this.getFullCreditApplication().loanType.imagePath;
}
public getLoanMessage(): Observable<string> {
const message = this.getFullCreditApplication().loanType.message;
return this.translateService.get(message);
}
public getNextTopicMessage(routeName: string): string {
const currentPageIndex = this.getPageIndexByRouteName(routeName);
const nextPage = this.getNextPageByIndex(currentPageIndex);
if (nextPage) {
return nextPage.titleKey;
}
return 'NONEXTROUTE';
}
public getNextRoute(routeName: string): string {
const currentPageIndex = this.getPageIndexByRouteName(routeName);
const nextPage = this.getNextPageByIndex(currentPageIndex);
if (nextPage) {
return '../' + nextPage.route;
}
return null;
}
public getPreviousRoute(routeName: string): string {
const currentPageIndex = this.getPageIndexByRouteName(routeName);
const prevPage = this.getPreviousPageByIndex(currentPageIndex);
if (prevPage) {
return '../' + prevPage.route;
}
return null;
}
public displayWaypointsForPage(currentPage: string) {
this.wayPointService.showCoApplicant(this.isCoApplicant());
this.wayPointService.showCollateral(this.needsCollateral());
this.wayPointService.handleActivePageRoute(currentPage);
}
public getSimpleRouteName(url: string): string {
const simpleurl = url.split('/').pop();
if (simpleurl) {
return simpleurl.match(/[a-zA-Z0-9\-_]+/)[0];
}
}
}
@@ -0,0 +1,12 @@
<div *ngIf="inputControl.invalid && (inputControl.dirty || inputControl.touched)">
<div *ngIf="msgId">
<div translate [translateParams]="inputControl.errors">{{ ('ERRORS.CUSTOM.' + msgId) }}</div>
</div>
<div *ngIf="!msgId">
<div *ngFor="let eachKey of messageKeySet" translate [translateParams]="eachKey">
{{eachKey.key}}
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More