Files
cbsnexora/src/app/clients/clients-view/nominee-tab/nominee-tab.component.html
T
Nitesh YadavandClaude Sonnet 4.6 d367e76104 merging code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:26:25 +05:30

460 lines
19 KiB
HTML

<!--
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">
<!-- Header -->
<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">Account Nominees</h3>
<p class="nom-page-header__sub">Manage nominees for each account. Nominees are account-specific.</p>
</div>
</div>
</div>
@if (loading) {
<div class="nom-loading">
<mat-progress-spinner diameter="24" mode="indeterminate"></mat-progress-spinner>
<span>Loading accounts...</span>
</div>
}
@if (!loading && !hasAccounts) {
<div class="nom-no-accounts">
<mat-icon class="nom-no-accounts__icon">account_balance_wallet</mat-icon>
<p>No active accounts found.</p>
<p class="nom-no-accounts__sub">Nominees can be added once an account is opened.</p>
</div>
}
@if (!loading && hasAccounts) {
<!-- Compact: Type chips + Account dropdown + Add button in one row -->
<div class="nom-toolbar">
<div class="nom-type-chips">
@for (group of accountGroups; track group.type) {
@if (group.accounts.length > 0) {
<button
type="button"
class="nom-type-chip"
[class.active]="selectedType === group.type"
(click)="selectedType = group.type; onTypeChange()"
>
<mat-icon>{{ group.icon }}</mat-icon>
{{ group.label }}
<span class="nom-type-chip__count">{{ group.accounts.length }}</span>
</button>
}
}
</div>
@if (selectedGroup && selectedGroup.accounts.length > 0) {
<div class="nom-toolbar__right">
<mat-form-field appearance="outline" class="nom-account-select">
<mat-label>Account</mat-label>
<mat-select [(ngModel)]="selectedAccountId" (ngModelChange)="onAccountChange()">
@for (acc of selectedGroup.accounts; track acc.id) {
<mat-option [value]="acc.id"
>{{ acc.productName }} — {{ acc.accountNo }}
@if (acc.accountBalance != null) {
({{ acc.accountBalance | number: '1.2-2' }})
}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (selectedAccountId && !showAddForm) {
<button
mat-flat-button
color="primary"
class="nom-add-btn"
[disabled]="totalShare >= 100"
(click)="showAddForm = true"
>
<mat-icon>add</mat-icon> Add Nominee
</button>
}
</div>
}
</div>
<!-- Nominees for selected account -->
@if (selectedAccountId) {
@if (nomineesLoading) {
<div class="nom-loading nom-loading--sm">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
<span>Loading...</span>
</div>
}
<!-- Share bar + Nominee list -->
@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>
}
<!-- Inline Edit -->
@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>
}
<!-- Empty -->
@if (!nomineesLoading && nominees.length === 0 && !showAddForm) {
<div class="nom-empty">
<mat-icon>person_add</mat-icon>
<span>No nominees for this account.</span>
</div>
}
<!-- ═══ ADD NOMINEE FORM ═══ -->
@if (showAddForm) {
<div class="nom-add-form">
<div class="nom-add-form__title"><fa-icon icon="search"></fa-icon> Search &amp; 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.fatherSpouseName) {
· {{ client.fatherSpouseName }}
}
@if (client.mobileNo) {
· {{ client.mobileNo }}
}
</span>
</div>
</div>
}
</div>
}
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
<div class="search-no-results-hint">No clients found</div>
}
</div>
}
@if (selectedClient) {
<!-- Linked Card -->
<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) {
<!-- KYC details -->
@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>
}
<!-- Nominee fields inline -->
<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" (click)="selectGuardian(client)">
<mat-icon class="nom-search-item__icon">person</mat-icon>
<span class="nom-search-item__name">{{ client.displayName }}</span>
</div>
}
</div>
}
} @else {
<span class="nom-guardian-selected"
>{{ selectedGuardian.displayName }}
<button mat-icon-button (click)="selectedGuardian = null">
<mat-icon>close</mat-icon>
</button></span
>
}
</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)
"
(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>