- Add 'Modify Application' button in kebab menu for rejected accounts (all 4 types)
- Rewrite edit components with tab-based UI matching create form
- Pre-populate all create-time values in edit form: product, account status,
customer segment, account type, joint holders, nominees, introducer,
deposit account link, senior citizen rate, tenure, flags (msme/balanceSwap/autoDebit)
- Fix edit mode detection using accountNo presence
- Add preselectedIntroducer @Input to IntroducerSearchComponent for pre-fill
- Support both camelCase and lowercase keys for joint holders (MySQL queryForList)
- Fix account number display: actual accountNo before previewAccountNo
- Fix cbsAccountType restore: cbsAccountType only, avoid Fineract enum object
- Add nominees fetch via /clients/{id}/nominees API in all 4 edit components
- Fix RD/FD Interest Rate Chart: SimpleChanges to prevent reset race condition
- Pre-check Senior Citizen checkbox in edit form from isSeniorCitizen DB field
- Auto-calculate rate when tenure pre-filled in edit mode
- FD calculator: use backend calculate-fd-interest API for all compounding types
- Support interestCompoundingPeriodInMonths=0 (daily) in FD calculator API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
739 lines
28 KiB
TypeScript
739 lines
28 KiB
TypeScript
/**
|
|
* 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 { RecurringDepositsService } from '../../recurring-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';
|
|
|
|
/**
|
|
* Recurring Deposits Account Details Step
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-recurring-deposits-account-details-step',
|
|
templateUrl: './recurring-deposits-account-details-step.component.html',
|
|
styleUrls: ['./recurring-deposits-account-details-step.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatTooltip,
|
|
MatStepperPrevious,
|
|
FaIconComponent,
|
|
MatStepperNext,
|
|
FormsModule,
|
|
MatIcon,
|
|
IntroducerSearchComponent
|
|
]
|
|
})
|
|
export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
|
private formBuilder = inject(UntypedFormBuilder);
|
|
private recurringDepositsService = inject(RecurringDepositsService);
|
|
private settingsService = inject(SettingsService);
|
|
private dateUtils = inject(Dates);
|
|
private cdr = inject(ChangeDetectorRef);
|
|
private clientsService = inject(ClientsService);
|
|
private dialog = inject(MatDialog);
|
|
private alertService = inject(AlertService);
|
|
|
|
/** Recurring Deposits Account Template */
|
|
@Input() recurringDepositsAccountTemplate: any;
|
|
|
|
/** 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 recurring deposits form */
|
|
isFieldOfficerPatched = false;
|
|
/** Recurring Deposits Account Details Form */
|
|
recurringDepositAccountDetailsForm!: UntypedFormGroup;
|
|
|
|
isProductSelected = false;
|
|
private isEditMode = false;
|
|
|
|
/** Recurring Deposits Account Template with product data */
|
|
@Output() recurringDepositsAccountProductTemplate = new EventEmitter();
|
|
|
|
/** Local copy of product template for displaying classification info */
|
|
productTemplate: any = null;
|
|
accountTypeOptions: string[] = [];
|
|
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 = 50;
|
|
|
|
// ── 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;
|
|
|
|
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.recurringDepositsAccountTemplate?.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;
|
|
// Pre-select linked account in edit mode
|
|
const linkedId =
|
|
this.recurringDepositsAccountTemplate?.linkedAccount?.id ??
|
|
this.recurringDepositsAccountTemplate?.linkedAccountId;
|
|
if (linkedId) {
|
|
const savings = this.dalAllSavings.find((a: any) => a.id === linkedId);
|
|
const current = this.dalAllCurrent.find((a: any) => a.id === linkedId);
|
|
if (savings) {
|
|
this.dalSelectedType = 'SAVINGS';
|
|
this.selectedLinkedAccount = savings;
|
|
} else if (current) {
|
|
this.dalSelectedType = 'CURRENT';
|
|
this.selectedLinkedAccount = current;
|
|
}
|
|
}
|
|
this.cdr.detectChanges();
|
|
},
|
|
error: () => {
|
|
this.dalLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
constructor() {
|
|
this.createRecurringDepositsAccountDetailsForm();
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.maxDate = this.settingsService.businessDate;
|
|
this.buildDependencies();
|
|
if (this.recurringDepositsAccountTemplate) {
|
|
this.productData = this.recurringDepositsAccountTemplate.productOptions;
|
|
const existingProductId =
|
|
this.recurringDepositsAccountTemplate.depositProductId ?? this.recurringDepositsAccountTemplate.productId;
|
|
// Edit mode: account response has accountNo; pure template has previewAccountNo but no accountNo
|
|
if (this.recurringDepositsAccountTemplate.accountNo) {
|
|
this.isEditMode = true;
|
|
}
|
|
if ((!this.productData || this.productData.length === 0) && existingProductId) {
|
|
this.isEditMode = true;
|
|
this.productData = [
|
|
{
|
|
id: existingProductId,
|
|
name:
|
|
this.recurringDepositsAccountTemplate.productName ??
|
|
this.recurringDepositsAccountTemplate.depositProductName ??
|
|
'Product'
|
|
}
|
|
];
|
|
}
|
|
if (existingProductId) {
|
|
this.recurringDepositAccountDetailsForm.patchValue({
|
|
productId: existingProductId,
|
|
submittedOnDate:
|
|
this.recurringDepositsAccountTemplate.timeline?.submittedOnDate &&
|
|
new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate),
|
|
externalId: this.recurringDepositsAccountTemplate.externalId
|
|
});
|
|
} else {
|
|
this.recurringDepositAccountDetailsForm.patchValue({
|
|
submittedOnDate: this.settingsService.businessDate
|
|
});
|
|
}
|
|
}
|
|
|
|
// Auto-detect account status from client DOB + capture primary holder info
|
|
const clientId = this.recurringDepositsAccountTemplate?.clientId;
|
|
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;
|
|
}
|
|
|
|
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.isEditMode) {
|
|
return;
|
|
}
|
|
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 recurring deposits account details form.
|
|
*/
|
|
createRecurringDepositsAccountDetailsForm() {
|
|
this.recurringDepositAccountDetailsForm = this.formBuilder.group({
|
|
productId: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
submittedOnDate: [
|
|
'',
|
|
Validators.required
|
|
],
|
|
fieldOfficerId: [''],
|
|
externalId: [''],
|
|
autoDebitFacility: [false]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches recurring deposits account product template on productId value changes
|
|
*/
|
|
buildDependencies() {
|
|
const clientId = this.recurringDepositsAccountTemplate.clientId;
|
|
this.recurringDepositAccountDetailsForm.get('productId')!.valueChanges.subscribe((productId: string) => {
|
|
this.recurringDepositsService
|
|
.getRecurringDepositsAccountTemplate(clientId, productId)
|
|
.subscribe((response: any) => {
|
|
if (!response.currency) {
|
|
const selectedProduct = this.recurringDepositsAccountTemplate.productOptions?.find(
|
|
(product: any) => product.id == productId
|
|
);
|
|
if (selectedProduct?.currency) {
|
|
response.currency = selectedProduct.currency;
|
|
} else if (this.recurringDepositsAccountTemplate.currency) {
|
|
response.currency = this.recurringDepositsAccountTemplate.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.recurringDepositsAccountProductTemplate.emit(response);
|
|
this.fieldOfficerData = response.fieldOfficerOptions;
|
|
this.isProductSelected = true;
|
|
if (response.shareLinkingEnabled) {
|
|
this.loadClientAccounts();
|
|
}
|
|
this.recurringDepositAccountDetailsForm.patchValue({
|
|
autoDebitFacility: !!response.autoDebitFacility
|
|
});
|
|
this.cdr.detectChanges();
|
|
if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) {
|
|
this.recurringDepositAccountDetailsForm
|
|
.get('fieldOfficerId')!
|
|
.patchValue(this.recurringDepositsAccountTemplate.fieldOfficerId);
|
|
this.isFieldOfficerPatched = true;
|
|
} else {
|
|
this.recurringDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue('');
|
|
}
|
|
// === Restore existing account values when in edit/modify mode ===
|
|
this.restoreExistingAccountValues();
|
|
});
|
|
});
|
|
}
|
|
|
|
private restoreExistingAccountValues(): void {
|
|
const ea = this.recurringDepositsAccountTemplate;
|
|
if (!ea?.depositProductId && !ea?.productId) {
|
|
return;
|
|
}
|
|
|
|
const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? ''));
|
|
|
|
const accountStatus = strVal(ea.accountStatus);
|
|
if (accountStatus) {
|
|
this.selectedAccountStatus = accountStatus;
|
|
}
|
|
|
|
const customerSegment = strVal(ea.customerSegment);
|
|
if (customerSegment) {
|
|
this.selectedCustomerSegment = customerSegment;
|
|
}
|
|
|
|
// RD API returns cbs_account_type aliased as "accountType" (string); Current/Savings return it as "cbsAccountType"
|
|
const rawType = ea.cbsAccountType ?? (typeof ea.accountType === 'string' ? ea.accountType : null);
|
|
const accountType = rawType ? strVal(rawType) : null;
|
|
if (accountType) {
|
|
this.selectedAccountType = accountType;
|
|
}
|
|
|
|
if (accountStatus === 'JOINT') {
|
|
const modeOfOp = strVal(ea.modeOfOperation);
|
|
if (modeOfOp) {
|
|
this.modeOfOperation = modeOfOp;
|
|
}
|
|
if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) {
|
|
const isPrimaryHolder = (h: any) => h.isPrimary ?? h.isprimary ?? false;
|
|
const primary = ea.jointHolders.find((h: any) => isPrimaryHolder(h));
|
|
const primaryShare = primary?.sharePercentage ?? primary?.sharepercentage;
|
|
if (primaryShare) {
|
|
this.primaryHolderShare = +primaryShare;
|
|
}
|
|
this.jointHolders = ea.jointHolders
|
|
.filter((h: any) => !isPrimaryHolder(h))
|
|
.map((h: any) => ({
|
|
clientId: h.clientId ?? h.clientid,
|
|
clientName: h.displayName ?? h.displayname ?? h.clientName ?? '',
|
|
clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '',
|
|
sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0),
|
|
isMandatory: !!(h.isMandatory ?? h.ismandatory)
|
|
}));
|
|
}
|
|
}
|
|
if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) {
|
|
this.selectedSpecialCategory = ea.specialCategory;
|
|
}
|
|
if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) {
|
|
this.selectedWeakerSection = ea.weakerSection;
|
|
}
|
|
if (accountStatus === 'MINOR') {
|
|
if (ea.guardianClientId) {
|
|
this.guardianClientId = ea.guardianClientId;
|
|
}
|
|
if (ea.guardianName) {
|
|
this.guardianName = ea.guardianName;
|
|
}
|
|
if (ea.guardianCifNumber) {
|
|
this.guardianCif = ea.guardianCifNumber;
|
|
}
|
|
if (ea.guardianRelationship) {
|
|
this.guardianRelationship = ea.guardianRelationship;
|
|
}
|
|
if (ea.minorDateOfBirth) {
|
|
const d = new Date(ea.minorDateOfBirth);
|
|
if (!isNaN(d.getTime())) {
|
|
this.minorDob = d;
|
|
}
|
|
}
|
|
}
|
|
// autoDebitFacility from existing account (override product default)
|
|
if (ea.autoDebitFacility !== undefined) {
|
|
this.recurringDepositAccountDetailsForm.patchValue({ autoDebitFacility: !!ea.autoDebitFacility });
|
|
}
|
|
this.cdr.detectChanges();
|
|
}
|
|
|
|
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 ?? '';
|
|
}
|
|
|
|
// ── 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();
|
|
}
|
|
});
|
|
}
|
|
|
|
removeJointHolder(index: number): void {
|
|
this.jointHolders = this.jointHolders.filter((_, i) => i !== index);
|
|
this.autoSetModeOfOperation();
|
|
}
|
|
|
|
toggleJointHolderMandatory(holder: any): void {
|
|
holder.isMandatory = !holder.isMandatory;
|
|
this.autoSetModeOfOperation();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
onPrimaryShareInput(event: Event): void {
|
|
const input = event.target as HTMLInputElement;
|
|
let val = +input.value;
|
|
if (val > 100) {
|
|
val = 100;
|
|
input.value = '100';
|
|
}
|
|
this.primaryHolderShare = val;
|
|
if (val >= 1) this.redistributeCoHolders();
|
|
}
|
|
|
|
onPrimaryShareBlur(event: Event): void {
|
|
const input = event.target as HTMLInputElement;
|
|
let val = +input.value;
|
|
if (!val || val < 1) {
|
|
val = 1;
|
|
input.value = '1';
|
|
this.primaryHolderShare = 1;
|
|
this.redistributeCoHolders();
|
|
}
|
|
}
|
|
|
|
private redistributeCoHolders(): void {
|
|
if (this.jointHolders.length === 0) return;
|
|
const remaining = 100 - this.primaryHolderShare;
|
|
const totalCo = this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0);
|
|
if (totalCo === 0) return;
|
|
let allocated = 0;
|
|
this.jointHolders.forEach((h, i) => {
|
|
if (i === this.jointHolders.length - 1) {
|
|
h.sharePercentage = Math.max(0, remaining - allocated);
|
|
} else {
|
|
const share = Math.round((h.sharePercentage / totalCo) * remaining);
|
|
h.sharePercentage = Math.max(0, share);
|
|
allocated += h.sharePercentage;
|
|
}
|
|
});
|
|
}
|
|
|
|
get isJointOwnershipValid(): boolean {
|
|
return (
|
|
this.totalJointSharePercentage === 100 &&
|
|
this.primaryHolderShare >= 1 &&
|
|
this.jointHolders.every((h) => h.sharePercentage >= 1)
|
|
);
|
|
}
|
|
|
|
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 recurring deposits account details form value.
|
|
*/
|
|
get recurringDepositAccountDetails() {
|
|
const value = this.recurringDepositAccountDetailsForm.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;
|
|
}
|
|
}
|