resolve merge conflicts: keep incoming .env version, incoming nominee HTML, HEAD guardian SCSS

This commit is contained in:
Nitesh Yadav
2026-05-15 13:17:44 +05:30
35 changed files with 4394 additions and 191 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;