Files
cbsnexora/src/app/cashier/account-register/account-register.component.ts
T

223 lines
6.3 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 { catchError, of } from 'rxjs';
import { CashierService } from '../cashier.service';
import { businessToday } from 'app/core/utils/business-today';
interface RegisterRow {
accountId: number;
accountNo: string;
section: 'SAVINGS' | 'FD' | 'RD' | 'SHARE';
clientName: string | null;
productName: string | null;
status: string;
openedOn: string | null;
closedOn: string | null;
/** True when this account was opened (or closed) on the very date being viewed. */
openedOnDate: boolean;
closedOnDate: boolean;
/** The account's balance as it stood on the register's date, for every section. */
balanceOnDate: number | null;
currentBalance: number | null;
sharesHeld?: number;
}
/**
* Account register for one date: which accounts opened and closed that day, each account's balance
* as it stood then, and what it holds now.
*/
@Component({
selector: 'mifosx-account-register',
templateUrl: './account-register.component.html',
styleUrls: [
'../cashier-shared.scss',
'../day-book/day-book.component.scss',
'./account-register.component.scss'
],
standalone: true,
imports: [
CommonModule,
FormsModule,
MatIconModule
]
})
export class AccountRegisterComponent implements OnInit {
private service = inject(CashierService);
date = '';
section: 'ALL' | 'SAVINGS' | 'FD' | 'RD' | 'SHARE' = 'ALL';
search = '';
/** Narrows to just the accounts that opened or closed on the chosen date. */
movementOnly = false;
rows: RegisterRow[] = [];
openedCount = 0;
closedCount = 0;
totalBalanceOnDate = 0;
totalCurrentBalance = 0;
loading = false;
loaded = false;
errorMsg = '';
readonly sections = [
{ value: 'ALL', label: 'All accounts' },
{ value: 'SAVINGS', label: 'Savings' },
{ value: 'FD', label: 'Fixed Deposit' },
{ value: 'RD', label: 'Recurring Deposit' },
{ value: 'SHARE', label: 'Share' }
];
ngOnInit(): void {
this.date = this.today();
this.load();
}
/** Local date — toISOString() would shift the day in any timezone behind UTC. */
private today(): string {
const d = businessToday();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
load(): void {
if (!this.date) return;
this.loading = true;
this.errorMsg = '';
this.service
.accountRegister(this.date, this.section)
.pipe(
catchError((err: any) => {
this.errorMsg =
err?.error?.errors?.[0]?.defaultUserMessage ??
err?.error?.message ??
'Could not load the account register.';
return of(null);
})
)
.subscribe((res: any) => {
this.loading = false;
this.loaded = true;
this.rows = res?.accounts ?? [];
this.openedCount = Number(res?.openedCount ?? 0);
this.closedCount = Number(res?.closedCount ?? 0);
this.totalBalanceOnDate = Number(res?.totalBalanceOnDate ?? 0);
this.totalCurrentBalance = Number(res?.totalCurrentBalance ?? 0);
});
}
shiftDate(days: number): void {
const [
y,
m,
d
] = this.date.split('-').map(Number);
const dt = new Date(y, m - 1, d + days);
this.date = `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
this.load();
}
goToday(): void {
this.date = this.today();
this.load();
}
/** Free-text narrowing over the rows already fetched — no round trip per keystroke. */
get filteredRows(): RegisterRow[] {
const base = this.movementOnly ? this.rows.filter((r) => r.openedOnDate || r.closedOnDate) : this.rows;
const q = this.search.trim().toLowerCase();
if (!q) return base;
return base.filter((r) =>
[
r.accountNo,
r.clientName,
r.productName,
r.status,
r.section
]
.filter(Boolean)
.some((v) => String(v).toLowerCase().includes(q))
);
}
/** Totals for what is on screen, so a filter or search still reconciles. */
get shownBalanceOnDate(): number {
return this.filteredRows.reduce((s, r) => s + Number(r.balanceOnDate ?? 0), 0);
}
get shownCurrentBalance(): number {
return this.filteredRows.reduce((s, r) => s + Number(r.currentBalance ?? 0), 0);
}
statusClass(status: string): string {
switch (status) {
case 'ACTIVE':
return 'db-badge db-badge--ok';
case 'CLOSED':
case 'PREMATURELY CLOSED':
case 'REJECTED':
case 'WITHDRAWN':
return 'db-badge db-badge--rejected';
case 'SUBMITTED':
case 'APPROVED':
return 'db-badge db-badge--pending';
default:
return 'db-badge';
}
}
exportCsv(): void {
const header = [
'Account No',
'Section',
'Client',
'Product',
'Status',
'Opened On',
'Closed On',
`Balance on ${this.date}`,
'Current Balance'
];
const lines = this.filteredRows.map((r) =>
[
r.accountNo,
r.section,
r.clientName ?? '',
r.productName ?? '',
r.status,
r.openedOn ?? '',
r.closedOn ?? '',
r.balanceOnDate == null ? '' : String(r.balanceOnDate),
String(r.currentBalance ?? 0)
]
// Quote every field and double embedded quotes: client names routinely contain commas.
.map((v) => `"${String(v).replace(/"/g, '""')}"`)
.join(',')
);
const csv = [
header.join(','),
...lines
].join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `account-register-${this.date}.csv`;
a.click();
URL.revokeObjectURL(url);
}
/** Hands the page to the browser's print dialog; print styles drop the toolbar and filters. */
printRegister(): void {
window.print();
}
}