Files
cbsnexora/src/app/shared/nominee-form/nominee-form.component.html
T
Jaanu423andClaude Sonnet 4.6 ad6f629c3f fix: nominee share & joint ownership validation across all account types
- 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>
2026-05-23 14:05:00 +05:30

284 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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 &amp; 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>