feat: KYC re-verify UX, family/nominee tab gating, savings dialogs, cfcfrms shell

- KYC tab: fix Verify (PAN) and Send OTP (Aadhaar) being disabled in edit mode;
  unmask Aadhaar input and show on-file masked value as hint; resolve kycId
  from id/kycId/resourceId fallbacks.
- Client stepper/view: add familyAllowed / nomineeAllowed predicates +
  CanActivate route guards. Family tab hidden for non-individuals, Nominee
  tab hidden for non-proprietor entities. Family-members step accepts a
  nomineeOnly input for proprietorship clients. Add/Edit/Delete buttons
  inside family-members and nominee tabs gated by canAdd.
- cfcfrms: add cfcfrms-shell wrapper, restyle complaint/fraud/mule/escalate
  pages and dashboard.
- Savings: identity-verify and transaction-confirm dialogs, manage / activate
  / add-charge flows, account preview styling.
- Tasks, core shell, account-nominees, icons: minor style and behaviour
  updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-21 18:49:43 +05:30
co-authored by Claude Opus 4.7
parent bf3e26a0d0
commit b149fef72e
99 changed files with 8575 additions and 2574 deletions
@@ -481,7 +481,7 @@
>
<i class="fa fa-map-marker cv-tab-icon"></i> {{ 'labels.inputs.Address' | translate }}
</a>
@if (clientViewData?.legalForm?.id !== 2) {
@if (showFamilyTab) {
<a
mat-tab-link
[routerLink]="['./family-members']"
@@ -492,15 +492,17 @@
<i class="fa fa-users cv-tab-icon"></i> Family Members
</a>
}
<a
mat-tab-link
[routerLink]="['./nominees']"
routerLinkActive
#nomineesTab="routerLinkActive"
[active]="nomineesTab.isActive"
>
<i class="fa fa-shield cv-tab-icon"></i> Nominee
</a>
@if (showNomineeTab) {
<a
mat-tab-link
[routerLink]="['./nominees']"
routerLinkActive
#nomineesTab="routerLinkActive"
[active]="nomineesTab.isActive"
>
<i class="fa fa-shield cv-tab-icon"></i> Nominee
</a>
}
<a
mat-tab-link
[routerLink]="['./identities']"
@@ -26,6 +26,7 @@ import { CaptureImageDialogComponent } from './custom-dialogs/capture-image-dial
/** Custom Services */
import { ClientsService } from '../clients.service';
import { LegalFormId } from '../models/legal-form.enum';
import { familyAllowed, nomineeAllowed } from '../utils/client-rules';
import {
MatCard,
MatCardHeader,
@@ -143,6 +144,21 @@ export class ClientsViewComponent implements OnInit {
clientImage: any;
clientTemplateData: any;
/** Constitution display name (entity clients only). Empty for individuals. */
get constitutionLabel(): string {
return this.clientViewData?.clientNonPersonDetails?.constitution?.name || '';
}
/** Whether the "Family Members" tab should appear. */
get showFamilyTab(): boolean {
return familyAllowed(this.clientViewData?.legalForm?.id, this.constitutionLabel);
}
/** Whether the "Nominees" tab should appear. */
get showNomineeTab(): boolean {
return nomineeAllowed(this.clientViewData?.legalForm?.id, this.constitutionLabel);
}
constructor() {
this.route.data.subscribe((data: { clientViewData: any; clientTemplateData: any; clientDatatables: any }) => {
this.clientViewData = data.clientViewData;
@@ -14,9 +14,11 @@
<h3 class="nom-header__title">Family Members</h3>
<span class="nom-header__badge">{{ familyMembers.length }}</span>
</div>
<button mat-flat-button color="primary" class="nom-header__btn" (click)="addFamilyMember()">
<mat-icon>add</mat-icon> Add Member
</button>
@if (canAdd) {
<button mat-flat-button color="primary" class="nom-header__btn" (click)="addFamilyMember()">
<mat-icon>add</mat-icon> Add Member
</button>
}
</div>
@if (familyMembers.length === 0) {
@@ -44,21 +46,28 @@
</mat-panel-description>
</mat-expansion-panel-header>
<div class="nom-actions">
<div class="nom-actions__buttons">
<button mat-stroked-button color="primary" class="nom-actions__edit" (click)="editFamilyMember(member, i)">
<fa-icon icon="edit" class="m-r-10"></fa-icon> Edit
</button>
<button
mat-stroked-button
color="warn"
class="nom-actions__delete"
(click)="deleteMember(member, 'Family Member')"
>
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
</button>
@if (canAdd) {
<div class="nom-actions">
<div class="nom-actions__buttons">
<button
mat-stroked-button
color="primary"
class="nom-actions__edit"
(click)="editFamilyMember(member, i)"
>
<fa-icon icon="edit" class="m-r-10"></fa-icon> Edit
</button>
<button
mat-stroked-button
color="warn"
class="nom-actions__delete"
(click)="deleteMember(member, 'Family Member')"
>
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
</button>
</div>
</div>
</div>
}
<div class="nom-grid">
@if (member.firstName) {
@@ -21,6 +21,7 @@ import { ClientsService } from '../../clients.service';
import { TranslateService } from '@ngx-translate/core';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { familyAllowed } from '../../utils/client-rules';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatIcon } from '@angular/material/icon';
@@ -71,6 +72,12 @@ export class FamilyMembersTabComponent {
clientId: string;
/** Family member template options */
familyMemberOptions: any;
/**
* Defensive flag — the route guard already blocks unauthorised clients, but
* we re-check here so that if the component is somehow reachable via direct
* link or stale state, the Add / Edit buttons stay hidden.
*/
canAdd = false;
constructor() {
this.route.data.subscribe((data: { clientFamilyMembers: any }) => {
@@ -80,6 +87,17 @@ export class FamilyMembersTabComponent {
// so the clientId param lives two levels up, not one.
this.clientId =
this.route.parent?.parent?.snapshot.params['clientId'] || this.route.parent?.snapshot.params['clientId'];
if (this.clientId) {
this.clientsService.getClientData(this.clientId).subscribe({
next: (client: any) => {
const constitutionLabel = client?.clientNonPersonDetails?.constitution?.name || '';
this.canAdd = familyAllowed(client?.legalForm?.id, constitutionLabel);
},
error: () => {
this.canAdd = false;
}
});
}
}
/** Family members (non-nominee entries) */
@@ -116,6 +134,7 @@ export class FamilyMembersTabComponent {
}
addFamilyMember() {
if (!this.canAdd) return;
this.ensureTemplateLoaded().then(() => {
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
width: '640px',
@@ -124,7 +143,10 @@ export class FamilyMembersTabComponent {
data: {
context: 'Add',
options: this.familyMemberOptions,
existingMembers: this.familyMembers
existingMembers: this.familyMembers,
// Pass the host client's id so the dialog's self-add guard can
// filter the search results and refuse self-link attempts.
hostClientId: this.clientId
}
});
dialogRef.afterClosed().subscribe((response: any) => {
@@ -139,6 +161,7 @@ export class FamilyMembersTabComponent {
}
editFamilyMember(member: any, _index: number) {
if (!this.canAdd) return;
this.ensureTemplateLoaded().then(() => {
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
width: '640px',
@@ -148,7 +171,8 @@ export class FamilyMembersTabComponent {
context: 'Edit',
options: this.familyMemberOptions,
member,
existingMembers: this.familyMembers
existingMembers: this.familyMembers,
hostClientId: this.clientId
}
});
dialogRef.afterClosed().subscribe((response: any) => {
@@ -226,6 +250,7 @@ export class FamilyMembersTabComponent {
// ═══ Common ═══
deleteMember(member: any, label: string) {
if (!this.canAdd) return;
const realIndex = this.clientFamilyMembers.indexOf(member);
const deleteFamilyMemberDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `${label} ${member.firstName || ''}` }
@@ -560,9 +560,14 @@
matInput
formControlName="aadhaarNumber"
maxlength="12"
type="password"
inputmode="numeric"
autocomplete="off"
placeholder="Enter 12-digit Aadhaar"
[readonly]="aadhaarOtpSent"
/>
@if (kycData?.aadhaarNumberMasked) {
<mat-hint>On file: {{ kycData.aadhaarNumberMasked }}</mat-hint>
}
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
}
@@ -147,12 +147,15 @@ export class KycTabComponent implements OnInit, OnDestroy {
this.loading = false;
this.kycData = data;
if (data) {
this.kycId = data.id?.toString() || null;
const rawKycId = data.id ?? data.kycId ?? data.resourceId;
this.kycId = rawKycId != null ? rawKycId.toString() : null;
this.kycForm.patchValue({
kycType: data.kycType || 1,
riskCategory: data.riskCategory || 1,
panNumber: data.panNumber || '',
aadhaarNumber: data.aadhaarNumberMasked || '',
// Aadhaar is stored masked on the server; user must re-enter
// the full 12-digit number to re-verify, so don't pre-fill.
aadhaarNumber: '',
gstinNumber: data.gstinNumber || '',
remarks: data.remarks || ''
});
@@ -181,6 +184,15 @@ export class KycTabComponent implements OnInit, OnDestroy {
toggleEditMode(): void {
this.isEditMode = !this.isEditMode;
this.resetOtpState();
if (this.isEditMode) {
// Clear Aadhaar so the user types a fresh 12-digit number for re-verify;
// server only returns the masked form which would fail validation.
this.kycForm.get('aadhaarNumber')?.reset('');
this.verificationStatus.aadhaar = null;
this.verificationStatus.pan = null;
this.verificationError.aadhaar = null;
this.verificationError.pan = null;
}
}
saveKyc(): void {
@@ -66,7 +66,7 @@
}
</mat-select>
</mat-form-field>
@if (selectedAccountId && !showAddForm) {
@if (canAdd && selectedAccountId && !showAddForm) {
<button
mat-flat-button
color="primary"
@@ -153,18 +153,20 @@
<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>
@if (canAdd) {
<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)) {
@@ -11,6 +11,7 @@ import { MatDialog } from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ClientsService } from '../../clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { nomineeAllowed } from '../../utils/client-rules';
import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
import { ImageZoomDialogComponent } from '../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -55,6 +56,12 @@ export class NomineeTabComponent implements OnInit {
clientId: string;
loading = true;
accountGroups: AccountGroup[] = [];
/**
* Defensive flag — route guard already blocks unauthorised clients, but
* we re-check here so Add / Edit / Delete stay hidden if the component is
* reached via direct link or stale state.
*/
canAdd = false;
selectedType: string = '';
selectedAccountId: number | null = null;
@@ -132,6 +139,23 @@ export class NomineeTabComponent implements OnInit {
this.clientId = this.route.parent.snapshot.params['clientId'];
this.loadAccounts();
this.loadTemplate();
this.loadClientEligibility();
}
private loadClientEligibility(): void {
if (!this.clientId) {
this.canAdd = false;
return;
}
this.clientsService.getClientData(this.clientId).subscribe({
next: (client: any) => {
const constitutionLabel = client?.clientNonPersonDetails?.constitution?.name || '';
this.canAdd = nomineeAllowed(client?.legalForm?.id, constitutionLabel);
},
error: () => {
this.canAdd = false;
}
});
}
private loadTemplate(): void {
@@ -462,6 +486,7 @@ export class NomineeTabComponent implements OnInit {
// ═══ Save Nominee ═══
saveNominee(): void {
if (!this.canAdd) return;
if (!this.selectedClient || !this.nomineeRelationshipId || !this.nomineeShare) return;
if (this.totalShare + this.nomineeShare > 100) return;
@@ -531,6 +556,7 @@ export class NomineeTabComponent implements OnInit {
}
editNominee(nominee: any, index: number): void {
if (!this.canAdd) return;
this.editingIndex = index;
this.editRelId = nominee.relationshipId || null;
this.editShare = nominee.percentageShare;
@@ -566,6 +592,7 @@ export class NomineeTabComponent implements OnInit {
}
deleteNominee(nominee: any, index: number): void {
if (!this.canAdd) return;
const ref = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `nominee ${nominee.nomineeDisplayName || nominee.firstName || ''}` }
});