Skip to content

Commit f69238e

Browse files
WEB-657: Working Capital loan account details enhanced
1 parent 42c0ef3 commit f69238e

27 files changed

Lines changed: 6675 additions & 5820 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { Injectable, inject } from '@angular/core';
10+
import { ActivatedRouteSnapshot } from '@angular/router';
11+
import { Observable } from 'rxjs';
12+
import { LoanBaseResolver } from '../loan-base.resolver';
13+
import { LoansService } from 'app/loans/loans.service';
14+
15+
@Injectable({
16+
providedIn: 'root'
17+
})
18+
export class LoanAmortizationScheduleResolver extends LoanBaseResolver {
19+
private loansService = inject(LoansService);
20+
21+
constructor() {
22+
super();
23+
}
24+
25+
/**
26+
* Returns the Loans with Association data.
27+
* @returns {Observable<any>}
28+
*/
29+
resolve(route: ActivatedRouteSnapshot): Observable<any> | null {
30+
this.initialize(route);
31+
const loanId = route.paramMap.get('loanId') || route.parent?.paramMap.get('loanId');
32+
if (!isNaN(+loanId)) {
33+
if (this.isWorkingCapital) {
34+
return this.loansService.getWorkingCapitalLoanAmortizationSchedule(loanId);
35+
}
36+
}
37+
return null;
38+
}
39+
}

src/app/loans/loans-routing.module.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ import { LoanOriginatorsTabComponent } from './loans-view/loan-originators-tab/l
7979
import { LoanOriginatorsResolver } from './common-resolvers/loan-originators.resolver';
8080
import { LoanProductsResolver } from './common-resolvers/loan-products.resolver';
8181
import { LoanDelinquencyRangeScheduleResolver } from './common-resolvers/working-capital/loan-delinquency-actions.resolver';
82+
import { LoanAmortizationScheduleTabComponent } from './loans-view/working-capital/loan-amortization-schedule-tab/loan-amortization-schedule-tab.component';
83+
import { LoanAmortizationScheduleResolver } from './common-resolvers/working-capital/loan-amortization-schedule.resolver';
84+
import { LoanBalancesTabComponent } from './loans-view/working-capital/loan-balances-tab/loan-balances-tab.component';
8285

8386
/** Loans Route. */
8487
const routes: Routes = [
@@ -136,6 +139,19 @@ const routes: Routes = [
136139
component: RepaymentScheduleTabComponent,
137140
data: { title: 'Repayment Schedule', breadcrumb: 'Repayment Schedule', routeParamBreadcrumb: false }
138141
},
142+
{
143+
path: 'balances',
144+
component: LoanBalancesTabComponent,
145+
data: { title: 'Balances', breadcrumb: 'Balances', routeParamBreadcrumb: false }
146+
},
147+
{
148+
path: 'amortization-schedule',
149+
component: LoanAmortizationScheduleTabComponent,
150+
data: { title: 'Amortization Schedule', breadcrumb: 'Amortization Schedule', routeParamBreadcrumb: false },
151+
resolve: {
152+
amortizationSchedule: LoanAmortizationScheduleResolver
153+
}
154+
},
139155
{
140156
path: 'transactions',
141157
data: { title: 'Loans Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false },
@@ -449,7 +465,8 @@ const routes: Routes = [
449465
LoanTermVariationsResolver,
450466
LoanDeferredIncomeDataResolver,
451467
LoanBuyDownFeesDataResolver,
452-
LoanProductsResolver
468+
LoanProductsResolver,
469+
LoanAmortizationScheduleResolver
453470
]
454471
})
455472
export class LoansRoutingModule {}

src/app/loans/loans-view/account-details/account-details.component.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@ <h3>{{ 'labels.heading.Loan Details' | translate }}</h3>
1616
<span class="flex-50">{{ loanDetails.transactionProcessingStrategyName | translateKey: 'catalogs' }}</span>
1717
</div>
1818
}
19+
<div class="flex-fill layout-row">
20+
<span class="flex-50">{{ 'labels.inputs.Currency' | translate }}</span>
21+
<span class="flex-50">{{ loanDetails.currency?.code }}</span>
22+
</div>
1923

2024
@if (loanProductService.isWorkingCapital) {
25+
<div class="flex-fill layout-row">
26+
<span class="flex-50"> {{ 'labels.inputs.Amortization Type' | translate }} </span>
27+
<span class="flex-50"> {{ loanDetails.product.amortizationType?.value | translateKey: 'catalogs' }} </span>
28+
</div>
29+
2130
<div class="flex-fill layout-row">
2231
<span class="flex-50">{{ 'labels.inputs.Period Payment Frequency' | translate }}</span>
2332
<span class="flex-50">

src/app/loans/loans-view/general-tab/general-tab.component.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,24 @@ <h3>{{ 'labels.heading.Loan Amounts' | translate }}</h3>
291291
}}</span>
292292
</div>
293293
}
294+
@if (loanDetails.discountProposed > 0) {
295+
<div class="flex-contents">
296+
<span class="flex-50">{{ 'labels.inputs.Proposed Discount' | translate }}:</span>
297+
<span class="flex-50">{{ loanDetails.discountProposed | formatNumber }}</span>
298+
</div>
299+
}
300+
@if (loanDetails.discountApproved > 0) {
301+
<div class="flex-contents">
302+
<span class="flex-50">{{ 'labels.inputs.Approved Discount' | translate }}:</span>
303+
<span class="flex-50">{{ loanDetails.discountApproved | formatNumber }}</span>
304+
</div>
305+
}
306+
@if (loanDetails.discount > 0) {
307+
<div class="flex-contents">
308+
<span class="flex-50">{{ 'labels.inputs.Discount' | translate }}:</span>
309+
<span class="flex-50">{{ loanDetails.discount | formatNumber }}</span>
310+
</div>
311+
}
294312
@if (loanDetails.balloonRepaymentAmount > 0) {
295313
<div class="flex-contents">
296314
<span class="flex-50">{{ 'labels.inputs.Balloon Repayment Amount' | translate }}:</span>

src/app/loans/loans-view/loans-view.component.html

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
</tbody>
8181
</table>
8282
</div>
83-
@if (loanDisplayArrearsDelinquency !== 1) {
83+
@if (loanProductService.isLoanProduct && loanDisplayArrearsDelinquency !== 1) {
8484
<div>
8585
<table class="account-overview">
8686
<tbody>
@@ -113,6 +113,29 @@
113113
</table>
114114
</div>
115115
}
116+
@if (
117+
loanProductService.isWorkingCapital &&
118+
loanDetailsData?.collectionData &&
119+
loanDetailsData.collectionData.delinquentDays > 0
120+
) {
121+
<div>
122+
<table class="account-overview">
123+
<tbody>
124+
<tr>
125+
<td>{{ 'labels.inputs.Delinquency Classification' | translate }} :</td>
126+
<td>
127+
<span class="m-r-3"><i [ngClass]="loanDelinquencyClassificationStyle"></i></span>
128+
{{ loanDetailsData?.delinquencyBucket?.name }}
129+
</td>
130+
</tr>
131+
<tr>
132+
<td>{{ 'labels.inputs.Delinquent Days' | translate }} :</td>
133+
<td>{{ loanDetailsData?.collectionData.delinquentDays | formatNumber }}</td>
134+
</tr>
135+
</tbody>
136+
</table>
137+
</div>
138+
}
116139
</div>
117140

118141
@if (loanDetailsData.summary) {
@@ -287,18 +310,46 @@ <h3>{{ 'labels.heading.Account Overview' | translate }}</h3>
287310
{{ 'labels.inputs.General' | translate }}
288311
</a>
289312
}
290-
<a
291-
mat-tab-link
292-
[routerLink]="['./repayment-schedule']"
293-
[queryParams]="{
294-
productType: loanProductService.productType.value
295-
}"
296-
routerLinkActive
297-
#repaymentSchedule="routerLinkActive"
298-
[active]="repaymentSchedule.isActive"
299-
>
300-
{{ 'labels.inputs.Repayment Schedule' | translate }}
301-
</a>
313+
@if (loanProductService.isLoanProduct) {
314+
<a
315+
mat-tab-link
316+
[routerLink]="['./repayment-schedule']"
317+
[queryParams]="{
318+
productType: loanProductService.productType.value
319+
}"
320+
routerLinkActive
321+
#repaymentSchedule="routerLinkActive"
322+
[active]="repaymentSchedule.isActive"
323+
>
324+
{{ 'labels.inputs.Repayment Schedule' | translate }}
325+
</a>
326+
}
327+
@if (loanProductService.isWorkingCapital) {
328+
<a
329+
mat-tab-link
330+
[routerLink]="['./balances']"
331+
[queryParams]="{
332+
productType: loanProductService.productType.value
333+
}"
334+
routerLinkActive
335+
#balances="routerLinkActive"
336+
[active]="balances.isActive"
337+
>
338+
{{ 'labels.inputs.Balances' | translate }}
339+
</a>
340+
<a
341+
mat-tab-link
342+
[routerLink]="['./amortization-schedule']"
343+
[queryParams]="{
344+
productType: loanProductService.productType.value
345+
}"
346+
routerLinkActive
347+
#amortizationSchedule="routerLinkActive"
348+
[active]="amortizationSchedule.isActive"
349+
>
350+
{{ 'labels.inputs.Amortization Schedule' | translate }}
351+
</a>
352+
}
302353
@if (loanDetailsData.transactions) {
303354
<a
304355
mat-tab-link

src/app/loans/loans-view/loans-view.component.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,20 @@ export class LoansViewComponent extends LoanProductBaseComponent implements OnIn
524524
if (!this.loanDetailsData) {
525525
return '';
526526
}
527-
if (this.loanDetailsData.chargedOff) {
528-
return 'loanStatusType.chargeoff';
529-
}
530-
if (this.isContractTermination(this.loanSubStatus)) {
531-
return 'loanSubStatusType.contractTermination';
532-
}
533-
if (this.loanDetailsData.inArrears) {
534-
return 'loanStatusType.activeOverdue';
527+
if (this.loanProductService.isLoanProduct) {
528+
if (this.loanDetailsData.chargedOff) {
529+
return 'loanStatusType.chargeoff';
530+
}
531+
if (this.isContractTermination(this.loanSubStatus)) {
532+
return 'loanSubStatusType.contractTermination';
533+
}
534+
if (this.loanDetailsData.inArrears) {
535+
return 'loanStatusType.activeOverdue';
536+
}
537+
} else if (this.loanProductService.isWorkingCapital) {
538+
if (this.loanDetailsData.collectionData?.delinquentDays > 0) {
539+
return 'loanStatusType.activeOverdue';
540+
}
535541
}
536542
return this.loanDetailsData.status?.code;
537543
}
@@ -543,8 +549,14 @@ export class LoansViewComponent extends LoanProductBaseComponent implements OnIn
543549
if (this.loanDetailsData.chargedOff) {
544550
return 'Chargeoff';
545551
}
546-
if (this.loanDetailsData.inArrears) {
547-
return 'activeOverdue';
552+
if (this.loanProductService.isWorkingCapital) {
553+
if (this.loanDetailsData.collectionData?.delinquentDays > 0) {
554+
return 'activeOverdue';
555+
}
556+
} else {
557+
if (this.loanDetailsData.inArrears) {
558+
return 'activeOverdue';
559+
}
548560
}
549561
return this.loanDetailsData.status?.code;
550562
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<!--
2+
Copyright since 2025 Mifos Initiative
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
-->
8+
9+
<div class="container">
10+
<div class="layout-row m-t-20 align-end align-items-center">
11+
<button mat-raised-button color="primary" (click)="exportToPDF()">
12+
<fa-icon icon="download" class="m-r-10"></fa-icon>{{ 'labels.buttons.Export to PDF' | translate }}
13+
</button>
14+
</div>
15+
16+
<table mat-table [dataSource]="amortizationSchedule?.payments" id="amortizationSchedule">
17+
<ng-container matColumnDef="number">
18+
<th mat-header-cell class="center mat-header-cell" *matHeaderCellDef>#</th>
19+
<td mat-cell class="right" *matCellDef="let item">{{ item.paymentNo }}</td>
20+
<td mat-footer-cell *matFooterCellDef></td>
21+
</ng-container>
22+
23+
<ng-container matColumnDef="paymentDate">
24+
<th mat-header-cell class="center mat-header-cell" *matHeaderCellDef>
25+
{{ 'labels.inputs.Payment Date' | translate }}
26+
</th>
27+
<td mat-cell class="m-r-5" *matCellDef="let item">{{ item.paymentDate | dateFormat }}</td>
28+
<td mat-footer-cell *matFooterCellDef></td>
29+
</ng-container>
30+
31+
<ng-container matColumnDef="expectedPaymentAmount">
32+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
33+
{{ 'labels.inputs.Expected Payment Amount' | translate }}
34+
</th>
35+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.expectedPaymentAmount | formatNumber }}</td>
36+
<td mat-footer-cell class="r-amount" *matFooterCellDef>
37+
<b>{{ amortizationSchedule?.totalPaymentValue | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}</b>
38+
</td>
39+
</ng-container>
40+
41+
<ng-container matColumnDef="discountFactor">
42+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
43+
{{ 'labels.inputs.Discount Factor' | translate }}
44+
</th>
45+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.discountFactor | formatNumber }}</td>
46+
<td mat-footer-cell *matFooterCellDef></td>
47+
</ng-container>
48+
49+
<ng-container matColumnDef="npvValue">
50+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
51+
{{ 'labels.inputs.NPV Value' | translate }}
52+
</th>
53+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.npvValue | formatNumber }}</td>
54+
<td mat-footer-cell *matFooterCellDef></td>
55+
</ng-container>
56+
57+
<ng-container matColumnDef="deferredBalance">
58+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
59+
{{ 'labels.inputs.Deferred Balance' | translate }}
60+
</th>
61+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.deferredBalance | formatNumber }}</td>
62+
<td mat-footer-cell *matFooterCellDef></td>
63+
</ng-container>
64+
65+
<ng-container matColumnDef="forecastPaymentAmount">
66+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
67+
{{ 'labels.inputs.Forecast Payment Amount' | translate }}
68+
</th>
69+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.forecastPaymentAmount | formatNumber }}</td>
70+
<td mat-footer-cell *matFooterCellDef></td>
71+
</ng-container>
72+
73+
<ng-container matColumnDef="expectedAmortizationAmount">
74+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
75+
{{ 'labels.inputs.Expected Amortization Amount' | translate }}
76+
</th>
77+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.expectedAmortizationAmount | formatNumber }}</td>
78+
<td mat-footer-cell *matFooterCellDef></td>
79+
</ng-container>
80+
81+
<ng-container matColumnDef="netAmortizationAmount">
82+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
83+
{{ 'labels.inputs.Net Amortization Amount' | translate }}
84+
</th>
85+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.netAmortizationAmount | formatNumber }}</td>
86+
<td mat-footer-cell *matFooterCellDef></td>
87+
</ng-container>
88+
89+
<ng-container matColumnDef="incomeModification">
90+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
91+
{{ 'labels.inputs.Income Modification' | translate }}
92+
</th>
93+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.incomeModification | formatNumber }}</td>
94+
<td mat-footer-cell *matFooterCellDef></td>
95+
</ng-container>
96+
97+
<ng-container matColumnDef="balance">
98+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
99+
{{ 'labels.inputs.Balance' | translate }}
100+
</th>
101+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.balance | formatNumber }}</td>
102+
<td mat-footer-cell *matFooterCellDef></td>
103+
</ng-container>
104+
105+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
106+
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
107+
<tr mat-footer-row *matFooterRowDef="displayedColumns"></tr>
108+
</table>
109+
</div>

0 commit comments

Comments
 (0)