222 lines
6.3 KiB
TypeScript
222 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 { 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<number, string> = {
|
|
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<void> {
|
|
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;
|
|
}
|
|
}
|
|
}
|