338 lines
12 KiB
TypeScript
338 lines
12 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';
|
|
import { businessToday } from 'app/core/utils/business-today';
|
|
|
|
/** 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;
|
|
|
|
/**
|
|
* Cash taken at the counter for a Demand Draft.
|
|
*
|
|
* A cash DD asks for a denomination breakdown but does not require one, and this report is built
|
|
* entirely from breakdowns — so that cash was missing from the drawer figure even though the
|
|
* ledger held it and the cashier held the notes. It is counted from the request itself and shown
|
|
* on its own line, because the denomination rows below cannot account for it.
|
|
*/
|
|
ddCashTotal = 0;
|
|
ddCashCount = 0;
|
|
/** Deposits including the DD cash — the figure the drawer is actually checked against. */
|
|
depositTotalWithDd = 0;
|
|
|
|
/**
|
|
* The drawer, opening to closing.
|
|
*
|
|
* `netTotal` above is only the day's counter movement — deposits less withdrawals. It is not what
|
|
* the cashier is holding, because it carries nothing forward from yesterday and ignores vault
|
|
* allocations and inter-cashier legs. The closing balance is the figure that has to match a
|
|
* physical count at the end of the day, so it is reported separately rather than inferred from
|
|
* the counter totals.
|
|
*
|
|
* Null when the server did not scope the report to one cashier: a branch-wide opening balance is
|
|
* the branch day register's business, not this report's.
|
|
*/
|
|
openingBalance: number | null = null;
|
|
closingBalance: number | null = null;
|
|
|
|
/**
|
|
* The day's own cash legs, which is what a cashier reconciles against the drawer.
|
|
*
|
|
* A cashier carries no balance: they draw from the vault in the morning and hand back at close.
|
|
* The cumulative opening/closing figures answer a question nobody at the counter asks, and one
|
|
* carrying years of history answers it with a number that cannot be counted against anything on
|
|
* the desk.
|
|
*/
|
|
todayVaultIn: number | null = null;
|
|
todaySettledOut: number | null = null;
|
|
todayInterCashierIn: number | null = null;
|
|
todayInterCashierOut: number | null = null;
|
|
/** Every leg of the day's cash movement, which is wider than deposits less withdrawals. */
|
|
cashMovement: number | null = null;
|
|
/** Whether the figures above are one cashier's drawer or the whole branch's counter work. */
|
|
scopedToCashier = 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;
|
|
|
|
/**
|
|
* What should be in the drawer at the end of the day, from the day's own legs.
|
|
*
|
|
* Drawn from the vault, plus anything taken over the counter and from another cashier, less what
|
|
* went back out. This is the figure a cashier counts against — it does not carry a balance
|
|
* forward, because a cashier does not.
|
|
*/
|
|
get expectedInDrawer(): number {
|
|
return (
|
|
Number(this.todayVaultIn ?? 0) +
|
|
Number(this.todayInterCashierIn ?? 0) +
|
|
Number(this.depositTotalWithDd ?? 0) -
|
|
Number(this.withdrawalTotal ?? 0) -
|
|
Number(this.todaySettledOut ?? 0) -
|
|
Number(this.todayInterCashierOut ?? 0)
|
|
);
|
|
}
|
|
|
|
/** True when any of the day's own legs carries a figure, so the panel is worth showing. */
|
|
get hasTodayLegs(): boolean {
|
|
return this.todayVaultIn !== null;
|
|
}
|
|
|
|
get noteRows(): any[] {
|
|
return this.rows.filter((r) => r.kind !== 'coin');
|
|
}
|
|
|
|
/**
|
|
* Totals for the notes table, including the un-itemised Demand Draft cash.
|
|
*
|
|
* The DD row carries an amount but no piece count, so pieces and amounts are summed separately
|
|
* rather than one being derived from the other — a denomination times a count would not reach the
|
|
* DD's figure, and adding a fictional count to make it balance would misstate the drawer.
|
|
*/
|
|
get noteDepositCount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.depositCount ?? 0), 0);
|
|
}
|
|
|
|
get noteDepositAmount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.depositAmount ?? 0), 0) + this.ddCashTotal;
|
|
}
|
|
|
|
get noteWithdrawalCount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.withdrawalCount ?? 0), 0);
|
|
}
|
|
|
|
get noteWithdrawalAmount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.withdrawalAmount ?? 0), 0);
|
|
}
|
|
|
|
get noteVaultInCount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.vaultInCount ?? 0), 0);
|
|
}
|
|
|
|
get noteVaultInAmount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.vaultInAmount ?? 0), 0);
|
|
}
|
|
|
|
get noteSettledOutCount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.settledOutCount ?? 0), 0);
|
|
}
|
|
|
|
get noteSettledOutAmount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.settledOutAmount ?? 0), 0);
|
|
}
|
|
|
|
/**
|
|
* Summed from each row's own `netCount`, not re-derived from today's legs — a row's `netCount`
|
|
* already carries forward what a cashier was holding from before today (`openingCount`), for the
|
|
* force-closed-day/late-settlement case. Recomputing here from only today's vaultIn/deposit/
|
|
* withdrawal/settledOut dropped that carry-forward and put the Total row back into negative
|
|
* numbers a cashier had already reconciled.
|
|
*/
|
|
get noteNetCount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.netCount ?? 0), 0);
|
|
}
|
|
|
|
/**
|
|
* What is left in the drawer in notes: everything that came in, less everything that went out —
|
|
* including cash carried forward from before today. See `noteNetCount` for why this sums each
|
|
* row's own `netAmount` rather than re-deriving it from today's legs.
|
|
*/
|
|
get noteNetAmount(): number {
|
|
return this.noteRows.reduce((s, r) => s + Number(r.netAmount ?? 0), 0);
|
|
}
|
|
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 = businessToday();
|
|
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.ddCashTotal = Number(res?.ddCashTotal || 0);
|
|
this.ddCashCount = Number(res?.ddCashCount || 0);
|
|
this.depositTotalWithDd = Number(res?.depositTotalWithDd ?? res?.depositTotal ?? 0);
|
|
this.scopedToCashier = res?.scopedToCashier === true;
|
|
// Sent only for a cashier-scoped call, and read as absent rather than zero when missing —
|
|
// a zero opening balance and no opening balance at all are different statements.
|
|
this.openingBalance = res?.openingBalance == null ? null : Number(res.openingBalance);
|
|
this.closingBalance = res?.closingBalance == null ? null : Number(res.closingBalance);
|
|
this.cashMovement = res?.cashMovement == null ? null : Number(res.cashMovement);
|
|
this.todayVaultIn = res?.todayVaultIn == null ? null : Number(res.todayVaultIn);
|
|
this.todaySettledOut = res?.todaySettledOut == null ? null : Number(res.todaySettledOut);
|
|
this.todayInterCashierIn = res?.todayInterCashierIn == null ? null : Number(res.todayInterCashierIn);
|
|
this.todayInterCashierOut = res?.todayInterCashierOut == null ? null : Number(res.todayInterCashierOut);
|
|
this.loading = false;
|
|
this.loaded = true;
|
|
},
|
|
error: () => {
|
|
this.rows = [];
|
|
this.exchangeRows = [];
|
|
this.depositTotal = 0;
|
|
this.withdrawalTotal = 0;
|
|
this.netTotal = 0;
|
|
this.ddCashTotal = 0;
|
|
this.ddCashCount = 0;
|
|
this.depositTotalWithDd = 0;
|
|
this.scopedToCashier = false;
|
|
this.openingBalance = null;
|
|
this.closingBalance = null;
|
|
this.cashMovement = null;
|
|
this.todayVaultIn = null;
|
|
this.todaySettledOut = null;
|
|
this.todayInterCashierIn = null;
|
|
this.todayInterCashierOut = null;
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
}
|