/** * 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, inject } from '@angular/core'; import { ActivatedRoute, Router, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component'; import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component'; import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component'; import { PaymentDialogComponent } from './custom-dialogs/payment-dialog/payment-dialog.component'; import { PrematureCloseDialogComponent } from './custom-dialogs/premature-close-dialog/premature-close-dialog.component'; import { DepositCertificateDialogComponent } from 'app/shared/deposit-certificate-dialog/deposit-certificate-dialog.component'; /** Custom Button Config. */ import { FixedDepositsButtonsConfiguration } from './fixed-deposits-buttons.config'; /** Custom Services */ import { FixedDepositsService } from '../fixed-deposits.service'; import { SavingsService } from 'app/savings/savings.service'; import { AccountsService } from 'app/accounts/accounts.service'; import { Currency } from 'app/shared/models/general.model'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { AlertService } from 'app/core/alert/alert.service'; import { AuthenticationService } from '../../../core/authentication/authentication.service'; import { MatTooltip } from '@angular/material/tooltip'; import { NgClass, CurrencyPipe } from '@angular/common'; import { AccountNumberComponent } from '../../../shared/account-number/account-number.component'; import { LongTextComponent } from '../../../shared/long-text/long-text.component'; import { MatIconButton } from '@angular/material/button'; import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../../pipes/status-lookup.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; /** * Fixed Deposits Account View Component */ @Component({ selector: 'mifosx-fixed-deposit-account-view', templateUrl: './fixed-deposit-account-view.component.html', styleUrls: ['./fixed-deposit-account-view.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, NgClass, LongTextComponent, AccountNumberComponent, MatIconButton, MatMenuTrigger, FaIconComponent, MatMenu, MatMenuItem, MatTabNav, MatTabLink, RouterLinkActive, MatTabNavPanel, RouterOutlet, CurrencyPipe, StatusLookupPipe, DormancyAlertPanelComponent ] }) export class FixedDepositAccountViewComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); private alertService = inject(AlertService); private fixedDepositsService = inject(FixedDepositsService); private savingsService = inject(SavingsService); private accountsService = inject(AccountsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); private authenticationService = inject(AuthenticationService); dialog = inject(MatDialog); /** Fixed Deposits Account Data */ fixedDepositsAccountData: any; /** Savings Data Tables */ savingsDatatables: any; /** Button Configurations */ buttonConfig: FixedDepositsButtonsConfiguration; /** Entity Type */ entityType: string; currency: Currency; showTransactions = false; private readonly ACTION_KIND: Record = { Deposit: 'transact', Withdrawal: 'transact', 'Transfer Funds': 'transact', 'Calculate Interest': 'interest', 'Post Interest': 'interest', 'Enable Withhold Tax': 'interest', 'Disable Withhold Tax': 'interest', Payment: 'transact', 'Premature Close': 'danger', 'Undo Activation': 'admin', Close: 'danger', 'Modify Application': 'admin', Approve: 'admin', 'Undo Approval': 'admin', Activate: 'admin', 'Add Charge': 'admin', 'Apply Annual Fees': 'admin', Reject: 'danger', 'Withdrawn by Client': 'danger', Delete: 'danger', Terminate: 'danger' }; private readonly ACTION_ICON: Record = { Deposit: 'arrow-down', Withdrawal: 'arrow-up', 'Transfer Funds': 'exchange-alt', 'Calculate Interest': 'calculator', 'Post Interest': 'percentage', 'Enable Withhold Tax': 'percentage', 'Disable Withhold Tax': 'percentage', Payment: 'sync-alt', 'Premature Close': 'times-circle', 'Undo Activation': 'undo', Close: 'times-circle', 'Modify Application': 'edit', Approve: 'check-circle', 'Undo Approval': 'undo', Activate: 'check', 'Add Charge': 'plus-circle', 'Apply Annual Fees': 'receipt', Reject: 'ban', 'Withdrawn by Client': 'user-slash', Delete: 'trash', Terminate: 'times-circle' }; kindOf(name: string): 'transact' | 'block' | 'interest' | 'admin' | 'danger' { return this.ACTION_KIND[name] ?? 'admin'; } iconOf(name: string, fallback?: string): string { return this.ACTION_ICON[name] ?? fallback ?? 'circle'; } hasKind(kind: 'transact' | 'block' | 'interest' | 'admin' | 'danger'): boolean { const all = [ ...(this.buttonConfig?.singleButtons ?? []), ...(this.buttonConfig?.options ?? []) ]; return all.some((item) => this.kindOf(item.name) === kind); } /** * Fetches fixed deposits account data from `resolve` * @param {ActivatedRoute} route Activated Route * @param {Router} router Router * @param {FixedDepositsService} fixedDepositsService Fixed Deposits Service * @param {SavingsService} savingsService Savings Service * @param {MatDialog} dialog Mat Dialog */ constructor() { this.route.data.subscribe((data: { fixedDepositsAccountData: any; savingsDatatables: any }) => { this.fixedDepositsAccountData = data.fixedDepositsAccountData; this.savingsDatatables = data.savingsDatatables; this.currency = this.fixedDepositsAccountData.currency; const status: any = data.fixedDepositsAccountData.status; this.showTransactions = status.id >= 300; }); if (this.router.url.includes('clients')) { this.entityType = 'Client'; } else if (this.router.url.includes('groups')) { this.entityType = 'Group'; } else if (this.router.url.includes('centers')) { this.entityType = 'Center'; } } ngOnInit() { this.setConditionalButtons(); } /** * Adds options to button config. conditionaly. */ setConditionalButtons() { const status = this.fixedDepositsAccountData.status.value; this.buttonConfig = new FixedDepositsButtonsConfiguration(status); if (this.fixedDepositsAccountData.taxGroup && status === 'Active') { if (this.fixedDepositsAccountData.withHoldTax) { this.buttonConfig.addOption({ name: 'Disable Withhold Tax' }); } else { this.buttonConfig.addOption({ name: 'Enable Withhold Tax' }); } } if (!this.authenticationService.hasCashierRole()) { this.buttonConfig.buttonsArray = this.buttonConfig.buttonsArray.filter( (btn) => btn.name !== 'Deposit' && btn.name !== 'Withdrawal' ); this.buttonConfig.optionArray = this.buttonConfig.optionArray.filter( (opt) => opt.name !== 'Deposit' && opt.name !== 'Withdrawal' ); } } /** * Refetches data fot the component * TODO: Replace by a custom reload component instead of hard-coded back-routing. */ reload() { const clientId = this.fixedDepositsAccountData.clientId; const url: string = this.router.url; this.router .navigateByUrl(`/clients/${clientId}/fixed-deposits-accounts`, { skipLocationChange: true }) .then(() => this.router.navigate([url])); } /** * Performs action button/option action. * @param {string} name action name. */ doAction(name: string) { switch (name) { case 'Approve': case 'Reject': case 'Activate': case 'Close': case 'Undo Approval': case 'Undo Activation': case 'Add Charge': case 'Withdrawn by Client': case 'Deposit': case 'Withdrawal': this.router.navigate([`actions/${name}`], { relativeTo: this.route }); break; case 'Modify Application': this.router.navigate(['edit'], { relativeTo: this.route }); break; case 'Delete': this.deleteFixedDepositsAccount(); break; case 'Calculate Interest': this.calculateInterest(); break; case 'Post Interest': this.postInterest(); break; case 'Enable Withhold Tax': this.enableWithHoldTax(); break; case 'Disable Withhold Tax': this.disableWithHoldTax(); break; case 'Terminate': this.terminateFixedDepositsAccount(); break; case 'Payment': this.openPaymentDialog(); break; case 'Premature Close': this.openPrematureCloseDialog(); break; } } /** * Opens the Payment action -- only offered once the FD has actually Matured (see * FixedDepositsButtonsConfiguration), and the only place a Partial Reinvest can be done. Also the * exact same dialog the FD Dashboard's Renew action opens (see fd-dashboard.component.ts's * openRenewDialog), just without dashboardRenewal data, so its date picker stays visible. */ private openPaymentDialog(): void { const paymentDialogRef = this.dialog.open(PaymentDialogComponent, { width: '720px', maxWidth: '95vw', data: { accountId: this.fixedDepositsAccountData.id, // The server bounds a partial reinvest by the balance AFTER it posts interest to the chosen date, // so the dialog needs both figures plus the maturity date to know which one that will be. accountBalance: this.fixedDepositsAccountData.summary?.accountBalance, maturityDate: this.fixedDepositsAccountData.maturityDate } }); paymentDialogRef.afterClosed().subscribe((response: any) => { if (response?.confirm) { this.reload(); } }); } /** * The start date of the term currently running -- the last entry in renewalHistory, which is ordered * oldest-first (see RenewalHistoryTabComponent). Falls back to the account's own activation date for * an FD that has never been renewed (or whose renewalHistory hasn't loaded), which is the same date * in that case anyway. */ private get currentTermStartDate(): any { const history = this.fixedDepositsAccountData?.renewalHistory; const latestTerm = Array.isArray(history) && history.length > 0 ? history[history.length - 1] : null; return latestTerm?.activatedondate ?? this.fixedDepositsAccountData?.timeline?.activatedOnDate; } /** * Opens the Premature Close action -- Active-only, no reinvest option, since the deposit hasn't * matured yet. */ private openPrematureCloseDialog(): void { const prematureCloseDialogRef = this.dialog.open(PrematureCloseDialogComponent, { width: '720px', maxWidth: '95vw', data: { accountId: this.fixedDepositsAccountData.id, accountNo: this.fixedDepositsAccountData.fdAccountNumber || this.fixedDepositsAccountData.accountNo, // calculatePrematureAmount only ever returns the final maturityAmount, not how it was reached -- // these come from the account's own data (already loaded for this page) so the dialog can show // the breakdown (deposit, contracted rate, penalty, interest earned) behind that one number. depositAmount: this.fixedDepositsAccountData.depositAmount, nominalAnnualInterestRate: this.fixedDepositsAccountData.nominalAnnualInterestRate, preClosurePenalApplicable: this.fixedDepositsAccountData.preClosurePenalApplicable, preClosurePenalInterest: this.fixedDepositsAccountData.preClosurePenalInterest, preClosurePenalInterestOnType: this.fixedDepositsAccountData.preClosurePenalInterestOnType, // Gate which RBI-mandated waiver reasons the dialog offers -- these two are the product's own // facility toggles (Deceased Settlement / No Penalty Nominee-Heir Closure), off by default, so // a product that never enabled them shouldn't offer a reason implying it did. deceasedSettlement: this.fixedDepositsAccountData.deceasedSettlement, noPenaltyNomineeHeirClosure: this.fixedDepositsAccountData.noPenaltyNomineeHeirClosure, // Tenure context (current term's start date, maturity date, current balance) so the dialog can // show how far into THIS term the account is and how much is actually sitting in it today, not // just the penalty-adjusted amount calculatePrematureAmount returns. Deliberately the current // term's own start (see currentTermStartDate), not the account's original activation -- a // renewed FD's interest/tenure runs from the renewal, and showing the original activation date // here would make an account renewed days ago look like it has been running for months. activatedOnDate: this.currentTermStartDate, maturityDate: this.fixedDepositsAccountData.maturityDate, currentBalance: this.fixedDepositsAccountData.summary?.accountBalance } }); prematureCloseDialogRef.afterClosed().subscribe((response: any) => { if (response?.confirm) { this.reload(); } }); } /** * Deletes Fixed Deposits Account. */ private deleteFixedDepositsAccount() { const deleteFixedDepositsAccountDialogRef = this.dialog.open(DeleteDialogComponent, { data: { deleteContext: `fixed deposit account with id: ${this.fixedDepositsAccountData.id}` } }); deleteFixedDepositsAccountDialogRef.afterClosed().subscribe((response: any) => { if (response.delete) { this.fixedDepositsService.deleteFixedDepositsAccount(this.fixedDepositsAccountData.id).subscribe(() => { this.router.navigate(['../../'], { relativeTo: this.route }); }); } }); } private terminateFixedDepositsAccount() { const terminateFixedDepositAccountDialogRef = this.dialog.open(ConfirmationDialogComponent, { data: { heading: 'Terminate Account', dialogContext: 'Are you sure you want to terminate this account?', type: 'Strong' } }); terminateFixedDepositAccountDialogRef.afterClosed().subscribe((response: any) => { if (response && response.confirm) { const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const terminatedOnDate = this.dateUtils.formatDate(new Date(), dateFormat); const data = { accountType: 'FD', terminatedOnDate, dateFormat, locale }; this.accountsService.terminateAccount(this.fixedDepositsAccountData.id, data).subscribe(() => { this.reload(); }); } }); } /** * Calculates fixed deposit account's interest */ private calculateInterest() { const calculateInterestAccountDialogRef = this.dialog.open(CalculateInterestDialogComponent); calculateInterestAccountDialogRef.afterClosed().subscribe((response: any) => { if (response.confirm) { this.fixedDepositsService .executeFixedDepositsAccountCommand(this.fixedDepositsAccountData.id, 'calculateInterest', {}) .subscribe(() => { this.reload(); }); } }); } /** * Posts fixed deposit account's interest */ private postInterest() { const postInterestAccountDialogRef = this.dialog.open(PostInterestDialogComponent); postInterestAccountDialogRef.afterClosed().subscribe((response: any) => { if (response.confirm) { this.fixedDepositsService .executeFixedDepositsAccountCommand(this.fixedDepositsAccountData.id, 'postInterest', {}) .subscribe((commandResponse: any) => { // Parked in the Checker Inbox rather than executed (HTTP 200 with // rollbackTransaction: true), so the reload below shows no new interest posting. if (commandResponse?.rollbackTransaction) { this.alertService.alert({ type: 'Success', message: 'Interest posting has been sent for checker approval. No interest is posted until a checker approves it.' }); } this.reload(); }); } }); } /** * Enables withhold tax for fixed deposits account. * Fixed deposits endpoint is not supported so using Savings endpoint. */ private enableWithHoldTax() { const deleteSavingsAccountDialogRef = this.dialog.open(ToggleWithholdTaxDialogComponent, { data: { isEnable: true } }); deleteSavingsAccountDialogRef.afterClosed().subscribe((response: any) => { if (response.confirm) { this.savingsService .executeSavingsAccountUpdateCommand(this.fixedDepositsAccountData.id, 'updateWithHoldTax', { withHoldTax: true }) .subscribe(() => { this.reload(); }); } }); } /** * Opens a printable deposit certificate for this account. */ printCertificate() { this.dialog.open(DepositCertificateDialogComponent, { width: '900px', data: { accountType: 'FD', accountData: this.fixedDepositsAccountData, clientId: this.fixedDepositsAccountData.clientId } }); } /** * Disables withhold tax for fixed deposits account * Fixed deposits endpoint is not supported so using Savings endpoint. */ private disableWithHoldTax() { const disableWithHoldTaxDialogRef = this.dialog.open(ToggleWithholdTaxDialogComponent, { data: { isEnable: false } }); disableWithHoldTaxDialogRef.afterClosed().subscribe((response: any) => { if (response.confirm) { this.savingsService .executeSavingsAccountUpdateCommand(this.fixedDepositsAccountData.id, 'updateWithHoldTax', { withHoldTax: false }) .subscribe(() => { this.reload(); }); } }); } }