From d0370781e85617e5f6ed6b8e56418290c92e129a Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 14 May 2026 07:40:04 +0530 Subject: [PATCH] update loan product code and rd,fd,saving,current account validation add Co-Authored-By: Claude Sonnet 4.6 --- .prettierrc | 3 +- .../general-tab/general-tab.component.html | 2 +- .../general-tab/general-tab.component.ts | 14 +- .../create-current-account.component.ts | 25 +- ...urrent-account-details-step.component.html | 18 +- .../current-account-details-step.component.ts | 25 +- ...urrent-account-preview-step.component.html | 48 --- .../create-fixed-deposit-account.component.ts | 35 +- ...-deposit-account-details-step.component.ts | 32 +- ...ount-interest-rate-chart-step.component.ts | 10 +- ...eposit-account-preview-step.component.html | 58 ++- ...eposit-account-preview-step.component.scss | 49 ++- ...-deposit-account-preview-step.component.ts | 33 +- ...te-recurring-deposits-account.component.ts | 10 + ...deposits-account-details-step.component.ts | 32 +- ...posits-account-preview-step.component.html | 24 +- .../create-loan-product.component.scss | 8 +- .../loan-product-details-step.component.html | 30 ++ .../loan-product-details-step.component.scss | 137 +++++++ .../loan-product-details-step.component.ts | 75 +++- .../loan-product-settings-step.component.html | 161 ++++++-- .../loan-product-settings-step.component.scss | 8 + .../loan-product-settings-step.component.ts | 57 ++- .../loan-product-terms-step.component.html | 87 ++++ .../loan-product-terms-step.component.scss | 382 ++++++++++++++++-- .../loan-product-terms-step.component.ts | 83 +++- src/app/products/products.service.ts | 18 + .../create-savings-account.component.ts | 10 + .../savings-account-details-step.component.ts | 12 +- .../dormancy-alert-panel.component.scss | 13 +- 30 files changed, 1286 insertions(+), 213 deletions(-) diff --git a/.prettierrc b/.prettierrc index 754ccd867..fb9acc4a1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "prettier-plugin-multiline-arrays" ], "singleQuote": true, - "trailingComma": "none" + "trailingComma": "none", + "endOfLine": "auto" } diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.html b/src/app/clients/clients-view/general-tab/general-tab.component.html index 0da32a41a..2123fb7ae 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.html +++ b/src/app/clients/clients-view/general-tab/general-tab.component.html @@ -65,7 +65,7 @@

{{ 'labels.heading.Saving Accounts' | translate }}

- {{ savingAccounts?.length || 0 }} + {{ pureSavingsCount }}
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index cc44792be..5c031ba97 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -22,6 +22,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add-related-client-dialog.component'; +import { AlertService } from 'app/core/alert/alert.service'; /** * Current Account Details Step @@ -46,6 +47,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { private settingsService = inject(SettingsService); private clientsService = inject(ClientsService); private dialog = inject(MatDialog); + private alertService = inject(AlertService); private dateUtils = inject(Dates); private cdr = inject(ChangeDetectorRef); @@ -365,9 +367,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit { }); ref.afterClosed().subscribe((result) => { if (result) { - const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + const isPrimary = this.primaryClientCif && this.primaryClientCif === result.clientCif; + const duplicate = isPrimary || this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); if (duplicate) { - alert(`Account holder with CIF ${result.clientCif} is already added.`); + this.alertService.alert({ + type: 'Error', + message: `Account holder with CIF ${result.clientCif} is already added.` + }); return; } this.jointHolders = [ @@ -407,6 +413,17 @@ export class CurrentAccountDetailsStepComponent implements OnInit { return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; } + get today(): Date { + const d = new Date(); + d.setHours(0, 0, 0, 0); + return d; + } + + get minStartOperationDate(): Date | null { + const val = this.currentAccountDetailsForm?.get('establishmentDate')?.value; + return val ? new Date(val) : null; + } + get majorityDate(): string { const dob = this.minorDob ?? this.clientDob; if (!dob) return ''; @@ -426,7 +443,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { if (result) { const duplicate = this.relatedClients.some((c) => c.clientCif && c.clientCif === result.clientCif); if (duplicate) { - alert(`Partner with CIF ${result.clientCif} is already added.`); + this.alertService.alert({ type: 'Error', message: `Partner with CIF ${result.clientCif} is already added.` }); return; } this.relatedClients = [ @@ -520,7 +537,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { value.guardianName = this.guardianName; value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; - value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'dd MMMM yyyy') : null; } // non-individual: partners are a separate dataset diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html index 8265860bf..1fd091156 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html @@ -649,54 +649,6 @@ } - - @if (currentAccount?.partners?.length > 0) { -
-
- - Non-Individual Account Details -
-
- -
Partners / Members
-
- - - - - - - - - - - @for (p of currentAccount.partners; track p.clientId) { - - - - - - - } - -
Partner NameCIF NoOwnership %Signing Authority
{{ p.clientName || '—' }}{{ p.clientCif || '—' }}{{ p.sharePercentage }}% - - {{ p.isMandatory ? 'Mandatory' : 'Optional' }} - -
-
- - @if (currentAccount?.signatoryRights) { -
- - Mode of Operation: - {{ currentAccount.signatoryRights }} -
- } -
-
- } - @if (hasDigitalSection) {
diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts index 2761036f7..44c48f58f 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { FixedDepositsService } from '../fixed-deposits.service'; import { SettingsService } from 'app/settings/settings.service'; +import { AlertService } from 'app/core/alert/alert.service'; /** Custom Components */ import { FixedDepositAccountDetailsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component'; @@ -59,6 +60,7 @@ export class CreateFixedDepositAccountComponent { private dateUtils = inject(Dates); private fixedDepositsService = inject(FixedDepositsService); private settingsService = inject(SettingsService); + private alertService = inject(AlertService); @ViewChild(FixedDepositAccountDetailsStepComponent, { static: true }) fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent; @@ -70,7 +72,14 @@ export class CreateFixedDepositAccountComponent { /** Fixed Deposit Account Product Template */ fixedDepositsAccountProductTemplate: any; /** Tenure selected from the interest rate chart step */ - selectedTenure: { depositPeriod: number; depositPeriodFrequencyId: number; depositAmount?: number } | null = null; + selectedTenure: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + maturityAmount?: number; + interestEarned?: number; + appliedRate?: number; + } | null = null; /** * Fetches FD account template from `resolve` @@ -94,7 +103,14 @@ export class CreateFixedDepositAccountComponent { this.fixedDepositsAccountProductTemplate = $event; } - onTenureSelected($event: { depositPeriod: number; depositPeriodFrequencyId: number; depositAmount?: number }) { + onTenureSelected($event: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + maturityAmount?: number; + interestEarned?: number; + appliedRate?: number; + }) { this.selectedTenure = $event; } @@ -126,6 +142,14 @@ export class CreateFixedDepositAccountComponent { * Submits the fixed deposit form and creates a new fixed deposit account */ submit() { + const detailsStep = this.fixedDepositsAccountDetailsStep; + if (detailsStep?.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; @@ -178,7 +202,12 @@ export class CreateFixedDepositAccountComponent { ...this.fixedDepositAccount, ...terms, ...settings, - ...(this.selectedTenure ?? {}), + ...(this.selectedTenure + ? { + depositPeriod: this.selectedTenure.depositPeriod, + depositPeriodFrequencyId: this.selectedTenure.depositPeriodFrequencyId + } + : {}), clientId: this.fixedDepositsAccountTemplate.clientId, charges: rawCharges.map((charge: any) => ({ chargeId: charge.chargeId ?? charge.id, diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts index 832f3d752..297db0824 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts @@ -22,6 +22,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatIcon } from '@angular/material/icon'; import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; +import { AlertService } from 'app/core/alert/alert.service'; /** * Fixed Deposits Account Details Step @@ -48,6 +49,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { private cdr = inject(ChangeDetectorRef); private clientsService = inject(ClientsService); private dialog = inject(MatDialog); + private alertService = inject(AlertService); /** Fixed Deposits Account Template */ @Input() fixedDepositsAccountTemplate: any; @@ -221,15 +223,17 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { this.dalLoading = true; this.clientsService.getClientAccountData(clientId).subscribe({ next: (data: any) => { - this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({ - id: a.id, - accountNo: a.accountNo, - productName: a.productName ?? '', - accountType: 'SAVINGS' as const, - status: a.status?.value ?? '', - balance: a.accountBalance ?? 0, - currencyCode: a.currency?.code ?? '' - })); + this.dalAllSavings = (data?.savingsAccounts ?? []) + .filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit') + .map((a: any) => ({ + id: a.id, + accountNo: a.accountNo, + productName: a.productName ?? '', + accountType: 'SAVINGS' as const, + status: a.status?.value ?? '', + balance: a.accountBalance ?? 0, + currencyCode: a.currency?.code ?? '' + })); this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ id: a.id, accountNo: a.accountNo, @@ -469,9 +473,13 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { }); ref.afterClosed().subscribe((result) => { if (result) { - const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + const isPrimary = this.primaryClientCif && this.primaryClientCif === result.clientCif; + const duplicate = isPrimary || this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); if (duplicate) { - alert(`Account holder with CIF ${result.clientCif} is already added.`); + this.alertService.alert({ + type: 'Error', + message: `Account holder with CIF ${result.clientCif} is already added.` + }); return; } this.jointHolders = [ @@ -582,7 +590,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { value.guardianName = this.guardianName; value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; - value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'dd MMMM yyyy') : null; } if (this.selectedLinkedAccount) { diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts index cf45ee1d1..6d683dc40 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts @@ -76,6 +76,9 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan depositPeriod: number; depositPeriodFrequencyId: number; depositAmount?: number; + maturityAmount?: number; + interestEarned?: number; + appliedRate?: number; }>(); interestRateChartData: any[] = []; @@ -254,13 +257,16 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan this.matchResult = result; - // Emit selected tenure and principal amount to parent for backend submission + // Emit selected tenure and calculated maturity to parent const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit); if (frequencyId !== null) { this.tenureSelected.emit({ depositPeriod: this.tenureValue, depositPeriodFrequencyId: frequencyId, - depositAmount: this.principalAmount || undefined + depositAmount: this.principalAmount || undefined, + maturityAmount: result.maturityAmount, + interestEarned: result.interestEarned, + appliedRate: result.appliedRate }); } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html index b05e5d69c..9cf7782fc 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html @@ -95,7 +95,7 @@
}
- Share Linking Enabled + Deposit Account Link
- @if (fixedDepositsAccountProductTemplate?.introducerRequired !== undefined) { -
- Introducer Required - - {{ fixedDepositsAccountProductTemplate?.introducerRequired ? 'Yes' : 'No' }} - -
- } @@ -316,12 +305,19 @@ - @if (fixedDepositsAccountProductTemplate?.dormancyAlert) { + @if ( + fixedDepositsAccountProductTemplate?.dormancyAlert || fixedDepositsAccountProductTemplate?.isDormancyTrackingActive + ) {
Dormancy Monitoring - Monitoring: Active + + {{ fixedDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }} +
@@ -330,7 +326,11 @@
Inactive - After {{ fixedDepositsAccountProductTemplate?.daysToInactiveFromProduct || 30 }} days of no transaction + @if (fixedDepositsAccountProductTemplate?.daysToInactive != null) { + After {{ fixedDepositsAccountProductTemplate.daysToInactive }} days of no transaction + } @else { + Not configured + }
@@ -340,7 +340,11 @@
Dormant - {{ fixedDepositsAccountProductTemplate?.daysToDormancyFromProduct || 60 }} days after inactive + @if (fixedDepositsAccountProductTemplate?.daysToDormancy != null) { + {{ fixedDepositsAccountProductTemplate.daysToDormancy }} days after inactive + } @else { + Not configured + }
@@ -350,11 +354,31 @@
Escheat - {{ fixedDepositsAccountProductTemplate?.daysToEscheatFromProduct || 90 }} days after dormant + @if (fixedDepositsAccountProductTemplate?.daysToEscheat != null) { + {{ fixedDepositsAccountProductTemplate.daysToEscheat }} days after dormant + } @else { + Not configured + }
+
+ + Alert + {{ fixedDepositsAccountProductTemplate?.dormancyAlert ? 'Enabled' : 'Disabled' }} + + + Tracking + {{ fixedDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }} + +
} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.scss index c7c88cafb..cc19947d5 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.scss @@ -423,31 +423,25 @@ .preview-dormancy-timeline { display: flex; align-items: flex-start; - gap: 12px; - margin: 1.25rem 0; - overflow-x: auto; - padding-bottom: 8px; + margin-bottom: 14px; } .preview-dorm-step { display: flex; flex-direction: column; align-items: center; - flex: 0 0 140px; - gap: 10px; + flex: 1; + gap: 6px; &__dot { - width: 32px; - height: 32px; + width: 36px; + height: 36px; border-radius: 50%; + border: 3px solid; display: flex; align-items: center; justify-content: center; - border: 3px solid #e2e8f0; - background: #f8fafc; - position: relative; - box-shadow: 0 0 0 4px #fff; - z-index: 2; + flex-shrink: 0; &--inactive { border-color: #f59e0b; @@ -517,3 +511,32 @@ margin-top: 16px; border-radius: 2px; } + +.preview-dorm-flags { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.preview-dorm-flag { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 12px; + border-radius: 20px; + font-size: 11.5px; + font-weight: 600; + background: #f1f5f9; + color: #94a3b8; + border: 1px solid #e2e8f0; + + i { + font-size: 11px; + } + + &--on { + background: #f0fdf4; + color: #15803d; + border-color: #bbf7d0; + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts index 7232f2d38..ee134eab3 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts @@ -76,9 +76,15 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { @Input() fixedDepositsAccountProductTemplate: any; /** Fixed Deposits Account Data */ @Input() fixedDepositAccountData: any; - /** Tenure selected from the interest rate chart step */ - @Input() selectedTenure: { depositPeriod: number; depositPeriodFrequencyId: number; depositAmount?: number } | null = - null; + /** Tenure selected from the interest rate chart step (includes pre-calculated maturity if user used calculator) */ + @Input() selectedTenure: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + maturityAmount?: number; + interestEarned?: number; + appliedRate?: number; + } | null = null; /** Charges Displayed Columns */ chargesDisplayedColumns: string[] = [ @@ -237,6 +243,20 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { this.fixedDepositsAccountProductTemplate?.depositAmount ?? 0; + // If the chart step already calculated maturity, use those values directly + if (this.selectedTenure?.maturityAmount != null && this.selectedTenure.maturityAmount > 0) { + const interestRate = + this.selectedTenure.appliedRate ?? this.fixedDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? 0; + return { + depositAmount, + maturityAmount: this.selectedTenure.maturityAmount, + totalInterest: this.selectedTenure.interestEarned ?? this.selectedTenure.maturityAmount - depositAmount, + interestRate, + tenureLabel: this.selectedTenureLabel + }; + } + + // Fallback: recalculate using product template settings const chartRate = this.matchedSlabIndex >= 0 ? this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate : null; const interestRate = chartRate ?? this.fixedDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? 0; @@ -248,20 +268,15 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365; if (depositPeriodFrequencyId === 0) { - // Days t = depositPeriod / daysInYear; } else if (depositPeriodFrequencyId === 1) { - // Weeks t = (depositPeriod * 7) / daysInYear; } else if (depositPeriodFrequencyId === 2) { - // Months t = depositPeriod / 12; } else if (depositPeriodFrequencyId === 3) { - // Years t = depositPeriod; } - // Compounding frequency (n) const compPeriodValue = ( this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.value || '' ).toLowerCase(); @@ -273,7 +288,6 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { else if (compPeriodValue.includes('biannual')) n = 2; else if (compPeriodValue.includes('annual')) n = 1; - // Interest Calculation Type const calcTypeValue = ( this.fixedDepositsAccountProductTemplate?.interestCalculationType?.value || '' ).toLowerCase(); @@ -283,7 +297,6 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { if (isSimple) { maturityAmount = depositAmount + depositAmount * r * t; } else { - // Compound interest: A = P(1 + r/n)^(nt) maturityAmount = depositAmount * Math.pow(1 + r / n, n * t); } diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts index bfc5384a9..995587be2 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts @@ -14,6 +14,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { RecurringDepositsService } from '../recurring-deposits.service'; import { SettingsService } from 'app/settings/settings.service'; import { ProductsService } from 'app/products/products.service'; +import { AlertService } from 'app/core/alert/alert.service'; /** Step Components */ import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; @@ -61,6 +62,7 @@ export class CreateRecurringDepositsAccountComponent { private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); private productsService = inject(ProductsService); + private alertService = inject(AlertService); @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent; @@ -157,6 +159,14 @@ export class CreateRecurringDepositsAccountComponent { * Submits the recurring deposit form to create a new recurring deposit account */ submit() { + const detailsStep = this.recurringDepositsAccountDetailsStep; + if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 3bd5b04a2..389803067 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -22,6 +22,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatIcon } from '@angular/material/icon'; import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; +import { AlertService } from 'app/core/alert/alert.service'; /** * Recurring Deposits Account Details Step @@ -48,6 +49,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { private cdr = inject(ChangeDetectorRef); private clientsService = inject(ClientsService); private dialog = inject(MatDialog); + private alertService = inject(AlertService); /** Recurring Deposits Account Template */ @Input() recurringDepositsAccountTemplate: any; @@ -198,15 +200,17 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.dalLoading = true; this.clientsService.getClientAccountData(clientId).subscribe({ next: (data: any) => { - this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({ - id: a.id, - accountNo: a.accountNo, - productName: a.productName ?? '', - accountType: 'SAVINGS' as const, - status: a.status?.value ?? '', - balance: a.accountBalance ?? 0, - currencyCode: a.currency?.code ?? '' - })); + this.dalAllSavings = (data?.savingsAccounts ?? []) + .filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit') + .map((a: any) => ({ + id: a.id, + accountNo: a.accountNo, + productName: a.productName ?? '', + accountType: 'SAVINGS' as const, + status: a.status?.value ?? '', + balance: a.accountBalance ?? 0, + currencyCode: a.currency?.code ?? '' + })); this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ id: a.id, accountNo: a.accountNo, @@ -430,9 +434,13 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { }); ref.afterClosed().subscribe((result) => { if (result) { - const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + const isPrimary = this.primaryClientCif && this.primaryClientCif === result.clientCif; + const duplicate = isPrimary || this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); if (duplicate) { - alert(`Account holder with CIF ${result.clientCif} is already added.`); + this.alertService.alert({ + type: 'Error', + message: `Account holder with CIF ${result.clientCif} is already added.` + }); return; } this.jointHolders = [ @@ -540,7 +548,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { value.guardianName = this.guardianName; value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; - value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'dd MMMM yyyy') : null; } if (this.selectedLinkedAccount) { diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index 824777e6b..e26d30815 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -463,7 +463,7 @@ @if ( recurringDepositsAccountProductTemplate?.dormancyAlert || - recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct + recurringDepositsAccountProductTemplate?.isDormancyTrackingActive ) {
@@ -471,11 +471,9 @@ Dormancy Monitoring - {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} + {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
@@ -485,8 +483,8 @@
Inactive - @if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct !== null) { - After {{ recurringDepositsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction + @if (recurringDepositsAccountProductTemplate?.daysToInactive != null) { + After {{ recurringDepositsAccountProductTemplate.daysToInactive }} days of no transaction } @else { Not configured } @@ -499,8 +497,8 @@
Dormant - @if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct !== null) { - {{ recurringDepositsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive + @if (recurringDepositsAccountProductTemplate?.daysToDormancy != null) { + {{ recurringDepositsAccountProductTemplate.daysToDormancy }} days after inactive } @else { Not configured } @@ -513,8 +511,8 @@
Escheat - @if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct !== null) { - {{ recurringDepositsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant + @if (recurringDepositsAccountProductTemplate?.daysToEscheat != null) { + {{ recurringDepositsAccountProductTemplate.daysToEscheat }} days after dormant } @else { Not configured } @@ -532,10 +530,10 @@ Tracking - {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} + {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss b/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss index d05cedb1a..68e4c9ac5 100644 --- a/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss @@ -9,7 +9,7 @@ $primary: #1353a0; .lp-container { - max-width: 960px; + max-width: 1200px; margin: 0 auto; padding: 24px 16px; } @@ -53,11 +53,11 @@ $primary: #1353a0; } .mat-mdc-tab-body { - overflow: visible !important; + overflow: hidden visible !important; } .mat-mdc-tab-body-content { - overflow: visible !important; + overflow: hidden visible !important; } } } @@ -69,6 +69,8 @@ $primary: #1353a0; .lp-tab-content { padding: 24px 16px; + min-width: 0; + overflow-x: hidden; } .lp-action-bar { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html index b5e37990a..434640cec 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.html @@ -90,4 +90,34 @@
+ + +
+
+ +

Product Classification

+
+
+ + Product Type Group + + @for (group of productTypeGroups; track group.id) { + {{ group.label }} + } + + + + @if (loanProductDetailsForm.get('productTypeGroup')?.value) { + + GL Account ({{ glClassificationLabel }}) + + @for (acc of availableGlAccounts; track acc.id) { + ({{ acc.glCode }}) {{ acc.name }} + } + + GL Account is required + + } +
+
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss index e0ff5016a..f6a3f62d5 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss @@ -68,3 +68,140 @@ $gray-900: #202124; align-items: center; padding: 4px 0; } + +// GL Section +.lpd-gl-section { + border-top: 2px solid #e0f2fe; +} + +.lpd-gl-subtype-badge { + margin-left: auto; + padding: 3px 12px; + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 20px; + font-size: 11.5px; + font-weight: 600; + color: #2563eb; +} + +.lpd-gl-body { + padding: 16px 24px 20px; +} + +.lpd-gl-hint { + font-size: 12.5px; + color: #64748b; + margin: 0 0 16px; +} + +.lpd-gl-cards { + display: flex; + flex-direction: column; + gap: 10px; +} + +.lpd-gl-card { + display: flex; + align-items: center; + gap: 16px; + padding: 14px 18px; + border: 2px solid #e2e8f0; + border-radius: 10px; + cursor: pointer; + background: #fff; + transition: + border-color 0.15s, + box-shadow 0.15s, + background 0.15s; + user-select: none; + + &:hover { + border-color: #93c5fd; + background: #f8faff; + } + + &--selected { + border-color: #2563eb !important; + background: #eff6ff !important; + box-shadow: 0 0 0 3px rgb(37 99 235 / 10%); + + .lpd-gl-card__check { + opacity: 1; + color: #2563eb; + } + } +} + +.lpd-gl-radio { + display: none; +} + +.lpd-gl-card__icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + flex-shrink: 0; + + &--asset { + background: #dcfce7; + color: #15803d; + } + + &--liability { + background: #fee2e2; + color: #b91c1c; + } +} + +.lpd-gl-card__info { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; +} + +.lpd-gl-card__title { + font-size: 14px; + font-weight: 700; + color: #1e293b; +} + +.lpd-gl-card__desc { + font-size: 12px; + color: #64748b; +} + +.lpd-gl-card__check { + font-size: 18px; + opacity: 0; + transition: opacity 0.15s; + flex-shrink: 0; +} + +.lpd-gl-account-field { + width: 100%; + max-width: 480px; +} + +.lpd-gl-type-badge { + padding: 3px 12px; + border-radius: 20px; + font-size: 11.5px; + font-weight: 700; + margin-left: 6px; + + &--asset { + background: #dcfce7; + color: #15803d; + } + + &--liability { + background: #fee2e2; + color: #b91c1c; + } +} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts index 5548aee15..68373a59d 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.ts @@ -57,6 +57,68 @@ export class LoanProductDetailsStepComponent implements OnInit { startDate: this.loanProductsTemplate.startDate && new Date(this.loanProductsTemplate.startDate), closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate) }); + + const mappingOptions = this.loanProductsTemplate?.accountingMappingOptions ?? {}; + this.assetAccountOptions = mappingOptions.assetAccountOptions ?? []; + this.liabilityAccountOptions = mappingOptions.liabilityAccountOptions ?? []; + } + + /** Product Classification data */ + readonly productTypeGroups = [ + { id: 'loans', label: 'Loan' }, + { id: 'advanced', label: 'Advanced' } + ]; + + readonly productTypeSubGroups: Record = { + loans: [ + { id: 'emi', label: 'EMI' }, + { id: 'cc', label: 'CC (Cash Credit)' }, + { id: 'overdraft', label: 'Overdraft' }, + { id: 'warehouse', label: 'Warehouse' }, + { id: 'gold', label: 'Gold' } + ], + advanced: [ + { id: 'fd', label: 'FD (Fixed Deposit)' }, + { id: 'rd', label: 'RD (Recurring Deposit)' } + ] + }; + + /** Default GL per product type group */ + private readonly glDefaultMap: Record = { + loans: 'asset', + advanced: 'asset' + }; + + get availableSubGroups() { + const group = this.loanProductDetailsForm.get('productTypeGroup')?.value; + return group ? (this.productTypeSubGroups[group] ?? []) : []; + } + + get glOptions(): string[] { + return [ + 'asset', + 'liability' + ]; + } + + assetAccountOptions: any[] = []; + liabilityAccountOptions: any[] = []; + + get availableGlAccounts(): any[] { + const type = this.loanProductDetailsForm.get('glClassification')?.value; + if (type === 'asset') return this.assetAccountOptions; + if (type === 'liability') return this.liabilityAccountOptions; + return []; + } + + get glClassificationLabel(): string { + const type = this.loanProductDetailsForm.get('glClassification')?.value; + return type === 'liability' ? 'Liability' : 'Asset'; + } + + get selectedGroupLabel(): string { + const group = this.loanProductDetailsForm.get('productTypeGroup')?.value; + return this.productTypeGroups.find((g) => g.id === group)?.label ?? ''; } createLoanProductDetailsForm() { @@ -71,7 +133,18 @@ export class LoanProductDetailsStepComponent implements OnInit { ], description: [''], startDate: [''], - closeDate: [''] + closeDate: [''], + productTypeGroup: [''], + productTypeSubGroup: [''], + glClassification: ['asset'], + glAccount: [''] + }); + + // Auto-set GL based on product type group + this.loanProductDetailsForm.get('productTypeGroup')?.valueChanges.subscribe((group: string) => { + const defaultVal = this.glDefaultMap[group] ?? 'asset'; + this.loanProductDetailsForm.get('glClassification')?.setValue(defaultVal); + this.loanProductDetailsForm.get('glAccount')?.setValue(''); }); } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html index a1259fe05..16658c103 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html @@ -467,7 +467,7 @@ - + Against Interest Calculation Enabled YES @@ -499,7 +499,7 @@ - + Consider Debit Closing Amount For Against Clearing YES @@ -517,7 +517,7 @@ - + Penalty Interest Calculation Enabled YES @@ -525,7 +525,7 @@ - + Penalty Interest Calculation Method Daywise ( 36500 ) @@ -559,7 +559,7 @@ - + Interest On Credit Balance YES @@ -567,7 +567,7 @@ - + Penalty Interest Date Different Than Regular Interest YES @@ -575,7 +575,7 @@ - + Fixed Deposit Overdraft Certificate Sort Order Order on Interest Rate @@ -599,12 +599,12 @@ - + Minimum Normal Interest Amount - + Minimum Interest Calculation Method Flat Amount @@ -613,12 +613,12 @@ - + Minimum Against Clearing Interest Amount - + Limit Increase Upto Interest Amount In Interest Calculation YES @@ -626,22 +626,22 @@ - + Minimum Penalty Interest Amount - + Days For Limit Increase Upto Interest Amount - + Additional Interest Rate For Fixed Deposit Overdraft - + Consider DD/Payorder In Against Clearing Interest Calculation YES @@ -649,12 +649,12 @@ - + Grace Days After Renewal Date - + Apply Undebited Interest Concept Enabled YES @@ -671,12 +671,12 @@ - + Normal Grace Days After Overdue - + Allow Against Clearing Entry YES @@ -684,7 +684,7 @@ - + Allow No Balance Entry YES @@ -692,7 +692,7 @@ - + Fixed Deposit Overdraft Penalty Interest Combined With Normal Interest YES @@ -700,7 +700,7 @@ - + Allow No Balance Entry With Password YES @@ -708,7 +708,7 @@ - + Undebited Simple Interest Account Wise YES @@ -716,7 +716,7 @@ - + Daily Interest Calculation Enabled YES @@ -724,12 +724,12 @@ - + Daily Interest Calculation Start Date - + Non Performing Asset Penal Interest Rate Update YES @@ -737,12 +737,12 @@ - + Non Performing Asset Penal Interest Rate - + Fixed Deposit Overdraft Penal Interest Rate Update In Branch Process YES @@ -750,13 +750,116 @@ - + Add Penal Interest Rate
+ +
+
+ +

RBI Compliance & KFS Settings

+
+
+ + Penal Charge Method + + RBI Compliant Penal Charges (Non-Compounding) + Legacy Penal Interest (Compounding) + + + +
+ + Is Floating Rate Loan? (Individual) + +
+ + + Foreclosure Charges Applicable + + YES + NO + + RBI: No charges for Floating Rate Loans to Individuals + + +
+ + Enable Key Fact Statement (KFS) + +
+ +
+ + Calculate APR + +
+
+
+ + +
+
+ +

Credit Bureau Settings (CIBIL/Highmark)

+
+
+ + Loan Product Category (RBI Reporting) + + Personal/Consumer Loan (Code 02) + Housing/Mortgage Loan (Code 15) + Business/Commercial Loan (Code 50) + Vehicle/Auto Loan (Code 04) + + Selecting a category will auto-fill the credit bureau account types below. + + + CIBIL Account Type + + + + + CIBIL Commercial Account Type + + + + + Highmark Account Type + + + + + Highmark Commercial Account Type + + + + + Experian Account Type + + + + + Experian Commercial Account Type + + + + + Equifax Account Type + + + + + Equifax Commercial Account Type + + +
+
+
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss index 5717de2fa..50934aecc 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss @@ -75,6 +75,14 @@ grid-column: 1 / -1; } +.lps-span-2 { + grid-column: span 2; +} + +.lps-span-3 { + grid-column: span 3; +} + .lps-checkbox-row { display: flex; align-items: center; diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts index 256b986d4..d230afc79 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts @@ -408,13 +408,68 @@ export class LoanProductSettingsStepComponent implements OnInit { nonPerformingAssetPenalInterestRateUpdate: ['YES'], nonPerformingAssetPenalInterestRate: [''], fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'], - addPenalInterestRate: [''] + addPenalInterestRate: [''], + + // Credit Bureau Settings (CIBIL) + cibilAccountType: [''], + cibilCommercialAccountType: [''], + highmarkAccountType: [''], + highmarkCommercialAccountType: [''], + experianAccountType: [''], + experianCommercialAccountType: [''], + equifaxAccountType: [''], + equifaxCommercialAccountType: [''], + + // RBI Compliance & Product Category + loanProductCategory: [''], + penalChargeMethod: ['RBI COMPLIANT PENAL CHARGES'], + isFloatingRate: [false], + foreclosureChargesApplicable: ['NO'], + keyFactStatementEnabled: [true], + annualPercentageRateCalculationEnabled: [true] }); } + private autoFillCibilFields(category: string) { + const cibilMappings: any = { + PERSONAL_LOAN: { ac: '02', comm: '0420' }, + HOUSING_LOAN: { ac: '15', comm: '9004' }, + BUSINESS_LOAN: { ac: '50', comm: '0100' }, + VEHICLE_LOAN: { ac: '04', comm: '9004' } + }; + + const mapping = cibilMappings[category]; + if (mapping) { + this.loanProductSettingsForm.patchValue({ + cibilAccountType: mapping.ac, + cibilCommercialAccountType: mapping.comm, + highmarkAccountType: mapping.ac, + highmarkCommercialAccountType: mapping.comm, + experianAccountType: mapping.ac, + experianCommercialAccountType: mapping.comm, + equifaxAccountType: mapping.ac, + equifaxCommercialAccountType: mapping.comm + }); + } + } + setConditionalControls() { const allowAttributeOverrides = this.loanProductSettingsForm.get('allowAttributeOverrides'); + // RBI & CIBIL Category Listener + this.loanProductSettingsForm.get('loanProductCategory').valueChanges.subscribe((category: string) => { + if (category) { + this.autoFillCibilFields(category); + } + }); + + // Floating Rate Logic (RBI: No foreclosure charges for floating rate loans to individuals) + this.loanProductSettingsForm.get('isFloatingRate').valueChanges.subscribe((isFloating: boolean) => { + if (isFloating) { + this.loanProductSettingsForm.patchValue({ foreclosureChargesApplicable: 'NO' }); + } + }); + this.loanProductSettingsForm.get('daysInYearType').valueChanges.subscribe((daysInYearType: any) => { if (this.isAdvancedTransactionProcessingStrategy) { this.useDaysInYearCustomStrategy = daysInYearType == 1; diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html index b6f0e8837..abde9687b 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html @@ -264,6 +264,93 @@ }
+ +
+
+ +

Interest Rate Slabs

+
+ +
+ +
+ + +
+ + +
+ @for (genGroup of slabGroups; track $index) { +
+ {{ interestSlabDefinitions[$index].label }} +
+ +
+ Gen +
+ + % +
+
+ +
+ Spl +
+ + % +
+
+
+
+ } +
+ + +
+ + @if (fixedRateGroup.get('hasFixed').value) { +
+ + % +
+ } +
+
+
+
diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss index 5f40cb5b5..bda5c6392 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss @@ -6,14 +6,42 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// ─── Design tokens ──────────────────────────────────────────────────────────── +$primary: #2563eb; +$primary-light: #eff6ff; +$primary-border: #bfdbfe; +$surface: #fff; +$surface-alt: #f8fafc; +$border: #e2e8f0; +$text-primary: #1e293b; +$text-secondary: #64748b; +$text-label: #475569; +$accent-green: #059669; +$accent-orange: #d97706; +$radius-card: 12px; +$radius-inner: 8px; +$shadow-card: + 0 1px 3px rgb(0 0 0 / 7%), + 0 1px 2px rgb(0 0 0 / 4%); +$shadow-hover: 0 4px 12px rgb(37 99 235 / 10%); + +// ─── Card / Section ─────────────────────────────────────────────────────────── .lpt-section { - border: 1px solid #e2e8f0; - border-radius: 10px; - overflow: hidden; - background: #fff; + background: $surface; + border: 1px solid $border; + border-radius: $radius-card; + box-shadow: $shadow-card; + // No overflow:hidden here — it clips right-side content inside grids/tables. + // Border-radius is preserved; header gets matching top-radius instead. + overflow: visible; + transition: box-shadow 0.2s ease; + + &:hover { + box-shadow: $shadow-hover; + } & + & { - margin-top: 16px; + margin-top: 20px; } } @@ -21,34 +49,52 @@ display: flex; align-items: center; gap: 10px; - padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + padding: 14px 24px; + background: linear-gradient(135deg, $surface-alt 0%, #f1f5f9 100%); + border-bottom: 1px solid $border; + border-radius: $radius-card $radius-card 0 0; h3 { font-size: 14px; - font-weight: 600; - color: #202124; + font-weight: 700; + color: $text-primary; margin: 0; + letter-spacing: 0.01em; + text-transform: uppercase; } } .lpt-section-icon { - color: #0ea5e9; + color: $primary; font-size: 15px; + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + background: $primary-light; + border-radius: 6px; + border: 1px solid $primary-border; } .lpt-help-icon { - color: #9aa0a6; + color: $text-secondary; font-size: 13px; cursor: help; + margin-left: auto; + transition: color 0.15s; + + &:hover { + color: $primary; + } } +// ─── Grid layout ────────────────────────────────────────────────────────────── .lpt-grid { display: grid; grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; + gap: 8px 20px; + padding: 20px 24px; @media (width <= 600px) { grid-template-columns: 1fr; @@ -58,6 +104,10 @@ .lpt-grid-3 { grid-template-columns: repeat(3, 1fr); + @media (width <= 768px) { + grid-template-columns: repeat(2, 1fr); + } + @media (width <= 600px) { grid-template-columns: 1fr; } @@ -79,28 +129,42 @@ grid-column: 1 / -1; } +// ─── Checkbox rows ──────────────────────────────────────────────────────────── .lpt-checkbox-row { display: flex; align-items: center; - padding: 4px 0; -} + padding: 6px 0; -.lpt-subsection { - padding: 0 24px 16px; - - &:first-of-type { - padding-top: 0; + mat-checkbox { + font-size: 13.5px; + color: $text-label; } } +// ─── Subsection ─────────────────────────────────────────────────────────────── +.lpt-subsection { + padding: 0 24px 20px; +} + .lpt-subsection-title { - font-size: 13px; - font-weight: 600; - color: #3c4043; - margin: 8px 0 12px; + font-size: 12.5px; + font-weight: 700; + color: $text-secondary; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 12px 0; display: flex; align-items: center; gap: 8px; + + &::before { + content: ''; + display: inline-block; + width: 3px; + height: 14px; + background: $primary; + border-radius: 2px; + } } .lpt-subsection-header { @@ -114,9 +178,10 @@ } } +// ─── Misc ───────────────────────────────────────────────────────────────────── .lpt-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid $border; + border-radius: $radius-inner; overflow: hidden; margin-bottom: 16px; @@ -128,8 +193,265 @@ .lpt-label-field { display: flex; align-items: center; - font-size: 14px; + font-size: 13.5px; font-weight: 500; - color: #3c4043; - padding: 8px 0; + color: $text-label; + padding: 8px 4px; +} + +// ─── Interest Rate Slabs — Option C (2-column grid) ────────────────────────── +.lpt-slab-c { + padding: 16px 24px 20px; + display: flex; + flex-direction: column; + gap: 0; +} + +// Particular row +.lpt-slab-c-particular { + display: flex; + align-items: center; + gap: 14px; + padding-bottom: 14px; + border-bottom: 1px solid $border; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.lpt-slab-c-particular-label { + font-size: 12px; + font-weight: 700; + color: $text-secondary; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.lpt-slab-c-name-input { + flex: 1; + max-width: 320px; + padding: 7px 12px; + border: 1px solid $border; + border-radius: 8px; + font-size: 13.5px; + font-weight: 600; + color: $text-primary; + background: $surface; + outline: none; + transition: + border-color 0.15s, + box-shadow 0.15s; + + &::placeholder { + color: #b0b8c4; + font-weight: 400; + } + + &:focus { + border-color: $primary; + box-shadow: 0 0 0 3px rgb(37 99 235 / 10%); + } +} + +// General / Special tab toggle +.lpt-slab-tabs { + display: flex; + gap: 6px; + margin-left: auto; +} + +.lpt-slab-tab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + border: 1.5px solid $border; + border-radius: 20px; + background: $surface; + font-size: 12.5px; + font-weight: 600; + color: $text-secondary; + cursor: pointer; + transition: all 0.15s; + + i { + font-size: 11px; + } + + &:hover { + border-color: $primary; + color: $primary; + } + + &--active { + background: $primary; + border-color: $primary; + color: $surface; + box-shadow: 0 2px 8px rgb(37 99 235 / 25%); + } +} + +// 2-column slab grid +.lpt-slab-c-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 16px; + + @media (width <= 540px) { + grid-template-columns: 1fr; + } +} + +.lpt-slab-c-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 12px; + border: 1px solid $border; + border-radius: 8px; + background: $surface; + transition: + border-color 0.15s, + box-shadow 0.15s; + + &:focus-within { + border-color: $primary; + box-shadow: 0 0 0 3px rgb(37 99 235 / 8%); + } +} + +// General + Special pair inside each card +.lpt-slab-c-rate-pair { + display: flex; + gap: 8px; + align-items: center; +} + +.lpt-slab-c-rate-field { + display: flex; + align-items: center; + gap: 4px; +} + +.lpt-slab-c-rate-label { + font-size: 10px; + font-weight: 700; + color: $primary; + background: #eff6ff; + padding: 2px 5px; + border-radius: 4px; + white-space: nowrap; + + &--special { + color: #92400e; + background: #fef9c3; + } +} + +.lpt-slab-c-range { + font-size: 12.5px; + font-weight: 500; + color: $text-label; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +// Input + suffix wrapper +.lpt-slab-c-input-wrap { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.lpt-slab-c-input { + width: 70px; + padding: 4px 6px; + border: 1px solid $border; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + color: $text-primary; + text-align: right; + background: $surface-alt; + outline: none; + transition: border-color 0.15s; + appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + appearance: none; + } + + &:focus { + border-color: $primary; + background: $primary-light; + } + + &--fixed { + width: 90px; + } +} + +.lpt-slab-c-suffix { + font-size: 12px; + font-weight: 700; + color: $text-secondary; + user-select: none; + + &--special { + color: #92400e; + } +} + +.lpt-slab-c-input--special { + &:focus { + border-color: #f59e0b; + background: #fef9c3; + } +} + +.lpt-slab-c-error { + font-size: 10px; + color: #ef4444; +} + +// Fixed Interest Rate row (below grid) +.lpt-slab-c-fixed { + display: flex; + align-items: center; + gap: 20px; + margin: 12px -24px -20px; + padding: 12px 16px; + border-top: 2px dashed $primary-border; + background: $primary-light; + border-radius: 0 0 $radius-card $radius-card; + padding-left: 24px; + padding-right: 24px; +} + +.lpt-slab-c-fixed-toggle { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + color: $primary; + user-select: none; + + i { + font-size: 12px; + opacity: 0.7; + } +} + +.lpt-slab-c-checkbox { + accent-color: $primary; + width: 15px; + height: 15px; + cursor: pointer; } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts index fbe2b20e6..b2b0cd069 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.ts @@ -12,6 +12,9 @@ import { UntypedFormBuilder, Validators, UntypedFormControl, + FormArray, + FormGroup, + FormControl, ReactiveFormsModule } from '@angular/forms'; import { ProcessingStrategyService } from '../../services/processing-strategy.service'; @@ -54,6 +57,25 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges { repaymentFrequencyTypeData: any; repaymentStartDateTypeOptions: any; + readonly interestSlabDefinitions = [ + { label: 'Upto 10,000', maxAmount: 10000 }, + { label: 'Upto 25,000', maxAmount: 25000 }, + { label: 'Upto 1,00,000', maxAmount: 100000 }, + { label: 'Upto 2,00,000', maxAmount: 200000 }, + { label: 'Upto 5,00,000', maxAmount: 500000 }, + { label: 'Upto 10,00,000', maxAmount: 1000000 }, + { label: 'Upto 15,00,000', maxAmount: 1500000 }, + { label: 'Upto 20,00,000', maxAmount: 2000000 }, + { label: 'Upto 25,00,000', maxAmount: 2500000 }, + { label: 'More than 25,00,000', maxAmount: null } + ]; + + interestRateSlabsArray: FormArray; + specialSlabsArray: FormArray; + fixedRateGroup: FormGroup; + particularLabel = new FormControl(''); + activeSlabTab: 'general' | 'special' = 'general'; + constructor() { this.createLoanProductTermsForm(); this.setConditionalControls(); @@ -109,6 +131,35 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges { } createLoanProductTermsForm() { + this.interestRateSlabsArray = new FormArray( + this.interestSlabDefinitions.map( + () => + new FormGroup({ + interestRate: new FormControl(null, [ + Validators.min(0), + Validators.max(100) + ]) + }) + ) + ); + + this.specialSlabsArray = new FormArray( + this.interestSlabDefinitions.map( + () => + new FormGroup({ + interestRate: new FormControl(null, [ + Validators.min(0), + Validators.max(100) + ]) + }) + ) + ); + + this.fixedRateGroup = new FormGroup({ + hasFixed: new FormControl(false), + fixedAmount: new FormControl({ value: null, disabled: true }, [Validators.min(0)]) + }); + this.loanProductTermsForm = this.formBuilder.group({ isLinkedToFloatingInterestRates: [false], allowApprovedDisbursedAmountsOverApplied: [false], @@ -230,6 +281,15 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges { } }); + this.fixedRateGroup.get('hasFixed').valueChanges.subscribe((hasFixed: boolean) => { + if (hasFixed) { + this.fixedRateGroup.get('fixedAmount').enable(); + } else { + this.fixedRateGroup.get('fixedAmount').disable(); + this.fixedRateGroup.get('fixedAmount').setValue(null); + } + }); + this.zeroInterest.valueChanges.subscribe((zeroInterest) => { if (zeroInterest) { this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0); @@ -256,6 +316,18 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges { }); } + get slabGroups(): FormGroup[] { + return this.interestRateSlabsArray.controls as FormGroup[]; + } + + get activeSlabGroups(): FormGroup[] { + return this.interestRateSlabsArray.controls as FormGroup[]; + } + + get specialSlabGroups(): FormGroup[] { + return this.specialSlabsArray.controls as FormGroup[]; + } + get loanProductTerms() { const formValue = this.loanProductTermsForm.getRawValue(); // Normalize decimal separators: convert comma to dot for backend compatibility @@ -270,7 +342,16 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges { ...formValue, minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod), interestRatePerPeriod: normalizeDecimal(formValue.interestRatePerPeriod), - maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod) + maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod), + interestRateSlabs: this.interestRateSlabsArray.getRawValue().map((slab, i) => ({ + maxAmount: this.interestSlabDefinitions[i].maxAmount, + label: this.interestSlabDefinitions[i].label, + interestRate: slab.interestRate + })), + fixedRate: (() => { + const fr = this.fixedRateGroup.getRawValue(); + return fr.hasFixed ? fr.fixedAmount : null; + })() }; } diff --git a/src/app/products/products.service.ts b/src/app/products/products.service.ts index d2eada87c..d9a8e4745 100644 --- a/src/app/products/products.service.ts +++ b/src/app/products/products.service.ts @@ -564,6 +564,24 @@ export class ProductsService { return this.http.post('/fixeddepositproducts/baseinterestrates', payload); } + /** Returns loan interest rate categories. Pass activeOnly=true to get only active ones. */ + getLoanInterestRateCategories(activeOnly = false): Observable { + const url = activeOnly + ? '/loanproducts/interestratecategories?activeOnly=true' + : '/loanproducts/interestratecategories'; + return this.http.get(url); + } + + /** Returns the active loan interest rate slab config (grid: loan type x amount range). */ + getLoanIrSlabs(): Observable { + return this.http.get('/loanproducts/interestrateslabs'); + } + + /** Returns all loan interest rate slab config versions. */ + getAllLoanIrSlabs(): Observable { + return this.http.get('/loanproducts/interestrateslabs/all'); + } + /** * @returns {Observable} Collateral data. */ diff --git a/src/app/savings/create-savings-account/create-savings-account.component.ts b/src/app/savings/create-savings-account/create-savings-account.component.ts index 8cefe0623..5aa07d5f6 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.ts +++ b/src/app/savings/create-savings-account/create-savings-account.component.ts @@ -21,6 +21,7 @@ import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/ import { SavingsService } from '../savings.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { AlertService } from 'app/core/alert/alert.service'; import { MatStepper, MatStepperIcon, @@ -61,6 +62,7 @@ export class CreateSavingsAccountComponent { private dateUtils = inject(Dates); private savingsService = inject(SavingsService); private settingsService = inject(SettingsService); + private alertService = inject(AlertService); /** Savings Account Template */ savingsAccountTemplate: any; @@ -110,6 +112,14 @@ export class CreateSavingsAccountComponent { } submit() { + const detailsStep = this.savingsAccountDetailsStep; + if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index 28fb63a38..17cd616b0 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -22,6 +22,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; import { MatIcon } from '@angular/material/icon'; import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; +import { AlertService } from 'app/core/alert/alert.service'; /** * Savings Account Details Step @@ -48,6 +49,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit { private dateUtils = inject(Dates); private cdr = inject(ChangeDetectorRef); private dialog = inject(MatDialog); + private alertService = inject(AlertService); /** Savings Account Template */ @Input() savingsAccountTemplate: any; @@ -335,9 +337,13 @@ export class SavingsAccountDetailsStepComponent implements OnInit { }); ref.afterClosed().subscribe((result) => { if (result) { - const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + const isPrimary = this.primaryClientCif && this.primaryClientCif === result.clientCif; + const duplicate = isPrimary || this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); if (duplicate) { - alert(`Account holder with CIF ${result.clientCif} is already added.`); + this.alertService.alert({ + type: 'Error', + message: `Account holder with CIF ${result.clientCif} is already added.` + }); return; } this.jointHolders = [ @@ -446,7 +452,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit { value.guardianName = this.guardianName; value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; - value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'dd MMMM yyyy') : null; } return value; diff --git a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss index 4cb26e9bc..a67c277b0 100644 --- a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss +++ b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss @@ -290,15 +290,15 @@ $surface: #f8fafc; // ── Metric Cards ───────────────────────────────────────────────── &__cards { display: grid; - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(5, minmax(0, 1fr)); border-bottom: 1px solid $border; &--product { - grid-template-columns: repeat(6, 1fr); + grid-template-columns: repeat(6, minmax(0, 1fr)); } @media (width <= 1024px) { - grid-template-columns: repeat(3, 1fr) !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; .dap__card:nth-child(3n) { border-right: none; @@ -306,7 +306,7 @@ $surface: #f8fafc; } @media (width <= 600px) { - grid-template-columns: repeat(2, 1fr) !important; + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; .dap__card:nth-child(2n) { border-right: none; @@ -321,6 +321,7 @@ $surface: #f8fafc; flex-direction: column; gap: 5px; transition: background 0.15s; + min-width: 0; // allow text to wrap within grid cell &:last-child { border-right: none; @@ -340,6 +341,9 @@ $surface: #f8fafc; display: flex; align-items: center; gap: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &__card-hint { @@ -353,6 +357,7 @@ $surface: #f8fafc; font-size: 9px; color: $subtle; cursor: help; + flex-shrink: 0; &:hover { color: $c-low;