Support to pause and resume Loan Delinquency Classification (#1918)

Co-authored-by: Jose Alberto Hernandez <alberto@blue-daemon.local>
This commit is contained in:
José Alberto Hernández
2023-12-01 18:06:31 -06:00
committed by GitHub
co-authored by Jose Alberto Hernandez
parent 3f4e9870b6
commit a26c906d2c
28 changed files with 414 additions and 73 deletions
@@ -1,16 +1,8 @@
/** Angular Imports */
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Component, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource, MatTable } from '@angular/material/table';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { FormControl } from '@angular/forms';
/** Custom Services */
import { AccountTransfersService } from '../account-transfers.service';
/** Dialog Components */
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
/**
* Recurring Deposits Standing Instructions Tab
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { LoansService } from '../loans.service';
@Injectable({
providedIn: 'root'
})
export class LoanDelinquencyActionsResolver implements Resolve<boolean> {
/**
* @param {LoansService} LoansService Loans service.
*/
constructor(private loansService: LoansService) { }
/**
* Returns the Loans with Association data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
return this.loansService.getDelinquencyActions(loanId);
}
}
@@ -0,0 +1,34 @@
<h2 mat-dialog-title>{{"labels.heading.Loan Delinquency Actions" | translate}}</h2>
<div mat-dialog-content [formGroup]="delinquencyActionForm" fxLayout="column">
<mat-form-field fxFlex="48%" (click)="validFromDatePicker.open()">
<mat-label>{{"labels.inputs.Start Date" | translate}}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="validFromDatePicker" required
formControlName="startDate">
<mat-datepicker-toggle matSuffix [for]="validFromDatePicker"></mat-datepicker-toggle>
<mat-datepicker #validFromDatePicker></mat-datepicker>
<mat-error *ngIf="delinquencyActionForm.controls.startDate.hasError('required')">
{{"labels.inputs.Start Date" | translate}} {{"labels.commons.is" | translate}} <strong>{{"labels.commons.required" | translate}}</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%" (click)="validTillDatePicker.open()">
<mat-label>{{"labels.inputs.End Date" | translate}}</mat-label>
<input matInput [min]="delinquencyActionForm.value.startDate" [max]="maxDate" [matDatepicker]="validTillDatePicker" required
formControlName="endDate">
<mat-datepicker-toggle matSuffix [for]="validTillDatePicker"></mat-datepicker-toggle>
<mat-datepicker #validTillDatePicker></mat-datepicker>
<mat-error *ngIf="delinquencyActionForm.controls.endDate.hasError('required')">
{{"labels.inputs.End Date" | translate}} {{"labels.commons.is" | translate}} <strong>{{"labels.commons.required" | translate}}</strong>
</mat-error>
</mat-form-field>
</div>
<mat-dialog-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button mat-dialog-close> {{"labels.buttons.Cancel" | translate}} </button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ data: delinquencyActionForm }"
[disabled]="!delinquencyActionForm.valid || delinquencyActionForm.pristine">{{"labels.buttons.Submit" | translate}}</button>
</mat-dialog-actions>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanDelinquencyActionDialogComponent } from './loan-delinquency-action-dialog.component';
describe('LoanDelinquencyActionDialogComponent', () => {
let component: LoanDelinquencyActionDialogComponent;
let fixture: ComponentFixture<LoanDelinquencyActionDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoanDelinquencyActionDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(LoanDelinquencyActionDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,37 @@
import { Component, Inject, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SettingsService } from 'app/settings/settings.service';
@Component({
selector: 'mifosx-loan-delinquency-action-dialog',
templateUrl: './loan-delinquency-action-dialog.component.html',
styleUrls: ['./loan-delinquency-action-dialog.component.scss']
})
export class LoanDelinquencyActionDialogComponent implements OnInit {
delinquencyActionForm: UntypedFormGroup;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date(2100, 0, 1);
constructor(public dialogRef: MatDialogRef<LoanDelinquencyActionDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: UntypedFormBuilder,
private settingsService: SettingsService) {
this.createDelinquencyActionForm();
}
ngOnInit(): void {
this.minDate = this.settingsService.businessDate;
}
createDelinquencyActionForm() {
this.delinquencyActionForm = this.formBuilder.group({
'startDate': [new Date(), Validators.required],
'endDate': ['', Validators.required],
});
}
}
+3 -1
View File
@@ -59,6 +59,7 @@ import { ExternalAssetOwnerResolver } from './common-resolvers/external-asset-ow
import { ExternalAssetOwnerActiveTransferResolver } from './common-resolvers/external-asset-owner-active-transfer.resolver';
import { LoanCollateralsResolver } from './common-resolvers/loan-collaterals.resolver';
import { LoanDelinquencyDataResolver } from './common-resolvers/loan-delinquency-data.resolver';
import { LoanDelinquencyActionsResolver } from './common-resolvers/loan-delinquency-actions.resolver';
/** Loans Route. */
const routes: Routes = [
@@ -133,7 +134,8 @@ const routes: Routes = [
data: { title: 'Loans Delinquency Tags', breadcrumb: 'Delinquency Tags', routeParamBreadcrumb: false },
resolve: {
loanDelinquencyTagsData: LoanDelinquencyTagsResolver,
loanDelinquencyData: LoanDelinquencyDataResolver
loanDelinquencyData: LoanDelinquencyDataResolver,
loanDelinquencyActions: LoanDelinquencyActionsResolver
},
children: [
{
@@ -60,4 +60,54 @@
</table>
</div>
<div fxLayout="row" fxLayoutAlign="flex-end" class="m-t-20 m-b-10" *ngIf="allowPause">
<button mat-raised-button color="primary" (click)="createDelinquencyAction()" *mifosxHasPermission="'CREATE_DELINQUENCY_ACTION'">
<fa-icon icon="pause" class="m-r-10"></fa-icon>{{ 'labels.buttons.Pause Delinquency Classification' | translate }}
</button>
</div>
<div *ngIf="loanDelinquencyActions.length > 0" class="m-t-10">
<h3>{{"labels.heading.Loan Delinquency Actions" | translate}}</h3>
<table mat-table [dataSource]="loanDelinquencyActions">
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef> {{"labels.inputs.Action" | translate}} </th>
<td mat-cell *matCellDef="let item" ngClass="{{actionClass(item.action)}}"> {{ item.action }} </td>
</ng-container>
<ng-container matColumnDef="startDate">
<th mat-header-cell *matHeaderCellDef> {{"labels.inputs.Start Date" | translate}} </th>
<td mat-cell *matCellDef="let item"> {{ item.startDate | dateFormat }} </td>
</ng-container>
<ng-container matColumnDef="endDate">
<th mat-header-cell *matHeaderCellDef> {{"labels.inputs.End Date" | translate}} </th>
<td mat-cell *matCellDef="let item"> {{ item.endDate | dateFormat }} </td>
</ng-container>
<ng-container matColumnDef="createdOn">
<th mat-header-cell *matHeaderCellDef> {{"labels.inputs.Created On" | translate}} </th>
<td mat-cell *matCellDef="let item"> {{ item.createdOn | datetimeFormat }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{"labels.inputs.Actions" | translate}} </th>
<td mat-cell *matCellDef="let item">
<span *ngIf="isCurrentAndPauseAction(item)">
<button mat-button color="primary" (click)="resumeDelinquencyClassification(item)"
*mifosxHasPermission="'CREATE_DELINQUENCY_ACTION'" matTooltip="{{'tooltips.Resume Delinquency Classification' | translate}}" >
<fa-icon icon="play" class="m-r-10"></fa-icon>
</button>
</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="loanDelinquencyActionsColumns"></tr>
<tr mat-row *matRowDef="let row; columns: loanDelinquencyActionsColumns;"></tr>
</table>
</div>
</div>
@@ -1,6 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { DelinquentData, InstallmentLevelDelinquency } from 'app/loans/models/loan-account.model';
import { Dates } from 'app/core/utils/dates';
import { LoanDelinquencyActionDialogComponent } from 'app/loans/custom-dialog/loan-delinquency-action-dialog/loan-delinquency-action-dialog.component';
import { LoansService } from 'app/loans/loans.service';
import { DelinquentData, InstallmentLevelDelinquency, LoanDelinquencyAction, LoanDelinquencyTags } from 'app/loans/models/loan-account.model';
import { SettingsService } from 'app/settings/settings.service';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
import { Currency } from 'app/shared/models/general.model';
@Component({
@@ -10,15 +16,34 @@ import { Currency } from 'app/shared/models/general.model';
})
export class LoanDelinquencyTagsTabComponent implements OnInit {
loanDelinquencyTags: any;
loanDelinquencyTags: LoanDelinquencyTags[] = [];
loanDelinquencyActions: LoanDelinquencyAction[] = [];
currentLoanDelinquencyAction: LoanDelinquencyAction | null;
currency: Currency;
installmentLevelDelinquency: InstallmentLevelDelinquency[] = [];
loanDelinquencyTagsColumns: string[] = ['classification', 'addedOn', 'liftedOn'];
loanDelinquencyActionsColumns: string[] = ['action', 'startDate', 'endDate', 'createdOn', 'actions'];
installmentDelinquencyTagsColumns: string[] = ['classification', 'minimumAgeDays', 'amount'];
constructor(private route: ActivatedRoute) {
this.route.parent.data.subscribe((data: { loanDelinquencyTagsData: any, loanDelinquencyData: any }) => {
allowPause = true;
loanId: any;
locale: string;
dateFormat: string;
constructor(private route: ActivatedRoute,
private loansServices: LoansService,
private dateUtils: Dates,
private settingsService: SettingsService,
public dialog: MatDialog) {
this.loanId = this.route.parent.parent.snapshot.params['loanId'];
this.route.parent.data.subscribe((data: { loanDelinquencyTagsData: LoanDelinquencyTags[],
loanDelinquencyData: any,
loanDelinquencyActions: LoanDelinquencyAction[] }) => {
this.loanDelinquencyTags = data.loanDelinquencyTagsData;
this.loanDelinquencyActions = data.loanDelinquencyActions || [];
this.validateDelinquencyActions();
const loanDelinquencyData: DelinquentData | null = data.loanDelinquencyData.delinquent || null;
this.currency = data.loanDelinquencyData.currency;
this.installmentLevelDelinquency = [];
@@ -29,6 +54,86 @@ export class LoanDelinquencyTagsTabComponent implements OnInit {
}
ngOnInit(): void {
this.locale = this.settingsService.language.code;
this.dateFormat = this.settingsService.dateFormat;
this.currentLoanDelinquencyAction = null;
this.validateDelinquencyActions();
}
validateDelinquencyActions(): void {
if (this.loanDelinquencyActions.length > 0) {
const businessDate: Date = this.settingsService.businessDate;
this.currentLoanDelinquencyAction = this.loanDelinquencyActions[this.loanDelinquencyActions.length - 1];
this.allowPause = (this.currentLoanDelinquencyAction.action === 'RESUME');
}
}
createDelinquencyAction(): void {
const action = 'pause';
const loanDelinquencyActionDialogRef = this.dialog.open(LoanDelinquencyActionDialogComponent, {
data: {
action: action
},
});
loanDelinquencyActionDialogRef.afterClosed().subscribe((response: { data: any }) => {
const startDate: Date = response.data.value.startDate;
const endDate: Date = response.data.value.endDate;
this.sendDelinquencyAction(action, startDate, endDate);
});
}
resumeDelinquencyClassification(item: LoanDelinquencyAction): void {
const removePauseDialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: { heading: 'Loan Delinquency Classification',
dialogContext: 'Are you sure you want resume the Delinquency Classification for Loan ' + this.loanId,
type: 'Mild'
}
});
removePauseDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.sendDelinquencyAction('resume', new Date(), null);
}
});
}
sendDelinquencyAction(action: string, startDate: Date, endDate: Date): void {
let payload: any = {
action,
locale: this.locale,
dateFormat: this.dateFormat,
startDate: this.dateUtils.formatDate(startDate, this.dateFormat)
};
if (action === 'pause') {
payload = {
action,
locale: this.locale,
dateFormat: this.dateFormat,
startDate: this.dateUtils.formatDate(startDate, this.dateFormat),
endDate: this.dateUtils.formatDate(endDate, this.dateFormat)
};
}
this.loansServices.createDelinquencyActions(this.loanId, payload).subscribe((result: any) => {
this.loansServices.getDelinquencyActions(this.loanId).subscribe((loanDelinquencyActions: LoanDelinquencyAction[]) => {
this.loanDelinquencyActions = loanDelinquencyActions;
this.validateDelinquencyActions();
});
});
}
isCurrentAndPauseAction(item: LoanDelinquencyAction): boolean {
if (this.currentLoanDelinquencyAction != null) {
return (this.currentLoanDelinquencyAction.id === item.id && item.action === 'PAUSE');
}
return false;
}
actionClass(action: string): string {
if (action === 'PAUSE') {
return 'status-pending';
}
return 'status-active';
}
}
@@ -20,28 +20,34 @@
<h3>
<i class="fa fa-stop" matTooltip="{{'labels.status.' + loanDetailsData.status.value | translate}}"
[ngClass]="loanDetailsData.inArrears?'status-active-overdue':( iconLoanStatusColor() | statusLookup)"></i>
{{'labels.heading.Loan Product' | translate}}: {{loanDetailsData.loanProductName}}
<span class="m-r-10">{{'labels.heading.Loan Product' | translate}}:</span> {{loanDetailsData.loanProductName}}
<mifosx-account-number accountNo="{{loanDetailsData.accountNo}}"></mifosx-account-number>
</h3>
<span class="loans-overview">
{{'labels.text.' + entityType | translate}} {{"labels.inputs.name" | translate}}: {{loanDetailsData.clientName ||
loanDetailsData.groupName}}<span class="m-l-10">({{loanDetailsData.clientAccountNo}})</span><br />
<span class="m-r-10">{{'labels.text.' + entityType | translate}} {{"labels.inputs.name" | translate}} :</span> {{loanDetailsData.clientName ||
loanDetailsData.groupName}}<span class="m-l-5">({{loanDetailsData.clientAccountNo}})</span><br />
</span>
<div *ngIf="loanDisplayArrearsDelinquency != 1">
<span class="loans-overview" *ngIf="loanDetailsData.delinquencyRange">
{{"labels.inputs.Loan Account Classification" | translate}} :
{{loanDetailsData?.delinquencyRange.classification}}<br />
</span>
<span class="loans-overview"
*ngIf="loanDetailsData.delinquent && loanDetailsData.delinquent.pastDueDays > 0">
{{"labels.inputs.Past Due Days" | translate}} : {{loanDetailsData?.delinquent.pastDueDays |
formatNumber }}<br />
</span>
<span class="loans-overview" *ngIf="loanDetailsData.delinquent && loanDetailsData.delinquent.delinquentDays > 0
&& loanDetailsData.delinquent.pastDueDays != loanDetailsData.delinquent.delinquentDays">
{{"labels.inputs.Delinquent Days" | translate}} : {{loanDetailsData?.delinquent.delinquentDays |
formatNumber }}
</span>
<table class="account-overview">
<tbody>
<tr *ngIf="loanDetailsData.delinquencyRange">
<td>{{"labels.inputs.Delinquency Classification" | translate}} :</td>
<td>
<span class="m-r-3"><i [ngClass]="loanDelinquencyClassificationStyle"></i></span>
{{loanDetailsData?.delinquencyRange.classification}}
</td>
</tr>
<tr *ngIf="loanDetailsData.delinquent && loanDetailsData.delinquent.pastDueDays > 0">
<td>{{"labels.inputs.Past Due Days" | translate}} :</td>
<td>{{loanDetailsData?.delinquent.pastDueDays | formatNumber }}</td>
</tr>
<tr *ngIf="loanDetailsData.delinquent && loanDetailsData.delinquent.delinquentDays > 0
&& loanDetailsData.delinquent.pastDueDays != loanDetailsData.delinquent.delinquentDays">
<td>{{"labels.inputs.Delinquent Days" | translate}} :</td>
<td>{{loanDetailsData?.delinquent.delinquentDays | formatNumber }}</td>
</tr>
</tbody>
</table>
</div>
</div>
@@ -1,40 +1,8 @@
.loan-card {
margin: 0 auto;
max-width: 80rem;
width: 90%;
padding: 0;
.header {
padding: 1%;
.header-title-group {
.loan-card-title {
color: white;
width: 90%;
p {
color: white;
}
}
}
.profile-image-container {
margin: 1%;
.profile-image {
object-fit: cover;
border-radius: 20px;
}
}
.loan-actions {
align-self: flex-end;
margin: 0 1%;
// min-width: 72%;
}
.loan-span {
margin: 0 0.5%;
}
}
.navigation-tabs {
overflow: auto;
}
}
.loans-overview {
font-size: 14px;
}
.account-overview {
min-width: 60%;
font-weight: 400;
}
@@ -14,6 +14,7 @@ import { ConfirmationDialogComponent } from '../../shared/confirmation-dialog/co
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { LoanStatus } from '../models/loan-status.model';
import { Currency } from 'app/shared/models/general.model';
import { DelinquencyPausePeriod } from '../models/loan-account.model';
@Component({
selector: 'mifosx-loans-view',
@@ -42,6 +43,7 @@ export class LoansViewComponent implements OnInit {
/** Disburse Transaction number */
disburseTransactionNo = 0;
loanDelinquencyClassificationStyle = '';
loanStatus: LoanStatus;
currency: Currency;
@@ -80,6 +82,7 @@ export class LoansViewComponent implements OnInit {
} else if (this.router.url.includes('centers')) {
this.entityType = 'Center';
}
this.loanDelinquencyClassification();
}
// Defines the buttons based on the status of the loan account
@@ -199,6 +202,17 @@ export class LoansViewComponent implements OnInit {
});
}
loanDelinquencyClassification(): void {
this.loanDelinquencyClassificationStyle = '';
if (this.loanDetailsData.delinquent && this.loanDetailsData.delinquent.delinquencyPausePeriods) {
this.loanDetailsData.delinquent.delinquencyPausePeriods.some((period: DelinquencyPausePeriod) => {
if (period.active) {
this.loanDelinquencyClassificationStyle = 'fa fa-stop status-pending';
}
});
}
}
iconLoanStatusColor() {
if (this.loanDetailsData.chargedOff) {
return 'loanStatusType.chargeoff';
+3 -1
View File
@@ -79,6 +79,7 @@ import { AssetTransferLoanComponent } from './loans-view/loan-account-actions/as
import { ExternalAssetOwnerTabComponent } from './loans-view/external-asset-owner-tab/external-asset-owner-tab.component';
import { ExternalAssetTransferComponent } from './loans-view/external-asset-owner-tab/external-asset-transfer/external-asset-transfer.component';
import { TranslateModule } from '@ngx-translate/core';
import { LoanDelinquencyActionDialogComponent } from './custom-dialog/loan-delinquency-action-dialog/loan-delinquency-action-dialog.component';
/**
* Loans Module
@@ -161,7 +162,8 @@ import { TranslateModule } from '@ngx-translate/core';
ChargeOffComponent,
AssetTransferLoanComponent,
ExternalAssetOwnerTabComponent,
ExternalAssetTransferComponent
ExternalAssetTransferComponent,
LoanDelinquencyActionDialogComponent
],
providers: [ ],
})
+8
View File
@@ -82,6 +82,14 @@ export class LoansService {
return this.http.get(`/loans/${loanId}`, { params: httpParams });
}
getDelinquencyActions(loanId: string) {
return this.http.get(`/loans/${loanId}/delinquency-actions`);
}
createDelinquencyActions(loanId: string, delinquencyActions: any) {
return this.http.post(`/loans/${loanId}/delinquency-actions`, delinquencyActions);
}
/**
* Returns the loan template data with specific condition
* @param loanId Loan Id
@@ -24,3 +24,28 @@ export interface InstallmentLevelDelinquency {
minimumAgeDays: number;
delinquentAmount: number;
}
export interface LoanDelinquencyTags {
id: number;
loanId: number;
delinquencyRange: DelinquencyRange;
addedOnDate: number[];
liftedOnDate: number[];
}
export interface LoanDelinquencyAction {
id: number;
action: string;
startDate: number[];
endDate: number[];
createdById: number;
createdOn: Date;
updatedById: number;
lastModifiedOn: Date;
}
export interface DelinquencyPausePeriod {
active: boolean;
pausePeriodStart: number[];
pausePeriodEnd: number[];
}
+3 -1
View File
@@ -107,7 +107,8 @@ import {
faFileExcel,
faFileExport,
faCalculator,
faCalendarCheck
faCalendarCheck,
faPause
} from '@fortawesome/free-solid-svg-icons';
/**
@@ -177,6 +178,7 @@ export class IconsModule {
faMinusCircle,
faMoneyBillAlt,
faMoneyCheck,
faPause,
faPencilAlt,
faPlay,
faPlus,