/** * 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(); } }