500 lines
20 KiB
TypeScript
500 lines
20 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, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||
import { Dates } from 'app/core/utils/dates';
|
||
|
||
/** Custom Services */
|
||
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
|
||
import { SettingsService } from 'app/settings/settings.service';
|
||
import { ClientsService } from 'app/clients/clients.service';
|
||
import { MatDialog } from '@angular/material/dialog';
|
||
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
||
import { MatIcon } from '@angular/material/icon';
|
||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
|
||
import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add-related-client-dialog.component';
|
||
|
||
/**
|
||
* Current Account Details Step
|
||
*/
|
||
@Component({
|
||
selector: 'mifosx-current-account-details-step',
|
||
templateUrl: './current-account-details-step.component.html',
|
||
styleUrls: ['./current-account-details-step.component.scss'],
|
||
imports: [
|
||
...STANDALONE_SHARED_IMPORTS,
|
||
MatStepperPrevious,
|
||
FaIconComponent,
|
||
MatStepperNext,
|
||
IntroducerSearchComponent,
|
||
MatIcon,
|
||
FormsModule
|
||
]
|
||
})
|
||
export class CurrentAccountDetailsStepComponent implements OnInit {
|
||
private formBuilder = inject(UntypedFormBuilder);
|
||
private currentAccountsService = inject(CurrentAccountsService);
|
||
private settingsService = inject(SettingsService);
|
||
private clientsService = inject(ClientsService);
|
||
private dialog = inject(MatDialog);
|
||
private dateUtils = inject(Dates);
|
||
private cdr = inject(ChangeDetectorRef);
|
||
|
||
@Input() currentAccountTemplate: any;
|
||
|
||
minDate = new Date(2000, 0, 1);
|
||
maxDate = new Date();
|
||
productData: any;
|
||
fieldOfficerData: any;
|
||
isFieldOfficerPatched = false;
|
||
currentAccountDetailsForm: UntypedFormGroup;
|
||
|
||
currentAccountProductSelected = false;
|
||
introducerId: number | null = null;
|
||
introducerName: string | null = null;
|
||
introducerCif: string | null = null;
|
||
|
||
// ── Non-Individual (existing) ──────────────────────────────────────────
|
||
isNonIndividualClient = false;
|
||
relatedClients: Array<{
|
||
clientId: number;
|
||
clientName: string;
|
||
clientCif: string;
|
||
sharePercentage: number;
|
||
isMandatory: boolean;
|
||
}> = [];
|
||
signatoryRights = '';
|
||
|
||
readonly signatoryRightsOptions = [
|
||
{ value: 'ANYONE', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' },
|
||
{ value: 'ONE_MANDATORY', label: 'J/One', sublabel: 'Joint – One Mandatory', icon: 'group' },
|
||
{ value: 'TWO_MANDATORY', label: 'J/Two', sublabel: 'Joint – Two Mandatory', icon: 'groups' },
|
||
{ value: 'ONLY_ONE', label: 'Single', sublabel: 'Only 1 Person', icon: 'person' }
|
||
];
|
||
|
||
@Output() currentAccountProductTemplate = new EventEmitter();
|
||
|
||
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 = 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 (Status of Account) ─────────────────────────────────
|
||
jointHolders: Array<{
|
||
clientId: number;
|
||
clientName: string;
|
||
clientCif: string;
|
||
sharePercentage: number;
|
||
isMandatory: boolean;
|
||
}> = [];
|
||
|
||
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'];
|
||
|
||
constructor() {
|
||
this.createCurrentAccountDetailsForm();
|
||
}
|
||
|
||
ngOnInit() {
|
||
this.maxDate = this.settingsService.businessDate;
|
||
this.buildDependencies();
|
||
|
||
const clientId = this.currentAccountTemplate?.clientId;
|
||
if (clientId) {
|
||
this.primaryClientId = clientId;
|
||
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
|
||
this.isNonIndividualClient = client?.legalForm?.id === 2;
|
||
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();
|
||
});
|
||
}
|
||
|
||
if (this.currentAccountTemplate) {
|
||
this.productData = this.currentAccountTemplate.productOptions;
|
||
if (this.currentAccountTemplate.currentAccountProductId) {
|
||
this.currentAccountDetailsForm.patchValue({
|
||
productId: this.currentAccountTemplate.currentAccountProductId,
|
||
submittedOnDate:
|
||
this.currentAccountTemplate.timeline.submittedOnDate &&
|
||
new Date(this.currentAccountTemplate.timeline.submittedOnDate),
|
||
externalId: this.currentAccountTemplate.externalId
|
||
});
|
||
} else {
|
||
this.currentAccountDetailsForm.patchValue({
|
||
submittedOnDate: this.settingsService.businessDate
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
createCurrentAccountDetailsForm() {
|
||
this.currentAccountDetailsForm = this.formBuilder.group({
|
||
productId: ['', Validators.required],
|
||
submittedOnDate: ['', Validators.required],
|
||
fieldOfficerId: [''],
|
||
externalId: [''],
|
||
balanceSwapOption: [false],
|
||
msme: [false],
|
||
autoDebitFacility: [false],
|
||
establishmentDate: [''],
|
||
startOperationDate: ['']
|
||
});
|
||
}
|
||
|
||
buildDependencies() {
|
||
const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId;
|
||
this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
|
||
this.currentAccountsService
|
||
.getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false)
|
||
.subscribe((response: any) => {
|
||
if (!response.currency) {
|
||
const selectedProduct = this.currentAccountTemplate.productOptions?.find(
|
||
(product: any) => product.id == productId
|
||
);
|
||
if (selectedProduct?.currency) {
|
||
response.currency = selectedProduct.currency;
|
||
} else if (this.currentAccountTemplate.currency) {
|
||
response.currency = this.currentAccountTemplate.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.currentAccountProductTemplate.emit(response);
|
||
this.fieldOfficerData = response.fieldOfficerOptions;
|
||
this.currentAccountProductSelected = true;
|
||
this.currentAccountDetailsForm.patchValue({
|
||
balanceSwapOption: !!response.balanceSwapOption,
|
||
msme: !!response.msme,
|
||
autoDebitFacility: !!response.autoDebitFacility
|
||
});
|
||
this.cdr.detectChanges();
|
||
if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) {
|
||
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId);
|
||
this.isFieldOfficerPatched = true;
|
||
} else {
|
||
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue('');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
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') || '';
|
||
}
|
||
|
||
// ── Client DOB helpers ─────────────────────────────────────────────────
|
||
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;
|
||
}
|
||
|
||
private calculateAge(dob: Date): number {
|
||
const today = this.settingsService.businessDate ?? new Date();
|
||
let age = today.getFullYear() - dob.getFullYear();
|
||
const m = today.getMonth() - dob.getMonth();
|
||
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
|
||
return age;
|
||
}
|
||
|
||
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';
|
||
}
|
||
}
|
||
|
||
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 '';
|
||
}
|
||
|
||
// ── Introducer ─────────────────────────────────────────────────────────
|
||
onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
|
||
this.introducerId = event?.clientId ?? null;
|
||
this.introducerName = event?.displayName ?? null;
|
||
this.introducerCif = event?.cifNumber ?? null;
|
||
}
|
||
|
||
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 (Status of Account) ─────────────────────────────────
|
||
openAddJointHolderDialog(): void {
|
||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||
width: '500px',
|
||
panelClass: 'arc-dialog-panel',
|
||
disableClose: true
|
||
});
|
||
ref.afterClosed().subscribe((result) => {
|
||
if (result) {
|
||
const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif);
|
||
if (duplicate) {
|
||
alert(`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);
|
||
}
|
||
|
||
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') || '';
|
||
}
|
||
|
||
// ── Non-Individual (existing) helpers ──────────────────────────────────
|
||
openAddRelatedClientDialog(): void {
|
||
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
|
||
width: '500px',
|
||
panelClass: 'arc-dialog-panel',
|
||
disableClose: true
|
||
});
|
||
ref.afterClosed().subscribe((result) => {
|
||
if (result) {
|
||
const duplicate = this.relatedClients.some((c) => c.clientCif && c.clientCif === result.clientCif);
|
||
if (duplicate) {
|
||
alert(`Partner with CIF ${result.clientCif} is already added.`);
|
||
return;
|
||
}
|
||
this.relatedClients = [...this.relatedClients, { ...result, isMandatory: false }];
|
||
this.autoSetSignatoryRights();
|
||
}
|
||
});
|
||
}
|
||
|
||
removeRelatedClient(index: number): void {
|
||
this.relatedClients = this.relatedClients.filter((_, i) => i !== index);
|
||
this.autoSetSignatoryRights();
|
||
}
|
||
|
||
toggleMandatory(client: any): void {
|
||
client.isMandatory = !client.isMandatory;
|
||
this.autoSetSignatoryRights();
|
||
}
|
||
|
||
autoSetSignatoryRights(): void {
|
||
const mandatory = this.relatedClients.filter((c) => c.isMandatory).length;
|
||
if (mandatory === 0) this.signatoryRights = 'ANYONE';
|
||
else if (mandatory === 1) this.signatoryRights = 'ONE_MANDATORY';
|
||
else this.signatoryRights = 'TWO_MANDATORY';
|
||
}
|
||
|
||
get totalSharePercentage(): number {
|
||
return this.relatedClients.reduce((sum, c) => sum + (c.sharePercentage || 0), 0);
|
||
}
|
||
|
||
get mandatoryCount(): number {
|
||
return this.relatedClients.filter((c) => c.isMandatory).length;
|
||
}
|
||
|
||
get selectedSignatoryLabel(): string {
|
||
const opt = this.signatoryRightsOptions.find((x) => x.value === this.signatoryRights);
|
||
return opt ? opt.sublabel : '';
|
||
}
|
||
|
||
get currentAccountDetails() {
|
||
const value = this.currentAccountDetailsForm.getRawValue();
|
||
if (this.introducerId) {
|
||
value.introducerId = this.introducerId;
|
||
value.introducerName = this.introducerName;
|
||
value.introducerCif = this.introducerCif;
|
||
}
|
||
if (this.isNonIndividualClient) {
|
||
value.signatoryRights = this.signatoryRights || null;
|
||
value.partners = this.relatedClients.map((c) => ({
|
||
clientId: c.clientId,
|
||
clientName: c.clientName,
|
||
clientCif: c.clientCif,
|
||
sharePercentage: c.sharePercentage,
|
||
isMandatory: c.isMandatory
|
||
}));
|
||
} else {
|
||
if (this.selectedAccountStatus) value.accountStatus = this.selectedAccountStatus;
|
||
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 === 'JOINT') {
|
||
value.modeOfOperation = this.modeOfOperation || null;
|
||
value.jointHolders = [
|
||
{
|
||
clientId: this.primaryClientId,
|
||
clientName: this.primaryClientName,
|
||
clientCif: this.primaryClientCif,
|
||
sharePercentage: this.primaryHolderShare,
|
||
isMandatory: true,
|
||
isPrimary: true
|
||
},
|
||
...this.jointHolders.map((h) => ({ ...h, isPrimary: false }))
|
||
];
|
||
}
|
||
if (this.selectedAccountStatus === 'MINOR') {
|
||
value.guardianClientId = this.guardianClientId;
|
||
value.guardianName = this.guardianName;
|
||
value.guardianCif = this.guardianCif;
|
||
value.guardianRelationship = this.guardianRelationship;
|
||
value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null;
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
}
|