Files
cbsnexora/src/app/cashier/closing-cash-report/closing-cash-report.component.ts
T
niteshandClaude Sonnet 5 07a4c8b8e3
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
fix(cashier,savings): real bank/branch letterhead on statements, print-preview flow, WITHDRAWN status fixes
Account Statement is no longer a jsPDF file that downloads the moment the
dialog opens. It now shows a live, on-screen preview styled the same way
the working Passbook screen already is (bank header, branch line,
account-details band, balance strip, transaction table), with its own
date-range filter, and only prints via the browser's print dialog on an
explicit click -- exactly the same clone-into-a-popup-window mechanism
Passbook already uses, so "Save as PDF" is the reader's own choice rather
than something forced on open.

Both Passbook and the new Statement preview pull the bank name, license
number, and branch name/address/IFSC/MICR from Institution Profile and
Branch Profile instead of a hardcoded bank name -- a printed passbook or
statement now actually identifies which bank and branch issued it. Cheque
Book's own issuance detail screen gets a matching "Print" action, laying
out a one-page issuance-slip PDF from data already on that screen (which
book/series, leaf range, issued-to account, issue date).

Cash Hold and Clearing Hold on the Transaction Entry balance strip were
both hardcoded 0.00 placeholders; they now read the account's real
hold-category breakdown, and a stray "Freeze Amount" placeholder a few
rows up is wired to the working getter that was already sitting right
next to it.

A clerk withdrawal's own terminal status is WITHDRAWN, not APPROVED --
the Day Book screen had the same status-string gap the backend GL report
did: neither its running CR/DR totals nor its status badge recognized a
completed withdrawal, so real money that had gone out never moved the
totals off zero. The Closing Cash Report's GL Entries panel also never
refreshed for a newly picked date once it had already been opened once --
"Load" hid the panel instead of refreshing it, leaving a stale, wrong-date
table under a header that had already relabelled itself correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 10:38:39 +05:30

186 lines
5.1 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/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { CashierService } from '../cashier.service';
/** Day-wise denomination-wise closing cash collected from deposits. */
@Component({
selector: 'mifosx-closing-cash-report',
templateUrl: './closing-cash-report.component.html',
styleUrls: ['../cashier-shared.scss'],
standalone: true,
imports: [
CommonModule,
FormsModule,
MatIconModule,
MatPaginatorModule
]
})
export class ClosingCashReportComponent implements OnInit {
private service = inject(CashierService);
date = '';
rows: any[] = [];
exchangeRows: any[] = [];
depositTotal = 0;
withdrawalTotal = 0;
netTotal = 0;
loading = false;
loaded = false;
// GL View
showGl = false;
glRows: any[] = [];
glTotalDr = 0;
glTotalCr = 0;
glLoading = false;
glLoaded = false;
// GL Pagination
glTotal = 0;
glOffset = 0;
glPageSize = 20;
glPageSizeOptions = [
10,
20,
50,
100
];
// Filters for GL
tellers: any[] = [];
cashiers: any[] = [];
selectedTellerId: number | null = null;
selectedCashierId: number | null = null;
get noteRows(): any[] {
return this.rows.filter((r) => r.kind !== 'coin');
}
get coinRows(): any[] {
return this.rows.filter((r) => r.kind === 'coin');
}
get exchangeNoteRows(): any[] {
return this.exchangeRows.filter((r) => r.kind !== 'coin');
}
get exchangeCoinRows(): any[] {
return this.exchangeRows.filter((r) => r.kind === 'coin');
}
ngOnInit(): void {
const d = new Date();
this.date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
this.load();
this.service.getTellers().subscribe({ next: (t: any[]) => (this.tellers = t || []), error: () => {} });
}
load(): void {
this.loading = true;
this.service.closingCashReport(this.date).subscribe({
next: (res: any) => {
this.rows = res?.rows || [];
this.exchangeRows = res?.exchangeRows || [];
this.depositTotal = Number(res?.depositTotal || 0);
this.withdrawalTotal = Number(res?.withdrawalTotal || 0);
this.netTotal = Number(res?.netTotal || 0);
this.loading = false;
this.loaded = true;
},
error: () => {
this.rows = [];
this.exchangeRows = [];
this.depositTotal = 0;
this.withdrawalTotal = 0;
this.netTotal = 0;
this.loading = false;
this.loaded = true;
}
});
// GL Entries is scoped to `this.date` too, but was never re-fetched here -- changing the date and
// clicking Load left glLoaded=true from whatever date was open before, so a stale, wrong-date GL
// table stayed on screen under a header that had already relabelled itself to the new date. If
// the panel was open, refresh it for the date just loaded; otherwise just invalidate it so the
// next time it's opened it fetches fresh instead of showing leftover rows.
this.glOffset = 0;
this.glLoaded = false;
if (this.showGl) {
this.loadGl();
} else {
this.glRows = [];
}
}
toggleGl(): void {
this.showGl = !this.showGl;
if (this.showGl && !this.glLoaded) {
this.loadGl();
}
}
onTellerChange(): void {
this.selectedCashierId = null;
this.cashiers = [];
if (this.selectedTellerId) {
this.service.getCashiersForTeller(this.selectedTellerId).subscribe({
next: (res: any) => (this.cashiers = res?.cashiers || res || []),
error: () => {}
});
}
this.glOffset = 0;
this.glLoaded = false;
this.loadGl();
}
onCashierChange(): void {
this.glOffset = 0;
this.glLoaded = false;
this.loadGl();
}
onGlPageChange(event: PageEvent): void {
this.glPageSize = event.pageSize;
this.glOffset = event.pageIndex * event.pageSize;
this.loadGl();
}
loadGl(): void {
this.glLoading = true;
this.service
.getGlEntries(
this.date,
this.selectedCashierId || undefined,
this.selectedTellerId || undefined,
this.glOffset,
this.glPageSize
)
.subscribe({
next: (res: any) => {
this.glRows = res?.pageItems || [];
this.glTotal = Number(res?.totalFilteredRecords || 0);
this.glTotalDr = Number(res?.totalDr || 0);
this.glTotalCr = Number(res?.totalCr || 0);
this.glLoading = false;
this.glLoaded = true;
},
error: () => {
this.glRows = [];
this.glTotal = 0;
this.glTotalDr = 0;
this.glTotalCr = 0;
this.glLoading = false;
this.glLoaded = true;
}
});
}
}