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:
co-authored by
Claude Opus 4.7
parent
bf3e26a0d0
commit
b149fef72e
@@ -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);
|
||||
Reference in New Issue
Block a user