Skip to content

Commit 77d572a

Browse files
WEB-657: Working Capital loan account details enriched
1 parent 72fab61 commit 77d572a

25 files changed

Lines changed: 6636 additions & 5816 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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> {
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+
}
38+
}

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/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: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@
113113
</table>
114114
</div>
115115
}
116-
@if (loanProductService.isWorkingCapital && loanDetailsData?.delinquencyBucket) {
116+
@if (
117+
loanProductService.isWorkingCapital &&
118+
loanDetailsData?.collectionData &&
119+
loanDetailsData.collectionData.delinquentDays > 0
120+
) {
117121
<div>
118122
<table class="account-overview">
119123
<tbody>
@@ -124,12 +128,10 @@
124128
{{ loanDetailsData?.delinquencyBucket.name }}
125129
</td>
126130
</tr>
127-
@if (loanDetailsData.collectionData && loanDetailsData.collectionData.delinquentDays > 0) {
128-
<tr>
129-
<td>{{ 'labels.inputs.Delinquent Days' | translate }} :</td>
130-
<td>{{ loanDetailsData?.collectionData.delinquentDays | formatNumber }}</td>
131-
</tr>
132-
}
131+
<tr>
132+
<td>{{ 'labels.inputs.Delinquent Days' | translate }} :</td>
133+
<td>{{ loanDetailsData?.collectionData.delinquentDays | formatNumber }}</td>
134+
</tr>
133135
</tbody>
134136
</table>
135137
</div>
@@ -308,18 +310,46 @@ <h3>{{ 'labels.heading.Account Overview' | translate }}</h3>
308310
{{ 'labels.inputs.General' | translate }}
309311
</a>
310312
}
311-
<a
312-
mat-tab-link
313-
[routerLink]="['./repayment-schedule']"
314-
[queryParams]="{
315-
productType: loanProductService.productType.value
316-
}"
317-
routerLinkActive
318-
#repaymentSchedule="routerLinkActive"
319-
[active]="repaymentSchedule.isActive"
320-
>
321-
{{ 'labels.inputs.Repayment Schedule' | translate }}
322-
</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+
}
323353
@if (loanDetailsData.transactions) {
324354
<a
325355
mat-tab-link
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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="paymentsLeft">
32+
<th mat-header-cell class="center mat-header-cell" *matHeaderCellDef>
33+
{{ 'labels.inputs.Payments Left' | translate }}
34+
</th>
35+
<td mat-cell class="center" *matCellDef="let item">{{ item.paymentsLeft }}</td>
36+
<td mat-footer-cell class="center" *matFooterCellDef><b>Total</b></td>
37+
</ng-container>
38+
39+
<ng-container matColumnDef="expectedPaymentAmount">
40+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
41+
{{ 'labels.inputs.Expected Payment Amount' | translate }}
42+
</th>
43+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.expectedPaymentAmount | formatNumber }}</td>
44+
<td mat-footer-cell class="r-amount" *matFooterCellDef>
45+
<b>{{ amortizationSchedule?.totalPaymentValue | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}</b>
46+
</td>
47+
</ng-container>
48+
49+
<ng-container matColumnDef="discountFactor">
50+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
51+
{{ 'labels.inputs.Discount Factor' | translate }}
52+
</th>
53+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.discountFactor | formatNumber }}</td>
54+
<td mat-footer-cell *matFooterCellDef></td>
55+
</ng-container>
56+
57+
<ng-container matColumnDef="npvValue">
58+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
59+
{{ 'labels.inputs.NPV Value' | translate }}
60+
</th>
61+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.npvValue | formatNumber }}</td>
62+
<td mat-footer-cell *matFooterCellDef></td>
63+
</ng-container>
64+
65+
<ng-container matColumnDef="deferredBalance">
66+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
67+
{{ 'labels.inputs.Deferred Balance' | translate }}
68+
</th>
69+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.deferredBalance | formatNumber }}</td>
70+
<td mat-footer-cell *matFooterCellDef></td>
71+
</ng-container>
72+
73+
<ng-container matColumnDef="forecastPaymentAmount">
74+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
75+
{{ 'labels.inputs.Forecast Payment Amount' | translate }}
76+
</th>
77+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.forecastPaymentAmount | formatNumber }}</td>
78+
<td mat-footer-cell *matFooterCellDef></td>
79+
</ng-container>
80+
81+
<ng-container matColumnDef="expectedAmortizationAmount">
82+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
83+
{{ 'labels.inputs.Expected Amortization Amount' | translate }}
84+
</th>
85+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.expectedAmortizationAmount | formatNumber }}</td>
86+
<td mat-footer-cell *matFooterCellDef></td>
87+
</ng-container>
88+
89+
<ng-container matColumnDef="netAmortizationAmount">
90+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
91+
{{ 'labels.inputs.Net Amortization Amount' | translate }}
92+
</th>
93+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.netAmortizationAmount | formatNumber }}</td>
94+
<td mat-footer-cell *matFooterCellDef></td>
95+
</ng-container>
96+
97+
<ng-container matColumnDef="incomeModification">
98+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
99+
{{ 'labels.inputs.Income Modification' | translate }}
100+
</th>
101+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.incomeModification | formatNumber }}</td>
102+
<td mat-footer-cell *matFooterCellDef></td>
103+
</ng-container>
104+
105+
<ng-container matColumnDef="balance">
106+
<th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef>
107+
{{ 'labels.inputs.Balance' | translate }}
108+
</th>
109+
<td mat-cell class="r-amount" *matCellDef="let item">{{ item.balance | formatNumber }}</td>
110+
<td mat-footer-cell *matFooterCellDef></td>
111+
</ng-container>
112+
113+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
114+
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
115+
<tr mat-footer-row *matFooterRowDef="displayedColumns"></tr>
116+
</table>
117+
</div>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
@use 'assets/styles/helper';
10+
@use 'assets/styles/colours' as *;
11+
12+
table {
13+
width: 100%;
14+
margin: 2% 0%;
15+
}
16+
17+
.table-row {
18+
font-size: small;
19+
}
20+
21+
.container {
22+
padding-bottom: 2%;
23+
width: 98%;
24+
}
25+
26+
#amortizationSchedule {
27+
width: 100%;
28+
}
29+
30+
.mat-header-cell {
31+
color: rgb(0 0 0 / 54%);
32+
font-size: 12px;
33+
font-weight: 500;
34+
}
35+
36+
:host-context(.dark-theme) .mat-header-cell {
37+
color: rgb(255 255 255 / 70%);
38+
}
39+
40+
div.container {
41+
overflow: auto;
42+
max-width: 100%;
43+
}

0 commit comments

Comments
 (0)