fix: Add possibility to create GLIM account applications (#2323)

This is a first step to support GLIM account applications. It enables
a user to create a GLIM account application and provides a
starting point for a discussion of the feature, which has not even
been working for the community app and is insufficiently documented.
This is part of a larger effort to improve group account management.

Partially addresses: WEB-9
This commit is contained in:
Felix van Hove
2025-03-16 14:49:49 -06:00
committed by GitHub
parent 9d2c42d55b
commit 8632abb34c
13 changed files with 1292 additions and 245 deletions
@@ -27,6 +27,7 @@
[loansAccountTemplate]="loansAccountTemplate"
(loansAccountProductTemplate)="setTemplate($event)"
[gsimData]="gsimData"
#loansAccountDetailsForm
>
</mifosx-glim-details-step>
</mat-step>
@@ -35,8 +36,10 @@
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-glim-terms-step
#loanAccountTerms
[loansAccountProductTemplate]="loansAccountProductTemplate"
[loansAccountTemplate]="loansAccountTemplate"
[loansAccountFormValid]="loansAccountFormValid"
>
</mifosx-glim-terms-step>
</mat-step>
@@ -45,8 +48,10 @@
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-glim-charges-step
#loanAccountCharges
[loansAccountProductTemplate]="loansAccountProductTemplate"
[loansAccountTemplate]="loansAccountTemplate"
[loansAccountFormValid]="loansAccountFormValid"
[activeClientMembers]="activeClientMembers"
>
</mifosx-glim-charges-step>
@@ -1,9 +1,12 @@
import { Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
/** Angular Imports */
import { Component, ViewChild, /* Felix: added: */ QueryList, ViewChildren } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */
import { LoansService } from 'app/loans/loans.service';
import { SettingsService } from 'app/settings/settings.service';
/** Step Components */
import { GlimChargesStepComponent } from './glim-account-stepper/glim-charges-step/glim-charges-step.component';
import { GlimDetailsStepComponent } from './glim-account-stepper/glim-details-step/glim-details-step.component';
import { GlimTermsStepComponent } from './glim-account-stepper/glim-terms-step/glim-terms-step.component';
@@ -14,7 +17,7 @@ import { GlimTermsStepComponent } from './glim-account-stepper/glim-terms-step/g
styleUrls: ['./create-glim-account.component.scss']
})
export class CreateGlimAccountComponent {
/** Imports all the step component */
/** Imports all the step components */
@ViewChild(GlimDetailsStepComponent, { static: true }) loansAccountDetailsStep: GlimDetailsStepComponent;
@ViewChild(GlimTermsStepComponent, { static: true }) loansAccountTermsStep: GlimTermsStepComponent;
@ViewChild(GlimChargesStepComponent, { static: true }) loansAccountChargesStep: GlimChargesStepComponent;
@@ -22,38 +25,39 @@ export class CreateGlimAccountComponent {
/** Loans Account Template */
loansAccountTemplate: any;
/** Loans Account Product Template */
loansAccountProductTemplate: any;
/** Collateral Options */
collateralOptions: any;
loansAccountProductTemplate: any | null = null;
/** Multi Disburse Loan */
multiDisburseLoan: any;
/** Principal Amount */
principal: any;
/** Currency Code */
currencyCode: string;
accountLinkingOptions: any;
// accountLinkingOptions: any;
totalLoan: any;
// totalLoan: any;
/** Table Data Source */
dataSource: any;
/** Selected Members */
// /** Table Data Source */
// dataSource: any;
// /** Selected Members */
selectedMembers: any;
/** Selected Members */
// /** Selected Members */
activeClientMembers: any;
gsimData: any;
/**
* Sets loans account details form.
* @param {FormBuilder} formBuilder Form Builder.
* @param {LoansService} loansService Loans Service.
* @param {SettingsService} settingsService SettingsService
* Sets loans account create form.
* @param {route} ActivatedRoute Activated Route.
* @param {router} Router Router.
* @param {loansService} LoansService Loans Service
* @param {SettingsService} settingsService Settings Service
* @param {ClientsService} clientService Client Service
*/
constructor(
private route: ActivatedRoute,
private router: Router,
private loansService: LoansService,
private settingsService: SettingsService,
public dialog: MatDialog,
private dateUtils: Dates
private settingsService: SettingsService
) {
this.route.data.subscribe((data: { loansAccountTemplate: any; gsimData: any; groupsData: any }) => {
this.loansAccountTemplate = data.loansAccountTemplate;
@@ -63,11 +67,19 @@ export class CreateGlimAccountComponent {
}
/**
* Sets loans account product template and collateral template
* Sets glim account product template and collateral template
* @param {any} $event API response
*/
setTemplate($event: any) {
this.loansAccountProductTemplate = $event;
this.currencyCode = this.loansAccountProductTemplate.currency.code;
const entityId = this.loansAccountTemplate.clientId
? this.loansAccountTemplate.clientId
: this.loansAccountTemplate.group.id;
const productId = this.loansAccountProductTemplate.loanProductId;
this.loansService.getLoansAccountTemplateResource(entityId, true, productId).subscribe((response: any) => {
this.multiDisburseLoan = response.multiDisburseLoan;
});
}
/** Get Loans Account Details Form Data */
@@ -80,86 +92,102 @@ export class CreateGlimAccountComponent {
return this.loansAccountTermsStep.loansAccountTermsForm;
}
/** Checks wheter all the forms in different steps are valid or not */
/** Checks whether all the forms in different steps are valid or not */
get loansAccountFormValid() {
return this.loansAccountDetailsForm.valid && this.loansAccountTermsForm.valid;
return (
this.loansAccountDetailsForm.valid && this.loansAccountTermsForm.valid && this.loansAccountChargesStep.isValid
);
}
/** Gets principal Amount */
get loanPrincipal() {
return this.loansAccountTermsStep.loansAccountTermsForm.value.principal;
return this.loansAccountChargesStep.selectedClientMembers.selectedMembers.reduce(
(acc: number, member: any) => acc + member.principal,
0
);
}
/** Retrieves Data of all forms except Currency to submit the data */
get loansAccount() {
this.selectedMembers = this.loansAccountChargesStep.selectedClientMembers;
return {
...this.loansAccountDetailsStep.loansAccountDetails,
...this.loansAccountTermsStep.loansAccountTerms,
...this.loansAccountChargesStep.loansAccountCharges
...this.loansAccountChargesStep.loansAccountCharges,
...this.loansAccountTermsStep.disbursementData
};
}
setData(client: any): any {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
// const monthDayFormat = 'dd MMMM';
const data = {
...this.loansAccount,
charges: this.loansAccount.charges.map((charge: any) => ({
chargeId: charge.id,
amount: charge.amount
})),
clientId: client.id,
totalLoan: this.totalLoan,
loanType: 'glim',
amortizationType: 1,
isParentAccount: true,
principal: client.principal,
syncDisbursementWithMeeting: false,
expectedDisbursementDate: this.dateUtils.formatDate(this.loansAccount.expectedDisbursementDate, dateFormat),
submittedOnDate: this.dateUtils.formatDate(this.loansAccount.submittedOnDate, dateFormat),
dateFormat,
// monthDayFormat,
locale
};
data.groupId = this.loansAccountTemplate.group.id;
// setData(client: any): any {
// const locale = this.settingsService.language.code;
// const dateFormat = this.settingsService.dateFormat;
// // const monthDayFormat = 'dd MMMM';
// const data = {
// ...this.loansAccount,
// charges: this.loansAccount.charges.map((charge: any) => ({
// chargeId: charge.id,
// amount: charge.amount
// })),
// clientId: client.id,
// totalLoan: this.totalLoan,
// loanType: 'glim',
// amortizationType: 1,
// isParentAccount: true,
// principal: client.principal,
// syncDisbursementWithMeeting: false,
// expectedDisbursementDate: this.dateUtils.formatDate(this.loansAccount.expectedDisbursementDate, dateFormat),
// submittedOnDate: this.dateUtils.formatDate(this.loansAccount.submittedOnDate, dateFormat),
// dateFormat,
// // monthDayFormat,
// locale
// };
// data.groupId = this.loansAccountTemplate.group.id;
return JSON.stringify(data);
}
/** Request Body Data */
buildRequestData(): any[] {
const requestData = [];
const memberSelected = this.selectedMembers.selectedMembers;
this.totalLoanAmount();
for (let index = 0; index < memberSelected.length; index++) {
requestData.push({
requestId: index.toString(),
method: 'POST',
relativeUrl: 'loans',
body: this.setData(memberSelected[index])
});
}
return requestData;
}
totalLoanAmount(): any {
let total = 0;
const memberSelected = this.selectedMembers.selectedMembers;
for (let index = 0; index < memberSelected.length; index++) {
total += memberSelected[index].principal;
}
this.totalLoan = total;
}
// return JSON.stringify(data);
// }
/**
* Creates a new GSIM account.
* Creates a new GLIM account.
*/
submit() {
const data = this.buildRequestData();
this.loansService.createGlimAccount(data).subscribe((response: any) => {
this.router.navigate(['../../../'], { relativeTo: this.route });
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const totalLoan = this.loanPrincipal;
const payloadData: any[] = [];
this.loansAccountChargesStep.selectedClientMembers.selectedMembers.forEach((member: any, index: number) => {
this.loansAccountTemplate.clientId = member.id;
this.loansAccountTemplate.loanPurposeId = member.loanPurposeId;
this.loansAccountTemplate.principal = member.principal;
this.loansAccountTemplate.totalLoan = totalLoan;
const payload = this.loansService.buildLoanRequestPayload(
this.loansAccount,
this.loansAccountTemplate,
this.loansAccountProductTemplate.calendarOptions,
locale,
dateFormat
);
payloadData.push(this.getWrapper(payload, index));
});
this.loansService.createGlimAccount(payloadData).subscribe((response: any) => {
this.router.navigate(
[
'../',
JSON.parse(response[0].body).glimId
],
{ relativeTo: this.route }
);
});
}
getWrapper(payload: any, requestId: number): any {
return {
body: JSON.stringify(payload),
method: 'POST',
relativeUrl: 'loans',
requestId: requestId
};
}
}
@@ -91,21 +91,6 @@
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.feeInterval || 'Not Provided' }}
<button
mat-icon-button
color="primary"
*ngIf="charge.chargeTimeType.value === 'Weekly Fee' || charge.chargeTimeType.value === 'Monthly Fee'"
(click)="editChargeFeeInterval(charge)"
>
<fa-icon icon="pen"></fa-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@@ -115,8 +100,35 @@
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.heading.Overdue Charges' | translate }}</h4>
<table mat-table class="mat-elevation-z1" [dataSource]="overDueChargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name }},{{ charge.currency.displaySymbol }}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeCalculationType.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
</ng-container>
<ng-container matColumnDef="collectedon">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType.value }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="overdueChargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: overdueChargesDisplayedColumns"></tr>
</table>
<div fxFlex="98%" *ngIf="activeClientMembers">
@@ -179,8 +191,11 @@
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
<button mat-raised-button matStepperNext [disabled]="!loansAccountFormValid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<button mat-raised-button *ngIf="loanId" [routerLink]="['../', 'general']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
</div>
@@ -1,13 +1,23 @@
/** Angular Imports */
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { Dates } from 'app/core/utils/dates';
/** Dialog Components */
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
/** Custom Services */
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { ActivatedRoute } from '@angular/router';
/**
* GLIM Account Charges Step
*/
@Component({
selector: 'mifosx-glim-charges-step',
templateUrl: './glim-charges-step.component.html',
@@ -18,29 +28,34 @@ export class GlimChargesStepComponent implements OnInit, OnChanges {
@Input() loansAccountProductTemplate: any;
/** Savings Account Template */
@Input() loansAccountTemplate: any;
/** Currency Code */
// @Input() currencyCode: FormControl;
// /** active Client Members in case of GLIM Account */
// @Input() loansAccountFormValid: LoansAccountFormValid
@Input() loansAccountFormValid: boolean;
// active Client Members in case of GLIM Account
@Input() activeClientMembers?: any;
/** Charge Data */
chargeData: any = [];
/** Charges Data Source */
chargesDataSource: {}[] = [];
/** Component is pristine if there has been no changes by user interaction */
pristine = true;
/** For Edit Savings Account Form */
isChargesPatched = false;
/** Overdue Charges Data Source */
overDueChargesDataSource: {}[] = [];
/** Display columns for charges table */
displayedColumns: string[] = [
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'date',
'repaymentsEvery',
'action'
];
/** Columns to be displayed in overdue charges table. */
overdueChargesDisplayedColumns: string[] = [
'name',
'type',
'amount',
'collectedon'
];
/** Table Data Source */
dataSource: any;
/** Check for select all the Clients List */
@@ -55,22 +70,53 @@ export class GlimChargesStepComponent implements OnInit, OnChanges {
'purpose',
'amount'
];
/** Component is pristine if there has been no changes by user interaction */
pristine = true;
loanId: any = null;
/**
* @param {MatDialog} dialog Mat Dialog
* GLIM Account Charges Form Step
* @param {dialog} MatDialog Mat Dialog
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {SettingsService} settingsService Settings Service
*/
constructor(
public dialog: MatDialog,
private dateUtils: Dates
) {}
private dateUtils: Dates,
private route: ActivatedRoute,
private settingsService: SettingsService
) {
this.loanId = this.route.snapshot.params['loanId'];
}
ngOnInit() {
// TODO: This might still need adjustments due to GLIM
this.dataSource = new MatTableDataSource<any>(this.activeClientMembers);
// Loan Account code:
if (this.loansAccountTemplate.charges) {
this.chargesDataSource =
this.loansAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
}
}
ngOnChanges() {
if (this.loansAccountProductTemplate) {
this.loanPurposeOptions = this.loansAccountProductTemplate.loanPurposeOptions;
this.chargeData = this.loansAccountProductTemplate.chargeOptions;
if (this.loansAccountProductTemplate.overdueCharges) {
this.overDueChargesDataSource = this.loansAccountProductTemplate.overdueCharges;
}
if (
this.loansAccountProductTemplate.charges &&
this.loansAccountProductTemplate.charges.length > 0 &&
this.chargesDataSource.length === 0
) {
this.chargesDataSource =
this.loansAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
}
}
}
@@ -139,7 +185,7 @@ export class GlimChargesStepComponent implements OnInit, OnChanges {
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
let newCharge: any;
const dateFormat = 'dd MMMM yyyy';
const dateFormat = this.settingsService.dateFormat;
const date = this.dateUtils.formatDate(response.data.value.date, dateFormat);
switch (charge.chargeTimeType.value) {
case 'Specified due date':
@@ -193,9 +239,16 @@ export class GlimChargesStepComponent implements OnInit, OnChanges {
* @param {any} charge Charge
*/
deleteCharge(charge: any) {
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
this.chargesDataSource = this.chargesDataSource.concat([]);
this.pristine = false;
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([]);
this.pristine = false;
}
});
}
/**
@@ -209,14 +262,19 @@ export class GlimChargesStepComponent implements OnInit, OnChanges {
return { selectedMembers: this.activeClientMembers.filter((item: any) => item.selected) };
}
/** Toggle all checks */
get isValid() {
const sum = this.activeClientMembers
?.filter((item: any) => item.selected)
.reduce((acc: number, member: any) => acc + member.principal, 0);
return sum > 0;
}
toggleSelects() {
for (const member of this.activeClientMembers) {
member.selected = this.selectAllItems;
}
}
/** Check if all the checks are selected */
toggleSelect() {
const len = this.activeClientMembers.length;
this.selectAllItems =
@@ -1,9 +1,16 @@
<form [formGroup]="loansAccountDetailsForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-form-field fxFlex="72%">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<mat-select formControlName="productId" required>
<mat-option *ngFor="let product of productData" [value]="product.id">
<mat-select
required
matTooltip="{{ 'tooltips.Name of the loan product' | translate }}"
formControlName="productId"
>
<mat-option>
<ngx-mat-select-search [formControl]="filterFormCtrl"></ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let product of productData | async" [value]="product.id">
{{ product.name }}
</mat-option>
</mat-select>
@@ -12,10 +19,15 @@
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" *ngIf="loanProductSelected">
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Loan officer' | translate }}</mat-label>
<mat-select formControlName="loanOfficerId">
<mat-select
formControlName="loanOfficerId"
matTooltip="{{ 'tooltips.Financial institution representative' | translate }}"
>
<mat-option *ngFor="let loanOfficer of loanOfficerOptions" [value]="loanOfficer.id">
{{ loanOfficer.displayName }}
</mat-option>
@@ -38,6 +50,7 @@
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submitPicker"
matTooltip="{{ 'tooltips.Date the loan account application' | translate }}"
required
formControlName="submittedOnDate"
/>
@@ -56,6 +69,7 @@
[min]="loansAccountDetailsForm.value.submittedOnDate"
[max]="maxDate"
[matDatepicker]="disbursementPicker"
matTooltip="{{ 'tooltips.Date that the loan account disbursed' | translate }}"
required
formControlName="expectedDisbursementDate"
/>
@@ -67,15 +81,11 @@
</mat-error>
</mat-form-field>
<mat-divider fxFlex="98%"></mat-divider>
<h3 class="mat-h3" fxFlexFill>{{ 'labels.heading.Savings Linkage' | translate }}</h3>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Link savings' | translate }}</mat-label>
<mat-select formControlName="linkAccountId">
<mat-option *ngFor="let savingaccount of gsimData" [value]="savingaccount.id">
{{ savingaccount.accountNumber }}
{{ savingaccount.accountNumber }} {{ savingaccount.productName }}
</mat-option>
</mat-select>
</mat-form-field>
@@ -90,5 +100,8 @@
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<button mat-raised-button *ngIf="loanId" [routerLink]="['../', 'general']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
</div>
</form>
@@ -1,14 +1,28 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { LoansService } from 'app/loans/loans.service';
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { SettingsService } from 'app/settings/settings.service';
/** Custom Services */
import { LoansService } from '../../../../loans.service';
import { Commons } from 'app/core/utils/commons';
import { takeUntil } from 'rxjs/operators';
import { ReplaySubject, Subject } from 'rxjs';
// Felix: TODO's:
// - gsimData was already declared, when I found this file. Apparently,
// glim accounts can only be linked to gsim accounts. Needs to be tested
// as soon as gsim accounts are implemented.
/**
* GLIM Account Details Step
*/
@Component({
selector: 'mifosx-glim-details-step',
templateUrl: './glim-details-step.component.html',
styleUrls: ['./glim-details-step.component.scss']
})
export class GlimDetailsStepComponent implements OnInit {
export class GlimDetailsStepComponent implements OnInit, OnDestroy {
/** Loans Account Template */
@Input() loansAccountTemplate: any;
@Input() gsimData: any;
@@ -17,7 +31,7 @@ export class GlimDetailsStepComponent implements OnInit {
/** Maximum date allowed. */
maxDate = new Date(2100, 0, 1);
/** Product Data */
productData: any;
productList: any;
/** Loan Officer Data */
loanOfficerOptions: any;
/** Loan Purpose Options */
@@ -31,6 +45,16 @@ export class GlimDetailsStepComponent implements OnInit {
/** Loans Account Details Form */
loansAccountDetailsForm: UntypedFormGroup;
loanId: any = null;
loanProductSelected = false;
/** Currency data. */
protected productData: ReplaySubject<string[]> = new ReplaySubject<string[]>(1);
/** control for the filter select */
protected filterFormCtrl: UntypedFormControl = new UntypedFormControl('');
/** Subject that emits when the component has been destroyed. */
protected _onDestroy = new Subject<void>();
/** Loans Account Template with product data */
@Output() loansAccountProductTemplate = new EventEmitter();
/**
@@ -42,17 +66,21 @@ export class GlimDetailsStepComponent implements OnInit {
constructor(
private formBuilder: UntypedFormBuilder,
private loansService: LoansService,
private settingsService: SettingsService
private route: ActivatedRoute,
private settingsService: SettingsService,
private commons: Commons
) {
this.createLoansAccountDetailsForm();
this.loanId = this.route.snapshot.params['loanId'];
}
ngOnInit(): void {
this.maxDate = this.settingsService.businessDate;
this.createLoansAccountDetailsForm();
this.maxDate = this.settingsService.maxFutureDate; // Felix: originally: businessDate;
this.buildDependencies();
if (this.loansAccountTemplate) {
this.productData = this.loansAccountTemplate.productOptions;
this.productList = this.loansAccountTemplate.productOptions.sort(this.commons.dynamicSort('name'));
if (this.loansAccountTemplate.loanProductId) {
console.log('this.loansAccountTemplate.loanOfficerId: ', this.loansAccountTemplate.loanOfficerId);
this.loansAccountDetailsForm.patchValue({
productId: this.loansAccountTemplate.loanProductId,
submittedOnDate:
@@ -68,6 +96,31 @@ export class GlimDetailsStepComponent implements OnInit {
});
}
}
this.filterFormCtrl.valueChanges.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this.searchItem();
});
this.productData.next(this.productList.slice());
}
ngOnDestroy(): void {
this._onDestroy.next();
this._onDestroy.complete();
}
searchItem(): void {
if (this.productList) {
const search: string = this.filterFormCtrl.value.toLowerCase();
if (!search) {
this.productData.next(this.productList.slice());
} else {
this.productData.next(
this.productList.filter((option: any) => {
return option['name'].toLowerCase().indexOf(search) >= 0;
})
);
}
}
}
/**
@@ -80,6 +133,7 @@ export class GlimDetailsStepComponent implements OnInit {
Validators.required
],
loanOfficerId: [''],
// Felix: found: loanPurposeId: [''], - not sure, where we add this list...
fundId: [''],
submittedOnDate: [
new Date(),
@@ -89,7 +143,9 @@ export class GlimDetailsStepComponent implements OnInit {
'',
Validators.required
],
linkAccountId: ['']
externalId: [''],
linkAccountId: [''],
createStandingInstructionAtDisbursement: ['']
});
}
@@ -105,6 +161,7 @@ export class GlimDetailsStepComponent implements OnInit {
this.loanPurposeOptions = response.loanPurposeOptions;
this.fundOptions = response.fundOptions;
this.accountLinkingOptions = response.accountLinkingOptions;
this.loanProductSelected = true;
});
});
}
@@ -113,6 +170,6 @@ export class GlimDetailsStepComponent implements OnInit {
* Returns loans account details form value.
*/
get loansAccountDetails() {
return this.loansAccountDetailsForm.value;
return this.loansAccountDetailsForm.getRawValue(); // Felix: was: value;
}
}
@@ -1,8 +1,6 @@
<div fxLayout="row wrap" fxLayout.lt-md="column">
<h3 class="mat-h3" fxFlexFill>{{ 'labels.heading.Details' | translate }}</h3>
<mat-divider fxFlexFill></mat-divider>
<div fxFlexFill>
<span fxFlex="40%">{{ 'labels.inputs.Product' | translate }}:</span>
<span fxFlex="60%">{{ loansAccount.productId | find: loansAccountTemplate.productOptions : 'id' : 'name' }}</span>
@@ -50,11 +48,16 @@
<div fxFlexFill>
<span fxFlex="40%">{{ 'labels.inputs.Principal' | translate }}:</span>
<span fxFlex="60%">{{ loansAccount.principal }} {{ loansAccountProductTemplate.currency.DisplaySymbol }}</span>
<span fxFlex="60%">
{{
loansAccount.principalAmount | currency: loansAccountProductTemplate.currency.code : 'symbol-narrow' : '1.2-2'
}}
<span class="m-l-5">{{ loansAccountProductTemplate.currency.code }}</span>
</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">{{ 'labels.inputs.Loan term' | translate }}:</span>
<span fxFlex="40%">{{ 'labels.inputs.Loan Term' | translate }}:</span>
<span fxFlex="60%"
>{{ loansAccount.loanTermFrequency }}
{{
@@ -86,6 +89,11 @@
>
</div>
<div fxFlexFill *ngIf="productEnableDownPayment">
<span fxFlex="40%">{{ 'labels.inputs.Enable Down Payments' | translate }}:</span>
<span fxFlex="60%">{{ loansAccount.enableDownPayment | yesNo }}</span>
</div>
<div fxFlexFill *ngIf="loansAccount.repaymentsStartingFromDate">
<span fxFlex="40%">{{ 'labels.inputs.First repayment on' | translate }}:</span>
<span fxFlex="60%">{{ loansAccount.repaymentsStartingFromDate | dateFormat }}</span>
@@ -137,9 +145,7 @@
<div fxFlexFill *ngIf="loansAccount.inArrearsTolerance">
<span fxFlex="40%">{{ 'labels.inputs.Arrears tolerance' | translate }}: </span>
<span fxFlex="60%"
>{{ loansAccount.inArrearsTolerance }}&nbsp;{{ loansAccountProductTemplate.currency.displaySymbol }}</span
>
<span fxFlex="60%">{{ loansAccount.inArrearsTolerance }}</span>
</div>
<div fxFlexFill *ngIf="loansAccount.graceOnInterestCharged">
@@ -151,14 +157,24 @@
<span fxFlex="40%">{{ 'labels.inputs.Repayment strategy' | translate }}: </span>
<span fxFlex="60%">{{
loansAccount.transactionProcessingStrategyCode
| find: loansAccountTemplate.transactionProcessingStrategyOptions : 'code' : 'name'
| find: loansAccountProductTemplate.transactionProcessingStrategyOptions : 'code' : 'name'
}}</span>
</div>
<h3 class="mat-h3" fxFlexFill>{{ 'labels.heading.Moratorium' | translate }}</h3>
<div fxFlexFill>
<span fxFlex="40%"> {{ 'labels.inputs.Installment Amount' | translate }} </span>
<span fxFlex="60%"> {{ loansAccount.fixedEmiAmount | formatNumber }} </span>
</div>
<div fxFlexFill>
<span fxFlex="40%"> {{ 'labels.inputs.Balloon Repayment Amount' | translate }} </span>
<span fxFlex="60%"> {{ loansAccount.balloonRepaymentAmount | formatNumber }} </span>
</div>
<mat-divider fxFlexFill></mat-divider>
<h3 class="mat-h3" fxFlexFill>{{ 'labels.heading.Moratorium' | translate }}</h3>
<div fxFlexFill *ngIf="loansAccount.graceOnPrincipalPayment">
<span fxFlex="40%">{{ 'labels.inputs.On principal payment' | translate }}: </span>
<span fxFlex="60%">{{ loansAccount.graceOnPrincipalPayment }}</span>
@@ -174,6 +190,11 @@
<span fxFlex="60%">{{ loansAccount.graceOnArrearsAgeing }}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%"> {{ 'labels.inputs.Enable installment level Delinquency' | translate }} </span>
<span fxFlex="60%"> {{ loansAccount.enableInstallmentLevelDelinquency | yesNo }} </span>
</div>
<div fxFlexFill *ngIf="loansAccount.isTopup">
<span fxFlex="40%">{{ 'labels.inputs.Is Topup Loan' | translate }}? </span>
<span fxFlex="60%">{{ loansAccount.isTopup }}</span>
@@ -291,7 +312,7 @@
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button [routerLink]="['../']">
<button mat-raised-button [routerLink]="['../..']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" (click)="submitEvent.emit()">
@@ -1,13 +1,16 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
/**
* Create GLIM Account Preview Step
*/
@Component({
selector: 'mifosx-glim-preview-step',
templateUrl: './glim-preview-step.component.html',
styleUrls: ['./glim-preview-step.component.scss']
})
export class GlimPreviewStepComponent {
export class GlimPreviewStepComponent implements OnChanges {
/** Loans Account Template */
@Input() loansAccountTemplate: any;
@Input() loansAccountTemplate: any = [];
/** Loans Account Product Template */
@Input() loansAccountProductTemplate: any;
/** Loans Account Data */
@@ -31,5 +34,11 @@ export class GlimPreviewStepComponent {
'collectedon'
];
productEnableDownPayment = false;
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
this.productEnableDownPayment = this.loansAccountProductTemplate.product.enableDownPayment;
}
}
@@ -1,21 +1,38 @@
<form [formGroup]="loansAccountTermsForm">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.heading.Term Options' | translate }}</h4>
<!-- <mifosx-input-amount
fxFlex="48%"
*ngIf="currency"
[currency]="currency"
[isRequired]="true"
[inputFormControl]="loansAccountTermsForm.controls.principalAmount"
[inputLabel]="'Principal'"
>
</mifosx-input-amount> -->
<mat-form-field fxFlex="48%">
<h4 fxFlex="98%" class="mat-h4">
{{ 'labels.heading.Term Options' | translate }}
<i
class="fas fa-question"
matTooltip="The loan term parameter in loan accounts
is the default-calculated field based on the number of repayments and repaid every value entered by the user"
></i>
</h4>
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Loan Term' | translate }}</mat-label>
<input matInput required formControlName="loanTermFrequency" />
<input type="number" matInput required disabled formControlName="loanTermFrequency" />
<mat-error *ngIf="loansAccountTermsForm.controls.loanTermFrequency.hasError('required')">
{{ 'labels.inputs.Loan Term' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<mat-select required formControlName="loanTermFrequencyType">
<mat-option *ngFor="let type of termFrequencyTypeData" [value]="type.id">
{{ type.value }}
{{ type.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
<mat-error *ngIf="loansAccountTermsForm.controls.loanTermFrequencyType.hasError('required')">
@@ -24,21 +41,38 @@
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="20%" *ngIf="hasFixedLength()">
<mat-label>{{ 'labels.inputs.Fixed Length' | translate }}</mat-label>
<input type="number" matInput formControlName="fixedLength" />
</mat-form-field>
<span fxFlex="20%" *ngIf="hasFixedLength()" class="label-field">{{
loansAccountTermsForm.value.loanTermFrequencyType | find: termFrequencyTypeData : 'id' : 'value'
}}</span>
<h4 fxFlex="98%" class="mat-h4">Repayments</h4>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Number of repayments' | translate }}</mat-label>
<input type="number" matInput formControlName="numberOfRepayments" />
<input
type="number"
matInput
formControlName="numberOfRepayments"
matTooltip="Enter the total count of repayments that loan schedule has excluding the down-payment. Example:
If the loan account has 4 Installments including down-payment then number of repayments is equal to 3"
/>
<mat-error *ngIf="loansAccountTermsForm.controls.numberOfRepayments.hasError('required')">
{{ 'labels.inputs.Number of repayments' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%" (click)="repaymentsPicker.open()">
<mat-form-field fxFlex="23%" (click)="repaymentsPicker.open()">
<mat-label>{{ 'labels.inputs.First repayment on' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
matTooltip="{{ 'tooltips.May be entered to override' | translate }}"
[matDatepicker]="repaymentsPicker"
formControlName="repaymentsStartingFromDate"
/>
@@ -46,15 +80,36 @@
<mat-datepicker #repaymentsPicker></mat-datepicker>
</mat-form-field>
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.heading.Repaid Every' | translate }}</h4>
<mat-form-field fxFlex="23%" (click)="interestPicker.open()">
<mat-label>{{ 'labels.inputs.Interest charged from' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="interestPicker"
matTooltip="{{ 'tooltips.May be entered to override the date' | translate }}"
formControlName="interestChargedFromDate"
/>
<mat-datepicker-toggle matSuffix [for]="interestPicker"></mat-datepicker-toggle>
<mat-datepicker #interestPicker></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex="48%">
<h4 fxFlex="98%" class="mat-h4">
{{ 'labels.heading.Repaid Every' | translate }}
<i
class="fas fa-question"
matTooltip="{{ 'tooltips.Fields are input to calculating the repayment schedule' | translate }}"
></i>
</h4>
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Repaid every' | translate }}</mat-label>
<input
type="number"
matInput
required
formControlName="repaymentEvery"
[disabled]="!loansAccountProductTemplate?.product.allowAttributeOverrides.repaymentEvery"
matTooltip="{{ 'tooltips.Fields are input to calculating the repayment schedule' | translate }}"
/>
<mat-error *ngIf="loansAccountTermsForm.controls.repaymentEvery.hasError('required')">
{{ 'labels.inputs.Repaid every' | translate }} {{ 'labels.commons.is' | translate }}
@@ -62,75 +117,487 @@
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<mat-select
formControlName="repaymentFrequencyType"
required
[disabled]="!loansAccountProductTemplate?.product.allowAttributeOverrides.repaymentEvery"
>
<mat-select formControlName="repaymentFrequencyType" disabled required>
<mat-option *ngFor="let repaymentFrequencyType of termFrequencyTypeData" [value]="repaymentFrequencyType.id">
{{ repaymentFrequencyType.value }}
{{ repaymentFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="48%" *ngIf="loansAccountTermsForm.controls.repaymentFrequencyType.value === 2">
<mat-form-field fxFlex="14%" *ngIf="loansAccountTermsForm.controls.repaymentFrequencyType.value === 2">
<mat-label>{{ 'labels.inputs.Select On' | translate }}</mat-label>
<mat-select formControlName="repaymentFrequencyNthDayType">
<mat-option
*ngFor="let repaymentFrequencyNthDayType of repaymentFrequencyNthDayTypeData"
[value]="repaymentFrequencyNthDayType.id"
>
{{ repaymentFrequencyNthDayType.value }}
{{ repaymentFrequencyNthDayType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="48%" *ngIf="loansAccountTermsForm.controls.repaymentFrequencyType.value === 2">
<mat-form-field fxFlex="14%" *ngIf="loansAccountTermsForm.controls.repaymentFrequencyType.value === 2">
<mat-label>{{ 'labels.inputs.Select Day' | translate }}</mat-label>
<mat-select formControlName="repaymentFrequencyDayOfWeekType">
<mat-option
*ngFor="let repaymentFrequencyDayOfWeekType of repaymentFrequencyDaysOfWeekTypeData"
[value]="repaymentFrequencyDayOfWeekType.id"
>
{{ repaymentFrequencyDayOfWeekType.value }}
{{ repaymentFrequencyDayOfWeekType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="48%" (click)="interestPicker.open()">
<mat-label>{{ 'labels.inputs.Interest charged from' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="interestPicker"
formControlName="interestChargedFromDate"
/>
<mat-datepicker-toggle matSuffix [for]="interestPicker"></mat-datepicker-toggle>
<mat-datepicker #interestPicker></mat-datepicker>
</mat-form-field>
<mat-checkbox
fxFlex="73%"
labelPosition="before"
formControlName="enableDownPayment"
matTooltip="Leave this checkbox checked if the loan has Down Payment, A Down Payment is
a sum a buyer pays upfront when purchasing a good. It represents a percentage of the total purchase price, and the balance is usually financed"
class="margin-b"
*ngIf="productEnableDownPayment"
>
{{ 'labels.inputs.Enable Down Payment' | translate }}
</mat-checkbox>
<ng-container *ngIf="!loansAccountProductTemplate?.isLoanProductLinkedToFloatingRate">
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Nominal interest rate' | translate }}</mat-label>
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.inputs.Nominal interest rate' | translate }}</h4>
<ng-container *ngIf="!loansAccountTermsData?.isLoanProductLinkedToFloatingRate">
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Nominal interest rate' | translate }} %</mat-label>
<input type="number" matInput formControlName="interestRatePerPeriod" />
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<mat-select formControlName="interestRateFrequencyType">
<mat-option
*ngFor="let interestRateFrequencyType of interestRateFrequencyTypeData"
[value]="interestRateFrequencyType.id"
>
{{ interestRateFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
<mat-error>
{{ 'labels.inputs.Nominal interest rate frequency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Interest method' | translate }}</mat-label>
<mat-select
[disabled]="!loansAccountProductTemplate?.product.allowAttributeOverrides.interestType"
formControlName="interestType"
>
<mat-select formControlName="interestType" matTooltip="{{ 'tooltips.The Interest method value' | translate }}">
<mat-option *ngFor="let interestType of interestTypeData" [value]="interestType.id">
{{ interestType.value }}
{{ interestType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Amortization' | translate }}</mat-label>
<mat-select
required
matTooltip="{{ 'tooltips.The Amortization value' | translate }}"
formControlName="amortizationType"
>
<mat-option *ngFor="let amortizationType of amortizationTypeData" [value]="amortizationType.id">
{{ amortizationType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
<mat-error *ngIf="loansAccountTermsForm.controls.amortizationType.hasError('required')">
{{ 'labels.inputs.Amortization Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="23%" *ngIf="isEqualPrincipalPayments()">
<mat-label>{{ 'labels.inputs.Principal Percentage Per Installment' | translate }}</mat-label>
<input type="number" matInput formControlName="fixedPrincipalPercentagePerInstallment" />
</mat-form-field>
<mat-checkbox
fxFlex="23%"
formControlName="isEqualAmortization"
[checked]="loansAccountTermsData?.isEqualAmortization"
>
<p>{{ 'labels.inputs.Is Equal Amortization' | translate }}</p>
</mat-checkbox>
</ng-container>
<ng-container *ngIf="loansAccountTermsData?.isLoanProductLinkedToFloatingRate">
<div fxFlex="48%" fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Interest Method' | translate }}</mat-label>
<mat-select formControlName="interestType">
<mat-option *ngFor="let interestType of interestTypeData" [value]="interestType.id">
{{ interestType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Amortization' | translate }}</mat-label>
<mat-select
required
matTooltip="{{ 'tooltips.The Amortization value' | translate }}"
formControlName="amortizationType"
>
<mat-option *ngFor="let amortizationType of amortizationTypeData" [value]="amortizationType.id">
{{ amortizationType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
<mat-error *ngIf="loansAccountTermsForm.controls.amortizationType.hasError('required')">
{{ 'labels.inputs.Amortization Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-checkbox fxFlex="48%" formControlName="isFloatingInterestRate">
<p>{{ 'labels.inputs.Is Floating Rate' | translate }}?</p>
</mat-checkbox>
</div>
</ng-container>
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.inputs.Loan Schedule' | translate }}</h4>
<div fxFlex="48%" *ngIf="loanScheduleType">
<span fxFlex="40%"
><p>{{ 'labels.inputs.Loan Schedule Type' | translate }}</p></span
>
<span fxFlex="60%"
><p>{{ loanScheduleType.value | translateKey: 'catalogs' }}</p></span
>
</div>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Repayment Strategy' | translate }}</mat-label>
<mat-select
formControlName="transactionProcessingStrategyCode"
matTooltip="{{ 'tooltips.The repayment strategy' | translate }}"
[disabled]="repaymentStrategyDisabled"
>
<mat-option
*ngFor="let transactionProcessingStrategy of transactionProcessingStrategyOptions"
[value]="transactionProcessingStrategy.code"
>
{{ transactionProcessingStrategy.name | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
<mat-error *ngIf="loansAccountTermsForm.controls.transactionProcessingStrategyCode.hasError('required')">
{{ 'labels.inputs.Repayment Strategy' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%" *ngIf="loansAccountTermsData?.canDefineInstallmentAmount">
<mat-label>{{ 'labels.inputs.Installment Amount' | translate }}</mat-label>
<input type="number" matInput formControlName="fixedEmiAmount" />
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Balloon Repayment Amount' | translate }}</mat-label>
<input type="number" matInput formControlName="balloonRepaymentAmount" />
</mat-form-field>
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.heading.Interest Calculations' | translate }}</h4>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Interest calculation period' | translate }}</mat-label>
<mat-select
formControlName="interestCalculationPeriodType"
matTooltip="{{ 'tooltips.Daily - Will Calculate the interest' | translate }}"
>
<mat-option
*ngFor="let interestCalculationPeriodType of interestCalculationPeriodTypeData"
[value]="interestCalculationPeriodType.id"
>
{{ interestCalculationPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox
fxFlex="48%"
formControlName="allowPartialPeriodInterestCalculation"
matTooltip="{{ 'tooltips.To be used with SAME AS REPAYMENT PERIOD' | translate }}"
>
<p>{{ 'labels.inputs.Calculate interest for exact days in partial period' | translate }}</p>
</mat-checkbox>
<ng-container *ngIf="isProgressive">
<mat-checkbox formControlName="interestRecognitionOnDisbursementDate" fxFlex="48%">
<p>{{ 'labels.inputs.Is interest recognition on disbursement date?' | translate }}</p>
</mat-checkbox>
</ng-container>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Arrears tolerance' | translate }} </mat-label>
<input
matInput
type="number"
formControlName="inArrearsTolerance"
matTooltip="{{ 'tooltips.With Arrears tolerance' | translate }}"
/>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Interest free period' | translate }}</mat-label>
<input
matInput
formControlName="graceOnInterestCharged"
matTooltip="{{ 'tooltips.If the Interest Free Period' | translate }}"
/>
</mat-form-field>
<!-- <h4 fxFlex="98%" class="mat-h4">
{{ 'labels.heading.Moratorium' | translate }}
<i class="fas fa-question" matTooltip="{{ 'tooltips.The moratorium information' | translate }}"></i>
</h4>
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Grace on principal payment' | translate }}</mat-label>
<input type="number" matInput formControlName="graceOnPrincipalPayment" />
</mat-form-field>
<mat-form-field fxFlex="23%">
<mat-label>{{ 'labels.inputs.Grace on interest payment' | translate }}</mat-label>
<input type="number" matInput formControlName="graceOnInterestPayment" />
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.On arrears ageing' | translate }}</mat-label>
<input type="number" matInput formControlName="graceOnArrearsAgeing" />
</mat-form-field> -->
<div fxFlexFill fxFlex="48%" *ngIf="isDelinquencyEnabled()">
<p>
<span fxFlex="53%"
><b>{{ 'labels.inputs.Delinquency Bucket' | translate }}</b></span
>
<span fxFlex="48%">{{ loanProduct?.delinquencyBucket.name }}</span>
</p>
</div>
<div fxFlex="48%" *ngIf="isDelinquencyEnabled()">
<mat-checkbox formControlName="enableInstallmentLevelDelinquency">
<p>{{ 'labels.inputs.Enable installment level Delinquency' | translate }}</p>
</mat-checkbox>
</div>
<ng-container fxFlex="48%" *ngIf="loansAccountTermsData?.isTopup">
<mat-checkbox fxFlex="20%" formControlName="isTopup">
<p>{{ 'labels.inputs.Is Topup Loan' | translate }}?</p>
</mat-checkbox>
<mat-form-field fxFlex="24%" *ngIf="loansAccountTermsForm.controls.isTopup.value">
<mat-label>{{ 'labels.inputs.Loan closed with Topup' | translate }}</mat-label>
<mat-select formControlName="loanIdToClose">
<mat-option *ngFor="let clientActiveLoan of clientActiveLoanData" [value]="clientActiveLoan.id">
{{ clientActiveLoan.accountNo }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
<mat-divider fxFlex="98%" class="margin-t"></mat-divider>
<div fxFlexFill class="margin-t">
<span fxFlex="40%"
><b>{{ 'labels.inputs.Recalculate Interest' | translate }}</b></span
>
<span fxFlex="60%">{{ loansAccountTermsData?.isInterestRecalculationEnabled | yesNo }}</span>
</div>
<div fxFlexFill *ngIf="loansAccountTermsData?.isInterestRecalculationEnabled">
<span fxFlex="40%">{{ 'labels.inputs.Days in year' | translate }}</span>
<span fxFlex="60%">{{ loansAccountTermsData.daysInYearType.value | translateKey: 'catalogs' }}</span>
</div>
<ng-container *ngIf="loansAccountTermsData?.isInterestRecalculationEnabled">
<div fxFlexFill *ngIf="loansAccountTermsData?.isInterestRecalculationEnabled">
<span fxFlex="40%">{{ 'labels.inputs.Advance payments adjustment type' | translate }}</span>
<span fxFlex="60%">{{ loansAccountTermsData.interestRecalculationData.rescheduleStrategyType.value }}</span>
</div>
<div fxFlexFill *ngIf="loansAccountTermsData?.isInterestRecalculationEnabled">
<span fxFlex="40%">{{ 'labels.inputs.Days in month' | translate }}</span>
<span fxFlex="60%">{{ loansAccountTermsData.daysInMonthType.value | translateKey: 'catalogs' }}</span>
</div>
</ng-container>
<ng-container *ngIf="loansAccountTermsData?.isInterestRecalculationEnabled">
<div fxFlexFill>
<span fxFlex="40%">{{ 'labels.inputs.Interest recalculation compounding on' | translate }}</span>
<span fxFlex="60%">{{
loansAccountTermsData.interestRecalculationData.interestRecalculationCompoundingType.value
| translateKey: 'catalogs'
}}</span>
</div>
<div fxFlexFill>
<span fxFlex="40%">{{ 'labels.inputs.Frequency Interval for recalculation' | translate }}</span>
<span fxFlex="60%">
<span>{{ loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyType.value }}</span>
<span
*ngIf="
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyType.id === 3 &&
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyWeekday
"
>
on {{ loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyWeekday.value }}</span
>
<span
*ngIf="
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyType.id === 4 &&
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyOnDay
"
>on day {{ loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyOnDay }}</span
>
<span
*ngIf="
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyType.id === 4 &&
!loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyOnDay &&
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyNthDay
"
>on
{{ loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyNthDay.value }}
{{ loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyWeekday.value }}</span
>
</span>
</div>
</ng-container>
<div
fxFlexFill
*ngIf="
loansAccountTermsData?.isInterestRecalculationEnabled &&
loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyType.id !== 1
"
>
<span fxFlex="40%">{{ 'labels.inputs.Frequency Interval for recalculation' | translate }}</span>
<span fxFlex="60%">{{ loansAccountTermsData.interestRecalculationData.recalculationRestFrequencyInterval }}</span>
</div>
<ng-container *ngIf="multiDisburseLoan">
<mat-divider fxFlex="98%"></mat-divider>
<div fxFlexFill *ngIf="allowAddDisbursementDetails()">
<h4 fxFlex="98%" class="mat-h4">{{ 'labels.heading.Loan Tranche Details' | translate }}</h4>
<mat-form-field fxFlex="48%">
<mat-label>{{ 'labels.inputs.Maximum allowed outstanding balance' | translate }}</mat-label>
<input matInput required formControlName="maxOutstandingLoanBalance" />
</mat-form-field>
<span fxFlex>
<button
type="button"
mat-icon-button
color="primary"
required
(click)="addDisbursementDataEntry(disbursementData)"
[disabled]="isMultiDisbursedCompleted"
>
<fa-icon icon="plus-circle" size="lg"></fa-icon>
</button>
</span>
</div>
<div fxFlexFill *ngIf="!allowAddDisbursementDetails()">
<h4 fxFlex="98%" class="mat-h4">
{{ 'labels.heading.Loan Tranche Details are not allowed for this Loan Product' | translate }}
</h4>
</div>
<table mat-table [dataSource]="disbursementDataSource" [hidden]="disbursementDataSource.length === 0">
<ng-container matColumnDef="expectedDisbursementDate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Eexpected Disbursement Date' | translate }}</th>
<td mat-cell *matCellDef="let row">
{{ row.expectedDisbursementDate | dateFormat }}
</td>
</ng-container>
<ng-container matColumnDef="principal">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Principal' | translate }}</th>
<td mat-cell *matCellDef="let row">
{{ row.principal }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let row; let rowIndex = index">
<button
type="button"
mat-icon-button
color="warn"
(click)="removeDisbursementDataEntry(rowIndex)"
matTooltip="{{ 'tooltips.Delete' | translate }}"
matTooltipPosition="left"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="disbursementDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: disbursementDisplayedColumns"></tr>
</table>
</ng-container>
<ng-container
*ngIf="
loansAccountTermsData?.isInterestRecalculationEnabled &&
loansAccountTermsData.interestRecalculationData.interestRecalculationCompoundingType.id !== 0
"
>
<div fxFlexFill>
<span fxFlex="40%">{{ 'labels.inputs.Frequency for compounding' | translate }}</span>
<span fxFlex="60%"
>{{ loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyType.value }}
<span
*ngIf="
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyType.id === 3 &&
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyWeekday
"
>
on {{ loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyWeekday.value }}
</span>
<span
*ngIf="
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyType.id === 4 &&
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyOnDay
"
>on day
{{ loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyOnDay }}
</span>
<span
*ngIf="
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyType.id === 4 &&
!loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyOnDay &&
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyNthDay
"
>on
{{ loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyNthDay.value }}
{{ loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyWeekday.value }}
</span>
</span>
</div>
</ng-container>
<div
fxFlexFill
*ngIf="
loansAccountTermsData?.isInterestRecalculationEnabled &&
loansAccountTermsData.interestRecalculationData.interestRecalculationCompoundingType.id !== 0 &&
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyType.id !== 1
"
>
<span fxFlex="40%">{{ 'labels.inputs.Frequency Interval for compounding' | translate }}</span>
<span fxFlex="60%">{{
loansAccountTermsData.interestRecalculationData.recalculationCompoundingFrequencyInterval
}}</span>
</div>
</div>
<div fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
@@ -140,5 +607,8 @@
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<button mat-raised-button *ngIf="loanId" [routerLink]="['../', 'general']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
</div>
</form>
@@ -1,17 +1,45 @@
/** Angular Imports */
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormArray } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
/** Custom Services */
// import { LoansAccountAddCollateralDialogComponent } from 'app/loans/custom-dialog/loans-account-add-collateral-dialog/loans-account-add-collateral-dialog.component';
import { LoanProducts } from 'app/products/loan-products/loan-products';
import { LoanProduct } from 'app/products/loan-products/models/loan-product.model';
import { SettingsService } from 'app/settings/settings.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
// import { Currency } from 'app/shared/models/general.model';
import { CodeName, OptionData } from 'app/shared/models/option-data.model';
/**
* Create GLIM Terms Step
*/
@Component({
selector: 'mifosx-glim-terms-step',
templateUrl: './glim-terms-step.component.html',
styleUrls: ['./glim-terms-step.component.scss']
})
export class GlimTermsStepComponent implements OnInit, OnChanges {
/** Loans Product Options */
@Input() loansProductOptions: any;
/** Loans Account Product Template */
@Input() loansAccountProductTemplate: any;
/** Loans Account Template */
@Input() loansAccountTemplate: any;
loansAccountTermsData: any;
/** Is Multi Disburse Loan */
multiDisburseLoan: any;
// @Input() loansAccountFormValid: LoansAccountFormValid
@Input() loansAccountFormValid: boolean;
// @Input loanPrincipal: Loan Principle
@Input() loanPrincipal: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
@@ -31,86 +59,292 @@ export class GlimTermsStepComponent implements OnInit, OnChanges {
amortizationTypeData: any;
/** Interest Calculation Period Type Data */
interestCalculationPeriodTypeData: any;
/** Transaction Processing Strategy Data */
transactionProcessingStrategyData: any;
/** Client Active Loan Data */
clientActiveLoanData: any;
/** Multi Disbursement Data */
disbursementDataSource: {}[] = [];
/** Loan repayment strategies */
transactionProcessingStrategyOptions: any = [];
repaymentStrategyDisabled = false;
/** Disbursement Data Displayed Columns */
disbursementDisplayedColumns: string[] = [
'expectedDisbursementDate',
'principal',
'actions'
];
/** Multi Disbursement Control */
totalMultiDisbursed: any = 0;
isMultiDisbursedCompleted = false;
/** Component is pristine if there has been no changes by user interaction */
pristine = true;
loanId: any = null;
loanScheduleType: OptionData | null = null;
loanProduct: LoanProduct | null = null;
interestRateFrequencyTypeData: any[] = [];
// currency: Currency;
productEnableDownPayment = false;
isProgressive = false;
/**
* Create Loans Account Terms Form
* @param formBuilder FormBuilder
* @param {SettingsService} settingsService SettingsService
* @param {ActivatedRoute} route ActivatedRoute
* @param {MatDialog} dialog MatDialog
*/
constructor(
private formBuilder: UntypedFormBuilder,
private settingsService: SettingsService
private settingsService: SettingsService,
private route: ActivatedRoute,
public dialog: MatDialog
) {
this.createloansAccountTermsForm();
this.loanId = this.route.snapshot.params['loanId'];
this.createGLIMTermsForm();
}
/**
* Executes on change of input values
*/
ngOnChanges() {
if (this.loansAccountProductTemplate) {
// this.currency = this.loansAccountProductTemplate.currency;
this.loansAccountTermsData = this.loansAccountProductTemplate;
if (this.loanId != null && this.loansAccountTemplate.accountNo) {
this.loansAccountTermsData = this.loansAccountTemplate;
}
this.productEnableDownPayment = this.loansAccountTermsData.product.enableDownPayment;
this.isProgressive =
this.loansAccountTermsData.loanScheduleType.code == LoanProducts.LOAN_SCHEDULE_TYPE_PROGRESSIVE;
if (this.loansAccountTermsData.product) {
this.loanProduct = this.loansAccountTermsData.product;
}
this.interestRateFrequencyTypeData = this.loansAccountTermsData.interestRateFrequencyTypeOptions;
this.loansAccountTermsForm.patchValue({
principal: this.loansAccountProductTemplate.principal,
loanTermFrequency: this.loansAccountProductTemplate.termFrequency,
loanTermFrequencyType: this.loansAccountProductTemplate.termPeriodFrequencyType.id,
numberOfRepayments: this.loansAccountProductTemplate.numberOfRepayments,
repaymentEvery: this.loansAccountProductTemplate.repaymentEvery,
repaymentFrequencyType: this.loansAccountProductTemplate.repaymentFrequencyType.id,
interestRatePerPeriod: this.loansAccountProductTemplate.interestRatePerPeriod,
interestType: this.loansAccountProductTemplate.interestType.id,
interestCalculationPeriodType: this.loansAccountProductTemplate.interestCalculationPeriodType.id,
transactionProcessingStrategyCode: this.loansAccountProductTemplate.transactionProcessingStrategyCode
principalAmount: this.loansAccountTermsData.principal,
loanTermFrequency: this.loansAccountTermsData.termFrequency,
loanTermFrequencyType: this.loansAccountTermsData.termPeriodFrequencyType.id,
numberOfRepayments: this.loansAccountTermsData.numberOfRepayments,
repaymentEvery: this.loansAccountTermsData.repaymentEvery,
repaymentFrequencyType: this.loansAccountTermsData.repaymentFrequencyType.id,
amortizationType: this.loansAccountTermsData.amortizationType.id,
isEqualAmortization: this.loansAccountTermsData.isEqualAmortization,
interestType: this.loansAccountTermsData.interestType.id,
isFloatingInterestRate: this.loansAccountTermsData.isLoanProductLinkedToFloatingRate ? false : '',
interestCalculationPeriodType: this.loansAccountTermsData.interestCalculationPeriodType.id,
allowPartialPeriodInterestCalculation: this.loansAccountTermsData.allowPartialPeriodInterestCalculation,
inArrearsTolerance: this.loansAccountTermsData.inArrearsTolerance,
graceOnInterestCharged: this.loansAccountTermsData.graceOnInterestCharged,
fixedEmiAmount: this.loansAccountTermsData.fixedEmiAmount,
maxOutstandingLoanBalance: this.loansAccountTermsData.maxOutstandingLoanBalance,
transactionProcessingStrategyCode: this.loansAccountTermsData.transactionProcessingStrategyCode,
interestRateDifferential: this.loansAccountTermsData.interestRateDifferential,
multiDisburseLoan: this.loansAccountTermsData.multiDisburseLoan,
interestRateFrequencyType: this.loansAccountTermsData.interestRateFrequencyType.id,
balloonRepaymentAmount: this.loansAccountTermsData.balloonRepaymentAmount,
interestRecognitionOnDisbursementDate: this.loansAccountTermsData.interestRecognitionOnDisbursementDate || false
});
this.setAdvancedPaymentStrategyControls();
if (this.loansAccountTermsData.loanScheduleType.code == LoanProducts.LOAN_SCHEDULE_TYPE_CUMULATIVE) {
this.loansAccountTermsForm.removeControl('interestRecognitionOnDisbursementDate');
}
if (this.loansAccountTermsData.isLoanProductLinkedToFloatingRate) {
this.loansAccountTermsForm.removeControl('interestRatePerPeriod');
}
this.multiDisburseLoan = this.loansAccountTermsData.multiDisburseLoan;
if (this.loansAccountTermsData.disbursementDetails) {
this.disbursementDataSource = this.loansAccountTermsData.disbursementDetails;
this.totalMultiDisbursed = 0;
this.disbursementDataSource.forEach((item: any) => {
this.totalMultiDisbursed += item.principal;
});
}
if (this.isDelinquencyEnabled()) {
this.loansAccountTermsForm.addControl(
'enableInstallmentLevelDelinquency',
new UntypedFormControl(
this.loansAccountTermsData.enableInstallmentLevelDelinquency ||
this.loanProduct.enableInstallmentLevelDelinquency
)
);
}
if (this.productEnableDownPayment) {
const enableDownPayment = this.loansAccountTermsData['enableDownPayment'] === false ? false : true;
this.loansAccountTermsForm.addControl('enableDownPayment', new UntypedFormControl(enableDownPayment));
}
const allowAttributeOverrides = this.loansAccountTermsData.product.allowAttributeOverrides;
if (!allowAttributeOverrides.repaymentEvery) {
this.loansAccountTermsForm.controls.repaymentEvery.disable();
this.loansAccountTermsForm.controls.repaymentFrequencyType.disable();
}
if (!allowAttributeOverrides.interestType) {
this.loansAccountTermsForm.controls.interestType.disable();
}
if (!allowAttributeOverrides.amortizationType) {
this.loansAccountTermsForm.controls.amortizationType.disable();
}
if (!allowAttributeOverrides.interestCalculationPeriodType) {
this.loansAccountTermsForm.controls.interestCalculationPeriodType.disable();
this.loansAccountTermsForm.controls.allowPartialPeriodInterestCalculation.disable();
}
if (!allowAttributeOverrides.inArrearsTolerance) {
this.loansAccountTermsForm.controls.inArrearsTolerance.disable();
}
if (!allowAttributeOverrides.transactionProcessingStrategyCode) {
this.loansAccountTermsForm.controls.transactionProcessingStrategyCode.disable();
}
this.setOptions();
}
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
if (this.loansAccountTemplate) {
if (this.loansAccountTemplate.loanProductId) {
this.maxDate = this.settingsService.maxFutureDate;
this.loansAccountTermsData = this.loansAccountProductTemplate;
if (this.loanId != null && this.loansAccountTemplate.accountNo) {
this.loansAccountTermsData = this.loansAccountTemplate;
}
if (this.loansAccountTermsData) {
if (this.loansAccountTermsData.loanProductId) {
this.loansAccountTermsForm.patchValue({
repaymentsStartingFromDate:
this.loansAccountTemplate.expectedFirstRepaymentOnDate &&
new Date(this.loansAccountTemplate.expectedFirstRepaymentOnDate)
this.loansAccountTermsData.expectedFirstRepaymentOnDate &&
new Date(this.loansAccountTermsData.expectedFirstRepaymentOnDate)
});
}
this.loansAccountTermsForm.patchValue({
principalAmount: this.loansAccountTermsData.principal,
loanTermFrequency: this.loansAccountTermsData.termFrequency,
loanTermFrequencyType: this.loansAccountTermsData.termPeriodFrequencyType.id,
numberOfRepayments: this.loansAccountTermsData.numberOfRepayments,
repaymentEvery: this.loansAccountTermsData.repaymentEvery,
repaymentFrequencyType: this.loansAccountTermsData.repaymentFrequencyType.id,
amortizationType: this.loansAccountTermsData.amortizationType.id,
isEqualAmortization: this.loansAccountTermsData.isEqualAmortization,
interestType: this.loansAccountTermsData.interestType.id,
isFloatingInterestRate: this.loansAccountTermsData.isLoanProductLinkedToFloatingRate ? false : '',
interestCalculationPeriodType: this.loansAccountTermsData.interestCalculationPeriodType.id,
allowPartialPeriodInterestCalculation: this.loansAccountTermsData.allowPartialPeriodInterestCalculation,
inArrearsTolerance: this.loansAccountTermsData.inArrearsTolerance,
graceOnInterestCharged: this.loansAccountTermsData.graceOnInterestCharged,
fixedEmiAmount: this.loansAccountTermsData.fixedEmiAmount,
maxOutstandingLoanBalance: this.loansAccountTermsData.maxOutstandingLoanBalance,
transactionProcessingStrategyCode: this.loansAccountTermsData.transactionProcessingStrategyCode,
interestRateDifferential: this.loansAccountTermsData.interestRateDifferential,
multiDisburseLoan: this.loansAccountTermsData.multiDisburseLoan,
interestRateFrequencyType: this.loansAccountTermsData.interestRateFrequencyType.id,
balloonRepaymentAmount: this.loansAccountTermsData.balloonRepaymentAmount,
interestRecognitionOnDisbursementDate: this.loansAccountTermsData.interestRecognitionOnDisbursementDate || false
});
}
this.createloansAccountTermsForm();
this.setCustomValidators();
this.createGLIMTermsForm();
this.setAdvancedPaymentStrategyControls();
// this.setCustomValidators();
this.setLoanTermListener();
}
allowAddDisbursementDetails() {
return this.multiDisburseLoan && !this.loansAccountTermsData.disallowExpectedDisbursements;
}
/** Custom Validators for the form */
setCustomValidators() {
const repaymentFrequencyNthDayType = this.loansAccountTermsForm.get('repaymentFrequencyNthDayType');
const repaymentFrequencyDayOfWeekType = this.loansAccountTermsForm.get('repaymentFrequencyDayOfWeekType');
// setCustomValidators() {
// const repaymentFrequencyNthDayType = this.loansAccountTermsForm.get('repaymentFrequencyNthDayType');
// const repaymentFrequencyDayOfWeekType = this.loansAccountTermsForm.get('repaymentFrequencyDayOfWeekType');
this.loansAccountTermsForm.get('repaymentFrequencyType').valueChanges.subscribe((repaymentFrequencyType) => {
if (repaymentFrequencyType === 2) {
repaymentFrequencyNthDayType.setValidators([Validators.required]);
repaymentFrequencyDayOfWeekType.setValidators([Validators.required]);
// this.loansAccountTermsForm.get('repaymentFrequencyType').valueChanges.subscribe((repaymentFrequencyType) => {
// if (repaymentFrequencyType === 2) {
// repaymentFrequencyNthDayType.setValidators([Validators.required]);
// repaymentFrequencyDayOfWeekType.setValidators([Validators.required]);
// } else {
// repaymentFrequencyNthDayType.setValidators(null);
// repaymentFrequencyDayOfWeekType.setValidators(null);
// }
// repaymentFrequencyNthDayType.updateValueAndValidity();
// repaymentFrequencyDayOfWeekType.updateValueAndValidity();
// });
// }
/** Custom Listeners for the form to calculate Loan Term */
setLoanTermListener() {
this.loansAccountTermsForm.get('numberOfRepayments').valueChanges.subscribe((numberOfRepayments) => {
const repaymentEvery: number = this.loansAccountTermsForm.value.repaymentEvery;
this.calculateLoanTerm(numberOfRepayments, repaymentEvery);
});
this.loansAccountTermsForm.get('repaymentEvery').valueChanges.subscribe((repaymentEvery) => {
const numberOfRepayments: number = this.loansAccountTermsForm.value.numberOfRepayments;
this.calculateLoanTerm(numberOfRepayments, repaymentEvery);
});
this.loansAccountTermsForm.get('loanTermFrequencyType').valueChanges.subscribe((loanTermFrequencyType) => {
this.loansAccountTermsForm.patchValue({ repaymentFrequencyType: loanTermFrequencyType });
});
this.loansAccountTermsForm.get('amortizationType').valueChanges.subscribe((amortizationType) => {
if (amortizationType === 0) {
// Equal Principal Payments
this.loansAccountTermsForm.addControl('fixedPrincipalPercentagePerInstallment', new UntypedFormControl(''));
} else {
repaymentFrequencyNthDayType.setValidators(null);
repaymentFrequencyDayOfWeekType.setValidators(null);
// Equal Installments
this.loansAccountTermsForm.removeControl('fixedPrincipalPercentagePerInstallment');
}
repaymentFrequencyNthDayType.updateValueAndValidity();
repaymentFrequencyDayOfWeekType.updateValueAndValidity();
});
}
setAdvancedPaymentStrategyControls(): void {
// Fixed Length validation
if (this.loansAccountTermsData) {
this.loansAccountTermsForm.removeControl('interestRatePerPeriod');
this.loansAccountTermsForm.removeControl('fixedLength');
if (this.loansAccountTermsData.product.fixedLength) {
this.loansAccountTermsForm.addControl(
'interestRatePerPeriod',
new UntypedFormControl({ value: 0, disabled: true }, Validators.required)
);
this.loansAccountTermsForm.addControl(
'fixedLength',
new UntypedFormControl(this.loansAccountTermsData.product.fixedLength)
);
} else {
this.loansAccountTermsForm.addControl(
'interestRatePerPeriod',
new UntypedFormControl(this.loansAccountTermsData.interestRatePerPeriod, Validators.required)
);
}
}
}
hasFixedLength(): boolean {
if (this.loansAccountTermsData) {
return this.loansAccountTermsData.product?.fixedLength ? true : false;
}
return false;
}
isEqualPrincipalPayments(): boolean {
return this.loansAccountTermsForm.value.amortizationType === 0;
}
/** Create Loans Account Terms Form */
createloansAccountTermsForm() {
createGLIMTermsForm() {
this.loansAccountTermsForm = this.formBuilder.group({
principal: [
principalAmount: [
'',
Validators.required
],
loanTermFrequency: [
'',
{ value: '', disabled: true },
Validators.required
],
loanTermFrequencyType: [
@@ -126,7 +360,7 @@ export class GlimTermsStepComponent implements OnInit, OnChanges {
Validators.required
],
repaymentFrequencyType: [
'',
{ value: '', disabled: true },
Validators.required
],
repaymentFrequencyNthDayType: [''],
@@ -135,8 +369,109 @@ export class GlimTermsStepComponent implements OnInit, OnChanges {
interestChargedFromDate: [''],
interestRatePerPeriod: [''],
interestType: [''],
isFloatingInterestRate: [''],
isEqualAmortization: [''],
amortizationType: [
'',
Validators.required
],
interestCalculationPeriodType: [''],
transactionProcessingStrategyCode: ['']
allowPartialPeriodInterestCalculation: [''],
inArrearsTolerance: [''],
graceOnInterestCharged: [''],
loanIdToClose: [''],
fixedEmiAmount: [''],
isTopup: [''],
maxOutstandingLoanBalance: [''],
interestRateDifferential: [''],
transactionProcessingStrategyCode: [
'',
Validators.required
],
multiDisburseLoan: [false],
interestRateFrequencyType: [''],
balloonRepaymentAmount: [''],
interestRecognitionOnDisbursementDate: [false]
});
}
calculateLoanTerm(numberOfRepayments: number, repaymentEvery: number): void {
const loanTerm = numberOfRepayments * repaymentEvery;
this.loansAccountTermsForm.patchValue({ loanTermFrequency: loanTerm });
}
/**
* Gets the Disbursement Data array.
* @returns {Array} Disbursement Data array.
*/
get disbursementData() {
return {
disbursementData: this.disbursementDataSource
};
}
/**
* Adds the Disbursement Data entry form to given Disbursement Data entry.
*/
addDisbursementDataEntry() {
const currentPrincipalAmount = this.loansAccountTermsForm.get('principalAmount').value;
const formfields: FormfieldBase[] = [
new DatepickerBase({
controlName: 'expectedDisbursementDate',
label: 'Expected Disbursement Date',
value: new Date() || '',
type: 'datetime-local',
minDate: this.minDate,
maxDate: this.maxDate,
required: true,
order: 1
}),
new InputBase({
controlName: 'principal',
label: `Principal(It should be less than equal to the ${currentPrincipalAmount})`,
value: '',
type: 'number',
required: true,
order: 2
})
];
const data = {
title: 'Add Disbursement Details',
layout: { addButtonText: 'Add' },
formfields: formfields
};
const disbursementDialogRef = this.dialog.open(FormDialogComponent, { data });
disbursementDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const principal = response.data.value.principal * 1;
if (this.totalMultiDisbursed + principal <= currentPrincipalAmount) {
this.disbursementDataSource = this.disbursementDataSource.concat(response.data.value);
this.totalMultiDisbursed += principal;
this.isMultiDisbursedCompleted = this.totalMultiDisbursed === currentPrincipalAmount;
this.pristine = false;
}
}
});
}
/**
* Removes the Disbursement Data entry form from given Disbursement Data entry form array at given index.
* @param {number} index Array index from where Disbursement Data entry form needs to be removed.
*/
removeDisbursementDataEntry(index: number) {
const currentPrincipalAmount = this.loansAccountTermsForm.get('principalAmount').value;
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
const principal = this.disbursementDataSource[index]['principal'] * 1;
this.disbursementDataSource.splice(index, 1);
this.disbursementDataSource = this.disbursementDataSource.concat([]);
this.totalMultiDisbursed -= principal;
this.isMultiDisbursedCompleted = this.totalMultiDisbursed === currentPrincipalAmount;
}
});
}
@@ -151,14 +486,35 @@ export class GlimTermsStepComponent implements OnInit, OnChanges {
this.interestTypeData = this.loansAccountProductTemplate.interestTypeOptions;
this.amortizationTypeData = this.loansAccountProductTemplate.amortizationTypeOptions;
this.interestCalculationPeriodTypeData = this.loansAccountProductTemplate.interestCalculationPeriodTypeOptions;
this.transactionProcessingStrategyData = this.loansAccountProductTemplate.transactionProcessingStrategyOptions;
this.clientActiveLoanData = this.loansAccountProductTemplate.clientActiveLoanOptions;
this.loanScheduleType = this.loansAccountProductTemplate.loanScheduleType;
this.transactionProcessingStrategyOptions = [];
if (this.loanScheduleType.code === LoanProducts.LOAN_SCHEDULE_TYPE_CUMULATIVE) {
// Filter Advanced Payment Allocation Strategy
this.transactionProcessingStrategyOptions =
this.loansAccountProductTemplate.transactionProcessingStrategyOptions.filter(
(cn: CodeName) => !LoanProducts.isAdvancedPaymentAllocationStrategy(cn.code)
);
this.repaymentStrategyDisabled = false;
} else {
// Only Advanced Payment Allocation Strategy
this.loansAccountProductTemplate.transactionProcessingStrategyOptions.some((cn: CodeName) => {
if (LoanProducts.isAdvancedPaymentAllocationStrategy(cn.code)) {
this.transactionProcessingStrategyOptions.push(cn);
}
});
this.repaymentStrategyDisabled = true;
}
}
isDelinquencyEnabled(): boolean {
return !!this.loanProduct?.delinquencyBucket?.name;
}
/**
* Returns loans account terms form value.
*/
get loansAccountTerms() {
return this.loansAccountTermsForm.value;
return this.loansAccountTermsForm.getRawValue();
}
}
+18 -9
View File
@@ -563,13 +563,15 @@ export class LoansService {
getGLIMLoanAccountTemplate(groupId: any): Observable<any> {
const httpParams = new HttpParams()
.set('groupId', groupId)
.set('lendingStrategy', '300')
// Commenting parameter, because it doesn't exist:
// https://localhost:8443/fineract-provider/swagger-ui/index.html#/Loans/template_10
// .set('lendingStrategy', '300')
.set('templateType', 'jlgbulk');
return this.http.get('/loans/template', { params: httpParams });
}
createGlimAccount(glimAccount: any): Observable<any> {
return this.http.post('/batches?enclosingTransaction=true', glimAccount);
createGlimAccount(payload: any): Observable<any> {
return this.http.post('/batches?enclosingTransaction=true', payload);
}
calculateLoanSchedule(payload: any): Observable<any> {
@@ -598,10 +600,6 @@ export class LoansService {
amount: charge.amount,
dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat)
})),
collateral: loansAccount.collateral.map((collateralEle: any) => ({
clientCollateralId: collateralEle.type.collateralId,
quantity: collateralEle.value
})),
disbursementData: loansAccount.disbursementData.map((item: any) => ({
expectedDisbursementDate: this.dateUtils.formatDate(item.expectedDisbursementDate, dateFormat),
principal: item.principal
@@ -613,7 +611,19 @@ export class LoansService {
dateFormat,
locale
};
if (loansAccountTemplate.clientId) {
if (loansAccount.collateral) {
loansAccountData.collateral = loansAccount.collateral.map((collateralEle: any) => ({
clientCollateralId: collateralEle.type.collateralId,
quantity: collateralEle.value
}));
}
if (loansAccountTemplate.clientId && loansAccountTemplate.group.id) {
loansAccountData.clientId = loansAccountTemplate.clientId;
loansAccountData.groupId = loansAccountTemplate.group.id;
loansAccountData.loanType = 'glim';
} else if (loansAccountTemplate.clientId) {
loansAccountData.clientId = loansAccountTemplate.clientId;
loansAccountData.loanType = 'individual';
} else {
@@ -651,7 +661,6 @@ export class LoansService {
// allowPartialPeriodInterestCalculation. Until that is fixed, we need to replace the field name in the payload.
loansAccountData.allowPartialPeriodInterestCalcualtion = loansAccountData.allowPartialPeriodInterestCalculation;
delete loansAccountData.allowPartialPeriodInterestCalculation;
return loansAccountData;
}
}