- Block 0% nominee share in all steppers (Savings, FD, RD, Current) — Add button disabled via isValid, addNominee() guards < 1, nominee-form error condition fixed for value=0 - Joint ownership: default primary share 50%, enforce min 1% on blur, max 100% on input with DOM clamp via $event, auto-redistribute co-holders proportionally when primary share changes, isJointOwnershipValid guards each holder >= 1 - Pending/Rejected account tabs: normalize date format to 22 May 2026 for both array and string date values from API Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
284 lines
11 KiB
HTML
284 lines
11 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-section">
|
||
<!-- Header -->
|
||
<div class="nom-header">
|
||
<mat-icon class="nom-header__icon">search</mat-icon>
|
||
<span class="nom-header__title">Search & Link Nominee</span>
|
||
</div>
|
||
|
||
<div class="nom-body">
|
||
<!-- 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>
|
||
|
||
@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>
|
||
}
|
||
@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>
|
||
@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>
|
||
}
|
||
|
||
<!-- 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 (!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>
|
||
}
|
||
|
||
<!-- 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 !== null &&
|
||
percentageShare !== undefined &&
|
||
(percentageShare < 1 || percentageShare > 100)
|
||
) {
|
||
<mat-error>Share must be 1–100%</mat-error>
|
||
}
|
||
</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>
|
||
}
|
||
|
||
<!-- Remarks -->
|
||
<mat-form-field appearance="outline" class="nom-field--full">
|
||
<mat-label>Remarks</mat-label>
|
||
<input matInput [(ngModel)]="remarks" placeholder="Optional" />
|
||
</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>
|
||
}
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|