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 = '';
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
|
||||
<mifosx-current-account-currency-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[primaryClientId]="currentAccountTemplate?.clientId ?? null"
|
||||
></mifosx-current-account-currency-step>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
|
||||
+13
-2
@@ -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(
|
||||
|
||||
+5
-1
@@ -73,7 +73,11 @@
|
||||
</span>
|
||||
</div>
|
||||
@if (currentAccountProductTemplate?.nominationFacility) {
|
||||
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
|
||||
<mifosx-nominee-form
|
||||
[primaryClientId]="primaryClientId"
|
||||
[showActions]="false"
|
||||
(nomineeChange)="onNomineeChange($event)"
|
||||
></mifosx-nominee-form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
+10
-2
@@ -53,7 +53,11 @@
|
||||
@if (searchResults.length > 0) {
|
||||
<div class="arc-search-results">
|
||||
@for (client of searchResults; track client) {
|
||||
<div class="arc-search-result-item" (click)="selectClient(client)">
|
||||
<div
|
||||
class="arc-search-result-item"
|
||||
[class.arc-search-result-item--disabled]="isExcluded(client)"
|
||||
(click)="selectClient(client)"
|
||||
>
|
||||
<div class="arc-result-avatar">
|
||||
{{ (client.displayName || '?')[0].toUpperCase() }}
|
||||
</div>
|
||||
@@ -73,7 +77,11 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<mat-icon class="arc-result-arrow">chevron_right</mat-icon>
|
||||
@if (isExcluded(client)) {
|
||||
<span class="arc-excluded-tag">Already added</span>
|
||||
} @else {
|
||||
<mat-icon class="arc-result-arrow">chevron_right</mat-icon>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
+18
@@ -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 {
|
||||
|
||||
+9
-2
@@ -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<AddRelatedClientDialogComponent>);
|
||||
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 = [];
|
||||
|
||||
+8
-2
@@ -165,7 +165,10 @@
|
||||
</div>
|
||||
|
||||
@if (productTemplate?.introducerRequired) {
|
||||
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onIntroducerChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,7 +524,10 @@
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
<span>Search & Link Guardian (existing client)</span>
|
||||
</div>
|
||||
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onGuardianChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
|
||||
@if (guardianName) {
|
||||
<div class="soa-guardian-selected">
|
||||
|
||||
+12
-2
@@ -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) {
|
||||
|
||||
+28
-36
@@ -570,72 +570,64 @@
|
||||
<div class="preview-section__header">
|
||||
<i class="fas fa-user-shield preview-section__icon"></i>
|
||||
<span class="preview-section__title">Nominee Details</span>
|
||||
<span class="preview-section__badge">
|
||||
{{ currentAccountNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }}
|
||||
</span>
|
||||
<span class="preview-section__badge">{{ currentAccountNominee?.nominationType || 'DA1' }}</span>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-grid preview-grid--three">
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Name</span>
|
||||
<span class="preview-value">{{
|
||||
currentAccountNominee?.fullName || currentAccountNominee?.displayName || '—'
|
||||
}}</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.nomineeDisplayName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.nomineeClientId || '—' }}</span>
|
||||
</div>
|
||||
@if (currentAccountNominee?.cifNumber) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">CIF No</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee.cifNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Relationship</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.relationship || '—' }}</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.relationshipName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Share %</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.nomineeShare ?? '—' }}%</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.percentageShare ?? '—' }}%</span>
|
||||
</div>
|
||||
@if (currentAccountNominee?.dateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Minor</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.nomineeIsMinor ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
@if (currentAccountNominee?.nomineeDateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Date of Birth</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.dateOfBirth | dateFormat }}</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.nomineeDateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.nationality) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Nationality</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.nationality }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.mobileNo) {
|
||||
@if (currentAccountNominee?.nomineeMobileNo) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Mobile No</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.mobileNo }}</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.nomineeMobileNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.aadhaarNumber) {
|
||||
@if (currentAccountNominee?.nomineeAadhaar) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Aadhaar No</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.aadhaarNumber }}</span>
|
||||
<span class="preview-label">Aadhaar</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.nomineeAadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.panNumber) {
|
||||
@if (currentAccountNominee?.nomineePan) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PAN No</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.panNumber }}</span>
|
||||
<span class="preview-label">PAN</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.nomineePan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.guardianName) {
|
||||
@if (currentAccountNominee?.guardianDisplayName) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Name</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.guardianName }}</span>
|
||||
<span class="preview-label">Guardian</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.guardianDisplayName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.guardianRelationship) {
|
||||
@if (currentAccountNominee?.guardianClientId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Relationship</span>
|
||||
<span class="preview-value">{{ currentAccountNominee?.guardianRelationship }}</span>
|
||||
<span class="preview-label">Guardian Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ currentAccountNominee?.guardianClientId }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (currentAccountNominee?.remarks) {
|
||||
|
||||
@@ -213,6 +213,15 @@
|
||||
</a>
|
||||
}
|
||||
</ng-container>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nominees="routerLinkActive"
|
||||
[active]="nominees.isActive"
|
||||
>
|
||||
Nominees
|
||||
</a>
|
||||
@for (currentAccountDatatable of currentAccountDatatables; track currentAccountDatatable) {
|
||||
<a
|
||||
mat-tab-link
|
||||
|
||||
@@ -46,6 +46,7 @@ import { CurrentAccountDocumentsResolver } from './common-resolvers/current-acco
|
||||
import { CurrentAccountTransactionGeneralTabComponent } from './current-account-view/transactions/view-transaction/current-accounts-transaction-general-tab/current-accounts-transaction-general-tab.component';
|
||||
import { GeneralTabComponent } from './current-account-view/general-tab/general-tab.component';
|
||||
import { PassbookTabComponent } from './current-account-view/passbook-tab/passbook-tab.component';
|
||||
import { AccountNomineesTabComponent } from 'app/shared/account-nominees-tab/account-nominees-tab.component';
|
||||
|
||||
/** Current Account Routes */
|
||||
const routes: Routes = [
|
||||
@@ -135,6 +136,17 @@ const routes: Routes = [
|
||||
currentAccountDocuments: CurrentAccountDocumentsResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'nominees',
|
||||
component: AccountNomineesTabComponent,
|
||||
data: {
|
||||
title: 'Current Account Nominees',
|
||||
breadcrumb: 'Nominees',
|
||||
routeParamBreadcrumb: false,
|
||||
accountDataKey: 'currentAccountData',
|
||||
accountType: 'CB'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'datatables',
|
||||
children: [
|
||||
|
||||
+4
-4
@@ -52,8 +52,8 @@
|
||||
|
||||
<mifosx-fixed-deposit-account-currency-step
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-currency-step>
|
||||
[primaryClientId]="fixedDepositsAccountTemplate?.clientId ?? null"
|
||||
></mifosx-fixed-deposit-account-currency-step>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
<button mat-raised-button matStepperPrevious>
|
||||
@@ -98,9 +98,9 @@
|
||||
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
|
||||
[fixedDepositAccountData]="fixedDepositAccount"
|
||||
[selectedTenure]="selectedTenure"
|
||||
[fdNominee]="fdNominee"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-fixed-deposit-account-preview-step>
|
||||
></mifosx-fixed-deposit-account-preview-step>
|
||||
</mat-step>
|
||||
}
|
||||
</mat-stepper>
|
||||
|
||||
+17
@@ -138,6 +138,10 @@ export class CreateFixedDepositAccountComponent {
|
||||
};
|
||||
}
|
||||
|
||||
get fdNominee() {
|
||||
return this.fixedDepositAccountCurrencyStep?.liveNomineeData ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the fixed deposit form and creates a new fixed deposit account
|
||||
*/
|
||||
@@ -223,6 +227,19 @@ export class CreateFixedDepositAccountComponent {
|
||||
monthDayFormat,
|
||||
locale
|
||||
};
|
||||
const nominee = this.fdNominee;
|
||||
if (nominee) {
|
||||
fixedDepositAccount.nominee = {
|
||||
nomineeClientId: nominee.nomineeClientId,
|
||||
nominationType: nominee.nominationType,
|
||||
relationshipId: nominee.relationshipId,
|
||||
percentageShare: nominee.percentageShare,
|
||||
nomineeIsMinor: nominee.nomineeIsMinor,
|
||||
...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}),
|
||||
remarks: nominee.remarks
|
||||
};
|
||||
}
|
||||
|
||||
this.fixedDepositsService.createFixedDepositAccount(fixedDepositAccount).subscribe((response: any) => {
|
||||
this.router.navigate(
|
||||
[
|
||||
|
||||
+5
-1
@@ -58,7 +58,11 @@
|
||||
<mat-slide-toggle [(ngModel)]="nominationFacility" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
@if (nominationFacility) {
|
||||
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
|
||||
<mifosx-nominee-form
|
||||
[primaryClientId]="primaryClientId"
|
||||
[showActions]="false"
|
||||
(nomineeChange)="onNomineeChange($event)"
|
||||
></mifosx-nominee-form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
+9
-2
@@ -276,7 +276,11 @@
|
||||
} @else {
|
||||
<div class="dal-list">
|
||||
@for (account of dalFilteredAccounts; track account.id) {
|
||||
<div class="dal-item" (click)="selectLinkedAccount(account)">
|
||||
<div
|
||||
class="dal-item"
|
||||
[class.dal-item--disabled]="account.status !== 'Active'"
|
||||
(click)="selectLinkedAccount(account)"
|
||||
>
|
||||
<div class="dal-item__details">
|
||||
<span class="dal-item__accno">{{ account.accountNo }}</span>
|
||||
<span class="dal-item__product">{{ account.productName }}</span>
|
||||
@@ -653,7 +657,10 @@
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
<span>Search & Link Guardian (existing client)</span>
|
||||
</div>
|
||||
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onGuardianChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
|
||||
@if (guardianName) {
|
||||
<div class="soa-guardian-selected">
|
||||
|
||||
+17
-16
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -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) {
|
||||
|
||||
+47
-19
@@ -384,47 +384,75 @@
|
||||
}
|
||||
|
||||
<!-- ══ 2c. Nominee Details ══════════════════════════════════════ -->
|
||||
@if (fixedDepositAccountData?.nominee) {
|
||||
@if (fdNominee) {
|
||||
<div class="preview-section">
|
||||
<div class="preview-section__header">
|
||||
<i class="fas fa-user-shield preview-section__icon"></i>
|
||||
<span class="preview-section__title">Nominee Details</span>
|
||||
<span class="preview-section__badge">{{
|
||||
fixedDepositAccountData.nominee.mode === 'linked' ? 'Linked Client' : 'Manual Entry'
|
||||
}}</span>
|
||||
<span class="preview-section__badge">{{ fdNominee?.nominationType || 'DA1' }}</span>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-grid preview-grid--three">
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Full Name</span>
|
||||
<span class="preview-value">{{
|
||||
fixedDepositAccountData.nominee.fullName || fixedDepositAccountData.nominee.displayName
|
||||
}}</span>
|
||||
<span class="preview-label">Name</span>
|
||||
<span class="preview-value">{{ fdNominee?.nomineeDisplayName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ fdNominee?.nomineeClientId || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Relationship</span>
|
||||
<span class="preview-value">{{ fixedDepositAccountData.nominee.relationship }}</span>
|
||||
<span class="preview-value">{{ fdNominee?.relationshipName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Nominee Share (%)</span>
|
||||
<span class="preview-value">{{ fixedDepositAccountData.nominee.nomineeShare }}%</span>
|
||||
<span class="preview-label">Share %</span>
|
||||
<span class="preview-value">{{ fdNominee?.percentageShare ?? '—' }}%</span>
|
||||
</div>
|
||||
@if (fixedDepositAccountData.nominee.dateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Minor</span>
|
||||
<span class="preview-value">{{ fdNominee?.nomineeIsMinor ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
@if (fdNominee?.nomineeDateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Date of Birth</span>
|
||||
<span class="preview-value">{{ fixedDepositAccountData.nominee.dateOfBirth | dateFormat }}</span>
|
||||
<span class="preview-value">{{ fdNominee?.nomineeDateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (fixedDepositAccountData.nominee.nationality) {
|
||||
@if (fdNominee?.nomineeMobileNo) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Nationality</span>
|
||||
<span class="preview-value">{{ fixedDepositAccountData.nominee.nationality }}</span>
|
||||
<span class="preview-label">Mobile No</span>
|
||||
<span class="preview-value preview-mono">{{ fdNominee?.nomineeMobileNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (fixedDepositAccountData.nominee.mobileNo) {
|
||||
@if (fdNominee?.nomineeAadhaar) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Mobile Number</span>
|
||||
<span class="preview-value preview-mono">{{ fixedDepositAccountData.nominee.mobileNo }}</span>
|
||||
<span class="preview-label">Aadhaar</span>
|
||||
<span class="preview-value preview-mono">{{ fdNominee?.nomineeAadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (fdNominee?.nomineePan) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PAN</span>
|
||||
<span class="preview-value preview-mono">{{ fdNominee?.nomineePan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (fdNominee?.guardianDisplayName) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian</span>
|
||||
<span class="preview-value">{{ fdNominee?.guardianDisplayName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (fdNominee?.guardianClientId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ fdNominee?.guardianClientId }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (fdNominee?.remarks) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Remarks</span>
|
||||
<span class="preview-value">{{ fdNominee?.remarks }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
+9
@@ -166,6 +166,15 @@
|
||||
{{ 'labels.inputs.Standing Instructions' | translate }}
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nominees="routerLinkActive"
|
||||
[active]="nominees.isActive"
|
||||
>
|
||||
Nominees
|
||||
</a>
|
||||
@for (savingsDatatable of savingsDatatables; track savingsDatatable) {
|
||||
<a
|
||||
mat-tab-link
|
||||
|
||||
@@ -31,6 +31,7 @@ import { FixedDepositsAccountActionsResolver } from './common-resolvers/fixed-de
|
||||
import { FixedDepositsAccountTemplateResolver } from './common-resolvers/fixed-deposit-account-template.resolver';
|
||||
import { FixedDepositsAccountAndTemplateResolver } from './common-resolvers/fixed-deposit-account-and-template.resolver';
|
||||
import { GeneralTabComponent } from './fixed-deposit-account-view/general-tab/general-tab.component';
|
||||
import { AccountNomineesTabComponent } from 'app/shared/account-nominees-tab/account-nominees-tab.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -102,6 +103,17 @@ const routes: Routes = [
|
||||
routeParamBreadcrumb: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'nominees',
|
||||
component: AccountNomineesTabComponent,
|
||||
data: {
|
||||
title: 'Fixed Deposit Nominees',
|
||||
breadcrumb: 'Nominees',
|
||||
routeParamBreadcrumb: false,
|
||||
accountDataKey: 'fixedDepositsAccountData',
|
||||
accountType: 'FD'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'datatables',
|
||||
children: [
|
||||
|
||||
+2
-2
@@ -52,8 +52,8 @@
|
||||
|
||||
<mifosx-recurring-deposits-account-currency-step
|
||||
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
|
||||
>
|
||||
</mifosx-recurring-deposits-account-currency-step>
|
||||
[primaryClientId]="recurringDepositsAccountTemplate?.clientId ?? null"
|
||||
></mifosx-recurring-deposits-account-currency-step>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
<button mat-raised-button matStepperPrevious>
|
||||
|
||||
+12
-4
@@ -122,7 +122,7 @@ export class CreateRecurringDepositsAccountComponent {
|
||||
}
|
||||
|
||||
get rdNominee() {
|
||||
return this.recurringDepositsAccountCurrencyStep?.nomineeData ?? null;
|
||||
return this.recurringDepositsAccountCurrencyStep?.liveNomineeData ?? null;
|
||||
}
|
||||
|
||||
get rdTenureDays(): number | null {
|
||||
@@ -259,9 +259,17 @@ export class CreateRecurringDepositsAccountComponent {
|
||||
recurringDepositAccount.mandatoryRecommendedDepositAmount = this.selectedDepositAmount;
|
||||
}
|
||||
|
||||
// Nominee from currency step
|
||||
if (currencyStep?.nomineeData) {
|
||||
recurringDepositAccount.nominee = currencyStep.nomineeData;
|
||||
const nominee = this.rdNominee;
|
||||
if (nominee) {
|
||||
recurringDepositAccount.nominee = {
|
||||
nomineeClientId: nominee.nomineeClientId,
|
||||
nominationType: nominee.nominationType,
|
||||
relationshipId: nominee.relationshipId,
|
||||
percentageShare: nominee.percentageShare,
|
||||
nomineeIsMinor: nominee.nomineeIsMinor,
|
||||
...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}),
|
||||
remarks: nominee.remarks
|
||||
};
|
||||
}
|
||||
|
||||
this.recurringDepositsService.createRecurringDepositAccount(recurringDepositAccount).subscribe((response: any) => {
|
||||
|
||||
+5
-1
@@ -60,7 +60,11 @@
|
||||
</span>
|
||||
</div>
|
||||
@if (recurringDepositsAccountProductTemplate?.nominationFacility) {
|
||||
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
|
||||
<mifosx-nominee-form
|
||||
[primaryClientId]="primaryClientId"
|
||||
[showActions]="false"
|
||||
(nomineeChange)="onNomineeChange($event)"
|
||||
></mifosx-nominee-form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
+9
-2
@@ -246,7 +246,11 @@
|
||||
} @else {
|
||||
<div class="dal-list">
|
||||
@for (account of dalFilteredAccounts; track account.id) {
|
||||
<div class="dal-item" (click)="selectLinkedAccount(account)">
|
||||
<div
|
||||
class="dal-item"
|
||||
[class.dal-item--disabled]="account.status !== 'Active'"
|
||||
(click)="selectLinkedAccount(account)"
|
||||
>
|
||||
<div class="dal-item__details">
|
||||
<span class="dal-item__accno">{{ account.accountNo }}</span>
|
||||
<span class="dal-item__product">{{ account.productName }}</span>
|
||||
@@ -623,7 +627,10 @@
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
<span>Search & Link Guardian (existing client)</span>
|
||||
</div>
|
||||
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onGuardianChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
|
||||
@if (guardianName) {
|
||||
<div class="soa-guardian-selected">
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
+10
-2
@@ -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) {
|
||||
|
||||
+29
-35
@@ -546,76 +546,70 @@
|
||||
<div class="preview-section__header">
|
||||
<i class="fas fa-user-shield preview-section__icon"></i>
|
||||
<span class="preview-section__title">Nominee Details</span>
|
||||
<span class="preview-section__badge">
|
||||
{{ rdNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }}
|
||||
</span>
|
||||
<span class="preview-section__badge">{{ rdNominee?.nominationType || 'DA1' }}</span>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-grid preview-grid--three">
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Name</span>
|
||||
<span class="preview-value">{{ rdNominee?.fullName || rdNominee?.displayName || '—' }}</span>
|
||||
<span class="preview-value">{{ rdNominee?.nomineeDisplayName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee?.nomineeClientId || '—' }}</span>
|
||||
</div>
|
||||
@if (rdNominee?.cifNumber) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">CIF No</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee.cifNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Relationship</span>
|
||||
<span class="preview-value">{{ rdNominee?.relationship || '—' }}</span>
|
||||
<span class="preview-value">{{ rdNominee?.relationshipName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Share %</span>
|
||||
<span class="preview-value">{{ rdNominee?.nomineeShare ?? '—' }}%</span>
|
||||
<span class="preview-value">{{ rdNominee?.percentageShare ?? '—' }}%</span>
|
||||
</div>
|
||||
@if (rdNominee?.dateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Minor</span>
|
||||
<span class="preview-value">{{ rdNominee?.nomineeIsMinor ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
@if (rdNominee?.nomineeDateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Date of Birth</span>
|
||||
<span class="preview-value">{{ rdNominee?.dateOfBirth | dateFormat }}</span>
|
||||
<span class="preview-value">{{ rdNominee?.nomineeDateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.nationality) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Nationality</span>
|
||||
<span class="preview-value">{{ rdNominee.nationality }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.mobileNo) {
|
||||
@if (rdNominee?.nomineeMobileNo) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Mobile No</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee.mobileNo }}</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee?.nomineeMobileNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.aadhaarNumber) {
|
||||
@if (rdNominee?.nomineeAadhaar) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Aadhaar No</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee.aadhaarNumber }}</span>
|
||||
<span class="preview-label">Aadhaar</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee?.nomineeAadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.panNumber) {
|
||||
@if (rdNominee?.nomineePan) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PAN No</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee.panNumber }}</span>
|
||||
<span class="preview-label">PAN</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee?.nomineePan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.guardianName) {
|
||||
@if (rdNominee?.guardianDisplayName) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Name</span>
|
||||
<span class="preview-value">{{ rdNominee.guardianName }}</span>
|
||||
<span class="preview-label">Guardian</span>
|
||||
<span class="preview-value">{{ rdNominee?.guardianDisplayName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.guardianRelationship) {
|
||||
@if (rdNominee?.guardianClientId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Relationship</span>
|
||||
<span class="preview-value">{{ rdNominee.guardianRelationship }}</span>
|
||||
<span class="preview-label">Guardian Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ rdNominee?.guardianClientId }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (rdNominee?.remarks) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Remarks</span>
|
||||
<span class="preview-value">{{ rdNominee.remarks }}</span>
|
||||
<span class="preview-value">{{ rdNominee?.remarks }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
+9
@@ -172,6 +172,15 @@
|
||||
{{ 'labels.inputs.Standing Instructions' | translate }}
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nominees="routerLinkActive"
|
||||
[active]="nominees.isActive"
|
||||
>
|
||||
Nominees
|
||||
</a>
|
||||
@for (savingsDatatable of savingsDatatables; track savingsDatatable) {
|
||||
<a
|
||||
mat-tab-link
|
||||
|
||||
@@ -34,6 +34,7 @@ import { RecurringDepositsAccountAndTemplateResolver } from './common-resolvers/
|
||||
import { RecurringDepositsAccountTransactionResolver } from './common-resolvers/recurring-deposit-account-transaction.resolver';
|
||||
import { RecurringDepositsAccountTransactionTemplateResolver } from './common-resolvers/recurring-deposit-account-transaction-template.resolver';
|
||||
import { GeneralTabComponent } from './recurring-deposits-account-view/general-tab/general-tab.component';
|
||||
import { AccountNomineesTabComponent } from 'app/shared/account-nominees-tab/account-nominees-tab.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -102,6 +103,17 @@ const routes: Routes = [
|
||||
routeParamBreadcrumb: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'nominees',
|
||||
component: AccountNomineesTabComponent,
|
||||
data: {
|
||||
title: 'Recurring Deposit Nominees',
|
||||
breadcrumb: 'Nominees',
|
||||
routeParamBreadcrumb: false,
|
||||
accountDataKey: 'recurringDepositsAccountData',
|
||||
accountType: 'RD'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'datatables',
|
||||
children: [
|
||||
|
||||
+4
-1
@@ -99,9 +99,12 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="spd-field" (click)="endDatePicker.open()">
|
||||
<mat-label>End Date</mat-label>
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" />
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" [min]="minEndDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #endDatePicker></mat-datepicker>
|
||||
@if (CurrentProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) {
|
||||
<mat-error>End Date must be after Effective To date</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="spd-field">
|
||||
|
||||
+8
@@ -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 [];
|
||||
|
||||
+4
-1
@@ -106,9 +106,12 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="fdpd-field" (click)="endDatePicker.open()">
|
||||
<mat-label>End Date</mat-label>
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" />
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" [min]="minEndDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #endDatePicker></mat-datepicker>
|
||||
@if (fixedDepositProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) {
|
||||
<mat-error>End Date must be after Effective To date</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="fdpd-field fdpd-field--full fdpd-checkbox-row">
|
||||
|
||||
+8
@@ -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);
|
||||
|
||||
+4
-1
@@ -96,9 +96,12 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="rdpd-field" (click)="endDatePicker.open()">
|
||||
<mat-label>End Date</mat-label>
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" />
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" [min]="minEndDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #endDatePicker></mat-datepicker>
|
||||
@if (recurringDepositProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) {
|
||||
<mat-error>End Date must be after Effective To date</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="rdpd-field">
|
||||
|
||||
+8
@@ -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 [];
|
||||
|
||||
+4
-1
@@ -99,9 +99,12 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="spd-field" (click)="endDatePicker.open()">
|
||||
<mat-label>End Date</mat-label>
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" />
|
||||
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" [min]="minEndDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #endDatePicker></mat-datepicker>
|
||||
@if (savingProductDetailsForm.get('endDate')?.hasError('matDatepickerMin')) {
|
||||
<mat-error>End Date must be after Effective To date</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="spd-field">
|
||||
|
||||
+8
@@ -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 [];
|
||||
|
||||
@@ -50,8 +50,10 @@
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Currency</ng-template>
|
||||
|
||||
<mifosx-savings-account-currency-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
|
||||
</mifosx-savings-account-currency-step>
|
||||
<mifosx-savings-account-currency-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[primaryClientId]="savingsAccountTemplate?.clientId ?? null"
|
||||
></mifosx-savings-account-currency-step>
|
||||
|
||||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||||
<button mat-raised-button matStepperPrevious>
|
||||
|
||||
@@ -108,7 +108,7 @@ export class CreateSavingsAccountComponent {
|
||||
}
|
||||
|
||||
get savingsAccountNominee() {
|
||||
return this.savingsAccountCurrencyStep?.nomineeData ?? null;
|
||||
return this.savingsAccountCurrencyStep?.liveNomineeData ?? null;
|
||||
}
|
||||
|
||||
submit() {
|
||||
@@ -137,16 +137,25 @@ export class CreateSavingsAccountComponent {
|
||||
savingsAccount.accountFacilityConfig = this.savingsAccountDigital.accountFacilityConfig;
|
||||
}
|
||||
|
||||
// Nominee
|
||||
if (this.savingsAccountNominee) {
|
||||
savingsAccount.nominee = this.savingsAccountNominee;
|
||||
}
|
||||
|
||||
if (this.savingsAccountTemplate.clientId) {
|
||||
savingsAccount.clientId = this.savingsAccountTemplate.clientId;
|
||||
} else {
|
||||
savingsAccount.groupId = this.savingsAccountTemplate.groupId;
|
||||
}
|
||||
|
||||
const nominee = this.savingsAccountNominee;
|
||||
if (nominee) {
|
||||
savingsAccount.nominee = {
|
||||
nomineeClientId: nominee.nomineeClientId,
|
||||
nominationType: nominee.nominationType,
|
||||
relationshipId: nominee.relationshipId,
|
||||
percentageShare: nominee.percentageShare,
|
||||
nomineeIsMinor: nominee.nomineeIsMinor,
|
||||
...(nominee.guardianClientId ? { guardianClientId: nominee.guardianClientId } : {}),
|
||||
remarks: nominee.remarks
|
||||
};
|
||||
}
|
||||
|
||||
this.savingsService.createSavingsAccount(savingsAccount).subscribe((response: any) => {
|
||||
this.router.navigate(
|
||||
[
|
||||
|
||||
+5
-1
@@ -49,7 +49,11 @@
|
||||
</span>
|
||||
</div>
|
||||
@if (savingsAccountProductTemplate?.nominationFacility) {
|
||||
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
|
||||
<mifosx-nominee-form
|
||||
[primaryClientId]="primaryClientId"
|
||||
[showActions]="false"
|
||||
(nomineeChange)="onNomineeChange($event)"
|
||||
></mifosx-nominee-form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -132,7 +132,10 @@
|
||||
</div>
|
||||
|
||||
@if (productTemplate?.introducerRequired) {
|
||||
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onIntroducerChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +493,10 @@
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
<span>Search & Link Guardian (existing client)</span>
|
||||
</div>
|
||||
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onGuardianChange($event)"
|
||||
></mifosx-introducer-search>
|
||||
|
||||
@if (guardianName) {
|
||||
<div class="soa-guardian-selected">
|
||||
|
||||
+6
-1
@@ -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) {
|
||||
|
||||
+28
-43
@@ -478,79 +478,64 @@
|
||||
<div class="preview-section__header">
|
||||
<i class="fas fa-user-shield preview-section__icon"></i>
|
||||
<span class="preview-section__title">Nominee Details</span>
|
||||
<span class="preview-section__badge">
|
||||
{{ savingsAccountNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }}
|
||||
</span>
|
||||
<span class="preview-section__badge">{{ savingsAccountNominee?.nominationType || 'DA1' }}</span>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-grid preview-grid--three">
|
||||
<!-- Name: fullName for manual, displayName for linked -->
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Name</span>
|
||||
<span class="preview-value">{{
|
||||
savingsAccountNominee?.fullName || savingsAccountNominee?.displayName || '—'
|
||||
}}</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.nomineeDisplayName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.nomineeClientId || '—' }}</span>
|
||||
</div>
|
||||
@if (savingsAccountNominee?.cifNumber) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">CIF No</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee.cifNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Relationship</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.relationship || '—' }}</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.relationshipName || '—' }}</span>
|
||||
</div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Share %</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.nomineeShare ?? '—' }}%</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.percentageShare ?? '—' }}%</span>
|
||||
</div>
|
||||
@if (savingsAccountNominee?.dateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Minor</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.nomineeIsMinor ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
@if (savingsAccountNominee?.nomineeDateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Date of Birth</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.dateOfBirth | dateFormat }}</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.nomineeDateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.nationality) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Nationality</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.nationality }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.mobileNo) {
|
||||
@if (savingsAccountNominee?.nomineeMobileNo) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Mobile No</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.mobileNo }}</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.nomineeMobileNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.aadhaarNumber) {
|
||||
@if (savingsAccountNominee?.nomineeAadhaar) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Aadhaar No</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.aadhaarNumber }}</span>
|
||||
<span class="preview-label">Aadhaar</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.nomineeAadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.panNumber) {
|
||||
@if (savingsAccountNominee?.nomineePan) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PAN No</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.panNumber }}</span>
|
||||
<span class="preview-label">PAN</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.nomineePan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.isDependent) {
|
||||
@if (savingsAccountNominee?.guardianDisplayName) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Dependent</span>
|
||||
<span class="preview-value preview-badge preview-badge--warn">Yes</span>
|
||||
<span class="preview-label">Guardian</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.guardianDisplayName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.guardianName) {
|
||||
@if (savingsAccountNominee?.guardianClientId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Name</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.guardianName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.guardianRelationship) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Relationship</span>
|
||||
<span class="preview-value">{{ savingsAccountNominee?.guardianRelationship }}</span>
|
||||
<span class="preview-label">Guardian Client ID</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccountNominee?.guardianClientId }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountNominee?.remarks) {
|
||||
|
||||
@@ -213,6 +213,15 @@
|
||||
</a>
|
||||
}
|
||||
</ng-container>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nominees="routerLinkActive"
|
||||
[active]="nominees.isActive"
|
||||
>
|
||||
Nominees
|
||||
</a>
|
||||
@for (savingsDatatable of savingsDatatables; track savingsDatatable) {
|
||||
<a
|
||||
mat-tab-link
|
||||
|
||||
@@ -52,6 +52,7 @@ import { SavingDocumentsResolver } from './common-resolvers/saving-documents.res
|
||||
import { SavingsTransactionGeneralTabComponent } from './savings-account-view/transactions/view-transaction/savings-transaction-general-tab/savings-transaction-general-tab.component';
|
||||
import { GeneralTabComponent } from './savings-account-view/general-tab/general-tab.component';
|
||||
import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbook-tab.component';
|
||||
import { AccountNomineesTabComponent } from 'app/shared/account-nominees-tab/account-nominees-tab.component';
|
||||
|
||||
/** Savings Routes */
|
||||
const routes: Routes = [
|
||||
@@ -136,6 +137,17 @@ const routes: Routes = [
|
||||
savingsDocuments: SavingDocumentsResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'nominees',
|
||||
component: AccountNomineesTabComponent,
|
||||
data: {
|
||||
title: 'Savings Account Nominees',
|
||||
breadcrumb: 'Nominees',
|
||||
routeParamBreadcrumb: false,
|
||||
accountDataKey: 'savingsAccountData',
|
||||
accountType: 'SB'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'datatables',
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="nom-container mat-typography">
|
||||
<div class="nom-page-header">
|
||||
<div class="nom-page-header__left">
|
||||
<mat-icon class="nom-page-header__icon">shield</mat-icon>
|
||||
<div>
|
||||
<h3 class="nom-page-header__title">Nomination Facility</h3>
|
||||
<p class="nom-page-header__sub">Manage nominees for this account. Nominees are account-specific.</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm) {
|
||||
<button mat-flat-button color="primary" class="nom-add-btn" (click)="showAddForm = true">
|
||||
<mat-icon>add</mat-icon> Add Nominee
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="nom-loading">
|
||||
<mat-progress-spinner diameter="24" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Loading account...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading) {
|
||||
@if (nomineesLoading) {
|
||||
<div class="nom-loading nom-loading--sm">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Loading nominees...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (nominees.length > 0) {
|
||||
<div class="nom-share-bar">
|
||||
<span class="nom-share-bar__label">Share</span>
|
||||
<div class="nom-share-bar__track">
|
||||
<div
|
||||
class="nom-share-bar__fill"
|
||||
[style.width.%]="totalShare > 100 ? 100 : totalShare"
|
||||
[class.over]="totalShare > 100"
|
||||
[class.complete]="totalShare === 100"
|
||||
></div>
|
||||
</div>
|
||||
<span class="nom-share-bar__value" [class.over]="totalShare > 100">{{ totalShare }}%</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (nominee of nominees; track nominee.id; let i = $index) {
|
||||
<div class="nom-card">
|
||||
<div class="nom-card__main">
|
||||
<div class="nom-card__avatar">
|
||||
@if (nominee.profileImageUrl) {
|
||||
<img
|
||||
[src]="nominee.profileImageUrl"
|
||||
alt=""
|
||||
class="nom-card__photo zoomable"
|
||||
(click)="zoomImage(nominee.profileImageUrl, 'Nominee Photo')"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon>person</mat-icon>
|
||||
}
|
||||
</div>
|
||||
<div class="nom-card__info">
|
||||
<span class="nom-card__name">{{ nominee.nomineeDisplayName || nominee.firstName || '—' }}</span>
|
||||
<span class="nom-card__meta">
|
||||
{{ getRelationship(nominee) }}
|
||||
@if (nominee.nomineeDateOfBirth || nominee.dateOfBirth) {
|
||||
· DOB: {{ nominee.nomineeDateOfBirth || nominee.dateOfBirth | dateFormat }}
|
||||
}
|
||||
@if (nominee.nomineeMobileNo || nominee.mobileNo) {
|
||||
· {{ nominee.nomineeMobileNo || nominee.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (nominee.signatureUrl) {
|
||||
<div class="nom-card__sig">
|
||||
<img
|
||||
[src]="nominee.signatureUrl"
|
||||
alt="Signature"
|
||||
class="zoomable"
|
||||
(click)="zoomImage(nominee.signatureUrl, 'Nominee Signature')"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@if (nominee.nomineeIdentifier) {
|
||||
<span class="nom-card__id">{{ nominee.nomineeIdentifier }}</span>
|
||||
}
|
||||
<div class="nom-card__share">
|
||||
<span class="nom-card__share-value">{{ nominee.percentageShare || 0 }}%</span>
|
||||
</div>
|
||||
<div class="nom-card__actions">
|
||||
<button mat-icon-button type="button" matTooltip="View" (click)="viewNominee(nominee)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
matTooltip="Delete"
|
||||
class="nom-card__delete"
|
||||
(click)="deleteNominee(nominee, i)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) {
|
||||
<div class="nom-card__guardian">
|
||||
<mat-icon>shield</mat-icon>
|
||||
Guardian: <strong>{{ nominee.guardianDisplayName || nominee.guardianName }}</strong>
|
||||
@if (nominee.guardianRelationshipName) {
|
||||
({{ nominee.guardianRelationshipName }})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (editingIndex === i) {
|
||||
<div class="nom-card__edit">
|
||||
<div class="nom-edit-row">
|
||||
<mat-form-field appearance="outline" class="nom-edit-field">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select [(ngModel)]="editNomType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-edit-field">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="editRelId">
|
||||
@for (rel of relationshipOptions; track rel) {
|
||||
<mat-option [value]="rel.id">{{ rel.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-edit-field nom-edit-field--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="editShare" min="1" [max]="maxShareForEdit(i)" />
|
||||
@if (editShare && editShare > maxShareForEdit(i)) {
|
||||
<mat-error>Max {{ maxShareForEdit(i) }}%</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="nom-edit-save"
|
||||
[disabled]="!editRelId || !editShare || editShare > maxShareForEdit(i)"
|
||||
(click)="saveEdit(nominee, i)"
|
||||
>
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
<button mat-stroked-button type="button" class="nom-edit-cancel" (click)="cancelEdit()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!nomineesLoading && nominees.length === 0 && !showAddForm) {
|
||||
<div class="nom-empty">
|
||||
<mat-icon>person_add</mat-icon>
|
||||
<span>No nominees registered for this account.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showAddForm) {
|
||||
<div class="nom-add-form">
|
||||
<div class="nom-add-form__title"><fa-icon icon="search"></fa-icon> Search & Link Nominee</div>
|
||||
|
||||
@if (!selectedClient) {
|
||||
<div class="nom-search-wrapper">
|
||||
<mat-form-field appearance="outline" class="nom-search-field">
|
||||
<mat-label>Search by Name, Client ID or Mobile</mat-label>
|
||||
<span matPrefix class="nom-search-prefix"><mat-icon>search</mat-icon></span>
|
||||
<input
|
||||
matInput
|
||||
[value]="searchText"
|
||||
(input)="onSearch($any($event.target).value)"
|
||||
(blur)="dismissSearch()"
|
||||
placeholder="Type at least 2 characters..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (searchResults.length > 0 || searching) {
|
||||
<div class="nom-search-results">
|
||||
@if (searching) {
|
||||
<div class="nom-search-loading">
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> Searching...
|
||||
</div>
|
||||
}
|
||||
@for (client of searchResults; track client.id) {
|
||||
<div class="nom-search-item" (mousedown)="selectClient(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 }} · {{ client.officeName }}
|
||||
@if (client.dateOfBirth) {
|
||||
· DOB: {{ client.dateOfBirth | dateFormat }}
|
||||
}
|
||||
@if (client.mobileNo) {
|
||||
· {{ client.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
|
||||
<div class="search-no-results-hint">No clients found</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedClient) {
|
||||
<div class="nom-linked-card">
|
||||
<div class="nom-linked-card__left">
|
||||
@if (linkedProfileImage) {
|
||||
<img
|
||||
[src]="linkedProfileImage"
|
||||
class="nom-linked-card__photo zoomable"
|
||||
(click)="zoomImage(linkedProfileImage, 'Photo')"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon class="nom-linked-card__icon">person</mat-icon>
|
||||
}
|
||||
</div>
|
||||
<div class="nom-linked-card__info">
|
||||
<span class="nom-linked-card__name">{{ selectedClient.displayName }}</span>
|
||||
<span class="nom-linked-card__meta"
|
||||
>ID: {{ selectedClient.id }}
|
||||
@if (selectedClient.mobileNo) {
|
||||
· {{ selectedClient.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (linkedSignatureImage) {
|
||||
<div class="nom-linked-card__sig">
|
||||
<img
|
||||
[src]="linkedSignatureImage"
|
||||
alt="Signature"
|
||||
class="zoomable"
|
||||
(click)="zoomImage(linkedSignatureImage, 'Signature')"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<button mat-icon-button type="button" (click)="removeSelectedClient()"><mat-icon>close</mat-icon></button>
|
||||
</div>
|
||||
|
||||
@if (fetchingDetails) {
|
||||
<div class="nom-fetching-hint">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> Fetching details...
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!fetchingDetails) {
|
||||
@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
|
||||
>
|
||||
}
|
||||
@if (selectedClient.panNumber) {
|
||||
<span class="nom-ro-item"
|
||||
><span class="nom-ro-label">PAN</span> {{ selectedClient.panNumber }}
|
||||
@if (linkedKycStatus?.panVerified) {
|
||||
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (selectedClient.aadhaarMasked) {
|
||||
<span class="nom-ro-item"
|
||||
><span class="nom-ro-label">Aadhaar</span> {{ selectedClient.aadhaarMasked }}
|
||||
@if (linkedKycStatus?.aadhaarVerified) {
|
||||
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="nom-fields-row">
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Nomination Type</mat-label>
|
||||
<mat-select [(ngModel)]="nomineeNomType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="nomineeRelationshipId">
|
||||
@for (rel of relationshipOptions; track rel) {
|
||||
<mat-option [value]="rel.id">{{ rel.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-field-item nom-field-item--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="nomineeShare" min="1" [max]="maxAllowedShare" />
|
||||
@if (nomineeShare && nomineeShare > maxAllowedShare) {
|
||||
<mat-error>Max {{ maxAllowedShare }}%</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="nom-field-item nom-field-item--toggle">
|
||||
<mat-slide-toggle [(ngModel)]="nomineeIsMinor">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>
|
||||
@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"
|
||||
[class.nom-search-item--disabled]="selectedClient && client.id === selectedClient.id"
|
||||
(click)="selectGuardian(client)"
|
||||
>
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
<span class="nom-search-item__name">{{ client.displayName }}</span>
|
||||
@if (selectedClient && client.id === selectedClient.id) {
|
||||
<span class="nom-conflict-tag">Same as nominee</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<span class="nom-guardian-selected" [class.nom-guardian-selected--error]="guardianSameAsNominee"
|
||||
>{{ selectedGuardian.displayName }}
|
||||
<button mat-icon-button (click)="selectedGuardian = null">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button></span
|
||||
>
|
||||
@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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-remarks-field">
|
||||
<mat-label>Remarks</mat-label>
|
||||
<input matInput [(ngModel)]="nomineeRemarks" placeholder="Optional" />
|
||||
</mat-form-field>
|
||||
|
||||
<div class="nom-form-actions">
|
||||
<button mat-stroked-button type="button" (click)="resetAddForm()">Cancel</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="
|
||||
!nomineeRelationshipId ||
|
||||
!nomineeShare ||
|
||||
nomineeSaving ||
|
||||
nomineeShare > maxAllowedShare ||
|
||||
(nomineeIsMinor && !selectedGuardian) ||
|
||||
guardianSameAsNominee
|
||||
"
|
||||
(click)="saveNominee()"
|
||||
>
|
||||
@if (nomineeSaving) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<mat-icon>check</mat-icon> Save
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,11 @@
|
||||
@if (!searching && searchResults.length > 0) {
|
||||
<div class="intro-results-inline">
|
||||
@for (r of searchResults; track r.id) {
|
||||
<div class="intro-result-item" (click)="selectClient(r)">
|
||||
<div
|
||||
class="intro-result-item"
|
||||
[class.intro-result-item--disabled]="isExcluded(r)"
|
||||
(mousedown)="selectClient(r); $event.preventDefault()"
|
||||
>
|
||||
<mat-icon class="intro-result-item__icon">person</mat-icon>
|
||||
<div class="intro-result-item__info">
|
||||
<span class="intro-result-item__name">{{ r.displayName }}</span>
|
||||
@@ -49,6 +53,9 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (isExcluded(r)) {
|
||||
<span class="intro-excluded-tag">Cannot select (self)</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -9,271 +9,268 @@
|
||||
<div class="nom-section">
|
||||
<!-- Header -->
|
||||
<div class="nom-header">
|
||||
<div class="nom-header__left">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<span class="nom-header__title">Nomination Facility</span>
|
||||
</div>
|
||||
<div class="nom-mode-tabs">
|
||||
<button type="button" class="nom-tab" [class.nom-tab--active]="mode === 'manual'" (click)="switchMode('manual')">
|
||||
<i class="fas fa-pencil-alt"></i> Enter Manually
|
||||
</button>
|
||||
<button type="button" class="nom-tab" [class.nom-tab--active]="mode === 'linked'" (click)="switchMode('linked')">
|
||||
<i class="fas fa-search"></i> Link Existing Client
|
||||
</button>
|
||||
</div>
|
||||
<mat-icon class="nom-header__icon">search</mat-icon>
|
||||
<span class="nom-header__title">Search & Link Nominee</span>
|
||||
</div>
|
||||
|
||||
<div class="nom-body">
|
||||
<!-- ══════════════ MANUAL MODE ══════════════ -->
|
||||
@if (mode === 'manual') {
|
||||
<form [formGroup]="manualForm" class="nom-form">
|
||||
<!-- Basic Info -->
|
||||
<div class="nom-row">
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Full Name *</mat-label>
|
||||
<input matInput formControlName="fullName" placeholder="Nominee full name" />
|
||||
<mat-error>Full name is required</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Relationship *</mat-label>
|
||||
<mat-select formControlName="relationship">
|
||||
@for (rel of relationships; track rel) {
|
||||
<mat-option [value]="rel">{{ rel }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-error>Relationship is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="nom-row">
|
||||
<mat-form-field appearance="outline" class="nom-field" (click)="dobPicker.open()">
|
||||
<mat-label>Date of Birth</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="dobPicker"
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
formControlName="dateOfBirth"
|
||||
readonly
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="dobPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dobPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Nationality</mat-label>
|
||||
<mat-select formControlName="nationality">
|
||||
<mat-option value="Indian">Indian</mat-option>
|
||||
<mat-option value="NRI">NRI</mat-option>
|
||||
<mat-option value="Foreign National">Foreign National</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- ID Verification -->
|
||||
<div class="nom-section-label"><i class="fas fa-id-card"></i> ID Verification</div>
|
||||
|
||||
<div class="nom-row">
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Aadhaar Number</mat-label>
|
||||
<input matInput formControlName="aadhaarNumber" maxlength="12" placeholder="12-digit Aadhaar" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>PAN Number</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="panNumber"
|
||||
maxlength="10"
|
||||
placeholder="ABCDE1234F"
|
||||
style="text-transform: uppercase"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="nom-row">
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Mobile Number</mat-label>
|
||||
<input matInput formControlName="mobileNo" maxlength="10" placeholder="10-digit mobile" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Nominee Share (%)</mat-label>
|
||||
<input matInput type="number" formControlName="nomineeShare" readonly />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Guardian (shown if minor / not alive) -->
|
||||
<div class="nom-section-label">
|
||||
<i class="fas fa-user-friends"></i> Guardian Details
|
||||
<span class="nom-hint">(if nominee is a minor)</span>
|
||||
</div>
|
||||
|
||||
<div class="nom-row">
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Guardian Name</mat-label>
|
||||
<input matInput formControlName="guardianName" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Guardian Relationship</mat-label>
|
||||
<mat-select formControlName="guardianRelationship">
|
||||
@for (rel of relationships; track rel) {
|
||||
<mat-option [value]="rel">{{ rel }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Flags -->
|
||||
<div class="nom-flags">
|
||||
<mat-checkbox formControlName="isAlive" color="primary">Is Alive</mat-checkbox>
|
||||
<mat-checkbox formControlName="isDependent" color="primary">Is Dependent</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Remarks -->
|
||||
<mat-form-field appearance="outline" class="nom-field nom-field--full">
|
||||
<mat-label>Remarks</mat-label>
|
||||
<textarea matInput formControlName="remarks" rows="2"></textarea>
|
||||
<!-- Search box -->
|
||||
@if (!selectedClient) {
|
||||
<div class="nom-search-wrap">
|
||||
<mat-form-field appearance="outline" class="nom-field--full">
|
||||
<mat-label>Search by Name, Client ID or Mobile</mat-label>
|
||||
<mat-icon matPrefix class="nom-search-icon">search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[value]="searchText"
|
||||
(input)="onSearch($any($event.target).value)"
|
||||
(blur)="dismissSearch()"
|
||||
placeholder="Type at least 2 characters..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
}
|
||||
|
||||
<!-- ══════════════ LINK EXISTING CLIENT MODE ══════════════ -->
|
||||
@if (mode === 'linked') {
|
||||
@if (!selectedClient) {
|
||||
<!-- Search -->
|
||||
<div class="nom-search-wrap">
|
||||
<mat-form-field appearance="outline" class="nom-field--full">
|
||||
<mat-label>Search by Name or Client ID</mat-label>
|
||||
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" autocomplete="off" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
@if (searching) {
|
||||
<div class="nom-results">
|
||||
@if (searching || searchResults.length > 0) {
|
||||
<div class="nom-results">
|
||||
@if (searching) {
|
||||
<div class="nom-results__loading">
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Searching...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!searching && searchResults.length > 0) {
|
||||
<div class="nom-results">
|
||||
@for (r of searchResults; track r.id) {
|
||||
<div class="nom-result-item" (click)="selectClient(r)">
|
||||
<mat-icon class="nom-result-item__icon">person</mat-icon>
|
||||
<div class="nom-result-item__info">
|
||||
<span class="nom-result-item__name">{{ r.displayName }}</span>
|
||||
<span class="nom-result-item__meta">
|
||||
ID: {{ r.id }}
|
||||
@if (r.officeName) {
|
||||
· {{ r.officeName }}
|
||||
}
|
||||
@if (r.mobileNo) {
|
||||
· {{ r.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@for (r of searchResults; track r.id) {
|
||||
<div
|
||||
class="nom-result-item"
|
||||
[class.nom-result-item--disabled]="isExcludedNominee(r)"
|
||||
(mousedown)="selectClient(r); $event.preventDefault()"
|
||||
>
|
||||
<mat-icon class="nom-result-item__icon">person</mat-icon>
|
||||
<div class="nom-result-item__info">
|
||||
<span class="nom-result-item__name">{{ r.displayName }}</span>
|
||||
<span class="nom-result-item__meta">
|
||||
ID: {{ r.id }}
|
||||
@if (r.officeName) {
|
||||
· {{ r.officeName }}
|
||||
}
|
||||
@if (r.mobileNo) {
|
||||
· {{ r.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (isExcludedNominee(r)) {
|
||||
<span class="nom-self-tag">Cannot select (self)</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
|
||||
<div class="nom-no-results">No clients found</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
|
||||
<div class="nom-no-results">No clients found</div>
|
||||
<!-- Selected client card -->
|
||||
@if (selectedClient) {
|
||||
<div class="nom-client-card">
|
||||
<div class="nom-client-card__avatar">
|
||||
@if (linkedProfileImage) {
|
||||
<img [src]="linkedProfileImage" alt="" class="nom-client-card__photo" />
|
||||
} @else {
|
||||
<mat-icon class="nom-client-card__avatar-icon">person</mat-icon>
|
||||
}
|
||||
</div>
|
||||
<div class="nom-client-card__info">
|
||||
<span class="nom-client-card__name">{{ selectedClient.displayName }}</span>
|
||||
<span class="nom-client-card__meta">
|
||||
ID: {{ selectedClient.id }}
|
||||
@if (selectedClient.mobileNo) {
|
||||
· {{ selectedClient.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<button mat-icon-button type="button" class="nom-client-card__remove" (click)="removeClient()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (fetchingDetails) {
|
||||
<div class="nom-fetching">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Fetching details...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedClient) {
|
||||
<!-- Selected Client Card -->
|
||||
<div class="nom-client-card">
|
||||
<div class="nom-client-card__avatar">
|
||||
@if (linkedProfileImage) {
|
||||
<img [src]="linkedProfileImage" alt="Profile" class="nom-client-card__photo" />
|
||||
} @else {
|
||||
<mat-icon class="nom-client-card__avatar-icon">person</mat-icon>
|
||||
@if (!fetchingDetails) {
|
||||
<!-- KYC row -->
|
||||
@if (clientDob || clientPan || clientAadhaar) {
|
||||
<div class="nom-kyc-row">
|
||||
@if (clientDob) {
|
||||
<span class="nom-kyc-item">
|
||||
<span class="nom-kyc-label">DOB</span>
|
||||
<span class="nom-kyc-value">{{ clientDob | dateFormat }}</span>
|
||||
@if (isMinorByDob) {
|
||||
<span class="nom-minor-badge">Minor</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (clientPan) {
|
||||
<span class="nom-kyc-item">
|
||||
<span class="nom-kyc-label">PAN</span>
|
||||
<span class="nom-kyc-value">
|
||||
{{ clientPan }}
|
||||
@if (linkedKycStatus?.panVerified) {
|
||||
<mat-icon class="nom-kyc-ok">check_circle</mat-icon>
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
@if (clientAadhaar) {
|
||||
<span class="nom-kyc-item">
|
||||
<span class="nom-kyc-label">AADHAAR</span>
|
||||
<span class="nom-kyc-value">
|
||||
{{ clientAadhaar }}
|
||||
@if (linkedKycStatus?.aadhaarVerified) {
|
||||
<mat-icon class="nom-kyc-ok">check_circle</mat-icon>
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="nom-client-card__info">
|
||||
<span class="nom-client-card__name">{{ selectedClient.displayName }}</span>
|
||||
<span class="nom-client-card__meta">
|
||||
Client ID: {{ selectedClient.id }}
|
||||
@if (selectedClient.accountNumber) {
|
||||
· A/C: {{ selectedClient.accountNumber }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="removeLinked()"
|
||||
class="nom-client-card__remove"
|
||||
matTooltip="Remove"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (fetchingLinkedData) {
|
||||
<div class="nom-fetching">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Fetching nominee details...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!fetchingLinkedData) {
|
||||
<div class="nom-details-grid">
|
||||
<div class="nom-detail">
|
||||
<span class="nom-detail__label">Full Name</span>
|
||||
<span class="nom-detail__value">{{ selectedClient.displayName }}</span>
|
||||
</div>
|
||||
<div class="nom-detail">
|
||||
<span class="nom-detail__label">Date of Birth</span>
|
||||
<span class="nom-detail__value">{{ clientDob || '—' }}</span>
|
||||
</div>
|
||||
@if (clientAadhaar) {
|
||||
<div class="nom-detail">
|
||||
<span class="nom-detail__label">Aadhaar</span>
|
||||
<span class="nom-detail__value">
|
||||
{{ clientAadhaar }}
|
||||
@if (linkedKycStatus?.aadhaarVerified) {
|
||||
<span class="nom-verified"><mat-icon>verified</mat-icon> Verified</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Fields row -->
|
||||
<div class="nom-fields-row">
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Nomination Type</mat-label>
|
||||
<mat-select [(ngModel)]="nominationType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="relationshipId">
|
||||
@for (rel of relationshipOptions; track rel.id) {
|
||||
<mat-option [value]="rel.id">{{ rel.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field-item nom-field-item--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="percentageShare" min="1" max="100" />
|
||||
@if (percentageShare && (percentageShare < 1 || percentageShare > 100)) {
|
||||
<mat-error>1–100</mat-error>
|
||||
}
|
||||
@if (clientPan) {
|
||||
<div class="nom-detail">
|
||||
<span class="nom-detail__label">PAN</span>
|
||||
<span class="nom-detail__value">
|
||||
{{ clientPan }}
|
||||
@if (linkedKycStatus?.panVerified) {
|
||||
<span class="nom-verified"><mat-icon>verified</mat-icon> Verified</span>
|
||||
}
|
||||
</span>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="nom-field-item nom-minor-toggle">
|
||||
<mat-slide-toggle [(ngModel)]="nomineeIsMinor" [disabled]="minorToggleLocked">Minor</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Guardian section — shown only when minor is ON -->
|
||||
@if (nomineeIsMinor) {
|
||||
<div class="nom-guardian-section">
|
||||
<div class="nom-guardian-header">
|
||||
<mat-icon class="nom-guardian-icon">shield</mat-icon>
|
||||
<span class="nom-guardian-title">Guardian</span>
|
||||
<span class="nom-guardian-hint">(Required for minor nominee)</span>
|
||||
</div>
|
||||
|
||||
@if (!selectedGuardian) {
|
||||
<div class="nom-search-wrap">
|
||||
<mat-form-field appearance="outline" class="nom-field--full">
|
||||
<mat-label>Search Guardian by Name or Client ID</mat-label>
|
||||
<mat-icon matPrefix class="nom-search-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 (guardianSearching || guardianSearchResults.length > 0) {
|
||||
<div class="nom-results">
|
||||
@if (guardianSearching) {
|
||||
<div class="nom-results__loading">
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Searching...</span>
|
||||
</div>
|
||||
}
|
||||
@for (r of guardianSearchResults; track r.id) {
|
||||
<div
|
||||
class="nom-result-item"
|
||||
[class.nom-result-item--disabled]="selectedClient && r.id === selectedClient.id"
|
||||
(mousedown)="selectGuardian(r); $event.preventDefault()"
|
||||
>
|
||||
<mat-icon class="nom-result-item__icon">person</mat-icon>
|
||||
<div class="nom-result-item__info">
|
||||
<span class="nom-result-item__name">{{ r.displayName }}</span>
|
||||
<span class="nom-result-item__meta"
|
||||
>ID: {{ r.id }}
|
||||
@if (r.mobileNo) {
|
||||
· {{ r.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (selectedClient && r.id === selectedClient.id) {
|
||||
<span class="nom-result-item__conflict">Same as nominee</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!guardianSearching && guardianSearchText.length >= 2 && guardianSearchResults.length === 0) {
|
||||
<div class="nom-no-results">No clients found</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Selected guardian card -->
|
||||
<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)="removeGuardian()">
|
||||
<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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Relationship picker for linked client -->
|
||||
<div class="nom-linked-extras">
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Relationship *</mat-label>
|
||||
<mat-select [(ngModel)]="linkedRelationship" (ngModelChange)="onLinkedRelationshipChange($event)">
|
||||
@for (rel of relationships; track rel) {
|
||||
<mat-option [value]="rel">{{ rel }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Remarks -->
|
||||
<mat-form-field appearance="outline" class="nom-field--full">
|
||||
<mat-label>Remarks</mat-label>
|
||||
<input matInput [(ngModel)]="remarks" placeholder="Optional" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-field">
|
||||
<mat-label>Nominee Share (%)</mat-label>
|
||||
<input matInput type="number" [value]="100" readonly />
|
||||
</mat-form-field>
|
||||
<!-- Actions -->
|
||||
@if (showActions) {
|
||||
<div class="nom-actions">
|
||||
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="button" [disabled]="!isValid" (click)="save()">
|
||||
<mat-icon>check</mat-icon> Save
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<NomineeData | null>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user