feat(digital+toolbar): NEFT/RTGS module, AI menu, polished header icons

- Add /digital/neft-rtgs (entry) and /digital/neft-rtgs-report (analytics):
  KPIs, tabbed views (Trends, Bank-wise, Date-wise, Pending/Pass/Return,
  Branch/Mobile, User-wise), filterable transaction table with CSV export,
  and a non-dismissible Initiate dialog launched from a floating action.
- Make BANL verification mandatory before NEFT/RTGS submit; auto-fill and
  lock beneficiary name from the lookup response, invalidate match on
  A/C or IFSC edit, demo fallback for unknown accounts.
- Add Digital menu links (NEFT/RTGS + Report) in shell top-nav.
- Replace toolbar theme toggle with AI / Intelligence Modules mega menu:
  4 colour-coded columns covering Lending & Risk, Investment & Profit,
  Compliance & Advisory, Governance (9 modules total). Wire RBI
  Notifications Monitoring to /rbi-circulars.
- Unify notifications, settings, AI, and profile toolbar buttons into a
  26px ring style; colourful gradient bell SVG for notifications,
  custom gear+petals AI mark, online dot on the avatar.
- Trim ISO 27001 chip in the top bar.
- Add MPL header to generated .env.ts and its template in version.js.
- Bundles other pending working-tree changes (clients family/nominee
  tab tweaks, auth/2FA fixes, translations) into the same push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-15 11:50:26 +05:30
co-authored by Claude Opus 4.7
parent 6714897688
commit 595054b0b8
35 changed files with 4409 additions and 194 deletions
@@ -169,13 +169,28 @@
</div>
</div>
}
@if (linkedSelectError) {
<div class="fm-linked-error">
<mat-icon>error_outline</mat-icon>
<span>{{ linkedSelectError }}</span>
</div>
}
@if (!searching && searchResults.length > 0) {
<div class="fm-search-results">
@for (r of searchResults; track r.id) {
<div class="fm-search-item" (click)="selectClient(r)">
<div
class="fm-search-item"
[class.is-disabled]="isClientAlreadyLinked(r.id)"
(click)="selectClient(r)"
>
<mat-icon class="fm-search-item__icon">person</mat-icon>
<div class="fm-search-item__info">
<span class="fm-search-item__name">{{ r.displayName }}</span>
<span class="fm-search-item__name">
{{ r.displayName }}
@if (isClientAlreadyLinked(r.id)) {
<span class="fm-search-item__taken">· already linked</span>
}
</span>
<span class="fm-search-item__meta"
>ID: {{ r.id }} · {{ r.officeName }}
@if (r.dateOfBirth) {
@@ -306,7 +321,14 @@
</mat-option>
}
</mat-select>
<mat-hint>Father, Mother and Spouse can only be added once.</mat-hint>
<mat-icon
matSuffix
class="fm-rel-hint-icon"
matTooltip="Father, Mother and Spouse can only be added once."
matTooltipPosition="left"
>
info_outline
</mat-icon>
</mat-form-field>
@if (entryMode === 'manual') {
@@ -36,6 +36,60 @@
color: #94a3b8;
}
/* Small info icon in the Relationship field's suffix — hover reveals the
"only once" hint without consuming layout space below the field. */
.fm-rel-hint-icon {
color: #94a3b8;
font-size: 18px;
width: 18px;
height: 18px;
cursor: help;
margin-right: 4px;
&:hover {
color: #1074b9;
}
}
/* Inline error shown above the linked-client search when a duplicate is picked. */
.fm-linked-error {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
padding: 8px 12px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 6px;
color: #b91c1c;
font-size: 12.5px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
/* Greyed-out search result for clients already linked as family members. */
.fm-search-item.is-disabled {
opacity: 0.55;
cursor: not-allowed;
background: #f8fafc;
&:hover {
background: #f8fafc;
}
}
.fm-search-item__taken {
margin-left: 6px;
font-size: 11px;
font-style: italic;
color: #dc2626;
font-weight: 600;
}
.fm-mode-btn {
display: inline-flex;
align-items: center;
@@ -24,6 +24,7 @@ import { ClientsService } from 'app/clients/clients.service';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatTooltip } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
@@ -41,6 +42,7 @@ import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
MatDialogClose,
MatIcon,
MatProgressSpinner,
MatTooltip,
FaIconComponent,
DateFormatPipe
]
@@ -245,6 +247,7 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
/** Search existing clients */
onSearch(text: string): void {
this.searchText = text;
this.linkedSelectError = '';
clearTimeout(this.searchTimer);
if (!text || text.length < 2) {
this.searchResults = [];
@@ -252,7 +255,9 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
}
this.searching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
// Family-member link picker: only Individual clients can be linked as a relative —
// non-individuals (companies / trusts) have no person-to-person relationship to model here.
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.searchResults = data?.content || [];
this.searching = false;
@@ -266,8 +271,38 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
}, 400);
}
/**
* Inline error shown above the linked-client search box. Currently used
* to surface "this client is already linked" when the user tries to
* select a duplicate.
*/
linkedSelectError = '';
/**
* True if the given client id is already linked as a family member of
* this client. The member currently being edited is excluded so its own
* linked client stays selectable.
*/
isClientAlreadyLinked(clientId: number | string): boolean {
if (clientId == null) return false;
const existing = this.data?.existingMembers || [];
if (!existing.length) return false;
const editingId = this.data?.context === 'Edit' ? this.data?.member?.id : undefined;
const target = String(clientId);
return existing.some((m: any) => {
if (editingId && m.id === editingId) return false;
const linked = m.linkedClientId ?? m._linkedClient?.id;
return linked != null && String(linked) === target;
});
}
/** Select a client from search results and fetch full details */
selectClient(client: any): void {
if (this.isClientAlreadyLinked(client?.id)) {
this.linkedSelectError = `${client?.displayName || 'This client'} is already linked as a family member.`;
return;
}
this.linkedSelectError = '';
this.selectedClient = client;
this.searchResults = [];
this.searchText = '';
@@ -255,7 +255,8 @@ export class ClientFamilyMembersStepComponent implements OnInit {
}
this.nomineeSearching = true;
this.nomineeSearchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 10, '', '', true).subscribe({
// Nominee picker: a nominee must be a natural person, never a corporate entity.
this.clientsService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.nomineeSearchResults = data?.content || [];
this.nomineeSearching = false;
@@ -351,7 +352,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
}
this.guardianSearching = true;
this.guardianSearchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 10, '', '', true).subscribe({
// Guardian picker: a guardian is by definition a natural person responsible for a minor /
// dependent — non-individuals can never play that role.
this.clientsService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.guardianSearchResults = data?.content || [];
this.guardianSearching = false;
@@ -987,7 +987,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.guardianSearching = true;
this.guardianSearchTimer = setTimeout(() => {
this.clientService.searchByText(text, 0, 10, '', '', true).subscribe({
// Guardian picker: only natural persons can be a guardian.
this.clientService.searchByText(text, 0, 10, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.guardianSearchResults = data?.content || [];
this.guardianSearching = false;
+1 -1
View File
@@ -150,7 +150,7 @@ const routes: Routes = [
{
path: 'edit',
component: EditFamilyMemberComponent,
data: { title: 'Nominee View', routeParamBreadcrumb: 'familyMemberId' },
data: { title: 'Family Member View', routeParamBreadcrumb: 'familyMemberId' },
resolve: {
clientTemplate: ClientTemplateResolver,
editFamilyMember: ClientFamilyMemberResolver
@@ -51,7 +51,14 @@ export class AddFamilyMemberComponent implements OnInit {
this.route.data.subscribe((data: { clientTemplate: any }) => {
this.addFamilyMemberTemplate = data.clientTemplate.familyMemberOptions;
});
this.clientId = this.route.parent.parent.snapshot.params['clientId'];
// Walk up the route tree until we find a clientId param. Hard-coding parent.parent breaks
// when Angular flattens componentless intermediate routes (family-members has no component),
// which makes the depth unstable — and produces /clients/undefined/familymembers URLs.
let r: ActivatedRoute | null = this.route;
while (r && !r.snapshot.params['clientId']) {
r = r.parent;
}
this.clientId = r?.snapshot.params['clientId'];
}
ngOnInit() {
@@ -14,6 +14,7 @@ import { MatDialog } from '@angular/material/dialog';
/** Custom Components */
import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
import { NomineeDialogComponent } from './nominee-dialog/nominee-dialog.component';
import { ClientFamilyMemberDialogComponent } from '../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
/** Custom Services */
import { ClientsService } from '../../clients.service';
@@ -75,7 +76,10 @@ export class FamilyMembersTabComponent {
this.route.data.subscribe((data: { clientFamilyMembers: any }) => {
this.clientFamilyMembers = data.clientFamilyMembers || [];
});
this.clientId = this.route.parent.snapshot.params['clientId'];
// Route hierarchy is :clientId → family-members (wrapper) → this component,
// 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'];
}
/** Family members (non-nominee entries) */
@@ -101,52 +105,78 @@ export class FamilyMembersTabComponent {
// ═══ Family Member Actions ═══
/**
* Strip UI-only helper fields (`_linkedClient`, `_marriageCertFile`, `id`)
* from the dialog payload so the backend receives a clean DTO. Mirrors the
* pattern used in client-family-members-step.editFamilyMember.
*/
private toApiPayload(formValue: any): any {
const { id, _linkedClient, _marriageCertFile, ...payload } = formValue || {};
return payload;
}
addFamilyMember() {
this.ensureTemplateLoaded().then(() => {
const dialogRef = this.dialog.open(NomineeDialogComponent, {
width: '700px',
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
width: '640px',
maxWidth: '95vw',
panelClass: 'fm-dialog-panel',
data: {
title: 'Add Family Member',
familyMemberOptions: this.familyMemberOptions,
isFamilyMember: true
context: 'Add',
options: this.familyMemberOptions,
existingMembers: this.familyMembers
}
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
this.clientsService.addFamilyMember(this.clientId, result).subscribe((res: any) => {
this.clientsService.getClientFamilyMember(this.clientId, res.resourceId).subscribe((member: any) => {
this.clientFamilyMembers.push(member);
});
});
}
dialogRef.afterClosed().subscribe((response: any) => {
if (!response?.member) return;
const payload = this.toApiPayload(response.member);
this.clientsService.addFamilyMember(this.clientId, payload).subscribe({
next: () => this.refreshFamilyMembers(),
error: (err) => console.error('[FamilyMembers] add failed', err)
});
});
});
}
editFamilyMember(member: any, index: number) {
const realIndex = this.clientFamilyMembers.indexOf(member);
editFamilyMember(member: any, _index: number) {
this.ensureTemplateLoaded().then(() => {
const dialogRef = this.dialog.open(NomineeDialogComponent, {
width: '700px',
const dialogRef = this.dialog.open(ClientFamilyMemberDialogComponent, {
width: '640px',
maxWidth: '95vw',
panelClass: 'fm-dialog-panel',
data: {
title: 'Edit Family Member',
familyMemberOptions: this.familyMemberOptions,
context: 'Edit',
options: this.familyMemberOptions,
member,
isFamilyMember: true
existingMembers: this.familyMembers
}
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
this.clientsService.editFamilyMember(member.clientId, member.id, result).subscribe(() => {
this.clientsService.getClientFamilyMember(this.clientId, member.id).subscribe((updated: any) => {
this.clientFamilyMembers[realIndex] = updated;
});
});
}
dialogRef.afterClosed().subscribe((response: any) => {
if (!response?.member) return;
const payload = this.toApiPayload(response.member);
this.clientsService.editFamilyMember(member.clientId || this.clientId, member.id, payload).subscribe({
next: () => this.refreshFamilyMembers(),
error: (err) => console.error('[FamilyMembers] edit failed', err)
});
});
});
}
/**
* Re-pull the full family-members list from the backend after any
* mutation. More resilient than fetching a single resource by id —
* sidesteps cases where the POST response doesn't carry the new id in
* the expected `resourceId` field.
*/
private refreshFamilyMembers(): void {
this.clientsService.getClientFamilyMembers(this.clientId).subscribe({
next: (data: any) => {
this.clientFamilyMembers = Array.isArray(data) ? data : [];
},
error: (err) => console.error('[FamilyMembers] refresh failed', err)
});
}
// ═══ Nominee Actions ═══
addNominee() {
@@ -285,7 +285,7 @@
}
@if (selectedClient) {
<!-- Linked Card -->
<!-- Linked Client Card -->
<div class="nom-linked-card">
<div class="nom-linked-card__left">
@if (linkedProfileImage) {
@@ -295,15 +295,24 @@
(click)="zoomImage(linkedProfileImage, 'Photo')"
/>
} @else {
<mat-icon class="nom-linked-card__icon">person</mat-icon>
<div class="nom-linked-card__photo nom-linked-card__photo--placeholder">
<mat-icon>person</mat-icon>
</div>
}
</div>
<div class="nom-linked-card__info">
<span class="nom-linked-card__name">{{ selectedClient.displayName }}</span>
<span class="nom-linked-card__meta"
>ID: {{ selectedClient.id }}
<span class="nom-linked-card__meta">
<span class="nom-linked-card__meta-item"><mat-icon>badge</mat-icon>ID {{ selectedClient.id }}</span>
@if (selectedClient.dateOfBirth) {
<span class="nom-linked-card__meta-item"
><mat-icon>cake</mat-icon>{{ selectedClient.dateOfBirth | dateFormat }}</span
>
}
@if (selectedClient.mobileNo) {
· {{ selectedClient.mobileNo }}
<span class="nom-linked-card__meta-item">
<mat-icon>phone</mat-icon>{{ selectedClient.mobileNo }}
</span>
}
</span>
</div>
@@ -315,9 +324,18 @@
class="zoomable"
(click)="zoomImage(linkedSignatureImage, 'Signature')"
/>
<span class="nom-linked-card__sig-label">Signature</span>
</div>
}
<button mat-icon-button type="button" (click)="removeSelectedClient()"><mat-icon>close</mat-icon></button>
<button
mat-icon-button
type="button"
class="nom-linked-card__close"
matTooltip="Remove linked client"
(click)="removeSelectedClient()"
>
<mat-icon>close</mat-icon>
</button>
</div>
@if (fetchingDetails) {
@@ -327,34 +345,35 @@
}
@if (!fetchingDetails) {
<!-- KYC details -->
@if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
<!-- KYC chips -->
@if (selectedClient.panNumber || selectedClient.aadhaarMasked) {
<div class="nom-readonly-row">
@if (selectedClient.dateOfBirth) {
<span class="nom-ro-item"
><span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}</span
>
}
@if (selectedClient.panNumber) {
<span class="nom-ro-item"
><span class="nom-ro-label">PAN</span> {{ selectedClient.panNumber }}
<span class="nom-ro-item">
<span class="nom-ro-label">PAN</span>
<span class="nom-ro-value">{{ selectedClient.panNumber }}</span>
@if (linkedKycStatus?.panVerified) {
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
<mat-icon class="nom-ro-ok" matTooltip="PAN verified">check_circle</mat-icon>
}
</span>
}
@if (selectedClient.aadhaarMasked) {
<span class="nom-ro-item"
><span class="nom-ro-label">Aadhaar</span> {{ selectedClient.aadhaarMasked }}
<span class="nom-ro-item">
<span class="nom-ro-label">Aadhaar</span>
<span class="nom-ro-value">{{ selectedClient.aadhaarMasked }}</span>
@if (linkedKycStatus?.aadhaarVerified) {
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
<mat-icon class="nom-ro-ok" matTooltip="Aadhaar verified">check_circle</mat-icon>
}
</span>
}
</div>
}
<!-- Nominee fields inline -->
<!-- Section heading + Nominee fields -->
<div class="nom-section-heading">
<mat-icon>edit_note</mat-icon>
<span>Nomination Details</span>
</div>
<div class="nom-fields-row">
<mat-form-field appearance="outline" class="nom-field-item">
<mat-label>Nomination Type</mat-label>
@@ -520,12 +520,13 @@ $primary-light: #e8f0fe;
.nom-linked-card {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 8px;
gap: 14px;
padding: 12px 14px;
background: linear-gradient(135deg, #f0f7ff 0%, #fff 60%);
border: 1px solid #c7dbf2;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
&__left {
flex-shrink: 0;
@@ -536,44 +537,127 @@ $primary-light: #e8f0fe;
}
&__photo {
width: 36px;
height: 36px;
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #e2e8f0;
border: 2px solid #fff;
box-shadow: 0 0 0 1px #c7dbf2;
&--placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
background: #e0e7ff;
color: #4f46e5;
mat-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
}
}
&__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
&__name {
font-size: 13px;
font-weight: 600;
color: #1e293b;
font-size: 14px;
font-weight: 700;
color: #0f172a;
}
&__meta {
font-size: 11px;
color: #64748b;
font-family: 'Roboto Mono', monospace;
display: flex;
flex-wrap: wrap;
gap: 4px 12px;
font-size: 11.5px;
color: #475569;
}
&__meta-item {
display: inline-flex;
align-items: center;
gap: 3px;
mat-icon {
font-size: 13px;
width: 13px;
height: 13px;
color: #94a3b8;
}
}
&__sig {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
flex-shrink: 0;
padding: 4px 6px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 6px;
img {
height: 24px;
max-width: 60px;
height: 28px;
max-width: 72px;
object-fit: contain;
border: 1px solid #e2e8f0;
border-radius: 3px;
background: #fff;
padding: 1px 3px;
cursor: pointer;
}
}
&__sig-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #94a3b8;
}
&__close {
flex-shrink: 0;
width: 32px !important;
height: 32px !important;
line-height: 32px !important;
color: #64748b;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
&:hover {
color: #dc2626;
}
}
}
/* Section heading between linked card and nominee fields */
.nom-section-heading {
display: flex;
align-items: center;
gap: 6px;
margin: 6px 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #475569;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: #94a3b8;
}
}
.nom-fetching-hint {
@@ -585,32 +669,38 @@ $primary-light: #e8f0fe;
margin: -4px 0 8px;
}
/* Read-only KYC row */
/* Read-only KYC chips */
.nom-readonly-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 8px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 10px;
gap: 8px;
margin-bottom: 12px;
}
.nom-ro-item {
font-size: 12px;
color: #1e293b;
display: inline-flex;
align-items: center;
gap: 4px;
gap: 6px;
padding: 4px 10px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 999px;
font-size: 12px;
color: #1e293b;
}
.nom-ro-label {
font-size: 10px;
font-weight: 600;
color: #94a3b8;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
margin-right: 2px;
letter-spacing: 0.04em;
}
.nom-ro-value {
font-family: 'SF Mono', 'Roboto Mono', Consolas, monospace;
font-size: 11.5px;
font-weight: 600;
}
.nom-ro-ok {
@@ -654,25 +744,34 @@ $primary-light: #e8f0fe;
margin-top: 4px;
}
/* Guardian */
/* Guardian — amber sub-card to denote the minor-dependency */
.nom-guardian-section {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 8px;
padding: 10px 12px;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
margin-bottom: 10px;
}
.nom-guardian-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
gap: 5px;
font-size: 11.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #92400e;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: #d97706;
}
}
@@ -294,7 +294,8 @@ export class NomineeTabComponent implements OnInit {
}
this.searching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
// Nominee picker (post-create tab): nominees must be natural persons.
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.searchResults = data?.content || [];
this.searching = false;
@@ -412,7 +413,8 @@ export class NomineeTabComponent implements OnInit {
}
this.guardianSearching = true;
this.guardianSearchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({
// Guardian picker (post-create tab): guardians must be natural persons.
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.guardianSearchResults = data?.content || [];
this.guardianSearching = false;
+9 -1
View File
@@ -582,12 +582,20 @@ export class ClientsService {
pageSize: number,
sortAttribute: string = '',
sortDirection: string = '',
activeOnly: boolean = false
activeOnly: boolean = false,
legalFormType: string = ''
) {
const innerRequest: any = { text };
if (activeOnly) {
innerRequest.activeOnly = true;
}
// Restrict to one legal form when caller asks for it. Used by guardian/nominee/family-member
// pickers — non-individual entities (companies, partnerships) make no sense as a "guardian"
// or "spouse", so the SPA passes 'INDIVIDUAL' there. Backend treats this as authoritative;
// omitting the param still returns all forms (backwards-compatible for the general client list).
if (legalFormType) {
innerRequest.legalFormType = legalFormType;
}
let request: any = {
request: innerRequest,
page,