From 41cd56d08f4bd039f239747c384766cbb3d92e01 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Wed, 27 May 2026 22:50:29 -0500 Subject: [PATCH] WEB-657: Loan view and Repayment for Working Capital --- .../loan-action-button.resolver.ts | 4 +- .../common-resolvers/loan-base.resolver.ts | 2 +- .../loans-account-transaction.resolver.ts | 18 +- .../general-tab/general-tab.component.html | 95 +-- .../general-tab/general-tab.component.ts | 118 +--- ...n-summary-balance-component.component.html | 67 ++ ...n-summary-balance-component.component.scss | 7 + ...oan-summary-balance-component.component.ts | 176 +++++ .../make-repayment.component.html | 328 +++++---- .../make-repayment.component.ts | 218 +++--- .../loans-view/loans-view.component.html | 515 +++++++------- .../loans-view/loans-view.component.scss | 19 + .../loans/loans-view/loans-view.component.ts | 8 +- .../transactions-tab.component.html | 635 +++++++++--------- .../transactions-tab.component.scss | 277 +++++++- .../transactions-tab.component.ts | 106 +-- .../export-transactions.component.html | 4 +- .../export-transactions.component.scss | 6 +- .../view-transaction.component.html | 102 ++- .../view-transaction.component.scss | 2 +- .../view-transaction.component.ts | 20 +- ...n-amortization-schedule-tab.component.html | 2 +- .../loan-balances-tab.component.ts | 21 +- src/app/loans/loans.service.ts | 14 +- .../account-header.component.html | 35 + .../account-header.component.scss | 34 + .../account-header.component.ts | 33 + .../notifications-tray.component.scss | 13 + .../theme-toggle/theme-toggle.component.scss | 6 +- src/assets/translations/cs-CS.json | 1 + src/assets/translations/de-DE.json | 1 + src/assets/translations/en-US.json | 1 + src/assets/translations/es-CL.json | 1 + src/assets/translations/es-MX.json | 3 +- src/assets/translations/fr-FR.json | 1 + src/assets/translations/it-IT.json | 1 + src/assets/translations/ko-KO.json | 1 + src/assets/translations/lt-LT.json | 1 + src/assets/translations/lv-LV.json | 1 + src/assets/translations/ne-NE.json | 1 + src/assets/translations/pt-PT.json | 1 + src/assets/translations/sw-SW.json | 1 + 42 files changed, 1767 insertions(+), 1133 deletions(-) create mode 100644 src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.html create mode 100644 src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.scss create mode 100644 src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.ts create mode 100644 src/app/shared/account-header/account-header.component.html create mode 100644 src/app/shared/account-header/account-header.component.scss create mode 100644 src/app/shared/account-header/account-header.component.ts diff --git a/src/app/loans/common-resolvers/loan-action-button.resolver.ts b/src/app/loans/common-resolvers/loan-action-button.resolver.ts index 6d33d085da..521daf5818 100644 --- a/src/app/loans/common-resolvers/loan-action-button.resolver.ts +++ b/src/app/loans/common-resolvers/loan-action-button.resolver.ts @@ -38,7 +38,9 @@ export class LoanActionButtonResolver { if (loanActionButton === 'Assign Loan Officer' || loanActionButton === 'Change Loan Officer') { return this.loansService.getLoanTemplate(loanId); } else if (loanActionButton === 'Make Repayment') { - return this.loansService.getLoanActionTemplate(loanId, 'repayment'); + return this.loanProductService.isLoanProduct + ? this.loansService.getLoanActionTemplate(loanId, 'repayment') + : this.loansService.getWorkingCapitalLoanActionTemplate(loanId, 'repayment'); } else if (loanActionButton === 'Goodwill Credit') { return this.loansService.getLoanActionTemplate(loanId, 'goodwillCredit'); } else if (loanActionButton === 'Interest Payment Waiver') { diff --git a/src/app/loans/common-resolvers/loan-base.resolver.ts b/src/app/loans/common-resolvers/loan-base.resolver.ts index 1f22ba7f6f..c088944e48 100644 --- a/src/app/loans/common-resolvers/loan-base.resolver.ts +++ b/src/app/loans/common-resolvers/loan-base.resolver.ts @@ -42,7 +42,7 @@ export class LoanBaseResolver { return this.isLoanProduct ? 'loanproducts' : 'working-capital-loan-products'; } - get loanAccountPath(): string { + get loanAccountPath(): 'loans' | 'working-capital-loans' { return this.isLoanProduct ? 'loans' : 'working-capital-loans'; } } diff --git a/src/app/loans/common-resolvers/loans-account-transaction.resolver.ts b/src/app/loans/common-resolvers/loans-account-transaction.resolver.ts index 4930d1a3ba..d90d16c067 100644 --- a/src/app/loans/common-resolvers/loans-account-transaction.resolver.ts +++ b/src/app/loans/common-resolvers/loans-account-transaction.resolver.ts @@ -15,22 +15,36 @@ import { Observable } from 'rxjs'; /** Custom Services */ import { LoansService } from '../loans.service'; +import { LoanBaseResolver } from './loan-base.resolver'; /** * Loans Account Transaction data resolver. */ @Injectable() -export class LoansAccountTransactionResolver { +export class LoansAccountTransactionResolver extends LoanBaseResolver { private loansService = inject(LoansService); + constructor() { + super(); + } + /** * Returns the Loans Account Transaction data. * @param {ActivatedRouteSnapshot} route Route Snapshot * @returns {Observable} */ resolve(route: ActivatedRouteSnapshot): Observable { + this.initialize(route); const loanId = route.paramMap.get('loanId'); const transactionId = route.paramMap.get('id'); - return this.loansService.getLoansAccountTransaction(loanId, transactionId); + if ( + loanId === null || + transactionId === null || + Number.isNaN(Number(loanId)) || + Number.isNaN(Number(transactionId)) + ) { + throw new Error('Invalid loan or transaction route params'); + } + return this.loansService.getLoansAccountTransaction(this.loanAccountPath, loanId, transactionId); } } diff --git a/src/app/loans/loans-view/general-tab/general-tab.component.html b/src/app/loans/loans-view/general-tab/general-tab.component.html index 4d7907c46e..c96b841be2 100644 --- a/src/app/loans/loans-view/general-tab/general-tab.component.html +++ b/src/app/loans/loans-view/general-tab/general-tab.component.html @@ -13,18 +13,28 @@

{{ 'labels.heading.Performance History' | translate }}

- - - - + @if (loanProductService.isLoanProduct) { + + + + + } + @if (loanProductService.isWorkingCapital) { + + + }
- {{ 'labels.inputs.Number of Repayments' | translate }} : - - {{ loanDetails?.numberOfRepayments | formatNumber }} - - {{ 'labels.inputs.Maturity Date' | translate }} : - - {{ loanDetails?.timeline.expectedMaturityDate | dateFormat }} - + {{ 'labels.inputs.Number of Repayments' | translate }} : + + {{ loanDetails?.numberOfRepayments | formatNumber }} + + {{ 'labels.inputs.Maturity Date' | translate }} : + + {{ loanDetails?.timeline.expectedMaturityDate | dateFormat }} + + {{ 'labels.inputs.Number of Repayments' | translate }} : + + {{ loanDetails?.totalNoPayments | formatNumber }} +
@@ -32,61 +42,12 @@

{{ 'labels.heading.Performance History' | translate }}

} @if (loanDetails.summary) { -
-

{{ 'labels.heading.Loan Summary' | translate }}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.' + ele.property | translate }}{{ 'labels.inputs.Original' | translate }} - {{ ele.original | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - {{ 'labels.inputs.Paid' | translate }} - {{ ele.paid | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - - {{ 'labels.inputs.Credit Adjustments' | translate }} - - {{ ele.adjustment | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - {{ 'labels.inputs.Waived' | translate }} - {{ ele.waived | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - {{ 'labels.inputs.Written Off' | translate }} - {{ ele.writtenOff | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - {{ 'labels.inputs.Outstanding' | translate }} - {{ ele.outstanding | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - {{ 'labels.inputs.Over Due' | translate }} - {{ ele.overdue | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} -
-
+ +

{{ 'labels.heading.Loan Details' | translate }}

diff --git a/src/app/loans/loans-view/general-tab/general-tab.component.ts b/src/app/loans/loans-view/general-tab/general-tab.component.ts index b9e0e9a442..7e0b1db124 100644 --- a/src/app/loans/loans-view/general-tab/general-tab.component.ts +++ b/src/app/loans/loans-view/general-tab/general-tab.component.ts @@ -12,12 +12,8 @@ import { MatTableDataSource, MatTable, MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, MatCellDef, MatCell, - MatHeaderRowDef, - MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table'; @@ -28,6 +24,7 @@ import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { LoanProductService } from 'app/products/loan-products/services/loan-product.service'; import { LoanProductBaseComponent } from 'app/products/loan-products/common/loan-product-base.component'; +import { LoanSummaryBalanceComponentComponent } from './loan-summary-balance-component/loan-summary-balance-component.component'; @Component({ selector: 'mifosx-general-tab', @@ -37,18 +34,15 @@ import { LoanProductBaseComponent } from 'app/products/loan-products/common/loan ...STANDALONE_SHARED_IMPORTS, MatTable, MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, MatCellDef, MatCell, - MatHeaderRowDef, - MatHeaderRow, MatRowDef, MatRow, ExternalIdentifierComponent, CurrencyPipe, DateFormatPipe, - FormatNumberPipe + FormatNumberPipe, + LoanSummaryBalanceComponentComponent ], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -56,39 +50,18 @@ export class GeneralTabComponent extends LoanProductBaseComponent implements OnI private route = inject(ActivatedRoute); /** Currency Code */ - currencyCode: string; + currencyCode: string | null = null; loanDetails: any; status: any; - loanSummaryColumns: string[] = [ - 'Empty', - 'Original', - 'Paid', - 'Waived', - 'Written Off', - 'Outstanding', - 'Over Due' - ]; + loanDetailsColumns: string[] = [ 'Key', 'Value' ]; - loanSummaryTableData: { - property: string; - original: number; - adjustment: number; - paid: number; - waived: number; - writtenOff: number; - outstanding: number; - overdue: number; - }[]; - loanDetailsTableData: { - key: string; - value?: string; - }[]; + loanDetailsTableData: { key: string; value?: string }[] = []; + hasChargeBack: boolean = false; - /** Data source for loans summary table. */ - dataSource: MatTableDataSource; + /** Data source for loans details table. */ detailsDataSource: MatTableDataSource; constructor() { @@ -101,21 +74,9 @@ export class GeneralTabComponent extends LoanProductBaseComponent implements OnI this.loanDetails = data.loanDetailsData; this.currencyCode = this.loanDetails.currency.code; if (this.loanDetails.transactions) { - this.loanDetails.transactions.some((transaction: any) => { - if (transaction.type.code === 'loanTransactionType.chargeback') { - this.loanSummaryColumns = [ - 'Empty', - 'Original', - 'Adjustments', - 'Paid', - 'Waived', - 'Written Off', - 'Outstanding', - 'Over Due' - ]; - return; - } - }); + this.hasChargeBack = this.loanDetails.transactions.some( + (transaction: any) => transaction.type.code === 'loanTransactionType.chargeback' + ); } }); } @@ -123,69 +84,12 @@ export class GeneralTabComponent extends LoanProductBaseComponent implements OnI ngOnInit() { this.status = this.loanDetails.value; if (this.loanDetails.summary) { - this.setloanSummaryTableData(); this.setloanDetailsTableData(); } else { this.setloanNonDetailsTableData(); } } - setloanSummaryTableData() { - this.loanSummaryTableData = [ - { - property: 'Principal', - original: this.loanDetails.summary.totalPrincipal, - adjustment: this.loanDetails.summary.principalAdjustments || 0, - paid: this.loanDetails.summary.principalPaid, - waived: this.loanDetails.summary.principalWaived || 0, - writtenOff: this.loanDetails.summary.principalWrittenOff, - outstanding: this.loanDetails.summary.principalOutstanding, - overdue: this.loanDetails.summary.principalOverdue - }, - { - property: 'Interest', - original: this.loanDetails.summary.interestCharged, - adjustment: 0, - paid: this.loanDetails.summary.interestPaid, - waived: this.loanDetails.summary.interestWaived, - writtenOff: this.loanDetails.summary.interestWrittenOff, - outstanding: this.loanDetails.summary.interestOutstanding, - overdue: this.loanDetails.summary.interestOverdue - }, - { - property: 'Fees', - original: this.loanDetails.summary.feeChargesCharged, - adjustment: 0, - paid: this.loanDetails.summary.feeChargesPaid, - waived: this.loanDetails.summary.feeChargesWaived, - writtenOff: this.loanDetails.summary.feeChargesWrittenOff, - outstanding: this.loanDetails.summary.feeChargesOutstanding, - overdue: this.loanDetails.summary.feeChargesOverdue - }, - { - property: 'Penalties', - original: this.loanDetails.summary.penaltyChargesCharged, - adjustment: 0, - paid: this.loanDetails.summary.penaltyChargesPaid, - waived: this.loanDetails.summary.penaltyChargesWaived, - writtenOff: this.loanDetails.summary.penaltyChargesWrittenOff, - outstanding: this.loanDetails.summary.penaltyChargesOutstanding, - overdue: this.loanDetails.summary.penaltyChargesOverdue - }, - { - property: 'Total', - original: this.loanDetails.summary.totalExpectedRepayment, - adjustment: this.loanDetails.summary.principalAdjustments || 0, - paid: this.loanDetails.summary.totalRepayment, - waived: this.loanDetails.summary.totalWaived, - writtenOff: this.loanDetails.summary.totalWrittenOff, - outstanding: this.loanDetails.summary.totalOutstanding, - overdue: this.loanDetails.summary.totalOverdue - } - ]; - this.dataSource = new MatTableDataSource(this.loanSummaryTableData); - } - setloanDetailsTableData() { this.loanDetailsTableData = [ { diff --git a/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.html b/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.html new file mode 100644 index 0000000000..7c180cca56 --- /dev/null +++ b/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.html @@ -0,0 +1,67 @@ + + +
+ @if (summary) { +
+

{{ 'labels.heading.Loan Summary' | translate }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.' + ele.property | translate }}{{ 'labels.inputs.Original' | translate }} + {{ ele.original | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Paid' | translate }} + {{ ele.paid | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} + + {{ 'labels.inputs.Credit Adjustments' | translate }} + + {{ ele.adjustment | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Waived' | translate }} + {{ ele.waived | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Written Off' | translate }} + {{ ele.writtenOff | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Outstanding' | translate }} + {{ ele.outstanding | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Over Due' | translate }} + {{ ele.overdue | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} +
+
+ } +
diff --git a/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.scss b/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.scss new file mode 100644 index 0000000000..af9f4c0753 --- /dev/null +++ b/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.scss @@ -0,0 +1,7 @@ +/** + * 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/. + */ diff --git a/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.ts b/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.ts new file mode 100644 index 0000000000..e61cc64fda --- /dev/null +++ b/src/app/loans/loans-view/general-tab/loan-summary-balance-component/loan-summary-balance-component.component.ts @@ -0,0 +1,176 @@ +/** + * 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 { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatTable, + MatTableDataSource +} from '@angular/material/table'; +import { LoanProductBaseComponent } from 'app/products/loan-products/common/loan-product-base.component'; +import { Currency } from 'app/shared/models/general.model'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { CurrencyPipe } from '@angular/common'; + +@Component({ + selector: 'mifosx-loan-summary-balance-component', + templateUrl: './loan-summary-balance-component.component.html', + styleUrl: './loan-summary-balance-component.component.scss', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + CurrencyPipe + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LoanSummaryBalanceComponentComponent extends LoanProductBaseComponent implements OnInit { + @Input() summary: any | null = null; + @Input() currency: Currency | null = null; + @Input() hasChargeBack: boolean = false; + + /** Data source for loans summary table. */ + dataSource: MatTableDataSource; + loanSummaryColumns: string[] = []; + currencyCode: string | null = null; + + constructor() { + super(); + } + + ngOnInit(): void { + this.currencyCode = this.currency?.code ?? null; + if (this.summary) { + if (this.loanProductService.isWorkingCapital) { + this.setWorkingCapitalSummaryTableData(); + } else { + this.setLoanSummaryTableData(); + } + } + } + + setLoanSummaryTableData(): void { + this.loanSummaryColumns = [ + 'Empty', + 'Original', + 'Paid', + 'Waived', + 'Written Off', + 'Outstanding', + 'Over Due' + ]; + if (this.hasChargeBack) { + this.loanSummaryColumns.splice(2, 0, 'Adjustments'); + } + this.dataSource = new MatTableDataSource([ + { + property: 'Principal', + original: this.summary.totalPrincipal, + adjustment: this.summary.principalAdjustments || 0, + paid: this.summary.principalPaid, + waived: this.summary.principalWaived || 0, + writtenOff: this.summary.principalWrittenOff, + outstanding: this.summary.principalOutstanding, + overdue: this.summary.principalOverdue + }, + { + property: 'Interest', + original: this.summary.interestCharged, + adjustment: 0, + paid: this.summary.interestPaid, + waived: this.summary.interestWaived, + writtenOff: this.summary.interestWrittenOff, + outstanding: this.summary.interestOutstanding, + overdue: this.summary.interestOverdue + }, + { + property: 'Fees', + original: this.summary.feeChargesCharged, + adjustment: 0, + paid: this.summary.feeChargesPaid, + waived: this.summary.feeChargesWaived, + writtenOff: this.summary.feeChargesWrittenOff, + outstanding: this.summary.feeChargesOutstanding, + overdue: this.summary.feeChargesOverdue + }, + { + property: 'Penalties', + original: this.summary.penaltyChargesCharged, + adjustment: 0, + paid: this.summary.penaltyChargesPaid, + waived: this.summary.penaltyChargesWaived, + writtenOff: this.summary.penaltyChargesWrittenOff, + outstanding: this.summary.penaltyChargesOutstanding, + overdue: this.summary.penaltyChargesOverdue + }, + { + property: 'Total', + original: this.summary.totalExpectedRepayment, + adjustment: this.summary.principalAdjustments || 0, + paid: this.summary.totalRepayment, + waived: this.summary.totalWaived, + writtenOff: this.summary.totalWrittenOff, + outstanding: this.summary.totalOutstanding, + overdue: this.summary.totalOverdue + } + ]); + } + + setWorkingCapitalSummaryTableData(): void { + this.loanSummaryColumns = [ + 'Empty', + 'Original', + 'Paid', + 'Outstanding' + ]; + + this.dataSource = new MatTableDataSource([ + { + property: 'Principal', + original: this.summary.principalDisbursed, + paid: this.summary.principalPaid, + outstanding: this.summary.principalOutstanding + }, + { + property: 'Discount', + original: this.summary.discountCharged, + paid: this.summary.discountPaid, + outstanding: this.summary.discountOutstanding + }, + { + property: 'Fees', + original: this.summary.feeChargesCharged || 0, + paid: this.summary.feeChargesPaid || 0, + outstanding: this.summary.feeChargesOutstanding || 0 + }, + { + property: 'Total', + original: this.summary.totalExpectedRepayment || 0, + paid: this.summary.totalRepayment || 0, + outstanding: this.summary.totalOutstanding || 0 + } + ]); + } +} diff --git a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html index e9d127623b..e87e98354b 100644 --- a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html +++ b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html @@ -7,189 +7,187 @@ -->
- -
- -
- - {{ 'labels.inputs.Transaction Date' | translate }} - - - - @if (repaymentLoanForm.controls.transactionDate.hasError('required')) { - - {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - + @if (repaymentLoanForm) { + + + +
+ + {{ 'labels.inputs.Transaction Date' | translate }} + + + + @if (repaymentLoanForm.controls.transactionDate.hasError('required')) { + + {{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + - @if (showDetails()) { -
-
- {{ 'labels.inputs.Principal' | translate }} -
-
- {{ dataObject.principalPortion | formatNumber }} -
-
- {{ 'labels.inputs.Interest' | translate }} -
-
- {{ dataObject.interestPortion | formatNumber }} -
-
- {{ 'labels.inputs.Fees' | translate }} -
-
- {{ dataObject.feeChargesPortion | formatNumber }} -
-
- {{ 'labels.inputs.Penalties' | translate }} -
-
- {{ dataObject.penaltyChargesPortion | formatNumber }} + @if (showDetails()) { +
+
+ {{ 'labels.inputs.Principal' | translate }} +
+
+ {{ dataObject.principalPortion | formatNumber }} +
+
+ {{ 'labels.inputs.Interest' | translate }} +
+
+ {{ dataObject.interestPortion | formatNumber }} +
+
+ {{ 'labels.inputs.Fees' | translate }} +
+
+ {{ dataObject.feeChargesPortion | formatNumber }} +
+
+ {{ 'labels.inputs.Penalties' | translate }} +
+
+ {{ dataObject.penaltyChargesPortion | formatNumber }} +
-
- } + } + + + - - + + {{ 'labels.inputs.External Id' | translate }} + + - - {{ 'labels.inputs.External Id' | translate }} - - + @if (isCapitalizedIncome() || isBuyDownFee()) { + + {{ 'labels.inputs.Classification' | translate }} + + @for (classificationOption of classificationOptions; track classificationOption) { + + {{ classificationOption.name }} + + } + + + } - @if (isCapitalizedIncome() || isBuyDownFee()) { - {{ 'labels.inputs.Classification' | translate }} - - @for (classificationOption of classificationOptions; track classificationOption) { - - {{ classificationOption.name }} + {{ 'labels.inputs.Payment Type' | translate }} + + @for (paymentType of paymentTypes; track paymentType) { + + {{ paymentType.name }} } - } - - {{ 'labels.inputs.Payment Type' | translate }} - - @for (paymentType of paymentTypes; track paymentType) { - - {{ paymentType.name }} - - } - - - -
- - {{ 'labels.inputs.Show Payment Details' | translate }} - -
+
+ + {{ 'labels.inputs.Show Payment Details' | translate }} + +
-
- - {{ 'labels.inputs.Waive Penalties' | translate }} - -
+ @if (penalties.length > 0 && loanProductService.isLoanProduct) { +
+ + {{ 'labels.inputs.Waive Penalties' | translate }} + +
- @if (waivePenalties) { - @if (penalties.length > 0) { -
-
- - {{ 'labels.inputs.All' | translate }} - + @if (waivePenalties) { +
+
+ + {{ 'labels.inputs.All' | translate }} + +
+
+ @for (penalty of penalties; track penalty.id) { +
+ + {{ 'labels.inputs.Penalty' | translate }} + {{ getPenaltyDisplayKey(penalty) | translate }} + {{ penalty.dueDate | dateFormat }} + {{ + penalty.amountOutstanding || penalty.amount || 0 | formatNumber + }} +
+ } +
-
- @for (penalty of penalties; track penalty.id) { -
- - {{ 'labels.inputs.Penalty' | translate }} - {{ getPenaltyDisplayKey(penalty) | translate }} - {{ penalty.dueDate | dateFormat }} - {{ - penalty.amountOutstanding || penalty.amount || 0 | formatNumber - }} -
- } -
-
- } @else { -
- {{ 'labels.text.No penalties found' | translate }} -
+ } + } + + @if (showPaymentDetails) { + + {{ 'labels.inputs.Account' | translate }} # + + + + {{ 'labels.inputs.Cheque' | translate }} # + + + + {{ 'labels.inputs.Routing Code' | translate }} + + + + {{ 'labels.inputs.Reciept' | translate }} # + + + + {{ 'labels.inputs.Bank' | translate }} # + + } - } - @if (showPaymentDetails) { - - {{ 'labels.inputs.Account' | translate }} # - - - - {{ 'labels.inputs.Cheque' | translate }} # - - - - {{ 'labels.inputs.Routing Code' | translate }} - - - - {{ 'labels.inputs.Reciept' | translate }} # - - - {{ 'labels.inputs.Bank' | translate }} # - + {{ 'labels.inputs.Note' | translate }} + - } - - {{ 'labels.inputs.Note' | translate }} - - - - @if (showInterestRefundCheckbox()) { - - {{ 'labels.inputs.Skip Interest Refund Transaction Posting' | translate }} - - } -
+ @if (showInterestRefundCheckbox()) { + + {{ 'labels.inputs.Skip Interest Refund Transaction Posting' | translate }} + + } +
- - - - - - - + + + + + + + + }
diff --git a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts index f9a556ccd9..f034c2673c 100644 --- a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts +++ b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts @@ -7,13 +7,15 @@ */ /** Angular Imports */ -import { ChangeDetectionStrategy, Component, OnInit, inject, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, OnInit, inject, ChangeDetectorRef, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; /** Custom Services */ import { Dates } from 'app/core/utils/dates'; -import { Currency } from 'app/shared/models/general.model'; +import { Currency, PaymentType } from 'app/shared/models/general.model'; import { PenaltyManagementService } from 'app/loans/services/penalty-management.service'; +import { AlertService } from 'app/core/alert/alert.service'; import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatCheckbox } from '@angular/material/checkbox'; @@ -40,13 +42,15 @@ import { LoanAccountActionsBaseComponent } from '../loan-account-actions-base.co changeDetection: ChangeDetectionStrategy.OnPush }) export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); + private formBuilder = inject(FormBuilder); private dateUtils = inject(Dates); private penaltyManagementService = inject(PenaltyManagementService); private cdr = inject(ChangeDetectorRef); + private destroyRef = inject(DestroyRef); + private alertService = inject(AlertService); /** Payment Type Options */ - paymentTypes: any; + paymentTypes: PaymentType[] = []; /** Show payment details */ showPaymentDetails = false; /** Waive Penalties toggle */ @@ -64,12 +68,11 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl /** Maximum Date allowed. */ maxDate = new Date(); /** Repayment Loan Form */ - repaymentLoanForm: UntypedFormGroup; + repaymentLoanForm: FormGroup | null = null; currency: Currency | null = null; - - command: string | null = null; - + command = ''; classificationOptions: any[] = []; + private originalAmount = 0; /** * @param {FormBuilder} formBuilder Form Builder. @@ -87,14 +90,29 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl * and initialize with the required values */ ngOnInit() { - this.command = this.dataObject.type.code.split('.')[1]; + this.command = this.resolveCommandFromActionName(this.dataObject.actionName); this.maxDate = this.settingsService.businessDate; this.createRepaymentLoanForm(); this.setRepaymentLoanDetails(); - if (this.dataObject.currency) { + if (this.dataObject?.currency) { this.currency = this.dataObject.currency; } - this.loadPenalties(); + if (this.loanProductService.isLoanProduct && this.isRepayment()) { + this.loadPenalties(); + } + } + + private resolveCommandFromActionName(actionName: string | undefined): string { + const map: Record = { + 'Make Repayment': 'repayment', + 'Capitalized Income': 'capitalizedIncome', + 'Goodwill Credit': 'goodwillCredit', + 'Buy Down Fee': 'buyDownFee', + 'Interest Payment Waiver': 'interestPaymentWaiver', + 'Payout Refund': 'payoutRefund', + 'Merchant Issued Refund': 'merchantIssuedRefund' + }; + return actionName ? (map[actionName] ?? '') : ''; } /** @@ -106,30 +124,28 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl this.settingsService.businessDate, Validators.required ], - externalId: '', - paymentTypeId: '', + externalId: null, + paymentTypeId: null, note: '', skipInterestRefund: [false] }); - if (this.isCapitalizedIncome()) { - this.repaymentLoanForm.addControl('transactionAmount', new UntypedFormControl('', [])); - this.updateTransactionAmountValidators(false); - } else { - this.repaymentLoanForm.addControl('transactionAmount', new UntypedFormControl('', [])); - this.updateTransactionAmountValidators(false); - } + this.repaymentLoanForm.addControl('transactionAmount', new FormControl(0, [])); + this.updateTransactionAmountValidators(false); if (this.isCapitalizedIncome() || this.isBuyDownFee()) { - this.repaymentLoanForm.addControl('classificationId', new UntypedFormControl('')); + this.repaymentLoanForm.addControl('classificationId', new FormControl(null)); } } setRepaymentLoanDetails() { this.paymentTypes = this.dataObject.paymentTypeOptions; this.classificationOptions = this.dataObject.classificationOptions; - this.repaymentLoanForm.patchValue({ - transactionAmount: this.dataObject.amount - }); + this.originalAmount = Number(this.dataObject.amount) || 0; + if (this.repaymentLoanForm) { + this.repaymentLoanForm.patchValue({ + transactionAmount: this.originalAmount + }); + } } /** @@ -137,18 +153,20 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl */ addPaymentDetails() { this.showPaymentDetails = !this.showPaymentDetails; - if (this.showPaymentDetails) { - this.repaymentLoanForm.addControl('accountNumber', new UntypedFormControl('')); - this.repaymentLoanForm.addControl('checkNumber', new UntypedFormControl('')); - this.repaymentLoanForm.addControl('routingCode', new UntypedFormControl('')); - this.repaymentLoanForm.addControl('receiptNumber', new UntypedFormControl('')); - this.repaymentLoanForm.addControl('bankNumber', new UntypedFormControl('')); - } else { - this.repaymentLoanForm.removeControl('accountNumber'); - this.repaymentLoanForm.removeControl('checkNumber'); - this.repaymentLoanForm.removeControl('routingCode'); - this.repaymentLoanForm.removeControl('receiptNumber'); - this.repaymentLoanForm.removeControl('bankNumber'); + if (this.repaymentLoanForm) { + if (this.showPaymentDetails) { + this.repaymentLoanForm.addControl('accountNumber', new FormControl('')); + this.repaymentLoanForm.addControl('checkNumber', new FormControl('')); + this.repaymentLoanForm.addControl('routingCode', new FormControl('')); + this.repaymentLoanForm.addControl('receiptNumber', new FormControl('')); + this.repaymentLoanForm.addControl('bankNumber', new FormControl('')); + } else { + this.repaymentLoanForm.removeControl('accountNumber'); + this.repaymentLoanForm.removeControl('checkNumber'); + this.repaymentLoanForm.removeControl('routingCode'); + this.repaymentLoanForm.removeControl('receiptNumber'); + this.repaymentLoanForm.removeControl('bankNumber'); + } } } @@ -169,6 +187,12 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl ].includes(this.command); } + isRepayment(): boolean { + return [ + 'repayment' + ].includes(this.command); + } + showInterestRefundCheckbox(): boolean { const code = this.dataObject?.type?.code?.toLowerCase() || ''; return code.includes('merchantissuedrefund') || code.includes('payoutrefund'); @@ -180,14 +204,17 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl * Each penalty charge has a dueDate that corresponds to an installment due date. */ loadPenalties() { - this.penaltyManagementService.loadPenalties(this.loanId).subscribe({ - next: (penalties: any[]) => { - this.penalties = penalties; - }, - error: (error: any) => { - this.penalties = []; - } - }); + this.penaltyManagementService + .loadPenalties(this.loanId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (penalties: any[]) => { + this.penalties = penalties; + }, + error: () => { + this.penalties = []; + } + }); } /** @@ -252,16 +279,10 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl * Recalculate transaction amount when penalties are waived */ recalculateTransactionAmount() { - const currentAmountValue = this.repaymentLoanForm.value.transactionAmount; - const currentAmount = - currentAmountValue !== undefined && currentAmountValue !== null - ? Number(currentAmountValue) - : Number(this.dataObject.amount ?? 0); - const baseAmount = isNaN(currentAmount) ? Number(this.dataObject.amount ?? 0) : currentAmount; + const baseAmount = this.originalAmount; if (!this.waivePenalties || this.selectedPenalties.length === 0) { - // Reset to original amount if no penalties selected - this.repaymentLoanForm.patchValue( + this.repaymentLoanForm?.patchValue( { transactionAmount: baseAmount }, @@ -285,7 +306,7 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl // Allow zero when fully waived this.updateTransactionAmountValidators(this.waivePenalties && newAmount === 0); - this.repaymentLoanForm.patchValue( + this.repaymentLoanForm?.patchValue( { transactionAmount: newAmount }, @@ -299,27 +320,27 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl private updateTransactionAmountValidators(allowZero: boolean) { const validators = [ Validators.required, - Validators.min(allowZero ? 0 : 0.001) + ...(allowZero ? [] : [Validators.min(0.001)]) ]; if (this.isCapitalizedIncome()) { validators.push(Validators.max(this.dataObject.amount)); } - this.repaymentLoanForm.controls.transactionAmount.setValidators(validators); - this.repaymentLoanForm.controls.transactionAmount.updateValueAndValidity({ emitEvent: false }); + this.repaymentLoanForm?.controls.transactionAmount.setValidators(validators); + this.repaymentLoanForm?.controls.transactionAmount.updateValueAndValidity({ emitEvent: false }); } /** Submits the repayment form */ submit() { - if (this.repaymentLoanForm.invalid || this.isSubmitting) { + if (this.repaymentLoanForm?.invalid || this.isSubmitting) { return; } this.isSubmitting = true; this.cdr.markForCheck(); - const repaymentLoanFormData = this.repaymentLoanForm.value; + const repaymentLoanFormData: any = this.repaymentLoanForm?.value; const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; - const prevTransactionDate: Date = this.repaymentLoanForm.value.transactionDate; + const prevTransactionDate: Date = this.repaymentLoanForm?.value.transactionDate; if (repaymentLoanFormData.transactionDate instanceof Date) { repaymentLoanFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); } @@ -334,32 +355,63 @@ export class MakeRepaymentComponent extends LoanAccountActionsBaseComponent impl } delete data.skipInterestRefund; - // Waive penalties first if selected, then submit repayment - if (this.waivePenalties && this.selectedPenalties.length > 0) { - this.penaltyManagementService.waivePenalties(this.loanId, this.selectedPenalties).subscribe({ - next: () => { - this.submitRepayment(data); - }, - error: (error: any) => { - // Continue with repayment even if waive fails - this.submitRepayment(data); - } - }); + if (data['paymentTypeId'] === null) { + delete data.paymentTypeId; + } + + if (this.loanProductService.isLoanProduct && this.isRepayment()) { + // Waive penalties first if selected, then submit repayment + if (this.waivePenalties && this.selectedPenalties.length > 0) { + this.penaltyManagementService + .waivePenalties(this.loanId, this.selectedPenalties) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.submitCommandAction(data); + }, + error: () => { + this.alertService.alert({ + type: 'Warning', + message: 'Some penalties could not be waived. Proceeding with repayment.' + }); + this.submitCommandAction(data); + } + }); + } else { + this.submitCommandAction(data); + } } else { - this.submitRepayment(data); + this.submitCommandAction(data); } } - /** Submit the repayment after penalties are waived */ - private submitRepayment(data: any) { - this.loanService.submitLoanActionButton(this.loanId, data, this.command).subscribe({ - next: (response: any) => { - this.gotoLoanView('transactions'); - }, - error: (error: any) => { - this.isSubmitting = false; - this.cdr.markForCheck(); - } - }); + private submitCommandAction(data: any) { + if (this.loanProductService.isLoanProduct) { + this.loanService + .submitLoanActionButton(this.loanId, data, this.command) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.gotoLoanView('transactions'); + }, + error: () => { + this.isSubmitting = false; + this.cdr.markForCheck(); + } + }); + } else { + this.loanService + .applyWorkingCapitalLoanActionCommand(this.loanId, data, this.command) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.gotoLoanView('transactions'); + }, + error: () => { + this.isSubmitting = false; + this.cdr.markForCheck(); + } + }); + } } } diff --git a/src/app/loans/loans-view/loans-view.component.html b/src/app/loans/loans-view/loans-view.component.html index 557b81bb1e..8e802d7060 100644 --- a/src/app/loans/loans-view/loans-view.component.html +++ b/src/app/loans/loans-view/loans-view.component.html @@ -8,285 +8,258 @@ @if (loanDetailsData && buttonConfig) {
+
+ - - @for (item of buttonConfig.singleButtons; track item) { - - } + + @for (item of buttonConfig.singleButtons; track item) { + + } - @if (buttonConfig.optionsPayment.length) { - - - - @for (item of buttonConfig.optionsPayment; track item) { - - - - } - - - } + @if (buttonConfig.optionsPayment.length) { + + + + @for (item of buttonConfig.optionsPayment; track item) { + + } + + + } - @if (buttonConfig.options.length) { - - - - @for (item of buttonConfig.options; track item) { - - - - } - - - } - - - + @if (buttonConfig.options.length) { + + + + @for (item of buttonConfig.options; track item) { + + } + + + } +