/** * 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, OnDestroy, inject, ElementRef, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { UntypedFormBuilder } from '@angular/forms'; import { CurrencyPipe } from '@angular/common'; import { Subject, takeUntil } from 'rxjs'; /** Custom Services */ import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { Currency } from 'app/shared/models/general.model'; import { CurrentAccountTransaction, CurrentAccountTransactionType } from 'app/current-accounts/models/current-account-transaction.model'; import { MatIcon } from '@angular/material/icon'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { DateFormatPipe } from '../../../pipes/date-format.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ selector: 'mifosx-passbook-tab', templateUrl: './passbook-tab.component.html', styleUrls: ['./passbook-tab.component.scss'], standalone: true, imports: [ ...STANDALONE_SHARED_IMPORTS, MatIcon, FaIconComponent, DateFormatPipe, CurrencyPipe ] }) export class PassbookTabComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private formBuilder = inject(UntypedFormBuilder); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); private destroy$ = new Subject(); @ViewChild('passbookContent') passbookContent: ElementRef; /** Account data */ accountNo: string = ''; clientName: string = ''; productName: string = ''; currency: Currency | null = null; status: string = ''; interestRate: number = 0; accountBalance: number = 0; availableBalance: number = 0; activatedOnDate: any = null; /** Transactions */ allTransactions: CurrentAccountTransaction[] = []; filteredTransactions: CurrentAccountTransaction[] = []; /** Date range form */ minDate = new Date(2000, 0, 1); maxDate = new Date(); dateRangeForm: any; isFiltered = false; ngOnInit(): void { this.maxDate = this.settingsService.businessDate; this.route.parent.data.pipe(takeUntil(this.destroy$)).subscribe((data: { currentAccountData: any }) => { const account = data.currentAccountData; this.accountNo = account.accountNo || ''; this.clientName = account.clientName || account.groupName || ''; this.productName = account.currentAccountProductName || ''; this.currency = account.currency || null; this.status = account.status?.value || ''; this.interestRate = account.nominalAnnualInterestRate || 0; this.accountBalance = account.summary?.accountBalance || 0; this.availableBalance = account.summary?.availableBalance || 0; this.activatedOnDate = account.timeline?.activatedOnDate || null; this.allTransactions = this.computeRunningBalances(account.transactions || []); this.filteredTransactions = [...this.allTransactions]; }); this.dateRangeForm = this.formBuilder.group({ fromDate: [''], toDate: [''] }); } private computeRunningBalances(transactions: CurrentAccountTransaction[]): CurrentAccountTransaction[] { let balance = 0; return transactions.map((txn) => { if (txn.runningBalance != null) { balance = txn.runningBalance; } else { const amount = txn.amount || 0; if (this.isDebit(txn.transactionType)) { balance = balance - amount; } else { balance = balance + amount; } txn = { ...txn, runningBalance: balance }; } return txn; }); } isDebit(transactionType: CurrentAccountTransactionType): boolean { return ( transactionType.withdrawal === true || transactionType.feeDeduction === true || transactionType.overdraftInterest === true || transactionType.withholdTax === true ); } filterTransactions(): void { const fromDate = this.dateRangeForm.value.fromDate; const toDate = this.dateRangeForm.value.toDate; this.filteredTransactions = this.allTransactions.filter((txn: CurrentAccountTransaction) => { const txnDate = this.getTransactionDateAsTime(txn.date); if (fromDate && new Date(fromDate).getTime() > txnDate) return false; if (toDate) { const toEnd = new Date(toDate); toEnd.setHours(23, 59, 59, 999); if (toEnd.getTime() < txnDate) return false; } return true; }); this.isFiltered = true; } clearFilter(): void { this.dateRangeForm.reset(); this.filteredTransactions = [...this.allTransactions]; this.isFiltered = false; } private getTransactionDateAsTime(date: any): number { if (Array.isArray(date) && date.length >= 3) { return new Date(date[0], date[1] - 1, date[2]).getTime(); } return new Date(date).getTime(); } printPassbook(): void { const source = this.passbookContent?.nativeElement as HTMLElement; if (!source) return; /** Work on a copy so the on-screen statement is untouched */ const clone = source.cloneNode(true) as HTMLElement; clone.querySelectorAll('.no-print').forEach((el: Element) => el.remove()); /** Material icon fonts are not available in the popup, so swap them for inline SVG */ clone.querySelectorAll('mat-icon, .mat-icon').forEach((el: Element) => { const insideLogo = !!el.closest('.bank-logo-circle'); if (insideLogo) { el.outerHTML = PassbookTabComponent.BANK_LOGO_SVG; } else { el.remove(); } }); const printWindow = window.open('', 'Passbook', 'height=800,width=1100'); if (!printWindow) return; printWindow.document.write(` Current Account Passbook - ${this.accountNo}
${clone.outerHTML}
`); printWindow.document.close(); printWindow.focus(); } /** Material "account_balance" glyph, inlined so the print window needs no icon font */ private static readonly BANK_LOGO_SVG = ` `; /** * Mirrors passbook-tab.component.scss so the printed statement looks like the * on-screen one. Kept in sync manually - component styles are scoped and are * not reachable from the popup window. */ private static readonly PRINT_STYLES = ` @page { size: A4 portrait; margin: 10mm; } * { margin: 0; padding: 0; box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; } body { font-family: 'Segoe UI', Arial, sans-serif; color: #333; background: #fff; } .statement-print-page { padding: 0; } .statement-wrapper { background: #fff; border: 1px solid #d0d7e3; border-radius: 6px; overflow: hidden; } /* Bank header */ .bank-header { background: #1a3a6b; padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 4px solid #c8a951; } .bank-logo-area { display: flex; align-items: center; gap: 12px; } .bank-logo-circle { width: 44px; height: 44px; border-radius: 50%; background: #c8a951; display: flex; align-items: center; justify-content: center; } .bank-logo-svg { fill: #1a3a6b; display: block; } .bank-name-block .bank-title { color: #fff; font-size: 16px; font-weight: 700; letter-spacing: 0.3px; } .bank-name-block .bank-subtitle { color: #c8a951; font-size: 10px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; margin-top: 2px; } .statement-label { text-align: right; } .statement-label .stmt-label-text { color: #c8a951; font-size: 12px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; } .statement-label .stmt-period { color: rgba(255, 255, 255, 0.75); font-size: 10px; margin-top: 3px; } /* Account details band */ .account-details-band { background: #f0f3f9; border-bottom: 1px solid #d0d7e3; padding: 12px 20px; } .account-details-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 24px; } .detail-block .detail-label { font-size: 8.5px; font-weight: 700; color: #7a8aaa; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 2px; } .detail-block .detail-value { font-size: 12px; color: #222; } .detail-block .detail-value.bold { font-weight: 600; } .detail-block .detail-value.mono { font-family: 'Courier New', monospace; letter-spacing: 1px; } .detail-block .detail-value.status-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; background: #eee; color: #555; } .detail-block .detail-value.status-badge.active { background: #d4edda; color: #155724; } /* Balance summary strip */ .balance-summary-strip { display: flex; align-items: stretch; background: #1a3a6b; } .balance-box { flex: 1; padding: 10px 18px; text-align: center; } .balance-box .balance-box-label { font-size: 9px; font-weight: 600; color: rgba(255, 255, 255, 0.65); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; } .balance-box .balance-box-value { font-size: 15px; font-weight: 700; color: #c8a951; } .balance-divider { width: 1px; background: rgba(255, 255, 255, 0.15); margin: 6px 0; } /* Transactions table */ .statement-table { width: 100%; border-collapse: collapse; font-size: 11px; } .statement-table thead { display: table-header-group; } .statement-table tfoot { display: table-row-group; } .statement-table tr { page-break-inside: avoid; break-inside: avoid; } .statement-table thead tr { background: #e8edf5; border-top: 1px solid #d0d7e3; border-bottom: 2px solid #1a3a6b; } .statement-table thead th { padding: 8px 9px; font-size: 10px; font-weight: 700; color: #1a3a6b; text-transform: uppercase; letter-spacing: 0.4px; white-space: nowrap; text-align: left; } .statement-table tbody tr { border-bottom: 1px solid #e8ecf3; } .statement-table tbody tr:nth-child(even) { background: #f4f7fb; } .statement-table tbody tr.reversed-row { opacity: 0.55; text-decoration: line-through; } .statement-table tbody td { padding: 6px 9px; vertical-align: middle; color: #333; } .statement-table tfoot .total-row { background: #e8edf5; border-top: 2px solid #1a3a6b; } .statement-table tfoot .total-row td { padding: 8px 9px; } /* Column widths */ .col-sr { width: 34px; } .col-date { width: 100px; white-space: nowrap; } .col-desc { min-width: 150px; } .col-ref { width: 74px; } .col-amount { width: 100px; } .col-balance { width: 110px; } /* Amount colors */ .debit-amount { color: #c0392b; font-weight: 500; } .credit-amount { color: #1a7a3f; font-weight: 500; } .balance-amount { color: #1a3a6b; font-weight: 600; font-family: 'Courier New', monospace; } .total-label { font-size: 11px; font-weight: 700; color: #1a3a6b; text-transform: uppercase; letter-spacing: 0.5px; } .closing-balance { font-size: 13px; font-weight: 700; color: #1a3a6b; font-family: 'Courier New', monospace; } /* Badges & misc */ .txn-desc { font-weight: 500; } .reversed-badge { display: inline-block; margin-left: 6px; font-size: 8px; font-weight: 700; color: #fff; background: #e74c3c; padding: 1px 5px; border-radius: 3px; letter-spacing: 0.5px; } .text-right { text-align: right; } .text-center { text-align: center; } .text-muted { color: #999; } .mono { font-family: 'Courier New', monospace; font-size: 10px; } /* No data */ .no-data-block { padding: 40px; text-align: center; color: #aaa; font-size: 13px; } /* Footer */ .statement-footer { background: #f0f3f9; border-top: 1px solid #d0d7e3; padding: 10px 20px; text-align: center; } .statement-footer .footer-note { font-size: 9px; color: #888; margin-bottom: 3px; font-style: italic; } .statement-footer .footer-meta { font-size: 9px; color: #aaa; letter-spacing: 0.3px; } `; ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } }