resolve merge conflicts: keep incoming .env version, incoming nominee HTML, HEAD guardian SCSS
This commit is contained in:
+25
-3
@@ -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') {
|
||||
|
||||
+54
@@ -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;
|
||||
|
||||
+36
-1
@@ -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 = '';
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-1
@@ -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,37 +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 }}
|
||||
@if (nomineeIsMinor && minorToggleLocked) {
|
||||
<span class="nom-minor-badge">Minor</span>
|
||||
}
|
||||
</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 {
|
||||
|
||||
@@ -317,7 +317,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;
|
||||
@@ -455,7 +456,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user