feat: KYC risk profiling, AML flagged transactions bulk review, Risk Review report & Board Resolution register
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="bor-root">
|
||||
<div class="bor-head">
|
||||
<div class="bor-head-left">
|
||||
<fa-icon icon="gavel" class="bor-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="bor-head-t">Board of Resolution Register — Share Capital</h2>
|
||||
<p class="bor-head-s">
|
||||
Every share account with the board resolution that authorised the allotment. Accounts with no resolution on
|
||||
record sort to the top.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="bor-btn bor-btn--xls" type="button" [disabled]="exporting || !filtered.length" (click)="exportExcel()">
|
||||
<fa-icon icon="file-excel"></fa-icon>
|
||||
{{ exporting ? 'Exporting…' : 'Export' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="bor-note">Loading the register…</div>
|
||||
} @else if (loadError) {
|
||||
<div class="bor-note bor-note--warn"><fa-icon icon="exclamation-triangle"></fa-icon> {{ loadError }}</div>
|
||||
}
|
||||
|
||||
@if (!loading && !loadError) {
|
||||
<!-- Counted over the whole register, not the filtered view: it is a standing figure the
|
||||
secretary acts on, and hiding it behind a filter would hide the problem. -->
|
||||
@if (missingResolutionCount > 0) {
|
||||
<div class="bor-note bor-note--warn">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
{{ missingResolutionCount }} share account{{ missingResolutionCount === 1 ? '' : 's' }} carr{{
|
||||
missingResolutionCount === 1 ? 'ies' : 'y'
|
||||
}}
|
||||
no resolution number. Share capital allotted without a board resolution cannot be evidenced to the Registrar.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="bor-kpis">
|
||||
<div class="bor-kpi">
|
||||
<div class="bor-kpi-n">{{ filtered.length | number }}</div>
|
||||
<div class="bor-kpi-l">Share Accounts</div>
|
||||
</div>
|
||||
<div class="bor-kpi bor-kpi--blue">
|
||||
<div class="bor-kpi-n">{{ distinctResolutions | number }}</div>
|
||||
<div class="bor-kpi-l">Resolutions</div>
|
||||
</div>
|
||||
<div class="bor-kpi bor-kpi--green">
|
||||
<div class="bor-kpi-n">{{ totalShares | number }}</div>
|
||||
<div class="bor-kpi-l">Shares Allotted</div>
|
||||
</div>
|
||||
<div class="bor-kpi bor-kpi--amber">
|
||||
<div class="bor-kpi-n">₹{{ totalValue | number: '1.0-0' }}</div>
|
||||
<div class="bor-kpi-l">Share Value</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bor-filters">
|
||||
<div class="bor-filter">
|
||||
<label>Resolution No.</label>
|
||||
<input class="bor-inp" type="text" [(ngModel)]="filterResolution" placeholder="e.g. BOR/2026-27/001" />
|
||||
</div>
|
||||
<div class="bor-filter">
|
||||
<label>Branch</label>
|
||||
<select class="bor-inp" [(ngModel)]="filterBranch">
|
||||
<option value="">All branches</option>
|
||||
@for (b of branches; track b) {
|
||||
<option [value]="b">{{ b }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="bor-filter">
|
||||
<label>Status</label>
|
||||
<select class="bor-inp" [(ngModel)]="filterStatus">
|
||||
<option [ngValue]="''">Any status</option>
|
||||
@for (s of statuses; track s) {
|
||||
<option [ngValue]="s">{{ statusText(s) }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="bor-filter bor-filter--grow">
|
||||
<label>Search</label>
|
||||
<input class="bor-inp" type="text" [(ngModel)]="search" placeholder="Member name, share A/c or CIF…" />
|
||||
</div>
|
||||
<label class="bor-check">
|
||||
<input type="checkbox" [(ngModel)]="missingOnly" />
|
||||
No resolution only
|
||||
</label>
|
||||
<button class="bor-btn" type="button" (click)="resetFilters()"><fa-icon icon="undo"></fa-icon> Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="bor-table-wrap">
|
||||
<table class="bor-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resolution No.</th>
|
||||
<th>Resolution Date</th>
|
||||
<th>Share A/c No.</th>
|
||||
<th>CIF No.</th>
|
||||
<th>Member Name</th>
|
||||
<th>Branch</th>
|
||||
<th>Product</th>
|
||||
<th class="num">Shares</th>
|
||||
<th class="num">Share Value</th>
|
||||
<th class="num">Tranches</th>
|
||||
<th>Status</th>
|
||||
<th>Activated On</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filtered; track r.shareAccountId) {
|
||||
<tr [class.bor-row--missing]="!r.resolutionNumber">
|
||||
<td>
|
||||
@if (r.resolutionNumber) {
|
||||
<strong class="bor-res">{{ r.resolutionNumber }}</strong>
|
||||
} @else {
|
||||
<span class="bor-missing">Not recorded</span>
|
||||
}
|
||||
</td>
|
||||
<td class="mono">{{ r.resolutionDate || '—' }}</td>
|
||||
<td class="mono">{{ r.accountNo }}</td>
|
||||
<td class="mono">{{ r.cifNumber || '—' }}</td>
|
||||
<td>{{ r.clientName }}</td>
|
||||
<td>{{ r.officeName || '—' }}</td>
|
||||
<td>{{ r.productName || '—' }}</td>
|
||||
<td class="num">{{ r.approvedShares | number }}</td>
|
||||
<td class="num strong">{{ r.shareValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ r.trancheCount }}</td>
|
||||
<td><span [class]="statusClass(r.statusEnum)">{{ statusText(r.statusEnum) }}</span></td>
|
||||
<td class="mono">{{ r.activatedDate || '—' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="12" class="bor-empty">No share account matches the filters.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.bor-root {
|
||||
padding: 20px 24px 40px;
|
||||
background: #f8fafc;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.bor-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.bor-head-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bor-head-ic {
|
||||
font-size: 26px;
|
||||
color: #0f4f3c;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bor-head-t {
|
||||
margin: 0 0 3px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.bor-head-s {
|
||||
margin: 0;
|
||||
max-width: 780px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bor-note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
padding: 11px 14px;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 9px;
|
||||
background: #eff6ff;
|
||||
color: #1e3a8a;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
|
||||
fa-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* An allotment with no resolution behind it is the finding, not a detail. */
|
||||
&--warn {
|
||||
border-color: #fde68a;
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
|
||||
fa-icon {
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bor-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bor-kpi {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-left: 4px solid #0f4f3c;
|
||||
border-radius: 11px;
|
||||
padding: 12px 16px;
|
||||
|
||||
&--blue {
|
||||
border-left-color: #2563eb;
|
||||
}
|
||||
|
||||
&--green {
|
||||
border-left-color: #15803d;
|
||||
}
|
||||
|
||||
&--amber {
|
||||
border-left-color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
.bor-kpi-n {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bor-kpi-l {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bor-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.bor-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&--grow {
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
}
|
||||
|
||||
.bor-inp {
|
||||
height: 34px;
|
||||
min-width: 170px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 7px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
background: #fff;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.bor-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
font-size: 12.5px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bor-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--xls {
|
||||
border-color: #15803d;
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #15803d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bor-table-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 11px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.bor-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
background: #f1f5f9;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #475569;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 11px 10px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
font-family: 'DM Mono', monospace;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'DM Mono', monospace;
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
/* The row an auditor is looking for, marked in the table as well as counted above it. */
|
||||
.bor-row--missing {
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.bor-res {
|
||||
color: #0f4f3c;
|
||||
font-family: 'DM Mono', monospace;
|
||||
}
|
||||
|
||||
.bor-missing {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bor-status {
|
||||
display: inline-block;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
|
||||
&--active {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&--submitted,
|
||||
&--approved {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
&--rejected,
|
||||
&--closed {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
.bor-empty {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable, map } from 'rxjs';
|
||||
|
||||
/** Custom Helpers */
|
||||
import { toIsoDate } from '../core/utils/api-date';
|
||||
|
||||
/** One share account and the board resolution that authorised it. */
|
||||
export interface BorRegisterRow {
|
||||
shareAccountId: number;
|
||||
accountNo: string;
|
||||
clientId: number;
|
||||
cifNumber: string;
|
||||
clientName: string;
|
||||
officeId: number | null;
|
||||
officeName: string;
|
||||
productName: string;
|
||||
currencyCode: string;
|
||||
statusEnum: number;
|
||||
approvedShares: number;
|
||||
pendingShares: number;
|
||||
unitPrice: number;
|
||||
shareValue: number;
|
||||
resolutionNumber: string;
|
||||
/** ISO date, or '' where no meeting date was ever recorded against the allotment. */
|
||||
resolutionDate: string;
|
||||
/** How many purchase tranches make up the holding. */
|
||||
trancheCount: number;
|
||||
submittedDate: string;
|
||||
approvedDate: string;
|
||||
activatedDate: string;
|
||||
closedDate: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The BOR register.
|
||||
*
|
||||
* One call returns every account with its resolution. The share allotment report next door fetches
|
||||
* each account separately and stops at two hundred, which is a limit on a register rather than on a
|
||||
* page of one.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BorReportService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getRegister(status?: number, officeId?: number): Observable<BorRegisterRow[]> {
|
||||
let params = new HttpParams();
|
||||
if (status != null) {
|
||||
params = params.set('status', String(status));
|
||||
}
|
||||
if (officeId != null) {
|
||||
params = params.set('officeId', String(officeId));
|
||||
}
|
||||
return this.http
|
||||
.get<any[]>('/shareaccounts/bor-register', { params })
|
||||
.pipe(map((rows) => (rows || []).map((r) => this.toRow(r))));
|
||||
}
|
||||
|
||||
/** Dates arrive as [y, M, d] arrays; flattened to ISO so they sort and filter as strings. */
|
||||
private toRow(r: any): BorRegisterRow {
|
||||
return {
|
||||
shareAccountId: r.shareAccountId,
|
||||
accountNo: r.accountNo || '',
|
||||
clientId: r.clientId,
|
||||
cifNumber: r.cifNumber || '',
|
||||
clientName: r.clientName || '',
|
||||
officeId: r.officeId ?? null,
|
||||
officeName: r.officeName || '',
|
||||
productName: r.productName || '',
|
||||
currencyCode: r.currencyCode || 'INR',
|
||||
statusEnum: Number(r.statusEnum) || 0,
|
||||
approvedShares: Number(r.approvedShares) || 0,
|
||||
pendingShares: Number(r.pendingShares) || 0,
|
||||
unitPrice: Number(r.unitPrice) || 0,
|
||||
shareValue: Number(r.shareValue) || 0,
|
||||
resolutionNumber: (r.resolutionNumber || '').trim(),
|
||||
resolutionDate: toIsoDate(r.resolutionDate),
|
||||
trancheCount: Number(r.trancheCount) || 0,
|
||||
submittedDate: toIsoDate(r.submittedDate),
|
||||
approvedDate: toIsoDate(r.approvedDate),
|
||||
activatedDate: toIsoDate(r.activatedDate),
|
||||
closedDate: toIsoDate(r.closedDate)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user