Files
cbsnexora/src/app/shares/shares-account-view/passbook-tab/passbook-tab.component.ts
T
Sonali Likhitkar 0d145c72a6
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
printing modification are done
2026-08-27 17:38:06 +05:30

432 lines
13 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, ElementRef, ViewChild, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { CurrencyPipe } from '@angular/common';
import { MatIcon } from '@angular/material/icon';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { isSettledShareTransaction } from 'app/shares/share-transactions.util';
/**
* Share Account Passbook — a printable, chronological (oldest-first) ledger of every
* Purchase/Redeem entry, mirroring PassbookTabComponent (Savings) but for share holdings:
* shares purchased/redeemed instead of amount deposited/withdrawn, plus the Board Resolution
* Number / Date of Meeting on redeem rows.
*/
@Component({
selector: 'mifosx-shares-passbook-tab',
templateUrl: './passbook-tab.component.html',
styleUrls: ['./passbook-tab.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatIcon,
DateFormatPipe,
CurrencyPipe
]
})
export class SharesPassbookTabComponent implements OnInit {
private route = inject(ActivatedRoute);
private formBuilder = inject(UntypedFormBuilder);
@ViewChild('passbookContent') passbookContent: ElementRef;
/** Account data */
accountNo = '';
clientName = '';
productName = '';
currency: any = null;
status = '';
activatedOnDate: any = null;
resolutionNumber = '';
resolutionDate: any = null;
savingsAccountNumber = '';
totalApprovedShares = 0;
currentMarketPrice = 0;
/** Transactions */
allTransactions: any[] = [];
filteredTransactions: any[] = [];
/** Date range form */
minDate = new Date(2000, 0, 1);
maxDate = new Date();
dateRangeForm: any;
isFiltered = false;
get currentValue(): number {
return this.totalApprovedShares * this.currentMarketPrice;
}
ngOnInit(): void {
this.route.parent.data.subscribe((data: { sharesAccountData: any }) => {
const account = data.sharesAccountData;
this.accountNo = account.accountNo || '';
this.clientName = account.clientName || '';
this.productName = account.productName || '';
this.currency = account.currency || null;
this.status = account.status?.value || '';
this.activatedOnDate = account.timeline?.activatedDate || null;
this.resolutionNumber = account.resolutionNumber || '';
this.resolutionDate = account.resolutionDate || null;
this.savingsAccountNumber = account.savingsAccountNumber || '';
this.totalApprovedShares = account.summary?.totalApprovedShares || 0;
this.currentMarketPrice = account.currentMarketPrice || 0;
const transactions = (account.purchasedShares || []).filter(isSettledShareTransaction);
this.allTransactions = this.recalculateRunningBalances(this.sortAscending(transactions));
this.filteredTransactions = [...this.allTransactions];
});
this.dateRangeForm = this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
]
});
}
isRedeem(transaction: any): boolean {
return transaction.type?.value === 'Redeem';
}
filterTransactions(): void {
const fromDate = this.dateRangeForm.value.fromDate;
const toDate = this.dateRangeForm.value.toDate;
if (!fromDate || !toDate) return;
const from = new Date(fromDate).getTime();
const to = new Date(toDate).getTime();
this.filteredTransactions = this.allTransactions.filter((txn: any) => {
const txnDate = this.getTransactionDateAsTime(txn.purchasedDate);
return txnDate >= from && txnDate <= to;
});
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 passbook 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 = SharesPassbookTabComponent.BANK_LOGO_SVG;
} else {
el.remove();
}
});
const printWindow = window.open('', 'SharePassbook', 'height=800,width=1200');
if (!printWindow) return;
printWindow.document.write(`
<html>
<head>
<meta charset="utf-8" />
<title>Share Account Passbook - ${this.accountNo}</title>
<style>${SharesPassbookTabComponent.PRINT_STYLES}</style>
</head>
<body>
<div class="statement-print-page">${clone.outerHTML}</div>
<script>
window.onafterprint = function () { window.close(); };
window.addEventListener('load', function () { window.focus(); window.print(); });
</script>
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
}
/** Material "account_balance" glyph, inlined so the print window needs no icon font */
private static readonly BANK_LOGO_SVG = `
<svg class="bank-logo-svg" viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">
<path d="M4 10h3v7H4zM10.5 10h3v7h-3zM2 19h20v3H2zM17 10h3v7h-3zM12 1L2 6v2h20V6z" />
</svg>`;
/**
* Mirrors passbook-tab.component.scss so the printed passbook looks like the
* on-screen one. Kept in sync manually - component styles are scoped and are
* not reachable from the popup window. Landscape, because the share ledger
* carries nine columns including the Resolution No. / Meeting column.
*/
private static readonly PRINT_STYLES = `
@page { size: A4 landscape; margin: 8mm; }
* { 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(4, 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; }
/* Share 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; }
/* Ledger table - no horizontal scroll on paper, the landscape page holds every column */
.statement-table-scroll { width: 100%; overflow: visible; }
.statement-table { width: 100%; border-collapse: collapse; font-size: 10.5px; }
.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: 9.5px;
font-weight: 700;
color: #1a3a6b;
text-transform: uppercase;
letter-spacing: 0.4px;
white-space: nowrap;
}
.statement-table tbody tr { border-bottom: 1px solid #e8ecf3; }
.statement-table tbody tr:nth-child(even) { background: #f4f7fb; }
.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: 32px; }
.col-date { width: 96px; white-space: nowrap; }
.col-desc { width: auto; min-width: 140px; }
.col-ref { width: 70px; white-space: nowrap; }
.col-amount { width: 108px; white-space: nowrap; }
.col-balance { width: 96px; white-space: nowrap; }
.col-resolution { width: 170px; white-space: nowrap; padding-left: 14px; }
/* Share movement 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: 10.5px;
font-weight: 700;
color: #1a3a6b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.closing-balance {
font-size: 12.5px;
font-weight: 700;
color: #1a3a6b;
font-family: 'Courier New', monospace;
}
/* Misc */
.txn-desc { font-weight: 500; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.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; }
`;
private recalculateRunningBalances(transactions: any[]): any[] {
let balance = 0;
return transactions.map((txn) => {
const shares = Number(txn.numberOfShares) || 0;
balance += this.isRedeem(txn) ? -shares : shares;
return { ...txn, runningBalance: balance };
});
}
private sortAscending(transactions: any[]): any[] {
return [...transactions].sort((a, b) => {
const dateA = this.getTransactionDateAsTime(a.purchasedDate);
const dateB = this.getTransactionDateAsTime(b.purchasedDate);
if (dateA !== dateB) return dateA - dateB;
return (a.id as number) - (b.id as number);
});
}
}