Adds the curated Loans A/C steps (security/collateral, amount & tenure, repayment & EMI, interest, prepayment & closure) and a product-type picker (loan-product-types.ts) shared by the create wizard, product list and account picker. The interest step and a new register-rate-view read the live Loan Interest Rate Register for a product's chart/CIBIL rate instead of a hardcoded value, and a new loan-product-catalog service + template card back the loan account stepper's product pick. Also carries forward in-progress Loan Interest Rate Register UI work: an inline Add Slab panel (replacing window.prompt) and a Submit Details dialog for confirming name/effective dates before submitting for approval. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
285 lines
10 KiB
TypeScript
285 lines
10 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
/** Angular Imports */
|
|
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy, inject } from '@angular/core';
|
|
import {
|
|
UntypedFormGroup,
|
|
UntypedFormBuilder,
|
|
Validators,
|
|
UntypedFormControl,
|
|
ReactiveFormsModule
|
|
} from '@angular/forms';
|
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
|
|
/** Custom Services */
|
|
import { LoansService } from '../../loans.service';
|
|
import { Commons } from 'app/core/utils/commons';
|
|
import { takeUntil } from 'rxjs/operators';
|
|
import { ReplaySubject, Subject } from 'rxjs';
|
|
import { MatTooltip } from '@angular/material/tooltip';
|
|
import { NgxMatSelectSearchModule } from 'ngx-mat-select-search';
|
|
import { AsyncPipe } from '@angular/common';
|
|
import { MatDivider } from '@angular/material/divider';
|
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { LoanProductCatalogService } from '../../services/loan-product-catalog.service';
|
|
import { LoanProductTemplateCardComponent } from '../loan-product-template-card/loan-product-template-card.component';
|
|
import { PRODUCT_SUB_TYPES, PRODUCT_TYPE_GROUPS } from 'app/products/loan-products/loan-product-types';
|
|
|
|
/**
|
|
* Loans Account Details Step
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-loans-account-details-step',
|
|
templateUrl: './loans-account-details-step.component.html',
|
|
styleUrls: ['./loans-account-details-step.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatTooltip,
|
|
NgxMatSelectSearchModule,
|
|
MatDivider,
|
|
MatCheckbox,
|
|
MatStepperPrevious,
|
|
FaIconComponent,
|
|
MatStepperNext,
|
|
AsyncPipe,
|
|
LoanProductTemplateCardComponent
|
|
]
|
|
})
|
|
export class LoansAccountDetailsStepComponent implements OnInit, OnDestroy {
|
|
private formBuilder = inject(UntypedFormBuilder);
|
|
private loansService = inject(LoansService);
|
|
private route = inject(ActivatedRoute);
|
|
private translateService = inject(TranslateService);
|
|
private settingsService = inject(SettingsService);
|
|
private commons = inject(Commons);
|
|
private catalog = inject(LoanProductCatalogService);
|
|
|
|
//** Defining PlaceHolders for the search bar */
|
|
placeHolderLabel = '';
|
|
noEntriesFoundLabel = '';
|
|
|
|
/** Loans Account Template */
|
|
@Input() loansAccountTemplate: any;
|
|
|
|
/** Minimum date allowed. */
|
|
minDate = new Date(2000, 0, 1);
|
|
/** Maximum date allowed. */
|
|
maxDate = new Date(2100, 0, 1);
|
|
/** Product Data */
|
|
productList: any;
|
|
/** Loan Officer Data */
|
|
loanOfficerOptions: any;
|
|
/** Loan Purpose Options */
|
|
loanPurposeOptions: any;
|
|
/** Fund Options */
|
|
fundOptions: any;
|
|
/** Account Linking Options */
|
|
accountLinkingOptions: any;
|
|
/** For edit loan accounts form */
|
|
isFieldOfficerPatched = false;
|
|
/** Loans Account Details Form */
|
|
loansAccountDetailsForm: UntypedFormGroup;
|
|
|
|
loanId: any = null;
|
|
|
|
/** Category and product type narrow the product picker to the active products of that kind. */
|
|
readonly categories = PRODUCT_TYPE_GROUPS;
|
|
categoryFilter = '';
|
|
typeFilter = '';
|
|
/** Ids of the products active today; null until read, or if the read failed (then nothing is hidden). */
|
|
private activeProductIds: Set<number> | null = null;
|
|
/** Every product from the template, before any filter. */
|
|
private allProducts: any[] = [];
|
|
|
|
/** Types offered for the chosen category; every type when no category is chosen. */
|
|
get typeOptions() {
|
|
return this.categoryFilter
|
|
? (PRODUCT_SUB_TYPES[this.categoryFilter] ?? [])
|
|
: Object.values(PRODUCT_SUB_TYPES).flat();
|
|
}
|
|
|
|
/** The product picked in the form, with its category and type, for the template card. */
|
|
get selectedProduct(): any {
|
|
return this.catalog.get(this.loansAccountDetailsForm?.get('productId')?.value);
|
|
}
|
|
|
|
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();
|
|
/**
|
|
* Sets loans account details form.
|
|
* @param {FormBuilder} formBuilder Form Builder.
|
|
* @param {LoansService} loansService Loans Service.
|
|
* @param {SettingsService} settingsService SettingsService
|
|
*/
|
|
constructor() {
|
|
this.loanId = this.route.snapshot.params['loanId'];
|
|
this.createLoansAccountDetailsForm();
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.placeHolderLabel = this.translateService.instant('labels.text.Search');
|
|
this.noEntriesFoundLabel = this.translateService.instant('labels.text.No data found');
|
|
this.maxDate = this.settingsService.maxFutureDate;
|
|
this.buildDependencies();
|
|
if (this.loansAccountTemplate) {
|
|
this.allProducts = this.loansAccountTemplate.productOptions.sort(this.commons.dynamicSort('name'));
|
|
this.productList = this.allProducts;
|
|
if (this.loansAccountTemplate.loanProductId) {
|
|
this.loansAccountDetailsForm.patchValue({
|
|
productId: this.loansAccountTemplate.loanProductId,
|
|
submittedOnDate:
|
|
this.loansAccountTemplate.timeline.submittedOnDate &&
|
|
new Date(this.loansAccountTemplate.timeline.submittedOnDate),
|
|
loanOfficerId: this.loansAccountTemplate.loanOfficerId,
|
|
loanPurposeId: this.loansAccountTemplate.loanPurposeId,
|
|
fundId: this.loansAccountTemplate.fundId,
|
|
expectedDisbursementDate:
|
|
this.loansAccountTemplate.timeline.expectedDisbursementDate &&
|
|
new Date(this.loansAccountTemplate.timeline.expectedDisbursementDate),
|
|
externalId: this.loansAccountTemplate.externalId
|
|
});
|
|
}
|
|
}
|
|
this.filterFormCtrl.valueChanges.pipe(takeUntil(this._onDestroy)).subscribe(() => {
|
|
this.searchItem();
|
|
});
|
|
this.productData.next(this.productList.slice());
|
|
this.loadActiveProducts();
|
|
}
|
|
|
|
/**
|
|
* A new account can only be opened on a product that is active today, and the picker can be narrowed by category
|
|
* and type. Editing an existing account keeps every product, since its own product may have closed since.
|
|
*/
|
|
private loadActiveProducts(): void {
|
|
if (this.loanId) {
|
|
return;
|
|
}
|
|
this.catalog.activeProducts().subscribe((products) => {
|
|
this.activeProductIds = products.length > 0 ? new Set(products.map((product) => Number(product.id))) : null;
|
|
this.applyProductFilters();
|
|
});
|
|
}
|
|
|
|
onCategoryChange(): void {
|
|
// A type belongs to one category, so a type from another category cannot stay selected.
|
|
if (this.typeFilter && !this.typeOptions.some((type) => type.id === this.typeFilter)) {
|
|
this.typeFilter = '';
|
|
}
|
|
this.applyProductFilters();
|
|
}
|
|
|
|
/** Rebuilds the product list from the template: active only, then category and type, then the search text. */
|
|
applyProductFilters(): void {
|
|
this.productList = this.allProducts.filter((product: any) => {
|
|
if (this.activeProductIds && !this.activeProductIds.has(Number(product.id))) {
|
|
return false;
|
|
}
|
|
const ext = this.catalog.classification(product.id);
|
|
if (this.categoryFilter && ext['productTypeGroup'] !== this.categoryFilter) {
|
|
return false;
|
|
}
|
|
return !this.typeFilter || ext['productTypeSubGroup'] === this.typeFilter;
|
|
});
|
|
this.searchItem();
|
|
}
|
|
|
|
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;
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates loans account details form.
|
|
*/
|
|
createLoansAccountDetailsForm() {
|
|
this.loansAccountDetailsForm = this.formBuilder.group({
|
|
productId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
loanOfficerId: [''],
|
|
loanPurposeId: [''],
|
|
fundId: [''],
|
|
submittedOnDate: [
|
|
this.settingsService.businessDate,
|
|
Validators.required
|
|
],
|
|
expectedDisbursementDate: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
externalId: [''],
|
|
linkAccountId: [''],
|
|
createStandingInstructionAtDisbursement: ['']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches loans account product template on productId value changes
|
|
*/
|
|
buildDependencies() {
|
|
const entityId = this.loansAccountTemplate.clientId
|
|
? this.loansAccountTemplate.clientId
|
|
: this.loansAccountTemplate.group.id;
|
|
const isGroup = this.loansAccountTemplate.clientId ? false : true;
|
|
this.loansAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
|
|
this.loansService.getLoansAccountTemplateResource(entityId, isGroup, productId).subscribe((response: any) => {
|
|
this.loansAccountProductTemplate.emit(response);
|
|
this.loanOfficerOptions = response.loanOfficerOptions;
|
|
this.loanPurposeOptions = response.loanPurposeOptions;
|
|
this.fundOptions = response.fundOptions;
|
|
this.accountLinkingOptions = response.accountLinkingOptions;
|
|
this.loanProductSelected = true;
|
|
if (response.createStandingInstructionAtDisbursement) {
|
|
this.loansAccountDetailsForm
|
|
.get('createStandingInstructionAtDisbursement')
|
|
.patchValue(response.createStandingInstructionAtDisbursement);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns loans account details form value.
|
|
*/
|
|
get loansAccountDetails() {
|
|
return this.loansAccountDetailsForm.getRawValue();
|
|
}
|
|
}
|