WEB-263: Add Buy Down Fees tabs

This commit is contained in:
Oleksii Novikov
2025-07-28 17:32:25 +03:00
parent e55aaf90be
commit c1128f424d
21 changed files with 397 additions and 27 deletions
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { LoansService } from '../loans.service';
@Injectable()
export class LoanBuyDownFeesDataResolver implements Resolve<Object> {
constructor(private loansService: LoansService) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
if (!loanId) {
console.error('LoanBuyDownFeesDataResolver: Could not find loanId in route parameters');
return new Observable((observer) => {
observer.next([]);
observer.complete();
});
}
return this.loansService.getBuyDownFeeData(loanId);
}
}
+12 -1
View File
@@ -28,6 +28,7 @@ import { ViewRecieptComponent } from './loans-view/transactions/view-reciept/vie
import { ExportTransactionsComponent } from './loans-view/transactions/export-transactions/export-transactions.component';
import { GlimAccountComponent } from './glim-account/glim-account.component';
import { CreateGlimAccountComponent } from './glim-account/create-glim-account/create-glim-account.component';
import { LoanBuyDownFeesTabComponent } from './loans-view/loan-buy-down-fees-tab/loan-buy-down-fees-tab.component';
/** Custom Resolvers */
import { LoanDetailsResolver } from './common-resolvers/loan-details.resolver';
@@ -64,6 +65,7 @@ import { LoanTermVariationsTabComponent } from './loans-view/loan-term-variation
import { LoanTermVariationsResolver } from './common-resolvers/loan-term-variations.resolver';
import { LoanDeferredIncomeTabComponent } from './loans-view/loan-deferred-income-tab/loan-deferred-income-tab.component';
import { LoanDeferredIncomeDataResolver } from './common-resolvers/loan-deferred-income-data.resolver';
import { LoanBuyDownFeesDataResolver } from './common-resolvers/loan-buy-down-fees-data.resolver';
/** Loans Route. */
const routes: Routes = [
@@ -260,6 +262,14 @@ const routes: Routes = [
}
}
]
},
{
path: 'buy-down-fees',
component: LoanBuyDownFeesTabComponent,
data: { title: 'Buy Down Fees', breadcrumb: 'Buy Down Fees', routeParamBreadcrumb: false },
resolve: {
loanBuyDownFeesData: LoanBuyDownFeesDataResolver
}
}
]
},
@@ -407,7 +417,8 @@ const routes: Routes = [
ExternalAssetOwnerResolver,
LoanDelinquencyDataResolver,
LoanTermVariationsResolver,
LoanDeferredIncomeDataResolver
LoanDeferredIncomeDataResolver,
LoanBuyDownFeesDataResolver
]
})
export class LoansRoutingModule {}
@@ -0,0 +1,60 @@
<div class="container">
<h3>{{ 'labels.heading.Buy Down Fees' | translate }}</h3>
<div *ngIf="isLoading" class="loading-indicator">
<mat-spinner diameter="30"></mat-spinner>
</div>
<div *ngIf="!isLoading">
<div *ngIf="buyDownFeeData.length === 0" class="no-data">
{{ 'labels.messages.No Data Found' | translate }}
</div>
<table mat-table [dataSource]="buyDownFeeData" *ngIf="buyDownFeeData.length > 0">
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Date' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.date | date: 'mediumDate' }}
</td>
</ng-container>
<ng-container matColumnDef="buyDownFeeAmount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Fee Amount' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.buyDownFeeAmount | formatNumber: '0.00' }}
</td>
</ng-container>
<ng-container matColumnDef="amortizedAmount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Amortized Amount' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.amortizedAmount | formatNumber: '0.00' }}
</td>
</ng-container>
<ng-container matColumnDef="notYetAmortizedAmount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Not Yet Amortized Amount' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.notYetAmortizedAmount | formatNumber: '0.00' }}
</td>
</ng-container>
<ng-container matColumnDef="adjustedAmount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Adjusted Amount' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.adjustedAmount | formatNumber: '0.00' }}
</td>
</ng-container>
<ng-container matColumnDef="chargedOffAmount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Charged Off Amount' | translate }}</th>
<td mat-cell *matCellDef="let item">
{{ item.chargedOffAmount | formatNumber: '0.00' }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="buyDownFeeColumns"></tr>
<tr mat-row *matRowDef="let row; columns: buyDownFeeColumns"></tr>
</table>
</div>
</div>
@@ -0,0 +1,106 @@
import { Component, OnInit } from '@angular/core';
import {
MatCell,
MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable
} from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { FormatNumberPipe } from '@pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { LoansService } from '../../loans.service';
import { BuyDownFeeAmortizationDetails } from '../../models/loan-account.model';
import { DatePipe } from '@angular/common';
@Component({
selector: 'mifosx-loan-buy-down-fees-tab',
templateUrl: './loan-buy-down-fees-tab.component.html',
styleUrl: './loan-buy-down-fees-tab.component.scss',
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FormatNumberPipe,
DatePipe
]
})
export class LoanBuyDownFeesTabComponent implements OnInit {
buyDownFeeData: BuyDownFeeAmortizationDetails[] = [];
loanId: string;
isLoading = true;
buyDownFeeColumns: string[] = [
'date',
'buyDownFeeAmount',
'amortizedAmount',
'notYetAmortizedAmount',
'adjustedAmount',
'chargedOffAmount'
];
constructor(
private route: ActivatedRoute,
private loansService: LoansService
) {}
ngOnInit(): void {
this.getLoanId();
this.loadBuyDownFees();
}
private getLoanId(): void {
if (this.route.snapshot.data && this.route.snapshot.data['loanId']) {
this.loanId = this.route.snapshot.data['loanId'];
return;
}
let currentRoute = this.route;
while (currentRoute) {
if (currentRoute.snapshot.paramMap.has('loanId')) {
this.loanId = currentRoute.snapshot.paramMap.get('loanId');
return;
}
if (currentRoute.parent) {
currentRoute = currentRoute.parent;
} else {
break;
}
}
console.error('Could not find loanId in route parameters');
}
private loadBuyDownFees(): void {
if (!this.loanId) {
console.error('Cannot load buy down fees: loanId is undefined');
this.isLoading = false;
return;
}
this.isLoading = true;
this.loansService.getBuyDownFeeData(this.loanId).subscribe({
next: (data: BuyDownFeeAmortizationDetails[]) => {
this.buyDownFeeData = data || [];
this.isLoading = false;
},
error: (error) => {
console.error('Error loading buy down fees:', error);
this.isLoading = false;
}
});
}
}
@@ -219,6 +219,17 @@
{{ 'labels.inputs.Deferred income' | translate }}
</a>
</ng-container>
<ng-container *ngIf="loanDetailsData.enableBuyDownFee">
<a
mat-tab-link
[routerLink]="['./buy-down-fees']"
routerLinkActive
#buyDownFees="routerLinkActive"
[active]="buyDownFees.isActive"
>
{{ 'labels.heading.Buy Down Fees' | translate }}
</a>
</ng-container>
<ng-container *ngIf="loanDetailsData.status.active">
<a
mat-tab-link
+4
View File
@@ -104,6 +104,10 @@ export class LoansService {
return this.http.get(`/loans/${loanId}/deferredincome`);
}
getBuyDownFeeData(loanId: string): Observable<any> {
return this.http.get(`/loans/${loanId}/buydownfees`);
}
/**
* Returns the loan template data with specific condition
* @param loanId Loan Id
@@ -133,3 +133,15 @@ export interface LoanCapitalizedIncomeData {
unrecognizedAmount?: number;
amountAdjustment?: number;
}
export interface BuyDownFeeAmortizationDetails {
id: number;
loanId: number;
transactionId: number;
date: string;
buyDownFeeAmount: number;
amortizedAmount: number;
notYetAmortizedAmount: number;
adjustedAmount: number;
chargedOffAmount: number;
}