update rd,fd,saving and current product update validation end date and and this all account nominee facility code

This commit is contained in:
Nitesh Yadav
2026-05-15 12:53:47 +05:30
parent 8758b9222f
commit 52e216fbe5
62 changed files with 3069 additions and 886 deletions
@@ -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 = '';