From 3df7d100dfdb17595bc593d2a9eee476d3cfcc13 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 20 May 2026 08:08:08 +0530 Subject: [PATCH] feat: add nominee, auto-debit, charges UI for SB/CA/FD/RD accounts; enforce required fields - SB/CA/FD/RD Currency Step: add Nomination Facility toggle with multi-nominee form (NomineeFormComponent), auto-debit facility toggle, dormancy alert panel - SB/CA/FD/RD Preview Step: display nominees list, auto-debit status, dormancy info - FD/RD create component: validate Deposit Tenure, Monthly Installment (RD), and Customer Segment are filled before submit; show clear alert messages on missing fields - FD account details step: remove duplicate Currency section (already shown in Currency & Account Status Configuration step) - CA/SB create component: handle duplicate externalId API error with user-friendly alert instead of raw error display Co-Authored-By: Claude Sonnet 4.6 --- .../create-current-account.component.html | 2 +- .../create-current-account.component.ts | 77 +++++-- ...rrent-account-currency-step.component.html | 62 ++++- ...rrent-account-currency-step.component.scss | 212 +++++++++++++++++ ...current-account-currency-step.component.ts | 49 +++- ...urrent-account-preview-step.component.html | 105 ++++----- .../current-account-preview-step.component.ts | 2 +- ...reate-fixed-deposit-account.component.html | 2 +- .../create-fixed-deposit-account.component.ts | 81 +++++-- ...posit-account-currency-step.component.html | 62 ++++- ...posit-account-currency-step.component.scss | 212 +++++++++++++++++ ...deposit-account-currency-step.component.ts | 51 ++++- ...eposit-account-details-step.component.html | 25 -- ...eposit-account-preview-step.component.html | 105 ++++----- ...-deposit-account-preview-step.component.ts | 2 +- ...-recurring-deposits-account.component.html | 2 +- ...te-recurring-deposits-account.component.ts | 88 ++++++-- ...osits-account-currency-step.component.html | 62 ++++- ...osits-account-currency-step.component.scss | 212 +++++++++++++++++ ...eposits-account-currency-step.component.ts | 50 +++- ...posits-account-preview-step.component.html | 105 ++++----- ...deposits-account-preview-step.component.ts | 2 +- .../create-savings-account.component.html | 2 +- .../create-savings-account.component.ts | 77 +++++-- ...vings-account-currency-step.component.html | 85 +++++-- ...vings-account-currency-step.component.scss | 213 ++++++++++++++++++ ...savings-account-currency-step.component.ts | 45 +++- ...avings-account-preview-step.component.html | 105 ++++----- .../savings-account-preview-step.component.ts | 4 +- 29 files changed, 1658 insertions(+), 443 deletions(-) diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.html b/src/app/current-accounts/create-current-account/create-current-account.component.html index 2b60f9cf0..e27c865f4 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.html +++ b/src/app/current-accounts/create-current-account/create-current-account.component.html @@ -98,7 +98,7 @@ [currentAccountTermsForm]="currentAccountTermsForm" [currentAccount]="currentAccount" [currentAccountDigital]="currentAccountDigital" - [currentAccountNominee]="currentAccountNominee" + [currentAccountNominees]="currentAccountCurrencyStep?.nominees ?? []" [currentAccountCurrencyFlags]="currentAccountCurrencyFlags" (submitEvent)="submit()" > diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.ts b/src/app/current-accounts/create-current-account/create-current-account.component.ts index 0a1bda31f..d6ae2b86e 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.ts +++ b/src/app/current-accounts/create-current-account/create-current-account.component.ts @@ -118,10 +118,6 @@ export class CreateCurrentAccountComponent { return this.currentAccountDigitalStep?.currentAccountDigital ?? null; } - get currentAccountNominee() { - return this.currentAccountCurrencyStep?.liveNomineeData ?? null; - } - get currentAccountCurrencyFlags() { return { balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false, @@ -142,6 +138,23 @@ export class CreateCurrentAccountComponent { }); return; } + if ( + this.currentAccountProductTemplate?.nominationFacility && + (this.currentAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (this.currentAccountProductTemplate?.introducerRequired && !this.currentAccountDetailsStep.introducerId) { + this.alertService.alert({ + type: 'Error', + message: 'Introducer is required for this product. Please select an introducer before submitting.' + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; @@ -185,27 +198,45 @@ export class CreateCurrentAccountComponent { currentAccount.groupId = this.currentAccountTemplate.groupId; } - const nominee = this.currentAccountNominee; - if (nominee) { - currentAccount.nominee = { - nomineeClientId: nominee.nomineeClientId, - nominationType: nominee.nominationType, - relationshipId: nominee.relationshipId, - percentageShare: nominee.percentageShare, - nomineeIsMinor: nominee.nomineeIsMinor, - ...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}), - remarks: nominee.remarks - }; + const nominees = this.currentAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + currentAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); } - this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => { - this.router.navigate( - [ - '../', - response.resourceId - ], - { relativeTo: this.route } - ); + this.currentAccountsService.createCurrentAccount(currentAccount).subscribe({ + next: (response: any) => { + this.router.navigate( + [ + '../', + response.resourceId + ], + { relativeTo: this.route } + ); + }, + error: (error: any) => { + const errorCode = + error?.error?.errors?.[0]?.userMessageGlobalisationCode ?? error?.error?.userMessageGlobalisationCode; + if (errorCode === 'error.msg.savings.account.duplicate.product.for.client') { + this.alertService.alert({ + type: 'Error', + message: + 'This client already has an active Current account with the selected product. Please choose a different product.' + }); + } else if (errorCode === 'error.msg.savings.account.duplicate.externalid') { + this.alertService.alert({ + type: 'Error', + message: 'An account with this External ID already exists. Please use a unique External ID.' + }); + } + } }); } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html index b55b3cfba..cf07e419f 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html @@ -73,11 +73,63 @@ @if (currentAccountProductTemplate?.nominationFacility) { - +
+
+
Nominees
+ +
+ @if (nominees.length > 0) { +
+ @for (nom of nominees; track nom.nomineeClientId; let i = $index) { +
+
+ {{ nom.nomineeDisplayName }} + {{ nom.relationshipName }} • {{ nom.percentageShare }}% share + @if (nom.nomineeIsMinor) { + Minor — Guardian: {{ nom.guardianDisplayName }} + } +
+ +
+ } +
+ } + @if (canAddMore && !showNomineeForm) { + + } + @if (totalShare === 100) { +
Nominee allocation complete (100%)
+ } + @if (showNomineeForm) { +
+ @if (nomineeError) { +
{{ nomineeError }}
+ } + +
+ + +
+
+ } +
} diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss index 68c9a94a2..88fc1b3cc 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss @@ -128,3 +128,215 @@ $gray-900: #202124; color: #0369a1; } } + +/* ── Nominees ── */ +.nom-section { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; +} + +.nom-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + + &__left { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 700; + color: #334155; + + i { + color: #1d4ed8; + font-size: 12px; + } + } +} + +.nom-share-badge { + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: #e0e7ff; + color: #3730a3; + border: 1px solid #c7d2fe; + + &--full { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.nom-list { + display: flex; + flex-direction: column; +} + +.nom-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px dashed #e2e8f0; + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + &__meta { + font-size: 11px; + color: #64748b; + } + + &__minor { + font-size: 11px; + color: #b45309; + } +} + +.nom-remove-btn { + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid #fecaca; + background: #fee2e2; + color: #b91c1c; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + flex-shrink: 0; + transition: background 0.15s; + + &:hover { + background: #fecaca; + } +} + +.nom-add-btn { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + background: #fff; + border: none; + border-top: 1px dashed #e2e8f0; + color: #1d4ed8; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + i { + font-size: 11px; + } + + &__hint { + font-size: 11px; + color: #64748b; + margin-left: 4px; + } + + &:hover { + background: #eff6ff; + } +} + +.nom-complete { + padding: 10px 16px; + font-size: 12px; + font-weight: 600; + color: #15803d; + background: #f0fdf4; + border-top: 1px solid #bbf7d0; + + i { + margin-right: 6px; + } +} + +.nom-form-wrap { + padding: 16px; + background: #f8fafc; + border-top: 1px solid #e2e8f0; +} + +.nom-error { + padding: 8px 12px; + margin-bottom: 12px; + background: #fee2e2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #b91c1c; + font-size: 12px; + font-weight: 600; + + i { + margin-right: 6px; + } +} + +.nom-form-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 14px; +} + +.nom-cancel-btn { + padding: 7px 18px; + border-radius: 7px; + border: 1px solid #e2e8f0; + background: #fff; + color: #64748b; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + &:hover { + background: #f1f5f9; + } +} + +.nom-confirm-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 18px; + border-radius: 7px; + border: none; + background: #1d4ed8; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + i { + font-size: 11px; + } + + &:hover { + background: #1e40af; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts index 63aa2b84d..8a8d97c2e 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts @@ -30,7 +30,10 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { @Input() primaryClientId: number | null = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; - nomineeData: any = null; + nominees: any[] = []; + pendingNominee: any = null; + showNomineeForm = false; + nomineeError = ''; // Editable feature toggles (pre-filled from product, overridable by user) balanceSwapOption = false; @@ -46,21 +49,53 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { } } + get totalShare(): number { + return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0); + } + + get remainingShare(): number { + return 100 - this.totalShare; + } + + get canAddMore(): boolean { + return this.totalShare < 100; + } + onNomineeChange(event: any): void { - this.nomineeData = event; + this.pendingNominee = event; + this.nomineeError = ''; } - get liveNomineeData(): any { - return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + addNominee(): void { + const data = this.nomineeFormComp?.currentNomineeData ?? this.pendingNominee; + if (!data?.nomineeClientId) { + this.nomineeError = 'Please select a nominee before adding.'; + return; + } + if (this.nominees.some((n) => n.nomineeClientId === data.nomineeClientId)) { + this.nomineeError = 'This person is already added as a nominee.'; + return; + } + const newTotal = this.totalShare + (+data.percentageShare || 0); + if (newTotal > 100) { + this.nomineeError = `Total share cannot exceed 100%. Remaining: ${this.remainingShare}%`; + return; + } + this.nominees.push({ ...data }); + this.pendingNominee = null; + this.showNomineeForm = false; + this.nomineeError = ''; + } + + removeNominee(index: number): void { + this.nominees.splice(index, 1); } - /** Called by parent to collect currency step values */ get currencyStepData() { return { balanceSwapOption: this.balanceSwapOption, msme: this.msme, - autoDebitFacility: this.autoDebitFacility, - ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + autoDebitFacility: this.autoDebitFacility }; } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html index e4ab463e5..fb65ed15d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html @@ -565,78 +565,53 @@ } - @if (currentAccountNominee) { + @if (currentAccountNominees?.length > 0) {
Nominee Details - {{ currentAccountNominee?.nominationType || 'DA1' }} + {{ currentAccountNominees.length }} Nominee(s)
-
-
- Name - {{ currentAccountNominee?.nomineeDisplayName || '—' }} + @for (nom of currentAccountNominees; track nom.nomineeClientId; let i = $index) { + @if (i > 0) { +
+ } +
+
+ Name + {{ nom.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ nom.nomineeClientId || '—' }} +
+
+ Relationship + {{ nom.relationshipName || '—' }} +
+
+ Share % + {{ nom.percentageShare ?? '—' }}% +
+
+ Minor + {{ nom.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (nom.guardianDisplayName) { +
+ Guardian + {{ nom.guardianDisplayName }} +
+ } + @if (nom.remarks) { +
+ Remarks + {{ nom.remarks }} +
+ }
-
- Client ID - {{ currentAccountNominee?.nomineeClientId || '—' }} -
-
- Relationship - {{ currentAccountNominee?.relationshipName || '—' }} -
-
- Share % - {{ currentAccountNominee?.percentageShare ?? '—' }}% -
-
- Minor - {{ currentAccountNominee?.nomineeIsMinor ? 'Yes' : 'No' }} -
- @if (currentAccountNominee?.nomineeDateOfBirth) { -
- Date of Birth - {{ currentAccountNominee?.nomineeDateOfBirth | dateFormat }} -
- } - @if (currentAccountNominee?.nomineeMobileNo) { -
- Mobile No - {{ currentAccountNominee?.nomineeMobileNo }} -
- } - @if (currentAccountNominee?.nomineeAadhaar) { -
- Aadhaar - {{ currentAccountNominee?.nomineeAadhaar }} -
- } - @if (currentAccountNominee?.nomineePan) { -
- PAN - {{ currentAccountNominee?.nomineePan }} -
- } - @if (currentAccountNominee?.guardianDisplayName) { -
- Guardian - {{ currentAccountNominee?.guardianDisplayName }} -
- } - @if (currentAccountNominee?.guardianClientId) { -
- Guardian Client ID - {{ currentAccountNominee?.guardianClientId }} -
- } - @if (currentAccountNominee?.remarks) { -
- Remarks - {{ currentAccountNominee?.remarks }} -
- } -
+ }
} diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts index c5c139fd7..3568d01aa 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts @@ -90,7 +90,7 @@ export class CurrentAccountPreviewStepComponent implements OnChanges { @Input() currentAccountSettingsForm: any; @Input() currentAccount: any; @Input() currentAccountDigital: any; - @Input() currentAccountNominee: any; + @Input() currentAccountNominees: any[] = []; @Input() currentAccountCurrencyFlags: any; @Input() activeClientMembers?: any; diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html index 23b059473..f78358d85 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html @@ -98,7 +98,7 @@ [fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate" [fixedDepositAccountData]="fixedDepositAccount" [selectedTenure]="selectedTenure" - [fdNominee]="fdNominee" + [fdNominees]="fixedDepositAccountCurrencyStep?.nominees ?? []" (submitEvent)="submit()" > diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts index 5ce038d0d..9365dae78 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts @@ -138,10 +138,6 @@ export class CreateFixedDepositAccountComponent { }; } - get fdNominee() { - return this.fixedDepositAccountCurrencyStep?.liveNomineeData ?? null; - } - /** * Submits the fixed deposit form and creates a new fixed deposit account */ @@ -154,6 +150,27 @@ export class CreateFixedDepositAccountComponent { }); return; } + if ( + this.fixedDepositsAccountProductTemplate?.nominationFacility && + (this.fixedDepositAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (!this.selectedTenure || !this.selectedTenure.depositPeriod) { + this.alertService.alert({ + type: 'Error', + message: 'Please enter the Deposit Tenure and click "Find Rate" before submitting.' + }); + return; + } + if (!detailsStep?.selectedCustomerSegment) { + this.alertService.alert({ type: 'Error', message: 'Please select a Customer Segment before submitting.' }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; @@ -227,27 +244,45 @@ export class CreateFixedDepositAccountComponent { monthDayFormat, locale }; - const nominee = this.fdNominee; - if (nominee) { - fixedDepositAccount.nominee = { - nomineeClientId: nominee.nomineeClientId, - nominationType: nominee.nominationType, - relationshipId: nominee.relationshipId, - percentageShare: nominee.percentageShare, - nomineeIsMinor: nominee.nomineeIsMinor, - ...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}), - remarks: nominee.remarks - }; + const nominees = this.fixedDepositAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + fixedDepositAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); } - this.fixedDepositsService.createFixedDepositAccount(fixedDepositAccount).subscribe((response: any) => { - this.router.navigate( - [ - '../', - response.resourceId - ], - { relativeTo: this.route } - ); + this.fixedDepositsService.createFixedDepositAccount(fixedDepositAccount).subscribe({ + next: (response: any) => { + this.router.navigate( + [ + '../', + response.resourceId + ], + { relativeTo: this.route } + ); + }, + error: (error: any) => { + const errorCode = + error?.error?.errors?.[0]?.userMessageGlobalisationCode ?? error?.error?.userMessageGlobalisationCode; + if (errorCode === 'error.msg.savings.account.duplicate.product.for.client') { + this.alertService.alert({ + type: 'Error', + message: + 'This client already has an active Fixed Deposit account with the selected product. Please choose a different product.' + }); + } else if (errorCode === 'error.msg.savingsaccount.duplicate.externalId') { + this.alertService.alert({ + type: 'Error', + message: 'An account with this External ID already exists. Please use a unique External ID.' + }); + } + } }); } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.html index c0ecf73b6..459161453 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.html @@ -58,11 +58,63 @@
@if (nominationFacility) { - +
+
+
Nominees
+ +
+ @if (nominees.length > 0) { +
+ @for (nom of nominees; track nom.nomineeClientId; let i = $index) { +
+
+ {{ nom.nomineeDisplayName }} + {{ nom.relationshipName }} • {{ nom.percentageShare }}% share + @if (nom.nomineeIsMinor) { + Minor — Guardian: {{ nom.guardianDisplayName }} + } +
+ +
+ } +
+ } + @if (canAddMore && !showNomineeForm) { + + } + @if (totalShare === 100) { +
Nominee allocation complete (100%)
+ } + @if (showNomineeForm) { +
+ @if (nomineeError) { +
{{ nomineeError }}
+ } + +
+ + +
+
+ } +
} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.scss index ccfce3c23..39149004a 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.scss @@ -122,3 +122,215 @@ font-size: 0.75rem; } } + +/* ── Nominees ── */ +.nom-section { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; +} + +.nom-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + + &__left { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 700; + color: #334155; + + i { + color: #1d4ed8; + font-size: 12px; + } + } +} + +.nom-share-badge { + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: #e0e7ff; + color: #3730a3; + border: 1px solid #c7d2fe; + + &--full { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.nom-list { + display: flex; + flex-direction: column; +} + +.nom-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px dashed #e2e8f0; + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + &__meta { + font-size: 11px; + color: #64748b; + } + + &__minor { + font-size: 11px; + color: #b45309; + } +} + +.nom-remove-btn { + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid #fecaca; + background: #fee2e2; + color: #b91c1c; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + flex-shrink: 0; + transition: background 0.15s; + + &:hover { + background: #fecaca; + } +} + +.nom-add-btn { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + background: #fff; + border: none; + border-top: 1px dashed #e2e8f0; + color: #1d4ed8; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + i { + font-size: 11px; + } + + &__hint { + font-size: 11px; + color: #64748b; + margin-left: 4px; + } + + &:hover { + background: #eff6ff; + } +} + +.nom-complete { + padding: 10px 16px; + font-size: 12px; + font-weight: 600; + color: #15803d; + background: #f0fdf4; + border-top: 1px solid #bbf7d0; + + i { + margin-right: 6px; + } +} + +.nom-form-wrap { + padding: 16px; + background: #f8fafc; + border-top: 1px solid #e2e8f0; +} + +.nom-error { + padding: 8px 12px; + margin-bottom: 12px; + background: #fee2e2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #b91c1c; + font-size: 12px; + font-weight: 600; + + i { + margin-right: 6px; + } +} + +.nom-form-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 14px; +} + +.nom-cancel-btn { + padding: 7px 18px; + border-radius: 7px; + border: 1px solid #e2e8f0; + background: #fff; + color: #64748b; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + &:hover { + background: #f1f5f9; + } +} + +.nom-confirm-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 18px; + border-radius: 7px; + border: none; + background: #1d4ed8; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + i { + font-size: 11px; + } + + &:hover { + background: #1e40af; + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts index b581ff0b7..4e7054a35 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts @@ -30,13 +30,14 @@ export class FixedDepositAccountCurrencyStepComponent implements OnChanges { @Input() primaryClientId: number | null = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; - nomineeData: any = null; + nominees: any[] = []; + pendingNominee: any = null; + showNomineeForm = false; + nomineeError = ''; autoDebitFacility = false; nominationFacility = false; - // Editable feature toggles (pre-filled from product, overridable by user) - ngOnChanges(changes: SimpleChanges): void { if (changes['fixedDepositsAccountProductTemplate']?.currentValue) { const pt = changes['fixedDepositsAccountProductTemplate'].currentValue; @@ -45,20 +46,52 @@ export class FixedDepositAccountCurrencyStepComponent implements OnChanges { } } + get totalShare(): number { + return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0); + } + + get remainingShare(): number { + return 100 - this.totalShare; + } + + get canAddMore(): boolean { + return this.totalShare < 100; + } + onNomineeChange(event: any): void { - this.nomineeData = event; + this.pendingNominee = event; + this.nomineeError = ''; } - get liveNomineeData(): any { - return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + addNominee(): void { + const data = this.nomineeFormComp?.currentNomineeData ?? this.pendingNominee; + if (!data?.nomineeClientId) { + this.nomineeError = 'Please select a nominee before adding.'; + return; + } + if (this.nominees.some((n) => n.nomineeClientId === data.nomineeClientId)) { + this.nomineeError = 'This person is already added as a nominee.'; + return; + } + const newTotal = this.totalShare + (+data.percentageShare || 0); + if (newTotal > 100) { + this.nomineeError = `Total share cannot exceed 100%. Remaining: ${this.remainingShare}%`; + return; + } + this.nominees.push({ ...data }); + this.pendingNominee = null; + this.showNomineeForm = false; + this.nomineeError = ''; + } + + removeNominee(index: number): void { + this.nominees.splice(index, 1); } - /** Called by parent to collect currency step values */ get currencyStepData() { return { autoDebitFacility: this.autoDebitFacility, - nominationFacility: this.nominationFacility, - ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + nominationFacility: this.nominationFacility }; } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html index 5498ce839..a85665e72 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html @@ -129,31 +129,6 @@ - -
-
- -

Currency

- From Product -
-
- - {{ 'labels.inputs.Currency' | translate }} - - - - - {{ 'labels.inputs.Decimal Places' | translate }} - - - - - {{ 'labels.inputs.Currency In Multiples Of' | translate }} - - -
-
- @if (productTemplate?.shareLinkingEnabled) {
diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html index d6c29d337..17ea2a91c 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html @@ -384,78 +384,53 @@ } - @if (fdNominee) { + @if (fdNominees?.length > 0) {
Nominee Details - {{ fdNominee?.nominationType || 'DA1' }} + {{ fdNominees.length }} Nominee(s)
-
-
- Name - {{ fdNominee?.nomineeDisplayName || '—' }} + @for (nom of fdNominees; track nom.nomineeClientId; let i = $index) { + @if (i > 0) { +
+ } +
+
+ Name + {{ nom.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ nom.nomineeClientId || '—' }} +
+
+ Relationship + {{ nom.relationshipName || '—' }} +
+
+ Share % + {{ nom.percentageShare ?? '—' }}% +
+
+ Minor + {{ nom.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (nom.guardianDisplayName) { +
+ Guardian + {{ nom.guardianDisplayName }} +
+ } + @if (nom.remarks) { +
+ Remarks + {{ nom.remarks }} +
+ }
-
- Client ID - {{ fdNominee?.nomineeClientId || '—' }} -
-
- Relationship - {{ fdNominee?.relationshipName || '—' }} -
-
- Share % - {{ fdNominee?.percentageShare ?? '—' }}% -
-
- Minor - {{ fdNominee?.nomineeIsMinor ? 'Yes' : 'No' }} -
- @if (fdNominee?.nomineeDateOfBirth) { -
- Date of Birth - {{ fdNominee?.nomineeDateOfBirth | dateFormat }} -
- } - @if (fdNominee?.nomineeMobileNo) { -
- Mobile No - {{ fdNominee?.nomineeMobileNo }} -
- } - @if (fdNominee?.nomineeAadhaar) { -
- Aadhaar - {{ fdNominee?.nomineeAadhaar }} -
- } - @if (fdNominee?.nomineePan) { -
- PAN - {{ fdNominee?.nomineePan }} -
- } - @if (fdNominee?.guardianDisplayName) { -
- Guardian - {{ fdNominee?.guardianDisplayName }} -
- } - @if (fdNominee?.guardianClientId) { -
- Guardian Client ID - {{ fdNominee?.guardianClientId }} -
- } - @if (fdNominee?.remarks) { -
- Remarks - {{ fdNominee?.remarks }} -
- } -
+ }
} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts index 35ebf69ea..478585ec7 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts @@ -76,7 +76,7 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { @Input() fixedDepositsAccountProductTemplate: any; /** Fixed Deposits Account Data */ @Input() fixedDepositAccountData: any; - @Input() fdNominee: any = null; + @Input() fdNominees: any[] = []; /** Tenure selected from the interest rate chart step (includes pre-calculated maturity if user used calculator) */ @Input() selectedTenure: { depositPeriod: number; diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html index 969d15733..396c3bfa3 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html @@ -98,7 +98,7 @@ [recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositAccountData]="recurringDepositAccountData" - [rdNominee]="rdNominee" + [rdNominees]="recurringDepositsAccountCurrencyStep?.nominees ?? []" [rdCurrencyFlags]="rdCurrencyFlags" [rdTenureDays]="rdTenureDays" [rdApplicableSlab]="rdApplicableSlab" diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts index 7b7bb9efc..9f8e9e9ec 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts @@ -121,10 +121,6 @@ export class CreateRecurringDepositsAccountComponent { }; } - get rdNominee() { - return this.recurringDepositsAccountCurrencyStep?.liveNomineeData ?? null; - } - get rdTenureDays(): number | null { return this.recurringDepositsAccountChartStep?.matchResult?.tenureDays ?? null; } @@ -167,6 +163,34 @@ export class CreateRecurringDepositsAccountComponent { }); return; } + if ( + this.recurringDepositsAccountProductTemplate?.nominationFacility && + (this.recurringDepositsAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (!this.selectedTenureValue || this.selectedTenureFrequencyId === null) { + this.alertService.alert({ + type: 'Error', + message: 'Please enter the Deposit Tenure and click "Find Rate" before submitting.' + }); + return; + } + if (!this.selectedDepositAmount || this.selectedDepositAmount <= 0) { + this.alertService.alert({ + type: 'Error', + message: 'Please enter the Monthly Installment amount and click "Find Rate" before submitting.' + }); + return; + } + if (!detailsStep?.selectedCustomerSegment) { + this.alertService.alert({ type: 'Error', message: 'Please select a Customer Segment before submitting.' }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; @@ -259,27 +283,45 @@ export class CreateRecurringDepositsAccountComponent { recurringDepositAccount.mandatoryRecommendedDepositAmount = this.selectedDepositAmount; } - const nominee = this.rdNominee; - if (nominee) { - recurringDepositAccount.nominee = { - nomineeClientId: nominee.nomineeClientId, - nominationType: nominee.nominationType, - relationshipId: nominee.relationshipId, - percentageShare: nominee.percentageShare, - nomineeIsMinor: nominee.nomineeIsMinor, - ...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}), - remarks: nominee.remarks - }; + const nominees = this.recurringDepositsAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + recurringDepositAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); } - this.recurringDepositsService.createRecurringDepositAccount(recurringDepositAccount).subscribe((response: any) => { - this.router.navigate( - [ - '../', - response.resourceId - ], - { relativeTo: this.route } - ); + this.recurringDepositsService.createRecurringDepositAccount(recurringDepositAccount).subscribe({ + next: (response: any) => { + this.router.navigate( + [ + '../', + response.resourceId + ], + { relativeTo: this.route } + ); + }, + error: (error: any) => { + const errorCode = + error?.error?.errors?.[0]?.userMessageGlobalisationCode ?? error?.error?.userMessageGlobalisationCode; + if (errorCode === 'error.msg.savings.account.duplicate.product.for.client') { + this.alertService.alert({ + type: 'Error', + message: + 'This client already has an active Recurring Deposit account with the selected product. Please choose a different product.' + }); + } else if (errorCode === 'error.msg.savingsaccount.duplicate.externalId') { + this.alertService.alert({ + type: 'Error', + message: 'An account with this External ID already exists. Please use a unique External ID.' + }); + } + } }); } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html index 014821f2e..a10155424 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html @@ -60,11 +60,63 @@
@if (recurringDepositsAccountProductTemplate?.nominationFacility) { - +
+
+
Nominees
+ +
+ @if (nominees.length > 0) { +
+ @for (nom of nominees; track nom.nomineeClientId; let i = $index) { +
+
+ {{ nom.nomineeDisplayName }} + {{ nom.relationshipName }} • {{ nom.percentageShare }}% share + @if (nom.nomineeIsMinor) { + Minor — Guardian: {{ nom.guardianDisplayName }} + } +
+ +
+ } +
+ } + @if (canAddMore && !showNomineeForm) { + + } + @if (totalShare === 100) { +
Nominee allocation complete (100%)
+ } + @if (showNomineeForm) { +
+ @if (nomineeError) { +
{{ nomineeError }}
+ } + +
+ + +
+
+ } +
}
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss index 3ed604577..6f722d291 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss @@ -139,3 +139,215 @@ $gray-900: #202124; color: #0369a1; } } + +/* ── Nominees ── */ +.nom-section { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; +} + +.nom-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + + &__left { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 700; + color: #334155; + + i { + color: #1d4ed8; + font-size: 12px; + } + } +} + +.nom-share-badge { + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: #e0e7ff; + color: #3730a3; + border: 1px solid #c7d2fe; + + &--full { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.nom-list { + display: flex; + flex-direction: column; +} + +.nom-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px dashed #e2e8f0; + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + &__meta { + font-size: 11px; + color: #64748b; + } + + &__minor { + font-size: 11px; + color: #b45309; + } +} + +.nom-remove-btn { + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid #fecaca; + background: #fee2e2; + color: #b91c1c; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + flex-shrink: 0; + transition: background 0.15s; + + &:hover { + background: #fecaca; + } +} + +.nom-add-btn { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + background: #fff; + border: none; + border-top: 1px dashed #e2e8f0; + color: #1d4ed8; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + i { + font-size: 11px; + } + + &__hint { + font-size: 11px; + color: #64748b; + margin-left: 4px; + } + + &:hover { + background: #eff6ff; + } +} + +.nom-complete { + padding: 10px 16px; + font-size: 12px; + font-weight: 600; + color: #15803d; + background: #f0fdf4; + border-top: 1px solid #bbf7d0; + + i { + margin-right: 6px; + } +} + +.nom-form-wrap { + padding: 16px; + background: #f8fafc; + border-top: 1px solid #e2e8f0; +} + +.nom-error { + padding: 8px 12px; + margin-bottom: 12px; + background: #fee2e2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #b91c1c; + font-size: 12px; + font-weight: 600; + + i { + margin-right: 6px; + } +} + +.nom-form-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 14px; +} + +.nom-cancel-btn { + padding: 7px 18px; + border-radius: 7px; + border: 1px solid #e2e8f0; + background: #fff; + color: #64748b; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + &:hover { + background: #f1f5f9; + } +} + +.nom-confirm-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 18px; + border-radius: 7px; + border: none; + background: #1d4ed8; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + i { + font-size: 11px; + } + + &:hover { + background: #1e40af; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts index f52ec7331..7a8c05a53 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts @@ -30,9 +30,11 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges @Input() primaryClientId: number | null = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; - nomineeData: any = null; + nominees: any[] = []; + pendingNominee: any = null; + showNomineeForm = false; + nomineeError = ''; - // Editable feature toggles (pre-filled from product, overridable by user) autoDebitFacility = false; ngOnChanges(changes: SimpleChanges): void { @@ -42,19 +44,51 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges } } + get totalShare(): number { + return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0); + } + + get remainingShare(): number { + return 100 - this.totalShare; + } + + get canAddMore(): boolean { + return this.totalShare < 100; + } + onNomineeChange(event: any): void { - this.nomineeData = event; + this.pendingNominee = event; + this.nomineeError = ''; } - get liveNomineeData(): any { - return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + addNominee(): void { + const data = this.nomineeFormComp?.currentNomineeData ?? this.pendingNominee; + if (!data?.nomineeClientId) { + this.nomineeError = 'Please select a nominee before adding.'; + return; + } + if (this.nominees.some((n) => n.nomineeClientId === data.nomineeClientId)) { + this.nomineeError = 'This person is already added as a nominee.'; + return; + } + const newTotal = this.totalShare + (+data.percentageShare || 0); + if (newTotal > 100) { + this.nomineeError = `Total share cannot exceed 100%. Remaining: ${this.remainingShare}%`; + return; + } + this.nominees.push({ ...data }); + this.pendingNominee = null; + this.showNomineeForm = false; + this.nomineeError = ''; + } + + removeNominee(index: number): void { + this.nominees.splice(index, 1); } - /** Called by parent to collect currency step values */ get currencyStepData() { return { - autoDebitFacility: this.autoDebitFacility, - ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + autoDebitFacility: this.autoDebitFacility }; } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index a92d830b2..f4258d6e0 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -541,78 +541,53 @@ } - @if (rdNominee) { + @if (rdNominees?.length > 0) {
Nominee Details - {{ rdNominee?.nominationType || 'DA1' }} + {{ rdNominees.length }} Nominee(s)
-
-
- Name - {{ rdNominee?.nomineeDisplayName || '—' }} + @for (nom of rdNominees; track nom.nomineeClientId; let i = $index) { + @if (i > 0) { +
+ } +
+
+ Name + {{ nom.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ nom.nomineeClientId || '—' }} +
+
+ Relationship + {{ nom.relationshipName || '—' }} +
+
+ Share % + {{ nom.percentageShare ?? '—' }}% +
+
+ Minor + {{ nom.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (nom.guardianDisplayName) { +
+ Guardian + {{ nom.guardianDisplayName }} +
+ } + @if (nom.remarks) { +
+ Remarks + {{ nom.remarks }} +
+ }
-
- Client ID - {{ rdNominee?.nomineeClientId || '—' }} -
-
- Relationship - {{ rdNominee?.relationshipName || '—' }} -
-
- Share % - {{ rdNominee?.percentageShare ?? '—' }}% -
-
- Minor - {{ rdNominee?.nomineeIsMinor ? 'Yes' : 'No' }} -
- @if (rdNominee?.nomineeDateOfBirth) { -
- Date of Birth - {{ rdNominee?.nomineeDateOfBirth | dateFormat }} -
- } - @if (rdNominee?.nomineeMobileNo) { -
- Mobile No - {{ rdNominee?.nomineeMobileNo }} -
- } - @if (rdNominee?.nomineeAadhaar) { -
- Aadhaar - {{ rdNominee?.nomineeAadhaar }} -
- } - @if (rdNominee?.nomineePan) { -
- PAN - {{ rdNominee?.nomineePan }} -
- } - @if (rdNominee?.guardianDisplayName) { -
- Guardian - {{ rdNominee?.guardianDisplayName }} -
- } - @if (rdNominee?.guardianClientId) { -
- Guardian Client ID - {{ rdNominee?.guardianClientId }} -
- } - @if (rdNominee?.remarks) { -
- Remarks - {{ rdNominee?.remarks }} -
- } -
+ }
} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts index 32e55ca01..3ae7c0b52 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts @@ -73,7 +73,7 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges { @Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountProductTemplate: any; @Input() recurringDepositAccountData: any; - @Input() rdNominee: any = null; + @Input() rdNominees: any[] = []; @Input() rdCurrencyFlags: any = null; @Input() rdTenureDays: number | null = null; @Input() rdApplicableSlab: any = null; diff --git a/src/app/savings/create-savings-account/create-savings-account.component.html b/src/app/savings/create-savings-account/create-savings-account.component.html index 77584d630..b9a7272d1 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.html +++ b/src/app/savings/create-savings-account/create-savings-account.component.html @@ -95,7 +95,7 @@ [savingsAccountTermsForm]="savingsAccountTermsForm" [savingsAccount]="savingsAccount" [savingsAccountDigital]="savingsAccountDigital" - [savingsAccountNominee]="savingsAccountNominee" + [savingsAccountNominees]="savingsAccountCurrencyStep?.nominees ?? []" (submitEvent)="submit()" > diff --git a/src/app/savings/create-savings-account/create-savings-account.component.ts b/src/app/savings/create-savings-account/create-savings-account.component.ts index a5cd60e20..dcb32c39d 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.ts +++ b/src/app/savings/create-savings-account/create-savings-account.component.ts @@ -107,10 +107,6 @@ export class CreateSavingsAccountComponent { return this.savingsAccountDigitalStep?.savingsAccountDigital ?? null; } - get savingsAccountNominee() { - return this.savingsAccountCurrencyStep?.liveNomineeData ?? null; - } - submit() { const detailsStep = this.savingsAccountDetailsStep; if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { @@ -120,6 +116,23 @@ export class CreateSavingsAccountComponent { }); return; } + if ( + this.savingsAccountProductTemplate?.nominationFacility && + (this.savingsAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (this.savingsAccountProductTemplate?.introducerRequired && !this.savingsAccountDetailsStep.introducerId) { + this.alertService.alert({ + type: 'Error', + message: 'Introducer is required for this product. Please select an introducer before submitting.' + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; @@ -143,27 +156,45 @@ export class CreateSavingsAccountComponent { savingsAccount.groupId = this.savingsAccountTemplate.groupId; } - const nominee = this.savingsAccountNominee; - if (nominee) { - savingsAccount.nominee = { - nomineeClientId: nominee.nomineeClientId, - nominationType: nominee.nominationType, - relationshipId: nominee.relationshipId, - percentageShare: nominee.percentageShare, - nomineeIsMinor: nominee.nomineeIsMinor, - ...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}), - remarks: nominee.remarks - }; + const nominees = this.savingsAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + savingsAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); } - this.savingsService.createSavingsAccount(savingsAccount).subscribe((response: any) => { - this.router.navigate( - [ - '../', - response.resourceId - ], - { relativeTo: this.route } - ); + this.savingsService.createSavingsAccount(savingsAccount).subscribe({ + next: (response: any) => { + this.router.navigate( + [ + '../', + response.resourceId + ], + { relativeTo: this.route } + ); + }, + error: (error: any) => { + const errorCode = + error?.error?.errors?.[0]?.userMessageGlobalisationCode ?? error?.error?.userMessageGlobalisationCode; + if (errorCode === 'error.msg.savings.account.duplicate.product.for.client') { + this.alertService.alert({ + type: 'Error', + message: + 'This client already has an active account with the selected product. Please choose a different product.' + }); + } else if (errorCode === 'error.msg.savingsaccount.duplicate.externalId') { + this.alertService.alert({ + type: 'Error', + message: 'An account with this External ID already exists. Please use a unique External ID.' + }); + } + } }); } } diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html index c2a73c992..34db80a41 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html @@ -39,21 +39,78 @@ } -
-
-

Nomination Facility

-

Allow account holders to designate individuals to receive funds in the event of account holder's death.

-
- - {{ savingsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }} - -
+ @if (savingsAccountProductTemplate?.nominationFacility) { - +
+
+
+ + Nominees +
+
+ +
+
+ + + @if (nominees.length > 0) { +
+ @for (nom of nominees; track nom.nomineeClientId; let i = $index) { +
+
+ {{ nom.nomineeDisplayName }} + {{ nom.relationshipName }} • {{ nom.percentageShare }}% share + @if (nom.nomineeIsMinor) { + Minor — Guardian: {{ nom.guardianDisplayName }} + } +
+ +
+ } +
+ } + + + @if (canAddMore && !showNomineeForm) { + + } + + @if (totalShare === 100) { +
Nominee allocation complete (100%)
+ } + + + @if (showNomineeForm) { +
+ @if (nomineeError) { +
{{ nomineeError }}
+ } + +
+ + +
+
+ } +
}
diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss index 17712a898..18bd9e548 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss @@ -114,3 +114,216 @@ $gray-900: #202124; border-color: #bbf7d0; } } + +/* ── Nominees ── */ +.nom-section { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; +} + +.nom-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + + &__left { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 700; + color: #334155; + + i { + color: #1d4ed8; + font-size: 12px; + } + } +} + +.nom-share-badge { + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: #e0e7ff; + color: #3730a3; + border: 1px solid #c7d2fe; + + &--full { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.nom-list { + display: flex; + flex-direction: column; + gap: 0; +} + +.nom-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px dashed #e2e8f0; + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + &__meta { + font-size: 11px; + color: #64748b; + } + + &__minor { + font-size: 11px; + color: #b45309; + } +} + +.nom-remove-btn { + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid #fecaca; + background: #fee2e2; + color: #b91c1c; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + flex-shrink: 0; + transition: background 0.15s; + + &:hover { + background: #fecaca; + } +} + +.nom-add-btn { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + background: #fff; + border: none; + border-top: 1px dashed #e2e8f0; + color: #1d4ed8; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + + i { + font-size: 11px; + } + + &__hint { + font-size: 11px; + color: #64748b; + margin-left: 4px; + } + + &:hover { + background: #eff6ff; + } +} + +.nom-complete { + padding: 10px 16px; + font-size: 12px; + font-weight: 600; + color: #15803d; + background: #f0fdf4; + border-top: 1px solid #bbf7d0; + + i { + margin-right: 6px; + } +} + +.nom-form-wrap { + padding: 16px; + background: #f8fafc; + border-top: 1px solid #e2e8f0; +} + +.nom-error { + padding: 8px 12px; + margin-bottom: 12px; + background: #fee2e2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #b91c1c; + font-size: 12px; + font-weight: 600; + + i { + margin-right: 6px; + } +} + +.nom-form-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 14px; +} + +.nom-cancel-btn { + padding: 7px 18px; + border-radius: 7px; + border: 1px solid #e2e8f0; + background: #fff; + color: #64748b; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + &:hover { + background: #f1f5f9; + } +} + +.nom-confirm-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 18px; + border-radius: 7px; + border: none; + background: #1d4ed8; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + + i { + font-size: 11px; + } + + &:hover { + background: #1e40af; + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts index 30b5f3be5..85161bf35 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts @@ -26,13 +26,50 @@ export class SavingsAccountCurrencyStepComponent { @Input() primaryClientId: number | null = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; - nomineeData: any = null; + nominees: any[] = []; + pendingNominee: any = null; + showNomineeForm = false; + nomineeError = ''; + + get totalShare(): number { + return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0); + } + + get remainingShare(): number { + return 100 - this.totalShare; + } + + get canAddMore(): boolean { + return this.totalShare < 100; + } onNomineeChange(event: any): void { - this.nomineeData = event; + this.pendingNominee = event; + this.nomineeError = ''; } - get liveNomineeData(): any { - return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + addNominee(): void { + const data = this.nomineeFormComp?.currentNomineeData ?? this.pendingNominee; + if (!data?.nomineeClientId) { + this.nomineeError = 'Please select a nominee before adding.'; + return; + } + if (this.nominees.some((n) => n.nomineeClientId === data.nomineeClientId)) { + this.nomineeError = 'This person is already added as a nominee.'; + return; + } + const newTotal = this.totalShare + (+data.percentageShare || 0); + if (newTotal > 100) { + this.nomineeError = `Total share cannot exceed 100%. Remaining: ${this.remainingShare}%`; + return; + } + this.nominees.push({ ...data }); + this.pendingNominee = null; + this.showNomineeForm = false; + this.nomineeError = ''; + } + + removeNominee(index: number): void { + this.nominees.splice(index, 1); } } diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html index afc4205d2..283f8db6f 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html @@ -473,78 +473,53 @@ } - @if (savingsAccountNominee) { + @if (savingsAccountNominees?.length > 0) {
Nominee Details - {{ savingsAccountNominee?.nominationType || 'DA1' }} + {{ savingsAccountNominees.length }} Nominee(s)
-
-
- Name - {{ savingsAccountNominee?.nomineeDisplayName || '—' }} + @for (nom of savingsAccountNominees; track nom.nomineeClientId; let i = $index) { + @if (i > 0) { +
+ } +
+
+ Name + {{ nom.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ nom.nomineeClientId || '—' }} +
+
+ Relationship + {{ nom.relationshipName || '—' }} +
+
+ Share % + {{ nom.percentageShare ?? '—' }}% +
+
+ Minor + {{ nom.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (nom.guardianDisplayName) { +
+ Guardian + {{ nom.guardianDisplayName }} +
+ } + @if (nom.remarks) { +
+ Remarks + {{ nom.remarks }} +
+ }
-
- Client ID - {{ savingsAccountNominee?.nomineeClientId || '—' }} -
-
- Relationship - {{ savingsAccountNominee?.relationshipName || '—' }} -
-
- Share % - {{ savingsAccountNominee?.percentageShare ?? '—' }}% -
-
- Minor - {{ savingsAccountNominee?.nomineeIsMinor ? 'Yes' : 'No' }} -
- @if (savingsAccountNominee?.nomineeDateOfBirth) { -
- Date of Birth - {{ savingsAccountNominee?.nomineeDateOfBirth | dateFormat }} -
- } - @if (savingsAccountNominee?.nomineeMobileNo) { -
- Mobile No - {{ savingsAccountNominee?.nomineeMobileNo }} -
- } - @if (savingsAccountNominee?.nomineeAadhaar) { -
- Aadhaar - {{ savingsAccountNominee?.nomineeAadhaar }} -
- } - @if (savingsAccountNominee?.nomineePan) { -
- PAN - {{ savingsAccountNominee?.nomineePan }} -
- } - @if (savingsAccountNominee?.guardianDisplayName) { -
- Guardian - {{ savingsAccountNominee?.guardianDisplayName }} -
- } - @if (savingsAccountNominee?.guardianClientId) { -
- Guardian Client ID - {{ savingsAccountNominee?.guardianClientId }} -
- } - @if (savingsAccountNominee?.remarks) { -
- Remarks - {{ savingsAccountNominee?.remarks }} -
- } -
+ }
} diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts index f29663102..7ce5f9f6e 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts @@ -90,8 +90,8 @@ export class SavingsAccountPreviewStepComponent implements OnChanges { @Input() savingsAccount: any; /** Digital facilities — user-modified values from digital step */ @Input() savingsAccountDigital: any; - /** Nominee data from currency step */ - @Input() savingsAccountNominee: any; + /** Nominees list from currency step */ + @Input() savingsAccountNominees: any[] = []; /** active Client Members in case of GSIM Account */ @Input() activeClientMembers?: any;