Explorar el Código

email sending 2.0.50 ready

master
Patrick Sun hace 4 años
padre
commit
5d4909b45b
Se han modificado 32 ficheros con 726 adiciones y 123 borrados
  1. +2
    -2
      package-lock.json
  2. +1
    -1
      package.json
  3. +6
    -0
      src/app/app.module.ts
  4. +2
    -0
      src/app/broker-dashboard/broker-dashboard.component.html
  5. +5
    -4
      src/app/broker-loan-list/broker-loan-list.component.html
  6. +1
    -1
      src/app/broker-loan-list/broker-loan-list.component.ts
  7. +3
    -3
      src/app/list-all-loans/list-all-loans.component.html
  8. +1
    -1
      src/app/list-all-loans/list-all-loans.component.ts
  9. +4
    -24
      src/app/loan-detail/loan-detail-basic-info/loan-detail-basic-info.component.ts
  10. +66
    -1
      src/app/loan-detail/loan-detail-people/loan-detail-people.component.html
  11. +43
    -0
      src/app/loan-detail/loan-detail-people/loan-detail-people.component.scss
  12. +113
    -2
      src/app/loan-detail/loan-detail-people/loan-detail-people.component.ts
  13. +4
    -4
      src/app/loan-detail/loan-detail.component.html
  14. +1
    -1
      src/app/loan-row-list/loan-row-list.component.html
  15. +2
    -2
      src/app/loan-row-list/loan-row-list.component.ts
  16. +11
    -7
      src/app/loan-single-row/loan-single-row.component.html
  17. +13
    -0
      src/app/loan-single-row/loan-single-row.component.scss
  18. +16
    -0
      src/app/loan-single-row/loan-single-row.component.ts
  19. +98
    -40
      src/app/loan-steps/loan-steps.component.html
  20. +61
    -0
      src/app/loan-steps/loan-steps.component.scss
  21. +113
    -27
      src/app/loan-steps/loan-steps.component.ts
  22. +1
    -2
      src/app/main-menu-items.ts
  23. +8
    -0
      src/app/models/loan.model.ts
  24. +38
    -0
      src/app/models/step.model.ts
  25. +8
    -0
      src/app/people-card/people-card.component.html
  26. +7
    -0
      src/app/people-card/people-card.component.scss
  27. +17
    -1
      src/app/people-card/people-card.component.ts
  28. +10
    -0
      src/app/pipe/file.size.pipe.ts
  29. +30
    -0
      src/app/service/loan.step.service.ts
  30. +6
    -0
      src/app/service/loan_summary.service.ts
  31. +4
    -0
      src/app/service/people.service.ts
  32. +31
    -0
      src/app/service/toast.service.ts

+ 2
- 2
package-lock.json Ver fichero

@@ -1,12 +1,12 @@
{
"name": "broker",
"version": "2.0.39",
"version": "2.0.50",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "broker",
"version": "2.0.39",
"version": "2.0.50",
"dependencies": {
"@angular/animations": "^11.2.9",
"@angular/common": "^11.2.9",

+ 1
- 1
package.json Ver fichero

@@ -1,6 +1,6 @@
{
"name": "broker",
"version": "2.0.39",
"version": "2.0.50",
"scripts": {
"ng": "ng",
"versionIncrease": "npm --no-git-tag-version version patch",

+ 6
- 0
src/app/app.module.ts Ver fichero

@@ -126,6 +126,9 @@ import { LoanDetailPeopleComponent } from './loan-detail/loan-detail-people/loan
import { LoanDetailRewardComponent } from './loan-detail/loan-detail-reward/loan-detail-reward.component';
import { LoanDetailProgressComponent } from './loan-detail/loan-detail-progress/loan-detail-progress.component';
import { LoanDetailTrailComponent } from './loan-detail/loan-detail-trail/loan-detail-trail.component';
import { FileSizePipe } from './pipe/file.size.pipe';
import {LoanStepService} from './service/loan.step.service';
import {ToastService} from './service/toast.service';



@@ -220,6 +223,7 @@ export function initializeApp(appConfig: AppConfig): () => Promise<void> {
LoanDetailRewardComponent,
LoanDetailProgressComponent,
LoanDetailTrailComponent,
FileSizePipe,
],
imports: [
BrowserModule,
@@ -266,7 +270,9 @@ export function initializeApp(appConfig: AppConfig): () => Promise<void> {
SessionService,
WebSocketService,
LoanSummaryService,
ToastService,
LoanSingleService,
LoanStepService,
LenderNameService,
{
provide: HTTP_INTERCEPTORS,

+ 2
- 0
src/app/broker-dashboard/broker-dashboard.component.html Ver fichero

@@ -1,3 +1,5 @@
<div style="height: calc(100vh - 48px);">
<app-chart-type-of-loans></app-chart-type-of-loans>
<app-chart-amount-of-loans></app-chart-amount-of-loans>
<app-broker-loan-list></app-broker-loan-list>
</div>

+ 5
- 4
src/app/broker-loan-list/broker-loan-list.component.html Ver fichero

@@ -1,8 +1,6 @@
<kendo-grid
[data]="brokerLoans | async"
[height]="610"
[loading]="brokerLoans.loading"
class="fullheight_grid"
>
<kendo-grid-column field="Id"></kendo-grid-column>

@@ -10,7 +8,7 @@
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngFor="let p of dataItem.Client, let idx=index ">
<div class="customer-photo" [ngStyle]="{'background-image' : photoURL(dataItem.ClientIds[idx])}"></div>
<div class="customer-name"> {{ p }}</div>
<div class="customer-name"> {{ p.FullName }}</div>
</div>
</ng-template>
</kendo-grid-column>
@@ -43,9 +41,12 @@
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngFor="let p of dataItem.Broker, let idx=index ">
<div class="customer-photo" [ngStyle]="{'background-image' : photoURL(dataItem.BrokerIds[idx])}"></div>
<div class="customer-name"> {{ p }}</div>
<div class="customer-name"> {{ p.FullName }}</div>
</div>
</ng-template>
</kendo-grid-column>

<ng-template kendoGridDetailTemplate let-dataItem>
<app-loan-steps [Loan]="dataItem"></app-loan-steps>
</ng-template>
</kendo-grid>

+ 1
- 1
src/app/broker-loan-list/broker-loan-list.component.ts Ver fichero

@@ -29,7 +29,7 @@ export class BrokerLoanListComponent implements OnInit {
filter.filters.push({field:'client_ids', operator: 'contains', value: this.broker.Id, ignoreCase: true})
filter.filters.push({field:'broker_ids', operator: 'contains', value: this.broker.Id, ignoreCase: true})
filter.filters.push({field:'other_rewarder_ids', operator: 'contains', value: this.broker.Id, ignoreCase: true})
this.lss.query({ skip: 0, take: 1000, sort, filter});
this.lss.queryAsLoanModel({ skip: 0, take: 1000, sort, filter});
}

private photoURL(peopleId: any): string {

+ 3
- 3
src/app/list-all-loans/list-all-loans.component.html Ver fichero

@@ -54,7 +54,7 @@
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngFor="let p of dataItem.Client, let idx=index ">
<div *ngIf="dataItem.ClientIds.length >0" class="customer-photo" [ngStyle]="{'background-image' : photoURL(dataItem.ClientIds[idx])}"></div>
<div *ngIf="dataItem.ClientIds.length >0" class="customer-name"> {{ p }}</div>
<div *ngIf="dataItem.ClientIds.length >0" class="customer-name"> {{ p.FullName }}</div>
</div>
</ng-template>
<ng-template kendoGridFilterCellTemplate let-filter let-column="column">
@@ -67,7 +67,7 @@
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngFor="let p of dataItem.Broker, let idx=index ">
<div *ngIf="dataItem.BrokerIds.length >0" class="customer-photo" [ngStyle]="{'background-image' : photoURL(dataItem.BrokerIds[idx])}"></div>
<div *ngIf="dataItem.BrokerIds.length >0" class="customer-name"> {{ p }}</div>
<div *ngIf="dataItem.BrokerIds.length >0" class="customer-name"> {{ p.FullName }}</div>
</div>
</ng-template>
<ng-template kendoGridFilterCellTemplate let-filter let-column="column">
@@ -80,7 +80,7 @@
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngFor="let p of dataItem.OtherRewarder, let idx=index ">
<div *ngIf="dataItem.OtherRewarderIds.length > 0 " class="customer-photo" [ngStyle]="{'background-image' : photoURL(dataItem.OtherRewarderIds[idx])}"></div>
<div *ngIf="dataItem.OtherRewarderIds.length > 0 " class="customer-name"> {{ p }}</div>
<div *ngIf="dataItem.OtherRewarderIds.length > 0 " class="customer-name"> {{ p.FullName }}</div>
</div>
</ng-template>
<ng-template kendoGridFilterCellTemplate let-filter let-column="column">

+ 1
- 1
src/app/list-all-loans/list-all-loans.component.ts Ver fichero

@@ -126,7 +126,7 @@ export class ListAllLoansComponent implements OnInit {
console.log(filter);
}
private loadData(): void {
this.service.query({ skip: this.skip, take: this.pageSize, sort: this.sort, filter: this.filter});
this.service.queryAsLoanModel({ skip: this.skip, take: this.pageSize, sort: this.sort, filter: this.filter});
}

private photoURL(peopleId: any): string {

+ 4
- 24
src/app/loan-detail/loan-detail-basic-info/loan-detail-basic-info.component.ts Ver fichero

@@ -5,7 +5,7 @@ import {HttpErrorResponse} from '@angular/common/http';
import {Observable} from 'rxjs';
import {LoanSingleService} from '../../service/loan.single.service';
import {LenderNameService} from '../../service/lender-name.service';
import { NotificationService } from '@progress/kendo-angular-notification';
import {ToastService} from '../../service/toast.service';

@Component({
selector: 'app-loan-detail-basic-info',
@@ -21,7 +21,7 @@ export class LoanDetailBasicInfoComponent implements OnInit {
public errorMessage='';

constructor( private service: LoanSingleService,
private notificationService: NotificationService,
private toast: ToastService,
public lenderNameService: LenderNameService) {
}

@@ -44,11 +44,11 @@ export class LoanDetailBasicInfoComponent implements OnInit {
this.Loan.Id = resp.Id;
}

this.showSuccess(this.Loan.Id + ' saved successfully');
this.toast.showSuccess(this.Loan.Id + ' saved successfully');
},
err => {
this.errorMessage = err instanceof HttpErrorResponse ? err.statusText : 'unknown error occured';
this.showError(this.errorMessage);
this.toast.showError(this.errorMessage);
}
);
}
@@ -75,25 +75,5 @@ export class LoanDetailBasicInfoComponent implements OnInit {
this.Loan.Description = DemoDescription;
}

public showSuccess(msg : string ): void {
this.notificationService.show({
content: msg,
cssClass: "button-notification",
animation: { type: "slide", duration: 400 },
position: { horizontal: "center", vertical: "bottom" },
type: { style: "success", icon: true },
closable: true,
});
}

public showError(err: string): void {
this.notificationService.show({
content: err,
cssClass: "button-notification",
animation: { type: "slide", duration: 400 },
position: { horizontal: "center", vertical: "bottom" },
type: { style: "error", icon: true },
closable: true,
});
}
}

+ 66
- 1
src/app/loan-detail/loan-detail-people/loan-detail-people.component.html Ver fichero

@@ -1 +1,66 @@
<p>loan-detail-people works! {{ Loan.Id}}</p>
<form #form="ngForm">
<div class="container">
<div class="row">
<div class="col-sm-6">
<h5 class="title">Select Clients</h5>
<kendo-multiselect name="client" [data]="AllPeople" [textField]="'Display'"
[valueField]="'Id'" [(ngModel)]="this.Loan.Client" [virtual]="virtual" required="true"
(close)="rebuildPeopleMap($event)">
<ng-template kendoMultiSelectItemTemplate let-dataItem>
<img class="contact-image" [src]="getContactImageUrl(dataItem.Id)" />
<span>{{ dataItem.Display}}</span>
</ng-template>
</kendo-multiselect>

<div class="container">
<div class="row clientCardContainer justify-content-center">
<div *ngFor="let v of this.Loan.Client" class="col-sm-9">
<app-people-card [PeopleId]="v.Id"></app-people-card>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<h5 class="title">Assign Brokers</h5>
<kendo-multiselect name="brokers" [data]="AllBrokers" [textField]="'Display'"
[valueField]="'Id'" [(ngModel)]="this.Loan.Broker" [virtual]="virtual"
(close)="rebuildPeopleMap($event)"
>
<ng-template kendoMultiSelectItemTemplate let-dataItem>
<img class="contact-image" [src]="getContactImageUrl(dataItem.Id)" />
<span>{{ dataItem.First +' ' + dataItem.Last}}</span>
</ng-template>
</kendo-multiselect>
<div class="container">
<div class="row brokerCardContainer justify-content-center">
<div *ngFor="let v of this.Loan.Broker" class="col-sm-9">
<app-people-card [PeopleId]="v.Id"></app-people-card>
</div>
</div>
</div>
</div>
</div>
</div>



<h5 class="title">Other Related</h5>
<kendo-multiselect name="beneficiaries" [data]="AllPeople" [textField]="'Display'"
[valueField]="'Id'" [(ngModel)]="this.Loan.OtherRewarder" [virtual]="virtual"
(close)="rebuildPeopleMap($event)">
<ng-template kendoMultiSelectItemTemplate let-dataItem>
<img class="contact-image" [src]="getContactImageUrl(dataItem.Id)" />
<span>{{ dataItem.First +' ' + dataItem.Last}}</span>
</ng-template>
</kendo-multiselect>
<div class="container">
<div class="row beneficiaryCardContainer justify-content-center">
<div *ngFor="let v of this.Loan.OtherRewarder, let idx=index" class="col-sm-4">
<app-people-card [PeopleId]="v.Id"></app-people-card>
<kendo-combobox [name]="'beneficiary_'+idx" [data]="roles" [allowCustom]="true" [(ngModel)]="role[v.Id]"
class="beneficiaryRole" required="true"> </kendo-combobox>
</div>
</div>
</div>`
<bkp-divider-shadow-bottom></bkp-divider-shadow-bottom>
</form>

+ 43
- 0
src/app/loan-detail/loan-detail-people/loan-detail-people.component.scss Ver fichero

@@ -0,0 +1,43 @@
.contact-image {
width: 20px;
height: 20px;
margin-right: 8px;
border-radius: 50%;
}

.title {
margin-top:50px
}

.number {
display: inline-block;
background: #333;
color: #fff;
border-radius: 50%;
width: 18px;
height: 18px;
text-align: center;
}

.beneficiaryRole {
width: 100%;
}

.clientCardContainer, .brokerCardContainer, .beneficiaryCardContainer{
padding-top: 20px;
padding-bottom: 50px;
border-radius: 0px 0px 50px 50px;
box-shadow: rgb(204, 219, 232) 3px 3px 6px 0px inset, rgba(255, 255, 255, 0.5) -3px -3px 6px 1px inset;
}

.clientCardContainer{
background-color: #fbfbec;
}

.brokerCardContainer{
background-color: #e0ffdf;
}

.beneficiaryCardContainer{
background-color: #eceeff;
}

+ 113
- 2
src/app/loan-detail/loan-detail-people/loan-detail-people.component.ts Ver fichero

@@ -1,5 +1,13 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {LoanModel} from '../../models/loan.model';
import {ComboBoxComponent, PreventableEvent} from '@progress/kendo-angular-dropdowns';
import {FormGroup} from '@angular/forms';
import {PeopleModel} from '../../models/people.model';
import {BrokerModel} from '../../models/broker.model';
import {PeopleService} from '../../service/people.service';
import {AuthService} from '../../service/auth.service';
import {ClonerService} from '../../service/clone.service';
import {PeopleMapModel} from '../../models/people-map.model';

@Component({
selector: 'app-loan-detail-people',
@@ -8,9 +16,112 @@ import {LoanModel} from '../../models/loan.model';
})
export class LoanDetailPeopleComponent implements OnInit {
@Input() Loan: LoanModel;
constructor() { }

@ViewChildren(ComboBoxComponent) cb!: QueryList<ComboBoxComponent>;
@ViewChild('form', {static: true}) form: FormGroup;

public AllPeople: PeopleModel[];
public AllBrokers: BrokerModel[];
public role: any = {}; // string to string map
public roles = ['Admin', 'Introducer', 'Referral', 'Observer', 'Verifier', 'Ex-Broker', 'Ex-Client', 'Banned', 'Unknown'];

constructor(private ps: PeopleService, private auth: AuthService, private dcs: ClonerService) { }

public virtual: any = {
itemHeight: 28
};

ngOnInit(): void {
this.ps.getPeopleList('').subscribe(
resp => {
this.AllPeople = resp.List;
}
);

this.ps.getBrokerList('').subscribe(
resp => {
this.AllBrokers = resp.List;
}
);

// ensure role strings are available for selection
this.Loan.PeopleMap.forEach( v => {
if ( v.Role.toLowerCase() !== 'client' && v.Role.toLocaleLowerCase() !== 'broker'){
this.roles.push( v.Role);
this.roles = [...new Set(this.roles)]; // remove duplicates
this.role[v.PeopleId] = v.Role;
}
});
}

ngAfterViewInit(): void {
this.cb.changes.subscribe( (data: QueryList<ComboBoxComponent> ) => {
data.forEach( v => {
if (v.value === undefined && !v.isOpen) {
v.toggle(true);
}
});
});
}


public getContactImageUrl(contactId: string): string {
return this.auth.getUrl('avatar/' + contactId);
}

public next(): void{
const err = this.rebuildPeopleMap(null);
if (err) {
this.showError('Please Select The people\'s role');
return;
}

if ( this.Loan.Client.length === 0) {
this.showError('Please Select at least 1 client');
return;
}
this.ps.syncPeople(this.Loan.cloneForJson()).subscribe( resp => {});
}

public prev(): void {

}

public rebuildPeopleMap(event: PreventableEvent): boolean{
this.Loan.PeopleMap = [];
this.Loan.Client.forEach(v => {
this.Loan.PeopleMap.push(new PeopleMapModel(
this.Loan.Id,
'Client',
v.Id
));
});

this.Loan.Broker.forEach(v => {
this.Loan.PeopleMap.push(new PeopleMapModel(
this.Loan.Id,
'Broker',
v.Id
));
});

let err = false;
this.Loan.OtherRewarder.forEach(v => {
if (this.role[v.Id] === undefined){
err = true;
}
this.Loan.PeopleMap.push(new PeopleMapModel(
this.Loan.Id,
this.role[v.Id] || 'Unknown',
v.Id
));
});

return err;
}

public showError(err: string): void {
// this.errorOccurred.emit(err);
}

}

+ 4
- 4
src/app/loan-detail/loan-detail.component.html Ver fichero

@@ -9,7 +9,7 @@
</div>
</kendo-expansionpanel>

<kendo-expansionpanel
<kendo-expansionpanel *ngIf="false"
[title]="'People'"
[subtitle]="Loan.Amount | currency "
[expanded]="false"
@@ -19,7 +19,7 @@
</div>
</kendo-expansionpanel>

<kendo-expansionpanel
<kendo-expansionpanel *ngIf="false"
[title]="'Reward'"
[subtitle]="Loan.Amount | currency "
[expanded]="false"
@@ -29,7 +29,7 @@
</div>
</kendo-expansionpanel>

<kendo-expansionpanel
<kendo-expansionpanel *ngIf="false"
[title]="'Progress'"
[subtitle]="Loan.Amount | currency "
[expanded]="false"
@@ -39,7 +39,7 @@
</div>
</kendo-expansionpanel>

<kendo-expansionpanel
<kendo-expansionpanel *ngIf="false"
[title]="'Trail Income'"
[subtitle]="Loan.Amount | currency "
[expanded]="false"

+ 1
- 1
src/app/loan-row-list/loan-row-list.component.html Ver fichero

@@ -9,7 +9,7 @@
<ng-template kendoListViewHeaderTemplate>
<div class="header">
<div class="title">All Loans</div>
<kendo-textbox
<kendo-textbox *ngIf="false"
placeholder="Filter items..."
(valueChange)="handleFilterChange($event)"
>

+ 2
- 2
src/app/loan-row-list/loan-row-list.component.ts Ver fichero

@@ -26,13 +26,13 @@ export class LoanRowListComponent implements OnInit {
const sort: Array <SortDescriptor> = [{dir: 'desc', field: 'Settlement'}];
if ( this.ss.loggedIn.role.toLowerCase() == 'admin' || this.ss.loggedIn.role.toLowerCase() == 'manager' ){
const filter: CompositeFilterDescriptor = {logic: 'and', filters: [] };
this.lss.query({ skip: this.skip, take: this.take, sort, filter});
this.lss.queryAsLoanModel({ skip: this.skip, take: this.take, sort, filter});
}else{ // load broker's loans
const filter: CompositeFilterDescriptor = {logic: 'or', filters: [] };
filter.filters.push({field:'client_ids', operator: 'contains', value: people.Id, ignoreCase: true})
filter.filters.push({field:'broker_ids', operator: 'contains', value: people.Id, ignoreCase: true})
filter.filters.push({field:'other_rewarder_ids', operator: 'contains', value: people.Id, ignoreCase: true})
this.lss.query({ skip: this.skip, take: this.take, sort, filter});
this.lss.queryAsLoanModel({ skip: this.skip, take: this.take, sort, filter});
}
}


+ 11
- 7
src/app/loan-single-row/loan-single-row.component.html Ver fichero

@@ -1,4 +1,4 @@
<div class="container" style="border-top-style: none; border-top-color: #ff0000; border-left: 10px groove #969696; padding: 10px; margin: 5px; background-color: #e9e9e9; box-shadow: inset 1 1 10 10;">
<div class="container single-row">
<div class="row" style="padding-bottom: 10px;">
<div class="col-md-4">
<h5>Overview<div *ngIf="Loading" class="spinner-border text-success" role="status">
@@ -27,21 +27,21 @@
<li class="list-group-item d-flex justify-content-between align-items-center">
<div *ngFor="let p of Loan.Client, let idx=index ">
<div *ngIf="Loan.ClientIds.length >0" class="customer-photo" [ngStyle]="{'background-image' : photoURL(Loan.ClientIds[idx])}"></div>
<div *ngIf="Loan.ClientIds.length >0" class="customer-name"> {{ p }}</div>
<div *ngIf="Loan.ClientIds.length >0" class="customer-name"> {{ p.FullName }}</div>
</div>
<span class="badge badge-primary badge-pill">Client</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<div *ngFor="let p of Loan.Broker, let idx=index ">
<div *ngIf="Loan.BrokerIds.length >0" class="customer-photo" [ngStyle]="{'background-image' : photoURL(Loan.BrokerIds[idx])}"></div>
<div *ngIf="Loan.BrokerIds.length >0" class="customer-name"> {{ p }}</div>
<div *ngIf="Loan.BrokerIds.length >0" class="customer-name"> {{ p.FullName }}</div>
</div>
<span class="badge badge-primary badge-pill">BDM</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<div *ngFor="let p of Loan.OtherRewarder, let idx=index ">
<div *ngIf="Loan.OtherRewarderIds.length > 0 " class="customer-photo" [ngStyle]="{'background-image' : photoURL(Loan.OtherRewarderIds[idx])}"></div>
<div *ngIf="Loan.OtherRewarderIds.length > 0 " class="customer-name"> {{ p }}</div>
<div *ngIf="Loan.OtherRewarderIds.length > 0 " class="customer-name"> {{ p.FullName }}</div>
</div>
<span class="badge badge-primary badge-pill">Other</span>
</li>
@@ -61,13 +61,17 @@
<span class="badge badge-primary badge-pill">#</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<button kendoButton (click)="gotoSteps()">Actions List</button>
<button kendoButton (click)="editLoan()">Edit</button>
<button kendoButton [togglable]="true" [icon]="editIcon" (selectedChange)="onShowLoanDetails($event)">Edit</button>
<button kendoButton [togglable]="true" [icon]="stepIcon" (selectedChange)="onShowSteps($event)" >Steps</button>
<button *ngIf="false" kendoButton (click)="editLoan()">Admin Edit</button>
</li>
</ul>
</div>
<div class="col-md-12">
<div class="col-md-12" *ngIf="isShowDetail">
<app-loan-detail [Loan]="Loan"></app-loan-detail>
</div>
<div class="col-md-12" *ngIf="isShowSteps">
<app-loan-steps [Loan]="Loan"> </app-loan-steps>
</div>
</div>
</div>

+ 13
- 0
src/app/loan-single-row/loan-single-row.component.scss Ver fichero

@@ -18,3 +18,16 @@
line-height: 16px;
padding-left: 10px;
}

div.container.single-row{
border-top-style: none;
border-top-color: #ff0000;
border-left: 10px groove #969696;
padding: 10px;
margin-left: auto;
margin-right:auto;
margin-top: 10px;
margin-bottom:5px;
background-color: #e9e9e945;
box-shadow: inset -1px 0px 3px 0px black;
}

+ 16
- 0
src/app/loan-single-row/loan-single-row.component.ts Ver fichero

@@ -13,11 +13,17 @@ import {NotificationService} from '@progress/kendo-angular-notification';
export class LoanSingleRowComponent implements OnInit {
@Input() public Loan: LoanModel = new LoanModel({});
@Input() public Loading: boolean = false;
@Input() public isShowDetail = false;
@Input() public isShowSteps = false;

public editIcon = 'edit';
public stepIcon = 'aggregate-fields';
constructor(private service: LoanSummaryService,
private auth: AuthService,
private router: Router,
private notificationService: NotificationService) { }


ngOnInit(): void {
}

@@ -33,4 +39,14 @@ export class LoanSingleRowComponent implements OnInit {
public editLoan() {
this.router.navigate(['edit-loan/', this.Loan.Id]).then();
}

public onShowLoanDetails(e: boolean): void{
this.isShowDetail = e;
// this.editIcon = e? 'trash' : 'save';
}

public onShowSteps(e: boolean ) : void {
this.isShowSteps = e;
// this.stepIcon = e? 'aggregate-fields': '';
}
}

+ 98
- 40
src/app/loan-steps/loan-steps.component.html Ver fichero

@@ -1,47 +1,105 @@
{{LoanId}}
<kendo-listview
[data]="steps"
containerClass="k-d-flex k-flex-col k-flex-nowrap"
>
<ng-template
kendoListViewItemTemplate
let-dataItem="dataItem"
let-index="index"
let-isFirst="isFirst"
let-isLast="isLast"
>
<div class="product" [class.border-bottom]="!isLast">
<strong>[{{ index }}]:</strong>
<span class="product-name">{{ dataItem.label }}</span>
<span class="item-position"
>({{ isFirst ? "first" : isLast ? "last" : "mid" }})</span
>
</div>
</ng-template>
</kendo-listview>

<kendo-tilelayout [columns]="1" [gap]="gap" [reorderable]="true">
<kendo-tilelayout-item [col]="1" title="Tile 1">
<kendo-tilelayout-item-body>
{{ firstTileContent }}
</kendo-tilelayout-item-body>
</kendo-tilelayout-item>

<kendo-tilelayout-item [col]="1" title="Tile 2">
<kendo-tilelayout [columns]="1" [gap]="gap" [reorderable]="false">
<span *ngIf="Loan.Steps.length <=0">No action available, administrator will add new actions</span>
<kendo-tilelayout-item *ngFor="let step of Loan.Steps; let i=index" [col]="1"
[title]="title(step)">
<kendo-tilelayout-item-body>
{{ secondTileContent }}
</kendo-tilelayout-item-body>
</kendo-tilelayout-item>
<form class="k-form" #stepForm="ngForm" >
<div class="row" *ngIf="!step.Done">
<div class="col-sm-1">
<kendo-formfield>
<kendo-label [for]="done" text="Done"></kendo-label>
<kendo-switch [(ngModel)]="step.Done" [ngModelOptions]="{standalone: true}"
(valueChange)="onResolveStep(step, $event)"
[disabled]="!step.Editable && !isAdmin"
onLabel="Yes"
offLabel="No"></kendo-switch>
</kendo-formfield>
</div>
<div class="col-sm-3">
<kendo-formfield>
<kendo-label [for]="description" text="Description:"></kendo-label>
<input kendoTextBox #description name="description"
[(ngModel)]="step.Description" [ngModelOptions]="{standalone: true}"
[maxlength]="301" [required]="true"
[readOnly]="step.Done"
[placeholder]="'upload passport'"
[disabled]="!step.Editable && !isAdmin"
/>
<kendo-formerror> required, and 0 < length < 300 </kendo-formerror>
</kendo-formfield>
</div>

<kendo-tilelayout-item title="Tile 3" [col]="1">
<kendo-tilelayout-item-body>
{{ thirdTileContent }}
</kendo-tilelayout-item-body>
</kendo-tilelayout-item>
<div class="col-sm-2">
<kendo-formfield>
<kendo-label [for]="receviedAt" text="ReceivedAt:"></kendo-label>
<kendo-datepicker #receviedAt name="receviedAt"
[readOnly]="step.Done"
[disabled]="!step.Editable && !isAdmin"
[(ngModel)]="step.ReceivedAt" [ngModelOptions]="{standalone: true}"
>
</kendo-datepicker>
</kendo-formfield>
</div>

<kendo-tilelayout-item title="Tile 4" [col]="1">
<kendo-tilelayout-item-body>
{{ fourthTileContent }}
<div class="col-sm-2">
<kendo-formfield>
<kendo-label [for]="resolvedAt" text="ResolvedAt:"></kendo-label>
<kendo-datepicker #resolvedAt name="resolvedAt"
[readOnly]="step.Done"
[disabled]="!step.Editable && !isAdmin"
[(ngModel)]="step.ResolvedAt" [ngModelOptions]="{standalone: true}"
>
</kendo-datepicker>
</kendo-formfield>
</div>
<div *ngIf="step.FileName !=='' " class="col-sm-4 align-right attachment"><span class="step-text">
{{step.FileName}} <br>
Size: ( {{step.FileSize | filesize }} ) Uploaded at: {{step.UploadedAt | date:"yyyy-MMM-dd"}} <br>
</span>
<button [disabled]="!step.Editable && !isAdmin" kendoButton class="download_attach" (click)="onDownload(step)" [icon]="'download'"></button>
<button [disabled]="!step.Editable && !isAdmin" kendoButton class="remove-attach" (click)="onRemoveAttach(step)" [icon]="'trash'"></button>
</div>
<div *ngIf="step.FileName ==='' " class="col-sm-4">
<kendo-upload class="bottom" [saveUrl]="uploadUrl(step)"
[disabled]="!step.Editable && !isAdmin"
[showFileList]="false" (success)="onSuccess($event)"> </kendo-upload>
</div>
</div>


<div class="row align-bottom" *ngIf="step.Done">
<div class="col-sm-1">
<kendo-formfield>
<kendo-label [for]="done" text="Done"></kendo-label>
<kendo-switch [(ngModel)]="step.Done" [ngModelOptions]="{standalone: true}"
(valueChange)="onResolveStep(step, $event)"
[disabled]="!step.Editable && !isAdmin"
onLabel="Yes"
offLabel="No"></kendo-switch>
</kendo-formfield>
</div>
<div class="col-sm-3"><span class="step-text"> {{step.Description}} </span></div>
<div class="col-sm-2"><span class="step-text"> Receive: {{step.ReceivedAt | date:"yyyy-MMM-dd"}} </span></div>
<div class="col-sm-2"><span class="step-text"> Resolve: {{step.ResolvedAt | date:"yyyy-MMM-dd"}} </span></div>
<div class="col-sm-4" *ngIf="step.FileName !== ''">
<span class="step-text">
{{step.FileName}}
<button kendoButton class="download-readonly" (click)="onDownload(step)" [icon]="'download'"></button>
</span>
</div>
<div class="col-sm-4" *ngIf="step.FileName === ''"> <span class="step-text"> No file attached </span> </div>


</div>
</form>
<button *ngIf="isAdmin" kendoButton [icon]="'close-outline'" class="delete" (click)="onDeleteStep(step)"></button>
<button [disabled]="!step.Editable && !isAdmin" kendoButton [icon]="'save'" class="save" (click)="onSaveStep(step)"></button>
</kendo-tilelayout-item-body>
</kendo-tilelayout-item>

<button *ngIf="isAdmin" kendoButton class="add-step" [icon]="'plus'" (click)="onAddNewStep()"> </button>

</kendo-tilelayout>

<a #downloadLink ></a>

+ 61
- 0
src/app/loan-steps/loan-steps.component.scss Ver fichero

@@ -0,0 +1,61 @@
span.step-text {
position: absolute;
bottom:10px;
}

kendo-upload.bottom {
position: absolute;
bottom:0px;
}

button.delete{
position: absolute;
right: -5px;
top: -5px;
z-index: 1;
border-radius: 100%;
color: black;
}
button.save{
position: absolute;
right: 30px;
top: -5px;
z-index: 1;
color: black;
}
button.add-step{
position: absolute;
right: 0px;
top: 0px;
border-radius: 100%;
}

button.download-readonly{
border-radius: 100%;
background: whitesmoke;
}

div.attachment{
background: lightgoldenrodyellow;
border-radius: 10px;

button.remove-attach{
position: absolute;
right: 0px;
top: 0px;
border-radius: 100%;
background: lightcoral;
color: white;
}

button.download_attach{
position: absolute;
right: 0px;
top: 30px;
border-radius: 100%;
background: whitesmoke;
color: black;
}


}

+ 113
- 27
src/app/loan-steps/loan-steps.component.ts Ver fichero

@@ -1,8 +1,17 @@
import { Component, OnInit } from '@angular/core';
import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
import {LoanSingleService} from '../service/loan.single.service';
import {ActivatedRoute, Router} from '@angular/router';
import {AuthService} from '../service/auth.service';
import { TileLayoutGap } from '@progress/kendo-angular-layout';
import {LoanModel} from '../models/loan.model';
import { StepModel } from '../models/step.model';
import {HttpParams} from '@angular/common/http';
import {SuccessEvent} from '@progress/kendo-angular-upload';
import {UploadMetaModel} from '../models/uploadMetaModel';
import {LoanStepService} from '../service/loan.step.service';
import {NotificationService} from '@progress/kendo-angular-notification';
import {ToastService} from '../service/toast.service';
import {SessionService} from '../service/session.service';

@Component({
selector: 'app-loan-steps',
@@ -10,42 +19,119 @@ import { TileLayoutGap } from '@progress/kendo-angular-layout';
styleUrls: ['./loan-steps.component.scss']
})
export class LoanStepsComponent implements OnInit {
public LoanId: string;

public steps = [
{ label: "Step One" },
{ label: "Step Two" },
{ label: "Step Three", optional: true },
{ label: "Step Four" },
{ label: "Step Five" },
];

@Input() public Loan: LoanModel;
@ViewChild('downloadLink', {static: true}) downloadLink: ElementRef<HTMLAnchorElement>;
public gap: TileLayoutGap = {
rows: 25,
columns: 40
};

public firstTileContent = `Reactive Netscape cherry pick domain contribution lazy Edge program.`;
public secondTileContent = `Lazy reflog freelancer Dijkstra directed acyclic graph concurrent uglify concurrency Safari.`;
public thirdTileContent = `Senior-engineer Edge backend UI subclass tech debt duck typing merge sort lazy.`;
public fourthTileContent = `Infrastructure tl;dr spy data store remote procedure call bootcamp pairing child keycaps.`;

constructor(private lss: LoanSingleService, private actRoute: ActivatedRoute, private auth: AuthService, private router: Router) { }
public isAdmin = false;
constructor(private lss: LoanSingleService,
private actRoute: ActivatedRoute,
private lstep: LoanStepService,
private auth: AuthService,
private ss: SessionService,
private toast: ToastService,
private router: Router) { }

ngOnInit(): void {
const LoanId = this.actRoute.snapshot.params.id;
this.isAdmin = this.ss.isAdmin();
}

public onAddNewStep(): void{
let s = new StepModel();
s.LoanId = this.Loan.Id
s.StepIndex = this.Loan.Steps.length + 1;

this.lstep.getIdByLoanIdAndStepIndex(s.LoanId, s.StepIndex).subscribe(resp => {
s = new StepModel(resp);
let now = new Date();
s.Description = "New_Step_"+now.getFullYear() + now.getMonth() + now.getDate();
this.Loan.Steps.push(s);
})
}

public uploadUrl( s: StepModel): string {
return this.auth.getUrl('step-upload/' + s.Id);
}

setTimeout(() => {
this.steps = this.stepsKeys(50);
}, 2000);
private normalizeDate(d: Date): string {
if (! d ) { d = new Date(); }
return d.toISOString();
}

public onSuccess(ss: SuccessEvent ): void {
let step = new StepModel(ss.response.body);

private stepsKeys(num: number): {label: string}[] {
let ret = [];
for (let i=1; i<=num; i++){
ret.push( {label: i, optional: false});
}
return ret;
this.Loan.Steps.forEach( (v, idx, theArray) => {
if ( (v.Id === step.Id) || // match Id
( (v.Id === '') && (v.LoanId === step.LoanId && v.StepIndex == step.StepIndex) ) ){// match LoanId and StepIndex when Id is empty
theArray[idx] = step;
}
});
}

public onResolveStep(s: StepModel, resolved: boolean): void {
if ( resolved ) {
if ( !s.ResolvedAt ){
s.ResolvedAt = new Date();
}
} else {
s.ResolvedAt = null;
}
}

public onDownload(step: StepModel): void {
const el = this.downloadLink.nativeElement;
el.href = this.auth.getUrl('step-download/' + step.Id)
el.click();
}

public onRemoveAttach(step: StepModel): void {
this.lstep.DeleteStepFileOnly(step.Id).subscribe(
resp => {
this.toast.showSuccess(step.FileName + " deleted");
step.FileName = "";
step.FileSize = 0;
step.FileMime = "";
},
err => {
this.toast.showError("cannot remove File");
},
);
}

public onDeleteStep(step: StepModel): void {
this.lstep.DeleteStep(step.Id).subscribe(
resp=>{
this.toast.showSuccess( step.Description + " deleted");
this.Loan.Steps = this.Loan.Steps.filter( v => v.Id != step.Id);
},
err =>{this.toast.showError( step.Description + " not deleted")},
);
}

public onSaveStep(step: StepModel): void {
this.lstep.SaveStep(step).subscribe(
resp => {
let result = new StepModel(resp);
step.StepIndex = result.StepIndex;
step.FileSize = result.FileSize;
step.FileMime = result.FileName;
step.FileName = result.FileName;
step.Description = result.Description;
step.ResolvedAt = result.ResolvedAt;
step.ReceivedAt = result.ReceivedAt;
console.log(result);
}
);
}

public title(step: StepModel): string {
let lock = step.Editable ? '' : ' - 🔒' ;
return step.StepIndex + ' - ' + step.Description + lock;
}


}

+ 1
- 2
src/app/main-menu-items.ts Ver fichero

@@ -19,8 +19,7 @@ export const mainMenuItems: any[] = [
items: [
{ text: 'Start New Loan', icon: 'plus', url: './#edit-loan/' },
{ text: 'Export Loans', icon: 'table' , url: './#list-all-loans' },
{ text: 'List all', icon: 'table' , url: './#loan-row-list' },
{ text: 'Steps (demo)', icon: 'table' , url: './#loan-steps/6f024e92-6600-4a5a-8961-95fdba4ed31d' },
{ text: 'List all', icon: 'aggregate-fields' , url: './#loan-row-list' },
{ text: '--', separator: 'true' },
{ text: 'list income', icon: 'dollar', url: './#list-income' },
{ text: '--', separator: 'true' },

+ 8
- 0
src/app/models/loan.model.ts Ver fichero

@@ -6,6 +6,7 @@ import {RewardModel} from './reward.model';
import {AuthService} from '../service/auth.service';
import {LoanSingleService} from '../service/loan.single.service';
import {MilestoneModel} from './milestone.model';
import {StepModel} from './step.model';

export interface LoanModelCallBacks {
getUserName(userId: string): string;
@@ -48,6 +49,8 @@ export class LoanModel {
public SettlementBooked: Date;
public SettlementCompleted: Date;

public Steps: StepModel[];

constructor(payload: Partial<LoanModel>) {
this.Id = payload.Id || '';
this.Amount = payload.Amount || 0;
@@ -141,6 +144,11 @@ export class LoanModel {
this.FormalApproved= this.getDate(payload.FormalApproved);
this.SettlementBooked= this.getDate(payload.SettlementBooked);
this.SettlementCompleted= this.getDate(payload.SettlementCompleted);

this.Steps = [];
if (payload.Steps && payload.Steps.length >0) {
payload.Steps.forEach(v => this.Steps.push(new StepModel(v)));
}
}

public static EmptyNew(lss: LoanSingleService, auth: AuthService): LoanModel {

+ 38
- 0
src/app/models/step.model.ts Ver fichero

@@ -0,0 +1,38 @@


export class StepModel{
public Id: string;
public LoanId: string;
public StepIndex : number;
public Description: string;
public Done: boolean;
public ReceivedAt: Date;
public ResolvedAt: Date;
public UploadedAt: Date;
public FileName: string;
public FileSize: number;
public FileMime: string;
//only for GUI
public Editable: boolean;

constructor(payload?: Partial<StepModel>) {
if( !payload ) { payload = {}; }

this.Id = payload.Id || '';
this.LoanId = payload.LoanId || '';
this.StepIndex = payload.StepIndex || -1;
this.Description = payload.Description || '';
this.Done = payload.Done || false;
this.ReceivedAt = payload.ReceivedAt? new Date(payload.ReceivedAt) : new Date('1900-01-01');
if ( this.ReceivedAt.getFullYear() <= 1900 ){ this.ReceivedAt = new Date() ; }
this.ResolvedAt = payload.ResolvedAt? new Date(payload.ResolvedAt) : new Date('1900-01-01');
if ( this.ResolvedAt.getFullYear() <= 1900 ){ this.ResolvedAt = null ; }
this.UploadedAt = payload.UploadedAt? new Date(payload.UploadedAt) : new Date('1900-01-01');
if ( this.UploadedAt.getFullYear() <= 1900 ){ this.UploadedAt = null ; }
this.FileName = payload.FileName || '';
this.FileSize = payload.FileSize || 0 ;
this.FileMime = payload.FileMime || '';
this.Editable = ! this.Done; // only when it started
}

}

+ 8
- 0
src/app/people-card/people-card.component.html Ver fichero

@@ -17,8 +17,16 @@
<button #enabled *ngIf="contact.Id.length > 10" kendoButton (selectedChange)="onSelectedChange($event)" look="outline" icon="tick"
[toggleable]="true" [selected]="contact.Enabled"
>Enable</button>
<button *ngIf="UserExtra.Login !=''" kendoButton (click)="onEmailPassword()" look="outline" icon="email">Email Password</button>
<!-- <button kendoButton (click)="onEditContact()" look="outline" icon="edit">Contacts</button>-->
<div *ngIf="sendingEmail" class="email-sending-indicator">
<span>To...{{UserExtra.Login}}</span>
<kendo-loader [type]="'pulsing'" [themeColor]="'primary'" [size]="'small'" ></kendo-loader>
</div>


</div>
</div>
</div>
</div>


+ 7
- 0
src/app/people-card/people-card.component.scss Ver fichero

@@ -209,6 +209,13 @@ $card-width: 330px;
flex-direction: row;
justify-content: center;
align-items: center;

div.email-sending-indicator{
text-align:center;
font-size: 10px;
font-color: black;
width: 100%
}
}

.social-media-wrapper {

+ 17
- 1
src/app/people-card/people-card.component.ts Ver fichero

@@ -6,6 +6,7 @@ import {Button} from '@progress/kendo-angular-buttons';
import {Router} from '@angular/router';
import {UserExtraModel} from '../models/user-extra.model';
import {BadgeAlign} from '@progress/kendo-angular-indicators';
import {ToastService} from '../service/toast.service';

@Component({
selector: 'app-people-card',
@@ -20,8 +21,9 @@ export class PeopleCardComponent implements OnInit {
public UserExtra: UserExtraModel = UserExtraModel.EmptyNew();

public badgeAlign: BadgeAlign = { vertical: 'bottom', horizontal: 'end' };
public sendingEmail = false;

constructor(private ps: PeopleService, private auth: AuthService, private router: Router) { }
constructor(private ps: PeopleService, private auth: AuthService, private router: Router, private toast: ToastService) { }

ngOnInit(): void {
this.ps.getPeopleById(this.PeopleId).subscribe(
@@ -78,4 +80,18 @@ export class PeopleCardComponent implements OnInit {
this.contact.Enabled = resp;
});
}

public onEmailPassword(): void {
this.sendingEmail = true;
this.ps.EmailPassword(this.PeopleId).subscribe(
resp => {
this.sendingEmail = false;
this.toast.showSuccess("Email seny to: " + this.UserExtra.Login )
},
err => {
this.sendingEmail = false;
this.toast.showError("Failed to email " + this.UserExtra.Login )
}
);
}
}

+ 10
- 0
src/app/pipe/file.size.pipe.ts Ver fichero

@@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'filesize'
})
export class FileSizePipe implements PipeTransform {
transform(size: number, extension: string = 'MB') {
return (size / (1024 * 1024)).toFixed(2) + extension;
}
}

+ 30
- 0
src/app/service/loan.step.service.ts Ver fichero

@@ -0,0 +1,30 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AuthService} from './auth.service';
import {ClonerService} from './clone.service';
import {AppConfig} from '../app.config';
import {Observable} from 'rxjs';
import {StepModel} from '../models/step.model';

@Injectable()
export class LoanStepService {

constructor(private http: HttpClient, private cfg: AppConfig){ }

public getIdByLoanIdAndStepIndex(LoanId: string, StepIndex: number): Observable<StepModel>{
return this.http.post<StepModel>(this.cfg.getUrl('step-id/'), {LoanId,StepIndex});
}

public SaveStep(step: StepModel): Observable<StepModel>{
return this.http.post<StepModel>(this.cfg.getUrl('step-meta-update/'), step);
}

public DeleteStep(stepId: string): Observable<boolean> {
return this.http.delete<boolean>(this.cfg.getUrl('step/'+ stepId));
}

public DeleteStepFileOnly(stepId: string): Observable<boolean> {
return this.http.delete<boolean>(this.cfg.getUrl('step-file/'+ stepId));
}

}

+ 6
- 0
src/app/service/loan_summary.service.ts Ver fichero

@@ -21,6 +21,12 @@ export abstract class LoanQueryService extends BehaviorSubject<GridDataResult> {
}

public query(state: any): void {
this.fetch(this.tableName, state).subscribe(x => {
super.next(x);
});
}

public queryAsLoanModel(state: any): void {
this.fetch(this.tableName, state).subscribe(x => {
var ret: GridDataResult = {total: x.total, data:[]};
x.data.forEach( v => {

+ 4
- 0
src/app/service/people.service.ts Ver fichero

@@ -90,4 +90,8 @@ export class PeopleService {
return this.http.post<UserExModel>(this.auth.getUrl('user-ex/'), uex);
}

public EmailPassword(id: string): Observable<boolean> {
return this.http.post<boolean>(this.auth.getUrl('email-password/' + id), id);
}

}

+ 31
- 0
src/app/service/toast.service.ts Ver fichero

@@ -0,0 +1,31 @@
import { Injectable } from "@angular/core";
import {NotificationService} from '@progress/kendo-angular-notification';


@Injectable()
export class ToastService {

constructor(private notificationService: NotificationService ) {}

public showSuccess(msg : string ): void {
this.notificationService.show({
content: msg,
cssClass: "button-notification",
animation: { type: "slide", duration: 400 },
position: { horizontal: "center", vertical: "bottom" },
type: { style: "success", icon: true },
hideAfter: 5000,
});
}

public showError(err: string): void {
this.notificationService.show({
content: err,
cssClass: "button-notification",
animation: { type: "slide", duration: 400 },
position: { horizontal: "center", vertical: "bottom" },
type: { style: "error", icon: true },
hideAfter: 5000,
});
}
}

Cargando…
Cancelar
Guardar