/** * 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 { ActivatedRoute, Router } from '@angular/router'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { Subject, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { SharesService } from '../../shares.service'; import { SHARE_ACCOUNT_STATUS, SHARE_TXN_TYPE, fundingLabel, shareReportCsv, shareReportPrint } from '../share-report.util'; interface StatementLine { transactionId: number; transactionDate: string; typeEnum: number; statusEnum: number; sharesIn: number; sharesOut: number; unitPrice: number; amount: number; chargeAmount: number; amountPaid: number; resolutionNumber: string; meetingDate: string; fundingSource: string; balanceShares: number; balanceValue: number; } /** * Share Account Statement — one member's share account, movement by movement, with the holding each * movement left behind. * *

* The account is chosen first because a statement is always about one account; the search box is * server-side (the same summary endpoint the Share Holder report uses), so it finds a member across * every branch rather than only among accounts already downloaded. * *

* A period narrows which lines are shown but never how the running balance was reached — the server * walks the account's whole life and filters afterwards, so a mid-year statement opens at the * holding the member actually had, not at zero. */ @Component({ selector: 'mifosx-share-account-statement', templateUrl: './share-account-statement.component.html', styleUrls: [ '../../../letter-mgmt/certificates/certificates.shared.scss', '../share-report.shared.scss' ], imports: [ CommonModule, FormsModule, FaIconComponent ] }) export class ShareAccountStatementComponent implements OnInit { private sharesService = inject(SharesService); private route = inject(ActivatedRoute); private router = inject(Router); /** Account picker. */ accountSearch = ''; accountOptions: any[] = []; searchingAccounts = false; selectedAccountId: number | null = null; /** Statement. */ loading = false; loadError = ''; account: any = null; lines: StatementLine[] = []; totals: any = null; fromDate = ''; toDate = ''; private searchTerm$ = new Subject(); ngOnInit(): void { this.searchTerm$ .pipe( debounceTime(350), distinctUntilChanged(), switchMap((term) => { this.searchingAccounts = true; return this.sharesService.getShareAccountSummaries(term).pipe(catchError(() => of([]))); }) ) .subscribe((res: any) => { this.accountOptions = Array.isArray(res) ? res : res?.pageItems || []; this.searchingAccounts = false; }); // Deep-linkable: the account view's own statement button, and any report that drills in here, // can hand over ?shareAccountId= and land on a loaded statement rather than an empty picker. const preselected = this.route.snapshot.queryParamMap.get('shareAccountId'); if (preselected) { this.selectedAccountId = Number(preselected); this.load(); } this.searchTerm$.next(''); } onAccountSearch(term: string): void { this.accountSearch = term; this.searchTerm$.next(term.trim()); } selectAccount(option: any): void { this.selectedAccountId = option.id; this.accountOptions = []; this.accountSearch = `${option.clientName} — ${option.accountNo}`; this.load(); } load(): void { if (!this.selectedAccountId) { return; } this.loading = true; this.loadError = ''; this.sharesService .getShareAccountStatement(this.selectedAccountId, { fromDate: this.fromDate, toDate: this.toDate }) .pipe( catchError((err) => { this.loadError = this.readError(err, 'The share account statement could not be loaded.'); return of(null); }) ) .subscribe((res: any) => { this.account = res?.account || null; this.lines = res?.lines || []; this.totals = res?.totals || null; this.loading = false; }); } clearPeriod(): void { this.fromDate = ''; this.toDate = ''; this.load(); } typeLabel(typeEnum: number): string { return SHARE_TXN_TYPE[typeEnum]?.label || String(typeEnum ?? ''); } typeClass(typeEnum: number): string { return SHARE_TXN_TYPE[typeEnum]?.cls || 'srep-pill--grey'; } statusLabel(statusEnum: number): string { return SHARE_ACCOUNT_STATUS[statusEnum]?.label || String(statusEnum ?? ''); } statusClass(statusEnum: number): string { return SHARE_ACCOUNT_STATUS[statusEnum]?.cls || 'srep-pill--grey'; } funding(source: string): string { return fundingLabel(source); } openAccount(): void { if (this.account) { this.router.navigate([ '/clients', this.account.clientId, 'shares-accounts', this.account.shareAccountId, 'general' ]); } } exportCsv(): void { if (!this.account) { return; } shareReportCsv( `share-statement-${this.account.accountNo}`, [ 'Date', 'Particulars', 'Board Resolution', 'Funded By', 'Shares In', 'Shares Out', 'Unit Price', 'Amount', 'Entry Fee', 'Balance Shares', 'Balance Value' ], this.lines.map((l) => [ l.transactionDate, this.typeLabel(l.typeEnum), l.resolutionNumber, this.funding(l.fundingSource), l.sharesIn || '', l.sharesOut || '', l.unitPrice, l.amount, l.chargeAmount, l.balanceShares, l.balanceValue ]) ); } print(): void { shareReportPrint(); } private readError(err: any, fallback: string): string { return ( err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || err?.error?.developerMessage || fallback ); } }