/** * 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/. */ /** Angular Imports */ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { FixedDepositsService } from '../../fixed-deposits.service'; import { ClientsService } from 'app/clients/clients.service'; import { MatTooltip } from '@angular/material/tooltip'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatIcon } from '@angular/material/icon'; import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; import { AlertService } from 'app/core/alert/alert.service'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; /** * Fixed Deposits Account Details Step */ @Component({ selector: 'mifosx-fixed-deposit-account-details-step', templateUrl: './fixed-deposit-account-details-step.component.html', styleUrls: ['./fixed-deposit-account-details-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, MatStepperPrevious, FaIconComponent, MatStepperNext, FormsModule, MatIcon, IntroducerSearchComponent ] }) export class FixedDepositAccountDetailsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private fixedDepositsService = inject(FixedDepositsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); private cdr = inject(ChangeDetectorRef); private clientsService = inject(ClientsService); private dialog = inject(MatDialog); private alertService = inject(AlertService); /** Fixed Deposits Account Template */ @Input() fixedDepositsAccountTemplate: any; @Input() hideButtons = false; /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); /** Maximum date allowed. */ maxDate = new Date(); /** Product Data */ productData: any; /** Field Officer Data */ fieldOfficerData: any; /** For edit fixed deposits form */ isFieldOfficerPatched = false; /** Fixed Deposits Account Details Form */ fixedDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; /** Fixed Deposits Account Template with product data */ @Output() fixedDepositsAccountProductTemplate = new EventEmitter(); /** Local copy of product template for displaying classification info */ productTemplate: any = null; accountTypeOptions: string[] = []; readonly fdTypeLabels: Record = { SINGLE: 'FD Single (Days < 1 Year)', DOUBLE: 'FD Double (≥ 1 Year)', SPECIAL: 'FD Special (Monthly Interest Payout)' }; getFdTypeLabel(fdType: string): string { return this.fdTypeLabels[fdType] || fdType; } customerSegmentOptions: string[] = []; selectedAccountType: string | null = null; selectedCustomerSegment: string | null = null; // ── Client info (auto-fetched) ───────────────────────────────────────── clientDob: Date | null = null; clientAge: number | null = null; clientGender: string | null = null; primaryClientName = ''; primaryClientCif = ''; primaryClientId: number | null = null; primaryHolderShare = 0; // ── Status of Account (RBI) ──────────────────────────────────────────── readonly accountStatusOptions = [ { value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' }, { value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' }, { value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' }, { value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' }, { value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' }, { value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' } ]; selectedAccountStatus = ''; // ── Joint Account ────────────────────────────────────────────────────── jointHolders: Array<{ clientId: number; clientName: string; clientCif: string; sharePercentage: number; isMandatory: boolean; signingAuthority?: string; role?: string; }> = []; modeOfOperation = ''; readonly modeOfOperationOptions = [ { value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' }, { value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' }, { value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' }, { value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' } ]; // ── Individual Male — Special Category ──────────────────────────────── selectedSpecialCategory = ''; readonly specialCategoryOptions = [ { value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' }, { value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' }, { value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' }, { value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' }, { value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' }, { value: 'NONE', label: 'None / General', icon: 'person' } ]; // ── Individual Female — Weaker Section ──────────────────────────────── selectedWeakerSection = ''; readonly weakerSectionOptions = [ { value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' }, { value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' }, { value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' }, { value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' }, { value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' }, { value: 'NONE', label: 'None / General', icon: 'person_outline' } ]; // ── Minor Account ────────────────────────────────────────────────────── guardianClientId: number | null = null; guardianName = ''; guardianCif = ''; guardianRelationship = ''; minorDob: Date | null = null; readonly guardianRelationshipOptions = [ 'Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian' ]; // ── Deposit Account Link ─────────────────────────────────────────────── dalSelectedType: 'SAVINGS' | 'CURRENT' | null = null; dalAllSavings: any[] = []; dalAllCurrent: any[] = []; dalSearch = ''; dalLoading = false; selectedLinkedAccount: any = null; currencyFlags: any = null; get currencyName(): string { return this.productTemplate?.currency?.name || ''; } get decimalPlaces(): number { return this.productTemplate?.currency?.decimalPlaces ?? 0; } get inMultiplesOf(): number { return this.productTemplate?.currency?.inMultiplesOf ?? 0; } get dalAccounts(): any[] { return this.dalSelectedType === 'SAVINGS' ? this.dalAllSavings : this.dalAllCurrent; } get dalFilteredAccounts(): any[] { const q = this.dalSearch.toLowerCase().trim(); if (!q) return this.dalAccounts; return this.dalAccounts.filter( (a) => a.accountNo.toLowerCase().includes(q) || a.productName.toLowerCase().includes(q) ); } selectDalType(type: 'SAVINGS' | 'CURRENT'): void { this.dalSelectedType = type; this.dalSearch = ''; this.selectedLinkedAccount = null; } selectLinkedAccount(account: any): void { if (account.status !== 'Active') return; this.selectedLinkedAccount = account; } clearLinkedAccount(): void { this.selectedLinkedAccount = null; this.dalSelectedType = null; this.dalSearch = ''; } private loadClientAccounts(): void { const clientId = String(this.fixedDepositsAccountTemplate?.clientId); if (!clientId) return; this.dalLoading = true; this.clientsService.getClientAccountData(clientId).subscribe({ next: (data: any) => { this.dalAllSavings = (data?.savingsAccounts ?? []) .filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit') .map((a: any) => ({ id: a.id, accountNo: a.accountNo, productName: a.productName ?? '', accountType: 'SAVINGS' as const, status: a.status?.value ?? '', balance: a.accountBalance ?? 0, currencyCode: a.currency?.code ?? '' })); this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ id: a.id, accountNo: a.accountNo, productName: a.productName ?? '', accountType: 'CURRENT' as const, status: a.status?.value ?? '', balance: a.accountBalance ?? 0, currencyCode: a.currency?.code ?? '' })); this.dalLoading = false; this.cdr.detectChanges(); }, error: () => { this.dalLoading = false; } }); } constructor() { this.createFixedDepositsAccountDetailsForm(); } ngOnInit() { this.maxDate = this.settingsService.businessDate; this.buildDependencies(); if (this.fixedDepositsAccountTemplate) { this.productData = this.fixedDepositsAccountTemplate.productOptions; if (this.fixedDepositsAccountTemplate.depositProductId) { this.fixedDepositAccountDetailsForm.patchValue({ productId: this.fixedDepositsAccountTemplate.depositProductId, submittedOnDate: this.fixedDepositsAccountTemplate.timeline?.submittedOnDate && new Date(this.fixedDepositsAccountTemplate.timeline.submittedOnDate), externalId: this.fixedDepositsAccountTemplate.externalId }); } else { this.fixedDepositAccountDetailsForm.patchValue({ submittedOnDate: this.settingsService.businessDate }); } } // Auto-detect account status from client DOB + capture primary holder info const clientId = this.fixedDepositsAccountTemplate?.clientId || this.fixedDepositsAccountTemplate?.id; if (clientId) { this.primaryClientId = clientId; this.clientsService.getClientData(String(clientId)).subscribe((client: any) => { this.primaryClientName = client?.displayName ?? client?.fullname ?? ''; this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? ''; if (client?.dateOfBirth) { const dob = this.parseClientDob(client.dateOfBirth); if (dob) { this.clientDob = dob; this.clientAge = this.calculateAge(dob); this.clientGender = client.gender?.name?.toLowerCase() ?? null; this.autoDetectAccountStatus(); } } this.cdr.detectChanges(); }); } } /** Parse DOB from backend — handles array [yyyy, mm, dd] or string */ private parseClientDob(dob: any): Date | null { if (!dob) return null; if (Array.isArray(dob) && dob.length >= 3) { return new Date(dob[0], dob[1] - 1, dob[2]); } const d = new Date(dob); return isNaN(d.getTime()) ? null : d; } /** Calculate full years from a date to today */ private calculateAge(dob: Date): number { const today = this.settingsService.businessDate ?? new Date(); let age = today.getFullYear() - dob.getFullYear(); const monthDiff = today.getMonth() - dob.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) { age--; } return age; } isTileDisabled(value: string): boolean { if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; return false; } /** Handle Account Status selection */ selectAccountStatus(status: string): void { if (!this.isTileDisabled(status)) { this.selectedAccountStatus = status; this.cdr.detectChanges(); } } /** Handle Special Category selection */ selectSpecialCategory(category: string): void { this.selectedSpecialCategory = category; this.cdr.detectChanges(); } /** Handle Weaker Section selection */ selectWeakerSection(section: string): void { this.selectedWeakerSection = section; this.cdr.detectChanges(); } getTileTooltip(value: string): string { if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) { return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`; } if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) { return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`; } if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age'; if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age'; return ''; } private autoDetectAccountStatus(): void { if (this.clientAge === null) return; if (this.clientAge < 18) { this.selectedAccountStatus = 'MINOR'; this.minorDob = this.clientDob; } else if (this.clientAge >= 60) { this.selectedAccountStatus = 'SENIOR_CITIZEN'; } else if (this.clientGender === 'female') { this.selectedAccountStatus = 'INDIVIDUAL_FEMALE'; } else { this.selectedAccountStatus = 'INDIVIDUAL_MALE'; } } /** * Creates fixed deposits account details form. */ createFixedDepositsAccountDetailsForm() { this.fixedDepositAccountDetailsForm = this.formBuilder.group({ productId: [ '', Validators.required ], submittedOnDate: [ '', Validators.required ], fieldOfficerId: [''], externalId: [''] }); } /** * Fetches fixed deposits account product template on productId value changes */ buildDependencies() { if (!this.fixedDepositsAccountTemplate) return; const clientId = this.fixedDepositsAccountTemplate.clientId || this.fixedDepositsAccountTemplate.id; this.fixedDepositAccountDetailsForm.get('productId')!.valueChanges.subscribe((productId: string) => { this.fixedDepositsService.getFixedDepositsAccountTemplate(clientId, productId).subscribe((response: any) => { if (!response.currency) { const selectedProduct = this.fixedDepositsAccountTemplate.productOptions?.find( (product: any) => product.id == productId ); if (selectedProduct?.currency) { response.currency = selectedProduct.currency; } else if (this.fixedDepositsAccountTemplate.currency) { response.currency = this.fixedDepositsAccountTemplate.currency; } } this.productTemplate = response; this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType); this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment); this.selectedAccountType = response.accountType ?? null; this.selectedCustomerSegment = response.customerSegment ?? null; this.fixedDepositsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.isProductSelected = true; if (response.shareLinkingEnabled) { this.loadClientAccounts(); } this.cdr.detectChanges(); if (!this.isFieldOfficerPatched && this.fixedDepositsAccountTemplate.fieldOfficerId) { this.fixedDepositAccountDetailsForm .get('fieldOfficerId')! .patchValue(this.fixedDepositsAccountTemplate.fieldOfficerId); this.isFieldOfficerPatched = true; } else { this.fixedDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue(''); } }); }); } hasValue(val: any): boolean { return val !== null && val !== undefined && val !== ''; } private extractOptions(options: any[], fallback?: string): string[] { if (Array.isArray(options) && options.length > 0) { return options.map((o: any) => o.value ?? o.name ?? o); } return fallback ? [fallback] : []; } formatTemplateDate(value: any): string { if (!value) return ''; let dateValue: Date | null = null; if (value instanceof Date) { dateValue = value; } else if (Array.isArray(value) && value.length >= 3) { dateValue = new Date(value[0], value[1] - 1, value[2]); } else { dateValue = new Date(value); } if (!dateValue || Number.isNaN(dateValue.getTime())) { return Array.isArray(value) ? value.join('/') : String(value); } return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || ''; } onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { this.guardianClientId = event?.clientId ?? null; this.guardianName = event?.displayName ?? ''; this.guardianCif = event?.cifNumber ?? ''; this.cdr.detectChanges(); } // ── Joint Account helpers ────────────────────────────────────────────── openAddJointHolderDialog(): void { const excludedClientIds = [ ...(this.primaryClientId ? [this.primaryClientId] : []), ...this.jointHolders.map((h: any) => h.clientId).filter(Boolean) ]; const maxSharePercentage = Math.max(0, 100 - this.totalJointSharePercentage); const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', panelClass: 'arc-dialog-panel', disableClose: true, data: { excludedClientIds, maxSharePercentage } }); ref.afterClosed().subscribe((result) => { if (result) { const isPrimary = this.primaryClientCif && this.primaryClientCif === result.clientCif; const duplicate = isPrimary || this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); if (duplicate) { this.alertService.alert({ type: 'Error', message: `Account holder with CIF ${result.clientCif} is already added.` }); return; } this.jointHolders = [ ...this.jointHolders, { ...result, isMandatory: false }]; this.autoSetModeOfOperation(); this.cdr.detectChanges(); } }); } removeJointHolder(index: number): void { this.jointHolders = this.jointHolders.filter((_, i) => i !== index); this.autoSetModeOfOperation(); this.cdr.detectChanges(); } toggleJointHolderMandatory(holder: any): void { holder.isMandatory = !holder.isMandatory; this.autoSetModeOfOperation(); this.cdr.detectChanges(); } autoSetModeOfOperation(): void { const mandatory = this.jointHolders.filter((h) => h.isMandatory).length; if (mandatory === 0) { this.modeOfOperation = 'EITHER_OR_SURVIVOR'; } else if (mandatory === 1) { this.modeOfOperation = 'FORMER_OR_SURVIVOR'; } else { this.modeOfOperation = 'JOINTLY'; } } get totalJointSharePercentage(): number { return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0); } get mandatoryJointCount(): number { return this.jointHolders.filter((h) => h.isMandatory).length; } get selectedModeLabel(): string { return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; } get majorityDate(): string { const dob = this.minorDob ?? this.clientDob; if (!dob) return ''; const d = new Date(dob); d.setFullYear(d.getFullYear() + 18); return this.dateUtils.formatDate(d, 'M/d/yyyy') || ''; } /** * Returns fixed deposits account details form value. */ get fixedDepositAccountDetails() { const value = this.fixedDepositAccountDetailsForm.value; if (this.selectedAccountType) { value.accountType = this.selectedAccountType; } if (this.selectedCustomerSegment) { value.customerSegment = this.selectedCustomerSegment; } if (this.selectedAccountStatus) { value.accountStatus = this.selectedAccountStatus; } if (this.selectedAccountStatus === 'JOINT') { value.modeOfOperation = this.modeOfOperation || null; value.jointHolders = [ { clientId: this.primaryClientId, displayName: this.primaryClientName, cifNumber: this.primaryClientCif, sharePercentage: this.primaryHolderShare, isMandatory: true, isPrimary: true, signingAuthority: 'Mandatory', role: 'Primary Account Holder' }, ...this.jointHolders.map((h) => ({ clientId: h.clientId, displayName: h.clientName, cifNumber: h.clientCif, sharePercentage: h.sharePercentage, isMandatory: h.isMandatory, isPrimary: false, signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'), role: h.role || 'Co-Holder' })) ]; } if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) { value.specialCategory = this.selectedSpecialCategory; } if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) { value.weakerSection = this.selectedWeakerSection; } if (this.selectedAccountStatus === 'MINOR') { value.guardianClientId = this.guardianClientId; value.guardianName = this.guardianName; value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'dd MMMM yyyy') : null; } if (this.selectedLinkedAccount) { value.linkedAccountId = this.selectedLinkedAccount.id; value.linkedAccountType = this.selectedLinkedAccount.accountType; value.linkedAccountNo = this.selectedLinkedAccount.accountNo; } return value; } }