418 lines
15 KiB
TypeScript
418 lines
15 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, inject, ViewChild } from '@angular/core';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import { FormsModule, FormGroup } from '@angular/forms';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { MatIcon } from '@angular/material/icon';
|
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component';
|
|
import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component';
|
|
import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component';
|
|
import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component';
|
|
|
|
/** Custom Services */
|
|
import { CurrentAccountsService } from '../current-accounts.service';
|
|
import { ClientsService } from 'app/clients/clients.service';
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
|
|
/** Default state for every Digital & Account Feature toggle, mirrors
|
|
* current-account-digital-step's DEFAULT_FACILITY_CONFIG. */
|
|
const DEFAULT_FACILITY_CONFIG: Record<string, boolean> = {
|
|
atmFacility: false,
|
|
atmWithdrawalFacility: false,
|
|
upiFacility: false,
|
|
posTransactionsFacility: false,
|
|
chequeBook: false,
|
|
impsFacility: false,
|
|
smsAlerts: false,
|
|
bbpsFacility: false,
|
|
insuranceCover: false,
|
|
bepgFacility: false,
|
|
neftFacility: false,
|
|
rtgsFacility: false,
|
|
interBankTotalCapFacility: false,
|
|
withdrawalFacility: false,
|
|
depositFacilityKyc: false,
|
|
virtualDebitCard: false,
|
|
internetBanking: false,
|
|
mobileBanking: false
|
|
};
|
|
|
|
/** Toggle metadata used to render the feature grid — key must match a
|
|
* property of DEFAULT_FACILITY_CONFIG / the backend's accountFacilityConfig map. */
|
|
const FACILITY_TOGGLES: { key: string; label: string; icon: string }[] = [
|
|
{ key: 'atmFacility', label: 'ATM Facility', icon: 'fa-credit-card' },
|
|
{ key: 'atmWithdrawalFacility', label: 'ATM Withdrawal', icon: 'fa-wallet' },
|
|
{ key: 'upiFacility', label: 'UPI Facility', icon: 'fa-mobile-alt' },
|
|
{ key: 'posTransactionsFacility', label: 'POS Transactions', icon: 'fa-exchange-alt' },
|
|
{ key: 'chequeBook', label: 'Cheque Book', icon: 'fa-book' },
|
|
{ key: 'impsFacility', label: 'IMPS Facility', icon: 'fa-bolt' },
|
|
{ key: 'smsAlerts', label: 'SMS Alerts', icon: 'fa-sms' },
|
|
{ key: 'bbpsFacility', label: 'BBPS Facility', icon: 'fa-bell' },
|
|
{ key: 'insuranceCover', label: 'Insurance Cover', icon: 'fa-shield-alt' },
|
|
{ key: 'bepgFacility', label: 'BEPG Facility', icon: 'fa-plug' },
|
|
{ key: 'neftFacility', label: 'NEFT Facility', icon: 'fa-money-check-alt' },
|
|
{ key: 'rtgsFacility', label: 'RTGS Facility', icon: 'fa-money-check' },
|
|
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap', icon: 'fa-balance-scale' },
|
|
{ key: 'withdrawalFacility', label: 'Withdrawal Facility', icon: 'fa-arrow-right-arrow-left' },
|
|
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)', icon: 'fa-id-card' },
|
|
{ key: 'virtualDebitCard', label: 'Virtual Debit Card', icon: 'fa-credit-card' },
|
|
{ key: 'internetBanking', label: 'Internet Banking', icon: 'fa-globe' },
|
|
{ key: 'mobileBanking', label: 'Mobile Banking', icon: 'fa-mobile-screen-button' }
|
|
];
|
|
|
|
/**
|
|
* Edit Nominee & Features — maker-checker submission screen.
|
|
*
|
|
* A maker proposes changes to a current account's Digital & Account Features toggles and/or its
|
|
* nominee here; nothing is applied immediately. The request is staged and a different user (checker)
|
|
* must approve it from the "Pending Current Account Edits" screen before it takes effect.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-edit-current-account-features',
|
|
templateUrl: './edit-current-account-features.component.html',
|
|
styleUrls: ['./edit-current-account-features.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
FormsModule,
|
|
FaIconComponent,
|
|
MatIcon,
|
|
MatProgressSpinner,
|
|
MatTabsModule,
|
|
CurrentAccountDetailsStepComponent,
|
|
CurrentAccountCurrencyStepComponent,
|
|
CurrentAccountDigitalStepComponent,
|
|
CurrentAccountPreviewStepComponent
|
|
]
|
|
})
|
|
export class EditCurrentAccountFeaturesComponent {
|
|
private route = inject(ActivatedRoute);
|
|
private router = inject(Router);
|
|
private currentAccountsService = inject(CurrentAccountsService);
|
|
private clientsService = inject(ClientsService);
|
|
private settingsService = inject(SettingsService);
|
|
private dialog = inject(MatDialog);
|
|
|
|
@ViewChild(CurrentAccountDetailsStepComponent) currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
|
|
@ViewChild(CurrentAccountCurrencyStepComponent) currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent;
|
|
@ViewChild(CurrentAccountDigitalStepComponent) currentAccountDigitalStep: CurrentAccountDigitalStepComponent;
|
|
|
|
originalNominees: any[] = [];
|
|
|
|
get currentAccountTermForm(): FormGroup {
|
|
return new FormGroup({});
|
|
}
|
|
|
|
get currentAccountSettingsForm(): FormGroup {
|
|
return new FormGroup({});
|
|
}
|
|
|
|
get currentAccountTermsForm(): FormGroup {
|
|
return new FormGroup({});
|
|
}
|
|
|
|
get currentAccount() {
|
|
return this.currentAccountDetailsStep ? { ...this.currentAccountDetailsStep.currentAccountDetails } : null;
|
|
}
|
|
|
|
get currentAccountDigital() {
|
|
return this.currentAccountDigitalStep?.currentAccountDigital ?? null;
|
|
}
|
|
|
|
get currentAccountCurrencyFlags() {
|
|
return {
|
|
balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false,
|
|
msme: this.currentAccountCurrencyStep?.msme ?? false,
|
|
autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false
|
|
};
|
|
}
|
|
|
|
readonly toggles = FACILITY_TOGGLES;
|
|
|
|
accountId: string;
|
|
currentAccountAndTemplate: any;
|
|
clientId = '';
|
|
accountNo = '';
|
|
|
|
currentAccountProductTemplate: any = null;
|
|
selectedTabIndex = 0;
|
|
readonly totalTabs = 4;
|
|
|
|
originalFacilityConfig: Record<string, boolean> = { ...DEFAULT_FACILITY_CONFIG };
|
|
|
|
/** Current nominees on the account, fetched read-only for reference. */
|
|
nominees: any[] = [];
|
|
nomineesLoading = false;
|
|
relationshipOptions: any[] = [];
|
|
|
|
/** Pending edit already submitted for this account, if any. */
|
|
pendingEdit: any = null;
|
|
pendingEditLoading = true;
|
|
|
|
submitting = false;
|
|
|
|
constructor() {
|
|
this.accountId = this.route.snapshot.params['currentAccountId'];
|
|
this.loadPendingEdit();
|
|
this.route.data.subscribe((data: { currentAccountAndTemplate: any }) => {
|
|
const account = data.currentAccountAndTemplate;
|
|
this.currentAccountAndTemplate = account;
|
|
this.clientId = account?.clientId != null ? String(account.clientId) : '';
|
|
this.accountNo = account?.accountNo || '';
|
|
this.originalFacilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...(account?.accountFacilityConfig || {}) };
|
|
if (this.clientId) {
|
|
this.loadNominees();
|
|
this.loadRelationshipTemplate();
|
|
}
|
|
});
|
|
}
|
|
|
|
private todayIso(): string {
|
|
const d = new Date();
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
}
|
|
|
|
private loadPendingEdit(): void {
|
|
this.pendingEditLoading = true;
|
|
this.currentAccountsService.getPendingCurrentAccountEdit(this.accountId).subscribe({
|
|
next: (edit: any) => {
|
|
this.pendingEdit = edit && edit.id ? edit : null;
|
|
this.pendingEditLoading = false;
|
|
},
|
|
error: () => {
|
|
this.pendingEdit = null;
|
|
this.pendingEditLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private loadNominees(): void {
|
|
this.nomineesLoading = true;
|
|
this.clientsService.getClientNominees(this.clientId, this.accountNo, 'CB').subscribe({
|
|
next: (data: any) => {
|
|
this.nominees = Array.isArray(data) ? data : [];
|
|
this.originalNominees = JSON.parse(JSON.stringify(this.nominees));
|
|
this.nomineesLoading = false;
|
|
},
|
|
error: () => {
|
|
this.nominees = [];
|
|
this.originalNominees = [];
|
|
this.nomineesLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private loadRelationshipTemplate(): void {
|
|
this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({
|
|
next: (template: any) => {
|
|
this.relationshipOptions = template?.relationshipOptions || [];
|
|
},
|
|
error: () => {
|
|
this.relationshipOptions = [];
|
|
}
|
|
});
|
|
}
|
|
|
|
get hasPendingEdit(): boolean {
|
|
return !!this.pendingEdit && this.pendingEdit.status === 'PENDING';
|
|
}
|
|
|
|
get facilityConfig(): Record<string, boolean> {
|
|
return this.currentAccountDigitalStep?.facilityConfig ?? this.originalFacilityConfig;
|
|
}
|
|
|
|
get facilityChanged(): boolean {
|
|
const config = this.facilityConfig;
|
|
return this.toggles.some((t) => config[t.key] !== this.originalFacilityConfig[t.key]);
|
|
}
|
|
|
|
get changedToggles(): any[] {
|
|
const list: any[] = [];
|
|
const config = this.facilityConfig;
|
|
this.toggles.forEach((t) => {
|
|
if (config[t.key] !== this.originalFacilityConfig[t.key]) {
|
|
list.push({
|
|
label: t.label,
|
|
from: this.originalFacilityConfig[t.key],
|
|
to: config[t.key]
|
|
});
|
|
}
|
|
});
|
|
return list;
|
|
}
|
|
|
|
get nomineeChanged(): boolean {
|
|
return this.buildNomineePayload() !== null;
|
|
}
|
|
|
|
get canSubmit(): boolean {
|
|
return (
|
|
!this.hasPendingEdit &&
|
|
!this.pendingEditLoading &&
|
|
!this.submitting &&
|
|
(this.facilityChanged || this.nomineeChanged)
|
|
);
|
|
}
|
|
|
|
setTemplate($event: any): void {
|
|
this.currentAccountProductTemplate = $event;
|
|
}
|
|
|
|
next(): void {
|
|
if (this.selectedTabIndex < this.totalTabs - 1) {
|
|
this.selectedTabIndex += 1;
|
|
}
|
|
}
|
|
|
|
prev(): void {
|
|
if (this.selectedTabIndex > 0) {
|
|
this.selectedTabIndex -= 1;
|
|
}
|
|
}
|
|
|
|
getRelationshipName(relationshipId: any): string {
|
|
const opt = this.relationshipOptions.find((r: any) => r.id === relationshipId);
|
|
return opt?.name || '';
|
|
}
|
|
|
|
private buildFacilityDiff(): Record<string, boolean> | null {
|
|
const diff: Record<string, boolean> = {};
|
|
let changed = false;
|
|
const config = this.facilityConfig;
|
|
this.toggles.forEach((t) => {
|
|
if (config[t.key] !== this.originalFacilityConfig[t.key]) {
|
|
diff[t.key] = config[t.key];
|
|
changed = true;
|
|
}
|
|
});
|
|
return changed ? diff : null;
|
|
}
|
|
|
|
private buildNomineePayload(): any | null {
|
|
const original = this.originalNominees || [];
|
|
const current = this.currentAccountCurrencyStep?.nominees ?? [];
|
|
|
|
// Find if a nominee has been added
|
|
const added = current.find(
|
|
(curr: any) => !original.some((orig: any) => orig.id === curr.id || orig.nomineeClientId === curr.nomineeClientId)
|
|
);
|
|
if (added) {
|
|
return {
|
|
operation: 'ADD',
|
|
nomineeId: null,
|
|
nomineeClientId: added.nomineeClientId,
|
|
relationshipId: added.relationshipId,
|
|
percentageShare: added.percentageShare,
|
|
isActive: added.isActive !== undefined ? !!added.isActive : true,
|
|
nomineeIsMinor: !!added.nomineeIsMinor,
|
|
guardianClientId: added.nomineeIsMinor ? added.guardianClientId : null,
|
|
guardianRelationshipId: added.nomineeIsMinor ? added.guardianRelationshipId : null,
|
|
effectiveDate: this.todayIso(),
|
|
remarks: added.remarks || null
|
|
};
|
|
}
|
|
|
|
// Find if a nominee has been deleted
|
|
const deleted = original.find(
|
|
(orig: any) => !current.some((curr: any) => curr.id === orig.id || curr.nomineeClientId === orig.nomineeClientId)
|
|
);
|
|
if (deleted) {
|
|
return {
|
|
operation: 'DELETE',
|
|
nomineeId: deleted.id
|
|
};
|
|
}
|
|
|
|
// Find if any nominee has been updated
|
|
for (const curr of current) {
|
|
const orig = original.find((o: any) => o.id === curr.id || o.nomineeClientId === curr.nomineeClientId);
|
|
if (orig) {
|
|
const relChanged = orig.relationshipId !== curr.relationshipId;
|
|
const shareChanged = +orig.percentageShare !== +curr.percentageShare;
|
|
const minorChanged = !!orig.nomineeIsMinor !== !!curr.nomineeIsMinor;
|
|
const guardianChanged =
|
|
orig.guardianClientId !== curr.guardianClientId ||
|
|
orig.guardianRelationshipId !== curr.guardianRelationshipId;
|
|
const activeChanged = !!orig.isActive !== (curr.isActive !== undefined ? !!curr.isActive : true);
|
|
const remarksChanged = (orig.remarks || '') !== (curr.remarks || '');
|
|
|
|
if (relChanged || shareChanged || minorChanged || guardianChanged || activeChanged || remarksChanged) {
|
|
return {
|
|
operation: 'UPDATE',
|
|
nomineeId: orig.id,
|
|
relationshipId: curr.relationshipId,
|
|
percentageShare: curr.percentageShare,
|
|
isActive: curr.isActive !== undefined ? !!curr.isActive : true,
|
|
nomineeIsMinor: !!curr.nomineeIsMinor,
|
|
guardianClientId: curr.nomineeIsMinor ? curr.guardianClientId : null,
|
|
guardianRelationshipId: curr.nomineeIsMinor ? curr.guardianRelationshipId : null,
|
|
effectiveDate: this.todayIso(),
|
|
remarks: curr.remarks || null
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
submit(): void {
|
|
if (!this.canSubmit) return;
|
|
|
|
const locale = this.settingsService.language.code;
|
|
const payload: any = { locale, dateFormat: 'yyyy-MM-dd' };
|
|
|
|
const facilityDiff = this.buildFacilityDiff();
|
|
if (facilityDiff) {
|
|
payload.accountFacilityConfig = facilityDiff;
|
|
}
|
|
|
|
const nomineePayload = this.buildNomineePayload();
|
|
if (nomineePayload) {
|
|
payload.nominee = nomineePayload;
|
|
}
|
|
|
|
this.submitting = true;
|
|
this.currentAccountsService.submitCurrentAccountEdit(this.accountId, payload).subscribe({
|
|
next: () => {
|
|
this.submitting = false;
|
|
this.showSuccess();
|
|
},
|
|
error: () => {
|
|
this.submitting = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private showSuccess(): void {
|
|
const ref = this.dialog.open(SuccessDialogComponent, {
|
|
width: '420px',
|
|
maxWidth: '92vw',
|
|
autoFocus: false,
|
|
data: {
|
|
title: 'Submitted for Approval',
|
|
message: 'Your changes have been submitted and are awaiting checker approval.',
|
|
buttonLabel: 'Done'
|
|
}
|
|
});
|
|
ref.afterClosed().subscribe(() => this.cancel());
|
|
}
|
|
|
|
cancel(): void {
|
|
this.router.navigate(['../'], { relativeTo: this.route });
|
|
}
|
|
}
|