update loan product code and rd,fd,saving,current account validation add

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nitesh Yadav
2026-05-14 07:40:04 +05:30
co-authored by Claude Sonnet 4.6
parent ed45c8637e
commit d0370781e8
30 changed files with 1286 additions and 213 deletions
+2 -1
View File
@@ -4,5 +4,6 @@
"prettier-plugin-multiline-arrays" "prettier-plugin-multiline-arrays"
], ],
"singleQuote": true, "singleQuote": true,
"trailingComma": "none" "trailingComma": "none",
"endOfLine": "auto"
} }
@@ -65,7 +65,7 @@
<div class="section-header__left"> <div class="section-header__left">
<i class="fa fa-bank section-header__icon"></i> <i class="fa fa-bank section-header__icon"></i>
<h3 class="section-header__title">{{ 'labels.heading.Saving Accounts' | translate }}</h3> <h3 class="section-header__title">{{ 'labels.heading.Saving Accounts' | translate }}</h3>
<span class="section-header__badge">{{ savingAccounts?.length || 0 }}</span> <span class="section-header__badge">{{ pureSavingsCount }}</span>
</div> </div>
<div class="section-header__right"> <div class="section-header__right">
<button mat-raised-button color="primary" (click)="toggleSavingAccountsOverview()"> <button mat-raised-button color="primary" (click)="toggleSavingAccountsOverview()">
@@ -226,8 +226,20 @@ export class GeneralTabComponent implements OnDestroy {
clientAccountData: any; clientAccountData: any;
/** Loan Accounts Data */ /** Loan Accounts Data */
loanAccounts: any[] = []; loanAccounts: any[] = [];
/** Savings Accounts Data */ /** Savings Accounts Data (all: savings + FD + RD — filtered per section) */
savingAccounts: any[] = []; savingAccounts: any[] = [];
get pureSavingsCount(): number {
return this.savingAccounts.filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit').length;
}
get fixedDepositCount(): number {
return this.savingAccounts.filter((a: any) => a.depositType?.code === 'depositAccountType.fixedDeposit').length;
}
get recurringDepositCount(): number {
return this.savingAccounts.filter((a: any) => a.depositType?.code === 'depositAccountType.recurringDeposit').length;
}
/** Current Accounts Data */ /** Current Accounts Data */
currentAccounts: any[] = []; currentAccounts: any[] = [];
/** Shares Accounts Data */ /** Shares Accounts Data */
@@ -19,6 +19,7 @@ import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/c
import { CurrentAccountsService } from '../current-accounts.service'; import { CurrentAccountsService } from '../current-accounts.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
import { import {
MatStepper, MatStepper,
MatStepperIcon, MatStepperIcon,
@@ -60,6 +61,7 @@ export class CreateCurrentAccountComponent {
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private currentAccountsService = inject(CurrentAccountsService); private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Current Account Template */ /** Current Account Template */
currentAccountTemplate: any; currentAccountTemplate: any;
@@ -132,6 +134,14 @@ export class CreateCurrentAccountComponent {
* Creates a new current account. * Creates a new current account.
*/ */
submit() { submit() {
const detailsStep = this.currentAccountDetailsStep;
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 locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
@@ -155,8 +165,19 @@ export class CreateCurrentAccountComponent {
currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat); currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat);
} }
// strip UI-only flags not needed by backend // Explicitly re-set partners from the details step to ensure all are included
delete currentAccount.isNonIndividualClient; if (this.currentAccountDetailsStep.isNonIndividualClient) {
currentAccount.isNonIndividualClient = true;
currentAccount.partners = this.currentAccountDetailsStep.relatedClients.map((c: any) => ({
clientId: c.clientId,
clientName: c.clientName,
clientCif: c.clientCif,
sharePercentage: c.sharePercentage,
isMandatory: c.isMandatory
}));
} else {
delete currentAccount.isNonIndividualClient;
}
if (this.currentAccountTemplate.clientId) { if (this.currentAccountTemplate.clientId) {
currentAccount.clientId = this.currentAccountTemplate.clientId; currentAccount.clientId = this.currentAccountTemplate.clientId;
@@ -62,18 +62,32 @@
<mat-form-field appearance="outline" class="sad-field" (click)="establishmentDatePicker.open()"> <mat-form-field appearance="outline" class="sad-field" (click)="establishmentDatePicker.open()">
<mat-label>Establishment Date</mat-label> <mat-label>Establishment Date</mat-label>
<input matInput [matDatepicker]="establishmentDatePicker" formControlName="establishmentDate" /> <input matInput [matDatepicker]="establishmentDatePicker" formControlName="establishmentDate" [max]="today" />
<mat-datepicker-toggle matSuffix [for]="establishmentDatePicker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="establishmentDatePicker"></mat-datepicker-toggle>
<mat-datepicker #establishmentDatePicker></mat-datepicker> <mat-datepicker #establishmentDatePicker></mat-datepicker>
<mat-hint>Date of entity / business establishment</mat-hint> <mat-hint>Date of entity / business establishment</mat-hint>
@if (currentAccountDetailsForm.get('establishmentDate')?.hasError('matDatepickerMax')) {
<mat-error>Establishment Date cannot be a future date.</mat-error>
}
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="sad-field" (click)="startOperationDatePicker.open()"> <mat-form-field appearance="outline" class="sad-field" (click)="startOperationDatePicker.open()">
<mat-label>Start of Operation Date</mat-label> <mat-label>Start of Operation Date</mat-label>
<input matInput [matDatepicker]="startOperationDatePicker" formControlName="startOperationDate" /> <input
matInput
[matDatepicker]="startOperationDatePicker"
formControlName="startOperationDate"
[max]="today"
[min]="minStartOperationDate"
/>
<mat-datepicker-toggle matSuffix [for]="startOperationDatePicker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="startOperationDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startOperationDatePicker></mat-datepicker> <mat-datepicker #startOperationDatePicker></mat-datepicker>
<mat-hint>Date when operations commenced</mat-hint> <mat-hint>Date when operations commenced</mat-hint>
@if (currentAccountDetailsForm.get('startOperationDate')?.hasError('matDatepickerMax')) {
<mat-error>Start of Operation Date cannot be a future date.</mat-error>
} @else if (currentAccountDetailsForm.get('startOperationDate')?.hasError('matDatepickerMin')) {
<mat-error>Start of Operation Date cannot be before Establishment Date.</mat-error>
}
</mat-form-field> </mat-form-field>
} }
</div> </div>
@@ -22,6 +22,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add-related-client-dialog.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 * Current Account Details Step
@@ -46,6 +47,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private alertService = inject(AlertService);
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef); private cdr = inject(ChangeDetectorRef);
@@ -365,9 +367,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}); });
ref.afterClosed().subscribe((result) => { ref.afterClosed().subscribe((result) => {
if (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) { 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; return;
} }
this.jointHolders = [ this.jointHolders = [
@@ -407,6 +413,17 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; 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 { get majorityDate(): string {
const dob = this.minorDob ?? this.clientDob; const dob = this.minorDob ?? this.clientDob;
if (!dob) return ''; if (!dob) return '';
@@ -426,7 +443,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
if (result) { if (result) {
const duplicate = this.relatedClients.some((c) => c.clientCif && c.clientCif === result.clientCif); const duplicate = this.relatedClients.some((c) => c.clientCif && c.clientCif === result.clientCif);
if (duplicate) { 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; return;
} }
this.relatedClients = [ this.relatedClients = [
@@ -520,7 +537,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
value.guardianName = this.guardianName; value.guardianName = this.guardianName;
value.guardianCifNumber = this.guardianCif; value.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship; 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 // non-individual: partners are a separate dataset
@@ -649,54 +649,6 @@
</div> </div>
} }
<!-- ══ 7. Non-Individual Details (if applicable) ════════════════ -->
@if (currentAccount?.partners?.length > 0) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-building preview-section__icon"></i>
<span class="preview-section__title">Non-Individual Account Details</span>
</div>
<div class="preview-body">
<!-- Partners -->
<div class="preview-ni-label">Partners / Members</div>
<div class="preview-ni-table-wrap">
<table class="preview-ni-tbl">
<thead>
<tr>
<th>Partner Name</th>
<th>CIF No</th>
<th>Ownership %</th>
<th>Signing Authority</th>
</tr>
</thead>
<tbody>
@for (p of currentAccount.partners; track p.clientId) {
<tr>
<td>{{ p.clientName || '—' }}</td>
<td class="preview-mono">{{ p.clientCif || '—' }}</td>
<td>{{ p.sharePercentage }}%</td>
<td>
<span class="preview-badge" [class.preview-badge--yes]="p.isMandatory">
{{ p.isMandatory ? 'Mandatory' : 'Optional' }}
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Signatory Rights -->
@if (currentAccount?.signatoryRights) {
<div class="preview-ni-mode">
<i class="fas fa-how_to_vote"></i>
<span class="preview-ni-mode__label">Mode of Operation:</span>
<strong>{{ currentAccount.signatoryRights }}</strong>
</div>
}
</div>
</div>
}
<!-- ══ 8. Digital Facilities ════════════════════════════════════ --> <!-- ══ 8. Digital Facilities ════════════════════════════════════ -->
@if (hasDigitalSection) { @if (hasDigitalSection) {
<div class="preview-section"> <div class="preview-section">
@@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
/** Custom Services */ /** Custom Services */
import { FixedDepositsService } from '../fixed-deposits.service'; import { FixedDepositsService } from '../fixed-deposits.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { AlertService } from 'app/core/alert/alert.service';
/** Custom Components */ /** Custom Components */
import { FixedDepositAccountDetailsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component'; 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 dateUtils = inject(Dates);
private fixedDepositsService = inject(FixedDepositsService); private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
@ViewChild(FixedDepositAccountDetailsStepComponent, { static: true }) @ViewChild(FixedDepositAccountDetailsStepComponent, { static: true })
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent; fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
@@ -70,7 +72,14 @@ export class CreateFixedDepositAccountComponent {
/** Fixed Deposit Account Product Template */ /** Fixed Deposit Account Product Template */
fixedDepositsAccountProductTemplate: any; fixedDepositsAccountProductTemplate: any;
/** Tenure selected from the interest rate chart step */ /** 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` * Fetches FD account template from `resolve`
@@ -94,7 +103,14 @@ export class CreateFixedDepositAccountComponent {
this.fixedDepositsAccountProductTemplate = $event; 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; this.selectedTenure = $event;
} }
@@ -126,6 +142,14 @@ export class CreateFixedDepositAccountComponent {
* Submits the fixed deposit form and creates a new fixed deposit account * Submits the fixed deposit form and creates a new fixed deposit account
*/ */
submit() { 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 locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
@@ -178,7 +202,12 @@ export class CreateFixedDepositAccountComponent {
...this.fixedDepositAccount, ...this.fixedDepositAccount,
...terms, ...terms,
...settings, ...settings,
...(this.selectedTenure ?? {}), ...(this.selectedTenure
? {
depositPeriod: this.selectedTenure.depositPeriod,
depositPeriodFrequencyId: this.selectedTenure.depositPeriodFrequencyId
}
: {}),
clientId: this.fixedDepositsAccountTemplate.clientId, clientId: this.fixedDepositsAccountTemplate.clientId,
charges: rawCharges.map((charge: any) => ({ charges: rawCharges.map((charge: any) => ({
chargeId: charge.chargeId ?? charge.id, chargeId: charge.chargeId ?? charge.id,
@@ -22,6 +22,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { MatIcon } from '@angular/material/icon'; 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 { 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 * Fixed Deposits Account Details Step
@@ -48,6 +49,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
private cdr = inject(ChangeDetectorRef); private cdr = inject(ChangeDetectorRef);
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private alertService = inject(AlertService);
/** Fixed Deposits Account Template */ /** Fixed Deposits Account Template */
@Input() fixedDepositsAccountTemplate: any; @Input() fixedDepositsAccountTemplate: any;
@@ -221,15 +223,17 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
this.dalLoading = true; this.dalLoading = true;
this.clientsService.getClientAccountData(clientId).subscribe({ this.clientsService.getClientAccountData(clientId).subscribe({
next: (data: any) => { next: (data: any) => {
this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({ this.dalAllSavings = (data?.savingsAccounts ?? [])
id: a.id, .filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit')
accountNo: a.accountNo, .map((a: any) => ({
productName: a.productName ?? '', id: a.id,
accountType: 'SAVINGS' as const, accountNo: a.accountNo,
status: a.status?.value ?? '', productName: a.productName ?? '',
balance: a.accountBalance ?? 0, accountType: 'SAVINGS' as const,
currencyCode: a.currency?.code ?? '' status: a.status?.value ?? '',
})); balance: a.accountBalance ?? 0,
currencyCode: a.currency?.code ?? ''
}));
this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({
id: a.id, id: a.id,
accountNo: a.accountNo, accountNo: a.accountNo,
@@ -469,9 +473,13 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
}); });
ref.afterClosed().subscribe((result) => { ref.afterClosed().subscribe((result) => {
if (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) { 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; return;
} }
this.jointHolders = [ this.jointHolders = [
@@ -582,7 +590,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit {
value.guardianName = this.guardianName; value.guardianName = this.guardianName;
value.guardianCifNumber = this.guardianCif; value.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship; 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) { if (this.selectedLinkedAccount) {
@@ -76,6 +76,9 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
depositPeriod: number; depositPeriod: number;
depositPeriodFrequencyId: number; depositPeriodFrequencyId: number;
depositAmount?: number; depositAmount?: number;
maturityAmount?: number;
interestEarned?: number;
appliedRate?: number;
}>(); }>();
interestRateChartData: any[] = []; interestRateChartData: any[] = [];
@@ -254,13 +257,16 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan
this.matchResult = result; 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); const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit);
if (frequencyId !== null) { if (frequencyId !== null) {
this.tenureSelected.emit({ this.tenureSelected.emit({
depositPeriod: this.tenureValue, depositPeriod: this.tenureValue,
depositPeriodFrequencyId: frequencyId, depositPeriodFrequencyId: frequencyId,
depositAmount: this.principalAmount || undefined depositAmount: this.principalAmount || undefined,
maturityAmount: result.maturityAmount,
interestEarned: result.interestEarned,
appliedRate: result.appliedRate
}); });
} }
} }
@@ -95,7 +95,7 @@
</div> </div>
} }
<div class="preview-field"> <div class="preview-field">
<span class="preview-label">Share Linking Enabled</span> <span class="preview-label">Deposit Account Link</span>
<span <span
class="preview-value preview-badge" class="preview-value preview-badge"
[class.preview-badge--yes]="fixedDepositsAccountProductTemplate?.shareLinkingEnabled" [class.preview-badge--yes]="fixedDepositsAccountProductTemplate?.shareLinkingEnabled"
@@ -103,17 +103,6 @@
{{ fixedDepositsAccountProductTemplate?.shareLinkingEnabled ? 'Yes' : 'No' }} {{ fixedDepositsAccountProductTemplate?.shareLinkingEnabled ? 'Yes' : 'No' }}
</span> </span>
</div> </div>
@if (fixedDepositsAccountProductTemplate?.introducerRequired !== undefined) {
<div class="preview-field">
<span class="preview-label">Introducer Required</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="fixedDepositsAccountProductTemplate?.introducerRequired"
>
{{ fixedDepositsAccountProductTemplate?.introducerRequired ? 'Yes' : 'No' }}
</span>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -316,12 +305,19 @@
</div> </div>
<!-- ══ 2b. Dormancy Monitoring ═══════════════════════════════════ --> <!-- ══ 2b. Dormancy Monitoring ═══════════════════════════════════ -->
@if (fixedDepositsAccountProductTemplate?.dormancyAlert) { @if (
fixedDepositsAccountProductTemplate?.dormancyAlert || fixedDepositsAccountProductTemplate?.isDormancyTrackingActive
) {
<div class="preview-section"> <div class="preview-section">
<div class="preview-section__header"> <div class="preview-section__header">
<i class="fas fa-moon preview-section__icon"></i> <i class="fas fa-moon preview-section__icon"></i>
<span class="preview-section__title">Dormancy Monitoring</span> <span class="preview-section__title">Dormancy Monitoring</span>
<span class="preview-section__badge preview-section__badge--active">Monitoring: Active</span> <span
class="preview-section__badge"
[class.preview-section__badge--active]="fixedDepositsAccountProductTemplate?.isDormancyTrackingActive"
>
{{ fixedDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
</span>
</div> </div>
<div class="preview-body"> <div class="preview-body">
<div class="preview-dormancy-timeline"> <div class="preview-dormancy-timeline">
@@ -330,7 +326,11 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Inactive</span> <span class="preview-dorm-step__label">Inactive</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
After {{ fixedDepositsAccountProductTemplate?.daysToInactiveFromProduct || 30 }} days of no transaction @if (fixedDepositsAccountProductTemplate?.daysToInactive != null) {
After {{ fixedDepositsAccountProductTemplate.daysToInactive }} days of no transaction
} @else {
Not configured
}
</span> </span>
</div> </div>
</div> </div>
@@ -340,7 +340,11 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Dormant</span> <span class="preview-dorm-step__label">Dormant</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
{{ fixedDepositsAccountProductTemplate?.daysToDormancyFromProduct || 60 }} days after inactive @if (fixedDepositsAccountProductTemplate?.daysToDormancy != null) {
{{ fixedDepositsAccountProductTemplate.daysToDormancy }} days after inactive
} @else {
Not configured
}
</span> </span>
</div> </div>
</div> </div>
@@ -350,11 +354,31 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Escheat</span> <span class="preview-dorm-step__label">Escheat</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
{{ fixedDepositsAccountProductTemplate?.daysToEscheatFromProduct || 90 }} days after dormant @if (fixedDepositsAccountProductTemplate?.daysToEscheat != null) {
{{ fixedDepositsAccountProductTemplate.daysToEscheat }} days after dormant
} @else {
Not configured
}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="preview-dorm-flags">
<span
class="preview-dorm-flag"
[class.preview-dorm-flag--on]="fixedDepositsAccountProductTemplate?.dormancyAlert"
>
<i class="fas fa-bell"></i> Alert
{{ fixedDepositsAccountProductTemplate?.dormancyAlert ? 'Enabled' : 'Disabled' }}
</span>
<span
class="preview-dorm-flag"
[class.preview-dorm-flag--on]="fixedDepositsAccountProductTemplate?.isDormancyTrackingActive"
>
<i class="fas fa-eye"></i> Tracking
{{ fixedDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
</span>
</div>
</div> </div>
</div> </div>
} }
@@ -423,31 +423,25 @@
.preview-dormancy-timeline { .preview-dormancy-timeline {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 12px; margin-bottom: 14px;
margin: 1.25rem 0;
overflow-x: auto;
padding-bottom: 8px;
} }
.preview-dorm-step { .preview-dorm-step {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
flex: 0 0 140px; flex: 1;
gap: 10px; gap: 6px;
&__dot { &__dot {
width: 32px; width: 36px;
height: 32px; height: 36px;
border-radius: 50%; border-radius: 50%;
border: 3px solid;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 3px solid #e2e8f0; flex-shrink: 0;
background: #f8fafc;
position: relative;
box-shadow: 0 0 0 4px #fff;
z-index: 2;
&--inactive { &--inactive {
border-color: #f59e0b; border-color: #f59e0b;
@@ -517,3 +511,32 @@
margin-top: 16px; margin-top: 16px;
border-radius: 2px; 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;
}
}
@@ -76,9 +76,15 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
@Input() fixedDepositsAccountProductTemplate: any; @Input() fixedDepositsAccountProductTemplate: any;
/** Fixed Deposits Account Data */ /** Fixed Deposits Account Data */
@Input() fixedDepositAccountData: any; @Input() fixedDepositAccountData: any;
/** Tenure selected from the interest rate chart step */ /** Tenure selected from the interest rate chart step (includes pre-calculated maturity if user used calculator) */
@Input() selectedTenure: { depositPeriod: number; depositPeriodFrequencyId: number; depositAmount?: number } | null = @Input() selectedTenure: {
null; depositPeriod: number;
depositPeriodFrequencyId: number;
depositAmount?: number;
maturityAmount?: number;
interestEarned?: number;
appliedRate?: number;
} | null = null;
/** Charges Displayed Columns */ /** Charges Displayed Columns */
chargesDisplayedColumns: string[] = [ chargesDisplayedColumns: string[] = [
@@ -237,6 +243,20 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
this.fixedDepositsAccountProductTemplate?.depositAmount ?? this.fixedDepositsAccountProductTemplate?.depositAmount ??
0; 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 = const chartRate =
this.matchedSlabIndex >= 0 ? this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate : null; this.matchedSlabIndex >= 0 ? this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate : null;
const interestRate = chartRate ?? this.fixedDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? 0; const interestRate = chartRate ?? this.fixedDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? 0;
@@ -248,20 +268,15 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365; this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365;
if (depositPeriodFrequencyId === 0) { if (depositPeriodFrequencyId === 0) {
// Days
t = depositPeriod / daysInYear; t = depositPeriod / daysInYear;
} else if (depositPeriodFrequencyId === 1) { } else if (depositPeriodFrequencyId === 1) {
// Weeks
t = (depositPeriod * 7) / daysInYear; t = (depositPeriod * 7) / daysInYear;
} else if (depositPeriodFrequencyId === 2) { } else if (depositPeriodFrequencyId === 2) {
// Months
t = depositPeriod / 12; t = depositPeriod / 12;
} else if (depositPeriodFrequencyId === 3) { } else if (depositPeriodFrequencyId === 3) {
// Years
t = depositPeriod; t = depositPeriod;
} }
// Compounding frequency (n)
const compPeriodValue = ( const compPeriodValue = (
this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.value || '' this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.value || ''
).toLowerCase(); ).toLowerCase();
@@ -273,7 +288,6 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
else if (compPeriodValue.includes('biannual')) n = 2; else if (compPeriodValue.includes('biannual')) n = 2;
else if (compPeriodValue.includes('annual')) n = 1; else if (compPeriodValue.includes('annual')) n = 1;
// Interest Calculation Type
const calcTypeValue = ( const calcTypeValue = (
this.fixedDepositsAccountProductTemplate?.interestCalculationType?.value || '' this.fixedDepositsAccountProductTemplate?.interestCalculationType?.value || ''
).toLowerCase(); ).toLowerCase();
@@ -283,7 +297,6 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges {
if (isSimple) { if (isSimple) {
maturityAmount = depositAmount + depositAmount * r * t; maturityAmount = depositAmount + depositAmount * r * t;
} else { } else {
// Compound interest: A = P(1 + r/n)^(nt)
maturityAmount = depositAmount * Math.pow(1 + r / n, n * t); maturityAmount = depositAmount * Math.pow(1 + r / n, n * t);
} }
@@ -14,6 +14,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { RecurringDepositsService } from '../recurring-deposits.service'; import { RecurringDepositsService } from '../recurring-deposits.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { ProductsService } from 'app/products/products.service'; import { ProductsService } from 'app/products/products.service';
import { AlertService } from 'app/core/alert/alert.service';
/** Step Components */ /** Step Components */
import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; 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 recurringDepositsService = inject(RecurringDepositsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private productsService = inject(ProductsService); private productsService = inject(ProductsService);
private alertService = inject(AlertService);
@ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true })
recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent; recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent;
@@ -157,6 +159,14 @@ export class CreateRecurringDepositsAccountComponent {
* Submits the recurring deposit form to create a new recurring deposit account * Submits the recurring deposit form to create a new recurring deposit account
*/ */
submit() { 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 locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
@@ -22,6 +22,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { MatIcon } from '@angular/material/icon'; 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 { 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 * Recurring Deposits Account Details Step
@@ -48,6 +49,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
private cdr = inject(ChangeDetectorRef); private cdr = inject(ChangeDetectorRef);
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private alertService = inject(AlertService);
/** Recurring Deposits Account Template */ /** Recurring Deposits Account Template */
@Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountTemplate: any;
@@ -198,15 +200,17 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.dalLoading = true; this.dalLoading = true;
this.clientsService.getClientAccountData(clientId).subscribe({ this.clientsService.getClientAccountData(clientId).subscribe({
next: (data: any) => { next: (data: any) => {
this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({ this.dalAllSavings = (data?.savingsAccounts ?? [])
id: a.id, .filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit')
accountNo: a.accountNo, .map((a: any) => ({
productName: a.productName ?? '', id: a.id,
accountType: 'SAVINGS' as const, accountNo: a.accountNo,
status: a.status?.value ?? '', productName: a.productName ?? '',
balance: a.accountBalance ?? 0, accountType: 'SAVINGS' as const,
currencyCode: a.currency?.code ?? '' status: a.status?.value ?? '',
})); balance: a.accountBalance ?? 0,
currencyCode: a.currency?.code ?? ''
}));
this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({
id: a.id, id: a.id,
accountNo: a.accountNo, accountNo: a.accountNo,
@@ -430,9 +434,13 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
}); });
ref.afterClosed().subscribe((result) => { ref.afterClosed().subscribe((result) => {
if (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) { 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; return;
} }
this.jointHolders = [ this.jointHolders = [
@@ -540,7 +548,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
value.guardianName = this.guardianName; value.guardianName = this.guardianName;
value.guardianCifNumber = this.guardianCif; value.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship; 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) { if (this.selectedLinkedAccount) {
@@ -463,7 +463,7 @@
<!-- ══ 5. Dormancy Monitoring ════════════════════════════════════ --> <!-- ══ 5. Dormancy Monitoring ════════════════════════════════════ -->
@if ( @if (
recurringDepositsAccountProductTemplate?.dormancyAlert || recurringDepositsAccountProductTemplate?.dormancyAlert ||
recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct recurringDepositsAccountProductTemplate?.isDormancyTrackingActive
) { ) {
<div class="preview-section"> <div class="preview-section">
<div class="preview-section__header"> <div class="preview-section__header">
@@ -471,11 +471,9 @@
<span class="preview-section__title">Dormancy Monitoring</span> <span class="preview-section__title">Dormancy Monitoring</span>
<span <span
class="preview-section__badge" class="preview-section__badge"
[class.preview-section__badge--active]=" [class.preview-section__badge--active]="recurringDepositsAccountProductTemplate?.isDormancyTrackingActive"
recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct
"
> >
{{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
</span> </span>
</div> </div>
<div class="preview-body"> <div class="preview-body">
@@ -485,8 +483,8 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Inactive</span> <span class="preview-dorm-step__label">Inactive</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct !== null) { @if (recurringDepositsAccountProductTemplate?.daysToInactive != null) {
After {{ recurringDepositsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction After {{ recurringDepositsAccountProductTemplate.daysToInactive }} days of no transaction
} @else { } @else {
Not configured Not configured
} }
@@ -499,8 +497,8 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Dormant</span> <span class="preview-dorm-step__label">Dormant</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct !== null) { @if (recurringDepositsAccountProductTemplate?.daysToDormancy != null) {
{{ recurringDepositsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive {{ recurringDepositsAccountProductTemplate.daysToDormancy }} days after inactive
} @else { } @else {
Not configured Not configured
} }
@@ -513,8 +511,8 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Escheat</span> <span class="preview-dorm-step__label">Escheat</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct !== null) { @if (recurringDepositsAccountProductTemplate?.daysToEscheat != null) {
{{ recurringDepositsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant {{ recurringDepositsAccountProductTemplate.daysToEscheat }} days after dormant
} @else { } @else {
Not configured Not configured
} }
@@ -532,10 +530,10 @@
</span> </span>
<span <span
class="preview-dorm-flag" class="preview-dorm-flag"
[class.preview-dorm-flag--on]="recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct" [class.preview-dorm-flag--on]="recurringDepositsAccountProductTemplate?.isDormancyTrackingActive"
> >
<i class="fas fa-eye"></i> Tracking <i class="fas fa-eye"></i> Tracking
{{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
</span> </span>
</div> </div>
</div> </div>
@@ -9,7 +9,7 @@
$primary: #1353a0; $primary: #1353a0;
.lp-container { .lp-container {
max-width: 960px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 24px 16px; padding: 24px 16px;
} }
@@ -53,11 +53,11 @@ $primary: #1353a0;
} }
.mat-mdc-tab-body { .mat-mdc-tab-body {
overflow: visible !important; overflow: hidden visible !important;
} }
.mat-mdc-tab-body-content { .mat-mdc-tab-body-content {
overflow: visible !important; overflow: hidden visible !important;
} }
} }
} }
@@ -69,6 +69,8 @@ $primary: #1353a0;
.lp-tab-content { .lp-tab-content {
padding: 24px 16px; padding: 24px 16px;
min-width: 0;
overflow-x: hidden;
} }
.lp-action-bar { .lp-action-bar {
@@ -90,4 +90,34 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<!-- Product Classification + GL Account — combined section -->
<div class="lpd-section">
<div class="lpd-section__header">
<i class="fas fa-sitemap"></i>
<h3>Product Classification</h3>
</div>
<div class="lpd-grid">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Product Type Group</mat-label>
<mat-select formControlName="productTypeGroup">
@for (group of productTypeGroups; track group.id) {
<mat-option [value]="group.id">{{ group.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (loanProductDetailsForm.get('productTypeGroup')?.value) {
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>GL Account ({{ glClassificationLabel }})</mat-label>
<mat-select formControlName="glAccount">
@for (acc of availableGlAccounts; track acc.id) {
<mat-option [value]="acc.id"> ({{ acc.glCode }}) {{ acc.name }} </mat-option>
}
</mat-select>
<mat-error>GL Account is required</mat-error>
</mat-form-field>
}
</div>
</div>
</form> </form>
@@ -68,3 +68,140 @@ $gray-900: #202124;
align-items: center; align-items: center;
padding: 4px 0; 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;
}
}
@@ -57,6 +57,68 @@ export class LoanProductDetailsStepComponent implements OnInit {
startDate: this.loanProductsTemplate.startDate && new Date(this.loanProductsTemplate.startDate), startDate: this.loanProductsTemplate.startDate && new Date(this.loanProductsTemplate.startDate),
closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate) 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<string, { id: string; label: string }[]> = {
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<string, string> = {
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() { createLoanProductDetailsForm() {
@@ -71,7 +133,18 @@ export class LoanProductDetailsStepComponent implements OnInit {
], ],
description: [''], description: [''],
startDate: [''], 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('');
}); });
} }
@@ -467,7 +467,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Against Interest Calculation Enabled</mat-label> <mat-label>Against Interest Calculation Enabled</mat-label>
<mat-select formControlName="againstInterestCalculationEnabled"> <mat-select formControlName="againstInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -499,7 +499,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-3">
<mat-label>Consider Debit Closing Amount For Against Clearing</mat-label> <mat-label>Consider Debit Closing Amount For Against Clearing</mat-label>
<mat-select formControlName="considerDebitClosingAmountForAgainstClearing"> <mat-select formControlName="considerDebitClosingAmountForAgainstClearing">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -517,7 +517,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penalty Interest Calculation Enabled</mat-label> <mat-label>Penalty Interest Calculation Enabled</mat-label>
<mat-select formControlName="penaltyInterestCalculationEnabled"> <mat-select formControlName="penaltyInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -525,7 +525,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penalty Interest Calculation Method</mat-label> <mat-label>Penalty Interest Calculation Method</mat-label>
<mat-select formControlName="penaltyInterestCalculationMethod"> <mat-select formControlName="penaltyInterestCalculationMethod">
<mat-option value="Daywise ( 36500 )">Daywise ( 36500 )</mat-option> <mat-option value="Daywise ( 36500 )">Daywise ( 36500 )</mat-option>
@@ -559,7 +559,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Interest On Credit Balance</mat-label> <mat-label>Interest On Credit Balance</mat-label>
<mat-select formControlName="interestOnCreditBalance"> <mat-select formControlName="interestOnCreditBalance">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -567,7 +567,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penalty Interest Date Different Than Regular Interest</mat-label> <mat-label>Penalty Interest Date Different Than Regular Interest</mat-label>
<mat-select formControlName="penaltyInterestDateDifferentThanRegularInterest"> <mat-select formControlName="penaltyInterestDateDifferentThanRegularInterest">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -575,7 +575,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Fixed Deposit Overdraft Certificate Sort Order</mat-label> <mat-label>Fixed Deposit Overdraft Certificate Sort Order</mat-label>
<mat-select formControlName="fixedDepositOverdraftCertificateSortOrder"> <mat-select formControlName="fixedDepositOverdraftCertificateSortOrder">
<mat-option value="ORDER ON INTEREST RATE">Order on Interest Rate</mat-option> <mat-option value="ORDER ON INTEREST RATE">Order on Interest Rate</mat-option>
@@ -599,12 +599,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Normal Interest Amount</mat-label> <mat-label>Minimum Normal Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumNormalInterestAmount" /> <input type="number" matInput formControlName="minimumNormalInterestAmount" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Interest Calculation Method</mat-label> <mat-label>Minimum Interest Calculation Method</mat-label>
<mat-select formControlName="minimumInterestCalculationMethod"> <mat-select formControlName="minimumInterestCalculationMethod">
<mat-option value="Flat Amount">Flat Amount</mat-option> <mat-option value="Flat Amount">Flat Amount</mat-option>
@@ -613,12 +613,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Against Clearing Interest Amount</mat-label> <mat-label>Minimum Against Clearing Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumAgainstClearingInterestAmount" /> <input type="number" matInput formControlName="minimumAgainstClearingInterestAmount" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Limit Increase Upto Interest Amount In Interest Calculation</mat-label> <mat-label>Limit Increase Upto Interest Amount In Interest Calculation</mat-label>
<mat-select formControlName="limitIncreaseUptoInterestAmountInInterestCalculation"> <mat-select formControlName="limitIncreaseUptoInterestAmountInInterestCalculation">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -626,22 +626,22 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Penalty Interest Amount</mat-label> <mat-label>Minimum Penalty Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumPenaltyInterestAmount" /> <input type="number" matInput formControlName="minimumPenaltyInterestAmount" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Days For Limit Increase Upto Interest Amount</mat-label> <mat-label>Days For Limit Increase Upto Interest Amount</mat-label>
<input type="number" matInput formControlName="daysForLimitIncreaseUptoInterestAmount" /> <input type="number" matInput formControlName="daysForLimitIncreaseUptoInterestAmount" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Additional Interest Rate For Fixed Deposit Overdraft</mat-label> <mat-label>Additional Interest Rate For Fixed Deposit Overdraft</mat-label>
<input type="number" matInput formControlName="additionalInterestRateForFixedDepositOverdraft" /> <input type="number" matInput formControlName="additionalInterestRateForFixedDepositOverdraft" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Consider DD/Payorder In Against Clearing Interest Calculation</mat-label> <mat-label>Consider DD/Payorder In Against Clearing Interest Calculation</mat-label>
<mat-select formControlName="considerDdPayorderInAgainstClearingInterestCalculation"> <mat-select formControlName="considerDdPayorderInAgainstClearingInterestCalculation">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -649,12 +649,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Grace Days After Renewal Date</mat-label> <mat-label>Grace Days After Renewal Date</mat-label>
<input type="number" matInput formControlName="graceDaysAfterRenewDate" /> <input type="number" matInput formControlName="graceDaysAfterRenewDate" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Apply Undebited Interest Concept Enabled</mat-label> <mat-label>Apply Undebited Interest Concept Enabled</mat-label>
<mat-select formControlName="applyUndebitedInterestConceptEnabled"> <mat-select formControlName="applyUndebitedInterestConceptEnabled">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -671,12 +671,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Normal Grace Days After Overdue</mat-label> <mat-label>Normal Grace Days After Overdue</mat-label>
<input type="number" matInput formControlName="normalGraceDaysAfterOverdue" /> <input type="number" matInput formControlName="normalGraceDaysAfterOverdue" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Allow Against Clearing Entry</mat-label> <mat-label>Allow Against Clearing Entry</mat-label>
<mat-select formControlName="allowAgainstClearingEntry"> <mat-select formControlName="allowAgainstClearingEntry">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -684,7 +684,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Allow No Balance Entry</mat-label> <mat-label>Allow No Balance Entry</mat-label>
<mat-select formControlName="allowNoBalanceEntry"> <mat-select formControlName="allowNoBalanceEntry">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -692,7 +692,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Fixed Deposit Overdraft Penalty Interest Combined With Normal Interest</mat-label> <mat-label>Fixed Deposit Overdraft Penalty Interest Combined With Normal Interest</mat-label>
<mat-select formControlName="fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest"> <mat-select formControlName="fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -700,7 +700,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Allow No Balance Entry With Password</mat-label> <mat-label>Allow No Balance Entry With Password</mat-label>
<mat-select formControlName="allowNoBalanceEntryWithPassword"> <mat-select formControlName="allowNoBalanceEntryWithPassword">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -708,7 +708,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Undebited Simple Interest Account Wise</mat-label> <mat-label>Undebited Simple Interest Account Wise</mat-label>
<mat-select formControlName="undebitedSimpleInterestAccountWise"> <mat-select formControlName="undebitedSimpleInterestAccountWise">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -716,7 +716,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Daily Interest Calculation Enabled</mat-label> <mat-label>Daily Interest Calculation Enabled</mat-label>
<mat-select formControlName="dailyInterestCalculationEnabled"> <mat-select formControlName="dailyInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -724,12 +724,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Daily Interest Calculation Start Date</mat-label> <mat-label>Daily Interest Calculation Start Date</mat-label>
<input type="text" matInput formControlName="dailyInterestCalculationStartDate" /> <input type="text" matInput formControlName="dailyInterestCalculationStartDate" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Non Performing Asset Penal Interest Rate Update</mat-label> <mat-label>Non Performing Asset Penal Interest Rate Update</mat-label>
<mat-select formControlName="nonPerformingAssetPenalInterestRateUpdate"> <mat-select formControlName="nonPerformingAssetPenalInterestRateUpdate">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -737,12 +737,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Non Performing Asset Penal Interest Rate</mat-label> <mat-label>Non Performing Asset Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="nonPerformingAssetPenalInterestRate" /> <input type="number" matInput formControlName="nonPerformingAssetPenalInterestRate" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Fixed Deposit Overdraft Penal Interest Rate Update In Branch Process</mat-label> <mat-label>Fixed Deposit Overdraft Penal Interest Rate Update In Branch Process</mat-label>
<mat-select formControlName="fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess"> <mat-select formControlName="fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess">
<mat-option value="YES">YES</mat-option> <mat-option value="YES">YES</mat-option>
@@ -750,13 +750,116 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Add Penal Interest Rate</mat-label> <mat-label>Add Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="addPenalInterestRate" /> <input type="number" matInput formControlName="addPenalInterestRate" />
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<!-- RBI Compliance & KFS Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-balance-scale lps-section__icon"></i>
<h3 class="lps-section__title">RBI Compliance &amp; KFS Settings</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penal Charge Method</mat-label>
<mat-select formControlName="penalChargeMethod">
<mat-option value="RBI COMPLIANT PENAL CHARGES">RBI Compliant Penal Charges (Non-Compounding)</mat-option>
<mat-option value="PENAL INTEREST">Legacy Penal Interest (Compounding)</mat-option>
</mat-select>
</mat-form-field>
<div class="lps-checkbox-row lps-span-2">
<mat-checkbox labelPosition="before" formControlName="isFloatingRate">
Is Floating Rate Loan? (Individual)
</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Foreclosure Charges Applicable</mat-label>
<mat-select formControlName="foreclosureChargesApplicable">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
<mat-hint>RBI: No charges for Floating Rate Loans to Individuals</mat-hint>
</mat-form-field>
<div class="lps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="keyFactStatementEnabled">
Enable Key Fact Statement (KFS)
</mat-checkbox>
</div>
<div class="lps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="annualPercentageRateCalculationEnabled">
Calculate APR
</mat-checkbox>
</div>
</div>
</div>
<!-- Credit Bureau Settings (CIBIL) -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-file-invoice lps-section__icon"></i>
<h3 class="lps-section__title">Credit Bureau Settings (CIBIL/Highmark)</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Loan Product Category (RBI Reporting)</mat-label>
<mat-select formControlName="loanProductCategory">
<mat-option value="PERSONAL_LOAN">Personal/Consumer Loan (Code 02)</mat-option>
<mat-option value="HOUSING_LOAN">Housing/Mortgage Loan (Code 15)</mat-option>
<mat-option value="BUSINESS_LOAN">Business/Commercial Loan (Code 50)</mat-option>
<mat-option value="VEHICLE_LOAN">Vehicle/Auto Loan (Code 04)</mat-option>
</mat-select>
<mat-hint>Selecting a category will auto-fill the credit bureau account types below.</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>CIBIL Account Type</mat-label>
<input type="text" matInput formControlName="cibilAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>CIBIL Commercial Account Type</mat-label>
<input type="text" matInput formControlName="cibilCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Highmark Account Type</mat-label>
<input type="text" matInput formControlName="highmarkAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Highmark Commercial Account Type</mat-label>
<input type="text" matInput formControlName="highmarkCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Experian Account Type</mat-label>
<input type="text" matInput formControlName="experianAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Experian Commercial Account Type</mat-label>
<input type="text" matInput formControlName="experianCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Equifax Account Type</mat-label>
<input type="text" matInput formControlName="equifaxAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Equifax Commercial Account Type</mat-label>
<input type="text" matInput formControlName="equifaxCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
</div>
</div>
<!-- Moratorium & Grace --> <!-- Moratorium & Grace -->
<div class="lps-section"> <div class="lps-section">
<div class="lps-section__header"> <div class="lps-section__header">
@@ -75,6 +75,14 @@
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.lps-span-2 {
grid-column: span 2;
}
.lps-span-3 {
grid-column: span 3;
}
.lps-checkbox-row { .lps-checkbox-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -408,13 +408,68 @@ export class LoanProductSettingsStepComponent implements OnInit {
nonPerformingAssetPenalInterestRateUpdate: ['YES'], nonPerformingAssetPenalInterestRateUpdate: ['YES'],
nonPerformingAssetPenalInterestRate: [''], nonPerformingAssetPenalInterestRate: [''],
fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'], 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() { setConditionalControls() {
const allowAttributeOverrides = this.loanProductSettingsForm.get('allowAttributeOverrides'); 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) => { this.loanProductSettingsForm.get('daysInYearType').valueChanges.subscribe((daysInYearType: any) => {
if (this.isAdvancedTransactionProcessingStrategy) { if (this.isAdvancedTransactionProcessingStrategy) {
this.useDaysInYearCustomStrategy = daysInYearType == 1; this.useDaysInYearCustomStrategy = daysInYearType == 1;
@@ -264,6 +264,93 @@
} }
</div> </div>
<!-- Interest Rate Slabs Section -->
<div class="lpt-section">
<div class="lpt-section-header">
<i class="fas fa-table lpt-section-icon"></i>
<h3>Interest Rate Slabs</h3>
</div>
<div class="lpt-slab-c">
<!-- Particular Name -->
<div class="lpt-slab-c-particular">
<label class="lpt-slab-c-particular-label">Particular</label>
<input
type="text"
class="lpt-slab-c-name-input"
[formControl]="particularLabel"
placeholder="e.g. Consumer Loan"
/>
</div>
<!-- 2-column slab grid (same as before, General + Special inside each card) -->
<div class="lpt-slab-c-grid">
@for (genGroup of slabGroups; track $index) {
<div class="lpt-slab-c-item">
<span class="lpt-slab-c-range">{{ interestSlabDefinitions[$index].label }}</span>
<div class="lpt-slab-c-rate-pair">
<!-- General -->
<div class="lpt-slab-c-rate-field" [formGroup]="genGroup">
<span class="lpt-slab-c-rate-label">Gen</span>
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input"
formControlName="interestRate"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
</div>
<!-- Special -->
<div class="lpt-slab-c-rate-field" [formGroup]="specialSlabGroups[$index]">
<span class="lpt-slab-c-rate-label lpt-slab-c-rate-label--special">Spl</span>
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input lpt-slab-c-input--special"
formControlName="interestRate"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
</div>
</div>
</div>
}
</div>
<!-- Fixed Interest Rate row -->
<div class="lpt-slab-c-fixed" [formGroup]="fixedRateGroup">
<label class="lpt-slab-c-fixed-toggle">
<input type="checkbox" formControlName="hasFixed" class="lpt-slab-c-checkbox" />
<i class="fas fa-lock"></i>
<span>Fixed Interest Rate (%)</span>
</label>
@if (fixedRateGroup.get('hasFixed').value) {
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input lpt-slab-c-input--fixed"
formControlName="fixedAmount"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
}
</div>
</div>
</div>
<!-- Repayment Schedule Section --> <!-- Repayment Schedule Section -->
<div class="lpt-section"> <div class="lpt-section">
<div class="lpt-section-header"> <div class="lpt-section-header">
@@ -6,14 +6,42 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { .lpt-section {
border: 1px solid #e2e8f0; background: $surface;
border-radius: 10px; border: 1px solid $border;
overflow: hidden; border-radius: $radius-card;
background: #fff; 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; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 14px 24px;
background: #f8f9fa; background: linear-gradient(135deg, $surface-alt 0%, #f1f5f9 100%);
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $border;
border-radius: $radius-card $radius-card 0 0;
h3 { h3 {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 700;
color: #202124; color: $text-primary;
margin: 0; margin: 0;
letter-spacing: 0.01em;
text-transform: uppercase;
} }
} }
.lpt-section-icon { .lpt-section-icon {
color: #0ea5e9; color: $primary;
font-size: 15px; 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 { .lpt-help-icon {
color: #9aa0a6; color: $text-secondary;
font-size: 13px; font-size: 13px;
cursor: help; cursor: help;
margin-left: auto;
transition: color 0.15s;
&:hover {
color: $primary;
}
} }
// ─── Grid layout ──────────────────────────────────────────────────────────────
.lpt-grid { .lpt-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; gap: 8px 20px;
padding: 16px 24px; padding: 20px 24px;
@media (width <= 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -58,6 +104,10 @@
.lpt-grid-3 { .lpt-grid-3 {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@media (width <= 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -79,28 +129,42 @@
grid-column: 1 / -1; grid-column: 1 / -1;
} }
// ─── Checkbox rows ────────────────────────────────────────────────────────────
.lpt-checkbox-row { .lpt-checkbox-row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 0; padding: 6px 0;
}
.lpt-subsection { mat-checkbox {
padding: 0 24px 16px; font-size: 13.5px;
color: $text-label;
&:first-of-type {
padding-top: 0;
} }
} }
// ─── Subsection ───────────────────────────────────────────────────────────────
.lpt-subsection {
padding: 0 24px 20px;
}
.lpt-subsection-title { .lpt-subsection-title {
font-size: 13px; font-size: 12.5px;
font-weight: 600; font-weight: 700;
color: #3c4043; color: $text-secondary;
margin: 8px 0 12px; text-transform: uppercase;
letter-spacing: 0.05em;
margin: 12px 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background: $primary;
border-radius: 2px;
}
} }
.lpt-subsection-header { .lpt-subsection-header {
@@ -114,9 +178,10 @@
} }
} }
// ─── Misc ─────────────────────────────────────────────────────────────────────
.lpt-table-wrapper { .lpt-table-wrapper {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 8px; border-radius: $radius-inner;
overflow: hidden; overflow: hidden;
margin-bottom: 16px; margin-bottom: 16px;
@@ -128,8 +193,265 @@
.lpt-label-field { .lpt-label-field {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 14px; font-size: 13.5px;
font-weight: 500; font-weight: 500;
color: #3c4043; color: $text-label;
padding: 8px 0; 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;
} }
@@ -12,6 +12,9 @@ import {
UntypedFormBuilder, UntypedFormBuilder,
Validators, Validators,
UntypedFormControl, UntypedFormControl,
FormArray,
FormGroup,
FormControl,
ReactiveFormsModule ReactiveFormsModule
} from '@angular/forms'; } from '@angular/forms';
import { ProcessingStrategyService } from '../../services/processing-strategy.service'; import { ProcessingStrategyService } from '../../services/processing-strategy.service';
@@ -54,6 +57,25 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
repaymentFrequencyTypeData: any; repaymentFrequencyTypeData: any;
repaymentStartDateTypeOptions: 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() { constructor() {
this.createLoanProductTermsForm(); this.createLoanProductTermsForm();
this.setConditionalControls(); this.setConditionalControls();
@@ -109,6 +131,35 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
} }
createLoanProductTermsForm() { 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({ this.loanProductTermsForm = this.formBuilder.group({
isLinkedToFloatingInterestRates: [false], isLinkedToFloatingInterestRates: [false],
allowApprovedDisbursedAmountsOverApplied: [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) => { this.zeroInterest.valueChanges.subscribe((zeroInterest) => {
if (zeroInterest) { if (zeroInterest) {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0); 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() { get loanProductTerms() {
const formValue = this.loanProductTermsForm.getRawValue(); const formValue = this.loanProductTermsForm.getRawValue();
// Normalize decimal separators: convert comma to dot for backend compatibility // Normalize decimal separators: convert comma to dot for backend compatibility
@@ -270,7 +342,16 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
...formValue, ...formValue,
minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod), minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod),
interestRatePerPeriod: normalizeDecimal(formValue.interestRatePerPeriod), 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;
})()
}; };
} }
+18
View File
@@ -564,6 +564,24 @@ export class ProductsService {
return this.http.post('/fixeddepositproducts/baseinterestrates', payload); return this.http.post('/fixeddepositproducts/baseinterestrates', payload);
} }
/** Returns loan interest rate categories. Pass activeOnly=true to get only active ones. */
getLoanInterestRateCategories(activeOnly = false): Observable<any> {
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<any> {
return this.http.get('/loanproducts/interestrateslabs');
}
/** Returns all loan interest rate slab config versions. */
getAllLoanIrSlabs(): Observable<any> {
return this.http.get('/loanproducts/interestrateslabs/all');
}
/** /**
* @returns {Observable<any>} Collateral data. * @returns {Observable<any>} Collateral data.
*/ */
@@ -21,6 +21,7 @@ import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/
import { SavingsService } from '../savings.service'; import { SavingsService } from '../savings.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
import { import {
MatStepper, MatStepper,
MatStepperIcon, MatStepperIcon,
@@ -61,6 +62,7 @@ export class CreateSavingsAccountComponent {
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private savingsService = inject(SavingsService); private savingsService = inject(SavingsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Savings Account Template */ /** Savings Account Template */
savingsAccountTemplate: any; savingsAccountTemplate: any;
@@ -110,6 +112,14 @@ export class CreateSavingsAccountComponent {
} }
submit() { 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 locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
@@ -22,6 +22,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
import { MatIcon } from '@angular/material/icon'; 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 { 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 * Savings Account Details Step
@@ -48,6 +49,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef); private cdr = inject(ChangeDetectorRef);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private alertService = inject(AlertService);
/** Savings Account Template */ /** Savings Account Template */
@Input() savingsAccountTemplate: any; @Input() savingsAccountTemplate: any;
@@ -335,9 +337,13 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
}); });
ref.afterClosed().subscribe((result) => { ref.afterClosed().subscribe((result) => {
if (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) { 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; return;
} }
this.jointHolders = [ this.jointHolders = [
@@ -446,7 +452,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
value.guardianName = this.guardianName; value.guardianName = this.guardianName;
value.guardianCifNumber = this.guardianCif; value.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship; 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; return value;
@@ -290,15 +290,15 @@ $surface: #f8fafc;
// ── Metric Cards ───────────────────────────────────────────────── // ── Metric Cards ─────────────────────────────────────────────────
&__cards { &__cards {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, minmax(0, 1fr));
border-bottom: 1px solid $border; border-bottom: 1px solid $border;
&--product { &--product {
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(6, minmax(0, 1fr));
} }
@media (width <= 1024px) { @media (width <= 1024px) {
grid-template-columns: repeat(3, 1fr) !important; grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
.dap__card:nth-child(3n) { .dap__card:nth-child(3n) {
border-right: none; border-right: none;
@@ -306,7 +306,7 @@ $surface: #f8fafc;
} }
@media (width <= 600px) { @media (width <= 600px) {
grid-template-columns: repeat(2, 1fr) !important; grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
.dap__card:nth-child(2n) { .dap__card:nth-child(2n) {
border-right: none; border-right: none;
@@ -321,6 +321,7 @@ $surface: #f8fafc;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
transition: background 0.15s; transition: background 0.15s;
min-width: 0; // allow text to wrap within grid cell
&:last-child { &:last-child {
border-right: none; border-right: none;
@@ -340,6 +341,9 @@ $surface: #f8fafc;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
&__card-hint { &__card-hint {
@@ -353,6 +357,7 @@ $surface: #f8fafc;
font-size: 9px; font-size: 9px;
color: $subtle; color: $subtle;
cursor: help; cursor: help;
flex-shrink: 0;
&:hover { &:hover {
color: $c-low; color: $c-low;