Adding
This commit is contained in:
@@ -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 {}
|
||||
+81
@@ -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>
|
||||
+38
@@ -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;
|
||||
}
|
||||
+66
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
+126
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+175
@@ -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
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
+160
@@ -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>;
|
||||
}
|
||||
+41
@@ -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>
|
||||
+38
@@ -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;
|
||||
}
|
||||
+66
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
+65
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+106
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+10
@@ -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;
|
||||
}
|
||||
}
|
||||
+30
@@ -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>
|
||||
+1
@@ -0,0 +1 @@
|
||||
// see card-component.scss for styling
|
||||
+45
@@ -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);
|
||||
});
|
||||
});
|
||||
+20
@@ -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;
|
||||
}
|
||||
}
|
||||
+37
@@ -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>
|
||||
+39
@@ -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();
|
||||
}
|
||||
}
|
||||
+103
@@ -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;
|
||||
}
|
||||
+58
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -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>;
|
||||
}
|
||||
+2
@@ -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"/>
|
||||
+11
@@ -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;
|
||||
}
|
||||
+35
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
+15
@@ -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']);
|
||||
}
|
||||
}
|
||||
+20
@@ -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>
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
$max-width: 770px;
|
||||
|
||||
|
||||
@media screen and (max-width: $max-width) {
|
||||
.url-long {
|
||||
display: none;
|
||||
}
|
||||
.url-short[style] {
|
||||
display: inline !important;
|
||||
}
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<span >
|
||||
<div>HTTP Status: {{error.statusCode}}</div>
|
||||
<div>Message: {{error.message | translate}}</div>
|
||||
</span>
|
||||
+21
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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>
|
||||
+16
@@ -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;
|
||||
}
|
||||
+71
@@ -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});
|
||||
}
|
||||
}
|
||||
+53
@@ -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 {}
|
||||
+9
@@ -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>
|
||||
+67
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+15
@@ -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() { }
|
||||
}
|
||||
+5
@@ -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>
|
||||
|
||||
+73
@@ -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();
|
||||
});
|
||||
});
|
||||
+62
@@ -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>
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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'];
|
||||
}
|
||||
|
||||
}
|
||||
+236
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -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
Reference in New Issue
Block a user