/** * 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 = { 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 = { ...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 { 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 | null { const diff: Record = {}; 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 }); } }