diff --git a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.html b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.html index de5b75ec9..edc0c6cca 100644 --- a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.html +++ b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.html @@ -331,9 +331,12 @@ @if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
@if (selectedClient.dateOfBirth) { - DOB {{ selectedClient.dateOfBirth | dateFormat }} + + DOB {{ selectedClient.dateOfBirth | dateFormat }} + @if (nomineeIsMinor && minorToggleLocked) { + Minor + } + } @if (selectedClient.panNumber) {
- Minor + Minor
@if (nomineeIsMinor) { -
- shield Guardian +
+
+ shield + Guardian + (Required for minor nominee) +
+ @if (!selectedGuardian) { - - - - @if (guardianSearchResults.length > 0 || guardianSearching) { -
- @if (guardianSearching) { -
- -
- } - @for (client of guardianSearchResults; track client.id) { -
- person - {{ client.displayName }} -
- } +
+ + Search Guardian by Name or Client ID + search + + + + @if (guardianSearchResults.length > 0 || guardianSearching) { +
+ @if (guardianSearching) { +
+ + Searching... +
+ } + @for (client of guardianSearchResults; track client.id) { +
+ person +
+ {{ client.displayName }} + ID: {{ client.id }} + @if (client.mobileNo) { + · {{ client.mobileNo }} + } + +
+ @if (selectedClient && client.id === selectedClient.id) { + Same as nominee + } +
+ } + @if ( + !guardianSearching && guardianSearchText.length >= 2 && guardianSearchResults.length === 0 + ) { +
No clients found
+ } +
+ } +
+ } @else { +
+ person +
+ {{ selectedGuardian.displayName }} + ID: {{ selectedGuardian.id }} + @if (selectedGuardian.mobileNo) { + · {{ selectedGuardian.mobileNo }} + } + +
+ +
+ @if (guardianSameAsNominee) { +
+ error_outline + Guardian and nominee cannot be the same client (same Client ID / CIF).
} - } @else { - {{ selectedGuardian.displayName }} - }
} @@ -439,7 +489,8 @@ !nomineeShare || nomineeSaving || nomineeShare > maxAllowedShare || - (nomineeIsMinor && !selectedGuardian) + (nomineeIsMinor && !selectedGuardian) || + guardianSameAsNominee " (click)="saveNominee()" > diff --git a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss index c5980ff99..3396f6492 100644 --- a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss +++ b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss @@ -620,6 +620,16 @@ $primary-light: #e8f0fe; color: #16a34a; } +.nom-minor-badge { + font-size: 10px; + font-weight: 700; + color: #92400e; + background: #fef3c7; + border: 1px solid #fcd34d; + border-radius: 4px; + padding: 1px 6px; +} + /* Nominee fields */ .nom-fields-row { display: flex; @@ -654,44 +664,141 @@ $primary-light: #e8f0fe; margin-top: 4px; } -/* Guardian */ -.nom-guardian-section { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px; -} +/* Guardian box — matches nominee-form style */ +.nom-guardian-box { + border: 1px solid #fde68a; + border-radius: 8px; + background: #fffbeb; + padding: 12px 14px; + margin-bottom: 12px; -.nom-guardian-label { - display: flex; - align-items: center; - gap: 4px; - font-size: 12px; - font-weight: 600; - color: #92400e; + &__header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 10px; + } - mat-icon { + &__icon { font-size: 16px; + width: 16px; + height: 16px; + color: #92400e; + } + + &__title { + font-size: 13px; + font-weight: 700; + color: #92400e; + } + + &__hint { + font-size: 11px; + color: #b45309; } } -.nom-guardian-selected { +.nom-guardian-search-wrap { + position: relative; +} + +.nom-guardian-search-field { + width: 100%; +} + +.nom-guardian-prefix-icon { + color: #94a3b8; + font-size: 18px; +} + +.nom-guardian-results { + position: absolute; + top: 56px; + left: 0; + right: 0; + z-index: 100; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 6px; + box-shadow: 0 6px 20px rgb(0 0 0 / 10%); + max-height: 200px; + overflow-y: auto; +} + +.nom-guardian-card { display: flex; align-items: center; - gap: 4px; - font-size: 13px; + gap: 10px; + padding: 8px 12px; + background: #fff; + border: 1px solid #fcd34d; + border-radius: 6px; + + &--error { + border-color: #ef4444; + background: #fef2f2; + } + + &__icon { + color: #92400e; + font-size: 20px; + flex-shrink: 0; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1e293b; + } + + &__meta { + font-size: 11px; + color: #64748b; + } +} + +/* Conflict validation */ +.nom-search-item--disabled { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; +} + +.nom-conflict-tag { + font-size: 10px; + font-weight: 600; + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 4px; + padding: 1px 6px; + white-space: nowrap; + margin-left: auto; +} + +.nom-guardian-selected--error { + color: #dc2626 !important; +} + +.nom-conflict-error { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 12px; font-weight: 500; - color: #1e293b; + color: #dc2626; - button { - width: 24px !important; - height: 24px !important; - line-height: 24px !important; - - mat-icon { - font-size: 14px; - } + mat-icon { + font-size: 15px; + width: 15px; + height: 15px; } } diff --git a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts index 99731db5f..039315fb2 100644 --- a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts +++ b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts @@ -75,6 +75,7 @@ export class NomineeTabComponent implements OnInit { nomineeRelationshipId: number | null = null; nomineeShare: number | null = null; nomineeIsMinor = false; + minorToggleLocked = false; nomineeSaving = false; nomineeRemarks = ''; @@ -153,13 +154,35 @@ export class NomineeTabComponent implements OnInit { this.clientsService.getClientAccountData(this.clientId).subscribe({ next: (data: any) => { this.loading = false; + const allSavings: any[] = data.savingsAccounts || []; + const savingsOnly = allSavings.filter( + (a: any) => + !a.depositType || + a.depositType?.code === 'depositAccountType.savingsDeposit' || + a.depositType?.value === 'Savings Deposit' + ); + const fdAccounts = [ + ...(data.fixedDepositAccounts || []), + ...allSavings.filter( + (a: any) => + a.depositType?.code === 'depositAccountType.fixedDeposit' || a.depositType?.value === 'Fixed Deposit' + ) + ]; + const rdAccounts = [ + ...(data.recurringDepositAccounts || []), + ...allSavings.filter( + (a: any) => + a.depositType?.code === 'depositAccountType.recurringDeposit' || + a.depositType?.value === 'Recurring Deposit' + ) + ]; this.accountGroups = [ { type: 'savings', apiType: 'SB', label: 'Savings Account', icon: 'savings', - accounts: this.filterActive(data.savingsAccounts) + accounts: this.filterActive(savingsOnly) }, { type: 'current', @@ -173,14 +196,14 @@ export class NomineeTabComponent implements OnInit { apiType: 'FD', label: 'Fixed Deposit', icon: 'lock', - accounts: this.filterActive(data.fixedDepositAccounts) + accounts: this.filterActive(fdAccounts) }, { type: 'rd', apiType: 'RD', label: 'Recurring Deposit', icon: 'autorenew', - accounts: this.filterActive(data.recurringDepositAccounts) + accounts: this.filterActive(rdAccounts) }, { type: 'loan', @@ -327,6 +350,9 @@ export class NomineeTabComponent implements OnInit { this.linkedProfileImage = null; this.linkedSignatureImage = null; this.linkedKycStatus = null; + this.nomineeIsMinor = false; + this.minorToggleLocked = false; + this.selectedGuardian = null; } private fetchClientDetails(clientId: number): void { @@ -346,6 +372,7 @@ export class NomineeTabComponent implements OnInit { next: (data: any) => { if (data?.dateOfBirth) { this.selectedClient = { ...this.selectedClient, dateOfBirth: data.dateOfBirth }; + this.checkMinorFromDob(data.dateOfBirth); } done(); }, @@ -401,6 +428,22 @@ export class NomineeTabComponent implements OnInit { }); } + private checkMinorFromDob(dob: any): void { + let birthDate: Date; + if (Array.isArray(dob)) { + birthDate = new Date(dob[0], dob[1] - 1, dob[2]); + } else { + birthDate = new Date(dob); + } + if (isNaN(birthDate.getTime())) return; + const today = new Date(); + let age = today.getFullYear() - birthDate.getFullYear(); + const m = today.getMonth() - birthDate.getMonth(); + if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) age--; + this.nomineeIsMinor = age < 18; + this.minorToggleLocked = true; + } + // ═══ Guardian ═══ onGuardianSearch(text: string): void { @@ -426,17 +469,29 @@ export class NomineeTabComponent implements OnInit { }, 400); } + dismissGuardianSearch(): void { + setTimeout(() => { + this.guardianSearchResults = []; + }, 200); + } + selectGuardian(client: any): void { + if (this.selectedClient && client.id === this.selectedClient.id) return; this.selectedGuardian = client; this.guardianSearchResults = []; this.guardianSearchText = ''; } + get guardianSameAsNominee(): boolean { + return !!(this.selectedGuardian && this.selectedClient && this.selectedGuardian.id === this.selectedClient.id); + } + // ═══ Save Nominee ═══ saveNominee(): void { if (!this.selectedClient || !this.nomineeRelationshipId || !this.nomineeShare) return; if (this.totalShare + this.nomineeShare > 100) return; + if (this.guardianSameAsNominee) return; this.nomineeSaving = true; const account = this.selectedAccount; @@ -579,6 +634,7 @@ export class NomineeTabComponent implements OnInit { this.nomineeRelationshipId = null; this.nomineeShare = null; this.nomineeIsMinor = false; + this.minorToggleLocked = false; this.nomineeRemarks = ''; this.selectedGuardian = null; this.searchText = ''; 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 6a81d16b6..2b60f9cf0 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 @@ -52,6 +52,7 @@
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 b221ea374..0a1bda31f 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 @@ -119,7 +119,7 @@ export class CreateCurrentAccountComponent { } get currentAccountNominee() { - return this.currentAccountCurrencyStep?.nomineeData ?? null; + return this.currentAccountCurrencyStep?.liveNomineeData ?? null; } get currentAccountCurrencyFlags() { @@ -185,7 +185,18 @@ export class CreateCurrentAccountComponent { currentAccount.groupId = this.currentAccountTemplate.groupId; } - console.log('Current Account Payload:', JSON.stringify(currentAccount, null, 2)); + 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 + }; + } this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => { this.router.navigate( 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 935e312dd..b55b3cfba 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,7 +73,11 @@
@if (currentAccountProductTemplate?.nominationFacility) { - + }
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 c3cae7dce..63aa2b84d 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 @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; @@ -27,6 +27,8 @@ import { FormsModule } from '@angular/forms'; }) export class CurrentAccountCurrencyStepComponent implements OnChanges { @Input() currentAccountProductTemplate: any; + @Input() primaryClientId: number | null = null; + @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nomineeData: any = null; @@ -48,6 +50,10 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { this.nomineeData = event; } + get liveNomineeData(): any { + return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + } + /** Called by parent to collect currency step values */ get currencyStepData() { return { diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html index 29ffc269e..2cf0ae0b6 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html @@ -53,7 +53,11 @@ @if (searchResults.length > 0) {
@for (client of searchResults; track client) { -
+
{{ (client.displayName || '?')[0].toUpperCase() }}
@@ -73,7 +77,11 @@ }
- chevron_right + @if (isExcluded(client)) { + Already added + } @else { + chevron_right + }
}
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss index 3e9fcdc7e..2028c02af 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss @@ -168,6 +168,24 @@ mifosx-add-related-client-dialog { transform: translateX(2px); } } + + &--disabled { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; + } +} + +.arc-excluded-tag { + font-size: 10px; + font-weight: 600; + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 4px; + padding: 1px 6px; + white-space: nowrap; + margin-left: auto; } .arc-result-avatar { diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts index bba5fd60d..81cfb1945 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts @@ -8,7 +8,7 @@ import { Component, inject, OnDestroy, ViewEncapsulation } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; -import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog'; +import { MatDialogRef, MatDialogContent, MatDialogActions, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatIcon } from '@angular/material/icon'; import { ClientsService } from 'app/clients/clients.service'; @@ -32,6 +32,7 @@ export class AddRelatedClientDialogComponent implements OnDestroy { private fb = inject(UntypedFormBuilder); private clientsService = inject(ClientsService); dialogRef = inject(MatDialogRef); + data: { excludedClientIds?: number[] } = inject(MAT_DIALOG_DATA, { optional: true }) ?? {}; form: UntypedFormGroup = this.fb.group({ searchText: [''], @@ -73,7 +74,7 @@ export class AddRelatedClientDialogComponent implements OnDestroy { } this.isSearching = true; this.searchTimer = setTimeout(() => { - this.clientsService.searchByText(value.trim(), 0, 10).subscribe({ + this.clientsService.searchByText(value.trim(), 0, 10, '', '', true).subscribe({ next: (res: any) => { this.searchResults = res?.content || []; this.isSearching = false; @@ -86,7 +87,13 @@ export class AddRelatedClientDialogComponent implements OnDestroy { }, 400); } + isExcluded(client: any): boolean { + const excluded = this.data?.excludedClientIds ?? []; + return excluded.includes(client.id); + } + selectClient(client: any): void { + if (this.isExcluded(client)) return; this.selectedClient = client; this.form.patchValue({ searchText: client.displayName }); this.searchResults = []; diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 4233ca0ed..437f00aa2 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -165,7 +165,10 @@ @if (productTemplate?.introducerRequired) { - + } } @@ -521,7 +524,10 @@ manage_accounts Search & Link Guardian (existing client) - + @if (guardianName) {
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index 5c031ba97..196fea25d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -360,10 +360,15 @@ export class CurrentAccountDetailsStepComponent implements OnInit { // ── Joint Account (Status of Account) ───────────────────────────────── openAddJointHolderDialog(): void { + const excludedClientIds = [ + ...(this.primaryClientId ? [this.primaryClientId] : []), + ...this.jointHolders.map((h: any) => h.clientId).filter(Boolean) + ]; const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', panelClass: 'arc-dialog-panel', - disableClose: true + disableClose: true, + data: { excludedClientIds } }); ref.afterClosed().subscribe((result) => { if (result) { @@ -434,10 +439,15 @@ export class CurrentAccountDetailsStepComponent implements OnInit { // ── Non-Individual (existing) helpers ────────────────────────────────── openAddRelatedClientDialog(): void { + const excludedClientIds = [ + ...(this.primaryClientId ? [this.primaryClientId] : []), + ...this.relatedClients.map((c: any) => c.clientId).filter(Boolean) + ]; const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', panelClass: 'arc-dialog-panel', - disableClose: true + disableClose: true, + data: { excludedClientIds } }); ref.afterClosed().subscribe((result) => { if (result) { 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 1fd091156..e4ab463e5 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 @@ -570,72 +570,64 @@
Nominee Details - - {{ currentAccountNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }} - + {{ currentAccountNominee?.nominationType || 'DA1' }}
Name - {{ - currentAccountNominee?.fullName || currentAccountNominee?.displayName || '—' - }} + {{ currentAccountNominee?.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ currentAccountNominee?.nomineeClientId || '—' }}
- @if (currentAccountNominee?.cifNumber) { -
- CIF No - {{ currentAccountNominee.cifNumber }} -
- }
Relationship - {{ currentAccountNominee?.relationship || '—' }} + {{ currentAccountNominee?.relationshipName || '—' }}
Share % - {{ currentAccountNominee?.nomineeShare ?? '—' }}% + {{ currentAccountNominee?.percentageShare ?? '—' }}%
- @if (currentAccountNominee?.dateOfBirth) { +
+ Minor + {{ currentAccountNominee?.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (currentAccountNominee?.nomineeDateOfBirth) {
Date of Birth - {{ currentAccountNominee?.dateOfBirth | dateFormat }} + {{ currentAccountNominee?.nomineeDateOfBirth | dateFormat }}
} - @if (currentAccountNominee?.nationality) { -
- Nationality - {{ currentAccountNominee?.nationality }} -
- } - @if (currentAccountNominee?.mobileNo) { + @if (currentAccountNominee?.nomineeMobileNo) {
Mobile No - {{ currentAccountNominee?.mobileNo }} + {{ currentAccountNominee?.nomineeMobileNo }}
} - @if (currentAccountNominee?.aadhaarNumber) { + @if (currentAccountNominee?.nomineeAadhaar) {
- Aadhaar No - {{ currentAccountNominee?.aadhaarNumber }} + Aadhaar + {{ currentAccountNominee?.nomineeAadhaar }}
} - @if (currentAccountNominee?.panNumber) { + @if (currentAccountNominee?.nomineePan) {
- PAN No - {{ currentAccountNominee?.panNumber }} + PAN + {{ currentAccountNominee?.nomineePan }}
} - @if (currentAccountNominee?.guardianName) { + @if (currentAccountNominee?.guardianDisplayName) {
- Guardian Name - {{ currentAccountNominee?.guardianName }} + Guardian + {{ currentAccountNominee?.guardianDisplayName }}
} - @if (currentAccountNominee?.guardianRelationship) { + @if (currentAccountNominee?.guardianClientId) {
- Guardian Relationship - {{ currentAccountNominee?.guardianRelationship }} + Guardian Client ID + {{ currentAccountNominee?.guardianClientId }}
} @if (currentAccountNominee?.remarks) { diff --git a/src/app/current-accounts/current-account-view/current-account-view.component.html b/src/app/current-accounts/current-account-view/current-account-view.component.html index ebbf6adb4..52c36d9ef 100644 --- a/src/app/current-accounts/current-account-view/current-account-view.component.html +++ b/src/app/current-accounts/current-account-view/current-account-view.component.html @@ -213,6 +213,15 @@ } + + Nominees + @for (currentAccountDatatable of currentAccountDatatables; track currentAccountDatatable) { - + [primaryClientId]="fixedDepositsAccountTemplate?.clientId ?? null" + >
@if (nominationFacility) { - + }
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 83556df4f..b581ff0b7 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 @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; @@ -27,6 +27,8 @@ import { FormsModule } from '@angular/forms'; }) export class FixedDepositAccountCurrencyStepComponent implements OnChanges { @Input() fixedDepositsAccountProductTemplate: any; + @Input() primaryClientId: number | null = null; + @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nomineeData: any = null; @@ -47,6 +49,10 @@ export class FixedDepositAccountCurrencyStepComponent implements OnChanges { this.nomineeData = event; } + get liveNomineeData(): any { + return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + } + /** Called by parent to collect currency step values */ get currencyStepData() { return { 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 cb87d103a..5498ce839 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 @@ -276,7 +276,11 @@ } @else {
@for (account of dalFilteredAccounts; track account.id) { -
+
{{ account.accountNo }} {{ account.productName }} @@ -653,7 +657,10 @@ manage_accounts Search & Link Guardian (existing client)
- + @if (guardianName) {
diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.scss index 3b8d912a6..c64960323 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.scss @@ -415,12 +415,18 @@ $gray-900: #202124; background 0.15s, box-shadow 0.15s; - &:hover { + &:hover:not(&--disabled) { border-color: #0ea5e9; background: #f0f9ff; box-shadow: 0 2px 8px rgb(14 165 233 / 12%); } + &--disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f8fafc; + } + &__details { flex: 1; display: flex; @@ -1289,36 +1295,31 @@ $gray-900: #202124; } .soa-guardian-selected { - margin-top: 16px; display: flex; align-items: center; gap: 12px; - padding: 12px 16px; - background: #fff; - border: 2px solid #f59e0b; - border-radius: 10px; + padding: 10px 14px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + margin-top: 10px; &__icon { - color: #f59e0b; font-size: 24px; height: 24px; width: 24px; + color: #16a34a; } &__name { - font-size: 14px; - font-weight: 700; - color: #1e293b; + font-size: 13px; + font-weight: 600; + color: #1f2937; } &__cif { font-size: 11px; - color: #64748b; - font-family: monospace; - } - - .soa-pill { - margin-left: auto; + color: #6b7280; } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts index 297db0824..f7be4a3a0 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts @@ -23,6 +23,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatIcon } from '@angular/material/icon'; import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; import { AlertService } from 'app/core/alert/alert.service'; +import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; /** * Fixed Deposits Account Details Step @@ -38,7 +39,8 @@ import { AlertService } from 'app/core/alert/alert.service'; FaIconComponent, MatStepperNext, FormsModule, - MatIcon + MatIcon, + IntroducerSearchComponent ] }) export class FixedDepositAccountDetailsStepComponent implements OnInit { @@ -208,6 +210,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { } selectLinkedAccount(account: any): void { + if (account.status !== 'Active') return; this.selectedLinkedAccount = account; } @@ -466,10 +469,15 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { // ── Joint Account helpers ────────────────────────────────────────────── openAddJointHolderDialog(): void { + const excludedClientIds = [ + ...(this.primaryClientId ? [this.primaryClientId] : []), + ...this.jointHolders.map((h: any) => h.clientId).filter(Boolean) + ]; const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', panelClass: 'arc-dialog-panel', - disableClose: true + disableClose: true, + data: { excludedClientIds } }); ref.afterClosed().subscribe((result) => { if (result) { 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 9cf7782fc..d6c29d337 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,47 +384,75 @@ } - @if (fixedDepositAccountData?.nominee) { + @if (fdNominee) {
Nominee Details - {{ - fixedDepositAccountData.nominee.mode === 'linked' ? 'Linked Client' : 'Manual Entry' - }} + {{ fdNominee?.nominationType || 'DA1' }}
- Full Name - {{ - fixedDepositAccountData.nominee.fullName || fixedDepositAccountData.nominee.displayName - }} + Name + {{ fdNominee?.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ fdNominee?.nomineeClientId || '—' }}
Relationship - {{ fixedDepositAccountData.nominee.relationship }} + {{ fdNominee?.relationshipName || '—' }}
- Nominee Share (%) - {{ fixedDepositAccountData.nominee.nomineeShare }}% + Share % + {{ fdNominee?.percentageShare ?? '—' }}%
- @if (fixedDepositAccountData.nominee.dateOfBirth) { +
+ Minor + {{ fdNominee?.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (fdNominee?.nomineeDateOfBirth) {
Date of Birth - {{ fixedDepositAccountData.nominee.dateOfBirth | dateFormat }} + {{ fdNominee?.nomineeDateOfBirth | dateFormat }}
} - @if (fixedDepositAccountData.nominee.nationality) { + @if (fdNominee?.nomineeMobileNo) {
- Nationality - {{ fixedDepositAccountData.nominee.nationality }} + Mobile No + {{ fdNominee?.nomineeMobileNo }}
} - @if (fixedDepositAccountData.nominee.mobileNo) { + @if (fdNominee?.nomineeAadhaar) {
- Mobile Number - {{ fixedDepositAccountData.nominee.mobileNo }} + 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 ee134eab3..35ebf69ea 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,6 +76,7 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { @Input() fixedDepositsAccountProductTemplate: any; /** Fixed Deposits Account Data */ @Input() fixedDepositAccountData: any; + @Input() fdNominee: any = null; /** 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/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html index 6c75c3bdb..8aef297b1 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.html @@ -166,6 +166,15 @@ {{ 'labels.inputs.Standing Instructions' | translate }}
} + + Nominees + @for (savingsDatatable of savingsDatatables; track savingsDatatable) { - + [primaryClientId]="recurringDepositsAccountTemplate?.clientId ?? null" + >
@if (recurringDepositsAccountProductTemplate?.nominationFacility) { - + }
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 e8cfabdaa..f52ec7331 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 @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; @@ -27,6 +27,8 @@ import { FormsModule } from '@angular/forms'; }) export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges { @Input() recurringDepositsAccountProductTemplate: any; + @Input() primaryClientId: number | null = null; + @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nomineeData: any = null; @@ -44,6 +46,10 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges this.nomineeData = event; } + get liveNomineeData(): any { + return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + } + /** Called by parent to collect currency step values */ get currencyStepData() { return { diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index be84c1d84..c4454be7e 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -246,7 +246,11 @@ } @else {
@for (account of dalFilteredAccounts; track account.id) { -
+
{{ account.accountNo }} {{ account.productName }} @@ -623,7 +627,10 @@ manage_accounts Search & Link Guardian (existing client)
- + @if (guardianName) {
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss index a78fa8828..116518005 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss @@ -415,12 +415,18 @@ $gray-900: #202124; background 0.15s, box-shadow 0.15s; - &:hover { + &:hover:not(&--disabled) { border-color: #0ea5e9; background: #f0f9ff; box-shadow: 0 2px 8px rgb(14 165 233 / 12%); } + &--disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f8fafc; + } + &__details { flex: 1; display: flex; diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 389803067..4b02d15f8 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -23,6 +23,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatIcon } from '@angular/material/icon'; import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; import { AlertService } from 'app/core/alert/alert.service'; +import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; /** * Recurring Deposits Account Details Step @@ -38,7 +39,8 @@ import { AlertService } from 'app/core/alert/alert.service'; FaIconComponent, MatStepperNext, FormsModule, - MatIcon + MatIcon, + IntroducerSearchComponent ] }) export class RecurringDepositsAccountDetailsStepComponent implements OnInit { @@ -185,6 +187,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { } selectLinkedAccount(account: any): void { + if (account.status !== 'Active') return; this.selectedLinkedAccount = account; } @@ -427,10 +430,15 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { // ── Joint Account helpers ────────────────────────────────────────────── openAddJointHolderDialog(): void { + const excludedClientIds = [ + ...(this.primaryClientId ? [this.primaryClientId] : []), + ...this.jointHolders.map((h: any) => h.clientId).filter(Boolean) + ]; const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', panelClass: 'arc-dialog-panel', - disableClose: true + disableClose: true, + data: { excludedClientIds } }); ref.afterClosed().subscribe((result) => { if (result) { 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 e26d30815..a92d830b2 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 @@ -546,76 +546,70 @@
Nominee Details - - {{ rdNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }} - + {{ rdNominee?.nominationType || 'DA1' }}
Name - {{ rdNominee?.fullName || rdNominee?.displayName || '—' }} + {{ rdNominee?.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ rdNominee?.nomineeClientId || '—' }}
- @if (rdNominee?.cifNumber) { -
- CIF No - {{ rdNominee.cifNumber }} -
- }
Relationship - {{ rdNominee?.relationship || '—' }} + {{ rdNominee?.relationshipName || '—' }}
Share % - {{ rdNominee?.nomineeShare ?? '—' }}% + {{ rdNominee?.percentageShare ?? '—' }}%
- @if (rdNominee?.dateOfBirth) { +
+ Minor + {{ rdNominee?.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (rdNominee?.nomineeDateOfBirth) {
Date of Birth - {{ rdNominee?.dateOfBirth | dateFormat }} + {{ rdNominee?.nomineeDateOfBirth | dateFormat }}
} - @if (rdNominee?.nationality) { -
- Nationality - {{ rdNominee.nationality }} -
- } - @if (rdNominee?.mobileNo) { + @if (rdNominee?.nomineeMobileNo) {
Mobile No - {{ rdNominee.mobileNo }} + {{ rdNominee?.nomineeMobileNo }}
} - @if (rdNominee?.aadhaarNumber) { + @if (rdNominee?.nomineeAadhaar) {
- Aadhaar No - {{ rdNominee.aadhaarNumber }} + Aadhaar + {{ rdNominee?.nomineeAadhaar }}
} - @if (rdNominee?.panNumber) { + @if (rdNominee?.nomineePan) {
- PAN No - {{ rdNominee.panNumber }} + PAN + {{ rdNominee?.nomineePan }}
} - @if (rdNominee?.guardianName) { + @if (rdNominee?.guardianDisplayName) {
- Guardian Name - {{ rdNominee.guardianName }} + Guardian + {{ rdNominee?.guardianDisplayName }}
} - @if (rdNominee?.guardianRelationship) { + @if (rdNominee?.guardianClientId) {
- Guardian Relationship - {{ rdNominee.guardianRelationship }} + Guardian Client ID + {{ rdNominee?.guardianClientId }}
} @if (rdNominee?.remarks) {
Remarks - {{ rdNominee.remarks }} + {{ rdNominee?.remarks }}
}
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html index 869cb9f07..8d3622c06 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.html @@ -172,6 +172,15 @@ {{ 'labels.inputs.Standing Instructions' | translate }}
} + + Nominees + @for (savingsDatatable of savingsDatatables; track savingsDatatable) { End Date - + + @if (CurrentProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) { + End Date must be after Effective To date + } diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts index 0c7125ca5..6815b7ade 100644 --- a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts @@ -92,6 +92,14 @@ export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { }); } + get minEndDate(): Date | null { + const v = this.CurrentProductDetailsForm.get('effectiveTo')?.value; + if (!v) return null; + const d = new Date(v); + d.setDate(d.getDate() + 1); + return d; + } + private getOptionNames(options: any[]): string[] { if (!Array.isArray(options)) { return []; diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html index a00fbdef9..bf0e0f6b2 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html @@ -106,9 +106,12 @@ End Date - + + @if (fixedDepositProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) { + End Date must be after Effective To date + }
diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts index 76efeae01..8df6052c6 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts @@ -83,6 +83,14 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange }); } + get minEndDate(): Date | null { + const v = this.fixedDepositProductDetailsForm.get('effectiveTo')?.value; + if (!v) return null; + const d = new Date(v); + d.setDate(d.getDate() + 1); + return d; + } + private getOptionNames(options: any[]): string[] { if (!Array.isArray(options)) return []; return options.map((o) => o?.name ?? o?.value ?? o).filter((v) => typeof v === 'string' && v.trim().length > 0); diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html index c85e13da2..7940e9c35 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html @@ -96,9 +96,12 @@ End Date - + + @if (recurringDepositProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) { + End Date must be after Effective To date + } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts index c0b089668..3adb6da84 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts @@ -80,6 +80,14 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh }); } + get minEndDate(): Date | null { + const v = this.recurringDepositProductDetailsForm.get('effectiveTo')?.value; + if (!v) return null; + const d = new Date(v); + d.setDate(d.getDate() + 1); + return d; + } + private getOptionNames(options: any[]): string[] { if (!Array.isArray(options)) { return []; diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html index 7a8c5d3dc..f4ca2d0d5 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html @@ -99,9 +99,12 @@ End Date - + + @if (savingProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) { + End Date must be after Effective To date + } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts index c9d2ba566..6bf721fd4 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts @@ -95,6 +95,14 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { }); } + get minEndDate(): Date | null { + const v = this.savingProductDetailsForm.get('effectiveTo')?.value; + if (!v) return null; + const d = new Date(v); + d.setDate(d.getDate() + 1); + return d; + } + private getOptionNames(options: any[]): string[] { if (!Array.isArray(options)) { return []; 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 edd2aea77..77584d630 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 @@ -50,8 +50,10 @@ Currency - - +
@if (savingsAccountProductTemplate?.nominationFacility) { - + }
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 31e029241..30b5f3be5 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 @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input } from '@angular/core'; +import { Component, Input, ViewChild } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; @@ -23,10 +23,16 @@ import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dor }) export class SavingsAccountCurrencyStepComponent { @Input() savingsAccountProductTemplate: any; + @Input() primaryClientId: number | null = null; + @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nomineeData: any = null; onNomineeChange(event: any): void { this.nomineeData = event; } + + get liveNomineeData(): any { + return this.nomineeFormComp?.currentNomineeData ?? this.nomineeData; + } } diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index 1fd029dc5..5ac5521e6 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -132,7 +132,10 @@
@if (productTemplate?.introducerRequired) { - + } } @@ -490,7 +493,10 @@ manage_accounts Search & Link Guardian (existing client)
- + @if (guardianName) {
diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index 17cd616b0..ac67782d2 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -330,10 +330,15 @@ export class SavingsAccountDetailsStepComponent implements OnInit { // ── Joint Account helpers ────────────────────────────────────────────── openAddJointHolderDialog(): void { + const excludedClientIds = [ + ...(this.primaryClientId ? [this.primaryClientId] : []), + ...this.jointHolders.map((h: any) => h.clientId).filter(Boolean) + ]; const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', panelClass: 'arc-dialog-panel', - disableClose: true + disableClose: true, + data: { excludedClientIds } }); ref.afterClosed().subscribe((result) => { if (result) { 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 faf9e2b30..afc4205d2 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 @@ -478,79 +478,64 @@
Nominee Details - - {{ savingsAccountNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }} - + {{ savingsAccountNominee?.nominationType || 'DA1' }}
-
Name - {{ - savingsAccountNominee?.fullName || savingsAccountNominee?.displayName || '—' - }} + {{ savingsAccountNominee?.nomineeDisplayName || '—' }} +
+
+ Client ID + {{ savingsAccountNominee?.nomineeClientId || '—' }}
- @if (savingsAccountNominee?.cifNumber) { -
- CIF No - {{ savingsAccountNominee.cifNumber }} -
- }
Relationship - {{ savingsAccountNominee?.relationship || '—' }} + {{ savingsAccountNominee?.relationshipName || '—' }}
Share % - {{ savingsAccountNominee?.nomineeShare ?? '—' }}% + {{ savingsAccountNominee?.percentageShare ?? '—' }}%
- @if (savingsAccountNominee?.dateOfBirth) { +
+ Minor + {{ savingsAccountNominee?.nomineeIsMinor ? 'Yes' : 'No' }} +
+ @if (savingsAccountNominee?.nomineeDateOfBirth) {
Date of Birth - {{ savingsAccountNominee?.dateOfBirth | dateFormat }} + {{ savingsAccountNominee?.nomineeDateOfBirth | dateFormat }}
} - @if (savingsAccountNominee?.nationality) { -
- Nationality - {{ savingsAccountNominee?.nationality }} -
- } - @if (savingsAccountNominee?.mobileNo) { + @if (savingsAccountNominee?.nomineeMobileNo) {
Mobile No - {{ savingsAccountNominee?.mobileNo }} + {{ savingsAccountNominee?.nomineeMobileNo }}
} - @if (savingsAccountNominee?.aadhaarNumber) { + @if (savingsAccountNominee?.nomineeAadhaar) {
- Aadhaar No - {{ savingsAccountNominee?.aadhaarNumber }} + Aadhaar + {{ savingsAccountNominee?.nomineeAadhaar }}
} - @if (savingsAccountNominee?.panNumber) { + @if (savingsAccountNominee?.nomineePan) {
- PAN No - {{ savingsAccountNominee?.panNumber }} + PAN + {{ savingsAccountNominee?.nomineePan }}
} - @if (savingsAccountNominee?.isDependent) { + @if (savingsAccountNominee?.guardianDisplayName) {
- Dependent - Yes + Guardian + {{ savingsAccountNominee?.guardianDisplayName }}
} - @if (savingsAccountNominee?.guardianName) { + @if (savingsAccountNominee?.guardianClientId) {
- Guardian Name - {{ savingsAccountNominee?.guardianName }} -
- } - @if (savingsAccountNominee?.guardianRelationship) { -
- Guardian Relationship - {{ savingsAccountNominee?.guardianRelationship }} + Guardian Client ID + {{ savingsAccountNominee?.guardianClientId }}
} @if (savingsAccountNominee?.remarks) { diff --git a/src/app/savings/savings-account-view/savings-account-view.component.html b/src/app/savings/savings-account-view/savings-account-view.component.html index 50e708172..266957c89 100644 --- a/src/app/savings/savings-account-view/savings-account-view.component.html +++ b/src/app/savings/savings-account-view/savings-account-view.component.html @@ -213,6 +213,15 @@
} + + Nominees + @for (savingsDatatable of savingsDatatables; track savingsDatatable) { +
+
+
+ shield +
+

Nomination Facility

+

Manage nominees for this account. Nominees are account-specific.

+
+
+ @if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm) { + + } +
+ + @if (loading) { +
+ + Loading account... +
+ } + + @if (!loading) { + @if (nomineesLoading) { +
+ + Loading nominees... +
+ } + + @if (nominees.length > 0) { + + } + + @for (nominee of nominees; track nominee.id; let i = $index) { +
+
+
+ @if (nominee.profileImageUrl) { + + } @else { + person + } +
+
+ {{ nominee.nomineeDisplayName || nominee.firstName || '—' }} + + {{ getRelationship(nominee) }} + @if (nominee.nomineeDateOfBirth || nominee.dateOfBirth) { + · DOB: {{ nominee.nomineeDateOfBirth || nominee.dateOfBirth | dateFormat }} + } + @if (nominee.nomineeMobileNo || nominee.mobileNo) { + · {{ nominee.nomineeMobileNo || nominee.mobileNo }} + } + +
+ @if (nominee.signatureUrl) { +
+ Signature +
+ } + @if (nominee.nomineeIdentifier) { + {{ nominee.nomineeIdentifier }} + } + +
+ + + +
+
+ @if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) { +
+ shield + Guardian: {{ nominee.guardianDisplayName || nominee.guardianName }} + @if (nominee.guardianRelationshipName) { + ({{ nominee.guardianRelationshipName }}) + } +
+ } + @if (editingIndex === i) { +
+
+ + Type + + @for (opt of nominationTypeOptions; track opt.value) { + {{ opt.label }} + } + + + + Relationship + + @for (rel of relationshipOptions; track rel) { + {{ rel.name }} + } + + + + Share % + + @if (editShare && editShare > maxShareForEdit(i)) { + Max {{ maxShareForEdit(i) }}% + } + + + +
+
+ } +
+ } + + @if (!nomineesLoading && nominees.length === 0 && !showAddForm) { +
+ person_add + No nominees registered for this account. +
+ } + + @if (showAddForm) { +
+
Search & Link Nominee
+ + @if (!selectedClient) { +
+ + Search by Name, Client ID or Mobile + search + + + @if (searchResults.length > 0 || searching) { +
+ @if (searching) { +
+ Searching... +
+ } + @for (client of searchResults; track client.id) { +
+ person +
+ {{ client.displayName }} + ID: {{ client.id }} · {{ client.officeName }} + @if (client.dateOfBirth) { + · DOB: {{ client.dateOfBirth | dateFormat }} + } + @if (client.mobileNo) { + · {{ client.mobileNo }} + } + +
+
+ } + @if (!searching && searchText.length >= 2 && searchResults.length === 0) { +
No clients found
+ } +
+ } +
+ } + + @if (selectedClient) { +
+
+ @if (linkedProfileImage) { + + } @else { + person + } +
+
+ {{ selectedClient.displayName }} + ID: {{ selectedClient.id }} + @if (selectedClient.mobileNo) { + · {{ selectedClient.mobileNo }} + } + +
+ @if (linkedSignatureImage) { +
+ Signature +
+ } + +
+ + @if (fetchingDetails) { +
+ Fetching details... +
+ } + + @if (!fetchingDetails) { + @if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) { +
+ @if (selectedClient.dateOfBirth) { + DOB {{ selectedClient.dateOfBirth | dateFormat }} + } + @if (selectedClient.panNumber) { + PAN {{ selectedClient.panNumber }} + @if (linkedKycStatus?.panVerified) { + check_circle + } + + } + @if (selectedClient.aadhaarMasked) { + Aadhaar {{ selectedClient.aadhaarMasked }} + @if (linkedKycStatus?.aadhaarVerified) { + check_circle + } + + } +
+ } + +
+ + Nomination Type + + @for (opt of nominationTypeOptions; track opt.value) { + {{ opt.label }} + } + + + + Relationship + + @for (rel of relationshipOptions; track rel) { + {{ rel.name }} + } + + + + Share % + + @if (nomineeShare && nomineeShare > maxAllowedShare) { + Max {{ maxAllowedShare }}% + } + +
+ Minor +
+
+ + @if (nomineeIsMinor) { +
+ shield Guardian + @if (!selectedGuardian) { + + + + @if (guardianSearchResults.length > 0 || guardianSearching) { +
+ @if (guardianSearching) { +
+ +
+ } + @for (client of guardianSearchResults; track client.id) { +
+ person + {{ client.displayName }} + @if (selectedClient && client.id === selectedClient.id) { + Same as nominee + } +
+ } +
+ } + } @else { + {{ selectedGuardian.displayName }} + + @if (guardianSameAsNominee) { +
+ error_outline + Guardian and nominee cannot be the same client (same Client ID / CIF). +
+ } + } +
+ } + + + Remarks + + + +
+ + +
+ } + } +
+ } + } +
diff --git a/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss b/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss new file mode 100644 index 000000000..c25ab0e21 --- /dev/null +++ b/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss @@ -0,0 +1,647 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +$primary: #1074b9; +$primary-light: #e8f0fe; + +.nom-container { + padding: 16px 24px; +} + +.nom-page-header { + background: #0f172a; + padding: 14px 20px; + border-radius: 10px; + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + + &__left { + display: flex; + align-items: center; + gap: 10px; + } + + &__icon { + color: #60a5fa; + } + + &__title { + font-size: 15px; + font-weight: 600; + color: #fff; + margin: 0; + } + + &__sub { + font-size: 10px; + color: rgb(255 255 255 / 45%); + margin: 1px 0 0; + } +} + +.nom-add-btn { + border-radius: 6px !important; + font-size: 12px !important; + height: 36px !important; + white-space: nowrap; +} + +.nom-loading { + display: flex; + align-items: center; + gap: 8px; + padding: 24px 0; + justify-content: center; + font-size: 13px; + color: #64748b; + + &--sm { + padding: 12px 0; + } +} + +.nom-share-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 6px; + margin-bottom: 8px; + + &__label { + font-size: 11px; + font-weight: 600; + color: #64748b; + white-space: nowrap; + } + + &__track { + flex: 1; + height: 6px; + background: #e2e8f0; + border-radius: 3px; + overflow: hidden; + } + + &__fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + background: linear-gradient(90deg, #3b82f6, #10b981); + + &.over { + background: #ef4444; + } + + &.complete { + background: linear-gradient(90deg, #10b981, #059669); + } + } + + &__value { + font-size: 13px; + font-weight: 700; + color: #10b981; + min-width: 35px; + text-align: right; + + &.over { + color: #ef4444; + } + } +} + +.nom-card { + border: 1px solid #e2e8f0; + border-radius: 6px; + margin-bottom: 6px; + overflow: hidden; +} + +.nom-card__main { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; +} + +.nom-card__avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #dbeafe; + display: flex; + align-items: center; + justify-content: center; + color: #1565c0; + flex-shrink: 0; + + mat-icon { + font-size: 18px; + } +} + +.nom-card__photo { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; +} + +.nom-card__sig { + flex-shrink: 0; + + img { + height: 24px; + max-width: 60px; + object-fit: contain; + border: 1px solid #e2e8f0; + border-radius: 3px; + background: #fff; + padding: 1px 3px; + } +} + +.nom-card__info { + flex: 1; + min-width: 0; +} + +.nom-card__name { + font-weight: 600; + font-size: 13px; + color: #1e293b; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nom-card__meta { + font-size: 11px; + color: #64748b; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nom-card__id { + font-size: 10px; + font-weight: 600; + color: #1d4ed8; + background: #eff6ff; + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; + font-family: 'Roboto Mono', monospace; + letter-spacing: 0.3px; + flex-shrink: 0; +} + +.nom-card__share { + text-align: center; + min-width: 40px; + flex-shrink: 0; +} + +.nom-card__share-value { + font-size: 15px; + font-weight: 700; + color: #1565c0; +} + +.nom-card__actions { + display: flex; + gap: 0; + flex-shrink: 0; + + button { + width: 32px !important; + height: 32px !important; + line-height: 32px !important; + + mat-icon { + font-size: 18px; + } + } +} + +.nom-card__delete { + color: #94a3b8 !important; + + &:hover { + color: #ef4444 !important; + } +} + +.nom-card__guardian { + display: flex; + align-items: center; + gap: 5px; + padding: 5px 12px; + background: #fff7ed; + font-size: 11px; + color: #92400e; + border-top: 1px solid #fde68a; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } +} + +.nom-card__edit { + padding: 8px 12px; + border-top: 1px solid #e2e8f0; + background: #f8fafc; +} + +.nom-edit-row { + display: flex; + align-items: center; + gap: 8px; +} + +.nom-edit-field { + flex: 1; + + &--sm { + max-width: 100px; + } +} + +.nom-edit-save, +.nom-edit-cancel { + min-width: 36px !important; + height: 36px !important; + padding: 0 !important; +} + +.nom-empty { + text-align: center; + padding: 24px; + background: #f8fafc; + border-radius: 6px; + border: 1px dashed #e2e8f0; + + mat-icon { + font-size: 28px; + width: 28px; + height: 28px; + color: #94a3b8; + display: block; + margin: 0 auto 4px; + } + + span { + font-size: 12px; + color: #94a3b8; + } +} + +.nom-add-form { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 14px; + margin-top: 8px; + background: #fafbfc; + + &__title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: #1e293b; + margin-bottom: 12px; + } +} + +.nom-search-wrapper { + position: relative; + margin-bottom: 8px; +} + +.nom-search-field { + width: 100%; +} + +.nom-search-prefix { + color: #94a3b8; +} + +.nom-search-results { + position: absolute; + top: 56px; + left: 0; + right: 0; + z-index: 100; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 6px; + box-shadow: 0 6px 20px rgb(0 0 0 / 10%); + max-height: 220px; + overflow-y: auto; + + &--inline { + position: relative; + top: 0; + margin-top: 4px; + } +} + +.nom-search-loading { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 12px; + font-size: 12px; + color: #64748b; +} + +.nom-search-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid #f1f5f9; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: #f0f7ff; + } + + &__icon { + color: $primary; + font-size: 18px; + } + + &__info { + display: flex; + flex-direction: column; + flex: 1; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1e293b; + } + + &__meta { + font-size: 10px; + color: #64748b; + } +} + +.search-no-results-hint { + font-size: 11px; + color: #94a3b8; + text-align: center; + padding: 4px 0; +} + +.nom-linked-card { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 6px; + margin-bottom: 8px; + + &__left { + flex-shrink: 0; + } + + &__icon { + color: $primary; + } + + &__photo { + width: 36px; + height: 36px; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e2e8f0; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1e293b; + } + + &__meta { + font-size: 11px; + color: #64748b; + font-family: 'Roboto Mono', monospace; + } + + &__sig { + flex-shrink: 0; + + img { + height: 24px; + max-width: 60px; + object-fit: contain; + border: 1px solid #e2e8f0; + border-radius: 3px; + background: #fff; + padding: 1px 3px; + } + } +} + +.nom-fetching-hint { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: #64748b; + margin: -4px 0 8px; +} + +.nom-readonly-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + padding: 8px 12px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 6px; + margin-bottom: 10px; +} + +.nom-ro-item { + font-size: 12px; + color: #1e293b; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.nom-ro-label { + font-size: 10px; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + margin-right: 2px; +} + +.nom-ro-ok { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + color: #16a34a; +} + +.nom-fields-row { + display: flex; + flex-wrap: wrap; + gap: 0 12px; + align-items: center; + margin-bottom: 4px; +} + +.nom-field-item { + flex: 1; + min-width: 160px; + + &--sm { + max-width: 100px; + min-width: 80px; + } + + &--toggle { + padding-top: 8px; + } +} + +.nom-remarks-field { + width: 100%; +} + +.nom-form-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 4px; +} + +.nom-guardian-section { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.nom-guardian-label { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 600; + color: #92400e; + + mat-icon { + font-size: 16px; + } +} + +.nom-guardian-selected { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + font-weight: 500; + color: #1e293b; + + button { + width: 24px !important; + height: 24px !important; + line-height: 24px !important; + + mat-icon { + font-size: 14px; + } + } +} + +.nom-search-item--disabled { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; +} + +.nom-conflict-tag { + font-size: 10px; + font-weight: 600; + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 4px; + padding: 1px 6px; + white-space: nowrap; + margin-left: auto; +} + +.nom-guardian-selected--error { + color: #dc2626 !important; +} + +.nom-conflict-error { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 12px; + font-weight: 500; + color: #dc2626; + + mat-icon { + font-size: 15px; + width: 15px; + height: 15px; + } +} + +.zoomable { + cursor: zoom-in; + transition: transform 0.15s; + + &:hover { + transform: scale(1.08); + box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + } +} + +::ng-deep .image-zoom-panel { + .mat-mdc-dialog-container { + padding: 0 !important; + border-radius: 8px !important; + overflow: hidden; + } +} diff --git a/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts b/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts new file mode 100644 index 000000000..bb2bf805f --- /dev/null +++ b/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts @@ -0,0 +1,459 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { Component, OnInit, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { ClientsService } from 'app/clients/clients.service'; +import { DeleteDialogComponent } from '../delete-dialog/delete-dialog.component'; +import { ImageZoomDialogComponent } from 'app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { DateFormatPipe } from '../../pipes/date-format.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'mifosx-account-nominees-tab', + templateUrl: './account-nominees-tab.component.html', + styleUrls: ['./account-nominees-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + FaIconComponent, + MatIcon, + MatProgressSpinner, + MatTooltip, + MatSlideToggle, + DateFormatPipe + ] +}) +export class AccountNomineesTabComponent implements OnInit { + private route = inject(ActivatedRoute); + private clientsService = inject(ClientsService); + private dialog = inject(MatDialog); + private sanitizer = inject(DomSanitizer); + + /** Set by route data */ + accountDataKey: string = ''; + accountType: string = ''; + + clientId: string = ''; + accountNo: string = ''; + loading = true; + + nominees: any[] = []; + nomineesLoading = false; + + showAddForm = false; + searchText = ''; + searchResults: any[] = []; + searching = false; + private searchTimer: any = null; + selectedClient: any = null; + fetchingDetails = false; + + nomineeRelationshipId: number | null = null; + nomineeShare: number | null = null; + nomineeIsMinor = false; + nomineeSaving = false; + nomineeRemarks = ''; + nomineeNomType = 'DA1'; + + guardianSearchText = ''; + guardianSearchResults: any[] = []; + guardianSearching = false; + selectedGuardian: any = null; + private guardianSearchTimer: any = null; + + linkedProfileImage: SafeUrl | null = null; + linkedSignatureImage: SafeUrl | null = null; + linkedKycStatus: any = null; + + relationshipOptions: any[] = []; + + nominationTypeOptions = [ + { value: 'DA1', label: 'DA1 - Add Nominee' }, + { value: 'DA5', label: 'DA5 - Modify / Cancel Nominee' } + ]; + + editingIndex: number | null = null; + editRelId: number | null = null; + editShare: number | null = null; + editNomType: string = 'DA1'; + + get totalShare(): number { + return this.nominees.reduce((sum, n) => sum + (n.percentageShare || 0), 0); + } + + get maxAllowedShare(): number { + return 100 - this.totalShare; + } + + ngOnInit(): void { + this.accountDataKey = this.route.snapshot.data['accountDataKey'] || ''; + this.accountType = this.route.snapshot.data['accountType'] || ''; + + this.route.parent.data.subscribe((data: any) => { + const accountData = data[this.accountDataKey]; + if (accountData) { + this.clientId = accountData.clientId?.toString() || ''; + this.accountNo = accountData.accountNo || ''; + this.loading = false; + this.loadNominees(); + this.loadTemplate(); + } + }); + } + + private loadTemplate(): void { + this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({ + next: (template: any) => { + this.relationshipOptions = template?.relationshipOptions || []; + }, + error: () => { + this.clientsService.getClientTemplate().subscribe((t: any) => { + this.relationshipOptions = t?.familyMemberOptions?.relationshipIdOptions || []; + }); + } + }); + } + + private loadNominees(): void { + this.nomineesLoading = true; + this.clientsService.getClientNominees(this.clientId, this.accountNo, this.accountType).subscribe({ + next: (data: any) => { + this.nomineesLoading = false; + this.nominees = Array.isArray(data) ? data : []; + this.nominees.forEach((n) => this.enrichNomineeImages(n)); + }, + error: () => { + this.nomineesLoading = false; + this.nominees = []; + } + }); + } + + private enrichNomineeImages(nominee: any): void { + const cid = nominee.nomineeClientId?.toString(); + if (!cid) return; + this.clientsService.getClientProfileImage(cid).subscribe({ + next: (img: any) => { + if (img) nominee.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img); + }, + error: () => {} + }); + this.clientsService.getClientDocuments(cid).subscribe({ + next: (docs: any[]) => { + const sigDoc = docs?.find( + (d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature') + ); + if (sigDoc) { + this.clientsService.getClientSignatureImage(cid, sigDoc.id.toString()).subscribe({ + next: (blob: Blob) => { + nominee.signatureUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); + }, + error: () => {} + }); + } + }, + error: () => {} + }); + } + + onSearch(text: string): void { + this.searchText = text; + clearTimeout(this.searchTimer); + if (!text || text.length < 2) { + this.searchResults = []; + return; + } + this.searching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ + next: (data: any) => { + this.searchResults = data?.content || []; + this.searching = false; + this.clientsService.enrichSearchResults(this.searchResults); + }, + error: () => { + this.searchResults = []; + this.searching = false; + } + }); + }, 400); + } + + dismissSearch(): void { + setTimeout(() => { + this.searchResults = []; + this.searching = false; + }, 200); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.searchResults = []; + this.searchText = ''; + this.fetchClientDetails(client.id); + } + + removeSelectedClient(): void { + this.selectedClient = null; + this.linkedProfileImage = null; + this.linkedSignatureImage = null; + this.linkedKycStatus = null; + } + + private fetchClientDetails(clientId: number): void { + this.fetchingDetails = true; + this.linkedProfileImage = null; + this.linkedSignatureImage = null; + this.linkedKycStatus = null; + let pending = 4; + const done = () => { + pending--; + if (pending <= 0) this.fetchingDetails = false; + }; + + this.clientsService.getClientData(clientId.toString()).subscribe({ + next: (data: any) => { + if (data?.dateOfBirth) this.selectedClient = { ...this.selectedClient, dateOfBirth: data.dateOfBirth }; + done(); + }, + error: () => done() + }); + + this.clientsService.getClientKyc(clientId.toString()).subscribe({ + next: (kyc: any) => { + if (kyc) { + this.selectedClient = { + ...this.selectedClient, + panNumber: kyc.panNumber, + aadhaarMasked: kyc.aadhaarNumberMasked + }; + this.linkedKycStatus = { + panVerified: kyc.panVerified || false, + aadhaarVerified: kyc.aadhaarVerified || false + }; + } + done(); + }, + error: () => done() + }); + + this.clientsService.getClientProfileImage(clientId.toString()).subscribe({ + next: (img: any) => { + if (img) this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img); + done(); + }, + error: () => done() + }); + + this.clientsService.getClientDocuments(clientId.toString()).subscribe({ + next: (docs: any[]) => { + const sigDoc = docs?.find( + (d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature') + ); + if (sigDoc) { + this.clientsService.getClientSignatureImage(clientId.toString(), sigDoc.id.toString()).subscribe({ + next: (blob: Blob) => { + this.linkedSignatureImage = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); + done(); + }, + error: () => done() + }); + } else { + done(); + } + }, + error: () => done() + }); + } + + onGuardianSearch(text: string): void { + this.guardianSearchText = text; + clearTimeout(this.guardianSearchTimer); + if (!text || text.length < 2) { + this.guardianSearchResults = []; + return; + } + this.guardianSearching = true; + this.guardianSearchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ + next: (data: any) => { + this.guardianSearchResults = data?.content || []; + this.guardianSearching = false; + this.clientsService.enrichSearchResults(this.guardianSearchResults); + }, + error: () => { + this.guardianSearchResults = []; + this.guardianSearching = false; + } + }); + }, 400); + } + + selectGuardian(client: any): void { + if (this.selectedClient && client.id === this.selectedClient.id) return; + this.selectedGuardian = client; + this.guardianSearchResults = []; + this.guardianSearchText = ''; + } + + get guardianSameAsNominee(): boolean { + return !!(this.selectedGuardian && this.selectedClient && this.selectedGuardian.id === this.selectedClient.id); + } + + saveNominee(): void { + if (!this.selectedClient || !this.nomineeRelationshipId || !this.nomineeShare) return; + if (this.totalShare + this.nomineeShare > 100) return; + if (this.guardianSameAsNominee) return; + + this.nomineeSaving = true; + const payload: any = { + nomineeClientId: this.selectedClient.id, + accountNo: this.accountNo, + accountType: this.accountType, + nominationType: this.nomineeNomType, + relationshipId: this.nomineeRelationshipId, + percentageShare: this.nomineeShare, + nomineeIsMinor: this.nomineeIsMinor, + remarks: this.nomineeRemarks || null, + locale: 'en' + }; + + if (this.nomineeIsMinor && this.selectedGuardian) { + payload.guardianClientId = this.selectedGuardian.id; + } + + this.clientsService.addNominee(this.clientId, payload).subscribe({ + next: () => { + this.nomineeSaving = false; + this.loadNominees(); + this.resetAddForm(); + }, + error: () => { + this.nomineeSaving = false; + } + }); + } + + viewNominee(nominee: any): void { + const clientId = nominee.nomineeClientId || nominee.linkedClientId || nominee.clientId; + const displayName = nominee.nomineeDisplayName || nominee.firstName || ''; + import('app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component').then( + (m) => { + this.dialog.open(m.ClientFamilyMemberDialogComponent, { + data: { + context: 'View', + member: { + firstName: displayName, + relationshipId: nominee.relationshipId, + dateOfBirth: nominee.nomineeDateOfBirth || nominee.dateOfBirth, + linkedClientId: clientId, + linkedClientName: displayName, + isAlive: true, + isDependent: nominee.nomineeIsMinor, + panNumber: nominee.panNumber || '', + aadhaarNumber: nominee.aadhaarMasked || '' + }, + options: { relationshipIdOptions: this.relationshipOptions } + }, + width: '640px', + maxWidth: '95vw', + panelClass: 'fm-dialog-panel' + }); + } + ); + } + + editNominee(nominee: any, index: number): void { + this.editingIndex = index; + this.editRelId = nominee.relationshipId || null; + this.editShare = nominee.percentageShare; + this.editNomType = nominee.nominationType || 'DA1'; + } + + cancelEdit(): void { + this.editingIndex = null; + } + + maxShareForEdit(index: number): number { + return 100 - this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0); + } + + saveEdit(nominee: any, index: number): void { + if (!this.editRelId || !this.editShare || this.editShare > this.maxShareForEdit(index)) return; + + const payload: any = { + relationshipId: this.editRelId, + percentageShare: this.editShare, + nominationType: this.editNomType, + locale: 'en' + }; + + this.clientsService.updateNominee(this.clientId, nominee.id.toString(), payload).subscribe({ + next: () => { + this.loadNominees(); + this.editingIndex = null; + }, + error: () => {} + }); + } + + deleteNominee(nominee: any, index: number): void { + const ref = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: `nominee ${nominee.nomineeDisplayName || nominee.firstName || ''}` } + }); + ref.afterClosed().subscribe((response: any) => { + if (response?.delete) { + this.clientsService.deleteNominee(this.clientId, nominee.id.toString()).subscribe({ + next: () => this.nominees.splice(index, 1), + error: () => this.nominees.splice(index, 1) + }); + } + }); + } + + zoomImage(imageSrc: any, title: string): void { + this.dialog.open(ImageZoomDialogComponent, { + data: { imageSrc, title }, + panelClass: 'image-zoom-panel', + maxWidth: '90vw', + maxHeight: '90vh' + }); + } + + getRelationship(member: any): string { + if (member.relationshipName) return member.relationshipName; + if (member.relationship) return member.relationship; + if (typeof member.relationshipId === 'object' && member.relationshipId?.name) return member.relationshipId.name; + const opt = this.relationshipOptions.find((r: any) => r.id === member.relationshipId); + return opt?.name || ''; + } + + resetAddForm(): void { + this.showAddForm = false; + this.selectedClient = null; + this.linkedProfileImage = null; + this.linkedSignatureImage = null; + this.linkedKycStatus = null; + this.nomineeNomType = 'DA1'; + this.nomineeRelationshipId = null; + this.nomineeShare = null; + this.nomineeIsMinor = false; + this.nomineeRemarks = ''; + this.selectedGuardian = null; + this.searchText = ''; + this.guardianSearchText = ''; + } +} diff --git a/src/app/shared/introducer-search/introducer-search.component.html b/src/app/shared/introducer-search/introducer-search.component.html index d7663ef85..e93a3a5ae 100644 --- a/src/app/shared/introducer-search/introducer-search.component.html +++ b/src/app/shared/introducer-search/introducer-search.component.html @@ -35,7 +35,11 @@ @if (!searching && searchResults.length > 0) {
@for (r of searchResults; track r.id) { -
+
person
{{ r.displayName }} @@ -49,6 +53,9 @@ }
+ @if (isExcluded(r)) { + Cannot select (self) + }
}
diff --git a/src/app/shared/introducer-search/introducer-search.component.scss b/src/app/shared/introducer-search/introducer-search.component.scss index 4b882d3cd..2d6346682 100644 --- a/src/app/shared/introducer-search/introducer-search.component.scss +++ b/src/app/shared/introducer-search/introducer-search.component.scss @@ -136,6 +136,12 @@ $gray-bg: #f8fafc; background: $blue-light; } + &--disabled { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; + } + &__icon { color: $muted; font-size: 20px; @@ -170,6 +176,19 @@ $gray-bg: #f8fafc; } } +.intro-excluded-tag { + font-size: 10px; + font-weight: 600; + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 4px; + padding: 1px 6px; + white-space: nowrap; + margin-left: auto; + flex-shrink: 0; +} + .intro-no-results { padding: 10px 0; font-size: 13px; diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts index adf14cc46..bc7d592b5 100644 --- a/src/app/shared/introducer-search/introducer-search.component.ts +++ b/src/app/shared/introducer-search/introducer-search.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Output, EventEmitter, inject } from '@angular/core'; +import { Component, Input, Output, EventEmitter, inject } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { ClientsService } from 'app/clients/clients.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -31,6 +31,7 @@ export class IntroducerSearchComponent { private clientsService = inject(ClientsService); private sanitizer = inject(DomSanitizer); + @Input() excludedClientIds: number[] = []; @Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string; cifNumber?: string } | null>(); entryMode: 'linked' = 'linked'; @@ -58,10 +59,11 @@ export class IntroducerSearchComponent { } this.searching = true; this.searchTimer = setTimeout(() => { - this.clientsService.searchByText(text, 0, 8).subscribe({ + this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ next: (data: any) => { this.searchResults = data?.content || []; this.searching = false; + this.clientsService.enrichSearchResults(this.searchResults); }, error: () => { this.searchResults = []; @@ -71,7 +73,12 @@ export class IntroducerSearchComponent { }, 400); } + isExcluded(client: any): boolean { + return this.excludedClientIds.includes(client.id); + } + selectClient(client: any): void { + if (this.isExcluded(client)) return; this.selectedClient = client; this.searchResults = []; this.searchText = ''; diff --git a/src/app/shared/nominee-form/nominee-form.component.html b/src/app/shared/nominee-form/nominee-form.component.html index 49bd32d8a..48583fa00 100644 --- a/src/app/shared/nominee-form/nominee-form.component.html +++ b/src/app/shared/nominee-form/nominee-form.component.html @@ -9,271 +9,268 @@
-
- - Nomination Facility -
-
- - -
+ search + Search & Link Nominee
- - @if (mode === 'manual') { -
- -
- - Full Name * - - Full name is required - - - - Relationship * - - @for (rel of relationships; track rel) { - {{ rel }} - } - - Relationship is required - -
- -
- - Date of Birth - - - - - - - Nationality - - Indian - NRI - Foreign National - - -
- - - - -
- - Aadhaar Number - - - - - PAN Number - - -
- -
- - Mobile Number - - - - - Nominee Share (%) - - -
- - - - -
- - Guardian Name - - - - - Guardian Relationship - - @for (rel of relationships; track rel) { - {{ rel }} - } - - -
- - -
- Is Alive - Is Dependent -
- - - - Remarks - + + @if (!selectedClient) { +
+ + Search by Name, Client ID or Mobile + search + - - } - - @if (mode === 'linked') { - @if (!selectedClient) { - -
- - Search by Name or Client ID - - search - - - @if (searching) { -
+ @if (searching || searchResults.length > 0) { +
+ @if (searching) {
Searching...
-
- } - - @if (!searching && searchResults.length > 0) { -
- @for (r of searchResults; track r.id) { -
- person -
- {{ r.displayName }} - - ID: {{ r.id }} - @if (r.officeName) { - · {{ r.officeName }} - } - @if (r.mobileNo) { - · {{ r.mobileNo }} - } - -
+ } + @for (r of searchResults; track r.id) { +
+ person +
+ {{ r.displayName }} + + ID: {{ r.id }} + @if (r.officeName) { + · {{ r.officeName }} + } + @if (r.mobileNo) { + · {{ r.mobileNo }} + } +
- } -
- } + @if (isExcludedNominee(r)) { + Cannot select (self) + } +
+ } + @if (!searching && searchText.length >= 2 && searchResults.length === 0) { +
No clients found
+ } +
+ } +
+ } - @if (!searching && searchText.length >= 2 && searchResults.length === 0) { -
No clients found
+ + @if (selectedClient) { +
+
+ @if (linkedProfileImage) { + + } @else { + person }
+
+ {{ selectedClient.displayName }} + + ID: {{ selectedClient.id }} + @if (selectedClient.mobileNo) { + · {{ selectedClient.mobileNo }} + } + +
+ +
+ + @if (fetchingDetails) { +
+ + Fetching details... +
} - @if (selectedClient) { - -
-
- @if (linkedProfileImage) { - Profile - } @else { - person + @if (!fetchingDetails) { + + @if (clientDob || clientPan || clientAadhaar) { +
+ @if (clientDob) { + + DOB + {{ clientDob | dateFormat }} + @if (isMinorByDob) { + Minor + } + + } + @if (clientPan) { + + PAN + + {{ clientPan }} + @if (linkedKycStatus?.panVerified) { + check_circle + } + + + } + @if (clientAadhaar) { + + AADHAAR + + {{ clientAadhaar }} + @if (linkedKycStatus?.aadhaarVerified) { + check_circle + } + + } -
-
- {{ selectedClient.displayName }} - - Client ID: {{ selectedClient.id }} - @if (selectedClient.accountNumber) { - · A/C: {{ selectedClient.accountNumber }} - } - -
- -
- - @if (fetchingLinkedData) { -
- - Fetching nominee details...
} - @if (!fetchingLinkedData) { -
-
- Full Name - {{ selectedClient.displayName }} -
-
- Date of Birth - {{ clientDob || '—' }} -
- @if (clientAadhaar) { -
- Aadhaar - - {{ clientAadhaar }} - @if (linkedKycStatus?.aadhaarVerified) { - verified Verified - } - -
+ +
+ + Nomination Type + + @for (opt of nominationTypeOptions; track opt.value) { + {{ opt.label }} + } + + + + + Relationship + + @for (rel of relationshipOptions; track rel.id) { + {{ rel.name }} + } + + + + + Share % + + @if (percentageShare && (percentageShare < 1 || percentageShare > 100)) { + 1–100 } - @if (clientPan) { -
- PAN - - {{ clientPan }} - @if (linkedKycStatus?.panVerified) { - verified Verified - } - + + +
+ Minor +
+
+ + + @if (nomineeIsMinor) { +
+
+ shield + Guardian + (Required for minor nominee) +
+ + @if (!selectedGuardian) { +
+ + Search Guardian by Name or Client ID + search + + + + @if (guardianSearching || guardianSearchResults.length > 0) { +
+ @if (guardianSearching) { +
+ + Searching... +
+ } + @for (r of guardianSearchResults; track r.id) { +
+ person +
+ {{ r.displayName }} + ID: {{ r.id }} + @if (r.mobileNo) { + · {{ r.mobileNo }} + } + +
+ @if (selectedClient && r.id === selectedClient.id) { + Same as nominee + } +
+ } + @if (!guardianSearching && guardianSearchText.length >= 2 && guardianSearchResults.length === 0) { +
No clients found
+ } +
+ }
+ } @else { + +
+ person +
+ {{ selectedGuardian.displayName }} + ID: {{ selectedGuardian.id }} + @if (selectedGuardian.mobileNo) { + · {{ selectedGuardian.mobileNo }} + } + +
+ +
+ @if (guardianSameAsNominee) { +
+ error_outline + Guardian and nominee cannot be the same client (same Client ID / CIF). +
+ } }
+ } - -
- - Relationship * - - @for (rel of relationships; track rel) { - {{ rel }} - } - - + + + Remarks + + - - Nominee Share (%) - - + + @if (showActions) { +
+ +
} } diff --git a/src/app/shared/nominee-form/nominee-form.component.scss b/src/app/shared/nominee-form/nominee-form.component.scss index edfa9a96f..0971f4859 100644 --- a/src/app/shared/nominee-form/nominee-form.component.scss +++ b/src/app/shared/nominee-form/nominee-form.component.scss @@ -6,194 +6,93 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use 'sass:color'; - -$blue: #2563eb; -$blue-light: #eff6ff; -$green: #16a34a; -$green-light: #dcfce7; $border: #e2e8f0; $text: #1e293b; $muted: #64748b; $gray-bg: #f8fafc; -$purple: #7c3aed; -$purple-light: #f5f3ff; +$blue: #1074b9; +$blue-light: #e8f0fe; +$green: #16a34a; :host { display: block; } -// ─── Wrapper ─────────────────────────────────────────────────── .nom-section { border: 1px solid $border; - border-radius: 14px; + border-radius: 10px; background: #fff; - margin-top: 20px; + margin-top: 16px; overflow: visible; - position: relative; - z-index: 10; } -// ─── Header ──────────────────────────────────────────────────── +// Header .nom-header { display: flex; align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 12px; - padding: 14px 20px; - background: $gray-bg; + gap: 8px; + padding: 12px 16px; border-bottom: 1px solid $border; - border-radius: 14px 14px 0 0; + background: $gray-bg; + border-radius: 10px 10px 0 0; - &__left { - display: flex; - align-items: center; - gap: 10px; - - i { - color: $purple; - font-size: 15px; - } + &__icon { + color: $blue; + font-size: 18px; + width: 18px; + height: 18px; } &__title { - font-size: 14px; - font-weight: 700; + font-size: 13px; + font-weight: 600; color: $text; } } -// ─── Mode toggle tabs ────────────────────────────────────────── -.nom-mode-tabs { - display: flex; - gap: 8px; -} - -.nom-tab { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 14px; - border-radius: 8px; - font-size: 12px; - font-weight: 600; - cursor: pointer; - border: 1.5px solid $border; - background: #fff; - color: $muted; - transition: all 0.18s; - - i { - font-size: 11px; - } - - &:hover { - border-color: $blue; - color: $blue; - background: $blue-light; - } - - &--active { - border-color: $blue; - background: $blue; - color: #fff; - - &:hover { - background: color.adjust(#2563eb, $lightness: -5%); - } - } -} - -// ─── Body ────────────────────────────────────────────────────── +// Body .nom-body { - padding: 20px; + padding: 16px; overflow: visible; position: relative; z-index: 10; } -// ─── Manual form grid ────────────────────────────────────────── -.nom-form { - display: flex; - flex-direction: column; - gap: 0; -} - -.nom-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - - @media (width <= 600px) { - grid-template-columns: 1fr; - } -} - -.nom-field { +.nom-field--full { width: 100%; - - &--full { - width: 100%; - display: block; - } + display: block; } -.nom-section-label { - display: flex; - align-items: center; - gap: 8px; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.07em; - text-transform: uppercase; - color: $blue; - margin: 8px 0 4px; - - i { - font-size: 12px; - } -} - -.nom-hint { - font-size: 10px; - font-weight: 500; - text-transform: none; - letter-spacing: 0; +.nom-search-icon { color: $muted; - margin-left: 2px; + font-size: 18px; } -.nom-flags { - display: flex; - align-items: center; - gap: 24px; - margin: 8px 0 12px; -} - -// ─── Search (link mode) ──────────────────────────────────────── +// Search results dropdown .nom-search-wrap { position: relative; z-index: 1000; - overflow: visible; } .nom-results { - margin-top: 4px; - border: 1px solid $border; - border-radius: 8px; + position: absolute; + top: 56px; + left: 0; + right: 0; + z-index: 1001; background: #fff; - box-shadow: 0 4px 20px rgb(0 0 0 / 10%); - overflow: hidden; - max-height: 260px; + border: 1px solid $border; + border-radius: 6px; + box-shadow: 0 6px 20px rgb(0 0 0 / 10%); + max-height: 240px; overflow-y: auto; &__loading { display: flex; align-items: center; gap: 10px; - padding: 14px 16px; - font-size: 13px; + padding: 12px 14px; + font-size: 12px; color: $muted; } } @@ -201,11 +100,11 @@ $purple-light: #f5f3ff; .nom-result-item { display: flex; align-items: center; - gap: 12px; - padding: 12px 16px; + gap: 10px; + padding: 10px 14px; cursor: pointer; - transition: background 0.15s; border-bottom: 1px solid $border; + transition: background 0.12s; &:last-child { border-bottom: none; @@ -218,15 +117,12 @@ $purple-light: #f5f3ff; &__icon { color: $muted; font-size: 20px; - width: 20px; - height: 20px; - flex: 0 0 auto; + flex-shrink: 0; } &__info { display: flex; flex-direction: column; - gap: 2px; } &__name { @@ -242,32 +138,32 @@ $purple-light: #f5f3ff; } .nom-no-results { - padding: 12px 0; - font-size: 13px; + padding: 10px; + font-size: 12px; color: $muted; text-align: center; } -// ─── Selected client card ────────────────────────────────────── +// Client card .nom-client-card { display: flex; align-items: center; - gap: 14px; - padding: 14px 16px; + gap: 12px; + padding: 10px 14px; border: 1px solid $border; - border-radius: 10px; + border-radius: 8px; background: $gray-bg; - margin-bottom: 14px; + margin-bottom: 10px; &__avatar { - width: 46px; - height: 46px; + width: 38px; + height: 38px; border-radius: 50%; background: $blue-light; display: flex; align-items: center; justify-content: center; - flex: 0 0 auto; + flex-shrink: 0; overflow: hidden; } @@ -279,16 +175,16 @@ $purple-light: #f5f3ff; &__avatar-icon { color: $blue; - font-size: 26px; - width: 26px; - height: 26px; + font-size: 22px; + width: 22px; + height: 22px; } &__info { flex: 1; display: flex; flex-direction: column; - gap: 3px; + gap: 2px; } &__name { @@ -303,8 +199,8 @@ $purple-light: #f5f3ff; } &__remove { - flex: 0 0 auto; color: $muted; + flex-shrink: 0; } } @@ -314,74 +210,200 @@ $purple-light: #f5f3ff; gap: 8px; font-size: 12px; color: $muted; - padding: 6px 0; + padding: 4px 0 8px; } -// ─── Linked client details grid ──────────────────────────────── -.nom-details-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 14px 24px; - padding: 14px 16px; - border: 1px solid $border; - border-radius: 10px; - background: #fff; - margin-bottom: 14px; - - @media (width <= 600px) { - grid-template-columns: 1fr; - } -} - -.nom-detail { +// KYC row +.nom-kyc-row { display: flex; - flex-direction: column; - gap: 4px; - - &__label { - font-size: 10px; - font-weight: 700; - letter-spacing: 0.06em; - color: $muted; - text-transform: uppercase; - } - - &__value { - font-size: 13px; - font-weight: 600; - color: $text; - display: flex; - align-items: center; - gap: 6px; - } + flex-wrap: wrap; + gap: 6px 20px; + padding: 8px 12px; + background: $gray-bg; + border: 1px solid $border; + border-radius: 6px; + margin-bottom: 12px; } -.nom-verified { +.nom-kyc-item { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; +} + +.nom-kyc-label { + font-size: 10px; + font-weight: 700; + color: $muted; + text-transform: uppercase; +} + +.nom-kyc-value { + font-size: 12px; + font-weight: 600; + color: $text; display: inline-flex; align-items: center; gap: 3px; - font-size: 11px; - font-weight: 600; +} + +.nom-kyc-ok { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; color: $green; - background: $green-light; - border-radius: 999px; - padding: 2px 8px; +} + +// Fields row +.nom-fields-row { + display: flex; + flex-wrap: wrap; + gap: 0 12px; + align-items: center; + margin-bottom: 8px; +} + +.nom-field-item { + flex: 1; + min-width: 160px; + + &--sm { + flex: 0 0 100px; + min-width: 80px; + } +} + +.nom-minor-toggle { + display: flex; + align-items: center; + padding-top: 8px; + flex: 0 0 auto; +} + +// Minor badge in KYC row +.nom-minor-badge { + font-size: 10px; + font-weight: 700; + color: #92400e; + background: #fef3c7; + border: 1px solid #fcd34d; + border-radius: 4px; + padding: 1px 6px; +} + +// Guardian section +.nom-guardian-section { + border: 1px solid #fde68a; + border-radius: 8px; + background: #fffbeb; + padding: 12px 14px; + margin-bottom: 12px; +} + +.nom-guardian-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 10px; +} + +.nom-guardian-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #92400e; +} + +.nom-guardian-title { + font-size: 13px; + font-weight: 700; + color: #92400e; +} + +.nom-guardian-hint { + font-size: 11px; + color: #b45309; +} + +.nom-guardian-card { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: #fff; + border: 1px solid #fcd34d; + border-radius: 6px; + + &--error { + border-color: #ef4444; + background: #fef2f2; + } + + &__icon { + color: #92400e; + font-size: 20px; + flex-shrink: 0; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1e293b; + } + + &__meta { + font-size: 11px; + color: #64748b; + } +} + +.nom-conflict-error { + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; + font-size: 12px; + font-weight: 500; + color: #dc2626; mat-icon { - font-size: 12px; - width: 12px; - height: 12px; + font-size: 16px; + width: 16px; + height: 16px; } } -// ─── Extras for linked (relationship + share) ────────────────── -.nom-linked-extras { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - margin-top: 4px; +.nom-result-item--disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} - @media (width <= 600px) { - grid-template-columns: 1fr; - } +.nom-result-item__conflict, +.nom-self-tag { + font-size: 10px; + font-weight: 600; + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 4px; + padding: 2px 6px; + white-space: nowrap; + flex-shrink: 0; + margin-left: auto; +} + +// Actions +.nom-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 8px; } diff --git a/src/app/shared/nominee-form/nominee-form.component.ts b/src/app/shared/nominee-form/nominee-form.component.ts index 8d0e05652..cf3483b62 100644 --- a/src/app/shared/nominee-form/nominee-form.component.ts +++ b/src/app/shared/nominee-form/nominee-form.component.ts @@ -6,59 +6,35 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Output, EventEmitter, inject, OnInit } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms'; -import { DomSanitizer } from '@angular/platform-browser'; +import { Component, Input, Output, EventEmitter, inject, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { ClientsService } from 'app/clients/clients.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; -import { MatIconButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { DateFormatPipe } from 'app/pipes/date-format.pipe'; export interface NomineeData { - mode: 'manual' | 'linked'; - // Manual fields - fullName?: string; - relationship?: string; - dateOfBirth?: string; - nationality?: string; - aadhaarNumber?: string; - panNumber?: string; - mobileNo?: string; - nomineeShare?: number; - isAlive?: boolean; - isDependent?: boolean; - guardianName?: string; - guardianRelationship?: string; - remarks?: string; - // Linked client fields - clientId?: number; - displayName?: string; - cifNumber?: string; + nomineeClientId: number; + nomineeDisplayName: string; + nominationType: 'DA1' | 'DA5'; + relationshipId: number; + relationshipName: string; + percentageShare: number; + nomineeIsMinor: boolean; + guardianClientId?: number; + guardianDisplayName?: string; + remarks: string | null; + locale: 'en'; + // preview extras + nomineeDateOfBirth?: any; + nomineeMobileNo?: string; + nomineePan?: string; + nomineeAadhaar?: string; } -const RELATIONSHIPS = [ - 'Spouse', - 'Son', - 'Daughter', - 'Father', - 'Mother', - 'Brother', - 'Sister', - 'Grandfather', - 'Grandmother', - 'Grandson', - 'Granddaughter', - 'Uncle', - 'Aunt', - 'Nephew', - 'Niece', - 'Friend', - 'Other' -]; - @Component({ selector: 'mifosx-nominee-form', templateUrl: './nominee-form.component.html', @@ -67,91 +43,71 @@ const RELATIONSHIPS = [ ...STANDALONE_SHARED_IMPORTS, FormsModule, MatProgressSpinner, - MatIconButton, MatIcon, - MatCheckbox, - MatDatepickerModule + MatSlideToggle, + DateFormatPipe ] }) export class NomineeFormComponent implements OnInit { - private fb = inject(UntypedFormBuilder); private clientsService = inject(ClientsService); private sanitizer = inject(DomSanitizer); + @Input() primaryClientId: number | null = null; + @Input() showActions = true; @Output() nomineeChange = new EventEmitter(); - mode: 'manual' | 'linked' = 'manual'; - relationships = RELATIONSHIPS; + relationshipOptions: any[] = []; - // Manual form - manualForm!: UntypedFormGroup; - minDate = new Date(1900, 0, 1); - maxDate = new Date(); + nominationTypeOptions = [ + { value: 'DA1', label: 'DA1 - Add Nominee' }, + { value: 'DA5', label: 'DA5 - Modify / Cancel Nominee' } + ]; - // Link mode + /** Nominee search */ searchText = ''; searching = false; searchResults: any[] = []; - selectedClient: any = null; - fetchingLinkedData = false; - linkedProfileImage: any = null; - linkedKycStatus: any = null; - clientDob = ''; - clientAadhaar = ''; - clientPan = ''; - clientCifNumber = ''; - linkedRelationship = ''; - linkedShare = 100; - private searchTimer: any; + /** Selected nominee client */ + selectedClient: any = null; + fetchingDetails = false; + linkedProfileImage: SafeUrl | null = null; + linkedKycStatus: any = null; + clientDob: any = null; + clientAadhaar = ''; + clientPan = ''; + + /** Whether the nominee is confirmed to be a minor based on DOB */ + isMinorByDob = false; + /** Whether minor toggle is manually lockable */ + minorToggleLocked = false; + + /** Guardian search */ + guardianSearchText = ''; + guardianSearching = false; + guardianSearchResults: any[] = []; + selectedGuardian: any = null; + private guardianSearchTimer: any; + + /** Form fields */ + nominationType: 'DA1' | 'DA5' = 'DA1'; + relationshipId: number | null = null; + percentageShare: number | null = null; + nomineeIsMinor = false; + remarks = ''; + ngOnInit(): void { - this.manualForm = this.fb.group({ - fullName: [ - '', - Validators.required - ], - relationship: [ - '', - Validators.required - ], - dateOfBirth: [''], - nationality: ['Indian'], - aadhaarNumber: [''], - panNumber: [''], - mobileNo: [''], - nomineeShare: [100], - isAlive: [true], - isDependent: [false], - guardianName: [''], - guardianRelationship: [''], - remarks: [''] - }); - - this.manualForm.valueChanges.subscribe(() => { - if (this.mode === 'manual' && this.manualForm.valid) { - this.emitManual(); - } + this.clientsService.getClientTemplate().subscribe({ + next: (t: any) => { + this.relationshipOptions = t?.familyMemberOptions?.relationshipIdOptions || []; + }, + error: () => {} }); } - switchMode(m: 'manual' | 'linked'): void { - this.mode = m; - if (m === 'manual') { - this.clearLinked(); - } else { - this.manualForm.reset({ nationality: 'Indian', isAlive: true, isDependent: false, nomineeShare: 100 }); - this.nomineeChange.emit(null); - } - } + // ─── Nominee Search ────────────────────────────────────────── - // ─── Manual ────────────────────────────────────── - private emitManual(): void { - const v = this.manualForm.value; - this.nomineeChange.emit({ mode: 'manual', ...v }); - } - - // ─── Link existing client ───────────────────────── onSearch(text: string): void { this.searchText = text; clearTimeout(this.searchTimer); @@ -161,10 +117,11 @@ export class NomineeFormComponent implements OnInit { } this.searching = true; this.searchTimer = setTimeout(() => { - this.clientsService.searchByText(text, 0, 8).subscribe({ + this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ next: (data: any) => { this.searchResults = data?.content || []; this.searching = false; + this.clientsService.enrichSearchResults(this.searchResults); }, error: () => { this.searchResults = []; @@ -174,70 +131,61 @@ export class NomineeFormComponent implements OnInit { }, 400); } + dismissSearch(): void { + setTimeout(() => { + this.searchResults = []; + }, 200); + } + + isExcludedNominee(client: any): boolean { + return this.primaryClientId != null && client.id === this.primaryClientId; + } + selectClient(client: any): void { + if (this.isExcludedNominee(client)) return; this.selectedClient = client; this.searchResults = []; this.searchText = ''; - this.fetchLinkedClientData(client.id); - this.nomineeChange.emit({ - mode: 'linked', - clientId: client.id, - displayName: client.displayName, - cifNumber: client.cifNumber || client.cifNo || '' - }); + this.fetchClientDetails(client.id); } - removeLinked(): void { - this.clearLinked(); - this.nomineeChange.emit(null); - } - - onLinkedRelationshipChange(rel: string): void { - if (this.selectedClient) { - this.nomineeChange.emit({ - mode: 'linked', - clientId: this.selectedClient.id, - displayName: this.selectedClient.displayName, - cifNumber: this.selectedClient.accountNo || this.selectedClient.cifNumber || '', - relationship: rel, - nomineeShare: this.linkedShare - }); - } - } - - private clearLinked(): void { + removeClient(): void { this.selectedClient = null; this.linkedProfileImage = null; this.linkedKycStatus = null; - this.clientDob = ''; + this.clientDob = null; this.clientAadhaar = ''; this.clientPan = ''; - this.clientCifNumber = ''; - this.searchText = ''; - this.searchResults = []; - this.linkedRelationship = ''; - this.linkedShare = 100; + this.isMinorByDob = false; + this.minorToggleLocked = false; + this.nomineeIsMinor = false; + this.selectedGuardian = null; + this.relationshipId = null; + this.percentageShare = null; + this.remarks = ''; + this.nomineeChange.emit(null); } - private fetchLinkedClientData(clientId: string): void { - this.fetchingLinkedData = true; + private fetchClientDetails(clientId: number): void { + this.fetchingDetails = true; + let pending = 3; + const done = () => { + pending--; + if (pending <= 0) this.fetchingDetails = false; + }; - this.clientsService.getClientData(clientId).subscribe({ + this.clientsService.getClientData(clientId.toString()).subscribe({ next: (data: any) => { - if (data.dateOfBirth) { - const d = Array.isArray(data.dateOfBirth) - ? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2]) - : new Date(data.dateOfBirth); - this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + if (data?.dateOfBirth) { + this.clientDob = data.dateOfBirth; + this.checkMinorFromDob(data.dateOfBirth); } - this.fetchingLinkedData = false; + done(); }, - error: () => { - this.fetchingLinkedData = false; - } + error: () => done() }); - this.clientsService.getClientKyc(clientId).subscribe({ + this.clientsService.getClientKyc(clientId.toString()).subscribe({ next: (kyc: any) => { if (kyc) { this.clientAadhaar = kyc.aadhaarNumberMasked || ''; @@ -247,17 +195,159 @@ export class NomineeFormComponent implements OnInit { aadhaarVerified: kyc.aadhaarVerified || false }; } + done(); }, - error: () => {} + error: () => done() }); - this.clientsService.getClientProfileImage(clientId).subscribe({ - next: (imageData: any) => { - if (imageData) { - this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData); - } + this.clientsService.getClientProfileImage(clientId.toString()).subscribe({ + next: (img: any) => { + if (img) this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img); + done(); }, - error: () => {} + error: () => done() }); } + + /** Auto-set minor toggle based on DOB. Lock toggle if minor (must be minor). */ + private checkMinorFromDob(dob: any): void { + let birthDate: Date; + if (Array.isArray(dob)) { + birthDate = new Date(dob[0], dob[1] - 1, dob[2]); + } else { + birthDate = new Date(dob); + } + if (isNaN(birthDate.getTime())) return; + + const today = new Date(); + let age = today.getFullYear() - birthDate.getFullYear(); + const m = today.getMonth() - birthDate.getMonth(); + if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) age--; + + if (age < 18) { + // Confirmed minor — auto-enable and lock + this.nomineeIsMinor = true; + this.isMinorByDob = true; + this.minorToggleLocked = true; + } else { + // Not a minor — disable and lock OFF + this.nomineeIsMinor = false; + this.isMinorByDob = false; + this.minorToggleLocked = true; + } + } + + // ─── Guardian Search ───────────────────────────────────────── + + onGuardianSearch(text: string): void { + this.guardianSearchText = text; + clearTimeout(this.guardianSearchTimer); + if (!text || text.length < 2) { + this.guardianSearchResults = []; + return; + } + this.guardianSearching = true; + this.guardianSearchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ + next: (data: any) => { + this.guardianSearchResults = data?.content || []; + this.guardianSearching = false; + this.clientsService.enrichSearchResults(this.guardianSearchResults); + }, + error: () => { + this.guardianSearchResults = []; + this.guardianSearching = false; + } + }); + }, 400); + } + + dismissGuardianSearch(): void { + setTimeout(() => { + this.guardianSearchResults = []; + }, 200); + } + + selectGuardian(client: any): void { + if (this.selectedClient && client.id === this.selectedClient.id) { + return; // nominee and guardian cannot be the same + } + this.selectedGuardian = client; + this.guardianSearchResults = []; + this.guardianSearchText = ''; + } + + get guardianSameAsNominee(): boolean { + return !!(this.selectedGuardian && this.selectedClient && this.selectedGuardian.id === this.selectedClient.id); + } + + removeGuardian(): void { + this.selectedGuardian = null; + this.guardianSearchText = ''; + } + + // ─── Live preview data (before Save) ───────────────────────── + + get currentNomineeData(): NomineeData | null { + if (!this.selectedClient) return null; + const relName = this.relationshipOptions.find((r: any) => r.id === this.relationshipId)?.name || ''; + return { + nomineeClientId: this.selectedClient.id, + nomineeDisplayName: this.selectedClient.displayName || '', + nominationType: this.nominationType, + relationshipId: this.relationshipId || 0, + relationshipName: relName, + percentageShare: this.percentageShare || 0, + nomineeIsMinor: this.nomineeIsMinor, + guardianClientId: this.nomineeIsMinor && this.selectedGuardian ? this.selectedGuardian.id : undefined, + guardianDisplayName: this.nomineeIsMinor && this.selectedGuardian ? this.selectedGuardian.displayName : undefined, + remarks: this.remarks || null, + locale: 'en', + nomineeDateOfBirth: this.clientDob || null, + nomineeMobileNo: this.selectedClient.mobileNo || '', + nomineePan: this.clientPan || '', + nomineeAadhaar: this.clientAadhaar || '' + }; + } + + // ─── Validation ─────────────────────────────────────────────── + + get isValid(): boolean { + if (!this.selectedClient || !this.relationshipId) return false; + if (!this.percentageShare || this.percentageShare < 1 || this.percentageShare > 100) return false; + if (this.nomineeIsMinor && !this.selectedGuardian) return false; + if (this.guardianSameAsNominee) return false; + return true; + } + + // ─── Save ───────────────────────────────────────────────────── + + save(): void { + if (!this.isValid) return; + const relName = this.relationshipOptions.find((r: any) => r.id === this.relationshipId)?.name || ''; + const payload: NomineeData = { + nomineeClientId: this.selectedClient.id, + nomineeDisplayName: this.selectedClient.displayName || '', + nominationType: this.nominationType, + relationshipId: this.relationshipId!, + relationshipName: relName, + percentageShare: this.percentageShare!, + nomineeIsMinor: this.nomineeIsMinor, + remarks: this.remarks || null, + locale: 'en', + nomineeDateOfBirth: this.clientDob || null, + nomineeMobileNo: this.selectedClient.mobileNo || '', + nomineePan: this.clientPan || '', + nomineeAadhaar: this.clientAadhaar || '' + }; + if (this.nomineeIsMinor && this.selectedGuardian) { + payload.guardianClientId = this.selectedGuardian.id; + payload.guardianDisplayName = this.selectedGuardian.displayName || ''; + } + this.nomineeChange.emit(payload); + } + + cancel(): void { + this.removeClient(); + } }