(`/savingsaccounts/${acc.id}/holdbreakdown`)
+ .pipe(catchError(() => of(null)))
+ .subscribe((hb: any) => {
+ if (token !== this.searchToken) return;
+ this.holdBreakdown = hb
+ ? { cashHold: Number(hb.cashHold) || 0, clearingHold: Number(hb.clearingHold) || 0 }
+ : null;
+ });
+ }
// Track per-row account id/name for multi-row disbursement
if (this.selectedRow?.acNo) {
this.rowAccountIds[this.selectedRow.acNo] = acc.id;
@@ -2566,6 +3288,19 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return this.accountDetails?.savingsAmountOnHold ?? 0;
}
+ /** A manual, staff-placed lien — the same underlying figure "Transfer Hold" already shows
+ * ({@link freezeAmount}); Fineract has one bucket for this, not two, so both fields legitimately
+ * read the same number rather than one of them being invented data. */
+ get cashHold(): number {
+ return this.holdBreakdown?.cashHold ?? this.freezeAmount;
+ }
+
+ /** Money still moving through the clearing process — a cheque/DD deposit not yet realized, or a
+ * presented cheque not yet settled. Only populated for a plain savings account; 0 elsewhere. */
+ get clearingHold(): number {
+ return this.holdBreakdown?.clearingHold ?? 0;
+ }
+
get withdrawableAmount(): number {
const avail = this.accountDetails?.summary?.availableBalance ?? this.currentBalance;
const acTyp = this.selectedRow?.acTyp;
@@ -2872,6 +3607,23 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
});
}
+ /** Only meaningful for a plain savings account — the transactions array a Statement is built from
+ * is embedded on that account type's own detail response (?associations=all), never on
+ * FD/RD/Current/Share/GL. */
+ get canPrintStatement(): boolean {
+ return this.selectedRow?.acTyp === 'SD' && !!this.accountDetails?.id;
+ }
+
+ printStatement(): void {
+ if (!this.canPrintStatement) return;
+ this.dialog.open(AccountStatementDialogComponent, {
+ data: { account: this.accountDetails },
+ width: '820px',
+ maxWidth: '95vw',
+ autoFocus: false
+ });
+ }
+
// ── Same-day compliance indicators (copied from SavingsAccountTransactionsComponent) ──
private computeSameDayNeftRtgs(transactions: any[] | undefined): void {
diff --git a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html
index cf657e658..57a1fd6b3 100644
--- a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html
+++ b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.html
@@ -9,7 +9,12 @@
diff --git a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts
index 2a95702db..684e01f2d 100644
--- a/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts
+++ b/src/app/chequebook/cheque-book-detail/cheque-book-detail.component.ts
@@ -20,6 +20,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
import { ChequeBookService } from '../chequebook.service';
import { LeafDetailDialogComponent } from './leaf-detail-dialog/leaf-detail-dialog.component';
+import { buildChequeIssuanceSlipPdf } from './cheque-issuance-slip-pdf';
type LeafAction = 'stoppayment' | 'revokestoppayment' | 'reportlost' | 'cancelleaf' | 'markused' | 'return';
@@ -85,6 +86,11 @@ export class ChequeBookDetailComponent implements OnInit {
this.dialog.open(LeafDetailDialogComponent, { data: leaf, width: '420px' });
}
+ printIssuanceSlip(): void {
+ const doc = buildChequeIssuanceSlipPdf(this.book);
+ doc.save(`cheque-book-issuance-${this.book.id}.pdf`);
+ }
+
startAction(action: LeafAction): void {
this.activeAction = this.activeAction === action ? null : action;
this.actionForm.reset();
diff --git a/src/app/chequebook/cheque-book-detail/cheque-issuance-slip-pdf.ts b/src/app/chequebook/cheque-book-detail/cheque-issuance-slip-pdf.ts
new file mode 100644
index 000000000..ba1b5fc67
--- /dev/null
+++ b/src/app/chequebook/cheque-book-detail/cheque-issuance-slip-pdf.ts
@@ -0,0 +1,120 @@
+/**
+ * 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/.
+ */
+
+/**
+ * A one-page issuance-slip PDF for a cheque book already issued to an account — the "Cheque Book"
+ * print action was previously a dead menu stub. This is a record of the issuance itself (which
+ * book/series, leaf range, issued-to account, issue date, branch), not a physical cheque leaf.
+ */
+
+import { jsPDF } from 'jspdf';
+
+const BANK_NAME = 'Nexora Co-operative Bank Ltd.';
+const C_TEXT: [number, number, number] = [
+ 15,
+ 23,
+ 42
+];
+const C_MUTED: [number, number, number] = [
+ 100,
+ 116,
+ 139
+];
+const C_BORDER: [number, number, number] = [
+ 226,
+ 232,
+ 240
+];
+const ACCENT: [number, number, number] = [
+ 30,
+ 64,
+ 175
+];
+
+function formatDate(iso: string | null | undefined): string {
+ if (!iso) return '—';
+ const d = new Date(iso);
+ if (isNaN(d.getTime())) return String(iso);
+ return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
+}
+
+export interface ChequeIssuanceSlipBook {
+ id: number | string;
+ accountNo?: string;
+ clientName?: string;
+ accountType?: string;
+ officeName?: string;
+ seriesCode?: string;
+ rangeStart?: string | number;
+ rangeEnd?: string | number;
+ noOfBooks?: number;
+ status?: string;
+ issueDate?: string;
+}
+
+export function buildChequeIssuanceSlipPdf(book: ChequeIssuanceSlipBook): jsPDF {
+ const doc = new jsPDF({ unit: 'mm', format: 'a5' });
+ const pageWidth = doc.internal.pageSize.getWidth();
+ const margin = 12;
+ let y = 16;
+
+ doc.setFillColor(...ACCENT);
+ doc.rect(0, 0, pageWidth, 4, 'F');
+
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(14);
+ doc.setTextColor(...C_TEXT);
+ doc.text(BANK_NAME, margin, y);
+ y += 6;
+
+ doc.setFont('helvetica', 'normal');
+ doc.setFontSize(10);
+ doc.setTextColor(...C_MUTED);
+ doc.text('Cheque Book Issuance Slip', margin, y);
+ y += 8;
+
+ doc.setDrawColor(...C_BORDER);
+ doc.line(margin, y, pageWidth - margin, y);
+ y += 8;
+
+ const row = (label: string, value: string) => {
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(9);
+ doc.setTextColor(...C_MUTED);
+ doc.text(label, margin, y);
+ doc.setFont('helvetica', 'normal');
+ doc.setFontSize(10.5);
+ doc.setTextColor(...C_TEXT);
+ doc.text(value, margin + 45, y);
+ y += 7;
+ };
+
+ row('Issuance ID', String(book.id ?? '—'));
+ row('Account No.', book.accountNo ?? '—');
+ row('Account Holder', book.clientName ?? '—');
+ row('Account Type', book.accountType ?? '—');
+ row('Branch', book.officeName ?? '—');
+ row('Series', book.seriesCode ?? '—');
+ row('Leaf Range', book.rangeStart != null && book.rangeEnd != null ? `${book.rangeStart} – ${book.rangeEnd}` : '—');
+ row('No. of Books', String(book.noOfBooks ?? '—'));
+ row('Status', book.status ?? '—');
+ row('Issue Date', formatDate(book.issueDate));
+
+ y += 6;
+ doc.setDrawColor(...C_BORDER);
+ doc.line(margin, y, pageWidth - margin, y);
+ y += 14;
+
+ doc.setFont('helvetica', 'normal');
+ doc.setFontSize(9);
+ doc.setTextColor(...C_MUTED);
+ doc.text('Issued by: ______________________', margin, y);
+ doc.text('Received by: ______________________', margin, y + 16);
+
+ return doc;
+}
diff --git a/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.html b/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.html
index ee2fe2d98..0efb2fbcf 100644
--- a/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.html
+++ b/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.html
@@ -60,8 +60,8 @@
account_balance
-
LincPay Co-operative Bank Ltd.
-
Account Statement
+
{{ bankName }}
+
{{ bankSubtitle }}
@@ -70,6 +70,25 @@
+
+ @if (branchName || ifscCode || micrCode) {
+
+ @if (branchName) {
+ {{ branchName }} Branch
+ }
+ @if (branchAddress) {
+ {{ branchAddress }}
+ }
+ @if (ifscCode) {
+ IFSC: {{ ifscCode }}
+ }
+ @if (micrCode) {
+ MICR: {{ micrCode }}
+ }
+
+ }
+
diff --git a/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.scss b/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.scss
index 647d16d12..50d73a7f2 100644
--- a/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.scss
+++ b/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.scss
@@ -127,6 +127,18 @@ $header-text: #fff;
}
}
+// ─── Branch Identification Line ──────────────────────────────────────────────
+.branch-line {
+ background: #e8edf5;
+ border-bottom: 1px solid $border-color;
+ padding: 6px 24px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 18px;
+ font-size: 11px;
+ color: #4a5a7a;
+}
+
// ─── Account Details Band ────────────────────────────────────────────────────
.account-details-band {
background: #f0f3f9;
diff --git a/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.ts b/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.ts
index be2127a28..43591b32b 100644
--- a/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.ts
+++ b/src/app/savings/savings-account-view/passbook-tab/passbook-tab.component.ts
@@ -11,11 +11,12 @@ import { Component, OnInit, OnDestroy, inject, ElementRef, ViewChild } from '@an
import { ActivatedRoute } from '@angular/router';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { CurrencyPipe } from '@angular/common';
-import { Subject, takeUntil } from 'rxjs';
+import { Subject, takeUntil, catchError, forkJoin, of } from 'rxjs';
/** Custom Services */
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
+import { OrganizationService } from '../../../organization/organization.service';
import { Currency } from 'app/shared/models/general.model';
import {
SavingsAccountTransaction,
@@ -44,6 +45,7 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
private formBuilder = inject(UntypedFormBuilder);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
+ private organizationService = inject(OrganizationService);
private destroy$ = new Subject();
@@ -60,6 +62,17 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
availableBalance: number = 0;
activatedOnDate: any = null;
+ /** Bank/branch letterhead — real values from Institution Profile + Branch Profile (Organization
+ * module) rather than a hardcoded name, so a printed passbook actually identifies which bank and
+ * branch issued it, the way a real UCB's core banking system does. Falls back to a generic label
+ * if the profile was never filled in, rather than showing a fabricated bank name. */
+ bankName = 'Bank';
+ bankSubtitle = 'Account Statement';
+ branchName = '';
+ branchAddress = '';
+ ifscCode = '';
+ micrCode = '';
+
/** Transactions */
allTransactions: SavingsAccountTransaction[] = [];
filteredTransactions: SavingsAccountTransaction[] = [];
@@ -101,6 +114,31 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
Validators.required
]
});
+
+ this.loadLetterhead();
+ }
+
+ /** Institution Profile is the bank's own single record; Branch Profile is per-office. There is no
+ * office id on the savings account data reaching this tab, and this deployment runs a single
+ * branch, so the first branch profile on record is the correct one here — a genuinely
+ * multi-branch deployment would need the account's own office id threaded through instead. */
+ private loadLetterhead(): void {
+ forkJoin({
+ institution: this.organizationService.getInstitutionProfile().pipe(catchError(() => of(null))),
+ branches: this.organizationService.getBranchProfiles().pipe(catchError(() => of([])))
+ }).subscribe(({ institution, branches }: any) => {
+ if (institution) {
+ this.bankName = institution.passbookHeaderName || institution.bankName || institution.legalName || 'Bank';
+ this.bankSubtitle = institution.licenseNumber ? `Lic. No. ${institution.licenseNumber}` : 'Account Statement';
+ }
+ const branch = Array.isArray(branches) ? branches[0] : null;
+ if (branch) {
+ this.branchName = branch.officeName || '';
+ this.branchAddress = branch.address || '';
+ this.ifscCode = branch.ifscCode || '';
+ this.micrCode = branch.micrCode || '';
+ }
+ });
}
isDebit(transactionType: SavingsAccountTransactionType): boolean {
@@ -269,6 +307,18 @@ export class PassbookTabComponent implements OnInit, OnDestroy {
margin-top: 3px;
}
+ /* Branch identification line */
+ .branch-line {
+ background: #e8edf5;
+ border-bottom: 1px solid #d0d7e3;
+ padding: 5px 20px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 16px;
+ font-size: 9.5px;
+ color: #4a5a7a;
+ }
+
/* Account details band */
.account-details-band {
background: #f0f3f9;
diff --git a/src/environments/.env.ts b/src/environments/.env.ts
index f0cbf38b0..49a49be1e 100644
--- a/src/environments/.env.ts
+++ b/src/environments/.env.ts
@@ -9,8 +9,8 @@
/* tslint:disable */
export default {
'mifos_x': {
- 'version': '260828',
- 'hash': 'ce840d73'
+ 'version': '260829',
+ 'hash': 'fe81d159'
},
'allow_switching_backend_instance': true
};