From 97ccce531b7cfdf0201c0063e1052cbe9a58ba0c Mon Sep 17 00:00:00 2001 From: Abhay Chawla Date: Sat, 13 Jul 2019 16:43:05 +0530 Subject: [PATCH] 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 --- src/app/pipes/charges-filter.pipe.spec.ts | 8 + src/app/pipes/charges-filter.pipe.ts | 20 + .../pipes/charges-penalty-filter.pipe.spec.ts | 8 + src/app/pipes/charges-penalty-filter.pipe.ts | 20 + src/app/pipes/find.pipe.spec.ts | 8 + src/app/pipes/find.pipe.ts | 13 + src/app/pipes/pipes.module.ts | 11 +- .../create-loan-product.component.html | 102 +++ .../create-loan-product.component.scss | 4 + .../create-loan-product.component.spec.ts | 25 + .../create-loan-product.component.ts | 99 +++ ...oan-product-accounting-step.component.html | 329 ++++++++ ...oan-product-accounting-step.component.scss | 23 + ...-product-accounting-step.component.spec.ts | 25 + .../loan-product-accounting-step.component.ts | 231 ++++++ .../loan-product-charges-step.component.html | 140 ++++ .../loan-product-charges-step.component.scss | 19 + ...oan-product-charges-step.component.spec.ts | 25 + .../loan-product-charges-step.component.ts | 54 ++ .../loan-product-currency-step.component.html | 54 ++ .../loan-product-currency-step.component.scss | 3 + ...an-product-currency-step.component.spec.ts | 25 + .../loan-product-currency-step.component.ts | 39 + .../loan-product-details-step.component.html | 66 ++ .../loan-product-details-step.component.scss | 3 + ...oan-product-details-step.component.spec.ts | 25 + .../loan-product-details-step.component.ts | 54 ++ .../loan-product-preview-step.component.html | 730 ++++++++++++++++++ .../loan-product-preview-step.component.scss | 24 + ...oan-product-preview-step.component.spec.ts | 25 + .../loan-product-preview-step.component.ts | 25 + .../loan-product-settings-step.component.html | 466 +++++++++++ .../loan-product-settings-step.component.scss | 23 + ...an-product-settings-step.component.spec.ts | 25 + .../loan-product-settings-step.component.ts | 308 ++++++++ .../loan-product-terms-step.component.html | 386 +++++++++ .../loan-product-terms-step.component.scss | 23 + .../loan-product-terms-step.component.spec.ts | 25 + .../loan-product-terms-step.component.ts | 208 +++++ .../loan-products-template.resolver.ts | 24 + src/app/products/products-routing.module.ts | 11 + src/app/products/products.module.ts | 16 + src/app/products/products.service.ts | 8 + src/app/shared/icons.module.ts | 10 + src/app/shared/material.module.ts | 8 + src/assets/styles/_colours.scss | 2 + src/assets/styles/_helper.scss | 4 + 47 files changed, 3779 insertions(+), 5 deletions(-) create mode 100644 src/app/pipes/charges-filter.pipe.spec.ts create mode 100644 src/app/pipes/charges-filter.pipe.ts create mode 100644 src/app/pipes/charges-penalty-filter.pipe.spec.ts create mode 100644 src/app/pipes/charges-penalty-filter.pipe.ts create mode 100644 src/app/pipes/find.pipe.spec.ts create mode 100644 src/app/pipes/find.pipe.ts create mode 100644 src/app/products/loan-products/create-loan-product/create-loan-product.component.html create mode 100644 src/app/products/loan-products/create-loan-product/create-loan-product.component.scss create mode 100644 src/app/products/loan-products/create-loan-product/create-loan-product.component.spec.ts create mode 100644 src/app/products/loan-products/create-loan-product/create-loan-product.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.spec.ts create mode 100644 src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts create mode 100644 src/app/products/loan-products/loan-products-template.resolver.ts diff --git a/src/app/pipes/charges-filter.pipe.spec.ts b/src/app/pipes/charges-filter.pipe.spec.ts new file mode 100644 index 000000000..a16c680f7 --- /dev/null +++ b/src/app/pipes/charges-filter.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ChargesFilterPipe } from './charges-filter.pipe'; + +describe('ChargesFilterPipe', () => { + it('create an instance', () => { + const pipe = new ChargesFilterPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/charges-filter.pipe.ts b/src/app/pipes/charges-filter.pipe.ts new file mode 100644 index 000000000..aa8f7a1fe --- /dev/null +++ b/src/app/pipes/charges-filter.pipe.ts @@ -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; + } + +} diff --git a/src/app/pipes/charges-penalty-filter.pipe.spec.ts b/src/app/pipes/charges-penalty-filter.pipe.spec.ts new file mode 100644 index 000000000..25b021bf5 --- /dev/null +++ b/src/app/pipes/charges-penalty-filter.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ChargesPenaltyFilterPipe } from './charges-penalty-filter.pipe'; + +describe('ChargesPenaltyFilterPipe', () => { + it('create an instance', () => { + const pipe = new ChargesPenaltyFilterPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/charges-penalty-filter.pipe.ts b/src/app/pipes/charges-penalty-filter.pipe.ts new file mode 100644 index 000000000..f6571d5c8 --- /dev/null +++ b/src/app/pipes/charges-penalty-filter.pipe.ts @@ -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; + } + +} diff --git a/src/app/pipes/find.pipe.spec.ts b/src/app/pipes/find.pipe.spec.ts new file mode 100644 index 000000000..fb19e95a6 --- /dev/null +++ b/src/app/pipes/find.pipe.spec.ts @@ -0,0 +1,8 @@ +import { FindPipe } from './find.pipe'; + +describe('FindPipe', () => { + it('create an instance', () => { + const pipe = new FindPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/find.pipe.ts b/src/app/pipes/find.pipe.ts new file mode 100644 index 000000000..8d32e0eea --- /dev/null +++ b/src/app/pipes/find.pipe.ts @@ -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] : ''; + } + +} diff --git a/src/app/pipes/pipes.module.ts b/src/app/pipes/pipes.module.ts index 44cac041d..1ad28d333 100644 --- a/src/app/pipes/pipes.module.ts +++ b/src/app/pipes/pipes.module.ts @@ -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 { } diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.html b/src/app/products/loan-products/create-loan-product/create-loan-product.component.html new file mode 100644 index 000000000..7c2fb7374 --- /dev/null +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.html @@ -0,0 +1,102 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + DETAILS + + + + + + + + CURRENCY + + + + + + + + TERMS + + + + + + + + SETTINGS + + + + + + + + + CHARGES + + + + + + + + + ACCOUNTING + + + + + + + + + PREVIEW + + + + + + + + +
diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss b/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss new file mode 100644 index 000000000..fbc6592ec --- /dev/null +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss @@ -0,0 +1,4 @@ +fa-icon { + position: relative; + left: 5%; +} diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.spec.ts b/src/app/products/loan-products/create-loan-product/create-loan-product.component.spec.ts new file mode 100644 index 000000000..1991ac41d --- /dev/null +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CreateLoanProductComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateLoanProductComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts b/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts new file mode 100644 index 000000000..c7144a23e --- /dev/null +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.ts @@ -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 }); + }); + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html new file mode 100644 index 000000000..f6a088d8c --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html @@ -0,0 +1,329 @@ +
+ +
+ + + + {{ accountingRule }} + + + + + +
+ +

Assets

+ + + Fund source + + + {{ assetAccount.name }} + + + + Fund source is required + + + + + Loan portfolio + + + {{ assetAccount.name }} + + + + Loan portfolio is required + + + +
+ + + Interest Receivable + + + {{ assetAccount.name }} + + + + Interest Receivable is required + + + + + Fees Receivable + + + {{ assetAccount.name }} + + + + Fees Receivable is required + + + + + Penalties Receivable + + + {{ assetAccount.name }} + + + + Penalties Receivable is required + + + +
+ + + Transfer in suspense + + + {{ assetAccount.name }} + + + + Transfer in suspense is required + + + + + +

Income

+ + + Income from Interest + + + {{ incomeAccount.name }} + + + + Income from Interest is required + + + + + Income from fees + + + {{ incomeAccount.name }} + + + + Income from fees is required + + + + + Income from penalties + + + {{ incomeAccount.name }} + + + + Income from penalties is required + + + + + Income from Recovery Repayments + + + {{ incomeAccount.name }} + + + + Income from Recovery Repayments is required + + + + + +

Expenses

+ + + Losses written off + + + {{ expenseAccount.name }} + + + + Losses written off is required + + + + + +

Liabilities

+ + + Over payment liability + + + {{ liabilityAccount.name }} + + + + Over payment liability is required + + + + + +

Advanced Accounting Rules

+ + + +
+ +

Configure Fund Sources for Payment Channels

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
Payment Type + {{ paymentFundSource.paymentTypeId | find:paymentTypeData:'id':'name' }} + Fund Source + {{ paymentFundSource.fundSourceAccountId | find:assetAccountData:'id':'name' }} + Actions + + +
+ +

Map Fees to Specific Income Accounts

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
Fees + {{ feesIncome.chargeId | find:chargeData:'id':'name' }} + Income Account + {{ feesIncome.incomeAccountId | find:incomeAndLiabilityAccountData:'id':'name' }} + Actions + + +
+ +

Map Penalties to Specific Income Accounts

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
Penalty + {{ penaltyIncome.chargeId | find:penaltyData:'id':'name' }} + Income Account + {{ penaltyIncome.incomeAccountId | find:incomeAccountData:'id':'name' }} + Actions + + +
+ +
+ +
+ +
+ +
+ + +
+ +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss new file mode 100644 index 000000000..e9e5641d0 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss @@ -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; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.spec.ts new file mode 100644 index 000000000..c54daa1c3 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductAccountingStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductAccountingStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts new file mode 100644 index 000000000..1c71915e2 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.ts @@ -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; + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html new file mode 100644 index 000000000..f95ce4bfb --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html @@ -0,0 +1,140 @@ +
+ + + Charge + + + {{ charge.name }} + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ charge.name + ', ' + charge.currency.displaySymbol }} + Type + {{ charge.chargeCalculationType.value }} + Amount + {{ charge.amount }} + Collected On + {{ charge.chargeTimeType.value }} + Actions + +
+ + + +
+

Overdue Charges

+
+ + + Overdue Charge + + + {{ overdueCharge.name }} + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ overdueCharge.name + ', ' + overdueCharge.currency.displaySymbol }} + Type + {{ overdueCharge.chargeCalculationType.value }} + Amount + {{ overdueCharge.amount }} + Collected On + {{ overdueCharge.chargeTimeType.value }} + Actions + +
+ +
+ +
+ + +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss new file mode 100644 index 000000000..3da692340 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss @@ -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; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.spec.ts new file mode 100644 index 000000000..dabfa3948 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductChargesStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductChargesStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts new file mode 100644 index 000000000..9853dbc75 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.ts @@ -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 + }; + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.html new file mode 100644 index 000000000..92acedc58 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.html @@ -0,0 +1,54 @@ +
+ +
+ + + Currency + + + {{ currency.name }} + + + + Currency is required + + + + + Decimal Places + + + Decimal Places is required + + + + + Currency in multiples of + + + Currency in multiples of is required + + + + + Installment in multiples of + + + Installment in multiples of is required + + + +
+ +
+ + +
+ +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss new file mode 100644 index 000000000..aa8d23226 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss @@ -0,0 +1,3 @@ +.margin-t { + margin-top: 1em; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.spec.ts new file mode 100644 index 000000000..e4059542d --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductCurrencyStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductCurrencyStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.ts new file mode 100644 index 000000000..7ecec6100 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.ts @@ -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; + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html new file mode 100644 index 000000000..c1e2142fb --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html @@ -0,0 +1,66 @@ +
+ +
+ + + Product Name + + + Product Name is required + + + + + Short Name + + + Short Name is required + + + + + Fund + + + {{ fund.name }} + + + + + + Include in Customer Loan Counter + + + + Start Date + + + + + + + Close Date + + + + + + + Description + + + +
+ +
+ + +
+ +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss new file mode 100644 index 000000000..aa8d23226 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss @@ -0,0 +1,3 @@ +.margin-t { + margin-top: 1em; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.spec.ts new file mode 100644 index 000000000..46c1a2056 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductDetailsStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductDetailsStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts new file mode 100644 index 000000000..6fbf8ba18 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts @@ -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; + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html new file mode 100644 index 000000000..e6d53f45b --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.html @@ -0,0 +1,730 @@ +
+ +

{{ loanProduct.name }}

+ + + +

Details

+ + + +
+ Short Name: + {{ loanProduct.shortName }} +
+ +
+ Fund Name: + {{ loanProduct.fundId | find:loanProductsTemplate.fundOptions:'id':'name' }} +
+ +
+ Include in Customer Loan Counter: + {{ loanProduct.includeInBorrowerCycle ? 'Yes' : 'No' }} +
+ +
+ Start Date: + {{ loanProduct.startDate }} +
+ +
+ Close Date: + {{ loanProduct.closeDate }} +
+ +
+ Description: + {{ loanProduct.description }} +
+ +

Currency

+ + + +
+ Currency: + {{ loanProduct.currencyCode | find:loanProductsTemplate.currencyOptions:'code':'name' }} +
+ +
+ Decimal Places: + {{ loanProduct.digitsAfterDecimal }} +
+ +
+ Currency in multiples of: + {{ loanProduct.inMultiplesOf }} +
+ +
+ Installment in multiples of: + {{ loanProduct.installmentAmountInMultiplesOf }} +
+ +

Terms

+ + + +
+ Principal: + {{ loanProduct.principal + ' (Min: ' + (loanProduct.minPrincipal ? loanProduct.minPrincipal : loanProduct.principal) + ', Max: ' + (loanProduct.maxPrincipal ? loanProduct.maxPrincipal : loanProduct.principal) + ')' }} +
+ +
+ Number of Repayments: + {{ loanProduct.numberOfRepayments + ' (Min: ' + (loanProduct.minNumberOfRepayments ? loanProduct.minNumberOfRepayments : loanProduct.numberOfRepayments) + ', Max: ' + (loanProduct.maxNumberOfRepayments ? loanProduct.maxNumberOfRepayments : loanProduct.numberOfRepayments) + ')' }} +
+ +
+ Linked to floating interest rates: + {{ loanProduct.isLinkedToFloatingInterestRates ? 'Yes' : 'No' }} +
+ +
+ Nominal interest rate: + {{ loanProduct.interestRatePerPeriod + ' (Min: ' + (loanProduct.minInterestRatePerPeriod ? loanProduct.minInterestRatePerPeriod : loanProduct.interestRatePerPeriod) + ', Max: ' + (loanProduct.maxInterestRatePerPeriod ? loanProduct.maxInterestRatePerPeriod : loanProduct.interestRatePerPeriod) + ')' + ' ' + (loanProduct.interestRateFrequencyType | find:loanProductsTemplate.interestRateFrequencyTypeOptions:'id':'value') }} +
+ +
+ Floating Lending Interest Rate: + {{ loanProduct.floatingRatesId | find:loanProductsTemplate.floatingRateOptions:'id':'name' }} + Interest Rate Differential: + {{ loanProduct.interestRateDifferential }} + Floating Interest Rate Calculation Allowed: + {{ loanProduct.isFloatingInterestRateCalculationAllowed ? 'Yes' : 'No' }} + Floating Interest Rate: + {{ loanProduct.defaultDifferentialLendingRate + ' (Min: ' + (loanProduct.minDifferentialLendingRate ? loanProduct.minDifferentialLendingRate : loanProduct.defaultDifferentialLendingRate) + ', Max: ' + (loanProduct.maxDifferentialLendingRate ? loanProduct.maxDifferentialLendingRate : loanProduct.defaultDifferentialLendingRate) + ')' }} +
+ +
+ Terms vary based on loan cycle: + {{ loanProduct.useBorrowerCycle ? 'Yes' : 'No' }} +
+ +
+ +

Principal by loan cycle

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition + {{ variation.valueConditionType | find:loanProductsTemplate.valueConditionTypeOptions:'id':'value' }} + Loan Cycle + {{ variation.borrowerCycleNumber }} + Minimum + {{ variation.minValue }} + Default + {{ variation.defaultValue }} + Maximum + {{ variation.maxValue }} +
+ +
+ +
+ +

Number of Repayments by loan cycle

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition + {{ variation.valueConditionType | find:loanProductsTemplate.valueConditionTypeOptions:'id':'value' }} + Loan Cycle + {{ variation.borrowerCycleNumber }} + Minimum + {{ variation.minValue }} + Default + {{ variation.defaultValue }} + Maximum + {{ variation.maxValue }} +
+ +
+ +
+ +

Nominal Interest Rate by loan cycle

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition + {{ variation.valueConditionType | find:loanProductsTemplate.valueConditionTypeOptions:'id':'value' }} + Loan Cycle + {{ variation.borrowerCycleNumber }} + Minimum + {{ variation.minValue }} + Default + {{ variation.defaultValue }} + Maximum + {{ variation.maxValue }} +
+ +
+ +
+ Repay Every: + {{ loanProduct.repaymentEvery + ' ' + (loanProduct.repaymentFrequencyType | find:loanProductsTemplate.repaymentFrequencyTypeOptions:'id':'value') }} +
+ +
+ Minimum days between disbursal and first repayment date: + {{ loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment }} +
+ +

Settings

+ + + +
+ Amortization: + {{ loanProduct.amortizationType | find:loanProductsTemplate.amortizationTypeOptions:'id':'value' }} +
+ +
+ Is Equal Amortization: + {{ loanProduct.isEqualAmortization ? 'Yes' : 'No' }} +
+ +
+ Interest Method: + {{ loanProduct.interestType | find:loanProductsTemplate.interestTypeOptions:'id':'value' }} +
+ +
+ Interest Calculation Period: + {{ loanProduct.interestCalculationPeriodType | find:loanProductsTemplate.interestCalculationPeriodTypeOptions:'id':'value' }} +
+ +
+ Allow Partial Interest Calculation with same as repayment: + {{ loanProduct.allowPartialPeriodInterestCalcualtion ? 'Yes' : 'No' }} +
+ +
+ Repayment Strategy: + {{ loanProduct.transactionProcessingStrategyId | find:loanProductsTemplate.transactionProcessingStrategyOptions:'id':'name' }} +
+ +
+ Grace on principal payment: + {{ loanProduct.graceOnPrincipalPayment }} +
+ +
+ Grace on interest payment: + {{ loanProduct.graceOnInterestPayment }} +
+ +
+ Interest free period: + {{ loanProduct.graceOnInterestCharged }} +
+ +
+ Arrears tolerance: + {{ loanProduct.inArrearsTolerance }} +
+ +
+ Days in year: + {{ loanProduct.daysInYearType | find:loanProductsTemplate.daysInYearTypeOptions:'id':'value' }} +
+ +
+ Days in month: + {{ loanProduct.daysInMonthType | find:loanProductsTemplate.daysInMonthTypeOptions:'id':'value' }} +
+ +
+ Allow fixing of the installment amount: + {{ loanProduct.canDefineInstallmentAmount ? 'Yes' : 'No' }} +
+ +
+ Number of days a loan may be overdue before moving into arrears: + {{ loanProduct.graceOnArrearsAgeing }} +
+ +
+ Maximum number of days a loan may be overdue before becoming a NPA (non performing asset): + {{ loanProduct.overdueDaysForNPA }} +
+ +
+ Account moves out of NPA only after all arrears have been cleared: + {{ loanProduct.accountMovesOutOfNPAOnlyOnArrearsCompletion ? 'Yes' : 'No' }} +
+ +
+ Principal Threshold (%) for Last Instalment: + {{ loanProduct.principalThresholdForLastInstallment }} +
+ +
+ Variable Installments allowed: + {{ loanProduct.allowVariableInstallments ? 'Yes' : 'No' }} +
+ +
+ Variable Installments: + {{ '(Min: ' + loanProduct.minimumGap + ', Max: ' + (loanProduct.maximumGap ? loanProduct.maximumGap : loanProduct.minimumGap) + ')' }} +
+ +
+ Allowed to be used for providing Topup Loans: + {{ loanProduct.canUseForTopup ? 'Yes' : 'No' }} +
+ +

Interest Recalculation

+ + + +
+ Recalculate Interest: + {{ loanProduct.isInterestRecalculationEnabled ? 'Yes' : 'No' }} +
+ +
+ Pre-closure interest calculation rule: + {{ loanProduct.preClosureInterestCalculationStrategy | find:loanProductsTemplate.preClosureInterestCalculationStrategyOptions:'id':'value' }} + Advance payments adjustment type: + {{ loanProduct.rescheduleStrategyMethod | find:loanProductsTemplate.rescheduleStrategyTypeOptions:'id':'value' }} + Interest recalculation compounding on: + {{ loanProduct.interestRecalculationCompoundingMethod | find:loanProductsTemplate.interestRecalculationCompoundingTypeOptions:'id':'value' }} +
+ Frequency for compounding: + + {{ loanProduct.recalculationCompoundingFrequencyType | find:loanProductsTemplate.interestRecalculationFrequencyTypeOptions:'id':'value' }} +   {{ loanProduct.recalculationCompoundingFrequencyNthDayType | find:loanProductsTemplate.interestRecalculationNthDayTypeOptions:'id':'value' }} +   {{ loanProduct.recalculationCompoundingFrequencyDayOfWeekType | find:loanProductsTemplate.interestRecalculationDayOfWeekTypeOptions:'id':'value' }} +   {{ loanProduct.recalculationCompoundingFrequencyOnDayType }} + +
+ Frequency Interval for compounding: + {{ loanProduct.recalculationCompoundingFrequencyInterval }} +
+ Compounding Frequency Date: + {{ loanProduct.recalculationCompoundingFrequencyDate }} +
+
+
+
+ Frequency for recalculate Outstanding Principal: + + {{ loanProduct.recalculationRestFrequencyType | find:loanProductsTemplate.interestRecalculationFrequencyTypeOptions:'id':'value' }} +   {{ loanProduct.recalculationRestFrequencyNthDayType | find:loanProductsTemplate.interestRecalculationNthDayTypeOptions:'id':'value' }} +   {{ loanProduct.recalculationRestFrequencyDayOfWeekType | find:loanProductsTemplate.interestRecalculationDayOfWeekTypeOptions:'id':'value' }} +   {{ loanProduct.recalculationRestFrequencyOnDayType }} + +
+
+ Frequency Interval for recalculation: + {{ loanProduct.recalculationRestFrequencyInterval }} +
+ Rest Frequency Date: + {{ loanProduct.recalculationRestFrequencyDate }} +
+
+
+ Is Arrears recognization based on original schedule: + {{ loanProduct.isArrearsBasedOnOriginalSchedule ? 'Yes' : 'No' }} +
+
+ +

Guarantee Requirements

+ + + +
+ Place Guarantee Funds On-Hold: + {{ loanProduct.holdGuaranteeFunds ? 'Yes' : 'No' }} +
+ +
+ Mandatory Guarantee(%): + {{ loanProduct.mandatoryGuarantee }} +
+ Minimum Guarantee from Own Funds(%): + {{ loanProduct.minimumGuaranteeFromOwnFunds }} +
+
+ Minimum Guarantee from Guarantor Funds(%): + {{ loanProduct.minimumGuaranteeFromGuarantor }} +
+
+ +

Loan Tranche Details

+ + + +
+ Enable Multiple Disbursals: + {{ loanProduct.multiDisburseLoan ? 'Yes' : 'No' }} +
+ +
+ Maximum Tranche count: + {{ loanProduct.maxTrancheCount }} +
+ Maximum allowed outstanding balance: + {{ loanProduct.outstandingLoanBalance }} +
+
+ +

Configurable Terms and Settings

+ + + +
+ Allow overriding select terms and settings in loan accounts: + {{ loanProduct.allowAttributeConfiguration ? 'Yes' : 'No' }} +
+ +
+ Amortization: + {{ loanProduct.allowAttributeOverrides.amortizationType ? 'Yes' : 'No' }} + Interest method: + {{ loanProduct.allowAttributeOverrides.interestType ? 'Yes' : 'No' }} + Repayment strategy: + {{ loanProduct.allowAttributeOverrides.transactionProcessingStrategyId ? 'Yes' : 'No' }} + Interest calculation period: + {{ loanProduct.allowAttributeOverrides.interestCalculationPeriodType ? 'Yes' : 'No' }} + Arrears tolerance: + {{ loanProduct.allowAttributeOverrides.inArrearsTolerance ? 'Yes' : 'No' }} + Repaid every: + {{ loanProduct.allowAttributeOverrides.repaymentEvery ? 'Yes' : 'No' }} + Moratorium: + {{ loanProduct.allowAttributeOverrides.graceOnPrincipalAndInterestPayment ? 'Yes' : 'No' }} + Number of days a loan may be overdue before moving into arrears: + {{ loanProduct.allowAttributeOverrides.graceOnArrearsAgeing ? 'Yes' : 'No' }} +
+ +
+ +

Charges

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ charge.name + ', ' + charge.currency.displaySymbol }} + Type + {{ charge.chargeCalculationType.value }} + Amount + {{ charge.amount }} + Collected On + {{ charge.chargeTimeType.value }} +
+ +

Overdue Charges

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ overdueCharge.name + ', ' + overdueCharge.currency.displaySymbol }} + Type + {{ overdueCharge.chargeCalculationType.value }} + Amount + {{ overdueCharge.amount }} + Collected On + {{ overdueCharge.chargeTimeType.value }} +
+ +
+ +

Accounting

+ + + +
+ Type: + {{ accountingRuleData[loanProduct.accountingRule-1] }} +
+ +
+ +

Assets

+ +
+ Fund source: + {{ loanProduct.fundSourceAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} + Loan portfolio: + {{ loanProduct.loanPortfolioAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} +
+ Interest Receivable: + {{ loanProduct.receivableInterestAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} + Fees Receivable: + {{ loanProduct.receivableFeeAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} + Penalties Receivable: + {{ loanProduct.receivablePenaltyAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} +
+ Transfer in suspense: + {{ loanProduct.transfersInSuspenseAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} +
+ +

Income

+ +
+ Income from Interest: + {{ loanProduct.interestOnLoanAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }} + Income from fees: + {{ loanProduct.incomeFromFeeAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }} + Income from penalties: + {{ loanProduct.incomeFromPenaltyAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }} + Income from Recovery Repayments: + {{ loanProduct.incomeFromRecoveryAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }} +
+ +

Expenses

+ +
+ Losses written off: + {{ loanProduct.writeOffAccountId | find:loanProductsTemplate.accountingMappingOptions.expenseAccountOptions:'id':'name' }} +
+ +

Liabilities

+ +
+ Over payment liability: + {{ loanProduct.overpaymentLiabilityAccountId | find:loanProductsTemplate.accountingMappingOptions.liabilityAccountOptions:'id':'name' }} +
+ +
+ +

Advanced Accounting Rules

+ + + +
+ +

Fund Sources for Payment Channels

+ + + + + + + + + + + + + + + + +
Payment Type + {{ paymentFundSource.paymentTypeId | find:loanProductsTemplate.paymentTypeOptions:'id':'name' }} + Fund Source + {{ paymentFundSource.fundSourceAccountId | find:loanProductsTemplate.accountingMappingOptions.assetAccountOptions:'id':'name' }} +
+ +
+ +
+ +

Fees to Specific Income Accounts

+ + + + + + + + + + + + + + + + +
Fees + {{ feesIncome.chargeId | find:loanProductsTemplate.chargeOptions:'id':'name' }} + Income Account + {{ feesIncome.incomeAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions.concat(loanProductsTemplate.accountingMappingOptions.liabilityAccountOptions):'id':'name' }} +
+ +
+ +
+ +

Penalties to Specific Income Accounts

+ + + + + + + + + + + + + + + + +
Penalty + {{ penaltyIncome.chargeId | find:loanProductsTemplate.penaltyOptions:'id':'name' }} + Income Account + {{ penaltyIncome.incomeAccountId | find:loanProductsTemplate.accountingMappingOptions.incomeAccountOptions:'id':'name' }} +
+ +
+ +
+ +
+ +
+ +
+ + + +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss new file mode 100644 index 000000000..87258dacb --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.scss @@ -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; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.spec.ts new file mode 100644 index 000000000..ddc6589ec --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductPreviewStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductPreviewStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts new file mode 100644 index 000000000..6fdda9a55 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-preview-step/loan-product-preview-step.component.ts @@ -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() { + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html new file mode 100644 index 000000000..e45a682a0 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html @@ -0,0 +1,466 @@ +
+ +
+ + + Amortization + + + {{ amortizationType.value }} + + + + Amortization is required + + + + + Is Equal Amortization? + + + + Interest Method + + + {{ interestType.value }} + + + + Interest Method is required + + + + + Interest calculation period + + + {{ interestCalculationPeriodType.value }} + + + + Interest calculation period is required + + + + + Calculate interest for exact days in partial period + + + + Repayment strategy + + + {{ transactionProcessingStrategy.name }} + + + + Repayment strategy is required + + + + + +

Moratorium

+ + + Grace on principal payment + + + + + Grace on interest payment + + + + + + + Interest free period + + + + + Arrears tolerance + + + + + Days in year + + + {{ daysInYearType.value }} + + + + Days in year is required + + + + + Days in month + + + {{ daysInMonthType.value }} + + + + Days in month is required + + + + + Allow fixing of the installment amount + + +

Number of days a loan may be overdue before moving into arrears

+ + + On arrears aging + + + +

Maximum number of days a loan may be overdue before becoming a NPA (non-performing asset)

+ + + Overdue days for NPA + + + + + Account moves out of NPA only after all arrears have been cleared? + + + + Principal Threshold (%) for Last Instalment + + + + + Are Variable Installments allowed? + + +
+ +

Variable Installments

+ + + Minimum gap between Installments + + + Minimum gap between Installments is required + + + + + Maximum gap between Installments + + + +
+ + + Allowed to be used for providing Topup Loans + + + + +

Interest Recalculation

+ + + Recalculate Interest + + +
+ + + Pre-closure interest calculation rule + + + {{ preClosureInterestCalculationStrategy.value }} + + + + Pre-closure interest calculation rule is required + + + + + Advance payments adjustment type + + + {{ rescheduleStrategyType.value }} + + + + Advance payments adjustment type is required + + + + + Interest recalculation compounding on + + + {{ interestRecalculationCompoundingType.value }} + + + + Interest recalculation compounding on is required + + + + + +
+ + + Frequency Interval for compounding + + + Frequency Interval for compounding is required + + + + + Compounding Frequency Date + + + + + +
+ +
+ + + Frequency Interval for recalculation + + + Frequency Interval for recalculation is required + + + + + Rest Frequency Date + + + + + +
+ +
+ + + Frequency for compounding + + + {{ interestRecalculationFrequencyType.value }} + + + + +
+ + + + + {{ interestRecalculationNthDayType.value }} + + + + +
+ +
+ + + + + {{ interestRecalculationDayOfWeekType.value }} + + + + +
+ +
+ + + + + {{ interestRecalculationOnDayType }} + + + + +
+ +
+ + + Frequency for recalculate Outstanding Principal + + + {{ interestRecalculationFrequencyType.value }} + + + + Frequency for recalculate Outstanding Principal is required + + + +
+ + + + + {{ interestRecalculationNthDayType.value }} + + + + +
+ +
+ + + + + {{ interestRecalculationDayOfWeekType.value }} + + + + +
+ +
+ + + + + {{ interestRecalculationOnDayType }} + + + + +
+ + + Is Arrears recognization based on original schedule? + + +
+ + + +

Guarantee Requirements

+ + + Place Guarantee Funds On-Hold? + + +
+ + + Mandatory Guarantee(%) + + + Mandatory Guarantee is required + + + + + Minimum Guarantee from Own Funds(%) + + + + + Minimum Guarantee from Guarantor Funds(%) + + + +
+ + + +

Loan Tranche Details

+ + + Enable Multiple Disbursals + + +
+ + + Maximum Tranche count + + + Maximum Tranche count is required + + + + + Maximum allowed outstanding balance + + + +
+ + + +

Configurable Terms and Settings

+ + + Allow overriding select terms and settings in loan accounts + + +
+ + + Amortization + + + + Interest method + + + + Repayment strategy + + + + Interest calculation period + + + + Arrears tolerance + + + + Repaid every + + + + Moratorium + + + + Number of days a loan may be overdue before moving into arrears + + +
+ +
+ +
+ + +
+ +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss new file mode 100644 index 000000000..0902d871d --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss @@ -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; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.spec.ts new file mode 100644 index 000000000..30d2addd2 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductSettingsStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductSettingsStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts new file mode 100644 index 000000000..67b9d9d47 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts @@ -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; + } + +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html new file mode 100644 index 000000000..75cd87737 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html @@ -0,0 +1,386 @@ +
+ +
+ +

Principal

+ + + Minimum + + + + + Default + + + Default Principal is required + + + + + Maximum + + + +

Number of repayments

+ + + Minimum + + + + + Default + + + Default number of repayments is required + + + + + Maximum + + + + + +

Interest Rates

+ + + Is Linked to floating interest rates? + + +
+ +

Nominal interest rate

+ + + Minimum + + + + + Default + + + Default nominal interest rate is required + + + + + Maximum + + + + + Frequency + + + {{ interestRateFrequencyType.value }} + + + + Nominal interest rate frequency is required + + + +
+ +
+ +

Floating interest rate

+ + + Floating Rate + + + {{ floatingRate.name }} + + + + Floating rate is required + + + + + Differential Rate + + + Differential rate is required + + + + + Is Floating calculation allowed? + + + + Minimum + + + Minimum interest rate is required + + + + + Default + + + Default interest rate is required + + + + + Maximum + + + Maximum interest rate is required + + + +
+ + + +

Variations

+ + + Terms vary based on loan cycle + + +
+ +

Principal by loan cycle

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition + {{ variation.valueConditionType | find:valueConditionTypeData:'id':'value' }} + Loan Cycle + {{ variation.borrowerCycleNumber }} + Minimum + {{ variation.minValue }} + Default + {{ variation.defaultValue }} + Maximum + {{ variation.maxValue }} + Actions + + +
+ +

Number of repayments by loan cycle

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition + {{ variation.valueConditionType | find:valueConditionTypeData:'id':'value' }} + Loan Cycle + {{ variation.borrowerCycleNumber }} + Minimum + {{ variation.minValue }} + Default + {{ variation.defaultValue }} + Maximum + {{ variation.maxValue }} + Actions + + +
+ +

Nominal interest rate by loan cycle

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition + {{ variation.valueConditionType | find:valueConditionTypeData:'id':'value' }} + Loan Cycle + {{ variation.borrowerCycleNumber }} + Minimum + {{ variation.minValue }} + Default + {{ variation.defaultValue }} + Maximum + {{ variation.maxValue }} + Actions + + +
+ +
+ + + +

Repaid every

+ + + Frequency + + + Repaid every frequency is required + + + + + Type + + + {{ repaymentFrequencyType.value }} + + + + Repaid every type is required + + + + + Minimum days between disbursal and first repayment date + + + +
+ +
+ + +
+ +
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss new file mode 100644 index 000000000..8381fd426 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss @@ -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; +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.spec.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.spec.ts new file mode 100644 index 000000000..7c563f65b --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanProductTermsStepComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanProductTermsStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts new file mode 100644 index 000000000..aed23e309 --- /dev/null +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts @@ -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; + } + +} diff --git a/src/app/products/loan-products/loan-products-template.resolver.ts b/src/app/products/loan-products/loan-products-template.resolver.ts new file mode 100644 index 000000000..e5897d895 --- /dev/null +++ b/src/app/products/loan-products/loan-products-template.resolver.ts @@ -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 { + + constructor(private productsService: ProductsService) {} + + /** + * Returns the loan products template data. + * @returns {Observable} + */ + resolve(): Observable { + return this.productsService.getLoanProductsTemplate(); + } + +} diff --git a/src/app/products/products-routing.module.ts b/src/app/products/products-routing.module.ts index 1b825d5d0..d89cfc913 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -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, diff --git a/src/app/products/products.module.ts b/src/app/products/products.module.ts index b8776dc29..3038abffd 100644 --- a/src/app/products/products.module.ts +++ b/src/app/products/products.module.ts @@ -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, diff --git a/src/app/products/products.service.ts b/src/app/products/products.service.ts index f271dc5f1..48f38a515 100644 --- a/src/app/products/products.service.ts +++ b/src/app/products/products.service.ts @@ -25,6 +25,14 @@ export class ProductsService { return this.http.get('/loanproducts'); } + createLoanProduct(loanProduct: string): Observable { + return this.http.post('/loanproducts', loanProduct); + } + + getLoanProductsTemplate(): Observable { + return this.http.get('/loanproducts/template'); + } + /** * @returns {Observable} Recurring deposit products data */ diff --git a/src/app/shared/icons.module.ts b/src/app/shared/icons.module.ts index 0747552f4..5bfe5e6a9 100644 --- a/src/app/shared/icons.module.ts +++ b/src/app/shared/icons.module.ts @@ -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, diff --git a/src/app/shared/material.module.ts b/src/app/shared/material.module.ts index d77e1e113..7093f8481 100644 --- a/src/app/shared/material.module.ts +++ b/src/app/shared/material.module.ts @@ -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 + } } ] }) diff --git a/src/assets/styles/_colours.scss b/src/assets/styles/_colours.scss index b38b6a8ce..a72466c07 100644 --- a/src/assets/styles/_colours.scss +++ b/src/assets/styles/_colours.scss @@ -71,3 +71,5 @@ $status-approved: #0f36e4; $status-overpaid: #800080; $status-active-overdue: #ff0000; $status-matured: #ff4000; + +$state-done: #00ce00; diff --git a/src/assets/styles/_helper.scss b/src/assets/styles/_helper.scss index c3ac6ec44..e3bf0328f 100644 --- a/src/assets/styles/_helper.scss +++ b/src/assets/styles/_helper.scss @@ -1704,3 +1704,7 @@ textarea.noresize { .status-matured { color: $status-matured; } + +.mat-step-header .mat-step-icon-state-done { + background-color: $state-done; +}