feat: add create loan product component (#434)

- Add loan product stepper components
- Add create loan product component
- Add charges penalty filter pipe
- Add charges filter pipe
- Add find pipe

Fixes: #188
This commit is contained in:
Abhay Chawla
2019-12-01 23:20:51 +03:00
parent 620c508edb
commit 97ccce531b
47 changed files with 3779 additions and 5 deletions
@@ -0,0 +1,8 @@
import { ChargesFilterPipe } from './charges-filter.pipe';
describe('ChargesFilterPipe', () => {
it('create an instance', () => {
const pipe = new ChargesFilterPipe();
expect(pipe).toBeTruthy();
});
});
+20
View File
@@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'chargesFilter'
})
export class ChargesFilterPipe implements PipeTransform {
transform(charges: any, chargesDataSource: any, currencyCode: string, multiDisburseLoan?: boolean): any {
if (charges) {
charges = charges.filter((charge: any) => {
if ((charge.currency.code !== currencyCode) || (!!multiDisburseLoan && charge.chargeTimeType.id === 12) || chargesDataSource.includes(charge)) {
return false;
}
return true;
});
}
return charges;
}
}
@@ -0,0 +1,8 @@
import { ChargesPenaltyFilterPipe } from './charges-penalty-filter.pipe';
describe('ChargesPenaltyFilterPipe', () => {
it('create an instance', () => {
const pipe = new ChargesPenaltyFilterPipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'chargesPenaltyFilter'
})
export class ChargesPenaltyFilterPipe implements PipeTransform {
transform(charges: any, penalty: boolean): any {
if (charges) {
charges = charges.filter((charge: any) => {
if (charge.penalty === penalty) {
return true;
}
return false;
});
}
return charges;
}
}
+8
View File
@@ -0,0 +1,8 @@
import { FindPipe } from './find.pipe';
describe('FindPipe', () => {
it('create an instance', () => {
const pipe = new FindPipe();
expect(pipe).toBeTruthy();
});
});
+13
View File
@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'find'
})
export class FindPipe implements PipeTransform {
transform(value: any, options: any, key: string, property: string): any {
const optionFound = options.find((option: any) => option[key] === value);
return optionFound ? optionFound[property] : '';
}
}
+6 -5
View File
@@ -2,15 +2,16 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StatusLookupPipe } from './status-lookup.pipe';
import { AccountsFilterPipe } from './accounts-filter.pipe';
import { ChargesFilterPipe } from './charges-filter.pipe';
import { ChargesPenaltyFilterPipe } from './charges-penalty-filter.pipe';
import { FindPipe } from './find.pipe';
@NgModule({
imports: [
CommonModule
],
declarations: [StatusLookupPipe, AccountsFilterPipe],
providers: [StatusLookupPipe, AccountsFilterPipe],
exports: [StatusLookupPipe, AccountsFilterPipe]
declarations: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe],
providers: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe],
exports: [StatusLookupPipe, AccountsFilterPipe, ChargesFilterPipe, ChargesPenaltyFilterPipe, FindPipe]
})
export class PipesModule { }
@@ -0,0 +1,102 @@
<div class="container">
<mat-horizontal-stepper class="mat-elevation-z8" labelPosition="bottom" #loanProductStepper>
<ng-template matStepperIcon="number">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="done">
<fa-icon icon="check" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="error">
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
</ng-template>
<ng-template matStepperIcon="preview">
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="loanProductDetailsForm">
<ng-template matStepLabel>DETAILS</ng-template>
<mifosx-loan-product-details-step [loanProductsTemplate]="loanProductsTemplate"></mifosx-loan-product-details-step>
</mat-step>
<mat-step [stepControl]="loanProductCurrencyForm">
<ng-template matStepLabel>CURRENCY</ng-template>
<mifosx-loan-product-currency-step [loanProductsTemplate]="loanProductsTemplate"></mifosx-loan-product-currency-step>
</mat-step>
<mat-step [stepControl]="loanProductTermsForm">
<ng-template matStepLabel>TERMS</ng-template>
<mifosx-loan-product-terms-step [loanProductsTemplate]="loanProductsTemplate"></mifosx-loan-product-terms-step>
</mat-step>
<mat-step [stepControl]="loanProductSettingsForm">
<ng-template matStepLabel>SETTINGS</ng-template>
<mifosx-loan-product-settings-step
[loanProductsTemplate]="loanProductsTemplate"
[isLinkedToFloatingInterestRates]="loanProductTermsForm.get('isLinkedToFloatingInterestRates')"
>
</mifosx-loan-product-settings-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>CHARGES</ng-template>
<mifosx-loan-product-charges-step
[loanProductsTemplate]="loanProductsTemplate"
[currencyCode]="loanProductCurrencyForm.value.currencyCode"
[multiDisburseLoan]="loanProductSettingsForm.value.multiDisburseLoan"
>
</mifosx-loan-product-charges-step>
</mat-step>
<mat-step [stepControl]="loanProductAccountingForm">
<ng-template matStepLabel>ACCOUNTING</ng-template>
<mifosx-loan-product-accounting-step
[loanProductsTemplate]="loanProductsTemplate"
[accountingRuleData]="accountingRuleData"
>
</mifosx-loan-product-accounting-step>
</mat-step>
<mat-step state="preview" *ngIf="loanProductFormValid" completed>
<ng-template matStepLabel>PREVIEW</ng-template>
<mifosx-loan-product-preview-step
[loanProductsTemplate]="loanProductsTemplate"
[accountingRuleData]="accountingRuleData"
[loanProduct]="loanProduct"
(submit)="submit()"
>
</mifosx-loan-product-preview-step>
</mat-step>
</mat-horizontal-stepper>
</div>
@@ -0,0 +1,4 @@
fa-icon {
position: relative;
left: 5%;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateLoanProductComponent } from './create-loan-product.component';
describe('CreateLoanProductComponent', () => {
let component: CreateLoanProductComponent;
let fixture: ComponentFixture<CreateLoanProductComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CreateLoanProductComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateLoanProductComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,99 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { LoanProductDetailsStepComponent } from '../loan-product-stepper/loan-product-details-step/loan-product-details-step.component';
import { LoanProductCurrencyStepComponent } from '../loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component';
import { LoanProductTermsStepComponent } from '../loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component';
import { LoanProductSettingsStepComponent } from '../loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component';
import { LoanProductChargesStepComponent } from '../loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component';
import { LoanProductAccountingStepComponent } from '../loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component';
import { ProductsService } from 'app/products/products.service';
@Component({
selector: 'mifosx-create-loan-product',
templateUrl: './create-loan-product.component.html',
styleUrls: ['./create-loan-product.component.scss']
})
export class CreateLoanProductComponent implements OnInit {
@ViewChild(LoanProductDetailsStepComponent) loanProductDetailsStep: LoanProductDetailsStepComponent;
@ViewChild(LoanProductCurrencyStepComponent) loanProductCurrencyStep: LoanProductCurrencyStepComponent;
@ViewChild(LoanProductTermsStepComponent) loanProductTermsStep: LoanProductTermsStepComponent;
@ViewChild(LoanProductSettingsStepComponent) loanProductSettingsStep: LoanProductSettingsStepComponent;
@ViewChild(LoanProductChargesStepComponent) loanProductChargesStep: LoanProductChargesStepComponent;
@ViewChild(LoanProductAccountingStepComponent) loanProductAccountingStep: LoanProductAccountingStepComponent;
loanProductsTemplate: any;
accountingRuleData = ['None', 'Cash', 'Accrual (periodic)', 'Accrual (upfront)'];
constructor(private route: ActivatedRoute,
private productsService: ProductsService,
private router: Router) {
this.route.data.subscribe((data: { loanProductsTemplate: any }) => {
this.loanProductsTemplate = data.loanProductsTemplate;
});
}
ngOnInit() {
}
get loanProductDetailsForm() {
return this.loanProductDetailsStep.loanProductDetailsForm;
}
get loanProductCurrencyForm() {
return this.loanProductCurrencyStep.loanProductCurrencyForm;
}
get loanProductTermsForm() {
return this.loanProductTermsStep.loanProductTermsForm;
}
get loanProductSettingsForm() {
return this.loanProductSettingsStep.loanProductSettingsForm;
}
get loanProductAccountingForm() {
return this.loanProductAccountingStep.loanProductAccountingForm;
}
get loanProductFormValid() {
return (
this.loanProductDetailsForm.valid &&
this.loanProductCurrencyForm.valid &&
this.loanProductTermsForm.valid &&
this.loanProductSettingsForm.valid &&
this.loanProductAccountingForm.valid
);
}
get loanProduct() {
return {
...this.loanProductDetailsStep.loanProductDetails,
...this.loanProductCurrencyStep.loanProductCurrency,
...this.loanProductTermsStep.loanProductTerms,
...this.loanProductSettingsStep.loanProductSettings,
...this.loanProductChargesStep.charges,
...this.loanProductAccountingStep.loanProductAccounting
};
}
submit() {
// TODO: Update once language and date settings are setup
const dateFormat = 'yyyy-MM-dd';
const loanProduct = {
...this.loanProduct,
charges: this.loanProduct.charges.map((charge: any) => ({ id: charge.id })),
dateFormat,
locale: 'en'
};
delete loanProduct.allowAttributeConfiguration;
delete loanProduct.advancedAccountingRules;
this.productsService.createLoanProduct(loanProduct)
.subscribe((response: any) => {
this.router.navigate(['../', response.resourceId], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,329 @@
<form [formGroup]="loanProductAccountingForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-radio-group fxFlex="98%" fxLayout="row" fxLayoutGap="5%" fxLayout.lt-md="column" formControlName="accountingRule">
<mat-radio-button *ngFor="let accountingRule of accountingRuleData; let i = index" [value]="i+1">
{{ accountingRule }}
</mat-radio-button>
</mat-radio-group>
<mat-divider fxFlex="98%"></mat-divider>
<div *ngIf="loanProductAccountingForm.value.accountingRule >= 2 && loanProductAccountingForm.value.accountingRule <= 4" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="98%" class="mat-h4">Assets</h4>
<mat-form-field fxFlex="48%">
<mat-label>Fund source</mat-label>
<mat-select formControlName="fundSourceAccountId" required>
<mat-option *ngFor="let assetAccount of assetAccountData" [value]="assetAccount.id">
{{ assetAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Fund source is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Loan portfolio</mat-label>
<mat-select formControlName="loanPortfolioAccountId" required>
<mat-option *ngFor="let assetAccount of assetAccountData" [value]="assetAccount.id">
{{ assetAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Loan portfolio is <strong>required</strong>
</mat-error>
</mat-form-field>
<div *ngIf="loanProductAccountingForm.value.accountingRule === 3 || loanProductAccountingForm.value.accountingRule === 4" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Interest Receivable</mat-label>
<mat-select formControlName="receivableInterestAccountId" required>
<mat-option *ngFor="let assetAccount of assetAccountData" [value]="assetAccount.id">
{{ assetAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Interest Receivable is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Fees Receivable</mat-label>
<mat-select formControlName="receivableFeeAccountId" required>
<mat-option *ngFor="let assetAccount of assetAccountData" [value]="assetAccount.id">
{{ assetAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Fees Receivable is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Penalties Receivable</mat-label>
<mat-select formControlName="receivablePenaltyAccountId" required>
<mat-option *ngFor="let assetAccount of assetAccountData" [value]="assetAccount.id">
{{ assetAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Penalties Receivable is <strong>required</strong>
</mat-error>
</mat-form-field>
</div>
<mat-form-field fxFlex="48%">
<mat-label>Transfer in suspense</mat-label>
<mat-select formControlName="transfersInSuspenseAccountId" required>
<mat-option *ngFor="let assetAccount of assetAccountData" [value]="assetAccount.id">
{{ assetAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Transfer in suspense is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<h4 fxFlex="98%" class="mat-h4">Income</h4>
<mat-form-field fxFlex="48%">
<mat-label>Income from Interest</mat-label>
<mat-select formControlName="interestOnLoanAccountId" required>
<mat-option *ngFor="let incomeAccount of incomeAccountData" [value]="incomeAccount.id">
{{ incomeAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Income from Interest is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Income from fees</mat-label>
<mat-select formControlName="incomeFromFeeAccountId" required>
<mat-option *ngFor="let incomeAccount of incomeAccountData" [value]="incomeAccount.id">
{{ incomeAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Income from fees is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Income from penalties</mat-label>
<mat-select formControlName="incomeFromPenaltyAccountId" required>
<mat-option *ngFor="let incomeAccount of incomeAccountData" [value]="incomeAccount.id">
{{ incomeAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Income from penalties is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Income from Recovery Repayments</mat-label>
<mat-select formControlName="incomeFromRecoveryAccountId" required>
<mat-option *ngFor="let incomeAccount of incomeAccountData" [value]="incomeAccount.id">
{{ incomeAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Income from Recovery Repayments is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<h4 fxFlex="98%" class="mat-h4">Expenses</h4>
<mat-form-field fxFlex="48%">
<mat-label>Losses written off</mat-label>
<mat-select formControlName="writeOffAccountId" required>
<mat-option *ngFor="let expenseAccount of expenseAccountData" [value]="expenseAccount.id">
{{ expenseAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Losses written off is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<h4 fxFlex="98%" class="mat-h4">Liabilities</h4>
<mat-form-field fxFlex="48%">
<mat-label>Over payment liability</mat-label>
<mat-select formControlName="overpaymentLiabilityAccountId" required>
<mat-option *ngFor="let liabilityAccount of liabilityAccountData" [value]="liabilityAccount.id">
{{ liabilityAccount.name }}
</mat-option>
</mat-select>
<mat-error>
Over payment liability is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Advanced Accounting Rules</h3>
<mat-checkbox fxFlex="73%" formControlName="advancedAccountingRules"></mat-checkbox>
<div *ngIf="loanProductAccountingForm.value.advancedAccountingRules" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="33%" class="mat-h4">Configure Fund Sources for Payment Channels</h4>
<div fxFlex="63%">
<button type="button" mat-raised-button color="primary" (click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="paymentChannelToFundSourceMappings.value" *ngIf="paymentChannelToFundSourceMappings.value.length !== 0">
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef> Payment Type </th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find:paymentTypeData:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef> Fund Source </th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccountId | find:assetAccountData:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let paymentFundSource; let i = index">
<button mat-icon-button color="primary" (click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(paymentChannelToFundSourceMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns;"></tr>
</table>
<h4 fxFlex="33%" class="mat-h4">Map Fees to Specific Income Accounts</h4>
<div fxFlex="63%">
<button type="button" mat-raised-button color="primary" (click)="add('FeesIncome', feeToIncomeAccountMappings)">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="feeToIncomeAccountMappings.value" *ngIf="feeToIncomeAccountMappings.value.length !== 0">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef> Fees </th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.chargeId | find:chargeData:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef> Income Account </th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccountId | find:incomeAndLiabilityAccountData:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let feesIncome; let i = index">
<button mat-icon-button color="primary" (click)="edit('FeesIncome', feeToIncomeAccountMappings, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(feeToIncomeAccountMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns;"></tr>
</table>
<h4 fxFlex="33%" class="mat-h4">Map Penalties to Specific Income Accounts</h4>
<div fxFlex="63%">
<button type="button" mat-raised-button color="primary" (click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="penaltyToIncomeAccountMappings.value" *ngIf="penaltyToIncomeAccountMappings.value.length !== 0">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef> Penalty </th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find:penaltyData:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef> Income Account </th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccountId | find:incomeAccountData:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let penaltyIncome; let i = index">
<button mat-icon-button color="primary" (click)="edit('PenaltyIncome', penaltyToIncomeAccountMappings, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(penaltyToIncomeAccountMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns;"></tr>
</table>
</div>
</div>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button matStepperNext>
Next&nbsp;&nbsp;
<fa-icon icon="arrow-right"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,23 @@
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em 0;
}
h3, h4 {
font-weight: 500;
}
mat-radio-group, h4 {
margin: 1em 0;
}
mat-divider {
margin: 1em 0 2em 0;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductAccountingStepComponent } from './loan-product-accounting-step.component';
describe('LoanProductAccountingStepComponent', () => {
let component: LoanProductAccountingStepComponent;
let fixture: ComponentFixture<LoanProductAccountingStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductAccountingStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductAccountingStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,231 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, FormArray, Validators, FormControl } from '@angular/forms';
import { MatDialog } from '@angular/material';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-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-loan-product-accounting-step',
templateUrl: './loan-product-accounting-step.component.html',
styleUrls: ['./loan-product-accounting-step.component.scss']
})
export class LoanProductAccountingStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
@Input() accountingRuleData: any;
loanProductAccountingForm: FormGroup;
chargeData: any;
penaltyData: any;
paymentTypeData: any;
assetAccountData: any;
incomeAccountData: any;
expenseAccountData: any;
liabilityAccountData: any;
incomeAndLiabilityAccountData: any;
paymentFundSourceDisplayedColumns: string[] = ['paymentTypeId', 'fundSourceAccountId', 'actions'];
feesPenaltyIncomeDisplayedColumns: string[] = ['chargeId', 'incomeAccountId', 'actions'];
constructor(private formBuilder: FormBuilder,
public dialog: MatDialog) {
this.createLoanProductAccountingForm();
this.setConditionalControls();
}
ngOnInit() {
this.chargeData = this.loanProductsTemplate.chargeOptions || [];
this.penaltyData = this.loanProductsTemplate.penaltyOptions || [];
this.paymentTypeData = this.loanProductsTemplate.paymentTypeOptions || [];
this.assetAccountData = this.loanProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
this.incomeAccountData = this.loanProductsTemplate.accountingMappingOptions.incomeAccountOptions || [];
this.expenseAccountData = this.loanProductsTemplate.accountingMappingOptions.expenseAccountOptions || [];
this.liabilityAccountData = this.loanProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
this.incomeAndLiabilityAccountData = this.incomeAccountData.concat(this.liabilityAccountData);
}
createLoanProductAccountingForm() {
this.loanProductAccountingForm = this.formBuilder.group({
'accountingRule': [1]
});
}
setConditionalControls() {
this.loanProductAccountingForm.get('accountingRule').valueChanges
.subscribe((accountingRule: any) => {
if (accountingRule >= 2 && accountingRule <= 4) {
this.loanProductAccountingForm.addControl('fundSourceAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('loanPortfolioAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('transfersInSuspenseAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('interestOnLoanAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('incomeFromFeeAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('incomeFromPenaltyAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('incomeFromRecoveryAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('writeOffAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('overpaymentLiabilityAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('advancedAccountingRules', new FormControl(false));
this.loanProductAccountingForm.get('advancedAccountingRules').valueChanges
.subscribe((advancedAccountingRules: boolean) => {
if (advancedAccountingRules) {
this.loanProductAccountingForm.addControl('paymentChannelToFundSourceMappings', this.formBuilder.array([]));
this.loanProductAccountingForm.addControl('feeToIncomeAccountMappings', this.formBuilder.array([] ));
this.loanProductAccountingForm.addControl('penaltyToIncomeAccountMappings', this.formBuilder.array([]));
} else {
this.loanProductAccountingForm.removeControl('paymentChannelToFundSourceMappings');
this.loanProductAccountingForm.removeControl('feeToIncomeAccountMappings');
this.loanProductAccountingForm.removeControl('penaltyToIncomeAccountMappings');
}
});
} else {
this.loanProductAccountingForm.removeControl('fundSourceAccountId');
this.loanProductAccountingForm.removeControl('loanPortfolioAccountId');
this.loanProductAccountingForm.removeControl('transfersInSuspenseAccountId');
this.loanProductAccountingForm.removeControl('interestOnLoanAccountId');
this.loanProductAccountingForm.removeControl('incomeFromFeeAccountId');
this.loanProductAccountingForm.removeControl('incomeFromPenaltyAccountId');
this.loanProductAccountingForm.removeControl('incomeFromRecoveryAccountId');
this.loanProductAccountingForm.removeControl('writeOffAccountId');
this.loanProductAccountingForm.removeControl('overpaymentLiabilityAccountId');
this.loanProductAccountingForm.removeControl('advancedAccountingRules');
}
if (accountingRule === 3 || accountingRule === 4) {
this.loanProductAccountingForm.addControl('receivableInterestAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('receivableFeeAccountId', new FormControl('', Validators.required));
this.loanProductAccountingForm.addControl('receivablePenaltyAccountId', new FormControl('', Validators.required));
} else {
this.loanProductAccountingForm.removeControl('receivableInterestAccountId');
this.loanProductAccountingForm.removeControl('receivableFeeAccountId');
this.loanProductAccountingForm.removeControl('receivablePenaltyAccountId');
}
});
}
get paymentChannelToFundSourceMappings(): FormArray {
return this.loanProductAccountingForm.get('paymentChannelToFundSourceMappings') as FormArray;
}
get feeToIncomeAccountMappings(): FormArray {
return this.loanProductAccountingForm.get('feeToIncomeAccountMappings') as FormArray;
}
get penaltyToIncomeAccountMappings(): FormArray {
return this.loanProductAccountingForm.get('penaltyToIncomeAccountMappings') as FormArray;
}
add(formType: string, formArray: FormArray) {
const data = this.getData(formType);
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
formArray.push(response.data);
}
});
}
edit(formType: string, formArray: FormArray, index: number) {
const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } };
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
formArray.at(index).patchValue(response.data.value);
}
});
}
delete(formArray: FormArray, index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
formArray.removeAt(index);
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case 'PaymentFundSource': return { title: 'Configure Fund Sources for Payment Channels', formfields: this.getPaymentFundSourceFormfields(values) };
case 'FeesIncome': return { title: 'Map Fees to Income Accounts', formfields: this.getFeesIncomeFormfields(values) };
case 'PenaltyIncome': return { title: 'Map Penalties to Specific Income Accounts', formfields: this.getPenaltyIncomeFormfields(values) };
}
}
getPaymentFundSourceFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'paymentTypeId',
label: 'Payment Type',
value: values ? values.paymentTypeId : this.paymentTypeData[0].id,
options: { label: 'name', value: 'id', data: this.paymentTypeData },
required: true,
order: 1
}),
new SelectBase({
controlName: 'fundSourceAccountId',
label: 'Fund Source',
value: values ? values.fundSourceAccountId : this.assetAccountData[0].id,
options: { label: 'name', value: 'id', data: this.assetAccountData },
required: true,
order: 2
})
];
return formfields;
}
getFeesIncomeFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'chargeId',
label: 'Fees',
value: values ? values.chargeId : this.chargeData[0].id,
options: { label: 'name', value: 'id', data: this.chargeData },
required: true,
order: 1
}),
new SelectBase({
controlName: 'incomeAccountId',
label: 'Income Account',
value: values ? values.incomeAccountId : this.incomeAndLiabilityAccountData[0].id,
options: { label: 'name', value: 'id', data: this.incomeAndLiabilityAccountData },
required: true,
order: 2
})
];
return formfields;
}
getPenaltyIncomeFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'chargeId',
label: 'Penalty',
value: values ? values.chargeId : this.penaltyData[0].id,
options: { label: 'name', value: 'id', data: this.penaltyData },
required: true,
order: 1
}),
new SelectBase({
controlName: 'incomeAccountId',
label: 'Income Account',
value: values ? values.incomeAccountId : this.incomeAccountData[0].id,
options: { label: 'name', value: 'id', data: this.incomeAccountData },
required: true,
order: 2
})
];
return formfields;
}
get loanProductAccounting() {
return this.loanProductAccountingForm.value;
}
}
@@ -0,0 +1,140 @@
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Charge</mat-label>
<mat-select #charge>
<mat-option *ngFor="let charge of chargeData | chargesFilter:chargesDataSource:currencyCode:multiDisburseLoan" [value]="charge">
{{ charge.name }}
</mat-option>
</mat-select>
</mat-form-field>
<div fxFlex="48%" fxFlexAlign="center">
<button type="button" mat-raised-button color="primary" (click)="addCharge(charge)" [disabled]="!charge.value">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="chargesDataSource | chargesPenaltyFilter:false" #chargesTable [hidden]="chargesTable.dataSource.length === 0">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef> Type </th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef> Amount </th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef> Collected On </th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<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>
<mat-divider fxFlex="98%"></mat-divider>
<div fxFlex="98%">
<h3 class="mat-h3">Overdue Charges</h3>
</div>
<mat-form-field fxFlex="48%">
<mat-label>Overdue Charge</mat-label>
<mat-select #overdueCharge>
<mat-option *ngFor="let overdueCharge of overdueChargeData | chargesFilter:chargesDataSource:currencyCode" [value]="overdueCharge">
{{ overdueCharge.name }}
</mat-option>
</mat-select>
</mat-form-field>
<div fxFlex="48%" fxFlexAlign="center">
<button type="button" mat-raised-button color="primary" (click)="addCharge(overdueCharge)" [disabled]="!overdueCharge.value">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="chargesDataSource | chargesPenaltyFilter:true" #overdueChargesTable [hidden]="overdueChargesTable.dataSource.length === 0">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.name + ', ' + overdueCharge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef> Type </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef> Amount </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.amount }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef> Collected On </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let overdueCharge">
<button mat-icon-button color="warn" (click)="deleteCharge(overdueCharge)">
<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>
<div fxLayout="row" class="margin-t+" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button matStepperNext>
Next&nbsp;&nbsp;
<fa-icon icon="arrow-right"></fa-icon>
</button>
</div>
@@ -0,0 +1,19 @@
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em 0;
}
h3 {
font-weight: 500;
}
mat-divider {
margin: 1em 0 2em 0;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductChargesStepComponent } from './loan-product-charges-step.component';
describe('LoanProductChargesStepComponent', () => {
let component: LoanProductChargesStepComponent;
let fixture: ComponentFixture<LoanProductChargesStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductChargesStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductChargesStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,54 @@
import { Component, OnInit, Input } from '@angular/core';
import { MatDialog } from '@angular/material';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
@Component({
selector: 'mifosx-loan-product-charges-step',
templateUrl: './loan-product-charges-step.component.html',
styleUrls: ['./loan-product-charges-step.component.scss']
})
export class LoanProductChargesStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
@Input() currencyCode: any;
@Input() multiDisburseLoan: any;
chargeData: any;
overdueChargeData: any;
chargesDataSource: {}[] = [];
displayedColumns: string[] = ['name', 'chargeCalculationType', 'amount', 'chargeTimeType', 'action'];
constructor(public dialog: MatDialog) {
}
ngOnInit() {
this.chargeData = this.loanProductsTemplate.chargeOptions;
this.overdueChargeData = this.loanProductsTemplate.penaltyOptions.filter((penalty: any) => penalty.chargeTimeType.code === 'chargeTimeType.overdueInstallment');
}
addCharge(charge: any) {
this.chargesDataSource = this.chargesDataSource.concat([charge.value]);
charge.value = '';
}
deleteCharge(charge: any) {
const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `charge ${charge.name}` }
});
deleteChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
}
get charges() {
return {
charges: this.chargesDataSource
};
}
}
@@ -0,0 +1,54 @@
<form [formGroup]="loanProductCurrencyForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Currency</mat-label>
<mat-select formControlName="currencyCode" required>
<mat-option *ngFor="let currency of currencyData" [value]="currency.code">
{{ currency.name }}
</mat-option>
</mat-select>
<mat-error>
Currency is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Decimal Places</mat-label>
<input type="number" matInput formControlName="digitsAfterDecimal" required>
<mat-error>
Decimal Places is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Currency in multiples of</mat-label>
<input type="number" matInput formControlName="inMultiplesOf" required>
<mat-error>
Currency in multiples of is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Installment in multiples of</mat-label>
<input type="number" matInput formControlName="installmentAmountInMultiplesOf" required>
<mat-error>
Installment in multiples of is <strong>required</strong>
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button matStepperNext>
Next&nbsp;&nbsp;
<fa-icon icon="arrow-right"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductCurrencyStepComponent } from './loan-product-currency-step.component';
describe('LoanProductCurrencyStepComponent', () => {
let component: LoanProductCurrencyStepComponent;
let fixture: ComponentFixture<LoanProductCurrencyStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductCurrencyStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductCurrencyStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,39 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'mifosx-loan-product-currency-step',
templateUrl: './loan-product-currency-step.component.html',
styleUrls: ['./loan-product-currency-step.component.scss']
})
export class LoanProductCurrencyStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
loanProductCurrencyForm: FormGroup;
currencyData: any;
constructor(private formBuilder: FormBuilder) {
this.createLoanProductCurrencyForm();
}
ngOnInit() {
this.currencyData = this.loanProductsTemplate.currencyOptions;
this.loanProductCurrencyForm.patchValue({ 'currencyCode': this.currencyData[0].code });
}
createLoanProductCurrencyForm() {
this.loanProductCurrencyForm = this.formBuilder.group({
'currencyCode': ['', Validators.required],
'digitsAfterDecimal': [2, Validators.required],
'inMultiplesOf': ['', Validators.required],
'installmentAmountInMultiplesOf': ['', Validators.required]
});
}
get loanProductCurrency() {
return this.loanProductCurrencyForm.value;
}
}
@@ -0,0 +1,66 @@
<form [formGroup]="loanProductDetailsForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" fxLayoutAlign.gt-sm="start center">
<mat-form-field fxFlex="48%">
<mat-label>Product Name</mat-label>
<input matInput formControlName="name" required>
<mat-error>
Product Name is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Short Name</mat-label>
<input matInput formControlName="shortName" maxlength="4" required>
<mat-error>
Short Name is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Fund</mat-label>
<mat-select formControlName="fundId">
<mat-option *ngFor="let fund of fundData" [value]="fund.id">
{{ fund.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox labelPosition="before" formControlName="includeInBorrowerCycle" fxFlex="48%">
Include in Customer Loan Counter
</mat-checkbox>
<mat-form-field fxFlex="48%">
<mat-label>Start Date</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="startDatePicker" formControlName="startDate">
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Close Date</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="closeDatePicker" formControlName="closeDate">
<mat-datepicker-toggle matSuffix [for]="closeDatePicker"></mat-datepicker-toggle>
<mat-datepicker #closeDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex="98%">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button matStepperNext>
Next&nbsp;&nbsp;
<fa-icon icon="arrow-right"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductDetailsStepComponent } from './loan-product-details-step.component';
describe('LoanProductDetailsStepComponent', () => {
let component: LoanProductDetailsStepComponent;
let fixture: ComponentFixture<LoanProductDetailsStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductDetailsStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductDetailsStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,54 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { DatePipe } from '@angular/common';
@Component({
selector: 'mifosx-loan-product-details-step',
templateUrl: './loan-product-details-step.component.html',
styleUrls: ['./loan-product-details-step.component.scss']
})
export class LoanProductDetailsStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
loanProductDetailsForm: FormGroup;
fundData: any;
minDate = new Date(2000, 0, 1);
maxDate = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
constructor(private formBuilder: FormBuilder,
private datePipe: DatePipe) {
this.createLoanProductDetailsForm();
}
ngOnInit() {
this.fundData = this.loanProductsTemplate.fundOptions;
}
createLoanProductDetailsForm() {
this.loanProductDetailsForm = this.formBuilder.group({
'name': ['', Validators.required],
'shortName': ['', Validators.required],
'description': [''],
'fundId': [''],
'startDate': [''],
'closeDate': [''],
'includeInBorrowerCycle': [false]
});
}
get loanProductDetails() {
const prevStartDate: Date = this.loanProductDetailsForm.value.startDate;
const prevCloseDate: Date = this.loanProductDetailsForm.value.closeDate;
// TODO: Update once language and date settings are setup
const dateFormat = 'yyyy-MM-dd';
this.loanProductDetailsForm.patchValue({
startDate: this.datePipe.transform(prevStartDate, dateFormat) || '',
closeDate: this.datePipe.transform(prevCloseDate, dateFormat) || ''
});
return this.loanProductDetailsForm.value;
}
}
@@ -0,0 +1,730 @@
<div fxLayout="row wrap" fxLayout.lt-md="column">
<h2 class="mat-h2" fxFlexFill>{{ loanProduct.name }}</h2>
<mat-divider fxFlexFill></mat-divider>
<h3 class="mat-h3" fxFlexFill>Details</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Short Name:</span>
<span fxFlex="60%">{{ loanProduct.shortName }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.fundId">
<span fxFlex="40%">Fund Name:</span>
<span fxFlex="60%">{{ loanProduct.fundId | find:loanProductsTemplate.fundOptions:'id':'name' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Include in Customer Loan Counter:</span>
<span fxFlex="60%">{{ loanProduct.includeInBorrowerCycle ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.startDate">
<span fxFlex="40%">Start Date:</span>
<span fxFlex="60%">{{ loanProduct.startDate }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.closeDate">
<span fxFlex="40%">Close Date:</span>
<span fxFlex="60%">{{ loanProduct.closeDate }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.description">
<span fxFlex="40%">Description:</span>
<span fxFlex="60%">{{ loanProduct.description }}</span>
</div>
<h3 class="mat-h3" fxFlexFill>Currency</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Currency:</span>
<span fxFlex="60%">{{ loanProduct.currencyCode | find:loanProductsTemplate.currencyOptions:'code':'name' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Decimal Places:</span>
<span fxFlex="60%">{{ loanProduct.digitsAfterDecimal }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Currency in multiples of:</span>
<span fxFlex="60%">{{ loanProduct.inMultiplesOf }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Installment in multiples of:</span>
<span fxFlex="60%">{{ loanProduct.installmentAmountInMultiplesOf }}</span>
</div>
<h3 class="mat-h3" fxFlexFill>Terms</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Principal:</span>
<span fxFlex="60%">{{ loanProduct.principal + ' (Min: ' + (loanProduct.minPrincipal ? loanProduct.minPrincipal : loanProduct.principal) + ', Max: ' + (loanProduct.maxPrincipal ? loanProduct.maxPrincipal : loanProduct.principal) + ')' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Number of Repayments:</span>
<span fxFlex="60%">{{ loanProduct.numberOfRepayments + ' (Min: ' + (loanProduct.minNumberOfRepayments ? loanProduct.minNumberOfRepayments : loanProduct.numberOfRepayments) + ', Max: ' + (loanProduct.maxNumberOfRepayments ? loanProduct.maxNumberOfRepayments : loanProduct.numberOfRepayments) + ')' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Linked to floating interest rates:</span>
<span fxFlex="60%">{{ loanProduct.isLinkedToFloatingInterestRates ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="!loanProduct.isLinkedToFloatingInterestRates">
<span fxFlex="40%">Nominal interest rate:</span>
<span fxFlex="60%">{{ loanProduct.interestRatePerPeriod + ' (Min: ' + (loanProduct.minInterestRatePerPeriod ? loanProduct.minInterestRatePerPeriod : loanProduct.interestRatePerPeriod) + ', Max: ' + (loanProduct.maxInterestRatePerPeriod ? loanProduct.maxInterestRatePerPeriod : loanProduct.interestRatePerPeriod) + ')' + ' ' + (loanProduct.interestRateFrequencyType | find:loanProductsTemplate.interestRateFrequencyTypeOptions:'id':'value') }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.isLinkedToFloatingInterestRates" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Floating Lending Interest Rate:</span>
<span fxFlex="60%">{{ loanProduct.floatingRatesId | find:loanProductsTemplate.floatingRateOptions:'id':'name' }}</span>
<span fxFlex="40%">Interest Rate Differential:</span>
<span fxFlex="60%">{{ loanProduct.interestRateDifferential }}</span>
<span fxFlex="40%">Floating Interest Rate Calculation Allowed:</span>
<span fxFlex="60%">{{ loanProduct.isFloatingInterestRateCalculationAllowed ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Floating Interest Rate:</span>
<span fxFlex="60%">{{ loanProduct.defaultDifferentialLendingRate + ' (Min: ' + (loanProduct.minDifferentialLendingRate ? loanProduct.minDifferentialLendingRate : loanProduct.defaultDifferentialLendingRate) + ', Max: ' + (loanProduct.maxDifferentialLendingRate ? loanProduct.maxDifferentialLendingRate : loanProduct.defaultDifferentialLendingRate) + ')' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Terms vary based on loan cycle:</span>
<span fxFlex="60%">{{ loanProduct.useBorrowerCycle ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.principalVariationsForBorrowerCycle?.length">
<h4 class="mat-h4" fxFlexFill>Principal by loan cycle</h4>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.principalVariationsForBorrowerCycle">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef> Condition </th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find:loanProductsTemplate.valueConditionTypeOptions:'id':'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef> Loan Cycle </th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef> Minimum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef> Default </th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef> Maximum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="variationsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: variationsDisplayedColumns;"></tr>
</table>
</div>
<div fxFlexFill *ngIf="loanProduct.numberOfRepaymentVariationsForBorrowerCycle?.length">
<h4 class="mat-h4" fxFlexFill>Number of Repayments by loan cycle</h4>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.numberOfRepaymentVariationsForBorrowerCycle">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef> Condition </th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find:loanProductsTemplate.valueConditionTypeOptions:'id':'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef> Loan Cycle </th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef> Minimum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef> Default </th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef> Maximum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="variationsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: variationsDisplayedColumns;"></tr>
</table>
</div>
<div fxFlexFill *ngIf="loanProduct.interestRateVariationsForBorrowerCycle?.length">
<h4 class="mat-h4" fxFlexFill>Nominal Interest Rate by loan cycle</h4>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.interestRateVariationsForBorrowerCycle">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef> Condition </th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find:loanProductsTemplate.valueConditionTypeOptions:'id':'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef> Loan Cycle </th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef> Minimum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef> Default </th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef> Maximum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="variationsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: variationsDisplayedColumns;"></tr>
</table>
</div>
<div fxFlexFill>
<span fxFlex="40%">Repay Every:</span>
<span fxFlex="60%">{{ loanProduct.repaymentEvery + ' ' + (loanProduct.repaymentFrequencyType | find:loanProductsTemplate.repaymentFrequencyTypeOptions:'id':'value') }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment">
<span fxFlex="40%">Minimum days between disbursal and first repayment date:</span>
<span fxFlex="60%">{{ loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment }}</span>
</div>
<h3 class="mat-h3" fxFlexFill>Settings</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Amortization:</span>
<span fxFlex="60%">{{ loanProduct.amortizationType | find:loanProductsTemplate.amortizationTypeOptions:'id':'value' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Is Equal Amortization:</span>
<span fxFlex="60%">{{ loanProduct.isEqualAmortization ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Interest Method:</span>
<span fxFlex="60%">{{ loanProduct.interestType | find:loanProductsTemplate.interestTypeOptions:'id':'value' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Interest Calculation Period:</span>
<span fxFlex="60%">{{ loanProduct.interestCalculationPeriodType | find:loanProductsTemplate.interestCalculationPeriodTypeOptions:'id':'value' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.interestCalculationPeriodType === 1">
<span fxFlex="40%">Allow Partial Interest Calculation with same as repayment:</span>
<span fxFlex="60%">{{ loanProduct.allowPartialPeriodInterestCalcualtion ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Repayment Strategy:</span>
<span fxFlex="60%">{{ loanProduct.transactionProcessingStrategyId | find:loanProductsTemplate.transactionProcessingStrategyOptions:'id':'name' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.graceOnPrincipalPayment">
<span fxFlex="40%">Grace on principal payment:</span>
<span fxFlex="60%">{{ loanProduct.graceOnPrincipalPayment }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.graceOnInterestPayment">
<span fxFlex="40%">Grace on interest payment:</span>
<span fxFlex="60%">{{ loanProduct.graceOnInterestPayment }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.graceOnInterestCharged">
<span fxFlex="40%">Interest free period:</span>
<span fxFlex="60%">{{ loanProduct.graceOnInterestCharged }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.inArrearsTolerance">
<span fxFlex="40%">Arrears tolerance:</span>
<span fxFlex="60%">{{ loanProduct.inArrearsTolerance }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Days in year:</span>
<span fxFlex="60%">{{ loanProduct.daysInYearType | find:loanProductsTemplate.daysInYearTypeOptions:'id':'value' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Days in month:</span>
<span fxFlex="60%">{{ loanProduct.daysInMonthType | find:loanProductsTemplate.daysInMonthTypeOptions:'id':'value' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Allow fixing of the installment amount:</span>
<span fxFlex="60%">{{ loanProduct.canDefineInstallmentAmount ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.graceOnArrearsAgeing">
<span fxFlex="40%">Number of days a loan may be overdue before moving into arrears:</span>
<span fxFlex="60%">{{ loanProduct.graceOnArrearsAgeing }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.overdueDaysForNPA">
<span fxFlex="40%">Maximum number of days a loan may be overdue before becoming a NPA (non performing asset):</span>
<span fxFlex="60%">{{ loanProduct.overdueDaysForNPA }}</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>
</div>
<div fxFlexFill *ngIf="loanProduct.principalThresholdForLastInstallment">
<span fxFlex="40%">Principal Threshold (%) for Last Instalment:</span>
<span fxFlex="60%">{{ loanProduct.principalThresholdForLastInstallment }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Variable Installments allowed:</span>
<span fxFlex="60%">{{ loanProduct.allowVariableInstallments ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.allowVariableInstallments">
<span fxFlex="40%">Variable Installments:</span>
<span fxFlex="60%">{{ '(Min: ' + loanProduct.minimumGap + ', Max: ' + (loanProduct.maximumGap ? loanProduct.maximumGap : loanProduct.minimumGap) + ')' }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">Allowed to be used for providing Topup Loans:</span>
<span fxFlex="60%">{{ loanProduct.canUseForTopup ? 'Yes' : 'No' }}</span>
</div>
<h3 class="mat-h3" fxFlexFill>Interest Recalculation</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Recalculate Interest:</span>
<span fxFlex="60%">{{ loanProduct.isInterestRecalculationEnabled ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.isInterestRecalculationEnabled" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Pre-closure interest calculation rule:</span>
<span fxFlex="60%">{{ loanProduct.preClosureInterestCalculationStrategy | find:loanProductsTemplate.preClosureInterestCalculationStrategyOptions:'id':'value' }}</span>
<span fxFlex="40%">Advance payments adjustment type:</span>
<span fxFlex="60%">{{ loanProduct.rescheduleStrategyMethod | find:loanProductsTemplate.rescheduleStrategyTypeOptions:'id':'value' }}</span>
<span fxFlex="40%">Interest recalculation compounding on:</span>
<span fxFlex="60%">{{ loanProduct.interestRecalculationCompoundingMethod | find:loanProductsTemplate.interestRecalculationCompoundingTypeOptions:'id':'value' }}</span>
<div fxFlexFill *ngIf="loanProduct.interestRecalculationCompoundingMethod !== 0" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Frequency for compounding:</span>
<span fxFlex="60%">
{{ loanProduct.recalculationCompoundingFrequencyType | find:loanProductsTemplate.interestRecalculationFrequencyTypeOptions:'id':'value' }}
<span *ngIf="loanProduct.recalculationCompoundingFrequencyType === 4">&nbsp;&nbsp;{{ loanProduct.recalculationCompoundingFrequencyNthDayType | find:loanProductsTemplate.interestRecalculationNthDayTypeOptions:'id':'value' }}</span>
<span *ngIf="(loanProduct.recalculationCompoundingFrequencyType === 4 && loanProduct.recalculationCompoundingFrequencyNthDayType !== -2) || loanProduct.recalculationCompoundingFrequencyType === 3">&nbsp;&nbsp;{{ loanProduct.recalculationCompoundingFrequencyDayOfWeekType | find:loanProductsTemplate.interestRecalculationDayOfWeekTypeOptions:'id':'value' }}</span>
<span *ngIf="loanProduct.recalculationCompoundingFrequencyType === 4 && loanProduct.recalculationCompoundingFrequencyNthDayType === -2">&nbsp;&nbsp;{{ loanProduct.recalculationCompoundingFrequencyOnDayType }}</span>
</span>
<div fxFlexFill *ngIf="loanProduct.recalculationCompoundingFrequencyType !== 1" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Frequency Interval for compounding:</span>
<span fxFlex="60%">{{ loanProduct.recalculationCompoundingFrequencyInterval }}</span>
<div fxFlexFill *ngIf="loanProduct.recalculationCompoundingFrequencyDate">
<span fxFlex="40%">Compounding Frequency Date:</span>
<span fxFlex="60%">{{ loanProduct.recalculationCompoundingFrequencyDate }}</span>
</div>
</div>
</div>
<div fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Frequency for recalculate Outstanding Principal:</span>
<span fxFlex="60%">
{{ loanProduct.recalculationRestFrequencyType | find:loanProductsTemplate.interestRecalculationFrequencyTypeOptions:'id':'value' }}
<span *ngIf="loanProduct.recalculationRestFrequencyType === 4">&nbsp;&nbsp;{{ loanProduct.recalculationRestFrequencyNthDayType | find:loanProductsTemplate.interestRecalculationNthDayTypeOptions:'id':'value' }}</span>
<span *ngIf="(loanProduct.recalculationRestFrequencyType === 4 && loanProduct.recalculationRestFrequencyNthDayType !== -2) || loanProduct.recalculationRestFrequencyType === 3">&nbsp;&nbsp;{{ loanProduct.recalculationRestFrequencyDayOfWeekType | find:loanProductsTemplate.interestRecalculationDayOfWeekTypeOptions:'id':'value' }}</span>
<span *ngIf="loanProduct.recalculationRestFrequencyType === 4 && loanProduct.recalculationRestFrequencyNthDayType === -2">&nbsp;&nbsp;{{ loanProduct.recalculationRestFrequencyOnDayType }}</span>
</span>
</div>
<div fxFlexFill *ngIf="loanProduct.recalculationRestFrequencyType !== 1" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Frequency Interval for recalculation:</span>
<span fxFlex="60%">{{ loanProduct.recalculationRestFrequencyInterval }}</span>
<div fxFlexFill *ngIf="loanProduct.recalculationRestFrequencyDate">
<span fxFlex="40%">Rest Frequency Date:</span>
<span fxFlex="60%">{{ loanProduct.recalculationRestFrequencyDate }}</span>
</div>
</div>
<div fxFlexFill>
<span fxFlex="40%">Is Arrears recognization based on original schedule:</span>
<span fxFlex="60%">{{ loanProduct.isArrearsBasedOnOriginalSchedule ? 'Yes' : 'No' }}</span>
</div>
</div>
<h3 class="mat-h3" fxFlexFill>Guarantee Requirements</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Place Guarantee Funds On-Hold:</span>
<span fxFlex="60%">{{ loanProduct.holdGuaranteeFunds ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.holdGuaranteeFunds" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Mandatory Guarantee(%):</span>
<span fxFlex="60%">{{ loanProduct.mandatoryGuarantee }}</span>
<div fxFlexFill *ngIf="loanProduct.minimumGuaranteeFromOwnFunds">
<span fxFlex="40%">Minimum Guarantee from Own Funds(%):</span>
<span fxFlex="60%">{{ loanProduct.minimumGuaranteeFromOwnFunds }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.minimumGuaranteeFromGuarantor">
<span fxFlex="40%">Minimum Guarantee from Guarantor Funds(%):</span>
<span fxFlex="60%">{{ loanProduct.minimumGuaranteeFromGuarantor }}</span>
</div>
</div>
<h3 class="mat-h3" fxFlexFill>Loan Tranche Details</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Enable Multiple Disbursals:</span>
<span fxFlex="60%">{{ loanProduct.multiDisburseLoan ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.multiDisburseLoan" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Maximum Tranche count:</span>
<span fxFlex="60%">{{ loanProduct.maxTrancheCount }}</span>
<div fxFlexFill *ngIf="loanProduct.outstandingLoanBalance">
<span fxFlex="40%">Maximum allowed outstanding balance:</span>
<span fxFlex="60%">{{ loanProduct.outstandingLoanBalance }}</span>
</div>
</div>
<h3 class="mat-h3" fxFlexFill>Configurable Terms and Settings</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Allow overriding select terms and settings in loan accounts:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeConfiguration ? 'Yes' : 'No' }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.allowAttributeConfiguration" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Amortization:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.amortizationType ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Interest method:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.interestType ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Repayment strategy:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.transactionProcessingStrategyId ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Interest calculation period:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.interestCalculationPeriodType ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Arrears tolerance:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.inArrearsTolerance ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Repaid every:</span>
<span fxFlex="60%">{{ loanProduct.allowAttributeOverrides.repaymentEvery ? 'Yes' : 'No' }}</span>
<span fxFlex="40%">Moratorium:</span>
<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>
</div>
<div fxFlexFill *ngIf="loanProduct.charges.length" fxLayout="row wrap" fxLayout.lt-md="column">
<h3 class="mat-h3" fxFlexFill>Charges</h3>
<mat-divider fxFlexFill></mat-divider>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.charges | chargesPenaltyFilter:false">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef> Type </th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef> Amount </th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef> Collected On </th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns;"></tr>
</table>
<h3 class="mat-h3" fxFlexFill>Overdue Charges</h3>
<mat-divider fxFlexFill></mat-divider>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.charges | chargesPenaltyFilter:true">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.name + ', ' + overdueCharge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef> Type </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef> Amount </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.amount }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef> Collected On </th>
<td mat-cell *matCellDef="let overdueCharge">
{{ overdueCharge.chargeTimeType.value }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns;"></tr>
</table>
</div>
<h3 class="mat-h3" fxFlexFill>Accounting</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">Type:</span>
<span fxFlex="60%">{{ accountingRuleData[loanProduct.accountingRule-1] }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.accountingRule >= 2 && loanProduct.accountingRule <= 4" fxLayout="row wrap" fxLayout.lt-md="column">
<h4 fxFlexFill class="mat-h4">Assets</h4>
<div fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Fund source:</span>
<span fxFlex="60%">{{ loanProduct.fundSourceAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}</span>
<span fxFlex="40%">Loan portfolio:</span>
<span fxFlex="60%">{{ loanProduct.loanPortfolioAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}</span>
<div fxFlexFill *ngIf="loanProduct.accountingRule === 3 || loanProduct.accountingRule === 4" fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Interest Receivable:</span>
<span fxFlex="60%">{{ loanProduct.receivableInterestAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}</span>
<span fxFlex="40%">Fees Receivable:</span>
<span fxFlex="60%">{{ loanProduct.receivableFeeAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}</span>
<span fxFlex="40%">Penalties Receivable:</span>
<span fxFlex="60%">{{ loanProduct.receivablePenaltyAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}</span>
</div>
<span fxFlex="40%">Transfer in suspense:</span>
<span fxFlex="60%">{{ loanProduct.transfersInSuspenseAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}</span>
</div>
<h4 fxFlexFill class="mat-h4">Income</h4>
<div fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<span fxFlex="40%">Income from Interest:</span>
<span fxFlex="60%">{{ loanProduct.interestOnLoanAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }}</span>
<span fxFlex="40%">Income from fees:</span>
<span fxFlex="60%">{{ loanProduct.incomeFromFeeAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }}</span>
<span fxFlex="40%">Income from penalties:</span>
<span fxFlex="60%">{{ loanProduct.incomeFromPenaltyAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }}</span>
<span fxFlex="40%">Income from Recovery Repayments:</span>
<span fxFlex="60%">{{ loanProduct.incomeFromRecoveryAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }}</span>
</div>
<h4 fxFlexFill class="mat-h4">Expenses</h4>
<div fxFlexFill>
<span fxFlex="40%">Losses written off:</span>
<span fxFlex="60%">{{ loanProduct.writeOffAccountId | find:loanProductsTemplate.accountingMappingOptions.expenseAccountOptions:'id':'name' }}</span>
</div>
<h4 fxFlexFill class="mat-h4">Liabilities</h4>
<div fxFlexFill>
<span fxFlex="40%">Over payment liability:</span>
<span fxFlex="60%">{{ loanProduct.overpaymentLiabilityAccountId | find:loanProductsTemplate.accountingMappingOptions.liabilityAccountOptions:'id':'name' }}</span>
</div>
<div *ngIf="loanProduct.advancedAccountingRules && (loanProduct.paymentChannelToFundSourceMappings?.length || loanProduct.feeToIncomeAccountMappings?.length || loanProduct.penaltyToIncomeAccountMappings?.length)" fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<h3 class="mat-h3" fxFlexFill>Advanced Accounting Rules</h3>
<mat-divider fxFlexFill></mat-divider>
<div *ngIf="loanProduct.paymentChannelToFundSourceMappings?.length" fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<h4 class="mat-h4" fxFlexFill>Fund Sources for Payment Channels</h4>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.paymentChannelToFundSourceMappings">
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef> Payment Type </th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find:loanProductsTemplate.paymentTypeOptions:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef> Fund Source </th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns;"></tr>
</table>
</div>
<div *ngIf="loanProduct.feeToIncomeAccountMappings?.length" fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<h4 class="mat-h4" fxFlexFill>Fees to Specific Income Accounts</h4>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.feeToIncomeAccountMappings">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef> Fees </th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.chargeId | find:loanProductsTemplate.chargeOptions:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef> Income Account </th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions.concat(loanProductsTemplate.accountingMappingOptions.liabilityAccountOptions):'id':'name' }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns;"></tr>
</table>
</div>
<div *ngIf="loanProduct.penaltyToIncomeAccountMappings?.length" fxFlexFill fxLayout="row wrap" fxLayout.lt-md="column">
<h4 class="mat-h4" fxFlexFill>Penalties to Specific Income Accounts</h4>
<table fxFlexFill class="mat-elevation-z1" mat-table [dataSource]="loanProduct.penaltyToIncomeAccountMappings">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef> Penalty </th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find:loanProductsTemplate.penaltyOptions:'id':'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef> Income Account </th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns;"></tr>
</table>
</div>
</div>
</div>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button [routerLink]="['../']">
Cancel
</button>
<button mat-raised-button color="primary" (click)="submit.emit()">
Submit
</button>
</div>
@@ -0,0 +1,24 @@
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em 0;
}
h2, h3, h4 {
margin: 0;
font-weight: 500;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em 0;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductPreviewStepComponent } from './loan-product-preview-step.component';
describe('LoanProductPreviewStepComponent', () => {
let component: LoanProductPreviewStepComponent;
let fixture: ComponentFixture<LoanProductPreviewStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductPreviewStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductPreviewStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,25 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'mifosx-loan-product-preview-step',
templateUrl: './loan-product-preview-step.component.html',
styleUrls: ['./loan-product-preview-step.component.scss']
})
export class LoanProductPreviewStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() loanProduct: any;
@Output() submit = new EventEmitter();
variationsDisplayedColumns: string[] = ['valueConditionType', 'borrowerCycleNumber', 'minValue', 'defaultValue', 'maxValue'];
chargesDisplayedColumns: string[] = ['name', 'chargeCalculationType', 'amount', 'chargeTimeType'];
paymentFundSourceDisplayedColumns: string[] = ['paymentTypeId', 'fundSourceAccountId'];
feesPenaltyIncomeDisplayedColumns: string[] = ['chargeId', 'incomeAccountId'];
constructor() { }
ngOnInit() {
}
}
@@ -0,0 +1,466 @@
<form [formGroup]="loanProductSettingsForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" fxLayoutAlign.gt-sm="start center">
<mat-form-field fxFlex="31%">
<mat-label>Amortization</mat-label>
<mat-select formControlName="amortizationType" required>
<mat-option *ngFor="let amortizationType of amortizationTypeData" [value]="amortizationType.id">
{{ amortizationType.value }}
</mat-option>
</mat-select>
<mat-error>
Amortization is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-checkbox fxFlex="15%" labelPosition="before" formControlName="isEqualAmortization">
Is Equal Amortization?
</mat-checkbox>
<mat-form-field fxFlex="48%">
<mat-label>Interest Method</mat-label>
<mat-select formControlName="interestType" required>
<mat-option *ngFor="let interestType of interestTypeData" [value]="interestType.id">
{{ interestType.value }}
</mat-option>
</mat-select>
<mat-error>
Interest Method is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Interest calculation period</mat-label>
<mat-select formControlName="interestCalculationPeriodType" required>
<mat-option *ngFor="let interestCalculationPeriodType of interestCalculationPeriodTypeData" [value]="interestCalculationPeriodType.id">
{{ interestCalculationPeriodType.value }}
</mat-option>
</mat-select>
<mat-error>
Interest calculation period is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-checkbox *ngIf="loanProductSettingsForm.value.interestCalculationPeriodType === 1" fxFlex="48%" labelPosition="before" formControlName="allowPartialPeriodInterestCalcualtion">
Calculate interest for exact days in partial period
</mat-checkbox>
<mat-form-field fxFlex="48%">
<mat-label>Repayment strategy</mat-label>
<mat-select formControlName="transactionProcessingStrategyId" required>
<mat-option *ngFor="let transactionProcessingStrategy of transactionProcessingStrategyData" [value]="transactionProcessingStrategy.id">
{{ transactionProcessingStrategy.name }}
</mat-option>
</mat-select>
<mat-error>
Repayment strategy is <strong>required</strong>
</mat-error>
</mat-form-field>
<span fxFlex="48%" fxHide.lt-md></span>
<h4 fxFlex="98%" class="mat-h4">Moratorium</h4>
<mat-form-field fxFlex="48%">
<mat-label>Grace on principal payment</mat-label>
<input type="number" matInput formControlName="graceOnPrincipalPayment">
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Grace on interest payment</mat-label>
<input type="number" matInput formControlName="graceOnInterestPayment">
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<mat-form-field fxFlex="48%">
<mat-label>Interest free period</mat-label>
<input type="number" matInput formControlName="graceOnInterestCharged">
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Arrears tolerance</mat-label>
<input type="number" matInput formControlName="inArrearsTolerance">
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Days in year</mat-label>
<mat-select formControlName="daysInYearType" required>
<mat-option *ngFor="let daysInYearType of daysInYearTypeData" [value]="daysInYearType.id">
{{ daysInYearType.value }}
</mat-option>
</mat-select>
<mat-error>
Days in year is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Days in month</mat-label>
<mat-select formControlName="daysInMonthType" required>
<mat-option *ngFor="let daysInMonthType of daysInMonthTypeData" [value]="daysInMonthType.id">
{{ daysInMonthType.value }}
</mat-option>
</mat-select>
<mat-error>
Days in month is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-checkbox fxFlex="98%" labelPosition="before" formControlName="canDefineInstallmentAmount" class="margin-v">
Allow fixing of the installment amount
</mat-checkbox>
<h4 fxFlex="48%" class="mat-h4">Number of days a loan may be overdue before moving into arrears</h4>
<mat-form-field fxFlex="48%">
<mat-label>On arrears aging</mat-label>
<input type="number" matInput formControlName="graceOnArrearsAgeing">
</mat-form-field>
<h4 fxFlex="48%" class="mat-h4">Maximum number of days a loan may be overdue before becoming a NPA (non-performing asset)</h4>
<mat-form-field fxFlex="48%">
<mat-label>Overdue days for NPA</mat-label>
<input type="number" matInput formControlName="overdueDaysForNPA">
</mat-form-field>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="accountMovesOutOfNPAOnlyOnArrearsCompletion" class="margin-v">
Account moves out of NPA only after all arrears have been cleared?
</mat-checkbox>
<mat-form-field fxFlex="48%">
<mat-label>Principal Threshold (%) for Last Instalment</mat-label>
<input type="number" matInput formControlName="principalThresholdForLastInstallment">
</mat-form-field>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="allowVariableInstallments" class="margin-v">
Are Variable Installments allowed?
</mat-checkbox>
<div *ngIf="loanProductSettingsForm.value.allowVariableInstallments" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="98%" class="mat-h4">Variable Installments</h4>
<mat-form-field fxFlex="48%">
<mat-label>Minimum gap between Installments</mat-label>
<input type="number" matInput formControlName="minimumGap" required>
<mat-error>
Minimum gap between Installments is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Maximum gap between Installments</mat-label>
<input type="number" matInput formControlName="maximumGap">
</mat-form-field>
</div>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="canUseForTopup" class="margin-v">
Allowed to be used for providing Topup Loans
</mat-checkbox>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Interest Recalculation</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="isInterestRecalculationEnabled" class="margin-b">
Recalculate Interest
</mat-checkbox>
<div *ngIf="loanProductSettingsForm.value.isInterestRecalculationEnabled" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Pre-closure interest calculation rule</mat-label>
<mat-select formControlName="preClosureInterestCalculationStrategy" required>
<mat-option *ngFor="let preClosureInterestCalculationStrategy of preClosureInterestCalculationStrategyData" [value]="preClosureInterestCalculationStrategy.id">
{{ preClosureInterestCalculationStrategy.value }}
</mat-option>
</mat-select>
<mat-error>
Pre-closure interest calculation rule is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Advance payments adjustment type</mat-label>
<mat-select formControlName="rescheduleStrategyMethod" required>
<mat-option *ngFor="let rescheduleStrategyType of rescheduleStrategyTypeData" [value]="rescheduleStrategyType.id">
{{ rescheduleStrategyType.value }}
</mat-option>
</mat-select>
<mat-error>
Advance payments adjustment type is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Interest recalculation compounding on</mat-label>
<mat-select formControlName="interestRecalculationCompoundingMethod" required>
<mat-option *ngFor="let interestRecalculationCompoundingType of interestRecalculationCompoundingTypeData" [value]="interestRecalculationCompoundingType.id">
{{ interestRecalculationCompoundingType.value }}
</mat-option>
</mat-select>
<mat-error>
Interest recalculation compounding on is <strong>required</strong>
</mat-error>
</mat-form-field>
<span fxFlex="48%" fxHide.lt-md></span>
<div *ngIf="loanProductSettingsForm.value.interestRecalculationCompoundingMethod !== 0 && loanProductSettingsForm.value.recalculationCompoundingFrequencyType !== 1" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Frequency Interval for compounding</mat-label>
<input type="number" matInput formControlName="recalculationCompoundingFrequencyInterval" required>
<mat-error>
Frequency Interval for compounding is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Compounding Frequency Date</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="recalculationCompoundingFrequencyDatePicker" formControlName="recalculationCompoundingFrequencyDate">
<mat-datepicker-toggle matSuffix [for]="recalculationCompoundingFrequencyDatePicker"></mat-datepicker-toggle>
<mat-datepicker #recalculationCompoundingFrequencyDatePicker></mat-datepicker>
</mat-form-field>
</div>
<div *ngIf="loanProductSettingsForm.value.recalculationRestFrequencyType !== 1" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Frequency Interval for recalculation</mat-label>
<input type="number" matInput formControlName="recalculationRestFrequencyInterval" required>
<mat-error>
Frequency Interval for recalculation is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Rest Frequency Date</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="recalculationRestFrequencyDatePicker" formControlName="recalculationRestFrequencyDate">
<mat-datepicker-toggle matSuffix [for]="recalculationRestFrequencyDatePicker"></mat-datepicker-toggle>
<mat-datepicker #recalculationRestFrequencyDatePicker></mat-datepicker>
</mat-form-field>
</div>
<div *ngIf="loanProductSettingsForm.value.interestRecalculationCompoundingMethod !== 0" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="31%">
<mat-label>Frequency for compounding</mat-label>
<mat-select formControlName="recalculationCompoundingFrequencyType" required>
<mat-option *ngFor="let interestRecalculationFrequencyType of interestRecalculationFrequencyTypeData" [value]="interestRecalculationFrequencyType.id">
{{ interestRecalculationFrequencyType.value }}
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="loanProductSettingsForm.value.recalculationCompoundingFrequencyType === 4" fxFlex="31%" fxLayout="row" fxLayout.lt-md="column">
<mat-form-field fxFlexFill>
<mat-select formControlName="recalculationCompoundingFrequencyNthDayType">
<mat-option *ngFor="let interestRecalculationNthDayType of interestRecalculationNthDayTypeData" [value]="interestRecalculationNthDayType.id">
{{ interestRecalculationNthDayType.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="(loanProductSettingsForm.value.recalculationCompoundingFrequencyType === 4 && loanProductSettingsForm.value.recalculationCompoundingFrequencyNthDayType !== -2) || loanProductSettingsForm.value.recalculationCompoundingFrequencyType === 3" fxFlex="31%" fxLayout="row" fxLayout.lt-md="column">
<mat-form-field fxFlexFill>
<mat-select formControlName="recalculationCompoundingFrequencyDayOfWeekType">
<mat-option *ngFor="let interestRecalculationDayOfWeekType of interestRecalculationDayOfWeekTypeData" [value]="interestRecalculationDayOfWeekType.id">
{{ interestRecalculationDayOfWeekType.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="loanProductSettingsForm.value.recalculationCompoundingFrequencyType === 4 && loanProductSettingsForm.value.recalculationCompoundingFrequencyNthDayType === -2" fxFlex="31%" fxLayout="row" fxLayout.lt-md="column">
<mat-form-field fxFlexFill>
<mat-select formControlName="recalculationCompoundingFrequencyOnDayType">
<mat-option *ngFor="let interestRecalculationOnDayType of interestRecalculationOnDayTypeData" [value]="interestRecalculationOnDayType">
{{ interestRecalculationOnDayType }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<mat-form-field fxFlex="31%">
<mat-label>Frequency for recalculate Outstanding Principal</mat-label>
<mat-select formControlName="recalculationRestFrequencyType" required>
<mat-option *ngFor="let interestRecalculationFrequencyType of interestRecalculationFrequencyTypeData" [value]="interestRecalculationFrequencyType.id">
{{ interestRecalculationFrequencyType.value }}
</mat-option>
</mat-select>
<mat-error>
Frequency for recalculate Outstanding Principal is <strong>required</strong>
</mat-error>
</mat-form-field>
<div *ngIf="loanProductSettingsForm.value.recalculationRestFrequencyType === 4" fxFlex="31%" fxLayout="row" fxLayout.lt-md="column">
<mat-form-field fxFlexFill>
<mat-select formControlName="recalculationRestFrequencyNthDayType">
<mat-option *ngFor="let interestRecalculationNthDayType of interestRecalculationNthDayTypeData" [value]="interestRecalculationNthDayType.id">
{{ interestRecalculationNthDayType.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="(loanProductSettingsForm.value.recalculationRestFrequencyType === 4 && loanProductSettingsForm.value.recalculationRestFrequencyNthDayType !== -2) || loanProductSettingsForm.value.recalculationRestFrequencyType === 3" fxFlex="31%" fxLayout="row" fxLayout.lt-md="column">
<mat-form-field fxFlexFill>
<mat-select formControlName="recalculationRestFrequencyDayOfWeekType">
<mat-option *ngFor="let interestRecalculationDayOfWeekType of interestRecalculationDayOfWeekTypeData" [value]="interestRecalculationDayOfWeekType.id">
{{ interestRecalculationDayOfWeekType.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="loanProductSettingsForm.value.recalculationRestFrequencyType === 4 && loanProductSettingsForm.value.recalculationRestFrequencyNthDayType === -2" fxFlex="31%" fxLayout="row" fxLayout.lt-md="column">
<mat-form-field fxFlexFill>
<mat-select formControlName="recalculationRestFrequencyOnDayType">
<mat-option *ngFor="let interestRecalculationOnDayType of interestRecalculationOnDayTypeData" [value]="interestRecalculationOnDayType">
{{ interestRecalculationOnDayType }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-checkbox fxFlex="98%" labelPosition="before" formControlName="isArrearsBasedOnOriginalSchedule" class="margin-v">
Is Arrears recognization based on original schedule?
</mat-checkbox>
</div>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Guarantee Requirements</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="holdGuaranteeFunds" class="margin-b">
Place Guarantee Funds On-Hold?
</mat-checkbox>
<div *ngIf="loanProductSettingsForm.value.holdGuaranteeFunds" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="31%">
<mat-label>Mandatory Guarantee(%)</mat-label>
<input type="number" matInput formControlName="mandatoryGuarantee" required>
<mat-error>
Mandatory Guarantee is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Minimum Guarantee from Own Funds(%)</mat-label>
<input type="number" matInput formControlName="minimumGuaranteeFromOwnFunds">
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Minimum Guarantee from Guarantor Funds(%)</mat-label>
<input type="number" matInput formControlName="minimumGuaranteeFromGuarantor">
</mat-form-field>
</div>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Loan Tranche Details</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="multiDisburseLoan" class="margin-b">
Enable Multiple Disbursals
</mat-checkbox>
<div *ngIf="loanProductSettingsForm.value.multiDisburseLoan" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>Maximum Tranche count</mat-label>
<input type="number" matInput formControlName="maxTrancheCount" required>
<mat-error>
Maximum Tranche count is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Maximum allowed outstanding balance</mat-label>
<input type="number" matInput formControlName="outstandingLoanBalance">
</mat-form-field>
</div>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Configurable Terms and Settings</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="allowAttributeConfiguration" class="margin-b">
Allow overriding select terms and settings in loan accounts
</mat-checkbox>
<div *ngIf="loanProductSettingsForm.value.allowAttributeConfiguration" formGroupName="allowAttributeOverrides" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="amortizationType" class="margin-v">
Amortization
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="interestType" class="margin-v">
Interest method
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="transactionProcessingStrategyId" class="margin-v">
Repayment strategy
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="interestCalculationPeriodType" class="margin-v">
Interest calculation period
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="inArrearsTolerance" class="margin-v">
Arrears tolerance
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="repaymentEvery" class="margin-v">
Repaid every
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="graceOnPrincipalAndInterestPayment" class="margin-v">
Moratorium
</mat-checkbox>
<mat-checkbox fxFlex="48%" labelPosition="before" formControlName="graceOnArrearsAgeing" class="margin-v">
Number of days a loan may be overdue before moving into arrears
</mat-checkbox>
</div>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button matStepperNext>
Next&nbsp;&nbsp;
<fa-icon icon="arrow-right"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,23 @@
h3, h4 {
font-weight: 500;
}
h4 {
margin: 1em 0;
}
mat-divider {
margin: 1em 0 2em 0;
}
.margin-v {
margin: 1em 0;
}
.margin-b {
margin: 0 0 1em 0;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductSettingsStepComponent } from './loan-product-settings-step.component';
describe('LoanProductSettingsStepComponent', () => {
let component: LoanProductSettingsStepComponent;
let fixture: ComponentFixture<LoanProductSettingsStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductSettingsStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductSettingsStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,308 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms';
import { DatePipe } from '@angular/common';
@Component({
selector: 'mifosx-loan-product-settings-step',
templateUrl: './loan-product-settings-step.component.html',
styleUrls: ['./loan-product-settings-step.component.scss']
})
export class LoanProductSettingsStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
@Input() isLinkedToFloatingInterestRates: FormControl;
loanProductSettingsForm: FormGroup;
amortizationTypeData: any;
interestTypeData: any;
interestCalculationPeriodTypeData: any;
transactionProcessingStrategyData: any;
daysInYearTypeData: any;
daysInMonthTypeData: any;
preClosureInterestCalculationStrategyData: any;
rescheduleStrategyTypeData: any;
interestRecalculationCompoundingTypeData: any;
interestRecalculationFrequencyTypeData: any;
interestRecalculationNthDayTypeData: any;
interestRecalculationDayOfWeekTypeData: any;
interestRecalculationOnDayTypeData: any;
minDate = new Date(2000, 0, 1);
maxDate = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
constructor(private formBuilder: FormBuilder,
private datePipe: DatePipe) {
this.createLoanProductSettingsForm();
this.setConditionalControls();
}
ngOnInit() {
this.isLinkedToFloatingInterestRates.valueChanges
.subscribe((isLinkedToFloatingInterestRates: any) => {
if (isLinkedToFloatingInterestRates) {
this.loanProductSettingsForm.get('isInterestRecalculationEnabled').setValue(true);
}
});
this.amortizationTypeData = this.loanProductsTemplate.amortizationTypeOptions;
this.interestTypeData = this.loanProductsTemplate.interestTypeOptions;
this.interestCalculationPeriodTypeData = this.loanProductsTemplate.interestCalculationPeriodTypeOptions;
this.transactionProcessingStrategyData = this.loanProductsTemplate.transactionProcessingStrategyOptions;
this.daysInYearTypeData = this.loanProductsTemplate.daysInYearTypeOptions;
this.daysInMonthTypeData = this.loanProductsTemplate.daysInMonthTypeOptions;
this.preClosureInterestCalculationStrategyData = this.loanProductsTemplate.preClosureInterestCalculationStrategyOptions;
this.rescheduleStrategyTypeData = this.loanProductsTemplate.rescheduleStrategyTypeOptions;
this.interestRecalculationCompoundingTypeData = this.loanProductsTemplate.interestRecalculationCompoundingTypeOptions;
this.interestRecalculationFrequencyTypeData = this.loanProductsTemplate.interestRecalculationFrequencyTypeOptions;
this.interestRecalculationNthDayTypeData = this.loanProductsTemplate.interestRecalculationNthDayTypeOptions;
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.loanProductSettingsForm.patchValue({
'amortizationType': this.amortizationTypeData[0].id,
'interestType': this.interestTypeData[0].id,
'interestCalculationPeriodType': this.interestCalculationPeriodTypeData[0].id,
'transactionProcessingStrategyId': this.transactionProcessingStrategyData[0].id,
'daysInYearType': this.daysInYearTypeData[0].id,
'daysInMonthType': this.daysInMonthTypeData[0].id,
});
}
createLoanProductSettingsForm() {
this.loanProductSettingsForm = this.formBuilder.group({
'amortizationType': ['', Validators.required],
'interestType': ['', Validators.required],
'isEqualAmortization': [false],
'interestCalculationPeriodType': ['', Validators.required],
'transactionProcessingStrategyId': ['', Validators.required],
'graceOnPrincipalPayment': [''],
'graceOnInterestPayment': [''],
'graceOnInterestCharged': [''],
'inArrearsTolerance': [''],
'daysInYearType': ['', Validators.required],
'daysInMonthType': ['', Validators.required],
'canDefineInstallmentAmount': [false],
'graceOnArrearsAgeing': [''],
'overdueDaysForNPA': [''],
'accountMovesOutOfNPAOnlyOnArrearsCompletion': [false],
'principalThresholdForLastInstallment': [''],
'allowVariableInstallments': [false],
'canUseForTopup': [false],
'isInterestRecalculationEnabled': [false],
'holdGuaranteeFunds': [false],
'multiDisburseLoan': [false],
'allowAttributeConfiguration': [true],
'allowAttributeOverrides': this.formBuilder.group({
'amortizationType': [true],
'interestType': [true],
'transactionProcessingStrategyId': [true],
'interestCalculationPeriodType': [true],
'inArrearsTolerance': [true],
'repaymentEvery': [true],
'graceOnPrincipalAndInterestPayment': [true],
'graceOnArrearsAgeing': [true]
})
});
}
setConditionalControls() {
const allowAttributeOverrides = this.loanProductSettingsForm.get('allowAttributeOverrides');
this.loanProductSettingsForm.get('interestCalculationPeriodType').valueChanges
.subscribe((interestCalculationPeriodType: any) => {
if (interestCalculationPeriodType === 1) {
this.loanProductSettingsForm.addControl('allowPartialPeriodInterestCalcualtion', new FormControl(false));
} else {
this.loanProductSettingsForm.removeControl('allowPartialPeriodInterestCalcualtion');
}
});
this.loanProductSettingsForm.get('allowVariableInstallments').valueChanges
.subscribe((allowVariableInstallments: any) => {
if (allowVariableInstallments) {
this.loanProductSettingsForm.addControl('minimumGap', new FormControl('', Validators.required));
this.loanProductSettingsForm.addControl('maximumGap', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('minimumGap');
this.loanProductSettingsForm.removeControl('maximumGap');
}
});
this.loanProductSettingsForm.get('isInterestRecalculationEnabled').valueChanges
.subscribe((isInterestRecalculationEnabled: any) => {
if (isInterestRecalculationEnabled) {
this.loanProductSettingsForm.addControl('preClosureInterestCalculationStrategy', new FormControl(this.preClosureInterestCalculationStrategyData[0].id, Validators.required));
this.loanProductSettingsForm.addControl('rescheduleStrategyMethod', new FormControl(this.rescheduleStrategyTypeData[0].id, Validators.required));
this.loanProductSettingsForm.addControl('interestRecalculationCompoundingMethod', new FormControl(this.interestRecalculationCompoundingTypeData[0].id, Validators.required));
this.loanProductSettingsForm.addControl('recalculationRestFrequencyType', new FormControl(this.interestRecalculationFrequencyTypeData[0].id, Validators.required));
this.loanProductSettingsForm.addControl('isArrearsBasedOnOriginalSchedule', new FormControl(''));
// interestRecalculationCompoundingMethod (initially 0)
this.loanProductSettingsForm.get('interestRecalculationCompoundingMethod').valueChanges
.subscribe((interestRecalculationCompoundingMethod: any) => {
if (interestRecalculationCompoundingMethod !== 0) {
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyType', new FormControl(this.interestRecalculationFrequencyTypeData[0].id, Validators.required));
// recalculationCompoundingFrequencyType (initially 1)
this.loanProductSettingsForm.get('recalculationCompoundingFrequencyType').valueChanges
.subscribe((recalculationCompoundingFrequencyType: any) => {
if (recalculationCompoundingFrequencyType !== 1) {
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyInterval', new FormControl('', Validators.required));
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyDate', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyInterval');
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyDate');
}
if (recalculationCompoundingFrequencyType === 3) {
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyDayOfWeekType', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyDayOfWeekType');
}
if (recalculationCompoundingFrequencyType === 4) {
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyNthDayType', new FormControl(''));
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyDayOfWeekType', new FormControl(''));
// recalculationCompoundingFrequencyNthDayType
this.loanProductSettingsForm.get('recalculationCompoundingFrequencyNthDayType').valueChanges
.subscribe((recalculationCompoundingFrequencyNthDayType: any) => {
if (recalculationCompoundingFrequencyNthDayType === -2) {
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyOnDayType', new FormControl(''));
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyDayOfWeekType');
} else {
this.loanProductSettingsForm.addControl('recalculationCompoundingFrequencyDayOfWeekType', new FormControl(''));
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyOnDayType');
}
});
} else {
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyNthDayType');
}
});
} else {
this.loanProductSettingsForm.removeControl('recalculationCompoundingFrequencyType');
}
});
// recalculationRestFrequencyType
this.loanProductSettingsForm.get('recalculationRestFrequencyType').valueChanges
.subscribe((recalculationRestFrequencyType: any) => {
if (recalculationRestFrequencyType !== 1) {
this.loanProductSettingsForm.addControl('recalculationRestFrequencyInterval', new FormControl('', Validators.required));
this.loanProductSettingsForm.addControl('recalculationRestFrequencyDate', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyInterval');
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyDate');
}
if (recalculationRestFrequencyType === 3) {
this.loanProductSettingsForm.addControl('recalculationRestFrequencyDayOfWeekType', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyDayOfWeekType');
}
if (recalculationRestFrequencyType === 4) {
this.loanProductSettingsForm.addControl('recalculationRestFrequencyNthDayType', new FormControl(''));
this.loanProductSettingsForm.addControl('recalculationRestFrequencyDayOfWeekType', new FormControl(''));
// recalculationRestFrequencyNthDayType
this.loanProductSettingsForm.get('recalculationRestFrequencyNthDayType').valueChanges
.subscribe((recalculationRestFrequencyNthDayType: any) => {
if (recalculationRestFrequencyNthDayType === -2) {
this.loanProductSettingsForm.addControl('recalculationRestFrequencyOnDayType', new FormControl(''));
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyDayOfWeekType');
} else {
this.loanProductSettingsForm.addControl('recalculationRestFrequencyDayOfWeekType', new FormControl(''));
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyOnDayType');
}
});
} else {
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyNthDayType');
}
});
} else {
this.loanProductSettingsForm.removeControl('preClosureInterestCalculationStrategy');
this.loanProductSettingsForm.removeControl('rescheduleStrategyMethod');
this.loanProductSettingsForm.removeControl('interestRecalculationCompoundingMethod');
this.loanProductSettingsForm.removeControl('recalculationRestFrequencyType');
this.loanProductSettingsForm.removeControl('isArrearsBasedOnOriginalSchedule');
}
});
this.loanProductSettingsForm.get('holdGuaranteeFunds').valueChanges
.subscribe(holdGuaranteeFunds => {
if (holdGuaranteeFunds) {
this.loanProductSettingsForm.addControl('mandatoryGuarantee', new FormControl('', Validators.required));
this.loanProductSettingsForm.addControl('minimumGuaranteeFromOwnFunds', new FormControl(''));
this.loanProductSettingsForm.addControl('minimumGuaranteeFromGuarantor', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('mandatoryGuarantee');
this.loanProductSettingsForm.removeControl('minimumGuaranteeFromOwnFunds');
this.loanProductSettingsForm.removeControl('minimumGuaranteeFromGuarantor');
}
});
this.loanProductSettingsForm.get('multiDisburseLoan').valueChanges
.subscribe(multiDisburseLoan => {
if (multiDisburseLoan) {
this.loanProductSettingsForm.addControl('maxTrancheCount', new FormControl('', Validators.required));
this.loanProductSettingsForm.addControl('outstandingLoanBalance', new FormControl(''));
} else {
this.loanProductSettingsForm.removeControl('maxTrancheCount');
this.loanProductSettingsForm.removeControl('outstandingLoanBalance');
}
});
this.loanProductSettingsForm.get('allowAttributeConfiguration').valueChanges
.subscribe((allowAttributeConfiguration: any) => {
if (allowAttributeConfiguration) {
allowAttributeOverrides.patchValue({
'amortizationType': true,
'interestType': true,
'transactionProcessingStrategyId': true,
'interestCalculationPeriodType': true,
'inArrearsTolerance': true,
'repaymentEvery': true,
'graceOnPrincipalAndInterestPayment': true,
'graceOnArrearsAgeing': true
});
} else {
allowAttributeOverrides.patchValue({
'amortizationType': false,
'interestType': false,
'transactionProcessingStrategyId': false,
'interestCalculationPeriodType': false,
'inArrearsTolerance': false,
'repaymentEvery': false,
'graceOnPrincipalAndInterestPayment': false,
'graceOnArrearsAgeing': false
});
}
});
}
get loanProductSettings() {
const prevCompoundingFrequencyDate: Date = this.loanProductSettingsForm.value.recalculationCompoundingFrequencyDate;
const prevRestFrequencyDate: Date = this.loanProductSettingsForm.value.recalculationRestFrequencyDate;
// TODO: Update once language and date settings are setup
const dateFormat = 'yyyy-MM-dd';
if (prevCompoundingFrequencyDate) {
this.loanProductSettingsForm.patchValue({
recalculationCompoundingFrequencyDate: this.datePipe.transform(prevCompoundingFrequencyDate, dateFormat)
});
}
if (prevRestFrequencyDate) {
this.loanProductSettingsForm.patchValue({
recalculationRestFrequencyDate: this.datePipe.transform(prevRestFrequencyDate, dateFormat)
});
}
return this.loanProductSettingsForm.value;
}
}
@@ -0,0 +1,386 @@
<form [formGroup]="loanProductTermsForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="98%" class="mat-h4">Principal</h4>
<mat-form-field fxFlex="31%">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minPrincipal">
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Default</mat-label>
<input type="number" matInput formControlName="principal" required>
<mat-error>
Default Principal is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxPrincipal">
</mat-form-field>
<h4 fxFlex="98%" class="mat-h4">Number of repayments</h4>
<mat-form-field fxFlex="31%">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minNumberOfRepayments">
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Default</mat-label>
<input type="number" matInput formControlName="numberOfRepayments" required>
<mat-error>
Default number of repayments is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxNumberOfRepayments">
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Interest Rates</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="isLinkedToFloatingInterestRates">
Is Linked to floating interest rates?
</mat-checkbox>
<div *ngIf="!loanProductTermsForm.value.isLinkedToFloatingInterestRates" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="98%" class="mat-h4">Nominal interest rate</h4>
<mat-form-field fxFlex="23%">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minInterestRatePerPeriod">
</mat-form-field>
<mat-form-field fxFlex="23%">
<mat-label>Default</mat-label>
<input type="number" matInput formControlName="interestRatePerPeriod" required>
<mat-error>
Default nominal interest rate is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="23%">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxInterestRatePerPeriod">
</mat-form-field>
<mat-form-field fxFlex="23%">
<mat-label>Frequency</mat-label>
<mat-select formControlName="interestRateFrequencyType" required>
<mat-option *ngFor="let interestRateFrequencyType of interestRateFrequencyTypeData" [value]="interestRateFrequencyType.id">
{{ interestRateFrequencyType.value }}
</mat-option>
</mat-select>
<mat-error>
Nominal interest rate frequency is <strong>required</strong>
</mat-error>
</mat-form-field>
</div>
<div *ngIf="loanProductTermsForm.value.isLinkedToFloatingInterestRates" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" fxLayoutAlign.gt-sm="start center">
<h4 fxFlex="98%" class="mat-h4">Floating interest rate</h4>
<mat-form-field fxFlex="31%">
<mat-label>Floating Rate</mat-label>
<mat-select formControlName="floatingRatesId" required>
<mat-option *ngFor="let floatingRate of floatingRateData" [value]="floatingRate.id">
{{ floatingRate.name }}
</mat-option>
</mat-select>
<mat-error>
Floating rate is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Differential Rate</mat-label>
<input type="number" matInput formControlName="interestRateDifferential" required>
<mat-error>
Differential rate is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-checkbox fxFlex="31%" labelPosition="before" formControlName="isFloatingInterestRateCalculationAllowed">
Is Floating calculation allowed?
</mat-checkbox>
<mat-form-field fxFlex="31%">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minDifferentialLendingRate" required>
<mat-error>
Minimum interest rate is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Default</mat-label>
<input type="number" matInput formControlName="defaultDifferentialLendingRate" required>
<mat-error>
Default interest rate is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="31%">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxDifferentialLendingRate" required>
<mat-error>
Maximum interest rate is <strong>required</strong>
</mat-error>
</mat-form-field>
</div>
<mat-divider fxFlex="98%"></mat-divider>
<h3 fxFlex="23%" class="mat-h3">Variations</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="useBorrowerCycle">
Terms vary based on loan cycle
</mat-checkbox>
<div *ngIf="loanProductTermsForm.value.useBorrowerCycle" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="23%" class="mat-h4">Principal by loan cycle</h4>
<div fxFlex="73%">
<button type="button" mat-raised-button color="primary" (click)="addVariationsForBorrowerCycle('Principal', principalVariationsForBorrowerCycle)">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="principalVariationsForBorrowerCycle.value" *ngIf="principalVariationsForBorrowerCycle.value.length">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef> Condition </th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find:valueConditionTypeData:'id':'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef> Loan Cycle </th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef> Minimum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef> Default </th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef> Maximum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let variation; let i = index">
<button mat-icon-button color="primary" (click)="editVariationsForBorrowerCycle('Principal', principalVariationsForBorrowerCycle, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteVariationsForBorrowerCycle(principalVariationsForBorrowerCycle, i)">
<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>
<h4 fxFlex="23%" class="mat-h4">Number of repayments by loan cycle</h4>
<div fxFlex="73%">
<button type="button" mat-raised-button color="primary" (click)="addVariationsForBorrowerCycle('NumberOfRepayments', numberOfRepaymentVariationsForBorrowerCycle)">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="numberOfRepaymentVariationsForBorrowerCycle.value" *ngIf="numberOfRepaymentVariationsForBorrowerCycle.value.length">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef> Condition </th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find:valueConditionTypeData:'id':'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef> Loan Cycle </th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef> Minimum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef> Default </th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef> Maximum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let variation; let i = index">
<button mat-icon-button color="primary" (click)="editVariationsForBorrowerCycle('NumberOfRepayments', numberOfRepaymentVariationsForBorrowerCycle, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle, i)">
<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>
<h4 fxFlex="23%" class="mat-h4">Nominal interest rate by loan cycle</h4>
<div fxFlex="73%">
<button type="button" mat-raised-button color="primary" (click)="addVariationsForBorrowerCycle('NominalInterestRate', interestRateVariationsForBorrowerCycle)">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;
Add
</button>
</div>
<table fxFlex="98%" class="mat-elevation-z1" mat-table [dataSource]="interestRateVariationsForBorrowerCycle.value" *ngIf="interestRateVariationsForBorrowerCycle.value.length">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef> Condition </th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find:valueConditionTypeData:'id':'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef> Loan Cycle </th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef> Minimum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef> Default </th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef> Maximum </th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let element; let i = index">
<button mat-icon-button color="primary" (click)="editVariationsForBorrowerCycle('NominalInterestRate', interestRateVariationsForBorrowerCycle, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle, i)">
<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-divider fxFlex="98%"></mat-divider>
<h4 fxFlex="98%" class="mat-h4">Repaid every</h4>
<mat-form-field fxFlex="48%">
<mat-label>Frequency</mat-label>
<input type="number" matInput formControlName="repaymentEvery" required>
<mat-error>
Repaid every frequency is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Type</mat-label>
<mat-select formControlName="repaymentFrequencyType" required>
<mat-option *ngFor="let repaymentFrequencyType of repaymentFrequencyTypeData" [value]="repaymentFrequencyType.id">
{{ repaymentFrequencyType.value }}
</mat-option>
</mat-select>
<mat-error>
Repaid every type is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>Minimum days between disbursal and first repayment date</mat-label>
<input type="number" matInput formControlName="minimumDaysBetweenDisbursalAndFirstRepayment">
</mat-form-field>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left"></fa-icon>&nbsp;&nbsp;
Previous
</button>
<button mat-raised-button matStepperNext>
Next&nbsp;&nbsp;
<fa-icon icon="arrow-right"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,23 @@
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em 0;
}
h3, h4 {
font-weight: 500;
}
h4 {
margin: 1em 0;
}
mat-divider {
margin: 1em 0 2em 0;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoanProductTermsStepComponent } from './loan-product-terms-step.component';
describe('LoanProductTermsStepComponent', () => {
let component: LoanProductTermsStepComponent;
let fixture: ComponentFixture<LoanProductTermsStepComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoanProductTermsStepComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoanProductTermsStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,208 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormArray, FormControl } from '@angular/forms';
import { MatDialog } from '@angular/material';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
@Component({
selector: 'mifosx-loan-product-terms-step',
templateUrl: './loan-product-terms-step.component.html',
styleUrls: ['./loan-product-terms-step.component.scss']
})
export class LoanProductTermsStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
loanProductTermsForm: FormGroup;
valueConditionTypeData: any;
floatingRateData: any;
interestRateFrequencyTypeData: any;
repaymentFrequencyTypeData: any;
displayedColumns: string[] = ['valueConditionType', 'borrowerCycleNumber', 'minValue', 'defaultValue', 'maxValue', 'actions'];
constructor(private formBuilder: FormBuilder,
public dialog: MatDialog) {
this.createLoanProductTermsForm();
this.setConditionalControls();
}
ngOnInit() {
this.valueConditionTypeData = this.loanProductsTemplate.valueConditionTypeOptions;
this.floatingRateData = this.loanProductsTemplate.floatingRateOptions;
this.interestRateFrequencyTypeData = this.loanProductsTemplate.interestRateFrequencyTypeOptions;
this.repaymentFrequencyTypeData = this.loanProductsTemplate.repaymentFrequencyTypeOptions;
this.loanProductTermsForm.patchValue({
'interestRateFrequencyType': this.interestRateFrequencyTypeData[0].id,
'repaymentFrequencyType': this.repaymentFrequencyTypeData[0].id
});
}
createLoanProductTermsForm() {
this.loanProductTermsForm = this.formBuilder.group({
'useBorrowerCycle': [false],
'minPrincipal': [''],
'principal': ['', Validators.required],
'maxPrincipal': [''],
'minNumberOfRepayments': [''],
'numberOfRepayments': ['', Validators.required],
'maxNumberOfRepayments': [''],
'isLinkedToFloatingInterestRates': [false],
'minInterestRatePerPeriod': [''],
'interestRatePerPeriod': ['', Validators.required],
'maxInterestRatePerPeriod': [''],
'interestRateFrequencyType': ['', Validators.required],
'repaymentEvery': ['', Validators.required],
'repaymentFrequencyType': ['', Validators.required],
'minimumDaysBetweenDisbursalAndFirstRepayment': ['']
});
}
setConditionalControls() {
this.loanProductTermsForm.get('isLinkedToFloatingInterestRates').valueChanges
.subscribe(isLinkedToFloatingInterestRates => {
if (isLinkedToFloatingInterestRates) {
this.loanProductTermsForm.removeControl('minInterestRatePerPeriod');
this.loanProductTermsForm.removeControl('interestRatePerPeriod');
this.loanProductTermsForm.removeControl('maxInterestRatePerPeriod');
this.loanProductTermsForm.removeControl('interestRateFrequencyType');
this.loanProductTermsForm.addControl('floatingRatesId', new FormControl('', Validators.required));
this.loanProductTermsForm.addControl('interestRateDifferential', new FormControl('', Validators.required));
this.loanProductTermsForm.addControl('isFloatingInterestRateCalculationAllowed', new FormControl(false));
this.loanProductTermsForm.addControl('minDifferentialLendingRate', new FormControl('', Validators.required));
this.loanProductTermsForm.addControl('defaultDifferentialLendingRate', new FormControl('', Validators.required));
this.loanProductTermsForm.addControl('maxDifferentialLendingRate', new FormControl('', Validators.required));
} else {
this.loanProductTermsForm.addControl('minInterestRatePerPeriod', new FormControl(''));
this.loanProductTermsForm.addControl('interestRatePerPeriod', new FormControl('', Validators.required));
this.loanProductTermsForm.addControl('maxInterestRatePerPeriod', new FormControl(''));
this.loanProductTermsForm.addControl('interestRateFrequencyType', new FormControl(this.interestRateFrequencyTypeData[0].id, Validators.required));
this.loanProductTermsForm.removeControl('floatingRatesId');
this.loanProductTermsForm.removeControl('interestRateDifferential');
this.loanProductTermsForm.removeControl('isFloatingInterestRateCalculationAllowed');
this.loanProductTermsForm.removeControl('minDifferentialLendingRate');
this.loanProductTermsForm.removeControl('defaultDifferentialLendingRate');
this.loanProductTermsForm.removeControl('maxDifferentialLendingRate');
}
});
this.loanProductTermsForm.get('useBorrowerCycle').valueChanges
.subscribe(useBorrowerCycle => {
if (useBorrowerCycle) {
this.loanProductTermsForm.addControl('principalVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('numberOfRepaymentVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('interestRateVariationsForBorrowerCycle', this.formBuilder.array([]));
} else {
this.loanProductTermsForm.removeControl('principalVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('numberOfRepaymentVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('interestRateVariationsForBorrowerCycle');
}
});
}
get principalVariationsForBorrowerCycle(): FormArray {
return this.loanProductTermsForm.get('principalVariationsForBorrowerCycle') as FormArray;
}
get numberOfRepaymentVariationsForBorrowerCycle(): FormArray {
return this.loanProductTermsForm.get('numberOfRepaymentVariationsForBorrowerCycle') as FormArray;
}
get interestRateVariationsForBorrowerCycle(): FormArray {
return this.loanProductTermsForm.get('interestRateVariationsForBorrowerCycle') as FormArray;
}
addVariationsForBorrowerCycle(formType: string, variationsForBorrowerCycleFormArray: FormArray) {
const data = this.getData(formType);
const addVariationsForBorrowerCycleDialogRef = this.dialog.open(FormDialogComponent, { data });
addVariationsForBorrowerCycleDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
variationsForBorrowerCycleFormArray.push(response.data);
}
});
}
editVariationsForBorrowerCycle(formType: string, variationsForBorrowerCycleFormArray: FormArray, index: number) {
const data = { ...this.getData(formType, variationsForBorrowerCycleFormArray.at(index).value), layout: { addButtonText: 'Edit' } };
const addVariationsForBorrowerCycleDialogRef = this.dialog.open(FormDialogComponent, { data });
addVariationsForBorrowerCycleDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
variationsForBorrowerCycleFormArray.at(index).patchValue(response.data.value);
}
});
}
deleteVariationsForBorrowerCycle(variationsForBorrowerCycleFormArray: FormArray, index: number) {
const deleteVariationsForBorrowerCycleDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
deleteVariationsForBorrowerCycleDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
variationsForBorrowerCycleFormArray.removeAt(index);
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case 'Principal': return { title: 'Principal by loan cycle', formfields: this.getFormfields(values) };
case 'NumberOfRepayments': return { title: 'Number of repayments by loan cycle', formfields: this.getFormfields(values) };
case 'NominalInterestRate': return { title: 'Nominal interest rate by loan cycle', formfields: this.getFormfields(values) };
}
}
getFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'valueConditionType',
label: 'Condition',
value: values ? values.valueConditionType : this.valueConditionTypeData[0].id,
options: { label: 'value', value: 'id', data: this.valueConditionTypeData },
required: true,
order: 1
}),
new InputBase({
controlName: 'borrowerCycleNumber',
label: 'Loan Cycle',
value: values ? values.borrowerCycleNumber : undefined,
type: 'number',
required: true,
order: 2
}),
new InputBase({
controlName: 'minValue',
label: 'Minimum',
value: values ? values.minValue : undefined,
type: 'number',
order: 3
}),
new InputBase({
controlName: 'defaultValue',
label: 'Default',
value: values ? values.defaultValue : undefined,
type: 'number',
required: true,
order: 4
}),
new InputBase({
controlName: 'maxValue',
label: 'Maximum',
value: values ? values.maxValue : undefined,
type: 'number',
order: 5
})
];
return formfields;
}
get loanProductTerms() {
return this.loanProductTermsForm.value;
}
}
@@ -0,0 +1,24 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ProductsService } from '../products.service';
@Injectable()
export class LoanProductsTemplateResolver implements Resolve<Object> {
constructor(private productsService: ProductsService) {}
/**
* Returns the loan products template data.
* @returns {Observable<any>}
*/
resolve(): Observable<any> {
return this.productsService.getLoanProductsTemplate();
}
}
@@ -11,6 +11,7 @@ import { extract } from '../core/i18n/i18n.service';
/** Custom Components */
import { ProductsComponent } from './products.component';
import { LoanProductsComponent } from './loan-products/loan-products.component';
import { CreateLoanProductComponent } from './loan-products/create-loan-product/create-loan-product.component';
import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component';
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
import { ChargesComponent } from './charges/charges.component';
@@ -20,6 +21,7 @@ import { ManageTaxComponentsComponent } from './manage-tax-components/manage-tax
/** Custom Resolvers */
import { LoanProductsResolver } from './loan-products/loan-products.resolver';
import { LoanProductsTemplateResolver } from './loan-products/loan-products-template.resolver';
import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver';
import { ChargesResolver } from './charges/charges.resolver';
import { FixedDepositProductsResolver } from './fixed-deposit-products/fixed-deposit-products.resolver';
@@ -47,6 +49,14 @@ const routes: Routes = [
resolve: {
loanProducts: LoanProductsResolver
}
},
{
path: 'create',
component: CreateLoanProductComponent,
data: { title: extract('Create Loan Product'), breadcrumb: 'Create' },
resolve: {
loanProductsTemplate: LoanProductsTemplateResolver
}
}
]
},
@@ -115,6 +125,7 @@ const routes: Routes = [
exports: [RouterModule],
providers: [
LoanProductsResolver,
LoanProductsTemplateResolver,
RecurringDepositProductsResolver,
ChargesResolver,
FixedDepositProductsResolver,
+16
View File
@@ -10,6 +10,14 @@ import { PipesModule } from 'app/pipes/pipes.module';
/** Custom Components */
import { ProductsComponent } from './products.component';
import { LoanProductsComponent } from './loan-products/loan-products.component';
import { LoanProductDetailsStepComponent } from './loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component';
import { LoanProductCurrencyStepComponent } from './loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component';
import { LoanProductTermsStepComponent } from './loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component';
import { LoanProductSettingsStepComponent } from './loan-products//loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component';
import { LoanProductChargesStepComponent } from './loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component';
import { LoanProductAccountingStepComponent } from './loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component';
import { LoanProductPreviewStepComponent } from './loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component';
import { CreateLoanProductComponent } from './loan-products/create-loan-product/create-loan-product.component';
import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component';
import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component';
import { ChargesComponent } from './charges/charges.component';
@@ -31,6 +39,14 @@ import { ManageTaxComponentsComponent } from './manage-tax-components/manage-tax
declarations: [
ProductsComponent,
LoanProductsComponent,
LoanProductDetailsStepComponent,
LoanProductCurrencyStepComponent,
LoanProductTermsStepComponent,
LoanProductSettingsStepComponent,
LoanProductChargesStepComponent,
LoanProductAccountingStepComponent,
LoanProductPreviewStepComponent,
CreateLoanProductComponent,
ManageTaxConfigurationsComponent,
RecurringDepositProductsComponent,
ChargesComponent,
+8
View File
@@ -25,6 +25,14 @@ export class ProductsService {
return this.http.get('/loanproducts');
}
createLoanProduct(loanProduct: string): Observable<any> {
return this.http.post('/loanproducts', loanProduct);
}
getLoanProductsTemplate(): Observable<any> {
return this.http.get('/loanproducts/template');
}
/**
* @returns {Observable<any>} Recurring deposit products data
*/
+10
View File
@@ -7,12 +7,15 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import {
faAnchor,
faArchive,
faArrowLeft,
faArrowRight,
faBars,
faBell,
faBriefcase,
faBuilding,
faCalendar,
faChartBar,
faCheck,
faCheckCircle,
faChevronDown,
faChevronLeft,
@@ -26,6 +29,7 @@ import {
faEdit,
faEnvelope,
faExclamationCircle,
faExclamationTriangle,
faEye,
faEyeSlash,
faFileAlt,
@@ -44,6 +48,7 @@ import {
faMinusCircle,
faMoneyBillAlt,
faMoneyCheck,
faPencilAlt,
faPlay,
faPlus,
faPlusCircle,
@@ -77,12 +82,15 @@ import {
library.add(
faAnchor,
faArchive,
faArrowLeft,
faArrowRight,
faBars,
faBell,
faBriefcase,
faBuilding,
faCalendar,
faChartBar,
faCheck,
faCheckCircle,
faChevronDown,
faChevronLeft,
@@ -96,6 +104,7 @@ library.add(
faEdit,
faEnvelope,
faExclamationCircle,
faExclamationTriangle,
faEye,
faEyeSlash,
faFileAlt,
@@ -114,6 +123,7 @@ library.add(
faMinusCircle,
faMoneyBillAlt,
faMoneyCheck,
faPencilAlt,
faPlay,
faPlus,
faPlusCircle,
+8
View File
@@ -1,6 +1,7 @@
/** Angular Imports */
import { NgModule } from '@angular/core';
import { LayoutModule } from '@angular/cdk/layout';
import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
import { FlexLayoutModule } from '@angular/flex-layout';
import {
MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -86,6 +87,13 @@ import {
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: { appearance: 'standard' }
},
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: {
showError: true,
displayDefaultIndicatorType: false
}
}
]
})
+2
View File
@@ -71,3 +71,5 @@ $status-approved: #0f36e4;
$status-overpaid: #800080;
$status-active-overdue: #ff0000;
$status-matured: #ff4000;
$state-done: #00ce00;
+4
View File
@@ -1704,3 +1704,7 @@ textarea.noresize {
.status-matured {
color: $status-matured;
}
.mat-step-header .mat-step-icon-state-done {
background-color: $state-done;
}