update rd,fd,saving and current product update validation end date and and this all account nominee facility code
This commit is contained in:
@@ -331,9 +331,12 @@
|
||||
@if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
|
||||
<div class="nom-readonly-row">
|
||||
@if (selectedClient.dateOfBirth) {
|
||||
<span class="nom-ro-item"
|
||||
><span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}</span
|
||||
>
|
||||
<span class="nom-ro-item">
|
||||
<span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}
|
||||
@if (nomineeIsMinor && minorToggleLocked) {
|
||||
<span class="nom-minor-badge">Minor</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (selectedClient.panNumber) {
|
||||
<span class="nom-ro-item"
|
||||
@@ -380,45 +383,92 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="nom-field-item nom-field-item--toggle">
|
||||
<mat-slide-toggle [(ngModel)]="nomineeIsMinor">Minor</mat-slide-toggle>
|
||||
<mat-slide-toggle [(ngModel)]="nomineeIsMinor" [disabled]="minorToggleLocked">Minor</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (nomineeIsMinor) {
|
||||
<div class="nom-guardian-section">
|
||||
<span class="nom-guardian-label"><mat-icon>shield</mat-icon> Guardian</span>
|
||||
<div class="nom-guardian-box">
|
||||
<div class="nom-guardian-box__header">
|
||||
<mat-icon class="nom-guardian-box__icon">shield</mat-icon>
|
||||
<span class="nom-guardian-box__title">Guardian</span>
|
||||
<span class="nom-guardian-box__hint">(Required for minor nominee)</span>
|
||||
</div>
|
||||
|
||||
@if (!selectedGuardian) {
|
||||
<mat-form-field appearance="outline" class="nom-search-field">
|
||||
<input
|
||||
matInput
|
||||
[ngModel]="guardianSearchText"
|
||||
(ngModelChange)="onGuardianSearch($event)"
|
||||
placeholder="Search guardian..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (guardianSearchResults.length > 0 || guardianSearching) {
|
||||
<div class="nom-search-results nom-search-results--inline">
|
||||
@if (guardianSearching) {
|
||||
<div class="nom-search-loading">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
}
|
||||
@for (client of guardianSearchResults; track client.id) {
|
||||
<div class="nom-search-item" (click)="selectGuardian(client)">
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
<span class="nom-search-item__name">{{ client.displayName }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="nom-guardian-search-wrap">
|
||||
<mat-form-field appearance="outline" class="nom-guardian-search-field">
|
||||
<mat-label>Search Guardian by Name or Client ID</mat-label>
|
||||
<mat-icon matPrefix class="nom-guardian-prefix-icon">search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[value]="guardianSearchText"
|
||||
(input)="onGuardianSearch($any($event.target).value)"
|
||||
(blur)="dismissGuardianSearch()"
|
||||
placeholder="Type at least 2 characters..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
@if (guardianSearchResults.length > 0 || guardianSearching) {
|
||||
<div class="nom-guardian-results">
|
||||
@if (guardianSearching) {
|
||||
<div class="nom-search-loading">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Searching...</span>
|
||||
</div>
|
||||
}
|
||||
@for (client of guardianSearchResults; track client.id) {
|
||||
<div
|
||||
class="nom-search-item"
|
||||
[class.nom-search-item--disabled]="selectedClient && client.id === selectedClient.id"
|
||||
(mousedown)="selectGuardian(client); $event.preventDefault()"
|
||||
>
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
<div class="nom-search-item__info">
|
||||
<span class="nom-search-item__name">{{ client.displayName }}</span>
|
||||
<span class="nom-search-item__meta"
|
||||
>ID: {{ client.id }}
|
||||
@if (client.mobileNo) {
|
||||
· {{ client.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (selectedClient && client.id === selectedClient.id) {
|
||||
<span class="nom-conflict-tag">Same as nominee</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
!guardianSearching && guardianSearchText.length >= 2 && guardianSearchResults.length === 0
|
||||
) {
|
||||
<div class="search-no-results-hint">No clients found</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="nom-guardian-card" [class.nom-guardian-card--error]="guardianSameAsNominee">
|
||||
<mat-icon class="nom-guardian-card__icon">person</mat-icon>
|
||||
<div class="nom-guardian-card__info">
|
||||
<span class="nom-guardian-card__name">{{ selectedGuardian.displayName }}</span>
|
||||
<span class="nom-guardian-card__meta"
|
||||
>ID: {{ selectedGuardian.id }}
|
||||
@if (selectedGuardian.mobileNo) {
|
||||
· {{ selectedGuardian.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<button mat-icon-button type="button" (click)="selectedGuardian = null">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@if (guardianSameAsNominee) {
|
||||
<div class="nom-conflict-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
Guardian and nominee cannot be the same client (same Client ID / CIF).
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<span class="nom-guardian-selected"
|
||||
>{{ selectedGuardian.displayName }}
|
||||
<button mat-icon-button (click)="selectedGuardian = null">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -439,7 +489,8 @@
|
||||
!nomineeShare ||
|
||||
nomineeSaving ||
|
||||
nomineeShare > maxAllowedShare ||
|
||||
(nomineeIsMinor && !selectedGuardian)
|
||||
(nomineeIsMinor && !selectedGuardian) ||
|
||||
guardianSameAsNominee
|
||||
"
|
||||
(click)="saveNominee()"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
Reference in New Issue
Block a user