/** * 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 | 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 = new ReplaySubject(1); /** control for the filter select */ protected filterFormCtrl: UntypedFormControl = new UntypedFormControl(''); /** Subject that emits when the component has been destroyed. */ protected _onDestroy = new Subject(); /** 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(); } }