- 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>
313 lines
16 KiB
HTML
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 & 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>
|