/** * 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 { FormsModule } from '@angular/forms'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { exportWorkbook } from '../cashier/cashier-export'; import { BorRegisterRow, BorReportService } from './bor-report.service'; /** m_share_account.status_enum, as the register labels it. */ const STATUS_LABEL: Record = { 100: 'Submitted', 200: 'Approved', 300: 'Active', 400: 'Rejected', 500: 'Closed', 600: 'Inactive' }; /** * The Board of Resolution register for share capital. * * A co-operative allots shares only under a board resolution, and this is the list the Registrar or * an auditor asks for: every member's holding against the resolution that authorised it. The * account with no resolution recorded is the point of the register, so it sorts to the top and is * counted on its own rather than being left to be noticed. */ @Component({ selector: 'mifosx-bor-report', templateUrl: './bor-report.component.html', styleUrls: ['./bor-report.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, FormsModule ] }) export class BorReportComponent implements OnInit { private borService = inject(BorReportService); readonly statusLabel = STATUS_LABEL; rows: BorRegisterRow[] = []; loading = false; loadError = ''; /** '' = every status. */ filterStatus: number | '' = ''; filterBranch = ''; filterResolution = ''; search = ''; /** Only the accounts with no resolution on file — the ones that need chasing. */ missingOnly = false; exporting = false; ngOnInit(): void { this.load(); } load(): void { this.loading = true; this.loadError = ''; this.borService.getRegister().subscribe({ next: (rows) => { this.loading = false; this.rows = rows; }, error: (err: any) => { this.loading = false; this.rows = []; this.loadError = err?.error?.defaultUserMessage || 'Could not load the BOR register.'; } }); } /** Branches present in the data, so the filter offers only what can actually be selected. */ get branches(): string[] { return [...new Set(this.rows.map((r) => r.officeName).filter(Boolean))].sort(); } get statuses(): number[] { return [...new Set(this.rows.map((r) => r.statusEnum))].sort((a, b) => a - b); } get filtered(): BorRegisterRow[] { const needle = this.search.trim().toLowerCase(); const resolutionNeedle = this.filterResolution.trim().toLowerCase(); return this.rows.filter((r) => { if (this.filterStatus !== '' && r.statusEnum !== this.filterStatus) { return false; } if (this.filterBranch && r.officeName !== this.filterBranch) { return false; } if (this.missingOnly && r.resolutionNumber) { return false; } if (resolutionNeedle && !r.resolutionNumber.toLowerCase().includes(resolutionNeedle)) { return false; } if ( needle && !( r.clientName.toLowerCase().includes(needle) || r.accountNo.toLowerCase().includes(needle) || r.cifNumber.toLowerCase().includes(needle) ) ) { return false; } return true; }); } get totalShares(): number { return this.filtered.reduce((s, r) => s + r.approvedShares, 0); } get totalValue(): number { return this.filtered.reduce((s, r) => s + r.shareValue, 0); } /** How many resolutions the filtered holdings were authorised under. */ get distinctResolutions(): number { return new Set(this.filtered.map((r) => r.resolutionNumber).filter(Boolean)).size; } get missingResolutionCount(): number { return this.rows.filter((r) => !r.resolutionNumber).length; } statusText(statusEnum: number): string { return STATUS_LABEL[statusEnum] || '—'; } statusClass(statusEnum: number): string { return 'bor-status bor-status--' + (STATUS_LABEL[statusEnum] || 'unknown').toLowerCase(); } resetFilters(): void { this.filterStatus = ''; this.filterBranch = ''; this.filterResolution = ''; this.search = ''; this.missingOnly = false; } async exportExcel(): Promise { this.exporting = true; try { const rows = this.filtered; await exportWorkbook( [ { sheetName: 'BOR Register', title: 'Board of Resolution Register — Share Capital', subtitle: `${rows.length} share account(s) · ${this.distinctResolutions} resolution(s) · ${this.totalShares} shares`, headers: [ 'Resolution No.', 'Resolution Date', 'Share A/c No.', 'CIF No.', 'Member Name', 'Branch', 'Product', 'Shares', 'Unit Price', 'Share Value', 'Tranches', 'Status', 'Activated On' ], widths: [ 20, 16, 18, 16, 30, 18, 18, 10, 12, 14, 10, 12, 14 ], rows: rows.map((r) => [ // Named rather than left blank, because an empty cell in a filing reads as an // oversight in the export rather than a gap in the register. r.resolutionNumber || 'NOT RECORDED', r.resolutionDate || '—', r.accountNo, r.cifNumber, r.clientName, r.officeName, r.productName, r.approvedShares, r.unitPrice, r.shareValue, r.trancheCount, this.statusText(r.statusEnum), r.activatedDate || '—' ]) } ], 'BOR-Register' ); } finally { this.exporting = false; } } }