321 lines
11 KiB
TypeScript
321 lines
11 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 } 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 {
|
||
MatTable,
|
||
MatColumnDef,
|
||
MatHeaderCellDef,
|
||
MatHeaderCell,
|
||
MatCellDef,
|
||
MatCell,
|
||
MatHeaderRowDef,
|
||
MatHeaderRow,
|
||
MatRowDef,
|
||
MatRow
|
||
} from '@angular/material/table';
|
||
import { MatIcon } from '@angular/material/icon';
|
||
import { FormsModule } from '@angular/forms';
|
||
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,
|
||
MatTable,
|
||
MatColumnDef,
|
||
MatHeaderCellDef,
|
||
MatHeaderCell,
|
||
MatCellDef,
|
||
MatCell,
|
||
MatHeaderRowDef,
|
||
MatHeaderRow,
|
||
MatRowDef,
|
||
MatRow,
|
||
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);
|
||
|
||
/** Current Account Template */
|
||
@Input() currentAccountTemplate: 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 current account form */
|
||
isFieldOfficerPatched = false;
|
||
/** Current Account Details Form */
|
||
currentAccountDetailsForm: UntypedFormGroup;
|
||
|
||
currentAccountProductSelected = false;
|
||
introducerId: number | null = null;
|
||
introducerName: string | null = null;
|
||
introducerCif: string | null = null;
|
||
|
||
/** Non-Individual section */
|
||
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' }
|
||
];
|
||
|
||
/** Current Account Template with product data */
|
||
@Output() currentAccountProductTemplate = new EventEmitter();
|
||
|
||
/** Local copy of product template for displaying classification info */
|
||
productTemplate: any = null;
|
||
|
||
constructor() {
|
||
this.createCurrentAccountDetailsForm();
|
||
}
|
||
|
||
ngOnInit() {
|
||
this.maxDate = this.settingsService.businessDate;
|
||
this.buildDependencies();
|
||
if (this.currentAccountTemplate?.clientId) {
|
||
this.clientsService.getClientData(String(this.currentAccountTemplate.clientId)).subscribe((client: any) => {
|
||
this.isNonIndividualClient = client?.legalForm?.id === 2;
|
||
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
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates current account details form.
|
||
*/
|
||
createCurrentAccountDetailsForm() {
|
||
this.currentAccountDetailsForm = this.formBuilder.group({
|
||
productId: [
|
||
'',
|
||
Validators.required
|
||
],
|
||
submittedOnDate: [
|
||
'',
|
||
Validators.required
|
||
],
|
||
fieldOfficerId: [''],
|
||
externalId: [''],
|
||
balanceSwapOption: [false],
|
||
msme: [false],
|
||
autoDebitFacility: [false]
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Fetches current account product template on productId value changes
|
||
*/
|
||
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) => {
|
||
// Ensure currency is included in the product template
|
||
if (!response.currency) {
|
||
// Try to get currency from the selected product
|
||
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.currentAccountProductTemplate.emit(response);
|
||
this.fieldOfficerData = response.fieldOfficerOptions;
|
||
this.currentAccountProductSelected = true;
|
||
// Pre-populate toggles from product defaults (user can override)
|
||
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('');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Formats template dates into the current CBS date format.
|
||
*/
|
||
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') || '';
|
||
}
|
||
|
||
/**
|
||
* Returns current account form value.
|
||
*/
|
||
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;
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
/** Auto-compute Mode of Operation based on mandatory count */
|
||
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
|
||
}));
|
||
}
|
||
return value;
|
||
}
|
||
}
|