Files
cbsnexora/src/app/clients/clients-view/nominee-tab/nominee-tab.component.html
T
AdityaandClaude Opus 4.6 b890d24bb7 feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API
- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview
- Hide Financial & Classification section for non-individual entities in personal data and preview
- Fix legal form column in clients list — fetch legalForm from client API for correct display
- Add person/business icon before client name in list rows
- Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API
- Add registration number format hint (mat-hint) based on constitution type
- Add spacing between verification row and constitution type dropdowns
- Reduce table cell padding and prevent action buttons from being clipped
- Add missing MPL-2.0 license headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:10:51 +05:30

313 lines
16 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>