Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 23 additions & 31 deletions src/app/clients/clients-view/general-tab/general-tab.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ <h3>{{ 'labels.heading.Loan Accounts' | translate }}</h3>
<mifosx-account-number accountNo="{{ element.accountNo }}"></mifosx-account-number>
</td>
</ng-container>
<ng-container matColumnDef="Product Type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Product Type' | translate }}</th>
<td mat-cell *matCellDef="let element">
{{ loanProductTypeLabel(element.productType) | translate }}
</td>
</ng-container>
<ng-container matColumnDef="Loan Account">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Loan Product' | translate }}</th>
<td mat-cell *matCellDef="let element">
Expand Down Expand Up @@ -180,29 +186,19 @@ <h3>{{ 'labels.heading.Loan Accounts' | translate }}</h3>
<ng-container matColumnDef="Type">
<th mat-header-cell class="center" *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell class="center" *matCellDef="let element">
<i
class="fa fa-large"
[ngClass]="element.loanType.value === 'Individual' ? 'fa-user' : 'fa-group'"
matTooltip="{{ element.loanType.value }}"
matTooltipPosition="above"
></i>
@if (element.productType === 'loan') {
<i
class="fa fa-large"
[ngClass]="element.loanType.value === 'Individual' ? 'fa-user' : 'fa-group'"
matTooltip="{{ element.loanType.value }}"
matTooltipPosition="above"
></i>
}
</td>
</ng-container>
<ng-container matColumnDef="Actions">
<th mat-header-cell class="center" *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell class="center" *matCellDef="let element">
<!-- Print Icon -->
<button
class="account-action-button"
mat-raised-button
color="accent"
matTooltip="{{ 'tooltips.Print Loan Application' | translate }}"
matTooltipPosition="above"
aria-label="{{ 'tooltips.Print Loan Application' | translate }}"
(click)="openLoanApplicationReport($event, element.id)"
>
<i class="fa fa-print"></i>
</button>
@if (element.status.active) {
<button
class="account-action-button"
Expand Down Expand Up @@ -279,6 +275,7 @@ <h3>{{ 'labels.heading.Loan Accounts' | translate }}</h3>
mat-row
*matRowDef="let row; columns: openLoansColumns"
[routerLink]="['../', 'loans-accounts', row.id, 'general']"
[queryParams]="{ productType: row.productType }"
class="select-row"
></tr>
<tr
Expand Down Expand Up @@ -307,6 +304,12 @@ <h3>{{ 'labels.heading.Loan Accounts' | translate }}</h3>
<mifosx-long-text textValue="{{ element.productName }}" chars="35"></mifosx-long-text>
</td>
</ng-container>
<ng-container matColumnDef="Product Type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Product Type' | translate }}</th>
<td mat-cell *matCellDef="let element">
{{ loanProductTypeLabel(element.productType) | translate }}
</td>
</ng-container>
<ng-container matColumnDef="Original Loan">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Original Loan' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.lastActiveTransactionDate | dateFormat }}</td>
Expand Down Expand Up @@ -340,19 +343,7 @@ <h3>{{ 'labels.heading.Loan Accounts' | translate }}</h3>
</ng-container>
<ng-container matColumnDef="Actions">
<th mat-header-cell class="center" *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell class="center" *matCellDef="let element">
<!-- Print Icon -->
<button
class="account-action-button"
mat-raised-button
color="accent"
matTooltip="{{ 'tooltips.Print Loan Application' | translate }}"
matTooltipPosition="above"
(click)="openLoanApplicationReport($event, element.id)"
>
<i class="fa fa-print"></i>
</button>
</td>
<td mat-cell class="center" *matCellDef="let element"></td>
</ng-container>

<ng-container matColumnDef="no-data">
Expand All @@ -368,6 +359,7 @@ <h3>{{ 'labels.heading.Loan Accounts' | translate }}</h3>
mat-row
*matRowDef="let row; columns: closedLoansColumns"
[routerLink]="['../', 'loans-accounts', row.id, 'general']"
[queryParams]="{ productType: row.productType }"
class="select-row"
></tr>
<tr
Expand Down
26 changes: 23 additions & 3 deletions src/app/clients/clients-view/general-tab/general-tab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*/

/** Angular Imports */
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { Component, OnDestroy, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';

/** Custom Services. */
Expand Down Expand Up @@ -45,6 +45,7 @@ import { Subject } from 'rxjs';
import { takeUntil, catchError } from 'rxjs/operators';
import { AlertService } from 'app/core/alert/alert.service';
import { EMPTY } from 'rxjs';
import { LoanProductService } from 'app/products/loan-products/services/loan-product.service';

/**
* General Tab component.
Expand Down Expand Up @@ -155,6 +156,7 @@ export class GeneralTabComponent implements OnDestroy {
/** Open Loan Accounts Columns */
openLoansColumns: string[] = [
'Account No',
'Product Type',
'Loan Account',
'Original Loan',
'Loan Balance',
Expand All @@ -165,6 +167,7 @@ export class GeneralTabComponent implements OnDestroy {
/** Closed Loan Accounts Columns */
closedLoansColumns: string[] = [
'Account No',
'Product Type',
'Loan Account',
'Original Loan',
'Loan Balance',
Expand Down Expand Up @@ -226,6 +229,7 @@ export class GeneralTabComponent implements OnDestroy {
clientAccountData: any;
/** Loan Accounts Data */
loanAccounts: any[] = [];
workingCapitalLoanAccounts: any[] = [];
/** Savings Accounts Data */
savingAccounts: any[] = [];
/** Shares Accounts Data */
Expand Down Expand Up @@ -273,7 +277,10 @@ export class GeneralTabComponent implements OnDestroy {
(data: { clientAccountsData: any; clientChargesData: any; clientSummary: any; clientCollateralData: any }) => {
this.clientAccountData = data.clientAccountsData;
this.savingAccounts = data.clientAccountsData?.savingsAccounts ?? [];
this.loanAccounts = data.clientAccountsData?.loanAccounts ?? [];
this.loanAccounts = [];
this.processLoanAccounts(data.clientAccountsData?.loanAccounts ?? [], 'loan');
this.processLoanAccounts(data.clientAccountsData?.workingCapitalLoanAccounts ?? [], 'working-capital');
this.workingCapitalLoanAccounts = data.clientAccountsData?.workingCapitalLoanAccounts ?? [];
this.shareAccounts = data.clientAccountsData?.shareAccounts ?? [];

this.upcomingCharges = data.clientChargesData?.pageItems ?? [];
Expand Down Expand Up @@ -417,4 +424,17 @@ export class GeneralTabComponent implements OnDestroy {
return 'labels.buttons.View Closed Accounts';
}
}

loanProductTypeLabel(productType: string): string {
return LoanProductService.productTypeLabel(productType);
}

private processLoanAccounts(accounts: any[], productType: string): void {
accounts.map((account: any) => {
this.loanAccounts.push({
productType: productType,
...account
});
});
Comment on lines +432 to +438
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return mapped accounts instead of using map() for side effects.

Biome already flags this callback because it never returns. It also lets an existing account.productType overwrite the explicit type because the spread comes last.

♻️ Proposed fix
  private processLoanAccounts(accounts: any[], productType: string): void {
-    accounts.map((account: any) => {
-      this.loanAccounts.push({
-        productType: productType,
-        ...account
-      });
-    });
+    this.loanAccounts.push(
+      ...accounts.map((account: any) => ({
+        ...account,
+        productType
+      }))
+    );
  }
🧰 Tools
🪛 Biome (2.4.6)

[error] 433-433: This callback passed to map() iterable method should always return a value.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/clients/clients-view/general-tab/general-tab.component.ts` around
lines 432 - 438, processLoanAccounts currently uses Array.map for side effects
and places the spread after productType so account.productType can overwrite it;
change it to return the mapped accounts and ensure the explicit productType wins
by putting it last. Specifically, in processLoanAccounts(accounts, productType)
replace the side-effect map with const mapped = accounts.map(account => ({
...account, productType })); then merge/assign that result into
this.loanAccounts (e.g., this.loanAccounts = this.loanAccounts.concat(mapped) or
return mapped to be used by the caller) and update any callers if you choose to
return the array.

}
}
5 changes: 4 additions & 1 deletion src/app/core/shell/breadcrumb/breadcrumb.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/** Angular Imports */
import { Component, TemplateRef, ElementRef, ViewChild, AfterViewInit, OnDestroy, inject } from '@angular/core';
import { ActivatedRoute, Router, NavigationEnd, Data, RouterLink } from '@angular/router';
import { ActivatedRoute, Router, NavigationEnd, Data } from '@angular/router';

/** rxjs Imports */
import { filter, takeUntil } from 'rxjs/operators';
Expand Down Expand Up @@ -243,6 +243,9 @@ export class BreadcrumbComponent implements AfterViewInit, OnDestroy {
}

printableValue(value: string): string {
if (!value) {
return '';
}
if (value.length <= 30) {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { catchError } from 'rxjs/operators';
/** Custom Services */
import { LoansService } from '../loans.service';
import { OrganizationService } from 'app/organization/organization.service';
import { LoanProductService } from 'app/products/loan-products/services/loan-product.service';

/**
* Loans notes data resolver.
Expand All @@ -25,6 +26,7 @@ import { OrganizationService } from 'app/organization/organization.service';
export class LoanActionButtonResolver {
private loansService = inject(LoansService);
private organizationService = inject(OrganizationService);
private loanProductService = inject(LoanProductService);

/**
* Returns the Loans Notes Data.
Expand Down Expand Up @@ -74,7 +76,9 @@ export class LoanActionButtonResolver {
} else if (loanActionButton === 'Loan Screen Reports') {
return this.loansService.getLoanScreenReportsData();
} else if (loanActionButton === 'Approve') {
return this.loansService.getLoanApprovalTemplate(loanId);
return this.loanProductService.isLoanProduct
? this.loansService.getLoanApprovalTemplate(loanId)
: this.loansService.getWorkingCapitalLoanActionTemplate(loanId, loanActionButton.toLowerCase());
} else if (loanActionButton === 'Add Loan Charge') {
return this.loansService.getLoanChargeTemplateResource(loanId);
} else if (loanActionButton === 'Foreclosure') {
Expand Down
13 changes: 12 additions & 1 deletion src/app/loans/common-resolvers/loan-details.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,33 @@ import { Observable } from 'rxjs';

/** Custom Services */
import { LoansService } from '../loans.service';
import { LoanProductService } from 'app/products/loan-products/services/loan-product.service';
import { LOAN_PRODUCT_TYPE } from 'app/products/loan-products/models/loan-product.model';

/**
* Clients data resolver.
*/
@Injectable()
export class LoanDetailsResolver {
private loansService = inject(LoansService);
private loanProductService = inject(LoanProductService);

/**
* Returns the Loans with Association data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
const productType = route.queryParams['productType'];
const resolvedProductType =
productType === LOAN_PRODUCT_TYPE.WORKING_CAPITAL ? LOAN_PRODUCT_TYPE.WORKING_CAPITAL : LOAN_PRODUCT_TYPE.LOAN;
this.loanProductService.initialize(resolvedProductType);
if (!isNaN(+loanId)) {
return this.loansService.getLoanAccountAssociationDetails(loanId);
if (resolvedProductType === LOAN_PRODUCT_TYPE.LOAN) {
return this.loansService.getLoanAccountAssociationDetails(loanId);
} else {
return this.loansService.getWorkingCapitalLoannDetails(loanId);
}
}
}
Comment on lines 33 to 46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing return path when loanId is invalid.

When loanId is NaN, the resolver implicitly returns undefined. This could cause downstream issues as the route expects an Observable. Consider returning an empty observable or throwing an error for invalid loan IDs.

🛡️ Proposed fix to handle invalid loanId
+import { EMPTY } from 'rxjs';
+
 resolve(route: ActivatedRouteSnapshot): Observable<any> {
   const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
   const productType = route.queryParams['productType'];
   const resolvedProductType =
     productType === LOAN_PRODUCT_TYPE.WORKING_CAPITAL ? LOAN_PRODUCT_TYPE.WORKING_CAPITAL : LOAN_PRODUCT_TYPE.LOAN;
   this.loanProductService.initialize(resolvedProductType);
   if (!isNaN(+loanId)) {
     if (resolvedProductType === LOAN_PRODUCT_TYPE.LOAN) {
       return this.loansService.getLoanAccountAssociationDetails(loanId);
     } else {
       return this.loansService.getWorkingCapitalLoannDetails(loanId);
     }
   }
+  return EMPTY;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/loans/common-resolvers/loan-details.resolver.ts` around lines 33 -
46, The resolver's resolve method can return undefined when loanId is invalid;
update resolve in loan-details.resolver.ts to always return an Observable by
handling the NaN path—after calling
loanProductService.initialize(resolvedProductType) check the parsed loanId and
if invalid return an empty Observable (e.g., rxjs EMPTY) or a thrown error
Observable (e.g., throwError) instead of falling through; keep the existing
branches that call loansService.getLoanAccountAssociationDetails(loanId) and
loansService.getWorkingCapitalLoannDetails(loanId) for valid IDs so the method
consistently returns an Observable.

}
33 changes: 33 additions & 0 deletions src/app/loans/common-resolvers/loan-products.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';

/** rxjs Imports */
import { Observable } from 'rxjs';

/** Custom Services */
import { ProductsService } from 'app/products/products.service';

/**
* Loan Product list data resolver.
*/
@Injectable()
export class LoanProductsResolver {
private productsService = inject(ProductsService);

/**
* Returns the loan account template data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.productsService.getLoanProductsBasicDetails();
}
}
Loading
Loading