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
+37
View File
@@ -0,0 +1,37 @@
/**
* 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/.
*/
/**
* Predicates that decide which optional family/nominee UI a client is
* eligible for. Mirrors the backend authorization checks so the UI stays
* in lockstep with what the server will accept.
*
* Individual (legalForm = 1) → family + nominees
* Non-individual + Proprietorship → nominees only
* Non-individual + anything else → neither
*/
const normalize = (s: string | null | undefined): string => (s || '').toUpperCase().replace(/[\s_-]+/g, '');
export function isProprietorship(constitutionLabel: string | null | undefined): boolean {
const n = normalize(constitutionLabel);
return n === 'PROPRIETORSHIP' || n === 'PROPRIETOR' || n === 'SOLEPROPRIETORSHIP' || n === 'SOLEPROPRIETOR';
}
export function isIndividualClient(legalFormId: number | null | undefined): boolean {
return legalFormId === 1; // 1 = PERSON, 2 = ENTITY
}
export function familyAllowed(legalFormId: number | null | undefined, _constitutionLabel?: string | null): boolean {
return isIndividualClient(legalFormId);
}
export function nomineeAllowed(legalFormId: number | null | undefined, constitutionLabel?: string | null): boolean {
if (isIndividualClient(legalFormId)) return true;
return isProprietorship(constitutionLabel);
}
@@ -0,0 +1,66 @@
/**
* 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/.
*/
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ClientsService } from '../clients.service';
import { familyAllowed, nomineeAllowed } from './client-rules';
/**
* Walk up the route snapshot tree until we find a `clientId` path param. The
* family-members / nominees routes are nested two levels under the client
* shell, so simple `route.params['clientId']` returns undefined.
*/
function findClientId(route: ActivatedRouteSnapshot): string | null {
let cur: ActivatedRouteSnapshot | null = route;
while (cur) {
const id = cur.params?.['clientId'];
if (id) return id;
cur = cur.parent;
}
return null;
}
/**
* Build a guard that redirects to the client's General tab when the client
* isn't eligible for the section. Resolves the client lazily so we don't
* couple the guard to any specific parent resolver.
*/
function makeEligibilityGuard(
predicate: (legalFormId: number | null | undefined, constitutionLabel?: string | null) => boolean
): CanActivateFn {
return (route) => {
const router = inject(Router);
const clientsService = inject(ClientsService);
const clientId = findClientId(route);
// No clientId in the URL — let the framework's own missing-param handling kick in.
if (!clientId) return true;
return clientsService.getClientData(clientId).pipe(
map((client: any) => {
const legalFormId = client?.legalForm?.id;
const constitutionLabel = client?.clientNonPersonDetails?.constitution?.name || '';
if (predicate(legalFormId, constitutionLabel)) return true;
return router.createUrlTree([
'/clients',
clientId,
'general'
]);
}),
// If the client lookup fails, fail open and let downstream handle it
// rather than redirecting users into an infinite loop on a stale URL.
catchError(() => of(true))
) as Observable<boolean | ReturnType<Router['createUrlTree']>>;
};
}
export const familyAllowedGuard: CanActivateFn = makeEligibilityGuard(familyAllowed);
export const nomineeAllowedGuard: CanActivateFn = makeEligibilityGuard(nomineeAllowed);