/** * 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 { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { ClientsService } from '../../clients.service'; import { SettingsService } from 'app/settings/settings.service'; import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component'; import { ImageZoomDialogComponent } from '../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatIcon } from '@angular/material/icon'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatTooltip } from '@angular/material/tooltip'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { DateFormatPipe } from '../../../pipes/date-format.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { FormsModule } from '@angular/forms'; interface AccountGroup { type: string; apiType: string; // SB, CB, FD, RD, LA label: string; icon: string; accounts: any[]; } @Component({ selector: 'mifosx-nominee-tab', templateUrl: './nominee-tab.component.html', styleUrls: ['./nominee-tab.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FormsModule, FaIconComponent, MatIcon, MatProgressSpinner, MatTooltip, MatSlideToggle, DateFormatPipe ] }) export class NomineeTabComponent implements OnInit { private route = inject(ActivatedRoute); private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); private dialog = inject(MatDialog); private sanitizer = inject(DomSanitizer); clientId: string; loading = true; accountGroups: AccountGroup[] = []; selectedType: string = ''; selectedAccountId: number | null = null; /** Nominees for selected account */ nominees: any[] = []; nomineesLoading = false; /** Add nominee form */ showAddForm = false; searchText = ''; searchResults: any[] = []; searching = false; private searchTimer: any = null; selectedClient: any = null; fetchingDetails = false; /** Nominee form fields */ nomineeRelationshipId: number | null = null; nomineeShare: number | null = null; nomineeIsMinor = false; nomineeSaving = false; nomineeRemarks = ''; /** Guardian */ guardianSearchText = ''; guardianSearchResults: any[] = []; guardianSearching = false; selectedGuardian: any = null; private guardianSearchTimer: any = null; /** Linked client details */ linkedProfileImage: SafeUrl | null = null; linkedSignatureImage: SafeUrl | null = null; linkedKycStatus: any = null; /** Template data from API */ relationshipOptions: any[] = []; /** Nomination type options */ nominationTypeOptions = [ { value: 'DA1', label: 'DA1 - Add Nominee' }, { value: 'DA5', label: 'DA5 - Modify / Cancel Nominee' } ]; /** Add form nomination type */ nomineeNomType = 'DA1'; /** Editing state */ editingIndex: number | null = null; editRelId: number | null = null; editShare: number | null = null; editNomType: string = 'DA1'; get hasAccounts(): boolean { return this.accountGroups.some((g) => g.accounts.length > 0); } get selectedGroup(): AccountGroup | undefined { return this.accountGroups.find((g) => g.type === this.selectedType); } get selectedAccount(): any { return this.selectedGroup?.accounts.find((a: any) => a.id === this.selectedAccountId); } get totalShare(): number { return this.nominees.reduce((sum, n) => sum + (n.percentageShare || 0), 0); } get maxAllowedShare(): number { return 100 - this.totalShare; } ngOnInit(): void { this.clientId = this.route.parent.snapshot.params['clientId']; this.loadAccounts(); this.loadTemplate(); } private loadTemplate(): void { this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({ next: (template: any) => { this.relationshipOptions = template?.relationshipOptions || []; }, error: () => { // Fallback to family member template this.clientsService.getClientTemplate().subscribe((t: any) => { this.relationshipOptions = t?.familyMemberOptions?.relationshipIdOptions || []; }); } }); } private loadAccounts(): void { this.loading = true; this.clientsService.getClientAccountData(this.clientId).subscribe({ next: (data: any) => { this.loading = false; this.accountGroups = [ { type: 'savings', apiType: 'SB', label: 'Savings Account', icon: 'savings', accounts: this.filterActive(data.savingsAccounts) }, { type: 'current', apiType: 'CB', label: 'Current Account', icon: 'account_balance', accounts: this.filterActive(data.currentAccounts) }, { type: 'fd', apiType: 'FD', label: 'Fixed Deposit', icon: 'lock', accounts: this.filterActive(data.fixedDepositAccounts) }, { type: 'rd', apiType: 'RD', label: 'Recurring Deposit', icon: 'autorenew', accounts: this.filterActive(data.recurringDepositAccounts) }, { type: 'loan', apiType: 'LA', label: 'Loan Account', icon: 'credit_score', accounts: this.filterActive(data.loanAccounts) } ]; const firstWithAccounts = this.accountGroups.find((g) => g.accounts.length > 0); if (firstWithAccounts) { this.selectedType = firstWithAccounts.type; // Auto-select if only 1 account in the group if (firstWithAccounts.accounts.length === 1) { this.selectedAccountId = firstWithAccounts.accounts[0].id; this.loadNominees(); } } }, error: () => { this.loading = false; } }); } private filterActive(accounts: any[]): any[] { if (!accounts) return []; // Include all non-closed accounts (active, disbursed, pending, etc.) return accounts.filter((a: any) => a.status?.active || a.status?.value === 'Active' || !a.status?.closed); } onTypeChange(): void { this.selectedAccountId = null; this.nominees = []; this.resetAddForm(); // Auto-select if only 1 account if (this.selectedGroup?.accounts.length === 1) { this.selectedAccountId = this.selectedGroup.accounts[0].id; this.loadNominees(); } } onAccountChange(): void { this.nominees = []; this.resetAddForm(); if (this.selectedAccountId) { this.loadNominees(); } } private loadNominees(): void { this.nomineesLoading = true; const account = this.selectedAccount; const accountType = this.selectedGroup?.apiType || ''; this.clientsService.getClientNominees(this.clientId, account?.accountNo, accountType).subscribe({ next: (data: any) => { this.nomineesLoading = false; this.nominees = Array.isArray(data) ? data : []; // Enrich each nominee with profile image & signature this.nominees.forEach((n) => this.enrichNomineeImages(n)); }, error: () => { this.nomineesLoading = false; this.nominees = []; } }); } /** Fetch profile image & signature for a nominee card */ private enrichNomineeImages(nominee: any): void { const cid = nominee.nomineeClientId?.toString(); if (!cid) return; // Profile image this.clientsService.getClientProfileImage(cid).subscribe({ next: (img: any) => { if (img) { nominee.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img); } }, error: () => {} }); // Signature this.clientsService.getClientDocuments(cid).subscribe({ next: (docs: any[]) => { const sigDoc = docs?.find( (d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature') ); if (sigDoc) { this.clientsService.getClientSignatureImage(cid, sigDoc.id.toString()).subscribe({ next: (blob: Blob) => { nominee.signatureUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); }, error: () => {} }); } }, error: () => {} }); } // ═══ Search & Link ═══ onSearch(text: string): void { this.searchText = text; clearTimeout(this.searchTimer); if (!text || text.length < 2) { this.searchResults = []; return; } this.searching = true; this.searchTimer = setTimeout(() => { this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ next: (data: any) => { this.searchResults = data?.content || []; this.searching = false; this.clientsService.enrichSearchResults(this.searchResults); }, error: () => { this.searchResults = []; this.searching = false; } }); }, 400); } dismissSearch(): void { setTimeout(() => { this.searchResults = []; this.searching = false; }, 200); } selectClient(client: any): void { this.selectedClient = client; this.searchResults = []; this.searchText = ''; this.fetchClientDetails(client.id); } removeSelectedClient(): void { this.selectedClient = null; this.linkedProfileImage = null; this.linkedSignatureImage = null; this.linkedKycStatus = null; } private fetchClientDetails(clientId: number): void { this.fetchingDetails = true; this.linkedProfileImage = null; this.linkedSignatureImage = null; this.linkedKycStatus = null; let pending = 4; const done = () => { pending--; if (pending <= 0) { this.fetchingDetails = false; } }; this.clientsService.getClientData(clientId.toString()).subscribe({ next: (data: any) => { if (data?.dateOfBirth) { this.selectedClient = { ...this.selectedClient, dateOfBirth: data.dateOfBirth }; } done(); }, error: () => done() }); this.clientsService.getClientKyc(clientId.toString()).subscribe({ next: (kyc: any) => { if (kyc) { this.selectedClient = { ...this.selectedClient, panNumber: kyc.panNumber, aadhaarMasked: kyc.aadhaarNumberMasked }; this.linkedKycStatus = { panVerified: kyc.panVerified || false, aadhaarVerified: kyc.aadhaarVerified || false }; } done(); }, error: () => done() }); this.clientsService.getClientProfileImage(clientId.toString()).subscribe({ next: (img: any) => { if (img) { this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img); } done(); }, error: () => done() }); this.clientsService.getClientDocuments(clientId.toString()).subscribe({ next: (docs: any[]) => { const sigDoc = docs?.find( (d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature') ); if (sigDoc) { this.clientsService.getClientSignatureImage(clientId.toString(), sigDoc.id.toString()).subscribe({ next: (blob: Blob) => { this.linkedSignatureImage = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); done(); }, error: () => done() }); } else { done(); } }, error: () => done() }); } // ═══ Guardian ═══ onGuardianSearch(text: string): void { this.guardianSearchText = text; clearTimeout(this.guardianSearchTimer); if (!text || text.length < 2) { this.guardianSearchResults = []; return; } this.guardianSearching = true; this.guardianSearchTimer = setTimeout(() => { this.clientsService.searchByText(text, 0, 8, '', '', true).subscribe({ next: (data: any) => { this.guardianSearchResults = data?.content || []; this.guardianSearching = false; this.clientsService.enrichSearchResults(this.guardianSearchResults); }, error: () => { this.guardianSearchResults = []; this.guardianSearching = false; } }); }, 400); } selectGuardian(client: any): void { this.selectedGuardian = client; this.guardianSearchResults = []; this.guardianSearchText = ''; } // ═══ Save Nominee ═══ saveNominee(): void { if (!this.selectedClient || !this.nomineeRelationshipId || !this.nomineeShare) return; if (this.totalShare + this.nomineeShare > 100) return; this.nomineeSaving = true; const account = this.selectedAccount; const accountType = this.selectedGroup?.apiType || ''; const payload: any = { nomineeClientId: this.selectedClient.id, accountNo: account?.accountNo, accountType, nominationType: this.nomineeNomType, relationshipId: this.nomineeRelationshipId, percentageShare: this.nomineeShare, nomineeIsMinor: this.nomineeIsMinor, remarks: this.nomineeRemarks || null, locale: 'en' }; if (this.nomineeIsMinor && this.selectedGuardian) { payload.guardianClientId = this.selectedGuardian.id; } this.clientsService.addNominee(this.clientId, payload).subscribe({ next: (res: any) => { this.nomineeSaving = false; // Reload nominees to get the full response including nomineeIdentifier this.loadNominees(); this.resetAddForm(); }, error: () => { this.nomineeSaving = false; } }); } // ═══ View & Edit ═══ viewNominee(nominee: any): void { // Resolve the linked client ID — could be nomineeClientId or nested const clientId = nominee.nomineeClientId || nominee.linkedClientId || nominee.clientId; const displayName = nominee.nomineeDisplayName || nominee.firstName || ''; import('../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component').then( (m) => { this.dialog.open(m.ClientFamilyMemberDialogComponent, { data: { context: 'View', member: { firstName: displayName, relationshipId: nominee.relationshipId, dateOfBirth: nominee.nomineeDateOfBirth || nominee.dateOfBirth, linkedClientId: clientId, linkedClientName: displayName, isAlive: true, isDependent: nominee.nomineeIsMinor, panNumber: nominee.panNumber || '', aadhaarNumber: nominee.aadhaarMasked || '' }, options: { relationshipIdOptions: this.relationshipOptions } }, width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel' }); } ); } editNominee(nominee: any, index: number): void { this.editingIndex = index; this.editRelId = nominee.relationshipId || null; this.editShare = nominee.percentageShare; this.editNomType = nominee.nominationType || 'DA1'; } cancelEdit(): void { this.editingIndex = null; } maxShareForEdit(index: number): number { return 100 - this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0); } saveEdit(nominee: any, index: number): void { if (!this.editRelId || !this.editShare || this.editShare > this.maxShareForEdit(index)) return; const payload: any = { relationshipId: this.editRelId, percentageShare: this.editShare, nominationType: this.editNomType, locale: 'en' }; this.clientsService.updateNominee(this.clientId, nominee.id.toString(), payload).subscribe({ next: () => { // Reload to get updated data this.loadNominees(); this.editingIndex = null; }, error: () => {} }); } deleteNominee(nominee: any, index: number): void { const ref = this.dialog.open(DeleteDialogComponent, { data: { deleteContext: `nominee ${nominee.nomineeDisplayName || nominee.firstName || ''}` } }); ref.afterClosed().subscribe((response: any) => { if (response?.delete) { this.clientsService.deleteNominee(this.clientId, nominee.id.toString()).subscribe({ next: () => this.nominees.splice(index, 1), error: () => this.nominees.splice(index, 1) }); } }); } zoomImage(imageSrc: any, title: string): void { this.dialog.open(ImageZoomDialogComponent, { data: { imageSrc, title }, panelClass: 'image-zoom-panel', maxWidth: '90vw', maxHeight: '90vh' }); } getRelationship(member: any): string { if (member.relationshipName) return member.relationshipName; if (member.relationship) return member.relationship; if (typeof member.relationshipId === 'object' && member.relationshipId?.name) return member.relationshipId.name; const opt = this.relationshipOptions.find((r: any) => r.id === member.relationshipId); return opt?.name || ''; } resetAddForm(): void { this.showAddForm = false; this.selectedClient = null; this.linkedProfileImage = null; this.linkedSignatureImage = null; this.linkedKycStatus = null; this.nomineeNomType = 'DA1'; this.nomineeRelationshipId = null; this.nomineeShare = null; this.nomineeIsMinor = false; this.nomineeRemarks = ''; this.selectedGuardian = null; this.searchText = ''; this.guardianSearchText = ''; } }