Merge pull request #1491 from josehernandezfintecheandomx/enhancements/delinquency_buckets

Delinquency buckets management for Loans
This commit is contained in:
Adam Saghy
2022-08-08 21:18:50 +02:00
committed by GitHub
50 changed files with 1701 additions and 9 deletions
@@ -5,6 +5,7 @@
<div fxLayout="row" fxLayoutGap="32px" class="performance-history-container">
<p>
# of Repayments :{{loanDetails?.numberOfRepayments}}
</p>
<p>
Maturity Date :{{loanDetails?.timeline.expectedMaturityDate | dateFormat}}
</p>
@@ -69,7 +70,7 @@
<ng-container matColumnDef="Value">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let ele">
<td mat-cell *matCellDef="let ele">
<ng-container *ngIf="ele.key === 'Disbursement Date'" >
<span *ngIf="loanDetails.timeline.actualDisbursementDate"> {{loanDetails.timeline.actualDisbursementDate | dateFormat}} </span>
<span *ngIf="!loanDetails.timeline.actualDisbursementDate"> Not Available </span>
@@ -124,7 +125,7 @@
<ng-container matColumnDef="Value">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let ele">
<td mat-cell *matCellDef="let ele">
<ng-container *ngIf="ele.key === 'Disbursement Date'" >
<span *ngIf="loanDetails.timeline.actualDisbursementDate"> {{loanDetails.timeline.actualDisbursementDate | dateFormat}} </span>
<span *ngIf="!loanDetails.timeline.actualDisbursementDate"> Not Available </span>
@@ -1,5 +1,9 @@
<div class="container">
<ng-container *ngIf="loanDetails.delinquencyRange">
<h3>Loan Account Classification : {{loanDetails?.delinquencyRange.classification}}</h3>
</ng-container>
<table mat-table [dataSource]="repaymentScheduleDetails.periods">
<ng-container matColumnDef="number">
@@ -29,7 +33,7 @@
<ng-container matColumnDef="check">
<th mat-header-cell *matHeaderCellDef> &nbsp; </th>
<td mat-cell *matCellDef="let ele">
<span *ngIf="ele.obligationsMetOnDate"> <i class='fa fa-check'></i> </span>
<span *ngIf="ele.obligationsMetOnDate"> <i class='fa fa-check'></i> </span>
</td>
<td mat-footer-cell *matFooterCellDef> </td>
</ng-container>
@@ -135,4 +139,4 @@
</div>
<!-- <p>It works</p> -->
<!-- <p>It works</p> -->
@@ -9,4 +9,4 @@ table {
.check {
padding-left: 15px;
}
}
@@ -8,6 +8,7 @@ import { ActivatedRoute } from '@angular/router';
})
export class RepaymentScheduleTabComponent implements OnInit {
loanDetails: any;
/** Loan Repayment Schedule Details Data */
repaymentScheduleDetails: any;
/** Stores if there is any waived amount */
@@ -21,6 +22,7 @@ export class RepaymentScheduleTabComponent implements OnInit {
*/
constructor(private route: ActivatedRoute) {
this.route.parent.data.subscribe((data: { loanDetailsData: any }) => {
this.loanDetails = data.loanDetailsData;
this.repaymentScheduleDetails = data.loanDetailsData.repaymentSchedule;
});
}
@@ -346,6 +346,11 @@
<span fxFlex="60%">{{ loanProduct.overdueDaysForNPA }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Delinquency Bucket:</span>
<span fxFlex="60%">{{ loanProduct.delinquencyBucketId ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Account moves out of NPA only after all arrears have been cleared:</span>
<span fxFlex="60%">{{ loanProduct.accountMovesOutOfNPAOnlyOnArrearsCompletion ? 'Yes' : 'No' }}</span>
@@ -72,6 +72,15 @@
<input type="number" matInput formControlName="graceOnInterestPayment">
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Delinquency Bucket</mat-label>
<mat-select formControlName="delinquencyBucketId">
<mat-option *ngFor="let delinquencyBucket of delinquencyBucketData" [value]="delinquencyBucket.id">
{{ delinquencyBucket.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<mat-form-field fxFlex="48%">
@@ -26,6 +26,7 @@ export class LoanProductSettingsStepComponent implements OnInit {
interestRecalculationNthDayTypeData: any;
interestRecalculationDayOfWeekTypeData: any;
interestRecalculationOnDayTypeData: any;
delinquencyBucketData: any;
constructor(private formBuilder: FormBuilder) {
this.createLoanProductSettingsForm();
@@ -54,6 +55,7 @@ export class LoanProductSettingsStepComponent implements OnInit {
this.interestRecalculationNthDayTypeData.push({ 'id': -2, 'code': 'onDay', 'value': 'on day' });
this.interestRecalculationDayOfWeekTypeData = this.loanProductsTemplate.interestRecalculationDayOfWeekTypeOptions;
this.interestRecalculationOnDayTypeData = Array.from({ length: 28 }, (_, index) => index + 1);
this.delinquencyBucketData = this.loanProductsTemplate.delinquencyBucketOptions;
this.loanProductSettingsForm.patchValue({
'amortizationType': this.loanProductsTemplate.amortizationType.id,
@@ -82,7 +84,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
'holdGuaranteeFunds': this.loanProductsTemplate.holdGuaranteeFunds,
'multiDisburseLoan': this.loanProductsTemplate.multiDisburseLoan,
'maxTrancheCount': this.loanProductsTemplate.maxTrancheCount,
'outstandingLoanBalance': this.loanProductsTemplate.outstandingLoanBalance
'outstandingLoanBalance': this.loanProductsTemplate.outstandingLoanBalance,
'delinquencyBucketId': this.loanProductsTemplate.delinquencyBucket.id
});
if (this.loanProductsTemplate.isInterestRecalculationEnabled) {
@@ -170,7 +173,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
'repaymentEvery': [true],
'graceOnPrincipalAndInterestPayment': [true],
'graceOnArrearsAgeing': [true]
})
}),
'delinquencyBucketId': ['']
});
}
@@ -506,6 +506,8 @@
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.graceOnPrincipalAndInterestPayment ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Number of days a loan may be overdue before moving into arrears:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.graceOnArrearsAgeing ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Delinquency Bucket:</span>
<span fxFlex="60%">{{ loanProduct.delinquencyBucket.id ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.charges.length" fxLayout="row wrap" fxLayout.lt-md="column">
@@ -0,0 +1,77 @@
<div class="container">
<mat-card>
<form [formGroup]="bucketForm" (ngSubmit)="submit()">
<mat-card-content>
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="100%">
<mat-label>Name</mat-label>
<input matInput required formControlName="name">
<mat-error *ngIf="bucketForm.controls.name.hasError('required')">
Name is <strong>required</strong>
</mat-error>
</mat-form-field>
<h3 fxFlex="40%" class="mat-h3">Delinquency Ranges</h3>
<div fxFlex="40%" fxLayout="row" fxLayoutAlign="start center">
<button type="button" mat-raised-button color="primary" (click)="addDelinquencyRange()">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table mat-table [dataSource]="rangesDataSource" [hidden]="rangesDataSource.length === 0">
<ng-container matColumnDef="classification">
<th mat-header-cell *matHeaderCellDef> Classification </th>
<td mat-cell *matCellDef="let row">
{{ row.rangeId | find: delinquencyRangesData:'id':'classification'}} </td>
</ng-container>
<ng-container matColumnDef="minimumAgeDays">
<th mat-header-cell *matHeaderCellDef> Days From </th>
<td mat-cell *matCellDef="let row">
{{ row.rangeId | find: delinquencyRangesData:'id':'minimumAgeDays'}} </td>
</ng-container>
<ng-container matColumnDef="maximumAgeDays">
<th mat-header-cell *matHeaderCellDef> Days Till </th>
<td mat-cell *matCellDef="let row">
{{ row.rangeId | find: delinquencyRangesData:'id':'maximumAgeDays'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let row; let rowIndex = index">
<button type="button" mat-icon-button color="warn"
(click)="deleteDelinquencyRange(rowIndex)" matTooltip="Delete"
matTooltipPosition="left">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card-content>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!bucketForm.valid" *mifosxHasPermission="'CREATE_DELINQUENCY_BUCKET'">Submit</button>
</mat-card-actions>
</form>
</mat-card>
</div>
@@ -0,0 +1,10 @@
.container {
max-width: 37rem;
.content {
div {
margin: 0.8rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateBucketComponent } from './create-bucket.component';
describe('CreateBucketComponent', () => {
let component: CreateBucketComponent;
let fixture: ComponentFixture<CreateBucketComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CreateBucketComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CreateBucketComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,124 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
@Component({
selector: 'mifosx-create-bucket',
templateUrl: './create-bucket.component.html',
styleUrls: ['./create-bucket.component.scss']
})
export class CreateBucketComponent implements OnInit {
/** Delinquency Bucket form. */
bucketForm: FormGroup;
/** Delinquency Bucket template data. */
bucketTemplateData: any;
/** Delinquency Range Data Source */
rangesDataSource: {}[];
/** Delinquency Range Options */
delinquencyRangesData: any;
delinquencyRangesIds: any;
/** Delinquency Range Displayed Columns */
displayedColumns: string[] = ['classification', 'minimumAgeDays', 'maximumAgeDays', 'actions'];
constructor(private formBuilder: FormBuilder,
private productsService: ProductsService,
private router: Router,
private route: ActivatedRoute,
public dialog: MatDialog) {
this.route.data.subscribe((data: { delinquencyRanges: any }) => {
this.delinquencyRangesData = data.delinquencyRanges;
this.delinquencyRangesData = this.delinquencyRangesData.sort(
(objA: { minimumAgeDays: number; }, objB: { minimumAgeDays: number; }) => objA.minimumAgeDays - objB.minimumAgeDays,
);
});
}
ngOnInit(): void {
this.setupForm();
this.rangesDataSource = [];
this.delinquencyRangesIds = [];
}
/**
* Creates the Delinquency Bucket form
*/
setupForm(): void {
this.bucketForm = this.formBuilder.group({
'name': ['', Validators.required]
});
}
/**
* Add Delinquency Range for a Delinquency Bucket
*/
addDelinquencyRange() {
let delinquencyRanges = this.delinquencyRangesData;
if (this.delinquencyRangesIds.length > 0) {
delinquencyRanges = this.delinquencyRangesData.filter((item: any) => {
return (this.delinquencyRangesIds.indexOf(item.id) < 0);
});
}
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'rangeId',
label: 'Delinquency Range',
options: { label: 'classification', value: 'id', data: delinquencyRanges },
order: 1
})
];
const data = {
title: 'Add Delinquency Range',
layout: { addButtonText: 'Add' },
formfields: formfields
};
const rangeDialogRef = this.dialog.open(FormDialogComponent, { data });
rangeDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
this.rangesDataSource = this.rangesDataSource.concat(response.data.value);
this.delinquencyRangesIds.push(response.data.value.rangeId);
}
});
}
/**
* Delete particular Delinquency Range in Delinquency Bucket
*/
deleteDelinquencyRange(index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.delinquencyRangesIds.splice(index, 1);
this.rangesDataSource.splice(index, 1);
this.rangesDataSource = this.rangesDataSource.concat([]);
}
});
}
/**
* Submits the Delinquency Bucket form and creates the Delinquency Bucket,
* if successful redirects to Delinquency Buckets.
*/
submit() {
const ranges: any = [];
this.rangesDataSource.forEach((item: any) => { ranges.push(item.rangeId); });
if (ranges.length > 0) {
const data = {
...this.bucketForm.value,
ranges: ranges
};
this.productsService.createDelinquencyBucket(data).subscribe((response: any) => {
this.router.navigate(['../', response.resourceId], { relativeTo: this.route });
});
}
}
}
@@ -0,0 +1,35 @@
<div class="container m-b-20" fxLayout="row" fxLayoutAlign="end" fxLayoutGap="20px">
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_DELINQUENCY_BUCKET'">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Create Delinquency Bucket
</button>
</div>
<div class="container">
<div fxLayout="row">
<mat-form-field fxFlex>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)">
</mat-form-field>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
<td mat-cell *matCellDef="let delinquencyBucket"> {{ delinquencyBucket.name }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="[row.id]" class="select-row"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
@@ -0,0 +1,35 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../../products.service';
/**
* Delinquency Bucket Component data resolver.
*/
@Injectable()
export class DelinquencyBucketComponentsResolver implements Resolve<Object> {
/**
* @param {ProductsService} productsService Products service.
*/
constructor(private productsService: ProductsService) {}
/**
* Returns the delinquency buckets data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const delinquentcyBucketId = route.paramMap.get('bucketId');
if (delinquentcyBucketId === null) {
return this.productsService.getDelinquencyBuckets();
} else {
return this.productsService.getDelinquencyBucket(delinquentcyBucketId);
}
}
}
@@ -0,0 +1,18 @@
.container {
.action-button {
margin-left: auto;
}
.search-button {
min-width: 30px;
margin: 4px;
}
}
table {
tr.select-row:hover {
cursor: pointer;
}
tr:nth-child(odd).td{
color : black;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DelinquencyBucketComponent } from './delinquency-bucket.component';
describe('DelinquencyBucketComponent', () => {
let component: DelinquencyBucketComponent;
let fixture: ComponentFixture<DelinquencyBucketComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DelinquencyBucketComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DelinquencyBucketComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,51 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'mifosx-delinquency-bucket',
templateUrl: './delinquency-bucket.component.html',
styleUrls: ['./delinquency-bucket.component.scss']
})
export class DelinquencyBucketComponent implements OnInit {
delinquencyBucketData: any;
/** Columns to be displayed in delinquency bucket table. */
displayedColumns: string[] = ['name'];
/** Data source for delinquency bucket table. */
dataSource: MatTableDataSource<any>;
/** Paginator for delinquency bucket table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for delinquency bucket table. */
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor(private route: ActivatedRoute) {
this.route.data.subscribe(( data: { delinquencyBuckets: any }) => {
this.delinquencyBucketData = data.delinquencyBuckets;
});
}
ngOnInit(): void {
this.setDatasource();
}
/**
* Filters data in delinquency bucket table based on passed value.
* @param {string} filterValue Value to filter data.
*/
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
/**
* Initializes the data source, paginator and sorter for delinquency bucket table.
*/
setDatasource() {
this.dataSource = new MatTableDataSource(this.delinquencyBucketData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
}
@@ -0,0 +1,80 @@
<div class="container">
<mat-card>
<form [formGroup]="bucketForm" (ngSubmit)="submit()">
<mat-card-content>
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="100%">
<mat-label>Name</mat-label>
<input matInput required formControlName="name">
<mat-error *ngIf="bucketForm.controls.name.hasError('required')">
Name is <strong>required</strong>
</mat-error>
</mat-form-field>
<h3 fxFlex="40%" class="mat-h3">Delinquency Ranges</h3>
<div fxFlex="40%" fxLayout="row" fxLayoutAlign="start center">
<button type="button" mat-raised-button color="primary" (click)="addDelinquencyRange()">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table mat-table [dataSource]="rangesDataSource" [hidden]="rangesDataSource.length === 0">
<ng-container matColumnDef="classification">
<th mat-header-cell *matHeaderCellDef> Classification </th>
<td mat-cell *matCellDef="let row"> {{ row.classification }}</td>
<td mat-cell *matCellDef="let row">
{{ row.rangeId | find: delinquencyRangesData:'id':'classification'}} </td>
</ng-container>
<ng-container matColumnDef="minimumAgeDays">
<th mat-header-cell *matHeaderCellDef> Days From </th>
<td mat-cell *matCellDef="let row"> {{ row.minimumAgeDays }}</td>
<td mat-cell *matCellDef="let row">
{{ row.rangeId | find: delinquencyRangesData:'id':'minimumAgeDays'}} </td>
</ng-container>
<ng-container matColumnDef="maximumAgeDays">
<th mat-header-cell *matHeaderCellDef> Days Till </th>
<td mat-cell *matCellDef="let row"> {{ row.maximumAgeDays }}</td>
<td mat-cell *matCellDef="let row">
{{ row.rangeId | find: delinquencyRangesData:'id':'maximumAgeDays'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let row; let rowIndex = index">
<button type="button" mat-icon-button color="warn"
(click)="deleteDelinquencyRange(rowIndex)" matTooltip="Delete"
matTooltipPosition="left">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card-content>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!dataWasChanged" *mifosxHasPermission="'UPDATE_DELINQUENCY_BUCKET'">Submit</button>
</mat-card-actions>
</form>
</mat-card>
</div>
@@ -0,0 +1,10 @@
.container {
max-width: 37rem;
.content {
div {
margin: 0.8rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditBucketComponent } from './edit-bucket.component';
describe('EditBucketComponent', () => {
let component: EditBucketComponent;
let fixture: ComponentFixture<EditBucketComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EditBucketComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EditBucketComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,140 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
@Component({
selector: 'mifosx-edit-bucket',
templateUrl: './edit-bucket.component.html',
styleUrls: ['./edit-bucket.component.scss']
})
export class EditBucketComponent implements OnInit {
/** Delinquency Bucket form. */
bucketForm: FormGroup;
/** Delinquency Bucket template data. */
bucketTemplateData: any;
/** Delinquency Range Data Source */
rangesDataSource: {}[];
/** Delinquency Range Options */
delinquencyRangesData: any;
/** Delinquency Bucket data */
delinquencyBucketData: any;
delinquencyRangesIds: any;
/** Delinquency Bucket Id */
delinquencyBucketId: any;
/** Data was changed */
dataWasChanged = false;
/** Delinquency Range Displayed Columns */
displayedColumns: string[] = ['classification', 'minimumAgeDays', 'maximumAgeDays', 'actions'];
constructor(private formBuilder: FormBuilder,
private productsService: ProductsService,
private router: Router,
private route: ActivatedRoute,
public dialog: MatDialog) {
this.route.data.subscribe((data: { delinquencyBucket: any, delinquencyRanges: any }) => {
this.delinquencyRangesData = data.delinquencyRanges;
this.rangesDataSource = [];
this.delinquencyRangesIds = [];
this.delinquencyRangesData = this.delinquencyRangesData.sort(
(objA: { minimumAgeDays: number; }, objB: { minimumAgeDays: number; }) => objA.minimumAgeDays - objB.minimumAgeDays,
);
this.delinquencyBucketData = data.delinquencyBucket;
this.delinquencyBucketId = data.delinquencyBucket.id;
this.rangesDataSource = this.delinquencyBucketData.ranges;
this.rangesDataSource.forEach((item: any) => {
this.delinquencyRangesIds.push(item.id);
});
});
}
ngOnInit(): void {
this.setupForm();
}
/**
* Creates the Delinquency Bucket form
*/
setupForm(): void {
this.bucketForm = this.formBuilder.group({
'name': [{value: this.delinquencyBucketData.name, disabled: true}, Validators.required]
});
}
/**
* Add Delinquency Range for a Delinquency Bucket
*/
addDelinquencyRange() {
let delinquencyRanges = this.delinquencyRangesData;
if (this.delinquencyRangesIds.length > 0) {
delinquencyRanges = this.delinquencyRangesData.filter((item: any) => {
return (this.delinquencyRangesIds.indexOf(item.id) < 0);
});
}
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'rangeId',
label: 'Delinquency Range',
options: { label: 'classification', value: 'id', data: delinquencyRanges },
order: 1
})
];
const data = {
title: 'Add Delinquency Range',
layout: { addButtonText: 'Add' },
formfields: formfields
};
const rangeDialogRef = this.dialog.open(FormDialogComponent, { data });
rangeDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const item = response.data.value;
item.id = item.rangeId;
this.rangesDataSource = this.rangesDataSource.concat(item);
this.delinquencyRangesIds.push(item.id);
this.dataWasChanged = true;
}
});
}
/**
* Delete particular Delinquency Range in Delinquency Bucket
*/
deleteDelinquencyRange(index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.delinquencyRangesIds.splice(index, 1);
this.rangesDataSource.splice(index, 1);
this.rangesDataSource = this.rangesDataSource.concat([]);
this.dataWasChanged = true;
}
});
}
/**
* Submits the Delinquency Bucket form and creates the Delinquency Bucket,
* if successful redirects to Delinquency Buckets.
*/
submit() {
const ranges: any = [];
this.rangesDataSource.forEach((item: any) => { ranges.push(item.id); });
if (ranges.length > 0) {
const data = {
...this.bucketForm.value,
ranges: ranges
};
this.productsService.updateDelinquencyBucket(this.delinquencyBucketId, data).subscribe(() => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
}
}
@@ -0,0 +1,62 @@
<div class="container m-b-20" fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="end" fxLayoutGap="2%">
<button mat-raised-button color="primary" [routerLink]="['edit']" *mifosxHasPermission="'UPDATE_DELINQUENCY_BUCKET'">
<fa-icon icon="edit"></fa-icon>&nbsp;&nbsp;
Edit
</button>
<button mat-raised-button color="warn" (click)="deleteDelinquencyBucket()" *mifosxHasPermission="'DELETE_DELINQUENCY_BUCKET'">
<fa-icon icon="trash"></fa-icon>&nbsp;&nbsp;
Delete
</button>
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div fxLayout="row wrap" class="content">
<div fxFlex="50%" class="mat-body-strong">
Name
</div>
<div fxFlex="50%">
{{ delinquencyBucketData.name }}
</div>
<div fxFlex="50%" class="mat-body-strong">
Classification Name
</div>
<div fxFlex="25%" class="mat-body-strong">
Days From
</div>
<div fxFlex="25%" class="mat-body-strong">
Days Till
</div>
</div>
<div fxLayout="row wrap" class="content" *ngFor="let range of delinquencyBucketData.ranges">
<div fxFlex="50%">
{{ range.classification }}
</div>
<div fxFlex="25%">
{{ range.minimumAgeDays }}
</div>
<div fxFlex="25%">
{{ range.maximumAgeDays }}
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,10 @@
.container {
max-width: 37rem;
.content {
div {
margin: 0.8rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewBucketComponent } from './view-bucket.component';
describe('ViewBucketComponent', () => {
let component: ViewBucketComponent;
let fixture: ComponentFixture<ViewBucketComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ViewBucketComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ViewBucketComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,45 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
@Component({
selector: 'mifosx-view-bucket',
templateUrl: './view-bucket.component.html',
styleUrls: ['./view-bucket.component.scss']
})
export class ViewBucketComponent implements OnInit {
/** Delinquency Bucket Data. */
delinquencyBucketData: any;
constructor(private route: ActivatedRoute,
private router: Router,
private dialog: MatDialog,
private productsService: ProductsService) {
this.route.data.subscribe((data: { delinquencyBucket: any }) => {
this.delinquencyBucketData = data.delinquencyBucket;
this.delinquencyBucketData.ranges = this.delinquencyBucketData.ranges.sort(
(objA: { minimumAge: number; }, objB: { minimumAge: number; }) => objA.minimumAge - objB.minimumAge,
);
});
}
ngOnInit(): void {
}
deleteDelinquencyBucket() {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: this.delinquencyBucketData.name }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.productsService.deleteDelinquencyBucket(this.delinquencyBucketData.id).subscribe(() => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
});
}
}
@@ -0,0 +1,45 @@
<div class="container">
<mat-card>
<form [formGroup]="delinquencyRangeForm" (ngSubmit)="submit()">
<mat-card-content>
<div fxLayout="column">
<mat-form-field>
<mat-label>Classification</mat-label>
<input matInput required formControlName="classification">
<mat-error *ngIf="delinquencyRangeForm.controls.classification.hasError('required')">
Classification is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Days From</mat-label>
<input matInput type="number" required formControlName="minimumAgeDays">
<mat-error *ngIf="delinquencyRangeForm.controls.minimumAgeDays.hasError('required')">
Days From is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Days Till</mat-label>
<input matInput type="number" formControlName="maximumAgeDays">
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!delinquencyRangeForm.valid" *mifosxHasPermission="'CREATE_DELINQUENCY_RANGE'">Submit</button>
</mat-card-actions>
</form>
</mat-card>
</div>
@@ -0,0 +1,10 @@
.container {
max-width: 37rem;
.content {
div {
margin: 0.8rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateRangeComponent } from './create-range.component';
describe('CreateRangeComponent', () => {
let component: CreateRangeComponent;
let fixture: ComponentFixture<CreateRangeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CreateRangeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CreateRangeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,51 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
@Component({
selector: 'mifosx-create-range',
templateUrl: './create-range.component.html',
styleUrls: ['./create-range.component.scss']
})
export class CreateRangeComponent implements OnInit {
/** Delinquency Range form. */
delinquencyRangeForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private productsService: ProductsService,
private route: ActivatedRoute,
private router: Router,
private settingsService: SettingsService) {
}
ngOnInit(): void {
this.setInputForm();
}
/**
* Create Input form.
*/
setInputForm(): void {
this.delinquencyRangeForm = this.formBuilder.group({
'classification': ['', [Validators.required]],
'minimumAgeDays': [0, [Validators.required, Validators.pattern('^(0*[1-9][0-9]*?)$'), Validators.max(1000)]],
'maximumAgeDays': ['', [Validators.pattern('^(0*[1-9][0-9]*?)$'), Validators.max(10000)]],
});
}
submit(): void {
const delinquencyRangeFormData = this.delinquencyRangeForm.value;
const locale = this.settingsService.language.code;
const data = {
...delinquencyRangeFormData,
locale
};
this.productsService.createDelinquencyRange(data).subscribe((response: any) => {
this.router.navigate(['../', response.resourceId], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,45 @@
<div class="container m-b-20" fxLayout="row" fxLayoutAlign="end" fxLayoutGap="20px">
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_DELINQUENCY_RANGE'">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Create Delinquency Range
</button>
</div>
<div class="container">
<div fxLayout="row">
<mat-form-field fxFlex>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)">
</mat-form-field>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="classification">
<th mat-header-cell *matHeaderCellDef mat-sort-header="classification"> Classification </th>
<td mat-cell *matCellDef="let delinquencyRange"> {{ delinquencyRange.classification }} </td>
</ng-container>
<ng-container matColumnDef="minimumAgeDays">
<th mat-header-cell *matHeaderCellDef mat-sort-header="minimumAgeDays"> Days From </th>
<td mat-cell *matCellDef="let delinquencyRange"> {{ delinquencyRange.minimumAgeDays }} </td>
</ng-container>
<ng-container matColumnDef="maximumAgeDays">
<th mat-header-cell *matHeaderCellDef mat-sort-header="maximumAgeDays"> Days Till </th>
<td mat-cell *matCellDef="let delinquencyRange"> {{ delinquencyRange.maximumAgeDays }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="[row.id]" class="select-row"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
@@ -0,0 +1,35 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../../products.service';
/**
* Delinquency Range Component data resolver.
*/
@Injectable()
export class DelinquencyRangeComponentsResolver implements Resolve<Object> {
/**
* @param {ProductsService} productsService Products service.
*/
constructor(private productsService: ProductsService) {}
/**
* Returns the delinquency ranges data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const delinquentcyRangeId = route.paramMap.get('rangeId');
if (delinquentcyRangeId === null) {
return this.productsService.getDelinquencyRanges();
} else {
return this.productsService.getDelinquencyRange(delinquentcyRangeId);
}
}
}
@@ -0,0 +1,18 @@
.container {
.action-button {
margin-left: auto;
}
.search-button {
min-width: 30px;
margin: 4px;
}
}
table {
tr.select-row:hover {
cursor: pointer;
}
tr:nth-child(odd).td{
color : black;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DelinquencyRangeComponent } from './delinquency-range.component';
describe('DelinquencyRangeComponent', () => {
let component: DelinquencyRangeComponent;
let fixture: ComponentFixture<DelinquencyRangeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DelinquencyRangeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DelinquencyRangeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,51 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'mifosx-delinquency-range',
templateUrl: './delinquency-range.component.html',
styleUrls: ['./delinquency-range.component.scss']
})
export class DelinquencyRangeComponent implements OnInit {
delinquencyRangeData: any;
/** Columns to be displayed in delinquency range table. */
displayedColumns: string[] = ['classification', 'minimumAgeDays', 'maximumAgeDays'];
/** Data source for delinquency range table. */
dataSource: MatTableDataSource<any>;
/** Paginator for delinquency range table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for delinquency range table. */
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor(private route: ActivatedRoute) {
this.route.data.subscribe(( data: { delinquencyRanges: any }) => {
this.delinquencyRangeData = data.delinquencyRanges;
});
}
ngOnInit(): void {
this.setDatasource();
}
/**
* Filters data in delinquency range table based on passed value.
* @param {string} filterValue Value to filter data.
*/
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
/**
* Initializes the data source, paginator and sorter for delinquency range table.
*/
setDatasource() {
this.dataSource = new MatTableDataSource(this.delinquencyRangeData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
}
@@ -0,0 +1,45 @@
<div class="container">
<mat-card>
<form [formGroup]="delinquencyRangeForm" (ngSubmit)="submit()">
<mat-card-content>
<div fxLayout="column">
<mat-form-field>
<mat-label>Classification</mat-label>
<input matInput required formControlName="classification">
<mat-error *ngIf="delinquencyRangeForm.controls.classification.hasError('required')">
Classification is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Days From</mat-label>
<input matInput type="number" required formControlName="minimumAgeDays">
<mat-error *ngIf="delinquencyRangeForm.controls.minimumAgeDays.hasError('required')">
Days From is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Days Till</mat-label>
<input matInput type="number" formControlName="maximumAgeDays">
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!delinquencyRangeForm.valid" *mifosxHasPermission="'UPDATE_DELINQUENCY_RANGE'">Submit</button>
</mat-card-actions>
</form>
</mat-card>
</div>
@@ -0,0 +1,10 @@
.container {
max-width: 37rem;
.content {
div {
margin: 0.8rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditRangeComponent } from './edit-range.component';
describe('EditRangeComponent', () => {
let component: EditRangeComponent;
let fixture: ComponentFixture<EditRangeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EditRangeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EditRangeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,56 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
@Component({
selector: 'mifosx-edit-range',
templateUrl: './edit-range.component.html',
styleUrls: ['./edit-range.component.scss']
})
export class EditRangeComponent implements OnInit {
/** Delinquency Range Data. */
delinquencyRangeData: any;
/** Delinquency Range form. */
delinquencyRangeForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private productsService: ProductsService,
private route: ActivatedRoute,
private router: Router,
private settingsService: SettingsService) {
this.route.data.subscribe((data: { delinquencyRange: any }) => {
this.delinquencyRangeData = data.delinquencyRange;
});
}
ngOnInit(): void {
this.setInputForm();
}
/**
* Create Input form.
*/
setInputForm(): void {
this.delinquencyRangeForm = this.formBuilder.group({
'classification': [this.delinquencyRangeData.classification, [Validators.required]],
'minimumAgeDays': [this.delinquencyRangeData.minimumAgeDays, [Validators.required, Validators.pattern('^(0*[1-9][0-9]*?)$'), Validators.max(1000)]],
'maximumAgeDays': [this.delinquencyRangeData.maximumAgeDays, [Validators.pattern('^(0*[1-9][0-9]*?)$'), Validators.max(10000)]],
});
}
submit(): void {
const delinquencyRangeFormData = this.delinquencyRangeForm.value;
const locale = this.settingsService.language.code;
const data = {
...delinquencyRangeFormData,
locale
};
this.productsService.updateDelinquencyRange(this.delinquencyRangeData.id, data).subscribe((response: any) => {
this.router.navigate(['../../', response.resourceId], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,50 @@
<div class="container m-b-20" fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="end" fxLayoutGap="2%">
<button mat-raised-button color="primary" [routerLink]="['edit']" *mifosxHasPermission="'UPDATE_DELINQUENCY_RANGE'">
<fa-icon icon="edit"></fa-icon>&nbsp;&nbsp;
Edit
</button>
<button mat-raised-button color="warn" (click)="deleteDelinquencyRange()" *mifosxHasPermission="'DELETE_DELINQUENCY_RANGE'">
<fa-icon icon="trash"></fa-icon>&nbsp;&nbsp;
Delete
</button>
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div fxLayout="row wrap" class="content">
<div fxFlex="50%" class="mat-body-strong">
Classification
</div>
<div fxFlex="50%">
{{ delinquencyRangeData.classification }}
</div>
<div fxFlex="50%" class="mat-body-strong">
Days From
</div>
<div fxFlex="50%">
{{ delinquencyRangeData.minimumAgeDays }}
</div>
<div fxFlex="50%" class="mat-body-strong">
Days Till
</div>
<div fxFlex="50%">
{{ delinquencyRangeData.maximumAgeDays }}
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,10 @@
.container {
max-width: 37rem;
.content {
div {
margin: 0.8rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewRangeComponent } from './view-range.component';
describe('ViewRangeComponent', () => {
let component: ViewRangeComponent;
let fixture: ComponentFixture<ViewRangeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ViewRangeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ViewRangeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductsService } from 'app/products/products.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
@Component({
selector: 'mifosx-view-range',
templateUrl: './view-range.component.html',
styleUrls: ['./view-range.component.scss']
})
export class ViewRangeComponent implements OnInit {
/** Delinquency Range Data. */
delinquencyRangeData: any;
constructor(private route: ActivatedRoute,
private router: Router,
private dialog: MatDialog,
private productsService: ProductsService) {
this.route.data.subscribe((data: { delinquencyRange: any }) => {
this.delinquencyRangeData = data.delinquencyRange;
});
}
ngOnInit(): void {
}
deleteDelinquencyRange() {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: this.delinquencyRangeData.classification }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.productsService.deleteDelinquencyRange(this.delinquencyRangeData.id).subscribe(() => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
});
}
}
@@ -0,0 +1,43 @@
<div class="container">
<mat-card>
<div fxLayout="row" fxLayout.lt-md="column">
<div fxFlex="50%">
<mat-nav-list>
<mat-list-item [routerLink]="['ranges']" *mifosxHasPermission="'READ_DELINQUENCY_RANGE'">
<mat-icon matListIcon>
<fa-icon icon="building" size="sm"></fa-icon>
</mat-icon>
<h4 matLine>Manage Delinquency Ranges</h4>
<p matLine>Define delinquency day ranges</p>
</mat-list-item>
</mat-nav-list>
</div>
<div fxFlex="50%">
<mat-nav-list>
<mat-list-item [routerLink]="['buckets']" *mifosxHasPermission="'READ_DELINQUENCY_BUCKET'">
<mat-icon matListIcon>
<fa-icon icon="building" size="sm"></fa-icon>
</mat-icon>
<h4 matLine>Manage Delinquency Buckets</h4>
<p matLine>Define delinquency bucket as set of ranges</p>
</mat-list-item>
</mat-nav-list>
</div>
</div>
</mat-card>
</div>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageDelinquencyBucketsComponent } from './manage-delinquency-buckets.component';
describe('ManageDelinquencyBucketsComponent', () => {
let component: ManageDelinquencyBucketsComponent;
let fixture: ComponentFixture<ManageDelinquencyBucketsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ManageDelinquencyBucketsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ManageDelinquencyBucketsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'mifosx-manage-delinquency-buckets',
templateUrl: './manage-delinquency-buckets.component.html',
styleUrls: ['./manage-delinquency-buckets.component.scss']
})
export class ManageDelinquencyBucketsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
+112 -1
View File
@@ -101,6 +101,17 @@ import { FixedDepositProductAndTemplateResolver } from './fixed-deposit-products
import { CollateralResolver } from './collaterals/collateral.resolver';
import { CollateralsResolver } from './collaterals/collaterals.resolver';
import { CollateralTemplateResolver } from './collaterals/collaterals-template.resolver';
import { ManageDelinquencyBucketsComponent } from './manage-delinquency-buckets/manage-delinquency-buckets.component';
import { DelinquencyBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component';
import { DelinquencyRangeComponent } from './manage-delinquency-buckets/delinquency-range/delinquency-range.component';
import { DelinquencyBucketComponentsResolver } from './manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component.resolver';
import { DelinquencyRangeComponentsResolver } from './manage-delinquency-buckets/delinquency-range/delinquency-range.component.resolver';
import { ViewRangeComponent } from './manage-delinquency-buckets/delinquency-range/view-range/view-range.component';
import { EditRangeComponent } from './manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component';
import { CreateRangeComponent } from './manage-delinquency-buckets/delinquency-range/create-range/create-range.component';
import { CreateBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component';
import { EditBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component';
import { ViewBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component';
/** Products Routes */
const routes: Routes = [
@@ -377,6 +388,104 @@ const routes: Routes = [
},
]
},
{
path: 'delinquency-bucket-configurations',
data: { title: extract('Manage Delinquency Bucket Configurations'), breadcrumb: 'Manage Delinquency Bucket Configurations' },
children: [
{
path: '',
component: ManageDelinquencyBucketsComponent,
},
{
path: 'ranges',
data: { title: extract('Manage Delinquency Ranges'), breadcrumb: 'Delinquency Ranges'},
children: [
{
path: '',
component: DelinquencyRangeComponent,
resolve: {
delinquencyRanges: DelinquencyRangeComponentsResolver
}
},
{
path: 'create',
component: CreateRangeComponent,
data: { title: extract('Create Delinquency Range'), breadcrumb: 'Create' }
},
{
path: ':rangeId',
data: { title: extract('View Delinquency Range'), routeParamBreadcrumb: 'id' },
resolve: {
delinquencyRange: DelinquencyRangeComponentsResolver
},
children: [
{
path: '',
component: ViewRangeComponent,
resolve: {
delinquencyRange: DelinquencyRangeComponentsResolver
},
},
{
path: 'edit',
data: { title: extract('Edit Delinquency Range'), breadcrumb: 'Edit', routeParamBreadcrumb: false },
component: EditRangeComponent,
resolve: {
delinquencyRange: DelinquencyRangeComponentsResolver
}
},
]
}
]
},
{
path: 'buckets',
data: { title: extract('Manage Delinquency Bucket'), breadcrumb: 'Delinquency Buckets'},
children: [
{
path: '',
component: DelinquencyBucketComponent,
resolve: {
delinquencyBuckets: DelinquencyBucketComponentsResolver
}
},
{
path: 'create',
component: CreateBucketComponent,
data: { title: extract('Create Delinquency Bucket'), breadcrumb: 'Create' },
resolve: {
delinquencyRanges: DelinquencyRangeComponentsResolver
}
},
{
path: ':bucketId',
data: { title: extract('View Delinquency Bucket'), routeParamBreadcrumb: 'id' },
resolve: {
delinquencyBucket: DelinquencyBucketComponentsResolver
},
children: [
{
path: '',
component: ViewBucketComponent,
resolve: {
delinquencyBucket: DelinquencyBucketComponentsResolver
},
},
{
path: 'edit',
data: { title: extract('Edit Delinquency Bucket'), breadcrumb: 'Edit', routeParamBreadcrumb: false },
component: EditBucketComponent,
resolve: {
delinquencyBucket: DelinquencyBucketComponentsResolver,
delinquencyRanges: DelinquencyRangeComponentsResolver
}
},
]
}
]
},
]
},
{
path: 'recurring-deposit-products',
data: { title: extract('Recurring Deposit Products'), breadcrumb: 'Recurring Deposit Products' },
@@ -703,7 +812,9 @@ const routes: Routes = [
FloatingRatesResolver,
CollateralResolver,
CollateralsResolver,
CollateralTemplateResolver
CollateralTemplateResolver,
DelinquencyRangeComponentsResolver,
DelinquencyBucketComponentsResolver
]
})
export class ProductsRoutingModule { }
+8
View File
@@ -48,6 +48,14 @@
<p matLine>Define collaterals for Collateral Management</p>
</mat-list-item>
<mat-list-item [routerLink]="['delinquency-bucket-configurations']" *mifosxHasPermission="'READ_DELINQUENCY_BUCKET'">
<mat-icon matListIcon>
<fa-icon icon="briefcase" size="sm"></fa-icon>
</mat-icon>
<h4 matLine>Delinquency Buckets</h4>
<p matLine>Define delinquency day ranges and bucket set for loan products</p>
</mat-list-item>
</mat-nav-list>
</div>
+19 -1
View File
@@ -97,6 +97,15 @@ import { CollateralsComponent } from './collaterals/collaterals.component';
import { CreateCollateralComponent } from './collaterals/create-collateral/create-collateral.component';
import { EditCollateralComponent } from './collaterals/edit-collateral/edit-collateral.component';
import { ViewCollateralComponent } from './collaterals/view-collateral/view-collateral.component';
import { ManageDelinquencyBucketsComponent } from './manage-delinquency-buckets/manage-delinquency-buckets.component';
import { DelinquencyBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component';
import { DelinquencyRangeComponent } from './manage-delinquency-buckets/delinquency-range/delinquency-range.component';
import { CreateRangeComponent } from './manage-delinquency-buckets/delinquency-range/create-range/create-range.component';
import { ViewRangeComponent } from './manage-delinquency-buckets/delinquency-range/view-range/view-range.component';
import { EditRangeComponent } from './manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component';
import { ViewBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component';
import { CreateBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component';
import { EditBucketComponent } from './manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component';
/**
* Products Module
@@ -201,7 +210,16 @@ import { ViewCollateralComponent } from './collaterals/view-collateral/view-coll
CollateralsComponent,
CreateCollateralComponent,
EditCollateralComponent,
ViewCollateralComponent
ViewCollateralComponent,
ManageDelinquencyBucketsComponent,
DelinquencyBucketComponent,
DelinquencyRangeComponent,
CreateRangeComponent,
ViewRangeComponent,
EditRangeComponent,
ViewBucketComponent,
CreateBucketComponent,
EditBucketComponent
],
providers: [ ]
})
+80
View File
@@ -299,6 +299,86 @@ export class ProductsService {
return this.http.put(`/taxes/group/${taxGroupId}`, taxGroup);
}
/**
* @returns {Observable<any>} Delinquency Range data
*/
getDelinquencyRanges(): Observable<any> {
return this.http.get('/delinquency/ranges');
}
/**
* @param {any} delinquencyRateId Delinquency Range Id.
* @returns {Observable<any>} Delinquency Range data.
*/
getDelinquencyRange(delinquencyRateId: any): Observable<any> {
return this.http.get(`/delinquency/ranges/${delinquencyRateId}`);
}
/**
* @param {any} payload Delinquency Range data
* @returns {Observable<any>} Delinquency Range Resource Id
*/
createDelinquencyRange(payload: any): Observable<any> {
return this.http.post('/delinquency/ranges', payload);
}
/**
* @param {any} delinquencyRateId Delinquency Range ID
* @param {any} payload Delinquency Range data
* @returns {Observable<any>} Changes in the Delinquency Range
*/
updateDelinquencyRange(delinquencyRateId: any, payload: any): Observable<any> {
return this.http.put(`/delinquency/ranges/${delinquencyRateId}`, payload);
}
/**
* @param {any} delinquencyRateId Delinquency Range ID
* @returns {Observable<any>} Changes in the Delinquency Range
*/
deleteDelinquencyRange(delinquencyRateId: any): Observable<any> {
return this.http.delete(`/delinquency/ranges/${delinquencyRateId}`);
}
/**
* @returns {Observable<any>} Delinquency Bucket data
*/
getDelinquencyBuckets(): Observable<any> {
return this.http.get('/delinquency/buckets');
}
/**
* @param {any} delinquencyBucketId Delinquency Bucket Id.
* @returns {Observable<any>} Delinquency Bucket data.
*/
getDelinquencyBucket(delinquencyBucketId: any): Observable<any> {
return this.http.get(`/delinquency/buckets/${delinquencyBucketId}`);
}
/**
* @param {any} payload Delinquency Bucket data
* @returns {Observable<any>} Delinquency Bucket Resource Id
*/
createDelinquencyBucket(payload: any): Observable<any> {
return this.http.post('/delinquency/buckets', payload);
}
/**
* @param {any} delinquencyBucketId Delinquency Bucket ID
* @param {any} payload Delinquency Bucket data
* @returns {Observable<any>} Changes in the Delinquency Bucket
*/
updateDelinquencyBucket(delinquencyBucketId: any, payload: any): Observable<any> {
return this.http.put(`/delinquency/buckets/${delinquencyBucketId}`, payload);
}
/**
* @param {any} delinquencyBucketId Delinquency Bucket ID
* @returns {Observable<any>} Changes in the Delinquency Bucket
*/
deleteDelinquencyBucket(delinquencyBucketId: any): Observable<any> {
return this.http.delete(`/delinquency/buckets/${delinquencyBucketId}`);
}
/**
* @returns {Observable<any>} Product mixes data
*/