223 lines
6.3 KiB
TypeScript
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();
|
|
}
|
|
}
|