-
Notifications
You must be signed in to change notification settings - Fork 921
Web 657: Working Capital loan account details enhanced #3554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * 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/. | ||
| */ | ||
|
|
||
| import { Injectable, inject } from '@angular/core'; | ||
| import { ActivatedRouteSnapshot } from '@angular/router'; | ||
| import { Observable } from 'rxjs'; | ||
| import { LoanBaseResolver } from '../loan-base.resolver'; | ||
| import { LoansService } from 'app/loans/loans.service'; | ||
|
|
||
| @Injectable({ | ||
| providedIn: 'root' | ||
| }) | ||
| export class LoanAmortizationScheduleResolver extends LoanBaseResolver { | ||
| private loansService = inject(LoansService); | ||
|
|
||
| constructor() { | ||
| super(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the Loans with Association data. | ||
| * @returns {Observable<any>} | ||
| */ | ||
| resolve(route: ActivatedRouteSnapshot): Observable<any> | null { | ||
| this.initialize(route); | ||
| const loanId = route.paramMap.get('loanId') || route.parent?.paramMap.get('loanId'); | ||
| if (!isNaN(+loanId)) { | ||
| if (this.isWorkingCapital) { | ||
| return this.loansService.getWorkingCapitalLoanAmortizationSchedule(loanId); | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,8 +16,17 @@ <h3>{{ 'labels.heading.Loan Details' | translate }}</h3> | |||||||||||||||||
| <span class="flex-50">{{ loanDetails.transactionProcessingStrategyName | translateKey: 'catalogs' }}</span> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| } | ||||||||||||||||||
| <div class="flex-fill layout-row"> | ||||||||||||||||||
| <span class="flex-50">{{ 'labels.inputs.Currency' | translate }}</span> | ||||||||||||||||||
| <span class="flex-50">{{ loanDetails.currency?.code }}</span> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
|
|
||||||||||||||||||
| @if (loanProductService.isWorkingCapital) { | ||||||||||||||||||
| <div class="flex-fill layout-row"> | ||||||||||||||||||
| <span class="flex-50"> {{ 'labels.inputs.Amortization Type' | translate }} </span> | ||||||||||||||||||
| <span class="flex-50"> {{ loanDetails.product.amortizationType?.value | translateKey: 'catalogs' }} </span> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
Comment on lines
+19
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search translation asset files for the two new keys.
fd -e json . src/assets/i18n --exec grep -l "labels.inputs" {} \; | head -5 | xargs -I{} grep -n '"Currency"\|"Amortization Type"' {}Repository: openMF/web-app Length of output: 161 🏁 Script executed: #!/bin/bash
# Find translation-related files and directories
echo "=== Looking for translation files ==="
find . -type f -name "*translation*" -o -name "*i18n*" -o -name "*.i18n.*" 2>/dev/null | head -20
echo -e "\n=== Checking for language/locale directories ==="
find . -type d -name "i18n" -o -name "locale*" -o -name "translations" -o -name "lang*" 2>/dev/null | head -10
echo -e "\n=== Checking for JSON files in common asset paths ==="
fd -t f -e json . assets 2>/dev/null | head -20Repository: openMF/web-app Length of output: 596 🏁 Script executed: #!/bin/bash
# Explore the translations directory structure
echo "=== Translation directory contents ==="
ls -la src/assets/translations/
echo -e "\n=== Looking for the specific i18n keys ==="
rg "labels\.inputs\.Currency|labels\.inputs\.Amortization Type" src/assets/translations/ -l
echo -e "\n=== Search for labels.inputs. pattern ==="
rg "labels\.inputs\." src/assets/translations/ --no-filename | head -30Repository: openMF/web-app Length of output: 1108 🏁 Script executed: #!/bin/bash
# Check if the keys exist in any form or if they're simply missing
echo "=== Searching for 'Currency' key in translation files ==="
rg "Currency" src/assets/translations/ --max-count 5
echo -e "\n=== Searching for 'Amortization' in translation files ==="
rg "Amortization" src/assets/translations/ --max-count 5
echo -e "\n=== Checking the component file to verify the keys being used ==="
cat -n src/app/loans/loans-view/account-details/account-details.component.html | sed -n '19,28p'
echo -e "\n=== Checking if there's a translation extraction configuration ==="
find . -name "*.json" -o -name "*.config.*" | xargs grep -l "translations:extract" 2>/dev/null | head -5Repository: openMF/web-app Length of output: 12611 🏁 Script executed: #!/bin/bash
# Check the translation extraction script in package.json
grep -A 5 "translations:extract" package.json
echo -e "\n=== Verify that labels.inputs keys are missing ==="
rg "labels\.inputs\." src/assets/translations/ | wc -l
echo -e "\n=== Show existing structure of labels.inputs in code ==="
rg "labels\.inputs\." src/ --max-count 10Repository: openMF/web-app Length of output: 50371 🏁 Script executed: #!/bin/bash
# Double-check the exact keys in the component and search for them in translation files with proper escaping
echo "=== Verify exact keys in component ==="
grep -n "labels\.inputs\." src/app/loans/loans-view/account-details/account-details.component.html
echo -e "\n=== Final verification: Search for exact keys in ALL translation files ==="
rg "labels\.inputs\.Currency|labels\.inputs\.Amortization" src/assets/translations/ | wc -lRepository: openMF/web-app Length of output: 5733 The translation keys These keys are used in the component (lines 20 and 26) but do not exist in any of the 13 supported locale files in 🤖 Prompt for AI Agents
Comment on lines
+25
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add optional chaining on the
🛡️ Proposed fix- <span class="flex-50"> {{ loanDetails.product.amortizationType.value | translateKey: 'catalogs' }} </span>
+ <span class="flex-50"> {{ loanDetails.product?.amortizationType?.value | translateKey: 'catalogs' }} </span>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| <div class="flex-fill layout-row"> | ||||||||||||||||||
| <span class="flex-50">{{ 'labels.inputs.Period Payment Frequency' | translate }}</span> | ||||||||||||||||||
| <span class="flex-50"> | ||||||||||||||||||
|
|
@@ -33,9 +42,21 @@ <h3>{{ 'labels.heading.Loan Details' | translate }}</h3> | |||||||||||||||||
| </div> | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| @if (loanDetails.discount) { | ||||||||||||||||||
| <div class="flex-fill layout-row"> | ||||||||||||||||||
| <span class="flex-50"> {{ 'labels.inputs.Discount' | translate }} </span> | ||||||||||||||||||
| @if (loanDetails.discountProposed > 0) { | ||||||||||||||||||
| <div class="flex-contents"> | ||||||||||||||||||
| <span class="flex-50">{{ 'labels.inputs.Proposed Discount' | translate }}:</span> | ||||||||||||||||||
| <span class="flex-50">{{ loanDetails.discountProposed | formatNumber }}</span> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| } | ||||||||||||||||||
| @if (loanDetails.discountApproved > 0) { | ||||||||||||||||||
| <div class="flex-contents"> | ||||||||||||||||||
| <span class="flex-50">{{ 'labels.inputs.Approved Discount' | translate }}:</span> | ||||||||||||||||||
| <span class="flex-50">{{ loanDetails.discountApproved | formatNumber }}</span> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| } | ||||||||||||||||||
| @if (loanDetails.discount > 0) { | ||||||||||||||||||
| <div class="flex-contents"> | ||||||||||||||||||
| <span class="flex-50">{{ 'labels.inputs.Discount' | translate }}:</span> | ||||||||||||||||||
| <span class="flex-50">{{ loanDetails.discount | formatNumber }}</span> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| <!-- | ||
| 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/. | ||
| --> | ||
|
|
||
| <div class="container"> | ||
| <div class="layout-row m-t-20 align-end align-items-center"> | ||
| <button mat-raised-button color="primary" (click)="exportToPDF()"> | ||
| <fa-icon icon="download" class="m-r-10"></fa-icon>{{ 'labels.buttons.Export to PDF' | translate }} | ||
| </button> | ||
| </div> | ||
|
|
||
| <table mat-table [dataSource]="amortizationSchedule?.payments" id="amortizationSchedule"> | ||
| <ng-container matColumnDef="number"> | ||
| <th mat-header-cell class="center mat-header-cell" *matHeaderCellDef>#</th> | ||
| <td mat-cell class="right" *matCellDef="let item">{{ item.paymentNo }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="paymentDate"> | ||
| <th mat-header-cell class="center mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Payment Date' | translate }} | ||
| </th> | ||
| <td mat-cell class="m-r-5" *matCellDef="let item">{{ item.paymentDate | dateFormat }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="expectedPaymentAmount"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Expected Payment Amount' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.expectedPaymentAmount | formatNumber }}</td> | ||
| <td mat-footer-cell class="r-amount" *matFooterCellDef> | ||
| <b>{{ amortizationSchedule?.totalPaymentValue | currency: currencyCode : 'symbol-narrow' : '1.2-2' }}</b> | ||
| </td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="discountFactor"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Discount Factor' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.discountFactor | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="npvValue"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.NPV Value' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.npvValue | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="deferredBalance"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Deferred Balance' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.deferredBalance | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="forecastPaymentAmount"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Forecast Payment Amount' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.forecastPaymentAmount | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
Comment on lines
+65
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional columns (
If these columns are deferred intentionally for this PR, consider either removing the dead template code or restoring the Also applies to: 81-95 🤖 Prompt for AI Agents |
||
|
|
||
| <ng-container matColumnDef="expectedAmortizationAmount"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Expected Amortization Amount' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.expectedAmortizationAmount | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="netAmortizationAmount"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Net Amortization Amount' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.netAmortizationAmount | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="incomeModification"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Income Modification' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.incomeModification | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <ng-container matColumnDef="balance"> | ||
| <th mat-header-cell class="r-amount mat-header-cell" *matHeaderCellDef> | ||
| {{ 'labels.inputs.Balance' | translate }} | ||
| </th> | ||
| <td mat-cell class="r-amount" *matCellDef="let item">{{ item.balance | formatNumber }}</td> | ||
| <td mat-footer-cell *matFooterCellDef></td> | ||
| </ng-container> | ||
|
|
||
| <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | ||
| <tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr> | ||
| <tr mat-footer-row *matFooterRowDef="displayedColumns"></tr> | ||
| </table> | ||
| </div> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loanIdcan benullbut still passes the!isNaN(+loanId)guard.When neither
route.paramMapnorroute.parent?.paramMapcontainsloanId, the expression evaluates tonull. Since+null === 0and!isNaN(0)istrue, the guard is entered andgetWorkingCapitalLoanAmortizationSchedule(null)would be called.Replace the numeric coercion check with an explicit string guard:
🐛 Proposed fix
Also, per project conventions, prefer
Observable<ProjectedAmortizationSchedule>overObservable<any>for the return type.Based on learnings: "avoid using
Observable<any>as a project-wide pattern for API responses…introduce specific interfaces/types for the response shapes."🤖 Prompt for AI Agents