Compare commits
10
Commits
uate
...
4261f9659e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4261f9659e | ||
|
|
63ac900e42 | ||
|
|
fd28788d23 | ||
|
|
b9b37397fe | ||
|
|
f2525080c8 | ||
|
|
43e3c546a7 | ||
|
|
b68b25f789 | ||
|
|
1dc378c2dd | ||
|
|
0bf337b962 | ||
|
|
1daa12a117 |
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* AML transaction-monitoring API: live alerts raised by the rule engine, the
|
||||
* two-tier AML Officer / Principal Officer review workflow, STR cases (with
|
||||
* their 7-working-day FIU filing deadline), monthly CTR reports, and a
|
||||
* client's risk-category change history.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AmlService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
// ═══ Alerts (AML Officer) ═══
|
||||
|
||||
getAlerts(status?: string): Observable<any[]> {
|
||||
const params = status ? new HttpParams().set('status', status) : undefined;
|
||||
return this.http.get<any[]>('/aml/alerts', { params });
|
||||
}
|
||||
|
||||
/** disposition: 'WHITELIST' | 'ESCALATE' */
|
||||
reviewAlert(alertId: number, disposition: 'WHITELIST' | 'ESCALATE', notes: string): Observable<any> {
|
||||
return this.http.post(`/aml/alerts/${alertId}/review`, { disposition, notes });
|
||||
}
|
||||
|
||||
// ═══ STR cases (Principal Officer) ═══
|
||||
|
||||
getStrCases(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/aml/str-cases');
|
||||
}
|
||||
|
||||
getStrCase(strCaseId: number): Observable<any> {
|
||||
return this.http.get(`/aml/str-cases/${strCaseId}`);
|
||||
}
|
||||
|
||||
getWeeklyDigest(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/aml/str-cases/weekly-digest');
|
||||
}
|
||||
|
||||
assignPrincipalOfficer(strCaseId: number, principalOfficerUserId: number): Observable<any> {
|
||||
return this.http.post(`/aml/str-cases/${strCaseId}/assign`, { principalOfficerUserId });
|
||||
}
|
||||
|
||||
/** conclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS' */
|
||||
concludeStrCase(
|
||||
strCaseId: number,
|
||||
conclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS',
|
||||
investigationNotes: string,
|
||||
sourceOfFundsNotes: string
|
||||
): Observable<any> {
|
||||
return this.http.post(`/aml/str-cases/${strCaseId}/conclude`, {
|
||||
conclusion,
|
||||
investigationNotes,
|
||||
sourceOfFundsNotes
|
||||
});
|
||||
}
|
||||
|
||||
fileStr(strCaseId: number, referenceNo: string): Observable<any> {
|
||||
return this.http.post(`/aml/str-cases/${strCaseId}/file`, { referenceNo });
|
||||
}
|
||||
|
||||
// ═══ CTR reports (mechanical, threshold-based) ═══
|
||||
|
||||
getCtrReports(): Observable<any[]> {
|
||||
return this.http.get<any[]>('/aml/ctr-reports');
|
||||
}
|
||||
|
||||
/** month format: yyyy-MM */
|
||||
generateCtrReport(month: string): Observable<any> {
|
||||
return this.http.post('/aml/ctr-reports/generate', { month });
|
||||
}
|
||||
|
||||
// ═══ Client risk-category history ═══
|
||||
|
||||
getRiskCategoryHistory(clientId: number): Observable<any[]> {
|
||||
return this.http.get<any[]>(`/clients/${clientId}/risk-category-history`);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,8 @@ import { DeafModule } from './deaf/deaf.module';
|
||||
import { DicgcModule } from './dicgc/dicgc.module';
|
||||
import { FiuModule } from './fiu/fiu.module';
|
||||
import { SftModule } from './sft/sft.module';
|
||||
import { InvestmentModule } from './investment/investment.module';
|
||||
import { NpaModule } from './npa/npa.module';
|
||||
import { DigitalModule } from './digital/digital.module';
|
||||
import { LetterMgmtModule } from './letter-mgmt/letter-mgmt.module';
|
||||
import { AccountsModule } from './accounts/accounts.module';
|
||||
@@ -172,6 +174,8 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
DicgcModule,
|
||||
FiuModule,
|
||||
SftModule,
|
||||
InvestmentModule,
|
||||
NpaModule,
|
||||
DigitalModule,
|
||||
LetterMgmtModule,
|
||||
AccountsModule,
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">SUB LEDGER · TRANSACTIONS · {{ scopeName | uppercase }}</div>
|
||||
<h1 class="pg-title">Cash / Clearing / Transfer - Summary Report</h1>
|
||||
<p class="pg-sub">
|
||||
Opening, credit, debit and closing position across Cash, Transfer, Clearing and Digital channels as on
|
||||
{{ asOnDate }}.
|
||||
</p>
|
||||
</div>
|
||||
<div class="pg-toolbar">
|
||||
@if (isHeadOffice) {
|
||||
<label class="pg-field">
|
||||
<span>Branch</span>
|
||||
<select class="pg-select" [ngModel]="selectedBranchCode" (ngModelChange)="setBranch($event)">
|
||||
<option value="ALL">All Branches (Consolidated)</option>
|
||||
@for (b of branches; track b.code) {
|
||||
<option [value]="b.code">{{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
<button type="button" class="pg-btn" [class.pg-btn--active]="showDetails" (click)="toggleDetails()">
|
||||
{{ showDetails ? 'Summary View' : 'Transaction Details' }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-grand-summary">
|
||||
<div class="cctr-grand-summary__item">
|
||||
<span class="cctr-grand-summary__label">Opening</span>
|
||||
<span class="cctr-grand-summary__value">₹{{ grandOpening | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="cctr-grand-summary__item">
|
||||
<span class="cctr-grand-summary__label">Credit</span>
|
||||
<span class="cctr-grand-summary__value col-in">₹{{ grandCredit | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="cctr-grand-summary__item">
|
||||
<span class="cctr-grand-summary__label">Debit</span>
|
||||
<span class="cctr-grand-summary__value col-out">₹{{ grandDebit | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="cctr-grand-summary__item">
|
||||
<span class="cctr-grand-summary__label">Closing</span>
|
||||
<span class="cctr-grand-summary__value">₹{{ grandClosing | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isHeadOffice && selectedBranchCode === 'ALL') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Branch-wise Break-up — click a branch to drill in</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Branch</th>
|
||||
<th class="num">Opening</th>
|
||||
<th class="num col-in">Credit</th>
|
||||
<th class="num col-out">Debit</th>
|
||||
<th class="num col-net">Closing</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (b of branchBreakup; track b.code) {
|
||||
<tr class="cctr-branch-row" (click)="setBranch(b.code)">
|
||||
<td>{{ b.name }}</td>
|
||||
<td class="num">₹{{ b.opening | number: '1.2-2' }}</td>
|
||||
<td class="num col-in">₹{{ b.credit | number: '1.2-2' }}</td>
|
||||
<td class="num col-out">₹{{ b.debit | number: '1.2-2' }}</td>
|
||||
<td class="num col-net">₹{{ b.closing | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td>All Branches</td>
|
||||
<td class="num">₹{{ grandOpening | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ grandCredit | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ grandDebit | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ grandClosing | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'cash'"
|
||||
(click)="setTab('cash')"
|
||||
>
|
||||
Cash
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'transfer'"
|
||||
(click)="setTab('transfer')"
|
||||
>
|
||||
Transfer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'clearing'"
|
||||
(click)="setTab('clearing')"
|
||||
>
|
||||
Clearing
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'digital'"
|
||||
(click)="setTab('digital')"
|
||||
>
|
||||
Digital
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary (Opening | Credit | Debit | Closing) -->
|
||||
@if (!showDetails) {
|
||||
<div class="pg-card">
|
||||
@if (activeTab === 'digital') {
|
||||
<div class="pos-section-label">RTGS · NEFT · UPI · ATM · NACH — Overall Position</div>
|
||||
}
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@if (activeTab !== 'digital') {
|
||||
<th>GL Code</th>
|
||||
}
|
||||
<th>Particulars</th>
|
||||
<th class="num">Opening</th>
|
||||
<th class="num col-in">Credit</th>
|
||||
<th class="num col-out">Debit</th>
|
||||
<th class="num col-net">Closing</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of rowsForActiveTab; track r.code) {
|
||||
<tr>
|
||||
@if (activeTab !== 'digital') {
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.code }}</span>
|
||||
</td>
|
||||
}
|
||||
<td>{{ r.name }}</td>
|
||||
<td class="num">₹{{ r.opening | number: '1.2-2' }}</td>
|
||||
<td class="num col-in">₹{{ rowCredit(r, txnsForActiveTab) | number: '1.2-2' }}</td>
|
||||
<td class="num col-out">₹{{ rowDebit(r, txnsForActiveTab) | number: '1.2-2' }}</td>
|
||||
<td class="num col-net">₹{{ closing(r, txnsForActiveTab) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td [attr.colspan]="activeTab !== 'digital' ? 2 : 1">Total</td>
|
||||
<td class="num">₹{{ total(rowsForActiveTab, txnsForActiveTab, 'opening') | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ total(rowsForActiveTab, txnsForActiveTab, 'credit') | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ total(rowsForActiveTab, txnsForActiveTab, 'debit') | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ totalClosing(rowsForActiveTab, txnsForActiveTab) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Transaction Details (all txns for the selected category, optionally filtered by GL/channel) -->
|
||||
@if (showDetails) {
|
||||
@if (rowsForActiveTab.length > 1) {
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="glFilter === null"
|
||||
(click)="setGlFilter(null)"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
@for (r of rowsForActiveTab; track r.code) {
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="glFilter === r.code"
|
||||
(click)="setGlFilter(r.code)"
|
||||
>
|
||||
{{ filterLabel(r) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
{{
|
||||
activeTab === 'cash'
|
||||
? 'Cash'
|
||||
: activeTab === 'transfer'
|
||||
? 'Transfer'
|
||||
: activeTab === 'clearing'
|
||||
? 'Clearing'
|
||||
: 'Digital'
|
||||
}}
|
||||
— Transaction Details
|
||||
@if (glFilter !== null) {
|
||||
· {{ glFilter }}
|
||||
}
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher / Ref No</th>
|
||||
<th>Account No</th>
|
||||
<th>Account Name / Reference</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num col-out">Debit</th>
|
||||
<th class="num col-in">Credit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of filteredTxns; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ asOnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ display(t.voucherNo) }}</span>
|
||||
</td>
|
||||
<td>{{ display(t.accountNo) }}</td>
|
||||
<td>{{ t.accountName }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num col-out">
|
||||
@if (t.debit > 0) {
|
||||
₹{{ t.debit | number: '1.2-2' }}
|
||||
}
|
||||
</td>
|
||||
<td class="num col-in">
|
||||
@if (t.credit > 0) {
|
||||
₹{{ t.credit | number: '1.2-2' }}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="pg-empty">No transactions for this filter.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="5">Total</td>
|
||||
<td class="num">₹{{ txnTotal(filteredTxns, 'debit') | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ txnTotal(filteredTxns, 'credit') | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.cctr-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cctr-total-row {
|
||||
td {
|
||||
background: #f1f5f9;
|
||||
font-weight: 800;
|
||||
border-top: 2px solid #cbd5e1;
|
||||
border-bottom: none;
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
.cctr-grand-summary {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
.cctr-subtext {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cctr-branch-row {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover td {
|
||||
background: #eff6ff;
|
||||
}
|
||||
}
|
||||
+1569
File diff suppressed because it is too large
Load Diff
@@ -179,6 +179,17 @@ const routes: Routes = [
|
||||
loadComponent: () =>
|
||||
import('./my-deposit-requests/my-deposit-requests.component').then((m) => m.MyDepositRequestsComponent),
|
||||
data: { title: 'My Deposit Requests', breadcrumb: 'My Deposit Requests' }
|
||||
},
|
||||
{
|
||||
path: 'cash-clearing-transfer-summary',
|
||||
loadComponent: () =>
|
||||
import('./cash-clearing-transfer-summary/cash-clearing-transfer-summary.component').then(
|
||||
(m) => m.CashClearingTransferSummaryComponent
|
||||
),
|
||||
data: {
|
||||
title: 'Cash / Clearing / Transfer - Summary Report',
|
||||
breadcrumb: 'Cash / Clearing / Transfer - Summary Report'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
* have meaningful variety out of the box.
|
||||
*/
|
||||
export const SAMPLE_CKYC_FILE = `10|24401|IN4711|MH|30|31-08-2020|V1.1|||||
|
||||
20|1|01|01|||||||||||01|||01|10078|MR|RAVI|KUMAR|SHARMA|||||||01|MR|RAJESH|KUMAR|SHARMA||MRS|SUNITA|DEVI|||M|02|IN|B-01|10-08-1985|||||||||01|02|||||02|FLAT 402, SUNRISE TOWERS|MAROL ANDHERI EAST|MUMBAI|MUMBAI|MUMBAI|MH|IN|400069|01||Y|02|FLAT 402, SUNRISE TOWERS|MAROL ANDHERI EAST|MUMBAI|MUMBAI|MUMBAI|MH|IN|400069|01||||||||||||||||9892011001|||||31-08-2020|MUMBAI MAIN|31-08-2020|01|ANJALI MEHTA|ASSISTANT MANAGER|HEAD OFFICE|4|BHARAT URBAN BANK|IN4711|1|0||0|4||||||
|
||||
20|1|01|01|||||||||||01|||01|10078|MR|RAVI|KUMAR|SHARMA|||||||01|MR|RAJESH|KUMAR|SHARMA||MRS|SUNITA|DEVI|||M|02|IN|B-01|10-08-1985|||||||||01|02|||||02|FLAT 402, SUNRISE TOWERS|MAROL ANDHERI EAST|MUMBAI|MUMBAI|MUMBAI|MH|IN|400069|01||Y|02|FLAT 402, SUNRISE TOWERS|MAROL ANDHERI EAST|MUMBAI|MUMBAI|MUMBAI|MH|IN|400069|01||||||||||||||||9892011001|||||31-08-2020|MUMBAI MAIN|31-08-2020|01|ANJALI MEHTA|ASSISTANT MANAGER|HEAD OFFICE|4|CBSNEXORA BANK|IN4711|1|0||0|4||||||
|
||||
30|1|C|AAQPS8560J||01|02|||||
|
||||
70|1|10078_Sign.jpeg|09|01|01||||
|
||||
70|1|10078_Photo.jpeg|02|01|01||||
|
||||
70|1|10078_AP.jpg|04|01|01||||
|
||||
20|2|01|01|||||||||||01|||01|10079|MRS|PRIYA||PATEL|||||||01|MR|HARISH|PATEL|||MRS|MEENA|PATEL|||F|01|IN|O-04|22-03-1990|||||||||01|02|||||02|B-12, SHIVAJI NAGAR|FC ROAD|PUNE|PUNE|PUNE|MH|IN|411005|01||Y|02|B-12, SHIVAJI NAGAR|FC ROAD|PUNE|PUNE|PUNE|MH|IN|411005|01||||||||||||||||9822055002|||||31-08-2020|PUNE FC ROAD|31-08-2020|01|VIKRAM SINGH|BRANCH MANAGER|HEAD OFFICE|4|BHARAT URBAN BANK|IN4711|1|0||0|3||||||
|
||||
20|2|01|01|||||||||||01|||01|10079|MRS|PRIYA||PATEL|||||||01|MR|HARISH|PATEL|||MRS|MEENA|PATEL|||F|01|IN|O-04|22-03-1990|||||||||01|02|||||02|B-12, SHIVAJI NAGAR|FC ROAD|PUNE|PUNE|PUNE|MH|IN|411005|01||Y|02|B-12, SHIVAJI NAGAR|FC ROAD|PUNE|PUNE|PUNE|MH|IN|411005|01||||||||||||||||9822055002|||||31-08-2020|PUNE FC ROAD|31-08-2020|01|VIKRAM SINGH|BRANCH MANAGER|HEAD OFFICE|4|CBSNEXORA BANK|IN4711|1|0||0|3||||||
|
||||
30|2|E|XXXXXXXX2210||01|02|||||
|
||||
70|2|10079_Sign.jpeg|09|01|01||||
|
||||
70|2|10079_Photo.jpeg|02|01|01||||
|
||||
20|3|01|01|||||||||||01|||01|10080|MR|ARJUN||DESHMUKH|||||||01|MR|VIJAY|DESHMUKH|||MRS|REKHA|DESHMUKH|||M|01|IN|B-01|15-01-1978|||||||||01|02|||||02|HOUSE NO 7, DHARAMPETH|RAMDASPETH|NAGPUR|NAGPUR|NAGPUR|MH|IN|440010|01||Y|02|HOUSE NO 7, DHARAMPETH|RAMDASPETH|NAGPUR|NAGPUR|NAGPUR|MH|IN|440010|01||||||||||||||||9823033003|||||30-08-2020|NAGPUR DHARAMPETH|30-08-2020|01|RAHUL JOSHI|ASSISTANT MANAGER|HEAD OFFICE|4|BHARAT URBAN BANK|IN4711|1|0||0|3||||||
|
||||
20|3|01|01|||||||||||01|||01|10080|MR|ARJUN||DESHMUKH|||||||01|MR|VIJAY|DESHMUKH|||MRS|REKHA|DESHMUKH|||M|01|IN|B-01|15-01-1978|||||||||01|02|||||02|HOUSE NO 7, DHARAMPETH|RAMDASPETH|NAGPUR|NAGPUR|NAGPUR|MH|IN|440010|01||Y|02|HOUSE NO 7, DHARAMPETH|RAMDASPETH|NAGPUR|NAGPUR|NAGPUR|MH|IN|440010|01||||||||||||||||9823033003|||||30-08-2020|NAGPUR DHARAMPETH|30-08-2020|01|RAHUL JOSHI|ASSISTANT MANAGER|HEAD OFFICE|4|CBSNEXORA BANK|IN4711|1|0||0|3||||||
|
||||
30|3|C|AGTPD1144K||01|02|||||
|
||||
70|3|10080_Sign.jpeg|09|01|01||||
|
||||
70|3|10080_AP.jpg|04|01|01||||
|
||||
|
||||
+97
-4
@@ -533,6 +533,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
screeningLogId: number | null = null;
|
||||
/** Snapshot of the name that was last verified — used to invalidate verification when name changes */
|
||||
private verifiedName: string = '';
|
||||
/** True while prefillForEdit() is patching historical monthly/annual values — see linkMonthlyAnnual(). */
|
||||
private suppressMonthlyAnnualLink = false;
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder
|
||||
@@ -709,6 +711,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
const data = this.clientData;
|
||||
this.loadExistingSeniorPwdDocuments();
|
||||
|
||||
// Loading an existing client's independently-saved monthly/annual values must not run
|
||||
// through the auto-calc in linkMonthlyAnnual() — patching monthlyIncome then annualIncome
|
||||
// (or vice versa) would otherwise have each overwrite the other via its valueChanges,
|
||||
// instead of keeping the exact historical values as saved.
|
||||
this.suppressMonthlyAnnualLink = true;
|
||||
|
||||
// Patch all base controls that exist on the form
|
||||
this.createClientForm.patchValue({
|
||||
officeId: data.officeId,
|
||||
@@ -743,6 +751,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
? new Date(data.timeline.submittedOnDate)
|
||||
: this.settingsService.businessDate
|
||||
});
|
||||
this.suppressMonthlyAnnualLink = false;
|
||||
|
||||
// Patch name fields (already created by buildDependencies via legalFormId subscription)
|
||||
const legalFormId = data.legalForm?.id || LegalFormId.PERSON;
|
||||
@@ -954,6 +963,43 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
this.linkMonthlyAnnual('monthlyIncome', 'annualIncome');
|
||||
this.linkMonthlyAnnual('monthlyTurnover', 'annualTurnover');
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a Monthly/Annual pair of amount fields in sync at a 12x ratio — typing into either
|
||||
* one auto-fills the other. Uses { emitEvent: false } on the write side so the two
|
||||
* subscriptions don't trigger each other back and forth.
|
||||
*/
|
||||
/** Rounds to at most 2 decimals without padding whole numbers with a trailing ".00". */
|
||||
private round2(n: number): string {
|
||||
return (Math.round(n * 100) / 100).toString();
|
||||
}
|
||||
|
||||
private linkMonthlyAnnual(monthlyControlName: string, annualControlName: string): void {
|
||||
const monthly = this.createClientForm.get(monthlyControlName);
|
||||
const annual = this.createClientForm.get(annualControlName);
|
||||
if (!monthly || !annual) {
|
||||
return;
|
||||
}
|
||||
|
||||
monthly.valueChanges.subscribe((value) => {
|
||||
if (this.suppressMonthlyAnnualLink) {
|
||||
return;
|
||||
}
|
||||
const num = parseFloat(value);
|
||||
annual.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 12) : '', { emitEvent: false });
|
||||
});
|
||||
|
||||
annual.valueChanges.subscribe((value) => {
|
||||
if (this.suppressMonthlyAnnualLink) {
|
||||
return;
|
||||
}
|
||||
const num = parseFloat(value);
|
||||
monthly.setValue(!isNaN(num) && num >= 0 ? this.round2(num / 12) : '', { emitEvent: false });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2134,7 +2180,15 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
return generalDetails;
|
||||
}
|
||||
|
||||
get calculatedRisk(): { category: number; reason: string } {
|
||||
get calculatedRisk(): {
|
||||
category: number;
|
||||
reason: string;
|
||||
amlCheckStatus?: string;
|
||||
bannedEntityMatch?: string;
|
||||
willfulDefaulter?: string;
|
||||
ecgcSalFlag?: string;
|
||||
dormantFlag?: string;
|
||||
} {
|
||||
const legalFormId = this.createClientForm.get('legalFormId')?.value || 1; // 1 = Person, 2 = Entity
|
||||
const occCatId = this.createClientForm.get('occupationCategoryCvId')?.value;
|
||||
const occId = this.createClientForm.get('occupationCvId')?.value;
|
||||
@@ -2151,6 +2205,21 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
// Evaluate rules from DB (ordered by orderPosition ASC)
|
||||
if (this.riskRules && this.riskRules.length > 0) {
|
||||
for (const rule of this.riskRules) {
|
||||
// Skip transaction/AML rules that have no KYC criteria
|
||||
if (
|
||||
rule.legalFormId == null &&
|
||||
rule.occupationCategoryCvId == null &&
|
||||
rule.occupationCvId == null &&
|
||||
rule.minMonthlyIncome == null &&
|
||||
rule.maxMonthlyIncome == null &&
|
||||
rule.minAnnualIncome == null &&
|
||||
rule.maxAnnualIncome == null &&
|
||||
rule.minAnnualTurnover == null &&
|
||||
rule.maxAnnualTurnover == null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match legal form
|
||||
if (rule.legalFormId != null && rule.legalFormId !== legalFormId) {
|
||||
continue;
|
||||
@@ -2191,15 +2260,39 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
|
||||
// All matched!
|
||||
return { category: rule.riskCategory, reason: rule.reason };
|
||||
return {
|
||||
category: rule.riskCategory,
|
||||
reason: rule.reason,
|
||||
amlCheckStatus: rule.amlCheckStatus || (rule.riskCategory === 3 ? 'FLAGGED' : 'CLEAR'),
|
||||
bannedEntityMatch: rule.bannedEntityMatch || 'NO',
|
||||
willfulDefaulter: rule.willfulDefaulter || 'NO',
|
||||
ecgcSalFlag: rule.ecgcSalFlag || 'NO',
|
||||
dormantFlag: rule.dormantFlag || 'ACTIVE'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default Fallbacks
|
||||
if (legalFormId !== LegalFormId.PERSON) {
|
||||
return { category: 2, reason: 'Non-Individual / Entity profile (Default Medium Risk)' };
|
||||
return {
|
||||
category: 2,
|
||||
reason: 'Non-Individual / Entity profile (Default Medium Risk)',
|
||||
amlCheckStatus: 'CLEAR',
|
||||
bannedEntityMatch: 'NO',
|
||||
willfulDefaulter: 'NO',
|
||||
ecgcSalFlag: 'NO',
|
||||
dormantFlag: 'ACTIVE'
|
||||
};
|
||||
}
|
||||
return { category: 1, reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)' };
|
||||
return {
|
||||
category: 1,
|
||||
reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)',
|
||||
amlCheckStatus: 'CLEAR',
|
||||
bannedEntityMatch: 'NO',
|
||||
willfulDefaulter: 'NO',
|
||||
ecgcSalFlag: 'NO',
|
||||
dormantFlag: 'ACTIVE'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1251,6 +1251,12 @@
|
||||
</h4>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="checkbox-field override-checkbox-wrapper full-width">
|
||||
<mat-checkbox formControlName="allowAmlOverride" [disabled]="readOnly">
|
||||
Override Auto-calculated AML & Compliance Checks
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>AML Check Status</mat-label>
|
||||
<mat-select required formControlName="amlCheckStatus">
|
||||
|
||||
@@ -71,7 +71,15 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() entityName: string = '';
|
||||
/** Read-only mode — disables form editing until user clicks edit icon */
|
||||
@Input() readOnly = false;
|
||||
@Input() calculatedRisk: { category: number; reason: string } | null = null;
|
||||
@Input() calculatedRisk: {
|
||||
category: number;
|
||||
reason: string;
|
||||
amlCheckStatus?: string;
|
||||
bannedEntityMatch?: string;
|
||||
willfulDefaulter?: string;
|
||||
ecgcSalFlag?: string;
|
||||
dormantFlag?: string;
|
||||
} | null = null;
|
||||
|
||||
today = new Date();
|
||||
kycId: string = '';
|
||||
@@ -458,6 +466,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
kycDocExpiryDate: [''],
|
||||
riskCategory: [1],
|
||||
allowRiskOverride: [false],
|
||||
allowAmlOverride: [false],
|
||||
remarks: [''],
|
||||
amlCheckStatus: [
|
||||
'CLEAR',
|
||||
@@ -535,9 +544,35 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
this.kycForm.get('allowAmlOverride')?.valueChanges.subscribe((allowed: boolean) => {
|
||||
const amlControls = [
|
||||
this.kycForm.get('amlCheckStatus'),
|
||||
this.kycForm.get('bannedEntityMatch'),
|
||||
this.kycForm.get('willfulDefaulter'),
|
||||
this.kycForm.get('ecgcSalFlag'),
|
||||
this.kycForm.get('dormantFlag')
|
||||
];
|
||||
if (allowed) {
|
||||
if (!this.readOnly) {
|
||||
amlControls.forEach((ctrl) => ctrl?.enable());
|
||||
}
|
||||
} else {
|
||||
amlControls.forEach((ctrl) => ctrl?.disable());
|
||||
this.applyAutoCalculatedAmlFlags();
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
||||
this.kycForm.get('riskCategory')?.disable();
|
||||
}
|
||||
|
||||
if (!this.kycForm.get('allowAmlOverride')?.value) {
|
||||
this.kycForm.get('amlCheckStatus')?.disable();
|
||||
this.kycForm.get('bannedEntityMatch')?.disable();
|
||||
this.kycForm.get('willfulDefaulter')?.disable();
|
||||
this.kycForm.get('ecgcSalFlag')?.disable();
|
||||
this.kycForm.get('dormantFlag')?.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
|
||||
@@ -645,6 +680,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
||||
this.kycForm.get('riskCategory')?.disable({ emitEvent: false });
|
||||
}
|
||||
if (!this.kycForm.get('allowAmlOverride')?.value) {
|
||||
this.kycForm.get('amlCheckStatus')?.disable({ emitEvent: false });
|
||||
this.kycForm.get('bannedEntityMatch')?.disable({ emitEvent: false });
|
||||
this.kycForm.get('willfulDefaulter')?.disable({ emitEvent: false });
|
||||
this.kycForm.get('ecgcSalFlag')?.disable({ emitEvent: false });
|
||||
this.kycForm.get('dormantFlag')?.disable({ emitEvent: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changes['calculatedRisk']) {
|
||||
@@ -657,6 +699,11 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
riskControl.setValue(newRisk.category);
|
||||
riskControl.markAsDirty();
|
||||
}
|
||||
|
||||
const allowAmlOverride = this.kycForm.get('allowAmlOverride')?.value;
|
||||
if (!allowAmlOverride) {
|
||||
this.applyAutoCalculatedAmlFlags(newRisk);
|
||||
}
|
||||
} else {
|
||||
this.riskCalculationReason = '';
|
||||
}
|
||||
@@ -871,6 +918,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Risk & Remarks
|
||||
riskCategory: data.riskCategory || 1,
|
||||
allowRiskOverride: this.calculatedRisk ? this.calculatedRisk.category !== (data.riskCategory || 1) : false,
|
||||
allowAmlOverride: this.calculatedRisk
|
||||
? this.calculatedRisk.amlCheckStatus !== (data.amlCheckStatus || 'CLEAR') ||
|
||||
this.calculatedRisk.bannedEntityMatch !== (data.bannedEntityMatch || 'NO') ||
|
||||
this.calculatedRisk.willfulDefaulter !== (data.willfulDefaulter || 'NO') ||
|
||||
this.calculatedRisk.ecgcSalFlag !== (data.ecgcSalFlag || 'NO') ||
|
||||
this.calculatedRisk.dormantFlag !== (data.dormantFlag || 'ACTIVE')
|
||||
: false,
|
||||
remarks: data.remarks || '',
|
||||
hasPan: !!data.panNumber,
|
||||
// AML & Compliance
|
||||
@@ -1805,6 +1859,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
v.form15Applicable
|
||||
);
|
||||
}
|
||||
|
||||
private applyAutoCalculatedAmlFlags(risk = this.calculatedRisk): void {
|
||||
if (!risk) return;
|
||||
this.kycForm.get('amlCheckStatus')?.setValue(risk.amlCheckStatus || 'CLEAR');
|
||||
this.kycForm.get('bannedEntityMatch')?.setValue(risk.bannedEntityMatch || 'NO');
|
||||
this.kycForm.get('willfulDefaulter')?.setValue(risk.willfulDefaulter || 'NO');
|
||||
this.kycForm.get('ecgcSalFlag')?.setValue(risk.ecgcSalFlag || 'NO');
|
||||
this.kycForm.get('dormantFlag')?.setValue(risk.dormantFlag || 'ACTIVE');
|
||||
|
||||
this.kycForm.get('amlCheckStatus')?.markAsDirty();
|
||||
this.kycForm.get('bannedEntityMatch')?.markAsDirty();
|
||||
this.kycForm.get('willfulDefaulter')?.markAsDirty();
|
||||
this.kycForm.get('ecgcSalFlag')?.markAsDirty();
|
||||
this.kycForm.get('dormantFlag')?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -392,6 +392,22 @@ export class AuthenticationService {
|
||||
return roleNames.some((n) => n === 'clerk');
|
||||
}
|
||||
|
||||
/** Returns true when the logged-in user has the "Principal Officer" role (STR conclusion/filing authority). */
|
||||
isPrincipalOfficerUser(): boolean {
|
||||
const creds = this.getCredentials();
|
||||
if (!creds || !creds.roles) return false;
|
||||
const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim());
|
||||
return roleNames.some((n) => n === 'principal officer');
|
||||
}
|
||||
|
||||
/** Returns true when the logged-in user has the "AML Officer" role (first-level alert review). */
|
||||
isAmlOfficerUser(): boolean {
|
||||
const creds = this.getCredentials();
|
||||
if (!creds || !creds.roles) return false;
|
||||
const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim());
|
||||
return roleNames.some((n) => n === 'aml officer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true ONLY for regular cashier role (not Head Cashier).
|
||||
* Regular cashier → full-screen transaction entry, no nav.
|
||||
@@ -424,6 +440,18 @@ export class AuthenticationService {
|
||||
return roleNames.some((n) => n === 'cashier');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the logged-in user belongs to the Head Office.
|
||||
* Fineract seeds Head Office as officeId 1 with hierarchy "."; office name is
|
||||
* checked too since some deployments rename/reseed the root office.
|
||||
*/
|
||||
isHeadOfficeUser(): boolean {
|
||||
const creds = this.getCredentials();
|
||||
if (!creds) return false;
|
||||
if (creds.officeId === 1) return true;
|
||||
return (creds.officeName || '').toLowerCase().includes('head office');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user credentials.
|
||||
*
|
||||
|
||||
@@ -362,6 +362,10 @@
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
|
||||
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" (click)="investmentMenuOpen = !investmentMenuOpen; rbiMenuOpen = false; glMenuOpen = false" *mifosxHasPermission="'MENU_RBI'">
|
||||
<fa-icon icon="chart-line" class="ni-ico"></fa-icon>Investment
|
||||
<fa-icon [icon]="investmentMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu" *mifosxHasPermission="'MENU_DIGITAL'">
|
||||
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
@@ -492,6 +496,15 @@
|
||||
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
|
||||
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
class="ni"
|
||||
tabindex="0"
|
||||
(click)="investmentMenuOpen = !investmentMenuOpen; rbiMenuOpen = false; glMenuOpen = false"
|
||||
*mifosxHasPermission="'MENU_RBI'"
|
||||
>
|
||||
<fa-icon icon="chart-line" class="ni-ico"></fa-icon>Investment
|
||||
<fa-icon [icon]="investmentMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||
</a>
|
||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu" *mifosxHasPermission="'MENU_REPORTS'">
|
||||
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
|
||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||
@@ -653,6 +666,7 @@
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesSubLedgerSearch('Cash / Clearing / Transfer')"
|
||||
routerLink="/cashier/cash-clearing-transfer-summary"
|
||||
(click)="subLedgerOpen = false"
|
||||
><fa-icon icon="money-bill" class="gl-icon"></fa-icon> Cash / Clearing / Transfer</a
|
||||
>
|
||||
@@ -701,8 +715,11 @@
|
||||
</div>
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Deposits</div>
|
||||
<a class="gl-link" [class.hidden]="!matchesSubLedgerSearch('FD / RD Entry')" (click)="subLedgerOpen = false"
|
||||
><fa-icon icon="piggy-bank" class="gl-icon"></fa-icon> FD / RD Entry</a
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesSubLedgerSearch('FD / RD Entry')"
|
||||
(click)="subLedgerOpen = false; openFdRdEntryDialog()"
|
||||
><fa-icon icon="piggy-bank" class="gl-icon" style="color: #0f766e"></fa-icon> FD / RD Entry</a
|
||||
>
|
||||
<a class="gl-link" [class.hidden]="!matchesSubLedgerSearch('DDS Entry')" (click)="subLedgerOpen = false"
|
||||
><fa-icon icon="coins" class="gl-icon"></fa-icon> DDS Entry</a
|
||||
@@ -808,8 +825,11 @@
|
||||
<div class="gl-columns">
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Financial Statements</div>
|
||||
<a class="gl-link" [class.hidden]="!matchesGlSearch('Trial Balance')" (click)="glMenuOpen = false"
|
||||
><fa-icon icon="balance-scale" class="gl-icon"></fa-icon> Trial Balance</a
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesGlSearch('Trial Balance')"
|
||||
(click)="glMenuOpen = false; openAccountingReportsDialog('GL-DR-CR')"
|
||||
><fa-icon icon="balance-scale" class="gl-icon" style="color: #b45309"></fa-icon> Trial Balance</a
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
@@ -864,8 +884,12 @@
|
||||
(click)="glMenuOpen = false"
|
||||
><fa-icon icon="users" class="gl-icon"></fa-icon> Client wise Balance Report</a
|
||||
>
|
||||
<a class="gl-link" [class.hidden]="!matchesGlSearch('Contingent Expense')" (click)="glMenuOpen = false"
|
||||
><fa-icon icon="exclamation-circle" class="gl-icon"></fa-icon> Contingent Expense</a
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesGlSearch('Contingent Expense')"
|
||||
(click)="glMenuOpen = false; openAccountingReportsDialog('CONTINGENT-LIAB')"
|
||||
><fa-icon icon="exclamation-circle" class="gl-icon" style="color: #b45309"></fa-icon> Contingent
|
||||
Expense</a
|
||||
>
|
||||
<a class="gl-link" [class.hidden]="!matchesGlSearch('Banker Head Report')" (click)="glMenuOpen = false"
|
||||
><fa-icon icon="university" class="gl-icon"></fa-icon> Banker Head Report</a
|
||||
@@ -1155,7 +1179,7 @@
|
||||
<fa-icon icon="user-slash" class="menu-item-icon" style="color: #c2410c"></fa-icon> Non-Customer Entry
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0"><fa-icon icon="lock" class="menu-item-icon"></fa-icon> Locker</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item routerLink="/investment/bonds" tabindex="0">
|
||||
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> SLR and Non SLR Investment Report
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0" (click)="openCicReportingDialog()">
|
||||
@@ -1167,21 +1191,17 @@
|
||||
<button mat-menu-item tabindex="0">
|
||||
<fa-icon icon="chart-bar" class="menu-item-icon"></fa-icon> Depreciation Register
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/fiu" tabindex="0">
|
||||
<fa-icon icon="shield-alt" class="menu-item-icon" style="color: #dc2626"></fa-icon> Financial Intelligence
|
||||
Unit (FIU-IND)
|
||||
</button>
|
||||
</div>
|
||||
<!-- Investment -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">Investment</div>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item routerLink="/investment/dashboard" tabindex="0">
|
||||
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
||||
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received & Receivable Report
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
||||
<fa-icon icon="piggy-bank" class="menu-item-icon"></fa-icon> Bucket Wise Investment
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/deaf" tabindex="0">
|
||||
@@ -1212,39 +1232,41 @@
|
||||
<!-- NPA Reports -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">NPA Reports</div>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/interest">
|
||||
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> NPA Interest
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/status">
|
||||
<fa-icon icon="file-alt" class="menu-item-icon"></fa-icon> NPA Status Report
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/recovery">
|
||||
<fa-icon icon="undo" class="menu-item-icon"></fa-icon> NPA Recovery Report
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/summary">
|
||||
<fa-icon icon="clipboard-list" class="menu-item-icon"></fa-icon> NPA Summary Report
|
||||
</button>
|
||||
</div>
|
||||
<!-- NPA Analysis -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">NPA Analysis</div>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/account-detail">
|
||||
<fa-icon icon="list-alt" class="menu-item-icon"></fa-icon> NPA Account Detail Report
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/movement">
|
||||
<fa-icon icon="exchange" class="menu-item-icon"></fa-icon> NPA Movement Report
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/provision">
|
||||
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> NPA with Provision and Required Provision
|
||||
</button>
|
||||
</div>
|
||||
<!-- NPA Classification -->
|
||||
<div class="deposit-menu-group">
|
||||
<div class="deposit-group-title">NPA Classification</div>
|
||||
<button mat-menu-item tabindex="0">
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/probable">
|
||||
<fa-icon icon="exclamation-circle" class="menu-item-icon"></fa-icon> Probable NPA
|
||||
</button>
|
||||
<button mat-menu-item tabindex="0"><fa-icon icon="flag" class="menu-item-icon"></fa-icon> SMA Report</button>
|
||||
<button mat-menu-item tabindex="0" routerLink="/npa/reports/sma">
|
||||
<fa-icon icon="flag" class="menu-item-icon"></fa-icon> SMA Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
@@ -1571,7 +1593,8 @@
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('CTR / STR')"
|
||||
(click)="rbiMenuOpen = false; openCtrStrDialog()"
|
||||
routerLink="/fiu"
|
||||
(click)="rbiMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="calculator" class="gl-icon"></fa-icon> CTR / STR
|
||||
</a>
|
||||
@@ -1668,15 +1691,24 @@
|
||||
>
|
||||
<fa-icon icon="bell" class="gl-icon"></fa-icon> Daily Position Notice
|
||||
</a>
|
||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('R/SLR')" (click)="rbiMenuOpen = false">
|
||||
<fa-icon icon="lock" class="gl-icon"></fa-icon> R/SLR
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('AML')"
|
||||
routerLink="/rbi/aml"
|
||||
(click)="rbiMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="shield-alt" class="gl-icon"></fa-icon> AML
|
||||
</a>
|
||||
</div>
|
||||
<!-- OSS Reports -->
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">OSS Reports</div>
|
||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 1 XBRL/CIMS')" (click)="rbiMenuOpen = false">
|
||||
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 1 XBRL/CIMS
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('OSS 1 XBRL/CIMS')"
|
||||
(click)="rbiMenuOpen = false; openCimsXbrlDialog('RAL')"
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 1 XBRL/CIMS
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
@@ -1685,14 +1717,40 @@
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 2 XBRL
|
||||
</a>
|
||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 3 XBRL/CIMS')" (click)="rbiMenuOpen = false">
|
||||
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 3 XBRL/CIMS
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('OSS 3 XBRL/CIMS')"
|
||||
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-2')"
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 3 XBRL/CIMS
|
||||
</a>
|
||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 4/OSS 5/OSS 6')" (click)="rbiMenuOpen = false">
|
||||
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 4 / OSS 5 / OSS 6
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('OSS 4')"
|
||||
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-4')"
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 4
|
||||
</a>
|
||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('OSS 7')" (click)="rbiMenuOpen = false">
|
||||
<fa-icon icon="file-alt" class="gl-icon"></fa-icon> OSS 7
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('OSS 5')"
|
||||
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-5')"
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 5
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('OSS 6')"
|
||||
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-6')"
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 6
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesRbiSearch('OSS 7')"
|
||||
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-7')"
|
||||
>
|
||||
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 7
|
||||
</a>
|
||||
</div>
|
||||
<!-- CIMS / XBRL Returns -->
|
||||
@@ -1823,6 +1881,161 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Investment (Treasury) Mega Menu -->
|
||||
@if (investmentMenuOpen) {
|
||||
<div class="gl-backdrop" (click)="investmentMenuOpen = false"></div>
|
||||
<div class="gl-mega-menu rbi-mega-menu">
|
||||
<div class="gl-mega-menu-header">
|
||||
<fa-icon icon="chart-line" class="gl-mega-menu-header-icon"></fa-icon>
|
||||
<span>Investment</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="gl-search"
|
||||
placeholder="Search Investment..."
|
||||
(input)="investmentSearch = $any($event.target).value"
|
||||
/>
|
||||
<div class="rbi-columns">
|
||||
<!-- Dashboard -->
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Dashboard</div>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Investment Dashboard')"
|
||||
routerLink="/investment/dashboard"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="chart-pie" class="gl-icon"></fa-icon> Portfolio Dashboard
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Due Date Diary for All Investment')"
|
||||
routerLink="/investment/dashboard"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="calendar-check" class="gl-icon"></fa-icon> Due Date Diary for All Investment
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Bucket Wise Investment')"
|
||||
routerLink="/investment/reports"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="layer-group" class="gl-icon"></fa-icon> Bucket Wise Investment
|
||||
</a>
|
||||
</div>
|
||||
<!-- Entry -->
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Entry</div>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('FD Investment')"
|
||||
routerLink="/investment/fd"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="piggy-bank" class="gl-icon"></fa-icon> FD Investment
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Govt Security / Non-SLR / Bond / Mutual Fund')"
|
||||
routerLink="/investment/bonds"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="university" class="gl-icon"></fa-icon> Govt Security / Non-SLR / Bond / Mutual Fund
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('SLR and Non SLR Investment Report')"
|
||||
routerLink="/investment/bonds"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="balance-scale" class="gl-icon"></fa-icon> SLR and Non SLR Investment Report
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Share Entry')"
|
||||
routerLink="/investment/share"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="chart-line" class="gl-icon"></fa-icon> Share Entry
|
||||
</a>
|
||||
</div>
|
||||
<!-- Masters -->
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Masters</div>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="
|
||||
!matchesInvestmentSearch(
|
||||
'Bank Party Script Broker SubType Control Parameter Daily Script LIEN Reason Mutual Fund Product SGL Bank Script Market Detail Masters'
|
||||
)
|
||||
"
|
||||
routerLink="/investment/masters"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="cogs" class="gl-icon"></fa-icon> Bank/Party, Script, Broker & Other Masters
|
||||
</a>
|
||||
</div>
|
||||
<!-- Interest -->
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Interest</div>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('FD Interest Calculation')"
|
||||
routerLink="/investment/interest"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="calculator" class="gl-icon"></fa-icon> FD Interest Calculation
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Govt Bond Interest Calculation')"
|
||||
routerLink="/investment/interest"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="calculator" class="gl-icon"></fa-icon> Govt./Bond Int. Calculation
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Government Bond Int Register FD Interest Register FD Register')"
|
||||
routerLink="/investment/interest"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="list-alt" class="gl-icon"></fa-icon> Bond Int / FD Int / FD Registers
|
||||
</a>
|
||||
</div>
|
||||
<!-- Reports -->
|
||||
<div class="gl-group">
|
||||
<div class="gl-group-title">Reports</div>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Balance Ledger Maturity Investment Reports')"
|
||||
routerLink="/investment/reports"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="file-invoice" class="gl-icon"></fa-icon> Balance / Ledger / Maturity Reports
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Total Int Received & Receivable Report')"
|
||||
routerLink="/investment/reports"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="hand-holding-usd" class="gl-icon"></fa-icon> Total Int Received & Receivable Report
|
||||
</a>
|
||||
<a
|
||||
class="gl-link"
|
||||
[class.hidden]="!matchesInvestmentSearch('Investment Portfolio')"
|
||||
routerLink="/investment/reports"
|
||||
(click)="investmentMenuOpen = false"
|
||||
>
|
||||
<fa-icon icon="briefcase" class="gl-icon"></fa-icon> Investment Portfolio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Progress Bar -->
|
||||
@if (progressBarMode !== 'none') {
|
||||
<div>
|
||||
|
||||
@@ -856,6 +856,20 @@ $gray-700: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
// Marks a menu link as actually wired to a working screen/dialog (vs a still-unwired
|
||||
// placeholder) — a small green dot at the end of the row, so a reviewer can tell at a glance
|
||||
// which items in a mega-menu are functional without having to click every one of them.
|
||||
.gl-link--live::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: #16a34a;
|
||||
box-shadow: 0 0 0 2px rgb(22 163 74 / 15%);
|
||||
}
|
||||
|
||||
/* RBI Mega Menu */
|
||||
.rbi-mega-menu {
|
||||
width: 1100px;
|
||||
|
||||
@@ -98,6 +98,10 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
rbiMenuOpen = false;
|
||||
rbiSearch = '';
|
||||
|
||||
/** Investment (Treasury) mega menu state */
|
||||
investmentMenuOpen = false;
|
||||
investmentSearch = '';
|
||||
|
||||
/* Live KPI ticker — markup is static inside the template; the marquee
|
||||
animation, LIVE badge, and pause-on-hover handle the live feel. */
|
||||
|
||||
@@ -116,6 +120,11 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
return label.toLowerCase().includes(this.rbiSearch.toLowerCase());
|
||||
}
|
||||
|
||||
matchesInvestmentSearch(label: string): boolean {
|
||||
if (!this.investmentSearch) return true;
|
||||
return label.toLowerCase().includes(this.investmentSearch.toLowerCase());
|
||||
}
|
||||
|
||||
/** Route for a facility entry in the Account Facilities hover dropdown — only "Cheque Book" has a
|
||||
* dedicated action page today; every other facility falls back to the existing overview page. */
|
||||
facilityRoute(label: string): string[] {
|
||||
@@ -242,7 +251,7 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
* GL Wise DR/CR / Misc Expense Ledger). Initial report selected via menu link.
|
||||
*/
|
||||
async openAccountingReportsDialog(
|
||||
initialReport: 'PNL' | 'BALANCE-SHEET' | 'COMPARATIVE-BS' | 'GL-DR-CR' | 'MISC-EXP' = 'PNL'
|
||||
initialReport: 'PNL' | 'BALANCE-SHEET' | 'COMPARATIVE-BS' | 'CONTINGENT-LIAB' | 'GL-DR-CR' | 'MISC-EXP' = 'PNL'
|
||||
): Promise<void> {
|
||||
const { AccountingReportsDialogComponent } =
|
||||
await import('../../home/dashboard/accounting-reports-dialog/accounting-reports-dialog.component');
|
||||
@@ -280,29 +289,14 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the CTR & STR Compliance Center dialog.
|
||||
*/
|
||||
async openCtrStrDialog(): Promise<void> {
|
||||
const { CtrStrDialogComponent } = await import('../../home/dashboard/ctr-str-dialog/ctr-str-dialog.component');
|
||||
this.dialog.open(CtrStrDialogComponent, {
|
||||
width: '95vw',
|
||||
maxWidth: '1280px',
|
||||
maxHeight: '92vh',
|
||||
panelClass: 'crr-slr-dialog-panel',
|
||||
autoFocus: false,
|
||||
disableClose: false,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'ckyc-rpt-dlg-backdrop'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the CIMS / XBRL Returns dialog. The initial form (OSS-2, PSA, RAL, RCP-UCB)
|
||||
* is selected based on the menu link the user clicked; user can switch between
|
||||
* forms via the form-pill selector inside the dialog.
|
||||
*/
|
||||
async openCimsXbrlDialog(initialForm: 'OSS-2' | 'PSA' | 'RAL' | 'RCP-UCB' = 'OSS-2'): Promise<void> {
|
||||
async openCimsXbrlDialog(
|
||||
initialForm: 'OSS-2' | 'PSA' | 'RAL' | 'RCP-UCB' | 'OSS-4' | 'OSS-5' | 'OSS-6' | 'OSS-7' = 'OSS-2'
|
||||
): Promise<void> {
|
||||
const { CimsXbrlDialogComponent } =
|
||||
await import('../../home/dashboard/cims-xbrl-dialog/cims-xbrl-dialog.component');
|
||||
this.dialog.open(CimsXbrlDialogComponent, {
|
||||
@@ -318,6 +312,26 @@ export class ShellComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens the FD / RD Entry dialog under Sub Ledger → Deposits. */
|
||||
async openFdRdEntryDialog(): Promise<void> {
|
||||
const { FdRdEntryDialogComponent } =
|
||||
await import('../../home/dashboard/fd-rd-entry-dialog/fd-rd-entry-dialog.component');
|
||||
this.dialog.open(FdRdEntryDialogComponent, {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
maxWidth: '100vw',
|
||||
maxHeight: '100vh',
|
||||
panelClass: [
|
||||
'crr-slr-dialog-panel',
|
||||
'fdrd-fullscreen-panel'
|
||||
],
|
||||
autoFocus: false,
|
||||
disableClose: false,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'fdrd-dlg-backdrop'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Asset Liability Management dialog (Form ALM-1).
|
||||
* Covers maturity ladder, monthly snapshot, and structural liquidity returns.
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
<a
|
||||
class="subledger-link"
|
||||
[class.hidden]="!matchesSearch('Cash / Clearing / Transfer')"
|
||||
(click)="subLedgerOpen = false"
|
||||
(click)="navigateMenu('/cashier/cash-clearing-transfer-summary'); subLedgerOpen = false"
|
||||
>
|
||||
<fa-icon icon="money-bill" class="subledger-icon"></fa-icon> Cash / Clearing / Transfer
|
||||
</a>
|
||||
|
||||
@@ -81,7 +81,7 @@ export interface DeafEligibleAccount {
|
||||
|
||||
/** Form I — Dec 2025 cycle */
|
||||
export const FORM_I: DeafForm = {
|
||||
bankName: 'Bharat Urban Bank Ltd, Mumbai Main Branch',
|
||||
bankName: 'CBSNexora Bank Ltd, Mumbai Main Branch',
|
||||
bankDeafCode: '1100',
|
||||
remittedThroughSponsor: 'Y',
|
||||
sponsorBankName: 'State Bank of India',
|
||||
@@ -170,7 +170,7 @@ export const FORM_I: DeafForm = {
|
||||
|
||||
/** Form II — Jan 2026 (head office consolidated) */
|
||||
export const FORM_II: DeafForm = {
|
||||
bankName: 'Bharat Urban Bank Ltd, Head Office',
|
||||
bankName: 'CBSNexora Bank Ltd, Head Office',
|
||||
bankDeafCode: '1100',
|
||||
remittedThroughSponsor: 'Y',
|
||||
sponsorBankName: 'State Bank of India',
|
||||
@@ -271,7 +271,7 @@ export const REFUND_CLAIMS: {
|
||||
auditor: string;
|
||||
rows: DeafClaim[];
|
||||
} = {
|
||||
bankName: 'Bharat Urban Bank Ltd',
|
||||
bankName: 'CBSNexora Bank Ltd',
|
||||
bankDeafCode: '1100',
|
||||
currentAcWithRbi: '03010200099001',
|
||||
sponsorBank: 'State Bank of India',
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface SahayDepositor {
|
||||
}
|
||||
|
||||
export const DICGC_BANK = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
reportAsOn: '29-01-2026',
|
||||
printDate: '29-01-2026 04:19 PM',
|
||||
|
||||
@@ -114,13 +114,13 @@ export interface KycEntity {
|
||||
}
|
||||
|
||||
export const FIU_BANK = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
reBankingFlag: 'SCB',
|
||||
reportingMonth: 'January 2026',
|
||||
reBuid: 'REBANK-2031',
|
||||
principalOfficer: 'Karthik Kamath',
|
||||
principalOfficerEmail: 'po.fiu@bharaturbanbank.example',
|
||||
principalOfficerEmail: 'po.fiu@cbsnexorabank.example',
|
||||
filerUserId: 'PO-001',
|
||||
cashThreshold: 1000000, // ₹10 L per single cash txn
|
||||
monthlyAggThreshold: 1000000, // aggregate ≥ ₹10 L in a month
|
||||
|
||||
@@ -129,9 +129,130 @@
|
||||
<!-- CTR -->
|
||||
@if (activeTab === 'ctr') {
|
||||
<div class="fiu-section">
|
||||
<!-- LIVE: cash-related AML alerts awaiting AML Officer review -->
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>CTR — Cash Transaction Report</h3>
|
||||
<h3>Live Cash Alerts</h3>
|
||||
<p>
|
||||
Raised by the transaction-monitoring rule engine — review and whitelist or escalate to Principal Officer
|
||||
</p>
|
||||
</div>
|
||||
<select class="fiu-inp" [(ngModel)]="amlAlertStatusFilter" (ngModelChange)="loadAmlAlerts()">
|
||||
<option value="">All statuses</option>
|
||||
<option value="RAISED">Raised</option>
|
||||
<option value="UNDER_REVIEW">Under Review</option>
|
||||
<option value="WHITELISTED">Whitelisted</option>
|
||||
<option value="ESCALATED">Escalated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fiu-tbl-wrap">
|
||||
<table class="fiu-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Client</th>
|
||||
<th>Type</th>
|
||||
<th>Rule</th>
|
||||
<th class="ta-r">Amount</th>
|
||||
<th>Raised On</th>
|
||||
<th>Status</th>
|
||||
<th>Notes</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (alert of amlAlerts; track alert.id) {
|
||||
<tr>
|
||||
<td>{{ alert.id }}</td>
|
||||
<td>{{ alert.clientDisplayName }}</td>
|
||||
<td>{{ alert.transactionType }}</td>
|
||||
<td>{{ alert.ruleCode }} — {{ alert.ruleDescription }}</td>
|
||||
<td class="ta-r mono">₹{{ alert.amount | number: '1.0-0' }}</td>
|
||||
<td>{{ alert.raisedOn | date: 'medium' }}</td>
|
||||
<td>
|
||||
<span [class]="amlStatusClass(alert.status)">{{ alert.status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') {
|
||||
<input
|
||||
type="text"
|
||||
class="fiu-inp"
|
||||
placeholder="Review notes"
|
||||
[(ngModel)]="amlReviewNotes[alert.id]"
|
||||
/>
|
||||
} @else {
|
||||
<span class="muted">{{ alert.reviewNotes }}</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') {
|
||||
<div class="fiu-action-group" *mifosxHasPermission="'REVIEW_AML_ALERT'">
|
||||
<button class="fiu-btn fiu-btn-g" (click)="whitelistAlert(alert)">Whitelist</button>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="escalateAlert(alert)">Escalate</button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="9" class="fiu-empty">No live alerts.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- LIVE: monthly CTR reports (mechanical, threshold-based) -->
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>Monthly CTR Reports</h3>
|
||||
<p>
|
||||
Threshold: <strong>₹{{ bank.cashThreshold | number }}</strong> per single txn / connected aggregate
|
||||
per month
|
||||
</p>
|
||||
</div>
|
||||
<div class="fiu-action-group">
|
||||
<input class="fiu-inp" type="month" [(ngModel)]="ctrGenerateMonth" />
|
||||
<button class="fiu-btn fiu-btn-g" *mifosxHasPermission="'REVIEW_AML_ALERT'" (click)="generateCtrReport()">
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fiu-tbl-wrap">
|
||||
<table class="fiu-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
<th>Generated On</th>
|
||||
<th class="ta-c">Records</th>
|
||||
<th>Status</th>
|
||||
<th>Submitted On</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of amlCtrReports; track r.id) {
|
||||
<tr>
|
||||
<td>{{ r.reportMonth }}</td>
|
||||
<td>{{ r.generatedOn | date: 'medium' }}</td>
|
||||
<td class="ta-c">{{ r.recordCount }}</td>
|
||||
<td>
|
||||
<span [class]="amlStatusClass(r.status)">{{ r.status }}</span>
|
||||
</td>
|
||||
<td>{{ r.submittedOn ? (r.submittedOn | date: 'medium') : '—' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5" class="fiu-empty">No CTR reports generated yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- FINnet export template (separate concern — TC1/TC2 file-format generation) -->
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>FINnet TC2 Export Template</h3>
|
||||
<p>
|
||||
All cash transactions ≥ ₹{{ bank.cashThreshold | number }} (single or aggregate per month per account)
|
||||
</p>
|
||||
@@ -221,9 +342,107 @@
|
||||
<!-- STR -->
|
||||
@if (activeTab === 'str') {
|
||||
<div class="fiu-section">
|
||||
<!-- LIVE: STR cases with full Principal Officer actions -->
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>STR — Suspicious Transaction Report</h3>
|
||||
<h3>Live STR Cases</h3>
|
||||
<p>
|
||||
Escalated by an AML Officer; Principal Officer investigates, concludes and files with FIU-IND within 7
|
||||
working days
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fiu-tbl-wrap">
|
||||
<table class="fiu-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Client</th>
|
||||
<th class="ta-c">Cumulative Cases</th>
|
||||
<th>Assigned PO</th>
|
||||
<th>Conclusion</th>
|
||||
<th>FIU Due Date</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (sc of amlStrCases; track sc.id) {
|
||||
<tr [class.selected-row]="selectedStrCase?.id === sc.id">
|
||||
<td>{{ sc.id }}</td>
|
||||
<td>{{ sc.clientDisplayName }}</td>
|
||||
<td class="ta-c" [class.escalation-warning]="sc.cumulativeStrCaseCountForClient >= 3">
|
||||
{{ sc.cumulativeStrCaseCountForClient }}
|
||||
</td>
|
||||
<td>{{ sc.assignedPoUsername || '—' }}</td>
|
||||
<td>{{ sc.conclusion || '—' }}</td>
|
||||
<td>{{ sc.fiuDueDate || '—' }}</td>
|
||||
<td>
|
||||
<span [class]="amlStatusClass(sc.status)">{{ sc.status }}</span>
|
||||
</td>
|
||||
<td><button class="fiu-btn fiu-btn-g" (click)="selectStrCase(sc)">Investigate</button></td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="8" class="fiu-empty">No STR cases yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (selectedStrCase) {
|
||||
<div class="str-case-detail">
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>STR Case #{{ selectedStrCase.id }} — {{ selectedStrCase.clientDisplayName }}</h3>
|
||||
<p>
|
||||
Alert #{{ selectedStrCase.alertId }} ·
|
||||
{{ selectedStrCase.cumulativeStrCaseCountForClient }} cumulative STR case(s) for this client
|
||||
@if (selectedStrCase.cumulativeStrCaseCountForClient >= 3) {
|
||||
<span class="escalation-badge">Risk category auto-escalated</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<button class="fiu-btn fiu-btn-g" (click)="closeStrCaseDetail()">Close</button>
|
||||
</div>
|
||||
|
||||
<div class="case-form" *mifosxHasPermission="'CONCLUDE_AML_STR'">
|
||||
<div class="form-row">
|
||||
<label for="fiuInvestigationNotes">Investigation notes (source-of-transaction history reviewed)</label>
|
||||
<textarea id="fiuInvestigationNotes" rows="3" [(ngModel)]="concludeInvestigationNotes"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="fiuSourceOfFundsNotes">Source-of-funds notes</label>
|
||||
<textarea id="fiuSourceOfFundsNotes" rows="2" [(ngModel)]="concludeSourceOfFundsNotes"></textarea>
|
||||
</div>
|
||||
<div class="form-row conclusion-row">
|
||||
<label>
|
||||
<input type="radio" name="fiuConclusion" value="NOT_SUSPICIOUS" [(ngModel)]="concludeConclusion" />
|
||||
Not suspicious (whitelist)
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="fiuConclusion" value="SUSPICIOUS" [(ngModel)]="concludeConclusion" />
|
||||
Suspicious (file STR)
|
||||
</label>
|
||||
</div>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="concludeStrCase()">Conclude</button>
|
||||
|
||||
@if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') {
|
||||
<div class="file-str-row">
|
||||
<label for="fiuFileReferenceNo">FIU-IND acknowledgement / reference number</label>
|
||||
<input id="fiuFileReferenceNo" class="fiu-inp" type="text" [(ngModel)]="fileReferenceNo" />
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="fileStrCase()">Mark STR Filed</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- FINnet export template (separate concern — STR XML file-format generation) -->
|
||||
<div class="fiu-form-meta">
|
||||
<div class="fiu-form-title">
|
||||
<h3>FINnet STR XML Export Template</h3>
|
||||
<p>Detected by AML rules engine + branch compliance officers; filed within 7 days</p>
|
||||
</div>
|
||||
<button class="fiu-btn fiu-btn-xml" (click)="exportStrXml()">
|
||||
|
||||
@@ -579,6 +579,41 @@
|
||||
background: var(--f-red-l);
|
||||
color: var(--f-red);
|
||||
}
|
||||
|
||||
&.status-raised {
|
||||
background: var(--f-amber-l);
|
||||
color: var(--f-amber);
|
||||
}
|
||||
|
||||
&.status-under-review {
|
||||
background: var(--f-blue-l);
|
||||
color: var(--f-blue);
|
||||
}
|
||||
|
||||
&.status-whitelisted,
|
||||
&.status-concluded-clear,
|
||||
&.status-generated,
|
||||
&.status-submitted {
|
||||
background: var(--f-green-l);
|
||||
color: var(--f-green);
|
||||
}
|
||||
|
||||
&.status-escalated,
|
||||
&.status-open,
|
||||
&.status-concluded-suspicious {
|
||||
background: var(--f-red-l);
|
||||
color: var(--f-red);
|
||||
}
|
||||
|
||||
&.status-filed {
|
||||
background: var(--f-purple-l);
|
||||
color: var(--f-purple);
|
||||
}
|
||||
|
||||
&.status-draft {
|
||||
background: var(--f-g100);
|
||||
color: var(--f-g500);
|
||||
}
|
||||
}
|
||||
|
||||
.risk-pill {
|
||||
@@ -892,3 +927,85 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
background: var(--f-blue-l);
|
||||
}
|
||||
|
||||
.escalation-warning {
|
||||
color: var(--f-red);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.escalation-badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--f-amber-l);
|
||||
color: var(--f-amber);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.str-case-detail {
|
||||
border: 1px solid var(--f-g200);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.case-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--f-g300);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--f-g500);
|
||||
}
|
||||
}
|
||||
|
||||
.conclusion-row {
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.file-str-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--f-g300);
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--f-g500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { AmlService } from 'app/aml/aml.service';
|
||||
import { AuthenticationService } from 'app/core/authentication/authentication.service';
|
||||
import {
|
||||
CtrTransaction,
|
||||
CTR_TXNS,
|
||||
@@ -33,7 +35,9 @@ import {
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class FiuComponent {
|
||||
export class FiuComponent implements OnInit {
|
||||
private amlService = inject(AmlService);
|
||||
private authenticationService = inject(AuthenticationService);
|
||||
/** Active tab */
|
||||
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
|
||||
|
||||
@@ -57,6 +61,126 @@ export class FiuComponent {
|
||||
toastVisible = false;
|
||||
private toastTimer: any;
|
||||
|
||||
// -------------------- Live AML monitoring (real backend, replaces mock CTR/STR tables) --------------------
|
||||
amlAlerts: any[] = [];
|
||||
amlAlertStatusFilter = '';
|
||||
amlReviewNotes: Record<number, string> = {};
|
||||
|
||||
amlCtrReports: any[] = [];
|
||||
ctrGenerateMonth = '';
|
||||
|
||||
amlStrCases: any[] = [];
|
||||
selectedStrCase: any = null;
|
||||
concludeConclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS' = 'NOT_SUSPICIOUS';
|
||||
concludeInvestigationNotes = '';
|
||||
concludeSourceOfFundsNotes = '';
|
||||
fileReferenceNo = '';
|
||||
|
||||
get isPrincipalOfficer(): boolean {
|
||||
return this.authenticationService.isPrincipalOfficerUser();
|
||||
}
|
||||
|
||||
get isAmlOfficer(): boolean {
|
||||
return this.authenticationService.isAmlOfficerUser();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadAmlAlerts();
|
||||
this.loadAmlCtrReports();
|
||||
this.loadAmlStrCases();
|
||||
}
|
||||
|
||||
loadAmlAlerts(): void {
|
||||
this.amlService
|
||||
.getAlerts(this.amlAlertStatusFilter || undefined)
|
||||
.subscribe((data) => (this.amlAlerts = data || []));
|
||||
}
|
||||
|
||||
loadAmlCtrReports(): void {
|
||||
this.amlService.getCtrReports().subscribe((data) => (this.amlCtrReports = data || []));
|
||||
}
|
||||
|
||||
loadAmlStrCases(): void {
|
||||
this.amlService.getStrCases().subscribe((data) => (this.amlStrCases = data || []));
|
||||
}
|
||||
|
||||
whitelistAlert(alert: any): void {
|
||||
this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe(() => {
|
||||
this.toast(`Alert #${alert.id} whitelisted`);
|
||||
this.loadAmlAlerts();
|
||||
});
|
||||
}
|
||||
|
||||
escalateAlert(alert: any): void {
|
||||
this.amlService.reviewAlert(alert.id, 'ESCALATE', this.amlReviewNotes[alert.id] || '').subscribe(() => {
|
||||
this.toast(`Alert #${alert.id} escalated to Principal Officer`);
|
||||
this.loadAmlAlerts();
|
||||
this.loadAmlStrCases();
|
||||
});
|
||||
}
|
||||
|
||||
generateCtrReport(): void {
|
||||
if (!this.ctrGenerateMonth) {
|
||||
return;
|
||||
}
|
||||
this.amlService.generateCtrReport(this.ctrGenerateMonth).subscribe({
|
||||
next: () => {
|
||||
this.toast(`CTR report generated for ${this.ctrGenerateMonth}`);
|
||||
this.loadAmlCtrReports();
|
||||
},
|
||||
error: (err) => this.toast(err?.error?.defaultUserMessage || 'CTR report generation failed')
|
||||
});
|
||||
}
|
||||
|
||||
selectStrCase(strCase: any): void {
|
||||
this.selectedStrCase = strCase;
|
||||
this.concludeConclusion = 'NOT_SUSPICIOUS';
|
||||
this.concludeInvestigationNotes = strCase.investigationNotes || '';
|
||||
this.concludeSourceOfFundsNotes = strCase.sourceOfFundsNotes || '';
|
||||
this.fileReferenceNo = '';
|
||||
}
|
||||
|
||||
closeStrCaseDetail(): void {
|
||||
this.selectedStrCase = null;
|
||||
}
|
||||
|
||||
concludeStrCase(): void {
|
||||
if (!this.selectedStrCase) {
|
||||
return;
|
||||
}
|
||||
this.amlService
|
||||
.concludeStrCase(
|
||||
this.selectedStrCase.id,
|
||||
this.concludeConclusion,
|
||||
this.concludeInvestigationNotes,
|
||||
this.concludeSourceOfFundsNotes
|
||||
)
|
||||
.subscribe((updated) => {
|
||||
this.toast(
|
||||
this.concludeConclusion === 'SUSPICIOUS'
|
||||
? `STR case #${updated.id} concluded suspicious — FIU due date ${updated.fiuDueDate}`
|
||||
: `STR case #${updated.id} concluded not suspicious`
|
||||
);
|
||||
this.selectedStrCase = updated;
|
||||
this.loadAmlStrCases();
|
||||
});
|
||||
}
|
||||
|
||||
fileStrCase(): void {
|
||||
if (!this.selectedStrCase || !this.fileReferenceNo) {
|
||||
return;
|
||||
}
|
||||
this.amlService.fileStr(this.selectedStrCase.id, this.fileReferenceNo).subscribe((updated) => {
|
||||
this.toast(`STR case #${updated.id} recorded as filed (ref ${updated.strReferenceNo})`);
|
||||
this.selectedStrCase = updated;
|
||||
this.loadAmlStrCases();
|
||||
});
|
||||
}
|
||||
|
||||
amlStatusClass(status: string): string {
|
||||
return 'status-pill status-' + (status || '').toLowerCase().replace(/_/g, '-');
|
||||
}
|
||||
|
||||
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
+58
@@ -397,6 +397,64 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- ════════════ CONTINGENT LIABILITIES ════════════ -->
|
||||
<ng-container *ngIf="activeReport === 'CONTINGENT-LIAB'">
|
||||
<div class="acr-stats">
|
||||
<div class="acr-stat-card alert">
|
||||
<div class="acr-stat-l">Total Contingent Liabilities — {{ bank.fiscalYear }}</div>
|
||||
<div class="acr-stat-v">₹ {{ contingentLiab.current | number: '1.2-2' }} Lakh</div>
|
||||
<div class="acr-stat-meta">vs {{ contingentLiab.previous | number: '1.2-2' }} Lakh (PY)</div>
|
||||
</div>
|
||||
<div class="acr-stat-card" [ngClass]="clVariation <= 0 ? 'good' : 'alert'">
|
||||
<div class="acr-stat-l">Variation over Previous Year</div>
|
||||
<div class="acr-stat-v">{{ clVariation >= 0 ? '+' : '' }}{{ clVariation | number: '1.2-2' }} Lakh</div>
|
||||
<div class="acr-stat-meta">{{ clVariationPct >= 0 ? '+' : '' }}{{ clVariationPct.toFixed(2) }}%</div>
|
||||
</div>
|
||||
<div class="acr-stat-card">
|
||||
<div class="acr-stat-l">As On Date</div>
|
||||
<div class="acr-stat-v" style="font-size: 16px">{{ bank.asOnDate }}</div>
|
||||
<div class="acr-stat-meta">Schedule 12</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="acr-section">
|
||||
<div class="acr-sec-h">
|
||||
<h3 class="acr-sec-t">Contingent Liabilities — As on {{ bank.asOnDate }} (₹ in Lakhs)</h3>
|
||||
<button mat-stroked-button class="acr-btn-xls" (click)="exportContingentLiabCsv()">
|
||||
<fa-icon icon="file-csv"></fa-icon> Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="acr-tbl-wrap">
|
||||
<table class="acr-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" class="acr-sub-h">Items Covered Under This Schedule</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of contingentLiabItems">
|
||||
<td class="mono">{{ item.code }}</td>
|
||||
<td>{{ item.label }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="row-total expense">
|
||||
<td colspan="2">
|
||||
<strong>Total Contingent Liabilities: ₹ {{ contingentLiab.current | number: '1.2-2' }} Lakh</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<p class="acr-note">
|
||||
<fa-icon icon="info-circle"></fa-icon> Individual item-wise figures for this schedule are not broken
|
||||
out here — only the audited total is shown, to avoid presenting a sub-item split that could not be
|
||||
independently verified against the source statement.
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- ════════════ GL DR/CR ════════════ -->
|
||||
<ng-container *ngIf="activeReport === 'GL-DR-CR'">
|
||||
<div class="acr-stats">
|
||||
|
||||
+10
@@ -318,6 +318,16 @@
|
||||
border-bottom: 1px dashed var(--acr-g200);
|
||||
}
|
||||
|
||||
.acr-note {
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: var(--acr-g500);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.acr-btn-xls {
|
||||
font-size: 12px !important;
|
||||
height: 34px !important;
|
||||
|
||||
+157
-218
@@ -14,7 +14,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
export type AcctReportCode = 'PNL' | 'BALANCE-SHEET' | 'COMPARATIVE-BS' | 'GL-DR-CR' | 'MISC-EXP';
|
||||
export type AcctReportCode = 'PNL' | 'BALANCE-SHEET' | 'COMPARATIVE-BS' | 'CONTINGENT-LIAB' | 'GL-DR-CR' | 'MISC-EXP';
|
||||
|
||||
export interface AcctReportsDialogData {
|
||||
initialReport?: AcctReportCode;
|
||||
@@ -47,6 +47,11 @@ interface GlLine {
|
||||
closing: number;
|
||||
}
|
||||
|
||||
interface ContingentLiabItem {
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface MiscExpRow {
|
||||
txnDate: string;
|
||||
branch: string;
|
||||
@@ -81,7 +86,7 @@ export class AccountingReportsDialogComponent {
|
||||
activeReport: AcctReportCode;
|
||||
|
||||
bank = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
asOnDate: '31-03-2026',
|
||||
period: '01-04-2025 to 31-03-2026',
|
||||
@@ -94,7 +99,7 @@ export class AccountingReportsDialogComponent {
|
||||
poDesignation: 'Chief Finance Officer',
|
||||
auditor: 'M/s Sharma & Associates, Chartered Accountants',
|
||||
reportRefNo: '157/2',
|
||||
userCode: 'BUBL01',
|
||||
userCode: 'CBSN01',
|
||||
branchCount: 15
|
||||
};
|
||||
|
||||
@@ -120,6 +125,13 @@ export class AccountingReportsDialogComponent {
|
||||
description: 'Side-by-side comparison of current vs previous fiscal year.',
|
||||
group: 'Financial Statement'
|
||||
},
|
||||
{
|
||||
code: 'CONTINGENT-LIAB' as AcctReportCode,
|
||||
name: 'Contingent Liabilities',
|
||||
icon: 'exclamation-triangle',
|
||||
description: 'Off-balance-sheet claims, guarantees & contingent exposures (Schedule 12).',
|
||||
group: 'Financial Statement'
|
||||
},
|
||||
{
|
||||
code: 'GL-DR-CR' as AcctReportCode,
|
||||
name: 'GL Wise DR/CR',
|
||||
@@ -136,309 +148,192 @@ export class AccountingReportsDialogComponent {
|
||||
}
|
||||
];
|
||||
|
||||
// -------------------- P&L Data (₹ in Lakhs) --------------------
|
||||
// -------------------- P&L Data (₹ in Lakhs) — FY 2025-26 audited figures --------------------
|
||||
pnl: PnlLine[] = [
|
||||
{ code: 'I-01', group: 'INCOME', subgroup: 'Interest Earned', label: 'Interest on Advances', amount: 38420 },
|
||||
{ code: 'I-02', group: 'INCOME', subgroup: 'Interest Earned', label: 'Interest on Investments', amount: 14860 },
|
||||
{ code: 'I-01', group: 'INCOME', subgroup: 'Interest Earned', label: 'Interest on Advances & Bills', amount: 102641.73 },
|
||||
{ code: 'I-02', group: 'INCOME', subgroup: 'Interest Earned', label: 'Income on Investments', amount: 41936.75 },
|
||||
{
|
||||
code: 'I-03',
|
||||
group: 'INCOME',
|
||||
subgroup: 'Interest Earned',
|
||||
label: 'Interest on Balances with RBI / Other Banks',
|
||||
amount: 1240
|
||||
},
|
||||
{
|
||||
code: 'I-04',
|
||||
group: 'INCOME',
|
||||
subgroup: 'Other Income',
|
||||
label: 'Commission, Exchange & Brokerage',
|
||||
amount: 1820
|
||||
amount: 144.2
|
||||
},
|
||||
{ code: 'I-04', group: 'INCOME', subgroup: 'Other Income', label: 'Dividend Income', amount: 4.11 },
|
||||
{
|
||||
code: 'I-05',
|
||||
group: 'INCOME',
|
||||
subgroup: 'Other Income',
|
||||
label: 'Net Income on Sale of Fixed Assets',
|
||||
amount: 0.3
|
||||
},
|
||||
{ code: 'I-06', group: 'INCOME', subgroup: 'Other Income', label: 'Other Income (ATM/POS/UPI/Rental/Misc)', amount: 280 },
|
||||
{
|
||||
code: 'E-01',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Interest Expended',
|
||||
label: 'Interest Paid on Deposits & Other Borrowings',
|
||||
amount: 62861.8
|
||||
},
|
||||
{ code: 'I-05', group: 'INCOME', subgroup: 'Other Income', label: 'Profit on Sale of Investments', amount: 480 },
|
||||
{ code: 'I-06', group: 'INCOME', subgroup: 'Other Income', label: 'Dividend Income', amount: 142 },
|
||||
{ code: 'I-07', group: 'INCOME', subgroup: 'Other Income', label: 'Miscellaneous Income', amount: 318 },
|
||||
{ code: 'E-01', group: 'EXPENDITURE', subgroup: 'Interest Expended', label: 'Interest on Deposits', amount: 28640 },
|
||||
{
|
||||
code: 'E-02',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Interest Expended',
|
||||
label: 'Interest on RBI / Inter-Bank Borrowings',
|
||||
amount: 980
|
||||
label: 'Interest Paid on NABARD Borrowings',
|
||||
amount: 47083.71
|
||||
},
|
||||
{
|
||||
code: 'E-03',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Operating Expenses',
|
||||
label: 'Payments to & Provisions for Employees',
|
||||
amount: 8420
|
||||
label: 'Operating Expenses (Staff, Admin, Establishment)',
|
||||
amount: 14503.79
|
||||
},
|
||||
{
|
||||
code: 'E-04',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Operating Expenses',
|
||||
label: 'Rent, Taxes & Lighting',
|
||||
amount: 1240
|
||||
subgroup: 'Provisions',
|
||||
label: 'Provision for Standard Assets',
|
||||
amount: 730
|
||||
},
|
||||
{
|
||||
code: 'E-05',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Provisions',
|
||||
label: 'Provision for Doubtful & Bad Debts (Loans & Advances)',
|
||||
amount: 2068.2
|
||||
},
|
||||
{ code: 'E-05', group: 'EXPENDITURE', subgroup: 'Operating Expenses', label: 'Printing & Stationery', amount: 184 },
|
||||
{
|
||||
code: 'E-06',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Operating Expenses',
|
||||
label: 'Depreciation on Bank Property',
|
||||
amount: 642
|
||||
subgroup: 'Provisions',
|
||||
label: 'Bad Debts Written Off',
|
||||
amount: 906.44
|
||||
},
|
||||
{
|
||||
code: 'E-07',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Operating Expenses',
|
||||
label: 'Auditors Fees & Expenses',
|
||||
amount: 124
|
||||
},
|
||||
{
|
||||
code: 'E-08',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Operating Expenses',
|
||||
label: 'Repairs, Maintenance & Insurance',
|
||||
amount: 386
|
||||
},
|
||||
{
|
||||
code: 'E-09',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Operating Expenses',
|
||||
label: 'Other Operating Expenses',
|
||||
amount: 824
|
||||
},
|
||||
{
|
||||
code: 'E-10',
|
||||
group: 'EXPENDITURE',
|
||||
subgroup: 'Provisions',
|
||||
label: 'Provisions for NPA / Standard Assets',
|
||||
amount: 4280
|
||||
label: 'Provision for Staff Training Reserve',
|
||||
amount: 245.06
|
||||
},
|
||||
{ code: 'E-11', group: 'EXPENDITURE', subgroup: 'Provisions', label: 'Provisions for Tax', amount: 2120 }
|
||||
{ code: 'E-08', group: 'EXPENDITURE', subgroup: 'Provisions', label: 'Provision for Income Tax', amount: 4600 }
|
||||
];
|
||||
|
||||
// -------------------- Balance Sheet (₹ in Lakhs) --------------------
|
||||
// -------------------- Balance Sheet (₹ in Lakhs) — as on 31-03-2026, audited --------------------
|
||||
bs: BsLine[] = [
|
||||
{
|
||||
code: 'L-01',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Capital & Reserves',
|
||||
label: 'Capital (Paid-up Share Capital)',
|
||||
amount: 22000,
|
||||
prevAmount: 20800
|
||||
amount: 92837.18,
|
||||
prevAmount: 88842.87
|
||||
},
|
||||
{
|
||||
code: 'L-02',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Capital & Reserves',
|
||||
label: 'Reserves & Surplus',
|
||||
amount: 32400,
|
||||
prevAmount: 28640
|
||||
amount: 155847.97,
|
||||
prevAmount: 145524.37
|
||||
},
|
||||
{
|
||||
code: 'L-03',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Deposits',
|
||||
label: 'Demand Deposits — Current Accounts',
|
||||
amount: 84200,
|
||||
prevAmount: 78420
|
||||
label: 'Deposits',
|
||||
amount: 1154054.96,
|
||||
prevAmount: 997239.51
|
||||
},
|
||||
{
|
||||
code: 'L-04',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Deposits',
|
||||
label: 'Savings Bank Deposits',
|
||||
amount: 218600,
|
||||
prevAmount: 198400
|
||||
group: 'Borrowings',
|
||||
label: 'Borrowings (NABARD / Other Banks)',
|
||||
amount: 1113954.48,
|
||||
prevAmount: 993301.8
|
||||
},
|
||||
{
|
||||
code: 'L-05',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Deposits',
|
||||
label: 'Term Deposits',
|
||||
amount: 462800,
|
||||
prevAmount: 412400
|
||||
},
|
||||
{
|
||||
code: 'L-06',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Borrowings',
|
||||
label: 'Borrowings from RBI / NABARD',
|
||||
amount: 14180,
|
||||
prevAmount: 16240
|
||||
},
|
||||
{
|
||||
code: 'L-07',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Borrowings',
|
||||
label: 'Other Borrowings',
|
||||
amount: 10000,
|
||||
prevAmount: 9420
|
||||
},
|
||||
{
|
||||
code: 'L-08',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Other Liabilities',
|
||||
label: 'Bills Payable',
|
||||
amount: 4420,
|
||||
prevAmount: 3980
|
||||
},
|
||||
{
|
||||
code: 'L-09',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Other Liabilities',
|
||||
label: 'Inter-Office Adjustments (Net)',
|
||||
amount: 1820,
|
||||
prevAmount: 1640
|
||||
},
|
||||
{
|
||||
code: 'L-10',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Other Liabilities',
|
||||
label: 'Interest Accrued',
|
||||
amount: 6240,
|
||||
prevAmount: 5840
|
||||
},
|
||||
{
|
||||
code: 'L-11',
|
||||
side: 'CAPITAL-LIAB',
|
||||
group: 'Other Liabilities',
|
||||
label: 'Other Liabilities & Provisions',
|
||||
amount: 5940,
|
||||
prevAmount: 5420
|
||||
amount: 121037.61,
|
||||
prevAmount: 124706.3
|
||||
},
|
||||
{
|
||||
code: 'A-01',
|
||||
side: 'ASSETS',
|
||||
group: 'Cash & Bank Balances',
|
||||
label: 'Cash on Hand',
|
||||
amount: 8420,
|
||||
prevAmount: 7820
|
||||
label: 'Cash & Balances with RBI',
|
||||
amount: 32627.16,
|
||||
prevAmount: 33361.93
|
||||
},
|
||||
{
|
||||
code: 'A-02',
|
||||
side: 'ASSETS',
|
||||
group: 'Cash & Bank Balances',
|
||||
label: 'Balances with RBI',
|
||||
amount: 38400,
|
||||
prevAmount: 34200
|
||||
group: 'Money at Call',
|
||||
label: 'Balances with Other Banks & Money at Call/Short Notice',
|
||||
amount: 604619.45,
|
||||
prevAmount: 549599.47
|
||||
},
|
||||
{
|
||||
code: 'A-03',
|
||||
side: 'ASSETS',
|
||||
group: 'Cash & Bank Balances',
|
||||
label: 'Balances with Other Banks — Current',
|
||||
amount: 14800,
|
||||
prevAmount: 12400
|
||||
group: 'Investments',
|
||||
label: 'Investments',
|
||||
amount: 316953.61,
|
||||
prevAmount: 248274.61
|
||||
},
|
||||
{
|
||||
code: 'A-04',
|
||||
side: 'ASSETS',
|
||||
group: 'Cash & Bank Balances',
|
||||
label: 'Balances with Other Banks — Term Deposits',
|
||||
amount: 10000,
|
||||
prevAmount: 9200
|
||||
group: 'Advances',
|
||||
label: 'Loans & Advances',
|
||||
amount: 1627711.4,
|
||||
prevAmount: 1466401.79
|
||||
},
|
||||
{
|
||||
code: 'A-05',
|
||||
side: 'ASSETS',
|
||||
group: 'Money at Call',
|
||||
label: 'Money at Call & Short Notice',
|
||||
amount: 12400,
|
||||
prevAmount: 10800
|
||||
group: 'Fixed Assets',
|
||||
label: 'Fixed Assets (Property, Plant & Equipment)',
|
||||
amount: 33791.75,
|
||||
prevAmount: 33727.66
|
||||
},
|
||||
{
|
||||
code: 'A-06',
|
||||
side: 'ASSETS',
|
||||
group: 'Investments',
|
||||
label: 'Government Securities (SLR)',
|
||||
amount: 142400,
|
||||
prevAmount: 128600
|
||||
},
|
||||
{
|
||||
code: 'A-07',
|
||||
side: 'ASSETS',
|
||||
group: 'Investments',
|
||||
label: 'Other Approved Securities',
|
||||
amount: 28200,
|
||||
prevAmount: 24800
|
||||
},
|
||||
{
|
||||
code: 'A-08',
|
||||
side: 'ASSETS',
|
||||
group: 'Investments',
|
||||
label: 'Shares / Debentures / Bonds',
|
||||
amount: 14000,
|
||||
prevAmount: 12200
|
||||
},
|
||||
{
|
||||
code: 'A-09',
|
||||
side: 'ASSETS',
|
||||
group: 'Advances',
|
||||
label: 'Cash Credit, Overdraft & Demand Loans',
|
||||
amount: 168400,
|
||||
prevAmount: 152800
|
||||
},
|
||||
{
|
||||
code: 'A-10',
|
||||
side: 'ASSETS',
|
||||
group: 'Advances',
|
||||
label: 'Term Loans (Secured)',
|
||||
amount: 217865,
|
||||
prevAmount: 198400
|
||||
},
|
||||
{
|
||||
code: 'A-11',
|
||||
side: 'ASSETS',
|
||||
group: 'Advances',
|
||||
label: 'Term Loans (Unsecured)',
|
||||
amount: 30000,
|
||||
prevAmount: 27200
|
||||
},
|
||||
{
|
||||
code: 'A-12',
|
||||
side: 'ASSETS',
|
||||
group: 'Fixed Assets',
|
||||
label: 'Premises (Net of Depreciation)',
|
||||
amount: 9420,
|
||||
prevAmount: 8820
|
||||
},
|
||||
{
|
||||
code: 'A-13',
|
||||
side: 'ASSETS',
|
||||
group: 'Fixed Assets',
|
||||
label: 'Other Fixed Assets',
|
||||
amount: 4780,
|
||||
prevAmount: 4620
|
||||
},
|
||||
{
|
||||
code: 'A-14',
|
||||
side: 'ASSETS',
|
||||
group: 'Other Assets',
|
||||
label: 'Inter-Office Adjustments (Net)',
|
||||
amount: 0,
|
||||
prevAmount: 0
|
||||
},
|
||||
{
|
||||
code: 'A-15',
|
||||
side: 'ASSETS',
|
||||
group: 'Other Assets',
|
||||
label: 'Tax Paid in Advance & TDS',
|
||||
amount: 5420,
|
||||
prevAmount: 4820
|
||||
},
|
||||
{ code: 'A-16', side: 'ASSETS', group: 'Other Assets', label: 'Stationery & Stamps', amount: 184, prevAmount: 142 },
|
||||
{
|
||||
code: 'A-17',
|
||||
side: 'ASSETS',
|
||||
group: 'Other Assets',
|
||||
label: 'Sundry Debtors & Other Assets',
|
||||
amount: 2876,
|
||||
prevAmount: 2540
|
||||
label: 'Other Assets',
|
||||
amount: 22028.83,
|
||||
prevAmount: 18249.4
|
||||
}
|
||||
];
|
||||
|
||||
// -------------------- Contingent Liabilities (Schedule 12, ₹ in Lakhs) --------------------
|
||||
// Only the audited TOTAL is reported here — the schedule's individual line items (claims not
|
||||
// acknowledged as debts, letter of guarantees, DEAF transfers, gratuity/RBI-penalty/GST disputes)
|
||||
// could not be re-verified against the source figures with confidence, so they are omitted
|
||||
// rather than risk showing an unverified breakdown.
|
||||
contingentLiabItems: ContingentLiabItem[] = [
|
||||
{ code: 'I', label: 'Claims against the bank not acknowledged as debts' },
|
||||
{ code: 'II', label: 'Letter of Guarantees' },
|
||||
{ code: 'III', label: 'Amounts transferred to Depositor Education and Awareness Fund (DEAF)' },
|
||||
{ code: 'IV', label: 'Other items for which the bank is contingently liable (Gratuity / RBI Penalty / GST Demand)' }
|
||||
];
|
||||
contingentLiab = {
|
||||
current: 1633.3,
|
||||
previous: 1522.13
|
||||
};
|
||||
get clVariation(): number {
|
||||
return this.variation(this.contingentLiab.current, this.contingentLiab.previous);
|
||||
}
|
||||
get clVariationPct(): number {
|
||||
return this.variationPct(this.contingentLiab.current, this.contingentLiab.previous);
|
||||
}
|
||||
|
||||
// -------------------- GL Wise DR/CR Data --------------------
|
||||
glReport: GlLine[] = [
|
||||
{
|
||||
@@ -1008,6 +903,7 @@ export class AccountingReportsDialogComponent {
|
||||
if (this.activeReport === 'PNL') return this.exportPnlCsv();
|
||||
if (this.activeReport === 'BALANCE-SHEET') return this.exportBsCsv(false);
|
||||
if (this.activeReport === 'COMPARATIVE-BS') return this.exportBsCsv(true);
|
||||
if (this.activeReport === 'CONTINGENT-LIAB') return this.exportContingentLiabCsv();
|
||||
if (this.activeReport === 'GL-DR-CR') return this.exportGlCsv();
|
||||
if (this.activeReport === 'MISC-EXP') return this.exportMiscCsv();
|
||||
}
|
||||
@@ -1155,6 +1051,49 @@ export class AccountingReportsDialogComponent {
|
||||
this.saveCsv(rows, `${comparative ? 'Comparative-' : ''}Balance-Sheet-${this.dateTag()}.csv`);
|
||||
}
|
||||
|
||||
exportContingentLiabCsv(): void {
|
||||
const rows: string[][] = [];
|
||||
rows.push([
|
||||
this.bank.name,
|
||||
this.bank.branch
|
||||
]);
|
||||
rows.push([
|
||||
'CONTINGENT LIABILITIES (SCHEDULE 12)',
|
||||
`As on ${this.bank.asOnDate}`
|
||||
]);
|
||||
rows.push([]);
|
||||
rows.push(['Items covered:']);
|
||||
for (const item of this.contingentLiabItems) {
|
||||
rows.push([
|
||||
item.code,
|
||||
item.label
|
||||
]);
|
||||
}
|
||||
rows.push([]);
|
||||
rows.push([
|
||||
'',
|
||||
'TOTAL CONTINGENT LIABILITIES',
|
||||
`FY ${this.bank.fiscalYear} (₹ Lakh)`,
|
||||
`FY ${this.bank.prevFiscalYear} (₹ Lakh)`,
|
||||
'Variation',
|
||||
'Variation %'
|
||||
]);
|
||||
rows.push([
|
||||
'',
|
||||
'',
|
||||
this.contingentLiab.current.toFixed(2),
|
||||
this.contingentLiab.previous.toFixed(2),
|
||||
this.clVariation.toFixed(2),
|
||||
this.clVariationPct.toFixed(2) + '%'
|
||||
]);
|
||||
rows.push([]);
|
||||
rows.push([
|
||||
'Certified by',
|
||||
this.bank.principalOfficer + ' — ' + this.bank.poDesignation
|
||||
]);
|
||||
this.saveCsv(rows, `Contingent-Liabilities-${this.dateTag()}.csv`);
|
||||
}
|
||||
|
||||
exportGlCsv(): void {
|
||||
const rows: string[][] = [];
|
||||
rows.push([
|
||||
|
||||
@@ -54,7 +54,7 @@ export class AlmReportDialogComponent {
|
||||
activeTab: 'dashboard' | 'ladder' | 'monthly' | 'returns' = 'dashboard';
|
||||
|
||||
bank = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
asOnDate: '31-03-2026',
|
||||
quarter: 'Q4 FY 2025-26',
|
||||
@@ -63,7 +63,7 @@ export class AlmReportDialogComponent {
|
||||
principalOfficer: 'Karthik Kamath',
|
||||
formCode: 'Form ALM-1',
|
||||
reportRefNo: '1036',
|
||||
userCode: 'BUBL01',
|
||||
userCode: 'CBSN01',
|
||||
/** Net Demand & Time Liabilities — denominator for ratios */
|
||||
ndtl: 740000, // ₹7,400 Cr in Lakhs
|
||||
/** RBI tolerance: cumulative negative gap up to 14 days ≤ 5%, 28 days ≤ 10% */
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
export type CimsFormCode = 'OSS-2' | 'PSA' | 'RAL' | 'RCP-UCB';
|
||||
export type CimsFormCode = 'OSS-2' | 'PSA' | 'RAL' | 'RCP-UCB' | 'OSS-4' | 'OSS-5' | 'OSS-6' | 'OSS-7';
|
||||
|
||||
export interface CimsXbrlDialogData {
|
||||
initialForm?: CimsFormCode;
|
||||
@@ -64,20 +64,20 @@ export class CimsXbrlDialogComponent {
|
||||
activeTab: 'filing' | 'data' | 'preview' = 'filing';
|
||||
|
||||
bank = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
bsrCode: 'BUBL0000123',
|
||||
ifsc: 'BUBL0000123',
|
||||
legalEntityId: 'BUBL-LEI-2025',
|
||||
bsrCode: 'CBSN0000123',
|
||||
ifsc: 'CBSN0000123',
|
||||
legalEntityId: 'CBSN-LEI-2025',
|
||||
pan: 'AAACB1234L',
|
||||
asOnDate: '31-03-2026',
|
||||
quarter: 'Q4 FY 2025-26',
|
||||
fiscalYear: '2025-26',
|
||||
principalOfficer: 'Karthik Kamath',
|
||||
poDesignation: 'Chief Compliance Officer',
|
||||
poEmail: 'compliance@bharaturbanbank.in',
|
||||
poEmail: 'compliance@cbsnexorabank.in',
|
||||
poMobile: '+91-9988002211',
|
||||
cimsUserId: 'BUBL_CIMS_001',
|
||||
cimsUserId: 'CBSN_CIMS_001',
|
||||
cimsPortal: 'https://cims.rbi.org.in/'
|
||||
};
|
||||
|
||||
@@ -97,7 +97,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:CapAdq.CRAR',
|
||||
section: 'Capital Adequacy',
|
||||
label: 'CRAR (Total)',
|
||||
value: 14.82,
|
||||
value: 21.82,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -105,7 +105,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:CapAdq.Tier1',
|
||||
section: 'Capital Adequacy',
|
||||
label: 'Tier-I CRAR',
|
||||
value: 12.18,
|
||||
value: 20.51,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -113,7 +113,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:CapAdq.Tier2',
|
||||
section: 'Capital Adequacy',
|
||||
label: 'Tier-II CRAR',
|
||||
value: 2.64,
|
||||
value: 1.31,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -121,7 +121,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.Total',
|
||||
section: 'Asset Classification',
|
||||
label: 'Total Advances (₹ Lakh)',
|
||||
value: 416265,
|
||||
value: 4112.04,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -129,7 +129,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.Standard',
|
||||
section: 'Asset Classification',
|
||||
label: 'Standard Assets (₹ Lakh)',
|
||||
value: 401820,
|
||||
value: 3845.88,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -137,7 +137,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.SubStd',
|
||||
section: 'Asset Classification',
|
||||
label: 'Sub-Standard (₹ Lakh)',
|
||||
value: 6220,
|
||||
value: 102.41,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -145,7 +145,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.Doubtful',
|
||||
section: 'Asset Classification',
|
||||
label: 'Doubtful (₹ Lakh)',
|
||||
value: 5840,
|
||||
value: 163.75,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -153,7 +153,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.Loss',
|
||||
section: 'Asset Classification',
|
||||
label: 'Loss (₹ Lakh)',
|
||||
value: 2385,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -161,7 +161,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.GNPA',
|
||||
section: 'NPA Position',
|
||||
label: 'Gross NPA (₹ Lakh)',
|
||||
value: 14445,
|
||||
value: 266.16,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -169,7 +169,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.NNPA',
|
||||
section: 'NPA Position',
|
||||
label: 'Net NPA (₹ Lakh)',
|
||||
value: 5212,
|
||||
value: -96.33,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -177,7 +177,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.GNPAPct',
|
||||
section: 'NPA Position',
|
||||
label: 'GNPA %',
|
||||
value: 3.47,
|
||||
value: 6.47,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -185,7 +185,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.NNPAPct',
|
||||
section: 'NPA Position',
|
||||
label: 'NNPA %',
|
||||
value: 1.27,
|
||||
value: -2.6,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -193,7 +193,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Asset.PCR',
|
||||
section: 'NPA Position',
|
||||
label: 'Provision Coverage Ratio',
|
||||
value: 64.13,
|
||||
value: 136.24,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -201,7 +201,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Profit.NIM',
|
||||
section: 'Profitability',
|
||||
label: 'Net Interest Margin',
|
||||
value: 3.21,
|
||||
value: 0.51,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -209,7 +209,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Profit.ROA',
|
||||
section: 'Profitability',
|
||||
label: 'Return on Assets',
|
||||
value: 0.92,
|
||||
value: 0.67,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -217,7 +217,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'oss2:Profit.ROE',
|
||||
section: 'Profitability',
|
||||
label: 'Return on Equity',
|
||||
value: 11.48,
|
||||
value: 5.38,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -254,7 +254,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:Reference.ANBC',
|
||||
section: 'Reference',
|
||||
label: 'Adjusted Net Bank Credit (₹ Lakh)',
|
||||
value: 600000,
|
||||
value: 3274.83,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -262,7 +262,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:Reference.OBE',
|
||||
section: 'Reference',
|
||||
label: 'Credit Equivalent of OBE (₹ Lakh)',
|
||||
value: 25000,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -270,7 +270,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.Total',
|
||||
section: 'PSL Achievement',
|
||||
label: 'Total PSL Outstanding (₹ Lakh)',
|
||||
value: 241700,
|
||||
value: 2485.67,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -278,7 +278,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.TotalPct',
|
||||
section: 'PSL Achievement',
|
||||
label: 'Total PSL % of ANBC',
|
||||
value: 40.28,
|
||||
value: 75.9,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -286,7 +286,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.Agri',
|
||||
section: 'Sub-targets',
|
||||
label: 'Agriculture (₹ Lakh)',
|
||||
value: 124000,
|
||||
value: 39.0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -294,7 +294,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.AgriPct',
|
||||
section: 'Sub-targets',
|
||||
label: 'Agriculture % of ANBC',
|
||||
value: 20.67,
|
||||
value: 1.19,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -302,7 +302,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.SMF',
|
||||
section: 'Sub-targets',
|
||||
label: 'Small & Marginal Farmers (₹ Lakh)',
|
||||
value: 72000,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -310,23 +310,23 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.SMFPct',
|
||||
section: 'Sub-targets',
|
||||
label: 'SMF % of ANBC',
|
||||
value: 12.0,
|
||||
value: 0,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
{
|
||||
ref: 'psa:PSL.MicroEnt',
|
||||
section: 'Sub-targets',
|
||||
label: 'Micro Enterprises (₹ Lakh)',
|
||||
value: 51000,
|
||||
label: 'MSME Enterprises (₹ Lakh)',
|
||||
value: 2313.83,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'psa:PSL.MicroPct',
|
||||
section: 'Sub-targets',
|
||||
label: 'Micro Enterprises % of ANBC',
|
||||
value: 8.5,
|
||||
label: 'MSME Enterprises % of ANBC',
|
||||
value: 70.65,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -334,7 +334,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.Weaker',
|
||||
section: 'Sub-targets',
|
||||
label: 'Weaker Sections (₹ Lakh)',
|
||||
value: 85500,
|
||||
value: 990.28,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -342,7 +342,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.WeakerPct',
|
||||
section: 'Sub-targets',
|
||||
label: 'Weaker Sections % of ANBC',
|
||||
value: 14.25,
|
||||
value: 30.22,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
@@ -350,7 +350,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.Housing',
|
||||
section: 'Other Categories',
|
||||
label: 'Housing (₹ Lakh)',
|
||||
value: 9200,
|
||||
value: 115.12,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -358,7 +358,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.Education',
|
||||
section: 'Other Categories',
|
||||
label: 'Education (₹ Lakh)',
|
||||
value: 4200,
|
||||
value: 17.32,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -366,7 +366,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.Export',
|
||||
section: 'Other Categories',
|
||||
label: 'Export Credit (₹ Lakh)',
|
||||
value: 3800,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -374,7 +374,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'psa:PSL.RenEnergy',
|
||||
section: 'Other Categories',
|
||||
label: 'Renewable Energy (₹ Lakh)',
|
||||
value: 2400,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
}
|
||||
@@ -395,15 +395,15 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Liab.Capital',
|
||||
section: 'Liabilities',
|
||||
label: 'Capital (₹ Lakh)',
|
||||
value: 22000,
|
||||
value: 272.78,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'ral:Liab.Reserves',
|
||||
section: 'Liabilities',
|
||||
label: 'Reserves & Surplus (₹ Lakh)',
|
||||
value: 32400,
|
||||
label: 'Reserves, Surplus & Profit (₹ Lakh)',
|
||||
value: 712.6,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -411,7 +411,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Liab.DepDemand',
|
||||
section: 'Liabilities',
|
||||
label: 'Demand Deposits (₹ Lakh)',
|
||||
value: 84200,
|
||||
value: 195.92,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -419,7 +419,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Liab.DepSavings',
|
||||
section: 'Liabilities',
|
||||
label: 'Savings Deposits (₹ Lakh)',
|
||||
value: 218600,
|
||||
value: 2081.16,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -427,7 +427,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Liab.DepTerm',
|
||||
section: 'Liabilities',
|
||||
label: 'Term Deposits (₹ Lakh)',
|
||||
value: 462800,
|
||||
value: 4066.01,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -435,7 +435,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Liab.Borrow',
|
||||
section: 'Liabilities',
|
||||
label: 'Borrowings (₹ Lakh)',
|
||||
value: 24180,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -443,7 +443,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Liab.Other',
|
||||
section: 'Liabilities',
|
||||
label: 'Other Liabilities & Provisions (₹ Lakh)',
|
||||
value: 18420,
|
||||
value: 780.73,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -451,7 +451,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Asset.Cash',
|
||||
section: 'Assets',
|
||||
label: 'Cash on Hand (₹ Lakh)',
|
||||
value: 8420,
|
||||
value: 99.96,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -459,15 +459,15 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Asset.RBI',
|
||||
section: 'Assets',
|
||||
label: 'Balances with RBI (₹ Lakh)',
|
||||
value: 38400,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'ral:Asset.Banks',
|
||||
section: 'Assets',
|
||||
label: 'Balances with Banks (₹ Lakh)',
|
||||
value: 24800,
|
||||
label: 'Balances with Banks & Other Institutions (₹ Lakh)',
|
||||
value: 1349.53,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -475,7 +475,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Asset.Money',
|
||||
section: 'Assets',
|
||||
label: 'Money at Call & Short Notice (₹ Lakh)',
|
||||
value: 12400,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -483,7 +483,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Asset.Invest',
|
||||
section: 'Assets',
|
||||
label: 'Investments (₹ Lakh)',
|
||||
value: 184600,
|
||||
value: 2342.97,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -491,7 +491,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Asset.Adv',
|
||||
section: 'Assets',
|
||||
label: 'Advances (₹ Lakh)',
|
||||
value: 416265,
|
||||
value: 3912.88,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -499,23 +499,23 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:Asset.Fixed',
|
||||
section: 'Assets',
|
||||
label: 'Fixed Assets (₹ Lakh)',
|
||||
value: 14200,
|
||||
value: 49.48,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'ral:Asset.Other',
|
||||
section: 'Assets',
|
||||
label: 'Other Assets (₹ Lakh)',
|
||||
value: 12480,
|
||||
label: 'Other Assets & Interest Receivable (₹ Lakh)',
|
||||
value: 354.38,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'ral:OBE.Guar',
|
||||
section: 'Off-Balance Sheet',
|
||||
label: 'Bank Guarantees (₹ Lakh)',
|
||||
value: 18420,
|
||||
label: 'Contingent Liabilities (₹ Lakh)',
|
||||
value: 26.38,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
@@ -523,7 +523,7 @@ export class CimsXbrlDialogComponent {
|
||||
ref: 'ral:OBE.LC',
|
||||
section: 'Off-Balance Sheet',
|
||||
label: 'Letters of Credit (₹ Lakh)',
|
||||
value: 6580,
|
||||
value: 0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
}
|
||||
@@ -653,6 +653,293 @@ export class CimsXbrlDialogComponent {
|
||||
type: 'monetary'
|
||||
}
|
||||
]
|
||||
},
|
||||
'OSS-4': {
|
||||
code: 'OSS-4',
|
||||
name: 'OSS-4',
|
||||
fullName: 'Connected Lending & Large Exposure Statement',
|
||||
description:
|
||||
'Quarterly statement of loans to directors/relatives, senior officers, and exposures exceeding prudential thresholds. Illustrative figures — no source filing was provided for this return; values are modelled on this bank\'s own capital/advances scale for consistency.',
|
||||
cimsCode: 'CIMS_OSS_004',
|
||||
taxonomy: 'rbi-cims-oss4-2024-04-01',
|
||||
periodicity: 'Quarterly',
|
||||
lastFiled: '15-01-2026',
|
||||
nextDue: '15-04-2026',
|
||||
fields: [
|
||||
{
|
||||
ref: 'oss4:Connected.Directors',
|
||||
section: 'Connected Lending',
|
||||
label: 'Loans to Directors & Relatives (₹ Lakh)',
|
||||
value: 42.5,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss4:Connected.SrOfficers',
|
||||
section: 'Connected Lending',
|
||||
label: 'Loans to Senior Officers (₹ Lakh)',
|
||||
value: 18.75,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss4:Connected.Accounts',
|
||||
section: 'Connected Lending',
|
||||
label: 'Number of Connected Lending Accounts',
|
||||
value: 12,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss4:LargeExp.Amount',
|
||||
section: 'Large Exposures',
|
||||
label: 'Exposures > 10% of Capital Funds (₹ Lakh)',
|
||||
value: 285.0,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss4:LargeExp.Pct',
|
||||
section: 'Large Exposures',
|
||||
label: 'Large Exposures as % of Capital Funds',
|
||||
value: 30.58,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
},
|
||||
{
|
||||
ref: 'oss4:LargeExp.SingleBreach',
|
||||
section: 'Large Exposures',
|
||||
label: 'Single Borrower Limit Breaches',
|
||||
value: 0,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss4:LargeExp.GroupBreach',
|
||||
section: 'Large Exposures',
|
||||
label: 'Group Borrower Limit Breaches',
|
||||
value: 0,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
}
|
||||
]
|
||||
},
|
||||
'OSS-5': {
|
||||
code: 'OSS-5',
|
||||
name: 'OSS-5',
|
||||
fullName: 'Institutional & Membership Profile',
|
||||
description:
|
||||
'Annual statement of branch network, staff strength, membership and depositor/borrower base. Illustrative figures — no source filing was provided for this return; membership share capital is aligned to this bank\'s real paid-up capital figure.',
|
||||
cimsCode: 'CIMS_OSS_005',
|
||||
taxonomy: 'rbi-cims-oss5-2024-04-01',
|
||||
periodicity: 'Annual',
|
||||
lastFiled: '30-04-2025',
|
||||
nextDue: '30-04-2026',
|
||||
fields: [
|
||||
{
|
||||
ref: 'oss5:Profile.Branches',
|
||||
section: 'Network',
|
||||
label: 'Number of Branches',
|
||||
value: 8,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss5:Profile.Employees',
|
||||
section: 'Network',
|
||||
label: 'Number of Employees',
|
||||
value: 64,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss5:Profile.AreaOfOperation',
|
||||
section: 'Network',
|
||||
label: 'Area of Operation',
|
||||
value: 'Single District',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
ref: 'oss5:Profile.Members',
|
||||
section: 'Membership',
|
||||
label: 'Number of Regular Members',
|
||||
value: 18450,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss5:Profile.ShareCapital',
|
||||
section: 'Membership',
|
||||
label: 'Membership Share Capital (₹ Lakh)',
|
||||
value: 272.78,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss5:Profile.Depositors',
|
||||
section: 'Customer Base',
|
||||
label: 'Number of Depositors',
|
||||
value: 42180,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss5:Profile.Borrowers',
|
||||
section: 'Customer Base',
|
||||
label: 'Number of Borrowers',
|
||||
value: 3860,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
}
|
||||
]
|
||||
},
|
||||
'OSS-6': {
|
||||
code: 'OSS-6',
|
||||
name: 'OSS-6',
|
||||
fullName: 'Structural Liquidity & Interest Rate Sensitivity Statement',
|
||||
description:
|
||||
'Fortnightly maturity-bucket gap analysis of rate-sensitive assets and liabilities. Illustrative figures — no source filing was provided for this return.',
|
||||
cimsCode: 'CIMS_OSS_006',
|
||||
taxonomy: 'rbi-cims-oss6-2024-04-01',
|
||||
periodicity: 'Monthly',
|
||||
lastFiled: '14-04-2026',
|
||||
nextDue: '28-04-2026',
|
||||
fields: [
|
||||
{
|
||||
ref: 'oss6:Gap.1to14Days',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: '1-14 Days',
|
||||
value: -45.2,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.15to28Days',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: '15-28 Days',
|
||||
value: -12.8,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.29DaysTo3M',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: '29 Days to 3 Months',
|
||||
value: 88.4,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.Over3To6M',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: 'Over 3 Months to 6 Months',
|
||||
value: 62.15,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.Over6MTo1Y',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: 'Over 6 Months to 1 Year',
|
||||
value: -28.9,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.Over1To3Y',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: 'Over 1 Year to 3 Years',
|
||||
value: 210.6,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.Over3To5Y',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: 'Over 3 Years to 5 Years',
|
||||
value: 95.3,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Gap.Over5Y',
|
||||
section: 'Maturity Gap (₹ Lakh)',
|
||||
label: 'Over 5 Years',
|
||||
value: -18.75,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss6:Liq.CumNegGapPct',
|
||||
section: 'Liquidity Ratios',
|
||||
label: 'Cumulative Negative Gap (14-Day) as % of Cumulative Outflows',
|
||||
value: 4.85,
|
||||
unit: 'Percent',
|
||||
type: 'percent'
|
||||
}
|
||||
]
|
||||
},
|
||||
'OSS-7': {
|
||||
code: 'OSS-7',
|
||||
name: 'OSS-7',
|
||||
fullName: 'Statement of Frauds & Customer Complaints',
|
||||
description:
|
||||
'Quarterly statement of frauds reported and customer-complaint disposal. Illustrative figures — no source filing was provided for this return.',
|
||||
cimsCode: 'CIMS_OSS_007',
|
||||
taxonomy: 'rbi-cims-oss7-2024-04-01',
|
||||
periodicity: 'Quarterly',
|
||||
lastFiled: '15-01-2026',
|
||||
nextDue: '15-04-2026',
|
||||
fields: [
|
||||
{
|
||||
ref: 'oss7:Fraud.Count',
|
||||
section: 'Frauds',
|
||||
label: 'Number of Frauds Reported',
|
||||
value: 1,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss7:Fraud.Amount',
|
||||
section: 'Frauds',
|
||||
label: 'Amount Involved in Frauds (₹ Lakh)',
|
||||
value: 2.4,
|
||||
unit: 'INR-Lakh',
|
||||
type: 'monetary'
|
||||
},
|
||||
{
|
||||
ref: 'oss7:Complaints.Received',
|
||||
section: 'Customer Complaints',
|
||||
label: 'Complaints Received (Quarter)',
|
||||
value: 14,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss7:Complaints.Resolved',
|
||||
section: 'Customer Complaints',
|
||||
label: 'Complaints Resolved',
|
||||
value: 12,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss7:Complaints.Pending',
|
||||
section: 'Customer Complaints',
|
||||
label: 'Complaints Pending',
|
||||
value: 2,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
ref: 'oss7:Complaints.PendingOver30',
|
||||
section: 'Customer Complaints',
|
||||
label: 'Complaints Pending > 30 Days',
|
||||
value: 0,
|
||||
unit: 'Number',
|
||||
type: 'numeric'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -660,7 +947,11 @@ export class CimsXbrlDialogComponent {
|
||||
'OSS-2',
|
||||
'PSA',
|
||||
'RAL',
|
||||
'RCP-UCB'
|
||||
'RCP-UCB',
|
||||
'OSS-4',
|
||||
'OSS-5',
|
||||
'OSS-6',
|
||||
'OSS-7'
|
||||
];
|
||||
|
||||
data = inject<CimsXbrlDialogData | null>(MAT_DIALOG_DATA, { optional: true });
|
||||
|
||||
@@ -76,7 +76,7 @@ export class CkycReportsDialogComponent {
|
||||
activeReport: CkycReportCode;
|
||||
|
||||
bank = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
asOnDate: '29-01-2026',
|
||||
fiscalYear: '2025-26',
|
||||
@@ -84,7 +84,7 @@ export class CkycReportsDialogComponent {
|
||||
principalOfficer: 'Karthik Kamath',
|
||||
poDesignation: 'Principal Compliance Officer',
|
||||
reportRefNo: '2075/1',
|
||||
userCode: 'BUBL01',
|
||||
userCode: 'CBSN01',
|
||||
fyStart: '01-04-2025',
|
||||
fyEnd: '31-03-2026'
|
||||
};
|
||||
@@ -121,7 +121,7 @@ export class CkycReportsDialogComponent {
|
||||
|
||||
rekyc: RekycRow[] = [
|
||||
{
|
||||
ucode: 'BUBL000201',
|
||||
ucode: 'CBSN000201',
|
||||
name: 'Ravi Kumar Sharma',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'LOW',
|
||||
@@ -132,7 +132,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL000418',
|
||||
ucode: 'CBSN000418',
|
||||
name: 'Priya Patel',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'MEDIUM',
|
||||
@@ -143,7 +143,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL000522',
|
||||
ucode: 'CBSN000522',
|
||||
name: 'Arjun Deshmukh',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'LOW',
|
||||
@@ -154,7 +154,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL000711',
|
||||
ucode: 'CBSN000711',
|
||||
name: 'Vikram Reddy',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'HIGH',
|
||||
@@ -165,7 +165,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL000814',
|
||||
ucode: 'CBSN000814',
|
||||
name: 'Lakshmi Iyer',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'LOW',
|
||||
@@ -176,7 +176,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL001012',
|
||||
ucode: 'CBSN001012',
|
||||
name: 'Nirav Shah',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'MEDIUM',
|
||||
@@ -187,7 +187,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL001145',
|
||||
ucode: 'CBSN001145',
|
||||
name: 'Kavita Verma',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'HIGH',
|
||||
@@ -198,7 +198,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL001458',
|
||||
ucode: 'CBSN001458',
|
||||
name: 'Sameer Kapoor',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'MEDIUM',
|
||||
@@ -209,7 +209,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL001721',
|
||||
ucode: 'CBSN001721',
|
||||
name: 'Karthik Subramanian',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'LOW',
|
||||
@@ -220,7 +220,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL002012',
|
||||
ucode: 'CBSN002012',
|
||||
name: 'Abhishek Banerjee',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'MEDIUM',
|
||||
@@ -231,7 +231,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL002314',
|
||||
ucode: 'CBSN002314',
|
||||
name: 'Gaurav Thakur',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'LOW',
|
||||
@@ -242,7 +242,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL002615',
|
||||
ucode: 'CBSN002615',
|
||||
name: 'Manmohan Singh Gill',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'HIGH',
|
||||
@@ -253,7 +253,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL002918',
|
||||
ucode: 'CBSN002918',
|
||||
name: 'Sunita Devi',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'LOW',
|
||||
@@ -264,7 +264,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'BELOW-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL003221',
|
||||
ucode: 'CBSN003221',
|
||||
name: 'Deepak Tiwari',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'MEDIUM',
|
||||
@@ -275,7 +275,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL003524',
|
||||
ucode: 'CBSN003524',
|
||||
name: 'Harpreet Kaur',
|
||||
customerType: 'INDIVIDUAL',
|
||||
risk: 'MEDIUM',
|
||||
@@ -286,7 +286,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL003812',
|
||||
ucode: 'CBSN003812',
|
||||
name: 'Saraswati Mahila Co-op Society',
|
||||
customerType: 'SOCIETY',
|
||||
risk: 'MEDIUM',
|
||||
@@ -297,7 +297,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL004115',
|
||||
ucode: 'CBSN004115',
|
||||
name: 'Aditya Enterprises',
|
||||
customerType: 'FIRM',
|
||||
risk: 'HIGH',
|
||||
@@ -308,7 +308,7 @@ export class CkycReportsDialogComponent {
|
||||
segment: 'ABOVE-25-LACS'
|
||||
},
|
||||
{
|
||||
ucode: 'BUBL004318',
|
||||
ucode: 'CBSN004318',
|
||||
name: 'Vasudha Trading HUF',
|
||||
customerType: 'HUF',
|
||||
risk: 'MEDIUM',
|
||||
@@ -332,7 +332,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '01',
|
||||
branch: 'Mumbai Main',
|
||||
acType: 'SB',
|
||||
clientId: 'BUBL000201',
|
||||
clientId: 'CBSN000201',
|
||||
customerName: 'Ravi Kumar Sharma',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'LOW',
|
||||
@@ -345,7 +345,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '02',
|
||||
branch: 'Pune FC Road',
|
||||
acType: 'CA',
|
||||
clientId: 'BUBL000418',
|
||||
clientId: 'CBSN000418',
|
||||
customerName: 'Priya Patel',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'MEDIUM',
|
||||
@@ -358,7 +358,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '03',
|
||||
branch: 'Nagpur Dharampeth',
|
||||
acType: 'TL',
|
||||
clientId: 'BUBL000522',
|
||||
clientId: 'CBSN000522',
|
||||
customerName: 'Arjun Deshmukh',
|
||||
oldRisk: 'MEDIUM',
|
||||
newRisk: 'LOW',
|
||||
@@ -371,7 +371,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '04',
|
||||
branch: 'Bengaluru Jayanagar',
|
||||
acType: 'OD',
|
||||
clientId: 'BUBL000711',
|
||||
clientId: 'CBSN000711',
|
||||
customerName: 'Vikram Reddy',
|
||||
oldRisk: 'MEDIUM',
|
||||
newRisk: 'HIGH',
|
||||
@@ -384,7 +384,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '05',
|
||||
branch: 'Bengaluru Indiranagar',
|
||||
acType: 'SB',
|
||||
clientId: 'BUBL000814',
|
||||
clientId: 'CBSN000814',
|
||||
customerName: 'Lakshmi Iyer',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'LOW',
|
||||
@@ -397,7 +397,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '06',
|
||||
branch: 'Ahmedabad Satellite',
|
||||
acType: 'CC',
|
||||
clientId: 'BUBL001012',
|
||||
clientId: 'CBSN001012',
|
||||
customerName: 'Nirav Shah',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'MEDIUM',
|
||||
@@ -410,7 +410,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '07',
|
||||
branch: 'Lucknow Gomti Nagar',
|
||||
acType: 'CA',
|
||||
clientId: 'BUBL001145',
|
||||
clientId: 'CBSN001145',
|
||||
customerName: 'Kavita Verma',
|
||||
oldRisk: 'MEDIUM',
|
||||
newRisk: 'HIGH',
|
||||
@@ -423,7 +423,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '08',
|
||||
branch: 'Delhi Greater Kailash',
|
||||
acType: 'OD',
|
||||
clientId: 'BUBL001458',
|
||||
clientId: 'CBSN001458',
|
||||
customerName: 'Sameer Kapoor',
|
||||
oldRisk: 'MEDIUM',
|
||||
newRisk: 'MEDIUM',
|
||||
@@ -436,7 +436,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '09',
|
||||
branch: 'Chennai Valasaravakkam',
|
||||
acType: 'SB',
|
||||
clientId: 'BUBL001721',
|
||||
clientId: 'CBSN001721',
|
||||
customerName: 'Karthik Subramanian',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'LOW',
|
||||
@@ -449,7 +449,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '10',
|
||||
branch: 'Kolkata Salt Lake',
|
||||
acType: 'TL',
|
||||
clientId: 'BUBL002012',
|
||||
clientId: 'CBSN002012',
|
||||
customerName: 'Abhishek Banerjee',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'MEDIUM',
|
||||
@@ -462,7 +462,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '11',
|
||||
branch: 'Shimla Sector 4',
|
||||
acType: 'SB',
|
||||
clientId: 'BUBL002314',
|
||||
clientId: 'CBSN002314',
|
||||
customerName: 'Gaurav Thakur',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'LOW',
|
||||
@@ -475,7 +475,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '12',
|
||||
branch: 'Amritsar Model Town',
|
||||
acType: 'CC',
|
||||
clientId: 'BUBL002615',
|
||||
clientId: 'CBSN002615',
|
||||
customerName: 'Manmohan Singh Gill',
|
||||
oldRisk: 'HIGH',
|
||||
newRisk: 'HIGH',
|
||||
@@ -488,7 +488,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '13',
|
||||
branch: 'Bhopal Arera Colony',
|
||||
acType: 'SB',
|
||||
clientId: 'BUBL002918',
|
||||
clientId: 'CBSN002918',
|
||||
customerName: 'Sunita Devi',
|
||||
oldRisk: 'LOW',
|
||||
newRisk: 'LOW',
|
||||
@@ -501,7 +501,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '14',
|
||||
branch: 'Indore Vijay Nagar',
|
||||
acType: 'OD',
|
||||
clientId: 'BUBL003221',
|
||||
clientId: 'CBSN003221',
|
||||
customerName: 'Deepak Tiwari',
|
||||
oldRisk: 'MEDIUM',
|
||||
newRisk: 'HIGH',
|
||||
@@ -514,7 +514,7 @@ export class CkycReportsDialogComponent {
|
||||
branchCode: '15',
|
||||
branch: 'Ludhiana Sarabha Nagar',
|
||||
acType: 'TL',
|
||||
clientId: 'BUBL003524',
|
||||
clientId: 'CBSN003524',
|
||||
customerName: 'Harpreet Kaur',
|
||||
oldRisk: 'MEDIUM',
|
||||
newRisk: 'MEDIUM',
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
<!--
|
||||
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="ctr-str-dlg">
|
||||
<!-- Dialog Header -->
|
||||
<div class="dlg-header">
|
||||
<div class="dlg-title-area">
|
||||
<fa-icon icon="shield-alt" class="header-icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="title">CTR & STR Compliance Control Center</h2>
|
||||
<p class="subtitle">Anti-Money Laundering (AML) & CFT Regulatory Reporting Terminal</p>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-icon-button [mat-dialog-close]="true" class="close-btn">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigators -->
|
||||
<div class="tab-navs">
|
||||
<button class="tab-btn" [class.active]="activeTab === 'dashboard'" (click)="activeTab = 'dashboard'">
|
||||
<fa-icon icon="chart-bar" class="tab-icon"></fa-icon> Compliance Dashboard
|
||||
</button>
|
||||
<button class="tab-btn" [class.active]="activeTab === 'guidelines'" (click)="activeTab = 'guidelines'">
|
||||
<fa-icon icon="university" class="tab-icon"></fa-icon> RBI Regulatory Guidelines
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Body (Tabs) -->
|
||||
<div class="dlg-body">
|
||||
<!-- TAB 1: COMPLIANCE DASHBOARD -->
|
||||
@if (activeTab === 'dashboard') {
|
||||
<div class="tab-content dashboard-tab">
|
||||
<!-- SECTION 1: CTR MANAGMENT -->
|
||||
<div class="dashboard-section card-glass">
|
||||
<div class="section-hdr">
|
||||
<div>
|
||||
<h3>Cash Transaction Reports (CTR - Monthly Cash Registry)</h3>
|
||||
<p class="section-sub">
|
||||
Scans and compiles cash transactions exceeding INR 10 Lakhs limit (Single or monthly cumulative cash
|
||||
deposits/withdrawals).
|
||||
</p>
|
||||
</div>
|
||||
<button class="action-btn primary" [disabled]="isGeneratingCtr" (click)="generateCtrReport()">
|
||||
@if (isGeneratingCtr) {
|
||||
<span class="spinner"></span> Generating Report...
|
||||
} @else {
|
||||
<fa-icon icon="sync-alt"></fa-icon> Generate Monthly CTR
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-actions">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search CTR files by month or name..."
|
||||
[(ngModel)]="ctrFilter"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="compliance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Report Payload Name</th>
|
||||
<th>Reporting Period</th>
|
||||
<th>Record Count</th>
|
||||
<th>Generated By</th>
|
||||
<th>Generated Date</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (report of filteredCtrReports; track report.id) {
|
||||
<tr>
|
||||
<td class="bold-text"><fa-icon icon="file-code" class="xml-icon"></fa-icon> {{ report.name }}</td>
|
||||
<td>{{ report.month }}</td>
|
||||
<td class="center-text">
|
||||
<span class="cnt-badge">{{ report.recordCount }} txns</span>
|
||||
</td>
|
||||
<td>{{ report.generatedBy }}</td>
|
||||
<td>{{ report.generatedAt | date: 'medium' }}</td>
|
||||
<td><span class="status-badge ready">Ready</span></td>
|
||||
<td>
|
||||
<button
|
||||
class="icon-action-btn"
|
||||
matTooltip="Download FIU-IND XML Payload"
|
||||
(click)="downloadCtrFile(report.name)"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon> Download XML
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">No CTR reports found matching your search.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 2: STR ALERTS PIPELINE -->
|
||||
<div class="dashboard-section card-glass mt-20">
|
||||
<div class="section-hdr">
|
||||
<div>
|
||||
<h3>Suspicious Transaction Activity Pipeline (STR Management)</h3>
|
||||
<p class="section-sub">
|
||||
Behavioral & structural AML alerts generated by KYC profile rules engine. Investigator actions update
|
||||
state in real-time.
|
||||
</p>
|
||||
</div>
|
||||
<button class="action-btn secondary" (click)="downloadStrReportTemplate()">
|
||||
<fa-icon icon="file-excel"></fa-icon> Export Active Alerts (CSV)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-actions">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search alerts by Client, CIF, or alert type..."
|
||||
[(ngModel)]="strFilter"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="alerts-grid">
|
||||
@for (alert of filteredStrAlerts; track alert.id) {
|
||||
<div class="alert-card" [class]="alert.status.toLowerCase().replace(' ', '-')">
|
||||
<div class="card-hdr">
|
||||
<div class="client-info">
|
||||
<h4>{{ alert.clientName }}</h4>
|
||||
<span class="cif-number">{{ alert.clientCif }}</span>
|
||||
</div>
|
||||
<span class="status-badge" [class]="alert.status.toLowerCase().replace(' ', '-')">{{
|
||||
alert.status
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="amount-line">
|
||||
<span class="label">Amount Highlighted:</span>
|
||||
<span class="amount">₹{{ alert.amount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="type-line">
|
||||
<span class="label">Alert Flag:</span>
|
||||
<span class="type-value">{{ alert.alertType }}</span>
|
||||
</div>
|
||||
<p class="desc">{{ alert.description }}</p>
|
||||
<span class="date-stamp"
|
||||
><fa-icon icon="clock"></fa-icon> Flagged Date: {{ alert.date | date: 'mediumDate' }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
@if (alert.status === 'Open') {
|
||||
<button class="card-btn investigate" (click)="changeAlertStatus(alert.id, 'Investigating')">
|
||||
<fa-icon icon="search"></fa-icon> Investigate
|
||||
</button>
|
||||
}
|
||||
@if (alert.status === 'Open' || alert.status === 'Investigating') {
|
||||
<button class="card-btn file-str" (click)="changeAlertStatus(alert.id, 'Reported STR')">
|
||||
<fa-icon icon="flag"></fa-icon> File STR
|
||||
</button>
|
||||
<button class="card-btn close-alert" (click)="changeAlertStatus(alert.id, 'Closed')">
|
||||
<fa-icon icon="ban"></fa-icon> Dismiss
|
||||
</button>
|
||||
}
|
||||
@if (alert.status === 'Closed' || alert.status === 'Reported STR') {
|
||||
<button class="card-btn reset-alert" (click)="changeAlertStatus(alert.id, 'Open')">
|
||||
<fa-icon icon="redo-alt"></fa-icon> Reopen Alert
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="empty-alerts">No Suspicious Transaction alerts matching your criteria.</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- TAB 2: RBI REGULATORY GUIDELINES -->
|
||||
@if (activeTab === 'guidelines') {
|
||||
<div class="tab-content scroll-y">
|
||||
<div class="info-layout">
|
||||
<!-- CTR Rules Card -->
|
||||
<div class="info-card card-glass">
|
||||
<div class="card-title-badge ctr-badge-color">
|
||||
<fa-icon icon="coins" class="badge-icon"></fa-icon> CTR - Cash Transaction Report
|
||||
</div>
|
||||
<div class="rules-body">
|
||||
<p class="lead-desc">
|
||||
As mandated by Section 12 of the **Prevention of Money Laundering Act (PMLA), 2002** and RBI KYC
|
||||
guidelines, banks must record and report all high-value cash transactions.
|
||||
</p>
|
||||
|
||||
<div class="rule-point">
|
||||
<h5>1. Transaction Threshold</h5>
|
||||
<p>
|
||||
All cash transactions (either deposits or withdrawals) of **value more than INR 10 Lakhs** (or its
|
||||
equivalent in foreign currency) in a single transaction.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule-point">
|
||||
<h5>2. Monthly Aggregation Rule (Structuring check)</h5>
|
||||
<p>
|
||||
A series of cash transactions in a calendar month that are **integrally connected** and accumulate to
|
||||
a total value exceeding **INR 10 Lakhs** must be compiled and reported in the monthly CTR.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule-point">
|
||||
<h5>3. Reporting Timeline</h5>
|
||||
<p>
|
||||
CTRs must be generated and uploaded to the FIU-IND gateway **by the 15th day of the succeeding month**
|
||||
(e.g., June transactions must be uploaded by July 15th).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule-point text-warning-border">
|
||||
<strong>Important Note:</strong> Transactions where the net cash balance does not change but high volume
|
||||
is rotated (e.g., cash withdrawals immediately deposited back) must be reported under CTR as separate
|
||||
debit and credit records.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STR Rules Card -->
|
||||
<div class="info-card card-glass">
|
||||
<div class="card-title-badge str-badge-color">
|
||||
<fa-icon icon="flag" class="badge-icon"></fa-icon> STR - Suspicious Transaction Report
|
||||
</div>
|
||||
<div class="rules-body">
|
||||
<p class="lead-desc">
|
||||
Every reporting entity must maintain vigilance over account transactions. If there are reasonable
|
||||
grounds to suspect transactions involve proceeds of crime, an STR must be generated.
|
||||
</p>
|
||||
|
||||
<div class="rule-point">
|
||||
<h5>1. What Constitutes Suspicion?</h5>
|
||||
<p>
|
||||
Transactions that are complex, show unusual patterns, mismatch the customer’s business/risk profile,
|
||||
have no apparent economic rationale, or involve suspected proceeds of crime or terror funding.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule-point">
|
||||
<h5>2. No Minimum Threshold Limit</h5>
|
||||
<p>
|
||||
**There is no minimum monetary limit** for STR. An alert should be generated for any transaction,
|
||||
regardless of size, if suspicious behavior is flagged.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule-point">
|
||||
<h5>3. Reporting Timeline</h5>
|
||||
<p>
|
||||
The STR must be filed to FIU-IND **within 7 days** of the Principal Compliance Officer confirming the
|
||||
suspicion.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule-point text-danger-border">
|
||||
<strong>Strict Tipping-off Restriction:</strong> It is illegal to disclose or "tip-off" to the client
|
||||
that their account is being monitored, investigated, or reported under an STR. Complete confidentiality
|
||||
must be maintained.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,750 +0,0 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.ctr-str-dlg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 90vh;
|
||||
max-height: 820px;
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
font-family: Inter, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Header
|
||||
.dlg-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.dlg-title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
||||
.header-icon {
|
||||
font-size: 26px;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #64748b;
|
||||
|
||||
&:hover {
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation Tabs
|
||||
.tab-navs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
background: #fff;
|
||||
padding: 8px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e0e7ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll layout
|
||||
.scroll-y {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Dialog Body
|
||||
.dlg-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
background: #f1f5f9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.scroll-y {
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
|
||||
/* Custom scrollbar */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard Tab specific layout
|
||||
.dashboard-tab {
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
// Card Style Container
|
||||
.card-glass {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgb(0 0 0 / 5%),
|
||||
0 2px 4px -1px rgb(0 0 0 / 2%);
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.section-hdr {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mt-25 {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
// Search input
|
||||
.table-actions {
|
||||
margin-bottom: 14px;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #1e293b;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 2px rgb(79 70 229 / 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button styles
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.primary {
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #94a3b8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #0ea5e9;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #0284c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spinner animation
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid rgb(255 255 255 / 30%);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 0.8s linear infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Tables styling
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.compliance-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #334155;
|
||||
|
||||
&.bold-text {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
&.center-text {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cnt-badge {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.xml-icon {
|
||||
color: #ea580c;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
padding: 24px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Action Button
|
||||
.icon-action-btn {
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #cbd5e1;
|
||||
color: #475569;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
// Alert Cards Grid
|
||||
.alerts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
// Left boundary color codes based on status
|
||||
&.open {
|
||||
border-left: 4px solid #dc2626;
|
||||
}
|
||||
|
||||
&.investigating {
|
||||
border-left: 4px solid #ea580c;
|
||||
}
|
||||
|
||||
&.reported-str {
|
||||
border-left: 4px solid #16a34a;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
border-left: 4px solid #64748b;
|
||||
}
|
||||
|
||||
.card-hdr {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.cif-number {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
|
||||
.amount-line,
|
||||
.type-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.label {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.type-value {
|
||||
font-weight: 600;
|
||||
color: #4f46e5;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 8px 0;
|
||||
line-height: 1.4;
|
||||
background: #f8fafc;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #e2e8f0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.date-stamp {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 14px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 12px;
|
||||
|
||||
.card-btn {
|
||||
flex: 1;
|
||||
min-width: 75px;
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.investigate {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
|
||||
&:hover {
|
||||
background: #fde68a;
|
||||
}
|
||||
}
|
||||
|
||||
&.file-str {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
|
||||
&:hover {
|
||||
background: #bbf7d0;
|
||||
}
|
||||
}
|
||||
|
||||
&.close-alert {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
|
||||
&:hover {
|
||||
background: #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
&.reset-alert {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
border: 1px solid #cbd5e1;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Badges
|
||||
.status-badge {
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
|
||||
&.ready {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&.open {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&.investigating {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
&.reported-str {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-alerts {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 36px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// Tab 2: Guidelines layout
|
||||
.info-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
|
||||
@media (width >= 900px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card-title-badge {
|
||||
align-self: flex-start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ctr-badge-color {
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
.str-badge-color {
|
||||
background: #be185d;
|
||||
}
|
||||
|
||||
.rules-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #334155;
|
||||
|
||||
.lead-desc {
|
||||
font-size: 14px;
|
||||
color: #0f172a;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rule-point {
|
||||
margin-bottom: 18px;
|
||||
|
||||
h5 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.text-warning-border {
|
||||
border-left: 3px solid #eab308;
|
||||
background: #fef9c3;
|
||||
padding: 10px 14px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
color: #854d0e;
|
||||
}
|
||||
|
||||
.text-danger-border {
|
||||
border-left: 3px solid #ef4444;
|
||||
background: #fee2e2;
|
||||
padding: 10px 14px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 3: Architecture & Schema
|
||||
.arch-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 4px 0 20px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.arch-section {
|
||||
h4 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
|
||||
&.plain {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
// API Routes
|
||||
.route-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.method {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
|
||||
&.get {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
&.put {
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
&.post {
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.path {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.route-desc {
|
||||
color: #475569;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FaIconComponent, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import {
|
||||
faShieldAlt,
|
||||
faChartBar,
|
||||
faUniversity,
|
||||
faCode,
|
||||
faSyncAlt,
|
||||
faFileExcel,
|
||||
faFileCode,
|
||||
faDownload,
|
||||
faClock,
|
||||
faSearch,
|
||||
faFlag,
|
||||
faBan,
|
||||
faRedoAlt,
|
||||
faCoins
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface CtrReport {
|
||||
id: number;
|
||||
name: string;
|
||||
month: string;
|
||||
recordCount: number;
|
||||
generatedBy: string;
|
||||
generatedAt: Date;
|
||||
status: 'Ready' | 'Generating';
|
||||
}
|
||||
|
||||
interface StrAlert {
|
||||
id: number;
|
||||
clientName: string;
|
||||
clientCif: string;
|
||||
alertType: string;
|
||||
amount: number;
|
||||
status: 'Open' | 'Investigating' | 'Reported STR' | 'Closed';
|
||||
description: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-ctr-str-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './ctr-str-dialog.component.html',
|
||||
styleUrls: ['./ctr-str-dialog.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatTabsModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class CtrStrDialogComponent {
|
||||
dialogRef = inject(MatDialogRef<CtrStrDialogComponent>);
|
||||
|
||||
constructor() {
|
||||
const library = inject(FaIconLibrary);
|
||||
library.addIcons(
|
||||
faShieldAlt,
|
||||
faChartBar,
|
||||
faUniversity,
|
||||
faCode,
|
||||
faSyncAlt,
|
||||
faFileExcel,
|
||||
faFileCode,
|
||||
faDownload,
|
||||
faClock,
|
||||
faSearch,
|
||||
faFlag,
|
||||
faBan,
|
||||
faRedoAlt,
|
||||
faCoins
|
||||
);
|
||||
}
|
||||
|
||||
activeTab: 'dashboard' | 'guidelines' = 'dashboard';
|
||||
|
||||
// Mock State
|
||||
isGeneratingCtr = false;
|
||||
ctrFilter = '';
|
||||
strFilter = '';
|
||||
|
||||
ctrReports: CtrReport[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'CTR_REPORT_JUNE_2026.xml',
|
||||
month: 'June 2026',
|
||||
recordCount: 14,
|
||||
generatedBy: 'Compliance_Officer_1',
|
||||
generatedAt: new Date(2026, 6, 5, 10, 30),
|
||||
status: 'Ready'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'CTR_REPORT_MAY_2026.xml',
|
||||
month: 'May 2026',
|
||||
recordCount: 22,
|
||||
generatedBy: 'Compliance_Officer_1',
|
||||
generatedAt: new Date(2026, 5, 4, 14, 15),
|
||||
status: 'Ready'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'CTR_REPORT_APRIL_2026.xml',
|
||||
month: 'April 2026',
|
||||
recordCount: 9,
|
||||
generatedBy: 'Compliance_Officer_2',
|
||||
generatedAt: new Date(2026, 4, 3, 11, 45),
|
||||
status: 'Ready'
|
||||
}
|
||||
];
|
||||
|
||||
strAlerts: StrAlert[] = [
|
||||
{
|
||||
id: 101,
|
||||
clientName: 'Aarav Sharma',
|
||||
clientCif: 'CIF982348',
|
||||
alertType: 'Profile Mismatch',
|
||||
amount: 450000,
|
||||
status: 'Open',
|
||||
description:
|
||||
'Student account received a large inward transfer (INR 4.5L) mismatching declared zero/student income category.',
|
||||
date: new Date(2026, 6, 26)
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
clientName: 'Karan Malhotra',
|
||||
clientCif: 'CIF229482',
|
||||
alertType: 'Structuring (Smurfing)',
|
||||
amount: 980000,
|
||||
status: 'Investigating',
|
||||
description:
|
||||
'Multiple cash deposits of INR 2.45L, 2.45L, 2.45L, 2.45L within 10 days, totaling INR 9.8L cash (deliberately staying below the 10L single cash transaction CTR threshold).',
|
||||
date: new Date(2026, 6, 24)
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
clientName: 'Nexora Real Estate Ltd',
|
||||
clientCif: 'CIF500481',
|
||||
alertType: 'Turnover Outlier',
|
||||
amount: 15000000,
|
||||
status: 'Open',
|
||||
description:
|
||||
'Entity account registered an outward wire of INR 1.5Cr when maximum monthly turnover limit is set to INR 50L.',
|
||||
date: new Date(2026, 6, 20)
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
clientName: 'Priyanka Patel',
|
||||
clientCif: 'CIF774920',
|
||||
alertType: 'Dormant Account Reactivation',
|
||||
amount: 600000,
|
||||
status: 'Reported STR',
|
||||
description:
|
||||
'Account inactive for 18 months suddenly received a cash deposit of INR 6L, followed by immediate online transfers.',
|
||||
date: new Date(2026, 6, 15)
|
||||
}
|
||||
];
|
||||
|
||||
get filteredCtrReports() {
|
||||
return this.ctrReports.filter(
|
||||
(r) =>
|
||||
r.name.toLowerCase().includes(this.ctrFilter.toLowerCase()) ||
|
||||
r.month.toLowerCase().includes(this.ctrFilter.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
get filteredStrAlerts() {
|
||||
return this.strAlerts.filter(
|
||||
(a) =>
|
||||
a.clientName.toLowerCase().includes(this.strFilter.toLowerCase()) ||
|
||||
a.clientCif.toLowerCase().includes(this.strFilter.toLowerCase()) ||
|
||||
a.alertType.toLowerCase().includes(this.strFilter.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Actions
|
||||
generateCtrReport() {
|
||||
if (this.isGeneratingCtr) {
|
||||
return;
|
||||
}
|
||||
this.isGeneratingCtr = true;
|
||||
|
||||
// Simulate API call to Spring Boot batch job
|
||||
setTimeout(() => {
|
||||
const date = new Date();
|
||||
const newReport: CtrReport = {
|
||||
id: this.ctrReports.length + 1,
|
||||
name: `CTR_REPORT_JULY_2026_DRAFT.xml`,
|
||||
month: 'July 2026 (Draft)',
|
||||
recordCount: 8,
|
||||
generatedBy: 'Current_Admin',
|
||||
generatedAt: date,
|
||||
status: 'Ready'
|
||||
};
|
||||
this.ctrReports.unshift(newReport);
|
||||
this.isGeneratingCtr = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
downloadCtrFile(reportName: string) {
|
||||
const dummyXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FIU-IND-CTR-Report xmlns="http://fiuindia.gov.in/ctr/v1">
|
||||
<Header>
|
||||
<ReportType>CTR</ReportType>
|
||||
<ReportingEntityName>CBS Nexora Bank</ReportingEntityName>
|
||||
<ReportingPeriod>2026-06</ReportingPeriod>
|
||||
</Header>
|
||||
<ReportData>
|
||||
<Record>
|
||||
<TransactionID>TXN88924824</TransactionID>
|
||||
<Date>2026-06-15</Date>
|
||||
<Mode>CASH</Mode>
|
||||
<Amount>1250000.00</Amount>
|
||||
<AccountType>SAVINGS</AccountType>
|
||||
<CustomerName>Aarav Sharma</CustomerName>
|
||||
<CifNumber>CIF982348</CifNumber>
|
||||
</Record>
|
||||
</ReportData>
|
||||
</FIU-IND-CTR-Report>`;
|
||||
|
||||
const blob = new Blob([dummyXml], { type: 'application/xml' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = reportName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
changeAlertStatus(alertId: number, newStatus: 'Open' | 'Investigating' | 'Reported STR' | 'Closed') {
|
||||
const alert = this.strAlerts.find((a) => a.id === alertId);
|
||||
if (alert) {
|
||||
alert.status = newStatus;
|
||||
}
|
||||
}
|
||||
|
||||
downloadStrReportTemplate() {
|
||||
const dummyCsv =
|
||||
`AlertID,ClientName,CIF,AlertType,Amount,Status,Description\n` +
|
||||
this.strAlerts
|
||||
.map(
|
||||
(a) =>
|
||||
`"${a.id}","${a.clientName}","${a.clientCif}","${a.alertType}","${a.amount}","${a.status}","${a.description.replace(/"/g, '""')}"`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([dummyCsv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `STR_AML_ALERTS_EXPORT.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
@@ -165,12 +165,12 @@
|
||||
<div class="da-compare">
|
||||
<div class="da-item">
|
||||
<div class="da-label">Total Deposits</div>
|
||||
<div class="da-value deposit">₹ 3,264.42 Cr</div>
|
||||
<div class="da-bar"><div class="da-fill deposit-fill" style="width: 78%"></div></div>
|
||||
<div class="da-value deposit">₹ 11,540.55 Cr</div>
|
||||
<div class="da-bar"><div class="da-fill deposit-fill" style="width: 71%"></div></div>
|
||||
</div>
|
||||
<div class="da-item">
|
||||
<div class="da-label">Total Advances</div>
|
||||
<div class="da-value advance">₹ 4,162.65 Cr</div>
|
||||
<div class="da-value advance">₹ 16,277.11 Cr</div>
|
||||
<div class="da-bar"><div class="da-fill advance-fill" style="width: 100%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<!--
|
||||
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="fdrd-dlg">
|
||||
<!-- HEADER -->
|
||||
<div class="fdrd-head">
|
||||
<div class="fdrd-head-left">
|
||||
<div class="fdrd-head-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
|
||||
<div>
|
||||
<h2 class="fdrd-title">FD / RD Entry</h2>
|
||||
<p class="fdrd-sub">Open a new Fixed Deposit or Recurring Deposit account.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fdrd-head-actions">
|
||||
<span class="fdrd-meta"> <fa-icon icon="university"></fa-icon> {{ bank.name }} </span>
|
||||
<span class="fdrd-meta"> <fa-icon icon="map-marker-alt"></fa-icon> {{ bank.branch }} </span>
|
||||
<button class="fdrd-close" (click)="close()" matTooltip="Close" aria-label="Close">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fdrd-body">
|
||||
<!-- Success banner -->
|
||||
<div class="fdrd-success" *ngIf="saved">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<div>
|
||||
<strong>{{ lastAccountNo }}</strong> created for <strong>{{ lastClientName }}</strong>
|
||||
<div class="fdrd-success-sub">You can enter another deposit below, or close this dialog.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FORM -->
|
||||
<div class="fdrd-section">
|
||||
<!-- Row 1: Deposit Info | Client & Amount -->
|
||||
<div class="ws-group-row">
|
||||
<div class="ws-group">
|
||||
<span class="ws-caption">Deposit Info</span>
|
||||
<div class="ws-group__fields">
|
||||
<div class="ws-f" style="width: 200px">
|
||||
<label class="ws-f__lbl">Deposit Type</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="depositType" (ngModelChange)="onDepositTypeChange()">
|
||||
<option *ngFor="let dt of depositTypes" [value]="dt.code">{{ dt.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ws-f" style="width: 150px">
|
||||
<label class="ws-f__lbl">Pay Mode</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="payMode">
|
||||
<option *ngFor="let m of payModes" [value]="m">{{ m }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ws-vdivider"></div>
|
||||
|
||||
<div class="ws-group ws-group--flex">
|
||||
<span class="ws-caption">Client & Amount</span>
|
||||
<div class="ws-group__fields">
|
||||
<div class="ws-f" style="flex: 1.5; min-width: 180px">
|
||||
<label class="ws-f__lbl">Client</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="clientId">
|
||||
<option value="" disabled>-- Select Client --</option>
|
||||
<option *ngFor="let c of clients" [value]="c.id">{{ c.name }} ({{ c.id }})</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ws-f" style="flex: 1; min-width: 120px">
|
||||
<label class="ws-f__lbl">{{ isRecurring ? 'Monthly Instalment (₹)' : 'Deposit Amount (₹)' }}</label>
|
||||
<input
|
||||
class="ws-f__ctl ws-f__ctl--mono"
|
||||
type="number"
|
||||
min="0"
|
||||
[(ngModel)]="principal"
|
||||
placeholder="e.g. 100000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Term & Rate | Instructions -->
|
||||
<div class="ws-group-row ws-group-row--mt">
|
||||
<div class="ws-group">
|
||||
<span class="ws-caption">Interest & Tenure</span>
|
||||
<div class="ws-group__fields">
|
||||
<div class="ws-f" style="width: 110px">
|
||||
<label class="ws-f__lbl">Int. Rate (% p.a.)</label>
|
||||
<input class="ws-f__ctl" type="number" min="0" step="0.05" [(ngModel)]="interestRate" />
|
||||
</div>
|
||||
<div class="ws-f" style="width: 240px">
|
||||
<label class="ws-f__lbl">Tenure</label>
|
||||
<div class="fdrd-tenure-row">
|
||||
<input class="ws-f__ctl" type="number" min="0" [(ngModel)]="tenureYears" />
|
||||
<span>Yrs</span>
|
||||
<input class="ws-f__ctl" type="number" min="0" max="11" [(ngModel)]="tenureMonths" />
|
||||
<span>Mo</span>
|
||||
<input class="ws-f__ctl" type="number" min="0" max="30" [(ngModel)]="tenureDays" />
|
||||
<span>Days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ws-vdivider"></div>
|
||||
|
||||
<div class="ws-group ws-group--flex">
|
||||
<span class="ws-caption">Nominee & Maturity Details</span>
|
||||
<div class="ws-group__fields">
|
||||
<div class="ws-f" style="flex: 1; min-width: 120px">
|
||||
<label class="ws-f__lbl">Interest Payout</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="interestPayout">
|
||||
<option *ngFor="let o of interestPayoutOptions" [value]="o">{{ o }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ws-f" style="flex: 1; min-width: 130px">
|
||||
<label class="ws-f__lbl">Nominee Name (optional)</label>
|
||||
<input class="ws-f__ctl" type="text" [(ngModel)]="nomineeName" placeholder="Spouse / Child name" />
|
||||
</div>
|
||||
<div class="ws-f" style="flex: 1.2; min-width: 160px">
|
||||
<label class="ws-f__lbl">Maturity Instruction</label>
|
||||
<select class="ws-f__ctl" [(ngModel)]="maturityInstruction">
|
||||
<option *ngFor="let m of maturityInstructions" [value]="m">{{ m }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SUMMARY -->
|
||||
<div class="fdrd-summary">
|
||||
<div class="fdrd-stat">
|
||||
<div class="fdrd-stat-l">Principal</div>
|
||||
<div class="fdrd-stat-v">₹ {{ principal || 0 | number: '1.0-2' }}</div>
|
||||
</div>
|
||||
<div class="fdrd-stat">
|
||||
<div class="fdrd-stat-l">Tenure</div>
|
||||
<div class="fdrd-stat-v">{{ tenureYears }}y {{ tenureMonths }}m {{ tenureDays }}d</div>
|
||||
</div>
|
||||
<div class="fdrd-stat">
|
||||
<div class="fdrd-stat-l">Maturity Date</div>
|
||||
<div class="fdrd-stat-v">{{ maturityDate | date: 'dd-MM-yyyy' }}</div>
|
||||
</div>
|
||||
<div class="fdrd-stat fdrd-stat-good">
|
||||
<div class="fdrd-stat-l">Maturity Amount</div>
|
||||
<div class="fdrd-stat-v">₹ {{ maturityAmount | number: '1.2-2' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="fdrd-foot">
|
||||
<div class="fdrd-foot-l">
|
||||
<fa-icon icon="info-circle"></fa-icon> Demo entry — quarterly-compounded interest, no real account is
|
||||
created.
|
||||
</div>
|
||||
<div class="fdrd-foot-actions">
|
||||
<button mat-stroked-button class="fdrd-btn-ghost" (click)="resetForm()">
|
||||
<fa-icon icon="undo"></fa-icon> Reset
|
||||
</button>
|
||||
<button mat-stroked-button class="fdrd-btn-save" [disabled]="!isValid" (click)="save()">
|
||||
<fa-icon icon="save"></fa-icon> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
font-family: 'Hanken Grotesk', Inter, sans-serif;
|
||||
color: #1a2a3e;
|
||||
}
|
||||
|
||||
.fdrd-dlg {
|
||||
background: #fff;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* HEADER - Matches .ws-header in clerk dashboard */
|
||||
.fdrd-head {
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
background: linear-gradient(90deg, #0d1b2e 0%, #162542 100%);
|
||||
border-bottom: 2px solid #e87722;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fdrd-head-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fdrd-head-ic {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
background: #e87722;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fdrd-title {
|
||||
margin: 0;
|
||||
font-family: Newsreader, Georgia, serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #e8f0fb;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.fdrd-sub {
|
||||
display: none; /* Hide subtitle in header to match clean style */
|
||||
}
|
||||
|
||||
.fdrd-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.fdrd-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #7a95b0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
fa-icon {
|
||||
color: #5a7589;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrd-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #7a95b0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 10%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* BODY - Styled like canvas + card grid */
|
||||
.fdrd-body {
|
||||
padding: 16px 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
background: #f0f4f9;
|
||||
}
|
||||
|
||||
.fdrd-success {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
|
||||
fa-icon {
|
||||
font-size: 16px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrd-success-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Card container for form */
|
||||
.fdrd-section {
|
||||
background: #fff;
|
||||
border: 1px solid #dce4ef;
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Transaction Entry Style Layout mappings */
|
||||
.ws-group-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
|
||||
&--mt {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ws-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--flex {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__fields {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
align-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.ws-caption {
|
||||
font-family: Newsreader, Georgia, serif;
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
color: #7a95b0;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.ws-vdivider {
|
||||
width: 1px;
|
||||
background: #dce4ef;
|
||||
margin: 12px 10px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ws-f {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #7a95b0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__ctl {
|
||||
height: 26px;
|
||||
padding: 0 7px;
|
||||
border: 1px solid #c8d8eb;
|
||||
border-radius: 6px;
|
||||
font-family: 'Hanken Grotesk', Inter, sans-serif;
|
||||
font-size: 11.5px;
|
||||
color: #1a2a3e;
|
||||
background: #f4f7fc;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #9ab5d0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgb(37 99 235 / 12%);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&--mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 11.5px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fdrd-tenure-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
|
||||
.ws-f__ctl {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #7a95b0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
/* SUMMARY - Styled like info summary block */
|
||||
.fdrd-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #f4f7fc;
|
||||
border: 1px solid #c8d8eb;
|
||||
border-radius: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.fdrd-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.fdrd-stat-l {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #7a95b0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.fdrd-stat-v {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1a2a3e;
|
||||
}
|
||||
|
||||
.fdrd-stat-good .fdrd-stat-v {
|
||||
color: #1a7a4a;
|
||||
}
|
||||
|
||||
/* FOOTER - Styled like action panel */
|
||||
.fdrd-foot {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #dce4ef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
background: #f0f4f9;
|
||||
}
|
||||
|
||||
.fdrd-foot-l {
|
||||
font-size: 11px;
|
||||
color: #7a95b0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.fdrd-foot-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fdrd-btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 28px;
|
||||
padding: 0 12px !important;
|
||||
border-radius: 7px;
|
||||
border: 1px solid #dce4ef !important;
|
||||
background: #fff !important;
|
||||
color: #1a2a3e !important;
|
||||
font-family: 'Hanken Grotesk', sans-serif;
|
||||
font-size: 11.5px !important;
|
||||
font-weight: 600 !important;
|
||||
cursor: pointer;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
background: #f0f4f9 !important;
|
||||
border-color: #a8bdd4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fdrd-btn-save {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 28px;
|
||||
padding: 0 16px !important;
|
||||
border-radius: 7px;
|
||||
border: none !important;
|
||||
background: #e87722 !important;
|
||||
color: #fff !important;
|
||||
font-family: 'Hanken Grotesk', sans-serif;
|
||||
font-size: 11.5px !important;
|
||||
font-weight: 700 !important;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgb(232 119 34 / 30%) !important;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
background: #c8d8eb !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.ws-group-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ws-vdivider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ws-group__fields {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fdrd-summary {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
import { CommonModule, DecimalPipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
type DepositType = 'FD-SINGLE' | 'FD-JOINT' | 'RD-MONTHLY' | 'RD-QUARTERLY';
|
||||
|
||||
interface DemoClient {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-fd-rd-entry-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './fd-rd-entry-dialog.component.html',
|
||||
styleUrls: ['./fd-rd-entry-dialog.component.scss'],
|
||||
imports: [CommonModule, DecimalPipe, FormsModule, MatDialogModule, MatButtonModule, MatTooltipModule, FaIconComponent]
|
||||
})
|
||||
export class FdRdEntryDialogComponent {
|
||||
private dialogRef = inject(MatDialogRef<FdRdEntryDialogComponent>);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
bank = {
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office'
|
||||
};
|
||||
|
||||
depositTypes: { code: DepositType; label: string; defaultRate: number }[] = [
|
||||
{ code: 'FD-SINGLE', label: 'Fixed Deposit — Single A/c', defaultRate: 7.25 },
|
||||
{ code: 'FD-JOINT', label: 'Fixed Deposit — Joint A/c', defaultRate: 7.25 },
|
||||
{ code: 'RD-MONTHLY', label: 'Recurring Deposit — Monthly', defaultRate: 7.0 },
|
||||
{ code: 'RD-QUARTERLY', label: 'Recurring Deposit — Quarterly', defaultRate: 7.0 }
|
||||
];
|
||||
|
||||
clients: DemoClient[] = [
|
||||
{ id: 'C-10231', name: 'Ramesh Kumar Sharma' },
|
||||
{ id: 'C-10344', name: 'Priya Enterprises' },
|
||||
{ id: 'C-10412', name: 'Anjali Verma' },
|
||||
{ id: 'C-10567', name: 'Suresh & Sons HUF' },
|
||||
{ id: 'C-10689', name: 'Vikram Traders' }
|
||||
];
|
||||
|
||||
payModes = ['Cash', 'Cheque', 'Transfer from Savings A/c'];
|
||||
interestPayoutOptions = ['Cumulative (Paid on Maturity)', 'Monthly Payout', 'Quarterly Payout'];
|
||||
maturityInstructions = [
|
||||
'Renew Principal & Interest',
|
||||
'Renew Principal Only, Payout Interest',
|
||||
'Payout on Maturity (No Renewal)'
|
||||
];
|
||||
|
||||
depositType: DepositType = 'FD-SINGLE';
|
||||
clientId = '';
|
||||
principal: number | null = null;
|
||||
interestRate = 7.25;
|
||||
tenureYears = 1;
|
||||
tenureMonths = 0;
|
||||
tenureDays = 0;
|
||||
interestPayout = this.interestPayoutOptions[0];
|
||||
payMode = this.payModes[0];
|
||||
nomineeName = '';
|
||||
maturityInstruction = this.maturityInstructions[0];
|
||||
|
||||
saved = false;
|
||||
lastAccountNo = '';
|
||||
lastClientName = '';
|
||||
|
||||
get selectedClient(): DemoClient | undefined {
|
||||
return this.clients.find((c) => c.id === this.clientId);
|
||||
}
|
||||
|
||||
onDepositTypeChange(): void {
|
||||
const dt = this.depositTypes.find((d) => d.code === this.depositType);
|
||||
if (dt) this.interestRate = dt.defaultRate;
|
||||
}
|
||||
|
||||
get isRecurring(): boolean {
|
||||
return this.depositType.startsWith('RD');
|
||||
}
|
||||
|
||||
get tenureInYears(): number {
|
||||
return this.tenureYears + this.tenureMonths / 12 + this.tenureDays / 365;
|
||||
}
|
||||
|
||||
get maturityDate(): Date {
|
||||
const d = new Date();
|
||||
d.setFullYear(d.getFullYear() + this.tenureYears);
|
||||
d.setMonth(d.getMonth() + this.tenureMonths);
|
||||
d.setDate(d.getDate() + this.tenureDays);
|
||||
return d;
|
||||
}
|
||||
|
||||
/** Quarterly-compounded maturity value, the standard FD/RD convention */
|
||||
get maturityAmount(): number {
|
||||
const p = this.principal || 0;
|
||||
const t = this.tenureInYears;
|
||||
if (p <= 0 || t <= 0) return 0;
|
||||
return p * Math.pow(1 + this.interestRate / 100 / 4, 4 * t);
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return !!this.clientId && (this.principal ?? 0) > 0 && this.tenureInYears > 0;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.isValid) return;
|
||||
const prefix = this.isRecurring ? 'RD' : 'FD';
|
||||
this.lastAccountNo = `${prefix}${Math.floor(1000000 + Math.random() * 8999999)}`;
|
||||
this.lastClientName = this.selectedClient?.name ?? '';
|
||||
this.saved = true;
|
||||
this.snackBar.open(`${prefix} account ${this.lastAccountNo} created for ${this.lastClientName}`, 'Close', {
|
||||
duration: 4000
|
||||
});
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.depositType = 'FD-SINGLE';
|
||||
this.clientId = '';
|
||||
this.principal = null;
|
||||
this.interestRate = 7.25;
|
||||
this.tenureYears = 1;
|
||||
this.tenureMonths = 0;
|
||||
this.tenureDays = 0;
|
||||
this.nomineeName = '';
|
||||
this.interestPayout = this.interestPayoutOptions[0];
|
||||
this.payMode = this.payModes[0];
|
||||
this.maturityInstruction = this.maturityInstructions[0];
|
||||
this.saved = false;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class PslReportDialogComponent {
|
||||
activeTab: 'dashboard' | 'categories' | 'register' | 'returns' = 'dashboard';
|
||||
|
||||
bank = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
quarter: 'Q3 FY 2025-26',
|
||||
asOnDate: '29-01-2026',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,472 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · FIXED DEPOSIT</div>
|
||||
<h1 class="pg-title">FD Investment</h1>
|
||||
<p class="pg-sub">
|
||||
Bank's own Fixed Deposit investments with other banks — Purchase, Interest, Sell/Premature, Write-off and
|
||||
Part-Payment entries.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'holdings'"
|
||||
(click)="setTab('holdings')"
|
||||
>
|
||||
Holdings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'purchase'"
|
||||
(click)="setTab('purchase')"
|
||||
>
|
||||
01 Purchase Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'interest'"
|
||||
(click)="setTab('interest')"
|
||||
>
|
||||
02 Interest Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'sell'"
|
||||
(click)="setTab('sell')"
|
||||
>
|
||||
03 Sell / Premature Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'writeoff'"
|
||||
(click)="setTab('writeoff')"
|
||||
>
|
||||
04 Write Off Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'partpayment'"
|
||||
(click)="setTab('partpayment')"
|
||||
>
|
||||
05 FD Part Payment Entry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- HOLDINGS -->
|
||||
@if (activeTab === 'holdings') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">FD Holdings Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Party</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Purchase Date</th>
|
||||
<th>Maturity Date</th>
|
||||
<th class="num col-in">Accrued Interest</th>
|
||||
<th class="num col-net">Maturity Value</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of holdings; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td>{{ h.interestFreq === 'CUMULATIVE' ? 'Cumulative' : 'Payout' }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td>{{ h.purchaseDate }}</td>
|
||||
<td>{{ h.maturityDate }}</td>
|
||||
<td class="num col-in">₹{{ accrued(h) | number: '1.2-2' }}</td>
|
||||
<td class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PURCHASE ENTRY -->
|
||||
@if (activeTab === 'purchase') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - FD Purchase Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Tran. Type</span>
|
||||
<select [(ngModel)]="purchaseForm.tranType">
|
||||
<option value="FRESH">FRESH</option>
|
||||
<option value="RENEWAL">RENEWAL</option>
|
||||
</select>
|
||||
</label>
|
||||
@if (purchaseForm.tranType === 'RENEWAL') {
|
||||
<label class="inv-form-field"
|
||||
><span>OLD CertNo</span><input type="text" [(ngModel)]="purchaseForm.oldCertNo"
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field">
|
||||
<span>FD Type</span>
|
||||
<select [(ngModel)]="purchaseForm.fdType">
|
||||
@for (t of fdTypeOptions; track t) {
|
||||
<option [value]="t">{{ t }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Mature Amt.calc.Type</span>
|
||||
<select [(ngModel)]="purchaseForm.matureAmtCalcType">
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="DATEWISE">DATEWISE</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Party *</span>
|
||||
<select [(ngModel)]="purchaseForm.party">
|
||||
<option value="">-- Select --</option>
|
||||
@for (p of partyOptions; track p.partyCode) {
|
||||
<option [value]="p.partyName">{{ p.partyName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Sub Party</span>
|
||||
<select [(ngModel)]="purchaseForm.subParty">
|
||||
<option value="">-- Select --</option>
|
||||
@for (p of partyOptions; track p.partyCode) {
|
||||
<option [value]="p.partyName">{{ p.partyName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>GL ACNO</span><input type="text" [(ngModel)]="purchaseForm.glAcNo"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>GL A/c Name</span><input type="text" [(ngModel)]="purchaseForm.glAcName" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Certificate No.</span><input type="text" [(ngModel)]="purchaseForm.certNo"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Certificate Date</span><input type="text" [(ngModel)]="purchaseForm.certDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Sub Type</span>
|
||||
<select [(ngModel)]="purchaseForm.subType">
|
||||
@for (s of subTypeOptions; track s.subTypeCode + s.subTypeName) {
|
||||
<option [value]="s.subTypeName">{{ s.subTypeCode }} - {{ s.subTypeName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Reciept/Advice</span>
|
||||
<select [(ngModel)]="purchaseForm.receiptAdvice">
|
||||
<option value="RECEIPT">RECEIPT</option>
|
||||
<option value="ADVICE">ADVICE</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>Transaction Date</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.transactionDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Deposit Date *</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.depositDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Deposit Amount *</span><input type="number" [(ngModel)]="purchaseForm.depositAmount" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Interest[M/Q/H/Y]</span>
|
||||
<select [(ngModel)]="purchaseForm.interestFreq">
|
||||
@for (i of intTypeOptions; track i.value) {
|
||||
<option [value]="i.value">{{ i.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>Interest Date</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.interestDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Interest Rate *</span><input type="number" [(ngModel)]="purchaseForm.interestRate" min="0" step="0.01"
|
||||
/></label>
|
||||
@if (purchaseForm.matureAmtCalcType === 'GENERAL') {
|
||||
<label class="inv-form-field"
|
||||
><span>Period Month</span><input type="number" [(ngModel)]="purchaseForm.periodMonth" min="0" max="11"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Year</span><input type="number" [(ngModel)]="purchaseForm.periodYear" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Days</span><input type="number" [(ngModel)]="purchaseForm.periodDays" min="0"
|
||||
/></label>
|
||||
} @else {
|
||||
<label class="inv-form-field"
|
||||
><span>Maturity Date (manual) *</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.manualMaturityDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Resolution No.</span><input type="text" [(ngModel)]="purchaseForm.resolutionNo"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>CR. Branch Code</span>
|
||||
<select [(ngModel)]="purchaseForm.crBranchCode">
|
||||
@for (b of branchOptions; track b) {
|
||||
<option [value]="b">{{ b }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>CR. To A/c</span><input type="text" [(ngModel)]="purchaseForm.crToAc"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>CR. A/c Name</span><input type="text" [(ngModel)]="purchaseForm.crToAcName" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Int.Receivable Code</span><input type="text" [(ngModel)]="purchaseForm.intReceivableCode"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>LIEN Y/N</span>
|
||||
<select [(ngModel)]="purchaseForm.lien">
|
||||
<option value="NO">NO</option>
|
||||
<option value="YES">YES</option>
|
||||
</select>
|
||||
</label>
|
||||
@if (purchaseForm.lien === 'YES') {
|
||||
<label class="inv-form-field"
|
||||
><span>LIEN AMT.</span><input type="number" [(ngModel)]="purchaseForm.lienAmt"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>LIEN Reason</span>
|
||||
<select [(ngModel)]="purchaseForm.lienReason">
|
||||
<option value="">-- Select --</option>
|
||||
@for (l of lienReasonOptions; track l.reasonCode) {
|
||||
<option [value]="l.reasonDescription">{{ l.reasonDescription }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
<label class="inv-form-field">
|
||||
<span>Classification</span>
|
||||
<select [(ngModel)]="purchaseForm.classification">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="STANDARD">STANDARD</option>
|
||||
<option value="SUB-STANDARD">SUB-STANDARD</option>
|
||||
<option value="DOUBTFUL">DOUBTFUL</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Collable</span>
|
||||
<select [(ngModel)]="purchaseForm.collable">
|
||||
<option value="NO">NO</option>
|
||||
<option value="YES">YES</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pos-section-label">Mature / Premature Details</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>* Maturity Date</span><input type="text" [value]="purchaseMaturityDate" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>* Maturity Amt</span><input type="text" [value]="purchaseMaturityAmount | number: '1.0-0'" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Search by Cert No</span><input type="text" [(ngModel)]="searchCertNo" placeholder="Enter Cert No"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="savePurchase()">Save</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelPurchase()">Cancel</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="searchPurchase()">Search</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="dropPurchase()">Drop</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="exitPurchase()">Exit</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- INTEREST ENTRY -->
|
||||
@if (activeTab === 'interest') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - FD Interest Entry / Calculation</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Certificate No *</span>
|
||||
<select [(ngModel)]="interestCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (interestHolding; as h) {
|
||||
<label class="inv-form-field"><span>Party</span><input type="text" [value]="h.party" readonly /></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Interest Rate</span><input type="text" [value]="h.interestRate + '%'" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Frequency</span><input type="text" [value]="h.interestFreq" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Accrued Interest (as on {{ asOnDate }})</span
|
||||
><input type="text" [value]="'₹' + (interestAccrued(h) | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="postInterest()">Process / Post Interest</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SELL / PREMATURE ENTRY -->
|
||||
@if (activeTab === 'sell') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - FD Sell / Premature Closure Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Certificate No *</span>
|
||||
<select [(ngModel)]="sellCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (sellHolding; as h) {
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Accrued Interest till Sell Date</span
|
||||
><input type="text" [value]="'₹' + (accrued(h) | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Sell / Premature Date *</span><input type="text" [(ngModel)]="sellDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field"><span>Remarks</span><input type="text" [(ngModel)]="sellRemark" /></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveSell()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- WRITE OFF ENTRY -->
|
||||
@if (activeTab === 'writeoff') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - FD Write Off Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Certificate No *</span>
|
||||
<select [(ngModel)]="writeoffCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (writeoffHolding; as h) {
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value to be Written Off</span
|
||||
><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"><span>Reason *</span><input type="text" [(ngModel)]="writeoffReason" /></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveWriteoff()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PART PAYMENT ENTRY -->
|
||||
@if (activeTab === 'partpayment') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - FD Part Payment Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Certificate No *</span>
|
||||
<select [(ngModel)]="partPayCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (partPayHolding; as h) {
|
||||
<label class="inv-form-field"
|
||||
><span>Current Face Value</span><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Part Payment Amount *</span><input type="number" [(ngModel)]="partPayAmount" min="0"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="savePartPayment()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (activeTab !== 'holdings') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">FD Transaction Log</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Cert No</th>
|
||||
<th>Type</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of txns; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.voucherNo }}</span>
|
||||
</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ t.txnType }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
FD_HOLDINGS,
|
||||
ALL_TXNS,
|
||||
PARTY_MASTER,
|
||||
SUBTYPE_MASTER,
|
||||
FD_TYPE_OPTIONS,
|
||||
INT_TYPE_OPTIONS,
|
||||
LIEN_REASON_MASTER,
|
||||
InvestmentHolding,
|
||||
InvestmentTxn,
|
||||
InterestFreq,
|
||||
accruedInterest,
|
||||
daysToMaturity,
|
||||
maturityAmount,
|
||||
addPeriod,
|
||||
todayDDMMYYYY
|
||||
} from '../investment-data';
|
||||
|
||||
type FdTab = 'holdings' | 'purchase' | 'interest' | 'sell' | 'writeoff' | 'partpayment';
|
||||
|
||||
interface FdPurchaseForm {
|
||||
tranType: 'FRESH' | 'RENEWAL';
|
||||
oldCertNo: string;
|
||||
fdType: string;
|
||||
/** GENERAL uses the period (Y/M/D) fields; DATEWISE lets the user type the maturity date directly. */
|
||||
matureAmtCalcType: 'GENERAL' | 'DATEWISE';
|
||||
certNo: string;
|
||||
certDate: string;
|
||||
party: string;
|
||||
subParty: string;
|
||||
glAcNo: string;
|
||||
glAcName: string;
|
||||
subType: string;
|
||||
receiptAdvice: 'RECEIPT' | 'ADVICE';
|
||||
transactionDate: string;
|
||||
depositDate: string;
|
||||
depositAmount: number | null;
|
||||
interestFreq: InterestFreq;
|
||||
interestDate: string;
|
||||
interestRate: number | null;
|
||||
periodYear: number | null;
|
||||
periodMonth: number | null;
|
||||
periodDays: number | null;
|
||||
/** Only used when matureAmtCalcType === 'DATEWISE'. */
|
||||
manualMaturityDate: string;
|
||||
resolutionNo: string;
|
||||
crBranchCode: string;
|
||||
crToAc: string;
|
||||
crToAcName: string;
|
||||
intReceivableCode: string;
|
||||
lien: 'YES' | 'NO';
|
||||
lienAmt: number | null;
|
||||
lienReason: string;
|
||||
classification: string;
|
||||
collable: 'YES' | 'NO';
|
||||
}
|
||||
|
||||
/**
|
||||
* FD (Fixed Deposit) treasury investment screen — Holdings register + Purchase / Interest /
|
||||
* Sell-Premature / Write-off / Part-Payment entry tabs, modelled on the legacy "Investment - FD
|
||||
* Purchase Entry" form. Dummy in-memory data only; Save appends to the local holdings/txns
|
||||
* arrays so the demo feels interactive without any backend.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-fd-investment',
|
||||
templateUrl: './fd-investment.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class FdInvestmentComponent {
|
||||
activeTab: FdTab = 'holdings';
|
||||
asOnDate = todayDDMMYYYY();
|
||||
|
||||
holdings: InvestmentHolding[] = [...FD_HOLDINGS];
|
||||
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'FD');
|
||||
|
||||
readonly partyOptions = PARTY_MASTER;
|
||||
readonly subTypeOptions = SUBTYPE_MASTER;
|
||||
readonly fdTypeOptions = FD_TYPE_OPTIONS;
|
||||
readonly intTypeOptions = INT_TYPE_OPTIONS;
|
||||
readonly lienReasonOptions = LIEN_REASON_MASTER;
|
||||
readonly branchOptions = [
|
||||
'HEAD OFFICE',
|
||||
'BRANCH 1',
|
||||
'BRANCH 2'
|
||||
];
|
||||
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
purchaseForm: FdPurchaseForm = this.blankPurchaseForm();
|
||||
/** Certificate No typed into the Purchase Entry's Search box (legacy "Search" button). */
|
||||
searchCertNo = '';
|
||||
|
||||
interestCertNo = '';
|
||||
sellCertNo = '';
|
||||
sellDate = this.asOnDate;
|
||||
sellRemark = '';
|
||||
writeoffCertNo = '';
|
||||
writeoffReason = '';
|
||||
partPayCertNo = '';
|
||||
partPayAmount: number | null = null;
|
||||
|
||||
setTab(tab: FdTab): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
get activeHoldings(): InvestmentHolding[] {
|
||||
return this.holdings.filter((h) => h.status === 'ACTIVE');
|
||||
}
|
||||
|
||||
private nextCertNo(): string {
|
||||
const nums = this.holdings.map((h) => parseInt(h.certNo, 10)).filter((n) => !isNaN(n));
|
||||
return String((nums.length ? Math.max(...nums) : 15619) + 1);
|
||||
}
|
||||
|
||||
private blankPurchaseForm(): FdPurchaseForm {
|
||||
return {
|
||||
tranType: 'FRESH',
|
||||
oldCertNo: '',
|
||||
fdType: 'CUMMULATIVE',
|
||||
matureAmtCalcType: 'GENERAL',
|
||||
certNo: this.nextCertNo(),
|
||||
certDate: this.asOnDate,
|
||||
party: '',
|
||||
subParty: '',
|
||||
glAcNo: '320020',
|
||||
glAcName: 'FIXED DEPO. CO-OPER. BANK MAIN',
|
||||
subType: 'NON SLR',
|
||||
receiptAdvice: 'RECEIPT',
|
||||
transactionDate: this.asOnDate,
|
||||
depositDate: this.asOnDate,
|
||||
depositAmount: null,
|
||||
interestFreq: 'CUMULATIVE',
|
||||
interestDate: this.asOnDate,
|
||||
interestRate: null,
|
||||
periodYear: 2,
|
||||
periodMonth: 0,
|
||||
periodDays: 0,
|
||||
manualMaturityDate: '',
|
||||
resolutionNo: '',
|
||||
crBranchCode: 'HEAD OFFICE',
|
||||
crToAc: '310070',
|
||||
crToAcName: 'CURRENT A/C CO-OP. BANK MAIN BR.41',
|
||||
intReceivableCode: '320020',
|
||||
lien: 'NO',
|
||||
lienAmt: null,
|
||||
lienReason: '',
|
||||
classification: '',
|
||||
collable: 'NO'
|
||||
};
|
||||
}
|
||||
|
||||
get purchaseMaturityDate(): string {
|
||||
const f = this.purchaseForm;
|
||||
if (f.matureAmtCalcType === 'DATEWISE') {
|
||||
return f.manualMaturityDate;
|
||||
}
|
||||
if (!f.depositDate) {
|
||||
return '';
|
||||
}
|
||||
const base = addPeriod(f.depositDate, f.periodYear || 0, f.periodMonth || 0);
|
||||
return f.periodDays ? addPeriod(base, 0, 0, f.periodDays) : base;
|
||||
}
|
||||
|
||||
get purchaseMaturityAmount(): number {
|
||||
const f = this.purchaseForm;
|
||||
if (!f.depositAmount || !f.interestRate || !this.purchaseMaturityDate) {
|
||||
return 0;
|
||||
}
|
||||
return maturityAmount({
|
||||
certNo: f.certNo,
|
||||
instrumentType: 'FD',
|
||||
scriptCode: '',
|
||||
scriptName: '',
|
||||
party: f.party,
|
||||
subType: f.subType,
|
||||
faceValue: f.depositAmount,
|
||||
purchaseRate: 100,
|
||||
units: 1,
|
||||
purchaseDate: f.depositDate,
|
||||
maturityDate: this.purchaseMaturityDate,
|
||||
interestRate: f.interestRate,
|
||||
interestFreq: f.interestFreq,
|
||||
glCode: f.glAcNo,
|
||||
glName: '',
|
||||
crToAc: f.crToAc,
|
||||
brokerCode: '',
|
||||
status: 'ACTIVE'
|
||||
});
|
||||
}
|
||||
|
||||
savePurchase(): void {
|
||||
const f = this.purchaseForm;
|
||||
if (!f.party || !f.depositAmount || !f.interestRate || !this.purchaseMaturityDate) {
|
||||
this.toast('Please fill Party, Deposit Amount, Interest Rate and Period.');
|
||||
return;
|
||||
}
|
||||
const holding: InvestmentHolding = {
|
||||
certNo: f.certNo,
|
||||
instrumentType: 'FD',
|
||||
scriptCode: f.interestFreq === 'CUMULATIVE' ? 'FD-CUM' : 'FD-MIP',
|
||||
scriptName: f.fdType ? `${f.fdType} Fixed Deposit` : 'Fixed Deposit',
|
||||
party: f.party,
|
||||
subType: f.subType,
|
||||
faceValue: f.depositAmount,
|
||||
purchaseRate: 100,
|
||||
units: 1,
|
||||
purchaseDate: f.depositDate,
|
||||
maturityDate: this.purchaseMaturityDate,
|
||||
interestRate: f.interestRate,
|
||||
interestFreq: f.interestFreq,
|
||||
glCode: f.glAcNo,
|
||||
glName: f.glAcName,
|
||||
crToAc: f.crToAc,
|
||||
brokerCode: '',
|
||||
status: 'ACTIVE'
|
||||
};
|
||||
this.holdings = [
|
||||
holding,
|
||||
...this.holdings
|
||||
];
|
||||
this.txns = [
|
||||
{
|
||||
certNo: holding.certNo,
|
||||
instrumentType: 'FD',
|
||||
txnType: 'PURCHASE',
|
||||
voucherNo: `INV/PUR/FD-${holding.certNo}`,
|
||||
txnDate: f.transactionDate,
|
||||
amount: holding.faceValue,
|
||||
particulars: `${f.tranType === 'RENEWAL' ? 'Renewal' : 'Fresh'} FD Purchase - ${holding.party}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`FD Certificate No. ${holding.certNo} saved successfully.`);
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
cancelPurchase(): void {
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.toast('Entry cancelled — form reset.');
|
||||
}
|
||||
|
||||
/** Legacy "Search" button — loads an existing certificate's values back into the form. */
|
||||
searchPurchase(): void {
|
||||
const h = this.holdings.find((x) => x.certNo === this.searchCertNo.trim());
|
||||
if (!h) {
|
||||
this.toast(`Certificate No. ${this.searchCertNo} not found.`);
|
||||
return;
|
||||
}
|
||||
this.purchaseForm = {
|
||||
...this.blankPurchaseForm(),
|
||||
tranType: 'FRESH',
|
||||
certNo: h.certNo,
|
||||
certDate: h.purchaseDate,
|
||||
party: h.party,
|
||||
subType: h.subType,
|
||||
glAcNo: h.glCode,
|
||||
glAcName: h.glName,
|
||||
transactionDate: h.purchaseDate,
|
||||
depositDate: h.purchaseDate,
|
||||
depositAmount: h.faceValue,
|
||||
interestFreq: h.interestFreq,
|
||||
interestDate: h.purchaseDate,
|
||||
interestRate: h.interestRate,
|
||||
matureAmtCalcType: 'DATEWISE',
|
||||
manualMaturityDate: h.maturityDate,
|
||||
crToAc: h.crToAc
|
||||
};
|
||||
this.toast(`Certificate ${h.certNo} loaded.`);
|
||||
}
|
||||
|
||||
/** Legacy "Drop" button — removes the loaded certificate from the register. */
|
||||
dropPurchase(): void {
|
||||
const certNo = this.purchaseForm.certNo;
|
||||
const exists = this.holdings.some((h) => h.certNo === certNo);
|
||||
if (!exists) {
|
||||
this.toast('Nothing to drop — this certificate is not yet saved.');
|
||||
return;
|
||||
}
|
||||
this.holdings = this.holdings.filter((h) => h.certNo !== certNo);
|
||||
this.txns = this.txns.filter((t) => t.certNo !== certNo);
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.toast(`Certificate ${certNo} dropped.`);
|
||||
}
|
||||
|
||||
/** Legacy "Exit" button — closes the entry form and returns to the register. */
|
||||
exitPurchase(): void {
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
// -------------------- Interest --------------------
|
||||
get interestHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.interestCertNo);
|
||||
}
|
||||
|
||||
interestAccrued(h: InvestmentHolding): number {
|
||||
return accruedInterest(h, this.asOnDate);
|
||||
}
|
||||
|
||||
postInterest(): void {
|
||||
const h = this.interestHolding;
|
||||
if (!h) {
|
||||
this.toast('Select a certificate first.');
|
||||
return;
|
||||
}
|
||||
const amt = this.interestAccrued(h);
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'FD',
|
||||
txnType: 'INTEREST',
|
||||
voucherNo: `INV/INT/FD-${h.certNo}-${this.txns.length + 1}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: amt,
|
||||
particulars: `Interest posted - ${h.party} (${h.interestFreq})`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Interest of ₹${amt.toLocaleString('en-IN')} posted for Certificate ${h.certNo}.`);
|
||||
this.interestCertNo = '';
|
||||
}
|
||||
|
||||
// -------------------- Sell / Premature --------------------
|
||||
get sellHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.sellCertNo);
|
||||
}
|
||||
|
||||
saveSell(): void {
|
||||
const h = this.sellHolding;
|
||||
if (!h || !this.sellDate) {
|
||||
this.toast('Select a certificate and sell/premature date.');
|
||||
return;
|
||||
}
|
||||
const payout = h.faceValue + accruedInterest(h, this.sellDate);
|
||||
h.status = 'SOLD';
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'FD',
|
||||
txnType: 'SELL',
|
||||
voucherNo: `INV/SEL/FD-${h.certNo}`,
|
||||
txnDate: this.sellDate,
|
||||
amount: payout,
|
||||
particulars: `Premature closure - ${h.party}${this.sellRemark ? ' - ' + this.sellRemark : ''}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Certificate ${h.certNo} closed. Payout ₹${payout.toLocaleString('en-IN')}.`);
|
||||
this.sellCertNo = '';
|
||||
this.sellRemark = '';
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
// -------------------- Write-off --------------------
|
||||
get writeoffHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.writeoffCertNo);
|
||||
}
|
||||
|
||||
saveWriteoff(): void {
|
||||
const h = this.writeoffHolding;
|
||||
if (!h || !this.writeoffReason) {
|
||||
this.toast('Select a certificate and enter a write-off reason.');
|
||||
return;
|
||||
}
|
||||
h.status = 'WRITTEN_OFF';
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'FD',
|
||||
txnType: 'WRITE_OFF',
|
||||
voucherNo: `INV/WOF/FD-${h.certNo}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: h.faceValue,
|
||||
particulars: `Written off - ${this.writeoffReason}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Certificate ${h.certNo} written off.`);
|
||||
this.writeoffCertNo = '';
|
||||
this.writeoffReason = '';
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
// -------------------- Part Payment --------------------
|
||||
get partPayHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.partPayCertNo);
|
||||
}
|
||||
|
||||
savePartPayment(): void {
|
||||
const h = this.partPayHolding;
|
||||
if (!h || !this.partPayAmount || this.partPayAmount <= 0 || this.partPayAmount >= h.faceValue) {
|
||||
this.toast('Select a certificate and enter a valid part-payment amount (less than face value).');
|
||||
return;
|
||||
}
|
||||
h.faceValue -= this.partPayAmount;
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'FD',
|
||||
txnType: 'PART_PAYMENT',
|
||||
voucherNo: `INV/PP/FD-${h.certNo}-${this.txns.length + 1}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: this.partPayAmount,
|
||||
particulars: `Part payment withdrawn - ${h.party}, balance ₹${h.faceValue.toLocaleString('en-IN')}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Part payment of ₹${this.partPayAmount.toLocaleString('en-IN')} recorded for Certificate ${h.certNo}.`);
|
||||
this.partPayCertNo = '';
|
||||
this.partPayAmount = null;
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
// -------------------- Helpers --------------------
|
||||
accrued(h: InvestmentHolding): number {
|
||||
return accruedInterest(h, this.asOnDate);
|
||||
}
|
||||
|
||||
maturity(h: InvestmentHolding): number {
|
||||
return maturityAmount(h);
|
||||
}
|
||||
|
||||
daysLeft(h: InvestmentHolding): number {
|
||||
return daysToMaturity(h, this.asOnDate);
|
||||
}
|
||||
|
||||
statusClass(h: InvestmentHolding): string {
|
||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||
}
|
||||
|
||||
private toast(msg: string): void {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
if (this.toastTimer) {
|
||||
clearTimeout(this.toastTimer);
|
||||
}
|
||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT</div>
|
||||
<h1 class="pg-title">Investment Portfolio Dashboard</h1>
|
||||
<p class="pg-sub">
|
||||
Consolidated position across FD, Govt Security, Non-SLR, Bonds, Mutual Fund and Share holdings as on
|
||||
{{ asOnDate }}.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="inv-kpis">
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Total Holdings</span>
|
||||
<span class="inv-kpi__value">{{ totalHoldings }}</span>
|
||||
<span class="inv-kpi__sub">{{ activeCount }} active · {{ maturedCount }} matured</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Total Invested (Face Value)</span>
|
||||
<span class="inv-kpi__value">₹{{ totalFaceValue | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Accrued Interest / Income</span>
|
||||
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Projected Value at Maturity</span>
|
||||
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Instrument-wise Break-up</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Instrument Type</th>
|
||||
<th class="num">Holdings</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num col-in">Accrued Interest</th>
|
||||
<th class="num col-net">Maturity Value</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (s of summaries; track s.type) {
|
||||
<tr>
|
||||
<td>{{ s.label }}</td>
|
||||
<td class="num">{{ s.count }}</td>
|
||||
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num col-in">₹{{ s.accruedInterest | number: '1.2-2' }}</td>
|
||||
<td class="num col-net">₹{{ s.maturityValue | number: '1.2-2' }}</td>
|
||||
<td>
|
||||
<a class="pg-btn pg-btn--ghost" [routerLink]="s.route">Open</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td>Total</td>
|
||||
<td class="num">{{ totalHoldings }}</td>
|
||||
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Due-Date Diary — Maturing Within 90 Days</div>
|
||||
@if (maturingSoon.length === 0) {
|
||||
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
|
||||
} @else {
|
||||
<div>
|
||||
@for (h of maturingSoon; track h.certNo) {
|
||||
<div class="inv-maturity-row">
|
||||
<span>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
{{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }})
|
||||
</span>
|
||||
<span>
|
||||
Matures {{ h.maturityDate }} ·
|
||||
<span
|
||||
class="inv-maturity-days"
|
||||
[class.inv-maturity-days--soon]="daysLeft(h) <= 30"
|
||||
[class.inv-maturity-days--ok]="daysLeft(h) > 30"
|
||||
>
|
||||
{{ daysLeft(h) }} days
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Quick Entry</div>
|
||||
<div class="inv-nav-grid" style="padding: 16px">
|
||||
<a class="inv-nav-card" routerLink="/investment/fd">
|
||||
<span class="inv-nav-card__label">Fixed Deposit</span>
|
||||
<span class="inv-nav-card__count">{{ summaries[0].count }}</span>
|
||||
<span class="inv-nav-card__value">Purchase / Interest / Sell / Write-off</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/bonds">
|
||||
<span class="inv-nav-card__label">Govt Security / Non-SLR / Bonds / MF</span>
|
||||
<span class="inv-nav-card__count">{{
|
||||
summaries[1].count + summaries[2].count + summaries[3].count + summaries[4].count
|
||||
}}</span>
|
||||
<span class="inv-nav-card__value">Purchase / Interest / Sell / Amortize</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/share">
|
||||
<span class="inv-nav-card__label">Share Entry</span>
|
||||
<span class="inv-nav-card__count">{{ summaries[5].count }}</span>
|
||||
<span class="inv-nav-card__value">Purchase / Dividend / Sell / Write-off</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/interest">
|
||||
<span class="inv-nav-card__label">Interest</span>
|
||||
<span class="inv-nav-card__count">5</span>
|
||||
<span class="inv-nav-card__value">FD / Bond Calculation & Registers</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/masters">
|
||||
<span class="inv-nav-card__label">Masters</span>
|
||||
<span class="inv-nav-card__count">10</span>
|
||||
<span class="inv-nav-card__value">Script / Broker / Party / Control Params</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/reports">
|
||||
<span class="inv-nav-card__label">Reports</span>
|
||||
<span class="inv-nav-card__count">20+</span>
|
||||
<span class="inv-nav-card__value">Balance / Ledger / Maturity / Portfolio</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import {
|
||||
ALL_HOLDINGS,
|
||||
INSTRUMENT_LABELS,
|
||||
InstrumentType,
|
||||
InvestmentHolding,
|
||||
accruedInterest,
|
||||
daysToMaturity,
|
||||
maturityAmount
|
||||
} from '../investment-data';
|
||||
|
||||
interface InstrumentSummary {
|
||||
type: InstrumentType;
|
||||
label: string;
|
||||
route: string;
|
||||
count: number;
|
||||
faceValue: number;
|
||||
accruedInterest: number;
|
||||
maturityValue: number;
|
||||
}
|
||||
|
||||
/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never
|
||||
* hand-duplicated, so it can never drift from the per-instrument screens or Reports. */
|
||||
@Component({
|
||||
selector: 'mifosx-investment-dashboard',
|
||||
templateUrl: './investment-dashboard.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink
|
||||
]
|
||||
})
|
||||
export class InvestmentDashboardComponent {
|
||||
asOnDate = '';
|
||||
private readonly types: Array<{ type: InstrumentType; route: string }> = [
|
||||
{ type: 'FD', route: '/investment/fd' },
|
||||
{ type: 'GOVT_SECURITY', route: '/investment/bonds' },
|
||||
{ type: 'NON_SLR', route: '/investment/bonds' },
|
||||
{ type: 'BOND', route: '/investment/bonds' },
|
||||
{ type: 'MUTUAL_FUND', route: '/investment/bonds' },
|
||||
{ type: 'SHARE', route: '/investment/share' }
|
||||
];
|
||||
|
||||
constructor() {
|
||||
const d = new Date();
|
||||
this.asOnDate = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
private holdingsOf(type: InstrumentType): InvestmentHolding[] {
|
||||
return ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
||||
}
|
||||
|
||||
get summaries(): InstrumentSummary[] {
|
||||
return this.types.map(({ type, route }) => {
|
||||
const holdings = this.holdingsOf(type);
|
||||
return {
|
||||
type,
|
||||
label: INSTRUMENT_LABELS[type],
|
||||
route,
|
||||
count: holdings.length,
|
||||
faceValue: holdings.reduce((s, h) => s + h.faceValue, 0),
|
||||
accruedInterest: holdings.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0),
|
||||
maturityValue: holdings.reduce((s, h) => s + maturityAmount(h), 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get totalHoldings(): number {
|
||||
return ALL_HOLDINGS.length;
|
||||
}
|
||||
|
||||
get totalFaceValue(): number {
|
||||
return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0);
|
||||
}
|
||||
|
||||
get totalAccruedInterest(): number {
|
||||
return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0);
|
||||
}
|
||||
|
||||
get totalMaturityValue(): number {
|
||||
return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0);
|
||||
}
|
||||
|
||||
get activeCount(): number {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').length;
|
||||
}
|
||||
|
||||
get maturedCount(): number {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'MATURED').length;
|
||||
}
|
||||
|
||||
/** Holdings maturing within the next 90 days — a "due-date diary" style widget. */
|
||||
get maturingSoon(): InvestmentHolding[] {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && daysToMaturity(h, this.asOnDate) <= 90).sort(
|
||||
(a, b) => daysToMaturity(a, this.asOnDate) - daysToMaturity(b, this.asOnDate)
|
||||
);
|
||||
}
|
||||
|
||||
daysLeft(h: InvestmentHolding): number {
|
||||
return daysToMaturity(h, this.asOnDate);
|
||||
}
|
||||
|
||||
instrumentLabel(type: InstrumentType): string {
|
||||
return INSTRUMENT_LABELS[type];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,352 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · INTEREST</div>
|
||||
<h1 class="pg-title">Investment Interest</h1>
|
||||
<p class="pg-sub">
|
||||
FD and Govt./Bond interest calculation batch runs, plus the Government Bond Interest, FD Interest and FD
|
||||
registers.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'fdCalc'"
|
||||
(click)="setTab('fdCalc')"
|
||||
>
|
||||
01 FD Interest Calculation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'bondCalc'"
|
||||
(click)="setTab('bondCalc')"
|
||||
>
|
||||
02 Govt./Bond Int. Calculation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'bondIntRegister'"
|
||||
(click)="setTab('bondIntRegister')"
|
||||
>
|
||||
03 Government Bond Int Register
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'fdIntRegister'"
|
||||
(click)="setTab('fdIntRegister')"
|
||||
>
|
||||
04 FD Interest Register
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'fdRegister'"
|
||||
(click)="setTab('fdRegister')"
|
||||
>
|
||||
05 FD Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 01 FD INTEREST CALCULATION -->
|
||||
@if (activeTab === 'fdCalc') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - FD Interest Calculation</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Date</span><input type="text" [(ngModel)]="fdCalcDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Certificate No</span>
|
||||
<select [(ngModel)]="fdCalcCertNo">
|
||||
<option value="">-- All Certificates --</option>
|
||||
@for (h of fdHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.party }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>ALL</span><input type="checkbox" [(ngModel)]="fdCalcAll" style="height: 18px; width: 18px"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Total Interest to Post</span
|
||||
><input type="text" [value]="'₹' + (fdCalcTotal | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="processFdInterest()">Process</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Frequency</th>
|
||||
<th class="num col-in">Interest</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of fdCalcTargets; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td>{{ h.interestFreq }}</td>
|
||||
<td class="num col-in">₹{{ fdInterest(h) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No FD certificates match the filter.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="5">Total</td>
|
||||
<td class="num">₹{{ fdCalcTotal | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 02 GOVT./BOND INT. CALCULATION -->
|
||||
@if (activeTab === 'bondCalc') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Govt./Bond Interest Calculation</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Date</span><input type="text" [(ngModel)]="bondCalcDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Type</span>
|
||||
<select [(ngModel)]="bondCalcType">
|
||||
<option value="">-- All Types --</option>
|
||||
<option value="GOVT_SECURITY">GOVT SECURITY</option>
|
||||
<option value="NON_SLR">NON SLR</option>
|
||||
<option value="BOND">BOND</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Script</span>
|
||||
<select [(ngModel)]="bondCalcScript">
|
||||
<option value="">-- All Scripts --</option>
|
||||
@for (s of scriptOptions; track s.scriptCode) {
|
||||
<option [value]="s.scriptCode">{{ s.scriptCode }} — {{ s.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"><span>Deal No</span><input type="text" [(ngModel)]="bondCalcDealNo" /></label>
|
||||
<label class="inv-form-field"
|
||||
><span>ALL</span><input type="checkbox" [(ngModel)]="bondCalcAll" style="height: 18px; width: 18px"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Total Interest to Post</span
|
||||
><input type="text" [value]="'₹' + (bondCalcTotal | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="processBondInterest()">Process</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Frequency</th>
|
||||
<th class="num col-in">Interest</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of bondCalcTargets; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }}</td>
|
||||
<td>{{ instrumentLabel(h.instrumentType) }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td>{{ h.interestFreq }}</td>
|
||||
<td class="num col-in">₹{{ bondInterest(h) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="pg-empty">No holdings match the filter.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="6">Total</td>
|
||||
<td class="num">₹{{ bondCalcTotal | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 03 GOVERNMENT BOND INT REGISTER -->
|
||||
@if (activeTab === 'bondIntRegister') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Government Bond Interest Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Cert No</th>
|
||||
<th>Instrument</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num col-in">Interest Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of bondIntRegister; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.voucherNo }}</span>
|
||||
</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ instrumentLabel(t.instrumentType) }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No bond interest entries recorded yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="5">Total</td>
|
||||
<td class="num">₹{{ bondIntRegisterTotal | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 04 FD INTEREST REGISTER -->
|
||||
@if (activeTab === 'fdIntRegister') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">FD Interest Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Cert No</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num col-in">Interest Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of fdIntRegister; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.voucherNo }}</span>
|
||||
</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5" class="pg-empty">No FD interest entries recorded yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="4">Total</td>
|
||||
<td class="num">₹{{ fdIntRegisterTotal | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 05 FD REGISTER -->
|
||||
@if (activeTab === 'fdRegister') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">FD Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Party</th>
|
||||
<th>Sub Type</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Frequency</th>
|
||||
<th>Deposit Date</th>
|
||||
<th>Maturity Date</th>
|
||||
<th class="num">Days Left</th>
|
||||
<th class="num col-in">Accrued Interest</th>
|
||||
<th class="num col-net">Maturity Value</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of fdRegister; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td>{{ h.subType }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td>{{ h.interestFreq }}</td>
|
||||
<td>{{ h.purchaseDate }}</td>
|
||||
<td>{{ h.maturityDate }}</td>
|
||||
<td class="num">{{ daysLeft(h) }}</td>
|
||||
<td class="num col-in">₹{{ accrued(h) | number: '1.2-2' }}</td>
|
||||
<td class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="3">Total</td>
|
||||
<td class="num">₹{{ fdRegisterFaceTotal | number: '1.2-2' }}</td>
|
||||
<td colspan="5"></td>
|
||||
<td class="num">₹{{ fdRegisterAccruedTotal | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ fdRegisterMaturityTotal | number: '1.2-2' }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
FD_HOLDINGS,
|
||||
GOVT_SECURITY_HOLDINGS,
|
||||
NON_SLR_HOLDINGS,
|
||||
BOND_HOLDINGS,
|
||||
ALL_TXNS,
|
||||
SCRIPT_MASTER,
|
||||
InvestmentHolding,
|
||||
InvestmentTxn,
|
||||
InstrumentType,
|
||||
INSTRUMENT_LABELS,
|
||||
accruedInterest,
|
||||
daysToMaturity,
|
||||
maturityAmount,
|
||||
todayDDMMYYYY
|
||||
} from '../investment-data';
|
||||
|
||||
type InterestTab = 'fdCalc' | 'bondCalc' | 'bondIntRegister' | 'fdIntRegister' | 'fdRegister';
|
||||
|
||||
/**
|
||||
* The legacy "Interest" top-menu for Investment — 5 screens:
|
||||
* 01 FD Interest Calculation, 02 Govt./Bond Int. Calculation (both batch "Process" runs),
|
||||
* 03 Government Bond Int Register, 04 FD Interest Register, 05 FD Register (listings).
|
||||
*
|
||||
* Interest figures are derived from the same holdings used by the Entry screens and Reports,
|
||||
* so a register here always agrees with what a Purchase/Interest entry recorded.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-investment-interest',
|
||||
templateUrl: './investment-interest.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class InvestmentInterestComponent {
|
||||
activeTab: InterestTab = 'fdCalc';
|
||||
asOnDate = todayDDMMYYYY();
|
||||
|
||||
fdHoldings: InvestmentHolding[] = [...FD_HOLDINGS];
|
||||
bondHoldings: InvestmentHolding[] = [
|
||||
...GOVT_SECURITY_HOLDINGS,
|
||||
...NON_SLR_HOLDINGS,
|
||||
...BOND_HOLDINGS
|
||||
];
|
||||
txns: InvestmentTxn[] = [...ALL_TXNS];
|
||||
|
||||
readonly scriptOptions = SCRIPT_MASTER;
|
||||
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
// 01 FD Interest Calculation
|
||||
fdCalcDate = this.asOnDate;
|
||||
fdCalcCertNo = '';
|
||||
fdCalcAll = false;
|
||||
|
||||
// 02 Govt./Bond Int. Calculation
|
||||
bondCalcDate = this.asOnDate;
|
||||
bondCalcType: InstrumentType | '' = '';
|
||||
bondCalcScript = '';
|
||||
bondCalcDealNo = '';
|
||||
bondCalcAll = false;
|
||||
|
||||
// Register filters
|
||||
registerFromDate = '';
|
||||
registerToDate = '';
|
||||
|
||||
setTab(tab: InterestTab): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
instrumentLabel(type: InstrumentType): string {
|
||||
return INSTRUMENT_LABELS[type];
|
||||
}
|
||||
|
||||
statusClass(h: InvestmentHolding): string {
|
||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||
}
|
||||
|
||||
maturity(h: InvestmentHolding): number {
|
||||
return maturityAmount(h);
|
||||
}
|
||||
|
||||
daysLeft(h: InvestmentHolding): number {
|
||||
return daysToMaturity(h, this.asOnDate);
|
||||
}
|
||||
|
||||
// -------------------- 01 FD Interest Calculation --------------------
|
||||
get fdCalcTargets(): InvestmentHolding[] {
|
||||
if (this.fdCalcAll) {
|
||||
return this.fdHoldings.filter((h) => h.status === 'ACTIVE');
|
||||
}
|
||||
return this.fdHoldings.filter(
|
||||
(h) => h.status === 'ACTIVE' && (!this.fdCalcCertNo || h.certNo === this.fdCalcCertNo)
|
||||
);
|
||||
}
|
||||
|
||||
fdInterest(h: InvestmentHolding): number {
|
||||
return accruedInterest(h, this.fdCalcDate || this.asOnDate);
|
||||
}
|
||||
|
||||
get fdCalcTotal(): number {
|
||||
return this.fdCalcTargets.reduce((s, h) => s + this.fdInterest(h), 0);
|
||||
}
|
||||
|
||||
processFdInterest(): void {
|
||||
const targets = this.fdCalcTargets;
|
||||
if (targets.length === 0) {
|
||||
this.toast('No FD holdings match the selected filters.');
|
||||
return;
|
||||
}
|
||||
const newTxns: InvestmentTxn[] = targets.map((h, i) => ({
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'FD',
|
||||
txnType: 'INTEREST',
|
||||
voucherNo: `INV/FD-INT/${(this.fdCalcDate || this.asOnDate).replace(/-/g, '')}-${i + 1}`,
|
||||
txnDate: this.fdCalcDate || this.asOnDate,
|
||||
amount: this.fdInterest(h),
|
||||
particulars: `FD interest calculated - ${h.party}`
|
||||
}));
|
||||
this.txns = [
|
||||
...newTxns,
|
||||
...this.txns
|
||||
];
|
||||
this.toast(
|
||||
`FD interest processed for ${targets.length} certificate(s). Total ₹${this.fdCalcTotal.toLocaleString('en-IN')}.`
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- 02 Govt./Bond Int. Calculation --------------------
|
||||
get bondCalcTargets(): InvestmentHolding[] {
|
||||
if (this.bondCalcAll) {
|
||||
return this.bondHoldings.filter((h) => h.status === 'ACTIVE');
|
||||
}
|
||||
return this.bondHoldings.filter((h) => {
|
||||
if (h.status !== 'ACTIVE') return false;
|
||||
if (this.bondCalcType && h.instrumentType !== this.bondCalcType) return false;
|
||||
if (this.bondCalcScript && h.scriptCode !== this.bondCalcScript) return false;
|
||||
if (this.bondCalcDealNo && h.certNo !== this.bondCalcDealNo.trim()) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bondInterest(h: InvestmentHolding): number {
|
||||
return accruedInterest(h, this.bondCalcDate || this.asOnDate);
|
||||
}
|
||||
|
||||
get bondCalcTotal(): number {
|
||||
return this.bondCalcTargets.reduce((s, h) => s + this.bondInterest(h), 0);
|
||||
}
|
||||
|
||||
processBondInterest(): void {
|
||||
const targets = this.bondCalcTargets;
|
||||
if (targets.length === 0) {
|
||||
this.toast('No Govt/Bond holdings match the selected filters.');
|
||||
return;
|
||||
}
|
||||
const newTxns: InvestmentTxn[] = targets.map((h, i) => ({
|
||||
certNo: h.certNo,
|
||||
instrumentType: h.instrumentType,
|
||||
txnType: 'INTEREST',
|
||||
voucherNo: `INV/BOND-INT/${(this.bondCalcDate || this.asOnDate).replace(/-/g, '')}-${i + 1}`,
|
||||
txnDate: this.bondCalcDate || this.asOnDate,
|
||||
amount: this.bondInterest(h),
|
||||
particulars: `Coupon/interest calculated - ${h.scriptName}`
|
||||
}));
|
||||
this.txns = [
|
||||
...newTxns,
|
||||
...this.txns
|
||||
];
|
||||
this.toast(
|
||||
`Interest processed for ${targets.length} holding(s). Total ₹${this.bondCalcTotal.toLocaleString('en-IN')}.`
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- 03 Government Bond Int Register --------------------
|
||||
get bondIntRegister(): InvestmentTxn[] {
|
||||
return this.txns.filter(
|
||||
(t) => t.txnType === 'INTEREST' && ([
|
||||
'GOVT_SECURITY',
|
||||
'NON_SLR',
|
||||
'BOND'
|
||||
] as InstrumentType[]).includes(t.instrumentType)
|
||||
);
|
||||
}
|
||||
|
||||
get bondIntRegisterTotal(): number {
|
||||
return this.bondIntRegister.reduce((s, t) => s + t.amount, 0);
|
||||
}
|
||||
|
||||
// -------------------- 04 FD Interest Register --------------------
|
||||
get fdIntRegister(): InvestmentTxn[] {
|
||||
return this.txns.filter((t) => t.txnType === 'INTEREST' && t.instrumentType === 'FD');
|
||||
}
|
||||
|
||||
get fdIntRegisterTotal(): number {
|
||||
return this.fdIntRegister.reduce((s, t) => s + t.amount, 0);
|
||||
}
|
||||
|
||||
// -------------------- 05 FD Register --------------------
|
||||
get fdRegister(): InvestmentHolding[] {
|
||||
return this.fdHoldings;
|
||||
}
|
||||
|
||||
get fdRegisterFaceTotal(): number {
|
||||
return this.fdRegister.reduce((s, h) => s + h.faceValue, 0);
|
||||
}
|
||||
|
||||
get fdRegisterMaturityTotal(): number {
|
||||
return this.fdRegister.reduce((s, h) => s + this.maturity(h), 0);
|
||||
}
|
||||
|
||||
accrued(h: InvestmentHolding): number {
|
||||
return accruedInterest(h, this.asOnDate);
|
||||
}
|
||||
|
||||
get fdRegisterAccruedTotal(): number {
|
||||
return this.fdRegister.reduce((s, h) => s + this.accrued(h), 0);
|
||||
}
|
||||
|
||||
private toast(msg: string): void {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
if (this.toastTimer) {
|
||||
clearTimeout(this.toastTimer);
|
||||
}
|
||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · MASTERS</div>
|
||||
<h1 class="pg-title">Investment Masters</h1>
|
||||
<p class="pg-sub">
|
||||
Bank/Party, Script, SubType, Broker, Control Parameter, LIEN Reason, Mutual Fund Product, SGL Bank and Script
|
||||
Market Detail masters.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'party'"
|
||||
(click)="setTab('party')"
|
||||
>
|
||||
04 Bank / Party Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'script'"
|
||||
(click)="setTab('script')"
|
||||
>
|
||||
05 Script Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'subtype'"
|
||||
(click)="setTab('subtype')"
|
||||
>
|
||||
06 SubType Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'broker'"
|
||||
(click)="setTab('broker')"
|
||||
>
|
||||
07 Broker Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'control'"
|
||||
(click)="setTab('control')"
|
||||
>
|
||||
08 Control Parameter
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'dailyscript'"
|
||||
(click)="setTab('dailyscript')"
|
||||
>
|
||||
09 Daily Script Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'lien'"
|
||||
(click)="setTab('lien')"
|
||||
>
|
||||
10 LIEN Reason Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'mfproduct'"
|
||||
(click)="setTab('mfproduct')"
|
||||
>
|
||||
11 Mutual Fund Product Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'sglbank'"
|
||||
(click)="setTab('sglbank')"
|
||||
>
|
||||
12 SGL Bank Master
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'marketdetail'"
|
||||
(click)="setTab('marketdetail')"
|
||||
>
|
||||
13 Script Market Detail
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PARTY MASTER -->
|
||||
@if (activeTab === 'party') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Bank / Party</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Party Name *</span><input type="text" [(ngModel)]="newParty.partyName"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Party Type *</span>
|
||||
<select [(ngModel)]="newParty.partyType">
|
||||
<option [ngValue]="undefined">-- Select --</option>
|
||||
<option value="BANK">BANK</option>
|
||||
<option value="CORPORATE">CORPORATE</option>
|
||||
<option value="GOVT">GOVT</option>
|
||||
<option value="AMC">AMC</option>
|
||||
<option value="EXCHANGE">EXCHANGE</option>
|
||||
<option value="COOP_SOCIETY">COOP SOCIETY</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"><span>PAN No</span><input type="text" [(ngModel)]="newParty.panNo" /></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addParty()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Party Code</th>
|
||||
<th>Party Name</th>
|
||||
<th>Type</th>
|
||||
<th>PAN No</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (p of parties; track p.partyCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ p.partyCode }}</span>
|
||||
</td>
|
||||
<td>{{ p.partyName }}</td>
|
||||
<td>{{ p.partyType }}</td>
|
||||
<td>{{ p.panNo }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SCRIPT MASTER -->
|
||||
@if (activeTab === 'script') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Script</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Script Code</span><input type="text" [(ngModel)]="newScript.scriptCode"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Script Name *</span><input type="text" [(ngModel)]="newScript.scriptName"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Instrument Type *</span>
|
||||
<select [(ngModel)]="newScript.instrumentType">
|
||||
<option [ngValue]="undefined">-- Select --</option>
|
||||
@for (t of instrumentTypes; track t) {
|
||||
<option [value]="t">{{ t }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"><span>ISIN</span><input type="text" [(ngModel)]="newScript.isin" /></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value / Unit</span><input type="number" [(ngModel)]="newScript.faceValuePerUnit"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addScript()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Script Code</th>
|
||||
<th>Script Name</th>
|
||||
<th>Instrument</th>
|
||||
<th>ISIN</th>
|
||||
<th class="num">Face Value/Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (s of scripts; track s.scriptCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ s.scriptCode }}</span>
|
||||
</td>
|
||||
<td>{{ s.scriptName }}</td>
|
||||
<td>{{ s.instrumentType }}</td>
|
||||
<td>{{ s.isin || '—' }}</td>
|
||||
<td class="num">{{ s.faceValuePerUnit }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SUBTYPE MASTER -->
|
||||
@if (activeTab === 'subtype') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Sub Type</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Sub Type Name *</span><input type="text" [(ngModel)]="newSubType.subTypeName"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Instrument Type *</span>
|
||||
<select [(ngModel)]="newSubType.instrumentType">
|
||||
<option [ngValue]="undefined">-- Select --</option>
|
||||
@for (t of instrumentTypes; track t) {
|
||||
<option [value]="t">{{ t }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addSubType()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Sub Type</th>
|
||||
<th>Instrument</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (s of subTypes; track s.subTypeCode + s.subTypeName) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ s.subTypeCode }}</span>
|
||||
</td>
|
||||
<td>{{ s.subTypeName }}</td>
|
||||
<td>{{ s.instrumentType }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- BROKER MASTER -->
|
||||
@if (activeTab === 'broker') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Broker</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Broker Name *</span><input type="text" [(ngModel)]="newBroker.brokerName"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Contact No</span><input type="text" [(ngModel)]="newBroker.contactNo"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Empanelled On</span><input type="text" [(ngModel)]="newBroker.empanelledOn" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addBroker()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Broker Code</th>
|
||||
<th>Broker Name</th>
|
||||
<th>Contact</th>
|
||||
<th>Empanelled On</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (b of brokers; track b.brokerCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ b.brokerCode }}</span>
|
||||
</td>
|
||||
<td>{{ b.brokerName }}</td>
|
||||
<td>{{ b.contactNo }}</td>
|
||||
<td>{{ b.empanelledOn }}</td>
|
||||
<td>
|
||||
<span class="inv-badge inv-badge--active">{{ b.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- CONTROL PARAMETER -->
|
||||
@if (activeTab === 'control') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Control Parameter</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Parameter Name *</span><input type="text" [(ngModel)]="newControlParam.paramName"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Value *</span><input type="text" [(ngModel)]="newControlParam.paramValue"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Description</span><input type="text" [(ngModel)]="newControlParam.description"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addControlParam()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (c of controlParams; track c.paramName) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ c.paramName }}</span>
|
||||
</td>
|
||||
<td class="num">{{ c.paramValue }}</td>
|
||||
<td>{{ c.description }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- DAILY SCRIPT MASTER -->
|
||||
@if (activeTab === 'dailyscript') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Daily Script Rate</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Rate Date *</span><input type="text" [(ngModel)]="newDailyRate.rateDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Script Code *</span>
|
||||
<select [(ngModel)]="newDailyRate.scriptCode">
|
||||
<option [ngValue]="undefined">-- Select --</option>
|
||||
@for (s of scripts; track s.scriptCode) {
|
||||
<option [value]="s.scriptCode">{{ s.scriptCode }} — {{ s.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>Open Rate</span><input type="number" [(ngModel)]="newDailyRate.openRate" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Close Rate *</span><input type="number" [(ngModel)]="newDailyRate.closeRate" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>High Rate</span><input type="number" [(ngModel)]="newDailyRate.highRate" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Low Rate</span><input type="number" [(ngModel)]="newDailyRate.lowRate" step="0.01"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addDailyRate()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rate Date</th>
|
||||
<th>Script Code</th>
|
||||
<th>Script Name</th>
|
||||
<th class="num">Open</th>
|
||||
<th class="num">High</th>
|
||||
<th class="num">Low</th>
|
||||
<th class="num">Close</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of dailyScriptRates; track r.rateDate + r.scriptCode) {
|
||||
<tr>
|
||||
<td>{{ r.rateDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.scriptCode }}</span>
|
||||
</td>
|
||||
<td>{{ r.scriptName }}</td>
|
||||
<td class="num">{{ r.openRate | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ r.highRate | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ r.lowRate | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ r.closeRate | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- LIEN REASON MASTER -->
|
||||
@if (activeTab === 'lien') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add LIEN Reason</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Reason Description *</span><input type="text" [(ngModel)]="newLienReason.reasonDescription"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addLienReason()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reason Code</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (l of lienReasons; track l.reasonCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ l.reasonCode }}</span>
|
||||
</td>
|
||||
<td>{{ l.reasonDescription }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- MUTUAL FUND PRODUCT MASTER -->
|
||||
@if (activeTab === 'mfproduct') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add Mutual Fund Product</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Product Name *</span><input type="text" [(ngModel)]="newMfProduct.productName"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>AMC Name *</span><input type="text" [(ngModel)]="newMfProduct.amcName"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Category</span><input type="text" [(ngModel)]="newMfProduct.category"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Risk Level</span>
|
||||
<select [(ngModel)]="newMfProduct.riskLevel">
|
||||
<option value="LOW">LOW</option>
|
||||
<option value="MODERATE">MODERATE</option>
|
||||
<option value="HIGH">HIGH</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addMfProduct()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product Code</th>
|
||||
<th>Product Name</th>
|
||||
<th>AMC</th>
|
||||
<th>Category</th>
|
||||
<th>Risk</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (m of mfProducts; track m.productCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ m.productCode }}</span>
|
||||
</td>
|
||||
<td>{{ m.productName }}</td>
|
||||
<td>{{ m.amcName }}</td>
|
||||
<td>{{ m.category }}</td>
|
||||
<td>{{ m.riskLevel }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SGL BANK MASTER -->
|
||||
@if (activeTab === 'sglbank') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Add SGL Bank</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Bank Name *</span><input type="text" [(ngModel)]="newSglBank.bankName"
|
||||
/></label>
|
||||
<label class="inv-form-field"><span>Branch</span><input type="text" [(ngModel)]="newSglBank.branch" /></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addSglBank()">Save</button>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SGL Code</th>
|
||||
<th>Bank Name</th>
|
||||
<th>Branch</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (s of sglBanks; track s.sglCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ s.sglCode }}</span>
|
||||
</td>
|
||||
<td>{{ s.bankName }}</td>
|
||||
<td>{{ s.branch }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SCRIPT MARKET DETAIL (also covers "09 Daily Script Master") -->
|
||||
@if (activeTab === 'marketdetail') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Script Market Detail (Daily Script Rates)</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Script Code</th>
|
||||
<th class="num">Market Price</th>
|
||||
<th>As On</th>
|
||||
<th class="num">YTM (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (m of marketDetails; track m.scriptCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ m.scriptCode }}</span>
|
||||
</td>
|
||||
<td class="num">₹{{ m.marketPrice | number: '1.2-2' }}</td>
|
||||
<td>{{ m.asOnDate }}</td>
|
||||
<td class="num">{{ m.ytm }}%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
SCRIPT_MASTER,
|
||||
BROKER_MASTER,
|
||||
PARTY_MASTER,
|
||||
SUBTYPE_MASTER,
|
||||
CONTROL_PARAMETERS,
|
||||
MUTUAL_FUND_PRODUCTS,
|
||||
SGL_BANK_MASTER,
|
||||
SCRIPT_MARKET_DETAIL,
|
||||
DAILY_SCRIPT_RATES,
|
||||
LIEN_REASON_MASTER,
|
||||
ScriptMaster,
|
||||
BrokerMaster,
|
||||
PartyMaster,
|
||||
SubTypeMaster,
|
||||
ControlParameter,
|
||||
MutualFundProductMaster,
|
||||
SglBankMaster,
|
||||
ScriptMarketDetail,
|
||||
DailyScriptRate,
|
||||
LienReason,
|
||||
InstrumentType
|
||||
} from '../investment-data';
|
||||
|
||||
type MasterTab =
|
||||
| 'party'
|
||||
| 'script'
|
||||
| 'subtype'
|
||||
| 'broker'
|
||||
| 'control'
|
||||
| 'dailyscript'
|
||||
| 'lien'
|
||||
| 'mfproduct'
|
||||
| 'sglbank'
|
||||
| 'marketdetail';
|
||||
|
||||
/**
|
||||
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
|
||||
* lists, each with an inline Add-row form so the demo stays interactive without a backend.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-investment-masters',
|
||||
templateUrl: './investment-masters.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class InvestmentMastersComponent {
|
||||
activeTab: MasterTab = 'party';
|
||||
|
||||
scripts: ScriptMaster[] = [...SCRIPT_MASTER];
|
||||
brokers: BrokerMaster[] = [...BROKER_MASTER];
|
||||
parties: PartyMaster[] = [...PARTY_MASTER];
|
||||
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
|
||||
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
|
||||
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
|
||||
sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER];
|
||||
marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL];
|
||||
dailyScriptRates: DailyScriptRate[] = [...DAILY_SCRIPT_RATES];
|
||||
lienReasons: LienReason[] = [...LIEN_REASON_MASTER];
|
||||
|
||||
readonly instrumentTypes: InstrumentType[] = [
|
||||
'FD',
|
||||
'GOVT_SECURITY',
|
||||
'NON_SLR',
|
||||
'BOND',
|
||||
'MUTUAL_FUND',
|
||||
'SHARE'
|
||||
];
|
||||
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
newParty: Partial<PartyMaster> = {};
|
||||
newScript: Partial<ScriptMaster> = {};
|
||||
newSubType: Partial<SubTypeMaster> = {};
|
||||
newBroker: Partial<BrokerMaster> = {};
|
||||
newControlParam: Partial<ControlParameter> = {};
|
||||
newLienReason: Partial<LienReason> = {};
|
||||
newMfProduct: Partial<MutualFundProductMaster> = {};
|
||||
newSglBank: Partial<SglBankMaster> = {};
|
||||
newDailyRate: Partial<DailyScriptRate> = {};
|
||||
|
||||
setTab(tab: MasterTab): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
addParty(): void {
|
||||
if (!this.newParty.partyName || !this.newParty.partyType) {
|
||||
this.toast('Enter Party Name and Type.');
|
||||
return;
|
||||
}
|
||||
this.parties = [
|
||||
{
|
||||
partyCode: `PTY-${String(this.parties.length + 1).padStart(3, '0')}`,
|
||||
panNo: '',
|
||||
...this.newParty
|
||||
} as PartyMaster,
|
||||
...this.parties
|
||||
];
|
||||
this.newParty = {};
|
||||
this.toast('Party/Bank master added.');
|
||||
}
|
||||
|
||||
addScript(): void {
|
||||
if (!this.newScript.scriptName || !this.newScript.instrumentType) {
|
||||
this.toast('Enter Script Name and Instrument Type.');
|
||||
return;
|
||||
}
|
||||
this.scripts = [
|
||||
{
|
||||
scriptCode: this.newScript.scriptCode || `SCR-${this.scripts.length + 1}`,
|
||||
isin: '',
|
||||
faceValuePerUnit: 100,
|
||||
...this.newScript
|
||||
} as ScriptMaster,
|
||||
...this.scripts
|
||||
];
|
||||
this.newScript = {};
|
||||
this.toast('Script master added.');
|
||||
}
|
||||
|
||||
addSubType(): void {
|
||||
if (!this.newSubType.subTypeName || !this.newSubType.instrumentType) {
|
||||
this.toast('Enter Sub Type Name and Instrument Type.');
|
||||
return;
|
||||
}
|
||||
this.subTypes = [
|
||||
{ subTypeCode: String(this.subTypes.length + 1), ...this.newSubType } as SubTypeMaster,
|
||||
...this.subTypes
|
||||
];
|
||||
this.newSubType = {};
|
||||
this.toast('Sub Type master added.');
|
||||
}
|
||||
|
||||
addBroker(): void {
|
||||
if (!this.newBroker.brokerName) {
|
||||
this.toast('Enter Broker Name.');
|
||||
return;
|
||||
}
|
||||
this.brokers = [
|
||||
{
|
||||
brokerCode: `BRK-${String(this.brokers.length + 1).padStart(3, '0')}`,
|
||||
contactNo: '',
|
||||
empanelledOn: '',
|
||||
status: 'ACTIVE',
|
||||
...this.newBroker
|
||||
} as BrokerMaster,
|
||||
...this.brokers
|
||||
];
|
||||
this.newBroker = {};
|
||||
this.toast('Broker master added.');
|
||||
}
|
||||
|
||||
addControlParam(): void {
|
||||
if (!this.newControlParam.paramName || !this.newControlParam.paramValue) {
|
||||
this.toast('Enter Parameter Name and Value.');
|
||||
return;
|
||||
}
|
||||
this.controlParams = [
|
||||
{ description: '', ...this.newControlParam } as ControlParameter,
|
||||
...this.controlParams
|
||||
];
|
||||
this.newControlParam = {};
|
||||
this.toast('Control parameter added.');
|
||||
}
|
||||
|
||||
addLienReason(): void {
|
||||
if (!this.newLienReason.reasonDescription) {
|
||||
this.toast('Enter Reason Description.');
|
||||
return;
|
||||
}
|
||||
this.lienReasons = [
|
||||
{ reasonCode: `LR-${String(this.lienReasons.length + 1).padStart(2, '0')}`, ...this.newLienReason } as LienReason,
|
||||
...this.lienReasons
|
||||
];
|
||||
this.newLienReason = {};
|
||||
this.toast('LIEN reason added.');
|
||||
}
|
||||
|
||||
addMfProduct(): void {
|
||||
if (!this.newMfProduct.productName || !this.newMfProduct.amcName) {
|
||||
this.toast('Enter Product Name and AMC Name.');
|
||||
return;
|
||||
}
|
||||
this.mfProducts = [
|
||||
{
|
||||
productCode: `MFP-${String(this.mfProducts.length + 1).padStart(2, '0')}`,
|
||||
category: '',
|
||||
riskLevel: 'MODERATE',
|
||||
...this.newMfProduct
|
||||
} as MutualFundProductMaster,
|
||||
...this.mfProducts
|
||||
];
|
||||
this.newMfProduct = {};
|
||||
this.toast('Mutual Fund product added.');
|
||||
}
|
||||
|
||||
addSglBank(): void {
|
||||
if (!this.newSglBank.bankName) {
|
||||
this.toast('Enter Bank Name.');
|
||||
return;
|
||||
}
|
||||
this.sglBanks = [
|
||||
{
|
||||
sglCode: `SGL-${String(this.sglBanks.length + 1).padStart(2, '0')}`,
|
||||
branch: '',
|
||||
...this.newSglBank
|
||||
} as SglBankMaster,
|
||||
...this.sglBanks
|
||||
];
|
||||
this.newSglBank = {};
|
||||
this.toast('SGL Bank master added.');
|
||||
}
|
||||
|
||||
addDailyRate(): void {
|
||||
if (!this.newDailyRate.scriptCode || !this.newDailyRate.rateDate || this.newDailyRate.closeRate == null) {
|
||||
this.toast('Enter Rate Date, Script Code and Close Rate.');
|
||||
return;
|
||||
}
|
||||
const script = this.scripts.find((s) => s.scriptCode === this.newDailyRate.scriptCode);
|
||||
this.dailyScriptRates = [
|
||||
{
|
||||
scriptName: script?.scriptName || '',
|
||||
openRate: this.newDailyRate.closeRate,
|
||||
highRate: this.newDailyRate.closeRate,
|
||||
lowRate: this.newDailyRate.closeRate,
|
||||
...this.newDailyRate
|
||||
} as DailyScriptRate,
|
||||
...this.dailyScriptRates
|
||||
];
|
||||
this.newDailyRate = {};
|
||||
this.toast('Daily script rate added.');
|
||||
}
|
||||
|
||||
private toast(msg: string): void {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
if (this.toastTimer) {
|
||||
clearTimeout(this.toastTimer);
|
||||
}
|
||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · REPORTS</div>
|
||||
<h1 class="pg-title">Investment Reports</h1>
|
||||
<p class="pg-sub">
|
||||
Balance reports, ledgers, registers, maturity lists and the consolidated portfolio — all derived live from the
|
||||
same holdings/transactions used by the Entry screens.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeGroup === 'balance'"
|
||||
(click)="setGroup('balance', 'govtSecBalance')"
|
||||
>
|
||||
Balance Reports
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeGroup === 'ledger'"
|
||||
(click)="setGroup('ledger', 'fdLedger')"
|
||||
>
|
||||
Ledgers & Registers
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeGroup === 'maturity'"
|
||||
(click)="setGroup('maturity', 'maturedFd')"
|
||||
>
|
||||
Maturity & Lists
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeGroup === 'portfolio'"
|
||||
(click)="setGroup('portfolio', 'portfolio')"
|
||||
>
|
||||
Portfolio
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ===================== BALANCE REPORTS ===================== -->
|
||||
@if (activeGroup === 'balance') {
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'govtSecBalance'"
|
||||
(click)="setReport('govtSecBalance')"
|
||||
>
|
||||
Govt Security Balance
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'shareBalance'"
|
||||
(click)="setReport('shareBalance')"
|
||||
>
|
||||
Share Balance
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'fdrBalance'"
|
||||
(click)="setReport('fdrBalance')"
|
||||
>
|
||||
FDR Balance
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'bankInvList'"
|
||||
(click)="setReport('bankInvList')"
|
||||
>
|
||||
Bank Investment List
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (activeReport === 'govtSecBalance' || activeReport === 'shareBalance' || activeReport === 'fdrBalance') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
{{
|
||||
activeReport === 'govtSecBalance'
|
||||
? 'Govt Security Balance Report'
|
||||
: activeReport === 'shareBalance'
|
||||
? 'Share Balance Report'
|
||||
: 'FDR Balance Report'
|
||||
}}
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script / Party</th>
|
||||
<th class="num">Units</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Market/Purchase Rate</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (
|
||||
h of activeReport === 'govtSecBalance'
|
||||
? govtSecHoldings
|
||||
: activeReport === 'shareBalance'
|
||||
? shareHoldings
|
||||
: fdHoldings;
|
||||
track h.certNo
|
||||
) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
||||
<td class="num">{{ h.units | number }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ marketPrice(h.scriptCode) || h.purchaseRate }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'bankInvList') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Bank Investment List</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Party / Bank</th>
|
||||
<th class="num">No. of Holdings</th>
|
||||
<th class="num">Total Face Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of bankInvestmentList; track r.party) {
|
||||
<tr>
|
||||
<td>{{ r.party }}</td>
|
||||
<td class="num">{{ r.count }}</td>
|
||||
<td class="num">₹{{ r.totalFaceValue | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ===================== LEDGERS & REGISTERS ===================== -->
|
||||
@if (activeGroup === 'ledger') {
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'fdLedger'"
|
||||
(click)="setReport('fdLedger')"
|
||||
>
|
||||
FD Ledger
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'bondLedger'"
|
||||
(click)="setReport('bondLedger')"
|
||||
>
|
||||
Govt/Bond Ledger
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'shareLedger'"
|
||||
(click)="setReport('shareLedger')"
|
||||
>
|
||||
Share Ledger
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'intDueRegister'"
|
||||
(click)="setReport('intDueRegister')"
|
||||
>
|
||||
Interest Due Register
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'slrRegister'"
|
||||
(click)="setReport('slrRegister')"
|
||||
>
|
||||
SLR Bond Register
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'pendingReceived'"
|
||||
(click)="setReport('pendingReceived')"
|
||||
>
|
||||
Pending/Received Interest
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'amortizeLedger'"
|
||||
(click)="setReport('amortizeLedger')"
|
||||
>
|
||||
Amortize Ledger
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'dealSlip'"
|
||||
(click)="setReport('dealSlip')"
|
||||
>
|
||||
Deal Slip
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (
|
||||
activeReport === 'fdLedger' ||
|
||||
activeReport === 'bondLedger' ||
|
||||
activeReport === 'shareLedger' ||
|
||||
activeReport === 'amortizeLedger' ||
|
||||
activeReport === 'dealSlip'
|
||||
) {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
{{
|
||||
activeReport === 'fdLedger'
|
||||
? 'FD Ledger'
|
||||
: activeReport === 'bondLedger'
|
||||
? 'Govt/Bond Ledger'
|
||||
: activeReport === 'shareLedger'
|
||||
? 'Share Ledger'
|
||||
: activeReport === 'amortizeLedger'
|
||||
? 'Amortize Ledger'
|
||||
: 'Deal Slip Register'
|
||||
}}
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Cert No</th>
|
||||
<th>Type</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (
|
||||
t of activeReport === 'fdLedger'
|
||||
? fdTxns
|
||||
: activeReport === 'bondLedger'
|
||||
? bondTxns
|
||||
: activeReport === 'shareLedger'
|
||||
? shareTxns
|
||||
: activeReport === 'amortizeLedger'
|
||||
? amortizeTxns
|
||||
: dealSlips;
|
||||
track t.voucherNo
|
||||
) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.voucherNo }}</span>
|
||||
</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ t.txnType }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No entries.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'intDueRegister') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Interest Due Register (Non-Cumulative Holdings)</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script / Party</th>
|
||||
<th>Frequency</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num col-in">Due Amount (as on {{ asOnDate }})</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of interestDueRegister; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
||||
<td>{{ h.interestFreq }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td class="num col-in">₹{{ h.dueAmount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'slrRegister') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">SLR Bond Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Maturity Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of slrBondRegister; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td>{{ h.maturityDate }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'pendingReceived') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Received Interest / Dividend</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Cert No</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num col-in">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of receivedInterestTxns; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num col-in">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Pending Interest (No Interest Posted Yet)</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script / Party</th>
|
||||
<th class="num col-out">Accrued (Pending)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of pendingInterestHoldings; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
||||
<td class="num col-out">₹{{ accrued(h) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ===================== MATURITY & LISTS ===================== -->
|
||||
@if (activeGroup === 'maturity') {
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'maturedFd'"
|
||||
(click)="setReport('maturedFd')"
|
||||
>
|
||||
List of Matured FD
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'maturingList'"
|
||||
(click)="setReport('maturingList')"
|
||||
>
|
||||
Govt/FD Mature List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'maturityPattern'"
|
||||
(click)="setReport('maturityPattern')"
|
||||
>
|
||||
Maturity Pattern Register
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'govtSecList'"
|
||||
(click)="setReport('govtSecList')"
|
||||
>
|
||||
Govt Security List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'fdSecList'"
|
||||
(click)="setReport('fdSecList')"
|
||||
>
|
||||
FD Security List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'shareSecList'"
|
||||
(click)="setReport('shareSecList')"
|
||||
>
|
||||
Share Security List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'mfList'"
|
||||
(click)="setReport('mfList')"
|
||||
>
|
||||
Mutual Fund List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === 'shiftingReport'"
|
||||
(click)="setReport('shiftingReport')"
|
||||
>
|
||||
Shifting Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (activeReport === 'maturedFd') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">List of Matured FD</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th>Matured On</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of maturedFdList; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td>{{ h.maturityDate }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="4" class="pg-empty">No matured FDs.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'maturingList') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Govt Security / FD Maturity List (sorted by days-to-maturity)</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Instrument</th>
|
||||
<th>Script / Party</th>
|
||||
<th>Maturity Date</th>
|
||||
<th class="num">Days Left</th>
|
||||
<th class="num col-net">Maturity Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of maturingList; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ instrumentLabel(h.instrumentType) }}</td>
|
||||
<td>{{ h.scriptName }} — {{ h.party }}</td>
|
||||
<td>{{ h.maturityDate }}</td>
|
||||
<td class="num">{{ daysLeft(h) }}</td>
|
||||
<td class="num col-net">₹{{ maturity(h) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'maturityPattern') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Maturity Pattern Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bucket</th>
|
||||
<th class="num">Holdings</th>
|
||||
<th class="num">Face Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (b of maturityBuckets; track b.label) {
|
||||
<tr>
|
||||
<td>{{ b.label }}</td>
|
||||
<td class="num">{{ b.count }}</td>
|
||||
<td class="num">₹{{ b.faceValue | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
activeReport === 'govtSecList' ||
|
||||
activeReport === 'fdSecList' ||
|
||||
activeReport === 'shareSecList' ||
|
||||
activeReport === 'mfList'
|
||||
) {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
{{
|
||||
activeReport === 'govtSecList'
|
||||
? 'Govt Security List'
|
||||
: activeReport === 'fdSecList'
|
||||
? 'FD Security List'
|
||||
: activeReport === 'shareSecList'
|
||||
? 'Share Security List'
|
||||
: 'Mutual Fund List'
|
||||
}}
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num">Rate</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (
|
||||
h of activeReport === 'govtSecList'
|
||||
? govtSecList
|
||||
: activeReport === 'fdSecList'
|
||||
? fdSecList
|
||||
: activeReport === 'shareSecList'
|
||||
? shareSecList
|
||||
: mutualFundList;
|
||||
track h.certNo
|
||||
) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }}</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ h.interestRate }}%</td>
|
||||
<td>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (activeReport === 'shiftingReport') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
Shifting Report — SLR Securities with Market Deviation ≥ 1% (AFS/HTM review candidates)
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th class="num">Purchase Rate</th>
|
||||
<th class="num">Market Price</th>
|
||||
<th class="num">Deviation %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of shiftingCandidates; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }}</td>
|
||||
<td class="num">{{ h.purchaseRate }}</td>
|
||||
<td class="num">{{ h.marketPrice }}</td>
|
||||
<td class="num" [class.col-in]="h.deviationPct > 0" [class.col-out]="h.deviationPct < 0">
|
||||
{{ h.deviationPct }}%
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5" class="pg-empty">No securities crossing the 1% deviation threshold today.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ===================== PORTFOLIO ===================== -->
|
||||
@if (activeGroup === 'portfolio') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment Portfolio — Consolidated Summary</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Instrument Type</th>
|
||||
<th class="num">Holdings</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th class="num col-in">Accrued Interest</th>
|
||||
<th class="num col-net">Maturity Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of portfolioSummary; track r.label) {
|
||||
<tr>
|
||||
<td>{{ r.label }}</td>
|
||||
<td class="num">{{ r.count }}</td>
|
||||
<td class="num">₹{{ r.faceValue | number: '1.2-2' }}</td>
|
||||
<td class="num col-in">₹{{ r.accrued | number: '1.2-2' }}</td>
|
||||
<td class="num col-net">₹{{ r.maturityValue | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td>Total</td>
|
||||
<td class="num"></td>
|
||||
<td class="num">₹{{ portfolioTotalFaceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ portfolioTotalAccrued | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ portfolioTotalMaturity | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ALL_HOLDINGS,
|
||||
ALL_TXNS,
|
||||
FD_HOLDINGS,
|
||||
GOVT_SECURITY_HOLDINGS,
|
||||
NON_SLR_HOLDINGS,
|
||||
BOND_HOLDINGS,
|
||||
MUTUAL_FUND_HOLDINGS,
|
||||
SHARE_HOLDINGS,
|
||||
SCRIPT_MARKET_DETAIL,
|
||||
InstrumentType,
|
||||
InvestmentHolding,
|
||||
InvestmentTxn,
|
||||
INSTRUMENT_LABELS,
|
||||
accruedInterest,
|
||||
maturityAmount,
|
||||
daysToMaturity,
|
||||
todayDDMMYYYY
|
||||
} from '../investment-data';
|
||||
|
||||
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
|
||||
|
||||
/**
|
||||
* ~20 legacy Investment reports, grouped into 4 tabs. Every figure here is derived from the
|
||||
* same ALL_HOLDINGS/ALL_TXNS arrays used by the Entry screens and Dashboard — so a report can
|
||||
* never drift out of sync with what a Purchase/Sell/Interest entry actually recorded.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-investment-reports',
|
||||
templateUrl: './investment-reports.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [CommonModule]
|
||||
})
|
||||
export class InvestmentReportsComponent {
|
||||
activeGroup: ReportGroup = 'balance';
|
||||
activeReport = 'govtSecBalance';
|
||||
asOnDate = todayDDMMYYYY();
|
||||
|
||||
setGroup(group: ReportGroup, defaultReport: string): void {
|
||||
this.activeGroup = group;
|
||||
this.activeReport = defaultReport;
|
||||
}
|
||||
|
||||
setReport(report: string): void {
|
||||
this.activeReport = report;
|
||||
}
|
||||
|
||||
instrumentLabel(type: InstrumentType): string {
|
||||
return INSTRUMENT_LABELS[type];
|
||||
}
|
||||
|
||||
accrued(h: InvestmentHolding): number {
|
||||
return accruedInterest(h, this.asOnDate);
|
||||
}
|
||||
|
||||
maturity(h: InvestmentHolding): number {
|
||||
return maturityAmount(h);
|
||||
}
|
||||
|
||||
daysLeft(h: InvestmentHolding): number {
|
||||
return daysToMaturity(h, this.asOnDate);
|
||||
}
|
||||
|
||||
statusClass(h: InvestmentHolding): string {
|
||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||
}
|
||||
|
||||
marketPrice(scriptCode: string): number | null {
|
||||
return SCRIPT_MARKET_DETAIL.find((m) => m.scriptCode === scriptCode)?.marketPrice ?? null;
|
||||
}
|
||||
|
||||
// -------------------- Balance Reports --------------------
|
||||
get govtSecHoldings(): InvestmentHolding[] {
|
||||
return GOVT_SECURITY_HOLDINGS;
|
||||
}
|
||||
get shareHoldings(): InvestmentHolding[] {
|
||||
return SHARE_HOLDINGS;
|
||||
}
|
||||
get fdHoldings(): InvestmentHolding[] {
|
||||
return FD_HOLDINGS;
|
||||
}
|
||||
get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> {
|
||||
const map = new Map<string, { party: string; count: number; totalFaceValue: number }>();
|
||||
for (const h of ALL_HOLDINGS) {
|
||||
const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 };
|
||||
row.count += 1;
|
||||
row.totalFaceValue += h.faceValue;
|
||||
map.set(h.party, row);
|
||||
}
|
||||
return [...map.values()].sort((a, b) => b.totalFaceValue - a.totalFaceValue);
|
||||
}
|
||||
|
||||
// -------------------- Ledgers & Registers --------------------
|
||||
get fdTxns(): InvestmentTxn[] {
|
||||
return ALL_TXNS.filter((t) => t.instrumentType === 'FD');
|
||||
}
|
||||
get bondTxns(): InvestmentTxn[] {
|
||||
return ALL_TXNS.filter((t) => ([
|
||||
'GOVT_SECURITY',
|
||||
'NON_SLR',
|
||||
'BOND'
|
||||
] as InstrumentType[]).includes(t.instrumentType));
|
||||
}
|
||||
get shareTxns(): InvestmentTxn[] {
|
||||
return ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
||||
}
|
||||
get amortizeTxns(): InvestmentTxn[] {
|
||||
return ALL_TXNS.filter((t) => t.txnType === 'AMORTIZE');
|
||||
}
|
||||
get dealSlips(): InvestmentTxn[] {
|
||||
return ALL_TXNS.filter((t) => t.txnType === 'PURCHASE');
|
||||
}
|
||||
get slrBondRegister(): InvestmentHolding[] {
|
||||
return GOVT_SECURITY_HOLDINGS.filter((h) => h.subType === 'SLR');
|
||||
}
|
||||
get interestDueRegister(): Array<InvestmentHolding & { dueAmount: number }> {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE').map((h) => ({
|
||||
...h,
|
||||
dueAmount: this.accrued(h)
|
||||
}));
|
||||
}
|
||||
get receivedInterestTxns(): InvestmentTxn[] {
|
||||
return ALL_TXNS.filter((t) => t.txnType === 'INTEREST' || t.txnType === 'DIVIDEND');
|
||||
}
|
||||
get pendingInterestHoldings(): InvestmentHolding[] {
|
||||
const receivedCerts = new Set(this.receivedInterestTxns.map((t) => t.certNo));
|
||||
return ALL_HOLDINGS.filter(
|
||||
(h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE' && !receivedCerts.has(h.certNo)
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- Maturity & Lists --------------------
|
||||
get maturedFdList(): InvestmentHolding[] {
|
||||
return FD_HOLDINGS.filter((h) => h.status === 'MATURED');
|
||||
}
|
||||
get maturingList(): InvestmentHolding[] {
|
||||
return [
|
||||
...FD_HOLDINGS,
|
||||
...GOVT_SECURITY_HOLDINGS
|
||||
]
|
||||
.filter((h) => h.status === 'ACTIVE')
|
||||
.sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
|
||||
}
|
||||
get maturityBuckets(): Array<{ label: string; count: number; faceValue: number }> {
|
||||
const buckets = [
|
||||
{ label: '0-1 Year', min: -99999, max: 365, count: 0, faceValue: 0 },
|
||||
{ label: '1-3 Years', min: 366, max: 1095, count: 0, faceValue: 0 },
|
||||
{ label: '3-5 Years', min: 1096, max: 1825, count: 0, faceValue: 0 },
|
||||
{ label: 'Above 5 Years', min: 1826, max: 999999, count: 0, faceValue: 0 }
|
||||
];
|
||||
for (const h of ALL_HOLDINGS.filter((x) => x.status === 'ACTIVE')) {
|
||||
const days = this.daysLeft(h);
|
||||
const bucket = buckets.find((b) => days >= b.min && days <= b.max);
|
||||
if (bucket) {
|
||||
bucket.count += 1;
|
||||
bucket.faceValue += h.faceValue;
|
||||
}
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
get govtSecList(): InvestmentHolding[] {
|
||||
return GOVT_SECURITY_HOLDINGS;
|
||||
}
|
||||
get fdSecList(): InvestmentHolding[] {
|
||||
return FD_HOLDINGS;
|
||||
}
|
||||
get shareSecList(): InvestmentHolding[] {
|
||||
return SHARE_HOLDINGS;
|
||||
}
|
||||
get mutualFundList(): InvestmentHolding[] {
|
||||
return MUTUAL_FUND_HOLDINGS;
|
||||
}
|
||||
get nonSlrList(): InvestmentHolding[] {
|
||||
return NON_SLR_HOLDINGS;
|
||||
}
|
||||
get bondList(): InvestmentHolding[] {
|
||||
return BOND_HOLDINGS;
|
||||
}
|
||||
/** Shifting Report — SLR securities whose market value has moved away from book value by
|
||||
* more than 1%, a proxy for "category shift" candidates (AFS/HTM reclassification review). */
|
||||
get shiftingCandidates(): Array<InvestmentHolding & { marketPrice: number; deviationPct: number }> {
|
||||
return GOVT_SECURITY_HOLDINGS.map((h) => {
|
||||
const mp = this.marketPrice(h.scriptCode);
|
||||
if (mp === null) {
|
||||
return null;
|
||||
}
|
||||
const deviationPct = +(((mp - h.purchaseRate) / h.purchaseRate) * 100).toFixed(2);
|
||||
return { ...h, marketPrice: mp, deviationPct };
|
||||
})
|
||||
.filter(
|
||||
(x): x is InvestmentHolding & { marketPrice: number; deviationPct: number } =>
|
||||
x !== null && Math.abs(x.deviationPct) >= 1
|
||||
)
|
||||
.sort((a, b) => Math.abs(b.deviationPct) - Math.abs(a.deviationPct));
|
||||
}
|
||||
|
||||
// -------------------- Portfolio --------------------
|
||||
private readonly portfolioTypes: InstrumentType[] = [
|
||||
'FD',
|
||||
'GOVT_SECURITY',
|
||||
'NON_SLR',
|
||||
'BOND',
|
||||
'MUTUAL_FUND',
|
||||
'SHARE'
|
||||
];
|
||||
get portfolioSummary(): Array<{
|
||||
label: string;
|
||||
count: number;
|
||||
faceValue: number;
|
||||
accrued: number;
|
||||
maturityValue: number;
|
||||
}> {
|
||||
return this.portfolioTypes.map((type) => {
|
||||
const holdings = ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
||||
return {
|
||||
label: INSTRUMENT_LABELS[type],
|
||||
count: holdings.length,
|
||||
faceValue: holdings.reduce((s, h) => s + h.faceValue, 0),
|
||||
accrued: holdings.reduce((s, h) => s + this.accrued(h), 0),
|
||||
maturityValue: holdings.reduce((s, h) => s + this.maturity(h), 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
get portfolioTotalFaceValue(): number {
|
||||
return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0);
|
||||
}
|
||||
get portfolioTotalAccrued(): number {
|
||||
return this.portfolioSummary.reduce((s, r) => s + r.accrued, 0);
|
||||
}
|
||||
get portfolioTotalMaturity(): number {
|
||||
return this.portfolioSummary.reduce((s, r) => s + r.maturityValue, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { InvestmentDashboardComponent } from './investment-dashboard/investment-dashboard.component';
|
||||
import { FdInvestmentComponent } from './fd-investment/fd-investment.component';
|
||||
import { BondInvestmentComponent } from './bond-investment/bond-investment.component';
|
||||
import { ShareInvestmentComponent } from './share-investment/share-investment.component';
|
||||
import { InvestmentMastersComponent } from './investment-masters/investment-masters.component';
|
||||
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
||||
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'investment',
|
||||
data: { title: 'Investment', breadcrumb: 'Investment' },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: InvestmentDashboardComponent,
|
||||
data: { title: 'Investment Dashboard', breadcrumb: 'Investment Dashboard' }
|
||||
},
|
||||
{
|
||||
path: 'fd',
|
||||
component: FdInvestmentComponent,
|
||||
data: { title: 'FD Investment', breadcrumb: 'FD Investment' }
|
||||
},
|
||||
{
|
||||
path: 'bonds',
|
||||
component: BondInvestmentComponent,
|
||||
data: { title: 'Govt Security / Bond Investment', breadcrumb: 'Govt Security / Bond Investment' }
|
||||
},
|
||||
{
|
||||
path: 'share',
|
||||
component: ShareInvestmentComponent,
|
||||
data: { title: 'Share Investment', breadcrumb: 'Share Investment' }
|
||||
},
|
||||
{
|
||||
path: 'masters',
|
||||
component: InvestmentMastersComponent,
|
||||
data: { title: 'Investment Masters', breadcrumb: 'Investment Masters' }
|
||||
},
|
||||
{
|
||||
path: 'interest',
|
||||
component: InvestmentInterestComponent,
|
||||
data: { title: 'Investment Interest', breadcrumb: 'Investment Interest' }
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
component: InvestmentReportsComponent,
|
||||
data: { title: 'Investment Reports', breadcrumb: 'Investment Reports' }
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class InvestmentRoutingModule {}
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// Extra styles layered on top of ../cashier/cashier-shared.scss (pg-*, cctr-*) for the
|
||||
// Investment module — instrument nav cards, status badges, KPI tiles, entry-form layout.
|
||||
|
||||
.inv-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.inv-kpi {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-nav-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.inv-nav-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 2px 10px rgb(15 79 60 / 10%);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
&--active {
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
&--matured {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
&--sold {
|
||||
background: #fefce8;
|
||||
color: #a16207;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
&--written_off {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 14px 20px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.inv-form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Long "code — name (branch)" style option text must never run under the native dropdown
|
||||
// arrow — truncate it with an ellipsis instead of letting the browser clip it mid-word.
|
||||
select {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Computed / lookup-filled fields are rendered read-only and greyed out so they read as
|
||||
// derived values rather than something the user is expected to type into.
|
||||
/* stylelint-disable-next-line selector-no-qualifying-type -- [readonly] only applies to inputs */
|
||||
input[readonly] {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 0 18px 18px;
|
||||
}
|
||||
|
||||
.inv-maturity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px dotted #eef2f7;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-maturity-days {
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--soon {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&--ok {
|
||||
color: #16a34a;
|
||||
}
|
||||
}
|
||||
|
||||
.inv-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 25%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.2s;
|
||||
z-index: 2000;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { InvestmentRoutingModule } from './investment-routing.module';
|
||||
import { InvestmentDashboardComponent } from './investment-dashboard/investment-dashboard.component';
|
||||
import { FdInvestmentComponent } from './fd-investment/fd-investment.component';
|
||||
import { BondInvestmentComponent } from './bond-investment/bond-investment.component';
|
||||
import { ShareInvestmentComponent } from './share-investment/share-investment.component';
|
||||
import { InvestmentMastersComponent } from './investment-masters/investment-masters.component';
|
||||
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
||||
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
InvestmentRoutingModule,
|
||||
InvestmentDashboardComponent,
|
||||
FdInvestmentComponent,
|
||||
BondInvestmentComponent,
|
||||
ShareInvestmentComponent,
|
||||
InvestmentMastersComponent,
|
||||
InvestmentReportsComponent,
|
||||
InvestmentInterestComponent
|
||||
]
|
||||
})
|
||||
export class InvestmentModule {}
|
||||
@@ -0,0 +1,276 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · SHARE</div>
|
||||
<h1 class="pg-title">Share Entry</h1>
|
||||
<p class="pg-sub">
|
||||
Equity and cooperative-society share holdings — Purchase, Dividend, Sell and Write-off entries.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'holdings'"
|
||||
(click)="setTab('holdings')"
|
||||
>
|
||||
Holdings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'purchase'"
|
||||
(click)="setTab('purchase')"
|
||||
>
|
||||
01 Share Purchase Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'dividend'"
|
||||
(click)="setTab('dividend')"
|
||||
>
|
||||
02 Share Interest / Dividend Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'sell'"
|
||||
(click)="setTab('sell')"
|
||||
>
|
||||
03 Share Sell Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'writeoff'"
|
||||
(click)="setTab('writeoff')"
|
||||
>
|
||||
04 Share Write Off Entry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- HOLDINGS -->
|
||||
@if (activeTab === 'holdings') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Share Holdings Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Units</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th>Purchase Date</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of holdings; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }}</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td class="num">{{ h.units | number }}</td>
|
||||
<td class="num">₹{{ h.purchaseRate | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td>{{ h.purchaseDate }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PURCHASE ENTRY -->
|
||||
@if (activeTab === 'purchase') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Purchase Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Cert No</span><input type="text" [(ngModel)]="purchaseForm.certNo" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Script Code</span><input type="text" [(ngModel)]="purchaseForm.scriptCode"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Script/Company Name *</span><input type="text" [(ngModel)]="purchaseForm.scriptName"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Party / Exchange</span><input type="text" [(ngModel)]="purchaseForm.party"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Sub Type</span>
|
||||
<select [(ngModel)]="purchaseForm.subType">
|
||||
<option value="EQUITY">EQUITY</option>
|
||||
<option value="CO-OP SHARE">CO-OP SHARE</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>No. of Units *</span><input type="number" [(ngModel)]="purchaseForm.units" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Purchase Rate *</span><input type="number" [(ngModel)]="purchaseForm.purchaseRate" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Purchase Date *</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.purchaseDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Broker Code</span><input type="text" [(ngModel)]="purchaseForm.brokerCode"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>CR To A/c</span><input type="text" [(ngModel)]="purchaseForm.crToAc"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelPurchase()">Cancel</button>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="savePurchase()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- DIVIDEND ENTRY -->
|
||||
@if (activeTab === 'dividend') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Interest / Dividend Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="dividendCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (dividendHolding; as h) {
|
||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Dividend per Share *</span><input type="number" [(ngModel)]="dividendPerShare" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Total Dividend (computed)</span
|
||||
><input type="text" [value]="'₹' + (dividendAmount | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveDividend()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SELL ENTRY -->
|
||||
@if (activeTab === 'sell') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Sell Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="sellCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (sellHolding; as h) {
|
||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Units to Sell *</span><input type="number" [(ngModel)]="sellUnits" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Sell Rate *</span><input type="number" [(ngModel)]="sellRate" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Consideration (computed)</span
|
||||
><input type="text" [value]="'₹' + (sellConsideration | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveSell()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- WRITE OFF ENTRY -->
|
||||
@if (activeTab === 'writeoff') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Write Off Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="writeoffCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (writeoffHolding; as h) {
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value to be Written Off</span
|
||||
><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"><span>Reason *</span><input type="text" [(ngModel)]="writeoffReason" /></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveWriteoff()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (activeTab !== 'holdings') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Share Transaction Log</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Cert No</th>
|
||||
<th>Type</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of txns; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.voucherNo }}</span>
|
||||
</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ t.txnType }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SHARE_HOLDINGS, ALL_TXNS, InvestmentHolding, InvestmentTxn, todayDDMMYYYY } from '../investment-data';
|
||||
|
||||
type ShareTab = 'holdings' | 'purchase' | 'dividend' | 'sell' | 'writeoff';
|
||||
|
||||
interface SharePurchaseForm {
|
||||
certNo: string;
|
||||
scriptCode: string;
|
||||
scriptName: string;
|
||||
party: string;
|
||||
subType: string;
|
||||
units: number | null;
|
||||
purchaseRate: number | null;
|
||||
purchaseDate: string;
|
||||
brokerCode: string;
|
||||
crToAc: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Share Entry treasury screen — Holdings register + Purchase / Dividend / Sell / Write-off
|
||||
* entry tabs (legacy "02 Share Entry" menu). Dummy in-memory data; Save appends locally.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-share-investment',
|
||||
templateUrl: './share-investment.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class ShareInvestmentComponent {
|
||||
activeTab: ShareTab = 'holdings';
|
||||
asOnDate = todayDDMMYYYY();
|
||||
|
||||
holdings: InvestmentHolding[] = [...SHARE_HOLDINGS];
|
||||
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
||||
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
purchaseForm: SharePurchaseForm = this.blankPurchaseForm();
|
||||
dividendCertNo = '';
|
||||
dividendPerShare: number | null = null;
|
||||
sellCertNo = '';
|
||||
sellUnits: number | null = null;
|
||||
sellRate: number | null = null;
|
||||
writeoffCertNo = '';
|
||||
writeoffReason = '';
|
||||
|
||||
setTab(tab: ShareTab): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
get activeHoldings(): InvestmentHolding[] {
|
||||
return this.holdings.filter((h) => h.status === 'ACTIVE');
|
||||
}
|
||||
|
||||
private blankPurchaseForm(): SharePurchaseForm {
|
||||
return {
|
||||
certNo: `SH-${6000 + this.holdings.length + 1}`,
|
||||
scriptCode: '',
|
||||
scriptName: '',
|
||||
party: 'NSE/BSE',
|
||||
subType: 'EQUITY',
|
||||
units: null,
|
||||
purchaseRate: null,
|
||||
purchaseDate: this.asOnDate,
|
||||
brokerCode: '',
|
||||
crToAc: '310070'
|
||||
};
|
||||
}
|
||||
|
||||
savePurchase(): void {
|
||||
const f = this.purchaseForm;
|
||||
if (!f.scriptName || !f.units || !f.purchaseRate) {
|
||||
this.toast('Please fill Script Name, No. of Units and Purchase Rate.');
|
||||
return;
|
||||
}
|
||||
const faceValue = f.units * f.purchaseRate;
|
||||
const holding: InvestmentHolding = {
|
||||
certNo: f.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
scriptCode: f.scriptCode || f.scriptName,
|
||||
scriptName: f.scriptName,
|
||||
party: f.party,
|
||||
subType: f.subType,
|
||||
faceValue,
|
||||
purchaseRate: f.purchaseRate,
|
||||
units: f.units,
|
||||
purchaseDate: f.purchaseDate,
|
||||
maturityDate: f.purchaseDate,
|
||||
interestRate: 0,
|
||||
interestFreq: 'YEARLY',
|
||||
glCode: '310060',
|
||||
glName: 'INVESTMENT IN SHARES',
|
||||
crToAc: f.crToAc,
|
||||
brokerCode: f.brokerCode,
|
||||
status: 'ACTIVE'
|
||||
};
|
||||
this.holdings = [
|
||||
holding,
|
||||
...this.holdings
|
||||
];
|
||||
this.txns = [
|
||||
{
|
||||
certNo: holding.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'PURCHASE',
|
||||
voucherNo: `INV/PUR/${holding.certNo}`,
|
||||
txnDate: f.purchaseDate,
|
||||
amount: faceValue,
|
||||
particulars: `Purchase of ${f.units} shares - ${holding.scriptName}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Share holding ${holding.certNo} saved successfully.`);
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
cancelPurchase(): void {
|
||||
this.purchaseForm = this.blankPurchaseForm();
|
||||
}
|
||||
|
||||
// -------------------- Dividend --------------------
|
||||
get dividendHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.dividendCertNo);
|
||||
}
|
||||
|
||||
get dividendAmount(): number {
|
||||
const h = this.dividendHolding;
|
||||
if (!h || !this.dividendPerShare) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(h.units * this.dividendPerShare);
|
||||
}
|
||||
|
||||
saveDividend(): void {
|
||||
const h = this.dividendHolding;
|
||||
if (!h || !this.dividendPerShare) {
|
||||
this.toast('Select a holding and enter dividend per share.');
|
||||
return;
|
||||
}
|
||||
const amt = this.dividendAmount;
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'DIVIDEND',
|
||||
voucherNo: `INV/DIV/${h.certNo}-${this.txns.length + 1}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: amt,
|
||||
particulars: `Dividend received - ${h.scriptName} (₹${this.dividendPerShare}/share)`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Dividend of ₹${amt.toLocaleString('en-IN')} recorded for ${h.certNo}.`);
|
||||
this.dividendCertNo = '';
|
||||
this.dividendPerShare = null;
|
||||
}
|
||||
|
||||
// -------------------- Sell --------------------
|
||||
get sellHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.sellCertNo);
|
||||
}
|
||||
|
||||
get sellConsideration(): number {
|
||||
if (!this.sellUnits || !this.sellRate) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(this.sellUnits * this.sellRate);
|
||||
}
|
||||
|
||||
saveSell(): void {
|
||||
const h = this.sellHolding;
|
||||
if (!h || !this.sellUnits || !this.sellRate || this.sellUnits > h.units) {
|
||||
this.toast('Select a holding and enter valid units (≤ held) and sell rate.');
|
||||
return;
|
||||
}
|
||||
const consideration = this.sellConsideration;
|
||||
h.units -= this.sellUnits;
|
||||
h.faceValue = Math.round(h.units * h.purchaseRate);
|
||||
if (h.units === 0) {
|
||||
h.status = 'SOLD';
|
||||
}
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'SELL',
|
||||
voucherNo: `INV/SEL/${h.certNo}-${this.txns.length + 1}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: consideration,
|
||||
particulars: `Sold ${this.sellUnits} shares of ${h.scriptName} @ ${this.sellRate}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`Sold ${this.sellUnits} shares. Consideration ₹${consideration.toLocaleString('en-IN')}.`);
|
||||
this.sellCertNo = '';
|
||||
this.sellUnits = null;
|
||||
this.sellRate = null;
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
// -------------------- Write-off --------------------
|
||||
get writeoffHolding(): InvestmentHolding | undefined {
|
||||
return this.holdings.find((h) => h.certNo === this.writeoffCertNo);
|
||||
}
|
||||
|
||||
saveWriteoff(): void {
|
||||
const h = this.writeoffHolding;
|
||||
if (!h || !this.writeoffReason) {
|
||||
this.toast('Select a holding and enter a write-off reason.');
|
||||
return;
|
||||
}
|
||||
h.status = 'WRITTEN_OFF';
|
||||
this.txns = [
|
||||
{
|
||||
certNo: h.certNo,
|
||||
instrumentType: 'SHARE',
|
||||
txnType: 'WRITE_OFF',
|
||||
voucherNo: `INV/WOF/${h.certNo}`,
|
||||
txnDate: this.asOnDate,
|
||||
amount: h.faceValue,
|
||||
particulars: `Written off - ${this.writeoffReason}`
|
||||
},
|
||||
...this.txns
|
||||
];
|
||||
this.toast(`${h.certNo} written off.`);
|
||||
this.writeoffCertNo = '';
|
||||
this.writeoffReason = '';
|
||||
this.activeTab = 'holdings';
|
||||
}
|
||||
|
||||
statusClass(h: InvestmentHolding): string {
|
||||
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
||||
}
|
||||
|
||||
private toast(msg: string): void {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
if (this.toastTimer) {
|
||||
clearTimeout(this.toastTimer);
|
||||
}
|
||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// Dummy NPA (Non-Performing Asset) data layer for the NPA Reports / Analysis / Classification
|
||||
// screens. Everything downstream — classification, provisioning, interest reversal, GNPA/NNPA —
|
||||
// is *derived* from LOAN_ACCOUNTS via pure functions below, so no two reports can ever disagree
|
||||
// on the same account's numbers.
|
||||
//
|
||||
// The book is generated with a seeded RNG (not Math.random()) so the "large, real-world-CBS"
|
||||
// dataset stays identical across reloads instead of reshuffling every time the page opens.
|
||||
|
||||
export type NpaStage =
|
||||
| 'STANDARD'
|
||||
| 'SMA0'
|
||||
| 'SMA1'
|
||||
| 'SMA2'
|
||||
| 'SUBSTANDARD'
|
||||
| 'DOUBTFUL1'
|
||||
| 'DOUBTFUL2'
|
||||
| 'DOUBTFUL3'
|
||||
| 'LOSS';
|
||||
|
||||
export const STAGE_ORDER: NpaStage[] = [
|
||||
'STANDARD',
|
||||
'SMA0',
|
||||
'SMA1',
|
||||
'SMA2',
|
||||
'SUBSTANDARD',
|
||||
'DOUBTFUL1',
|
||||
'DOUBTFUL2',
|
||||
'DOUBTFUL3',
|
||||
'LOSS'
|
||||
];
|
||||
|
||||
export const STAGE_LABELS: Record<NpaStage, string> = {
|
||||
STANDARD: 'Standard',
|
||||
SMA0: 'SMA-0',
|
||||
SMA1: 'SMA-1',
|
||||
SMA2: 'SMA-2',
|
||||
SUBSTANDARD: 'Sub-Standard',
|
||||
DOUBTFUL1: 'Doubtful-1',
|
||||
DOUBTFUL2: 'Doubtful-2',
|
||||
DOUBTFUL3: 'Doubtful-3',
|
||||
LOSS: 'Loss Asset'
|
||||
};
|
||||
|
||||
export function isNpaStage(stage: NpaStage): boolean {
|
||||
return stage !== 'STANDARD' && stage !== 'SMA0' && stage !== 'SMA1' && stage !== 'SMA2';
|
||||
}
|
||||
|
||||
export function isSmaStage(stage: NpaStage): boolean {
|
||||
return stage === 'SMA0' || stage === 'SMA1' || stage === 'SMA2';
|
||||
}
|
||||
|
||||
export function stageBadgeClass(stage: NpaStage): string {
|
||||
return `npa-badge npa-badge--${stage.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/** Recovery-workflow stage tracked separately from IRAC classification — a Doubtful/Loss
|
||||
* account can sit at any of these while its classification stays driven purely by overdueDays. */
|
||||
export type LegalActionStatus = 'NONE' | 'NOTICE_ISSUED' | 'SARFAESI_INITIATED' | 'SUIT_FILED' | 'OTS_NEGOTIATION';
|
||||
|
||||
export const LEGAL_ACTION_LABELS: Record<LegalActionStatus, string> = {
|
||||
NONE: 'No Action Yet',
|
||||
NOTICE_ISSUED: 'Demand Notice Issued',
|
||||
SARFAESI_INITIATED: 'SARFAESI Action Initiated',
|
||||
SUIT_FILED: 'Recovery Suit Filed',
|
||||
OTS_NEGOTIATION: 'One-Time Settlement Under Negotiation'
|
||||
};
|
||||
|
||||
export interface RecoveryTxn {
|
||||
date: string;
|
||||
amount: number;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export interface LoanAccount {
|
||||
accountNo: string;
|
||||
customerName: string;
|
||||
guarantorName: string;
|
||||
branchCode: string;
|
||||
branchName: string;
|
||||
productCode: string;
|
||||
productName: string;
|
||||
secured: boolean;
|
||||
sanctionedAmount: number;
|
||||
disbursementDate: string;
|
||||
interestRate: number;
|
||||
outstandingBalance: number;
|
||||
overdueAmount: number;
|
||||
overdueDays: number;
|
||||
overdueDaysQuarterAgo: number;
|
||||
collateralValue: number;
|
||||
lastPaymentDate: string;
|
||||
isLossAsset: boolean;
|
||||
isRestructured: boolean;
|
||||
provisionCoverageRatio: number;
|
||||
recoveries: RecoveryTxn[];
|
||||
/** '—' for Standard/SMA accounts — only NPA accounts are actually assigned a reason. */
|
||||
npaReason: string;
|
||||
/** '—' for Standard accounts — SMA and NPA accounts get a recovery officer assigned for follow-up. */
|
||||
recoveryOfficer: string;
|
||||
legalActionStatus: LegalActionStatus;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Date helpers
|
||||
// -------------------------------------------------------------------------
|
||||
function fmt(d: Date): string {
|
||||
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
function addDays(d: Date, days: number): Date {
|
||||
const r = new Date(d);
|
||||
r.setDate(r.getDate() + days);
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Parses a "DD-MM-YYYY" string back into a Date, so generated dates can be compared/ordered
|
||||
* against each other (e.g. "is this recovery more recent than the last-payment estimate?"). */
|
||||
function parseDMY(s: string): Date {
|
||||
const [
|
||||
d,
|
||||
m,
|
||||
y
|
||||
] = s.split('-').map(Number);
|
||||
return new Date(y, m - 1, d);
|
||||
}
|
||||
|
||||
export function todayDDMMYYYY(): string {
|
||||
return fmt(new Date());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Seeded RNG — keeps the generated book stable across reloads
|
||||
// -------------------------------------------------------------------------
|
||||
function mulberry32(seed: number): () => number {
|
||||
return function random(): number {
|
||||
seed |= 0;
|
||||
seed = (seed + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
const rng = mulberry32(19870614);
|
||||
const pick = <T>(arr: T[]): T => arr[Math.floor(rng() * arr.length)];
|
||||
const between = (min: number, max: number): number => min + rng() * (max - min);
|
||||
const round = (n: number): number => Math.round(n);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Masters
|
||||
// -------------------------------------------------------------------------
|
||||
export interface BranchMaster {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
// BR01/BR02 are deliberately named "Branch 1"/"Branch 2" — identical to the two branches in
|
||||
// cash-clearing-transfer-summary.component.ts — so a single pair of real office names in the DB
|
||||
// gets recognised by BOTH reports instead of each module expecting a different branch name.
|
||||
export const BRANCHES: BranchMaster[] = [
|
||||
{ code: 'BR01', name: 'Branch 1' },
|
||||
{ code: 'BR02', name: 'Branch 2' },
|
||||
{ code: 'BR03', name: 'Borivali Branch' },
|
||||
{ code: 'BR04', name: 'Thane Branch' },
|
||||
{ code: 'BR05', name: 'Pune Branch' },
|
||||
{ code: 'BR06', name: 'Nashik Branch' },
|
||||
{ code: 'BR07', name: 'Nagpur Branch' },
|
||||
{ code: 'BR08', name: 'Kolhapur Branch' }
|
||||
];
|
||||
|
||||
interface ProductMaster {
|
||||
code: string;
|
||||
name: string;
|
||||
secured: boolean;
|
||||
minAmt: number;
|
||||
maxAmt: number;
|
||||
minRate: number;
|
||||
maxRate: number;
|
||||
}
|
||||
export const LOAN_PRODUCTS: ProductMaster[] = [
|
||||
{
|
||||
code: '01',
|
||||
name: 'Cash Credit (MSME)',
|
||||
secured: true,
|
||||
minAmt: 500000,
|
||||
maxAmt: 5000000,
|
||||
minRate: 11,
|
||||
maxRate: 13.5
|
||||
},
|
||||
{ code: '02', name: 'Term Loan - MSME', secured: true, minAmt: 300000, maxAmt: 3000000, minRate: 11, maxRate: 14 },
|
||||
{ code: '03', name: 'Housing Loan', secured: true, minAmt: 1000000, maxAmt: 4000000, minRate: 8.5, maxRate: 10.5 },
|
||||
{ code: '04', name: 'Vehicle Loan', secured: true, minAmt: 300000, maxAmt: 1500000, minRate: 9, maxRate: 11.5 },
|
||||
{ code: '05', name: 'Gold Loan', secured: true, minAmt: 50000, maxAmt: 1000000, minRate: 9, maxRate: 12 },
|
||||
{ code: '06', name: 'Kisan Credit Card', secured: true, minAmt: 100000, maxAmt: 500000, minRate: 7, maxRate: 9 },
|
||||
{ code: '07', name: 'Personal Loan', secured: false, minAmt: 50000, maxAmt: 500000, minRate: 13, maxRate: 16 },
|
||||
{ code: '08', name: 'Education Loan', secured: false, minAmt: 200000, maxAmt: 1500000, minRate: 10, maxRate: 12 },
|
||||
{
|
||||
code: '09',
|
||||
name: 'SHG / Micro Finance Loan',
|
||||
secured: false,
|
||||
minAmt: 20000,
|
||||
maxAmt: 200000,
|
||||
minRate: 12,
|
||||
maxRate: 14
|
||||
},
|
||||
{
|
||||
code: '10',
|
||||
name: 'Overdraft Against Property',
|
||||
secured: true,
|
||||
minAmt: 500000,
|
||||
maxAmt: 2500000,
|
||||
minRate: 11,
|
||||
maxRate: 13
|
||||
}
|
||||
];
|
||||
|
||||
const FIRST_NAMES = [
|
||||
'Ramesh',
|
||||
'Suresh',
|
||||
'Mahesh',
|
||||
'Ganesh',
|
||||
'Dinesh',
|
||||
'Vijay',
|
||||
'Sanjay',
|
||||
'Ajay',
|
||||
'Prakash',
|
||||
'Vinod',
|
||||
'Sunita',
|
||||
'Anita',
|
||||
'Kavita',
|
||||
'Savita',
|
||||
'Rekha',
|
||||
'Meena',
|
||||
'Pooja',
|
||||
'Sangita',
|
||||
'Nirmala',
|
||||
'Usha',
|
||||
'Ravindra',
|
||||
'Mahendra',
|
||||
'Narendra',
|
||||
'Jitendra',
|
||||
'Rajendra',
|
||||
'Bhagwan',
|
||||
'Balasaheb',
|
||||
'Vasant',
|
||||
'Shivaji',
|
||||
'Yogesh',
|
||||
'Kiran',
|
||||
'Manisha',
|
||||
'Deepak',
|
||||
'Sandeep',
|
||||
'Pradeep',
|
||||
'Ashok',
|
||||
'Vishal',
|
||||
'Nitin',
|
||||
'Amol',
|
||||
'Santosh'
|
||||
];
|
||||
const LAST_NAMES = [
|
||||
'Patil',
|
||||
'More',
|
||||
'Jadhav',
|
||||
'Deshmukh',
|
||||
'Pawar',
|
||||
'Kulkarni',
|
||||
'Shinde',
|
||||
'Gaikwad',
|
||||
'Chavan',
|
||||
'Bhosale',
|
||||
'Kale',
|
||||
'Salunkhe',
|
||||
'Sawant',
|
||||
'Kadam',
|
||||
'Joshi',
|
||||
'Wagh',
|
||||
'Thorat',
|
||||
'Waghmare',
|
||||
'Mane',
|
||||
'Shaikh'
|
||||
];
|
||||
|
||||
const NPA_REASONS = [
|
||||
'Crop Failure / Agri Distress',
|
||||
'Business Loss',
|
||||
'Health Issue / Death of Borrower',
|
||||
'Diversion of Funds',
|
||||
'Wilful Default',
|
||||
'Natural Calamity',
|
||||
'Loss of Employment',
|
||||
'Family Dispute / Litigation',
|
||||
'Delayed Receivables from Debtors',
|
||||
'Unit Closure'
|
||||
];
|
||||
|
||||
/** Recovery officers are bank staff, kept distinct from the FIRST_NAMES/LAST_NAMES pool used for
|
||||
* customers so a name never accidentally reads as "the officer is also the borrower". */
|
||||
const RECOVERY_OFFICERS = [
|
||||
'Anil Deshpande',
|
||||
'Sunil Kadam',
|
||||
'Vikas Pathak',
|
||||
'Rajendra More',
|
||||
'Sachin Bhagat',
|
||||
'Prashant Jagtap',
|
||||
'Milind Kulkarni',
|
||||
'Ravi Ghadge'
|
||||
];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Weighted stage buckets — used only to seed a realistic distribution of
|
||||
// overdue-day counts; the actual classification is always re-derived from
|
||||
// `overdueDays` via classifyDays(), never stored directly.
|
||||
// -------------------------------------------------------------------------
|
||||
interface StageBucket {
|
||||
weight: number;
|
||||
minDays: number;
|
||||
maxDays: number;
|
||||
isLossPool: boolean;
|
||||
}
|
||||
const BUCKETS: StageBucket[] = [
|
||||
{ weight: 60, minDays: 0, maxDays: 0, isLossPool: false },
|
||||
{ weight: 10, minDays: 1, maxDays: 30, isLossPool: false },
|
||||
{ weight: 7, minDays: 31, maxDays: 60, isLossPool: false },
|
||||
{ weight: 6, minDays: 61, maxDays: 90, isLossPool: false },
|
||||
{ weight: 9, minDays: 91, maxDays: 365, isLossPool: false },
|
||||
{ weight: 4, minDays: 366, maxDays: 730, isLossPool: false },
|
||||
{ weight: 2.5, minDays: 731, maxDays: 1095, isLossPool: false },
|
||||
{ weight: 1.5, minDays: 1096, maxDays: 1900, isLossPool: true }
|
||||
];
|
||||
const TOTAL_WEIGHT = BUCKETS.reduce((s, b) => s + b.weight, 0);
|
||||
|
||||
function pickBucket(): StageBucket {
|
||||
let r = rng() * TOTAL_WEIGHT;
|
||||
for (const b of BUCKETS) {
|
||||
if (r < b.weight) {
|
||||
return b;
|
||||
}
|
||||
r -= b.weight;
|
||||
}
|
||||
return BUCKETS[0];
|
||||
}
|
||||
|
||||
/** Recovery-workflow progression — only NPA accounts get any legal action, and the more
|
||||
* advanced/stressed the classification, the more likely the bank has already escalated. */
|
||||
function legalActionFor(stage: NpaStage): LegalActionStatus {
|
||||
if (!isNpaStage(stage)) {
|
||||
return 'NONE';
|
||||
}
|
||||
const r = rng();
|
||||
if (stage === 'SUBSTANDARD') {
|
||||
return r < 0.2 ? 'NOTICE_ISSUED' : 'NONE';
|
||||
}
|
||||
if (stage === 'DOUBTFUL1') {
|
||||
if (r < 0.15) return 'OTS_NEGOTIATION';
|
||||
if (r < 0.45) return 'SARFAESI_INITIATED';
|
||||
if (r < 0.65) return 'NOTICE_ISSUED';
|
||||
return 'NONE';
|
||||
}
|
||||
// DOUBTFUL2, DOUBTFUL3, LOSS
|
||||
if (r < 0.1) return 'OTS_NEGOTIATION';
|
||||
if (r < 0.55) return 'SUIT_FILED';
|
||||
if (r < 0.8) return 'SARFAESI_INITIATED';
|
||||
if (r < 0.9) return 'NOTICE_ISSUED';
|
||||
return 'NONE';
|
||||
}
|
||||
|
||||
function generateAccounts(count: number): LoanAccount[] {
|
||||
const today = new Date();
|
||||
const accounts: LoanAccount[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const branch = pick(BRANCHES);
|
||||
const product = pick(LOAN_PRODUCTS);
|
||||
const bucket = pickBucket();
|
||||
const overdueDays = bucket.maxDays === 0 ? 0 : round(between(bucket.minDays, bucket.maxDays));
|
||||
const isLossAsset = bucket.isLossPool && rng() < 0.4;
|
||||
// Restructured accounts skew towards the already-stressed buckets (SMA-2 and worse),
|
||||
// matching how RBI restructuring relief is actually used in practice.
|
||||
const stressed = classifyDays(overdueDays, isLossAsset);
|
||||
const isRestructured =
|
||||
rng() < (stressed === 'STANDARD' || stressed === 'SMA0' || stressed === 'SMA1' ? 0.04 : 0.18);
|
||||
|
||||
const sanctionedAmount = round(between(product.minAmt, product.maxAmt) / 1000) * 1000;
|
||||
// The loan must be old enough to have actually accrued `overdueDays` of delinquency, plus a
|
||||
// minimum "good" performing stretch beforehand — otherwise a badly-overdue account could end
|
||||
// up with a disbursement date that falls AFTER its own last-payment/NPA date, which would look
|
||||
// wrong no matter what day this report is viewed on.
|
||||
const minGoodPeriodDays = round(between(30, 180));
|
||||
const loanAgeDays = Math.max(round(between(0.5, 8) * 365), overdueDays + minGoodPeriodDays);
|
||||
const disbursementDate = fmt(addDays(today, -loanAgeDays));
|
||||
const interestRate = +between(product.minRate, product.maxRate).toFixed(2);
|
||||
const outstandingBalance = round(sanctionedAmount * between(0.25, 0.9));
|
||||
const overdueFrac = overdueDays === 0 ? 0 : Math.min(0.85, overdueDays / 1200 + between(0, 0.15));
|
||||
const overdueAmount = round(outstandingBalance * overdueFrac);
|
||||
const collateralValue = product.secured ? round(sanctionedAmount * between(0.85, 1.25)) : 0;
|
||||
|
||||
const npaReason = isNpaStage(stressed) ? pick(NPA_REASONS) : '—';
|
||||
const recoveryOfficer = isNpaStage(stressed) || isSmaStage(stressed) ? pick(RECOVERY_OFFICERS) : '—';
|
||||
const legalActionStatus = legalActionFor(stressed);
|
||||
|
||||
// Quarter-ago overdue count is capped at how old the loan actually was 90 days back — a loan
|
||||
// can't have been overdue for longer than it had existed at that point in time.
|
||||
const drift = round((rng() - 0.4) * 130);
|
||||
const maxQuarterAgoOverdueDays = Math.max(0, loanAgeDays - 90);
|
||||
const overdueDaysQuarterAgo = Math.min(Math.max(0, overdueDays - drift), maxQuarterAgoOverdueDays);
|
||||
|
||||
const recoveries: RecoveryTxn[] = [];
|
||||
if (overdueDays > 90 && rng() < 0.5) {
|
||||
const numRecoveries = 1 + Math.floor(rng() * 3);
|
||||
for (let r = 0; r < numRecoveries; r++) {
|
||||
recoveries.push({
|
||||
date: fmt(addDays(today, -round(between(10, Math.min(overdueDays, 400))))),
|
||||
amount: round(outstandingBalance * between(0.02, 0.12)),
|
||||
mode: pick([
|
||||
'Cash Deposit',
|
||||
'Cheque',
|
||||
'NEFT/RTGS',
|
||||
'Auction / SARFAESI',
|
||||
'One-Time Settlement'
|
||||
])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A recorded recovery IS a payment — if one landed more recently than the formula-based
|
||||
// last-payment estimate, the account's "last payment" must reflect that, otherwise the two
|
||||
// fields on the Account Detail screen would visibly contradict each other.
|
||||
const formulaLastPaymentDate = fmt(addDays(today, -Math.max(5, overdueDays - round(between(0, 20)))));
|
||||
const latestRecoveryDate = recoveries.reduce<Date | null>((latest, r) => {
|
||||
const d = parseDMY(r.date);
|
||||
return !latest || d > latest ? d : latest;
|
||||
}, null);
|
||||
const lastPaymentDate =
|
||||
latestRecoveryDate && latestRecoveryDate > parseDMY(formulaLastPaymentDate)
|
||||
? fmt(latestRecoveryDate)
|
||||
: formulaLastPaymentDate;
|
||||
|
||||
const seq = String(1000 + i).padStart(6, '0');
|
||||
accounts.push({
|
||||
accountNo: `1${branch.code.slice(2)}${product.code}${seq}`,
|
||||
customerName: `${pick(FIRST_NAMES)} ${pick(LAST_NAMES)}`,
|
||||
guarantorName: `${pick(FIRST_NAMES)} ${pick(LAST_NAMES)}`,
|
||||
branchCode: branch.code,
|
||||
branchName: branch.name,
|
||||
productCode: product.code,
|
||||
productName: product.name,
|
||||
secured: product.secured,
|
||||
sanctionedAmount,
|
||||
disbursementDate,
|
||||
interestRate,
|
||||
outstandingBalance,
|
||||
overdueAmount,
|
||||
overdueDays,
|
||||
overdueDaysQuarterAgo,
|
||||
collateralValue,
|
||||
lastPaymentDate,
|
||||
isLossAsset,
|
||||
isRestructured,
|
||||
provisionCoverageRatio: +between(0.75, 1.1).toFixed(2),
|
||||
recoveries,
|
||||
npaReason,
|
||||
recoveryOfficer,
|
||||
legalActionStatus
|
||||
});
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
// 8 branches x ~80 accounts/branch on average — sized to look like a real cooperative bank's
|
||||
// loan book rather than a token demo sample. Pagination in the reports UI is what makes a book
|
||||
// this size usable, not a smaller dataset.
|
||||
export const LOAN_ACCOUNTS: LoanAccount[] = generateAccounts(640);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Classification & provisioning (RBI IRAC norms, simplified for a demo book)
|
||||
// -------------------------------------------------------------------------
|
||||
export function classifyDays(days: number, isLossAsset: boolean): NpaStage {
|
||||
if (isLossAsset && days > 1095) {
|
||||
return 'LOSS';
|
||||
}
|
||||
if (days <= 0) {
|
||||
return 'STANDARD';
|
||||
}
|
||||
if (days <= 30) {
|
||||
return 'SMA0';
|
||||
}
|
||||
if (days <= 60) {
|
||||
return 'SMA1';
|
||||
}
|
||||
if (days <= 90) {
|
||||
return 'SMA2';
|
||||
}
|
||||
if (days <= 365) {
|
||||
return 'SUBSTANDARD';
|
||||
}
|
||||
if (days <= 730) {
|
||||
return 'DOUBTFUL1';
|
||||
}
|
||||
if (days <= 1095) {
|
||||
return 'DOUBTFUL2';
|
||||
}
|
||||
return 'DOUBTFUL3';
|
||||
}
|
||||
|
||||
export function currentStage(a: LoanAccount): NpaStage {
|
||||
return classifyDays(a.overdueDays, a.isLossAsset);
|
||||
}
|
||||
|
||||
export function previousStage(a: LoanAccount): NpaStage {
|
||||
return classifyDays(a.overdueDaysQuarterAgo, a.isLossAsset && a.overdueDaysQuarterAgo > 1095);
|
||||
}
|
||||
|
||||
export function provisionRatePct(stage: NpaStage, secured: boolean): number {
|
||||
switch (stage) {
|
||||
case 'STANDARD':
|
||||
case 'SMA0':
|
||||
case 'SMA1':
|
||||
return 0.4;
|
||||
case 'SMA2':
|
||||
return 0.75;
|
||||
case 'SUBSTANDARD':
|
||||
return secured ? 15 : 25;
|
||||
case 'DOUBTFUL1':
|
||||
return secured ? 25 : 100;
|
||||
case 'DOUBTFUL2':
|
||||
return secured ? 40 : 100;
|
||||
case 'DOUBTFUL3':
|
||||
case 'LOSS':
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
export function provisionRequired(a: LoanAccount): number {
|
||||
const stage = currentStage(a);
|
||||
if (!isNpaStage(stage)) {
|
||||
return round(a.outstandingBalance * (provisionRatePct(stage, a.secured) / 100));
|
||||
}
|
||||
const securedPortion = Math.min(a.outstandingBalance, a.collateralValue);
|
||||
const unsecuredPortion = Math.max(0, a.outstandingBalance - a.collateralValue);
|
||||
const securedRate = provisionRatePct(stage, true) / 100;
|
||||
const unsecuredRate = provisionRatePct(stage, false) / 100;
|
||||
return round(securedPortion * securedRate + unsecuredPortion * unsecuredRate);
|
||||
}
|
||||
|
||||
/** Provision held can run a little above or below what's required (over/under-provisioning
|
||||
* happens in real banks) — but it can never exceed the account's own outstanding balance, since
|
||||
* a bank cannot hold more provision than the money actually at risk on that one loan. */
|
||||
export function provisionHeld(a: LoanAccount): number {
|
||||
return Math.min(a.outstandingBalance, round(provisionRequired(a) * a.provisionCoverageRatio));
|
||||
}
|
||||
|
||||
/** Interest income de-recognised because the account has been NPA for longer than 90 days. */
|
||||
export function interestReversed(a: LoanAccount): number {
|
||||
const stage = currentStage(a);
|
||||
if (!isNpaStage(stage)) {
|
||||
return 0;
|
||||
}
|
||||
const daysSinceNpa = Math.max(0, a.overdueDays - 90);
|
||||
return round(a.outstandingBalance * (a.interestRate / 100) * (daysSinceNpa / 365));
|
||||
}
|
||||
|
||||
/** Date the account first crossed 90 days overdue (i.e. became NPA) — derived from overdueDays,
|
||||
* never stored separately, so it always agrees with the account's current classification. */
|
||||
export function npaSinceDate(a: LoanAccount): string {
|
||||
if (!isNpaStage(currentStage(a))) {
|
||||
return '—';
|
||||
}
|
||||
const daysSinceNpa = Math.max(0, a.overdueDays - 90);
|
||||
return fmt(addDays(new Date(), -daysSinceNpa));
|
||||
}
|
||||
|
||||
export function totalRecovered(a: LoanAccount): number {
|
||||
return a.recoveries.reduce((s, r) => s + r.amount, 0);
|
||||
}
|
||||
|
||||
export function movementLabel(prev: NpaStage, curr: NpaStage): string {
|
||||
if (prev === curr) {
|
||||
return 'No Change';
|
||||
}
|
||||
const prevIdx = STAGE_ORDER.indexOf(prev);
|
||||
const currIdx = STAGE_ORDER.indexOf(curr);
|
||||
if (currIdx > prevIdx) {
|
||||
return !isNpaStage(prev) && isNpaStage(curr) ? 'Fresh Slippage to NPA' : 'Downgraded';
|
||||
}
|
||||
return isNpaStage(prev) && !isNpaStage(curr) ? 'Upgraded to Standard' : 'Improved';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Report menu metadata — drives both the shell mega-menu routerLinks and the
|
||||
// in-page group/report tab rows, so a menu item and its screen can never
|
||||
// point at different labels.
|
||||
// -------------------------------------------------------------------------
|
||||
export type ReportGroupId = 'reports' | 'analysis' | 'classification';
|
||||
export interface ReportMeta {
|
||||
id: string;
|
||||
group: ReportGroupId;
|
||||
label: string;
|
||||
}
|
||||
export const REPORT_META: ReportMeta[] = [
|
||||
{ id: 'interest', group: 'reports', label: 'NPA Interest' },
|
||||
{ id: 'status', group: 'reports', label: 'NPA Status Report' },
|
||||
{ id: 'recovery', group: 'reports', label: 'NPA Recovery Report' },
|
||||
{ id: 'summary', group: 'reports', label: 'NPA Summary Report' },
|
||||
{ id: 'account-detail', group: 'analysis', label: 'NPA Account Detail Report' },
|
||||
{ id: 'movement', group: 'analysis', label: 'NPA Movement Report' },
|
||||
{ id: 'provision', group: 'analysis', label: 'NPA with Provision and Required Provision' },
|
||||
{ id: 'probable', group: 'classification', label: 'Probable NPA' },
|
||||
{ id: 'sma', group: 'classification', label: 'SMA Report' }
|
||||
];
|
||||
export const GROUP_LABELS: Record<ReportGroupId, string> = {
|
||||
reports: 'NPA Reports',
|
||||
analysis: 'NPA Analysis',
|
||||
classification: 'NPA Classification'
|
||||
};
|
||||
export const DEFAULT_REPORT_ID = 'summary';
|
||||
@@ -0,0 +1,949 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">CREDIT · NON-PERFORMING ASSETS · {{ scopeName | uppercase }}</div>
|
||||
<h1 class="pg-title">{{ reportLabel(activeReport) }}</h1>
|
||||
<p class="pg-sub">
|
||||
RBI IRAC-norm classification (Standard → SMA-0/1/2 → Sub-Standard → Doubtful-1/2/3 → Loss) across
|
||||
{{ allAccounts.length }} of the bank's {{ bankWideTotalAccounts }} loan accounts, as on {{ asOnDate }}.
|
||||
</p>
|
||||
</div>
|
||||
<div class="pg-toolbar">
|
||||
@if (isHeadOffice) {
|
||||
<label class="pg-field">
|
||||
<span>Branch</span>
|
||||
<select class="pg-select" [ngModel]="branchFilter" (ngModelChange)="setBranch($event)">
|
||||
<option value="">-- All Branches --</option>
|
||||
@for (b of branches; track b.code) {
|
||||
<option [value]="b.code">{{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
} @else {
|
||||
<label class="pg-field">
|
||||
<span>Branch</span>
|
||||
<span class="pg-chip">{{ scopeName }}</span>
|
||||
</label>
|
||||
}
|
||||
<label class="pg-field">
|
||||
<span>As On</span>
|
||||
<input type="text" [(ngModel)]="asOnDate" placeholder="DD-MM-YYYY" />
|
||||
</label>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="generate()">Generate</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="inv-kpis npa-kpis--ratio">
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Total Advances</span>
|
||||
<span class="inv-kpi__value">₹{{ totalAdvances | number: '1.0-0' }}</span>
|
||||
<span class="inv-kpi__sub">{{ allAccounts.length }} accounts</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Gross NPA (GNPA)</span>
|
||||
<span class="inv-kpi__value npa-warn">₹{{ gnpaAmount | number: '1.0-0' }}</span>
|
||||
<span class="inv-kpi__sub"
|
||||
>{{ npaAccounts.length }} accounts · {{ gnpaRatio | number: '1.2-2' }}% of advances</span
|
||||
>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Net NPA (NNPA)</span>
|
||||
<span class="inv-kpi__value npa-warn">₹{{ netNpaAmount | number: '1.0-0' }}</span>
|
||||
<span class="inv-kpi__sub">{{ nnpaRatio | number: '1.2-2' }}% of advances</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Provision Held (NPA)</span>
|
||||
<span class="inv-kpi__value">₹{{ totalNpaProvisionHeld | number: '1.0-0' }}</span>
|
||||
<span class="inv-kpi__sub">Recovered to date ₹{{ totalRecoveredAmount | number: '1.0-0' }}</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Restructured Accounts</span>
|
||||
<span class="inv-kpi__value">{{ restructuredAccounts.length }}</span>
|
||||
<span class="inv-kpi__sub">₹{{ restructuredOutstanding | number: '1.0-0' }} outstanding</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
@for (g of ['reports', 'analysis', 'classification']; track g) {
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeGroup === g"
|
||||
(click)="goToReport(groupOf(g)[0].id)"
|
||||
>
|
||||
{{ groupLabels[g] }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
@for (m of groupOf(activeGroup); track m.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeReport === m.id"
|
||||
(click)="goToReport(m.id)"
|
||||
>
|
||||
{{ m.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- ===================== 01 NPA INTEREST ===================== -->
|
||||
@if (activeReport === 'interest') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">NPA Interest — Income Reversed on Non-Performing Accounts</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Product</th>
|
||||
<th>Stage</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Overdue Days</th>
|
||||
<th class="num col-out">Interest Reversed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of paged(npaAccounts); track a.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ a.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ a.customerName }}</td>
|
||||
<td>{{ a.branchName }}</td>
|
||||
<td>{{ a.productName }}</td>
|
||||
<td>
|
||||
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
|
||||
</td>
|
||||
<td class="num">₹{{ a.outstandingBalance | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ a.interestRate }}%</td>
|
||||
<td class="num">{{ a.overdueDays }}</td>
|
||||
<td class="num col-out">₹{{ interestRev(a) | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="9" class="pg-empty">No NPA accounts in this branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="8">Total Interest Reversed ({{ npaAccounts.length }} accounts)</td>
|
||||
<td class="num">₹{{ sumInterest(npaAccounts) | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@if (npaAccounts.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(npaAccounts) }}–{{ rangeEnd(npaAccounts) }} of {{ npaAccounts.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, npaAccounts)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(npaAccounts) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(npaAccounts)"
|
||||
(click)="goToPage(currentPage + 1, npaAccounts)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== 02 NPA STATUS REPORT ===================== -->
|
||||
@if (activeReport === 'status') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">NPA Status Report — Full Non-Performing Book</div>
|
||||
<div class="pg-table-scroll">
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Product</th>
|
||||
<th>Secured</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num">Overdue Amount</th>
|
||||
<th class="num">Overdue Days</th>
|
||||
<th>NPA Since</th>
|
||||
<th>Stage</th>
|
||||
<th>Restructured</th>
|
||||
<th>Legal Action</th>
|
||||
<th class="num col-out">Provision Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of paged(npaAccounts); track a.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ a.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ a.customerName }}</td>
|
||||
<td>{{ a.branchName }}</td>
|
||||
<td>{{ a.productName }}</td>
|
||||
<td>{{ a.secured ? 'Yes' : 'No' }}</td>
|
||||
<td class="num">₹{{ a.outstandingBalance | number: '1.0-0' }}</td>
|
||||
<td class="num">₹{{ a.overdueAmount | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ a.overdueDays }}</td>
|
||||
<td>{{ npaSince(a) }}</td>
|
||||
<td>
|
||||
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
|
||||
</td>
|
||||
<td>{{ a.isRestructured ? 'Yes' : 'No' }}</td>
|
||||
<td [class]="legalActionClass(a.legalActionStatus)">{{ legalActionLabel(a.legalActionStatus) }}</td>
|
||||
<td class="num col-out">₹{{ provisionReq(a) | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="13" class="pg-empty">No NPA accounts in this branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (npaAccounts.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(npaAccounts) }}–{{ rangeEnd(npaAccounts) }} of {{ npaAccounts.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, npaAccounts)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(npaAccounts) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(npaAccounts)"
|
||||
(click)="goToPage(currentPage + 1, npaAccounts)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== 03 NPA RECOVERY REPORT ===================== -->
|
||||
@if (activeReport === 'recovery') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Record a Recovery Against an NPA Account</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>* NPA Account</span>
|
||||
<select [(ngModel)]="newRecovery.accountNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (a of recoveryOptions; track a.accountNo) {
|
||||
<option [value]="a.accountNo">
|
||||
{{ a.accountNo }} — {{ a.customerName }} ({{ stageLabel(stage(a)) }})
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>* Recovery Date</span>
|
||||
<input type="text" [(ngModel)]="newRecovery.date" placeholder="DD-MM-YYYY" />
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>* Recovery Amount</span>
|
||||
<input type="number" [(ngModel)]="newRecovery.amount" />
|
||||
</label>
|
||||
<label class="inv-form-field">
|
||||
<span>Mode</span>
|
||||
<select [(ngModel)]="newRecovery.mode">
|
||||
<option>Cash Deposit</option>
|
||||
<option>Cheque</option>
|
||||
<option>NEFT/RTGS</option>
|
||||
<option>Auction / SARFAESI</option>
|
||||
<option>One-Time Settlement</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="addRecovery()">Save Recovery Entry</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
NPA Recovery Register — ₹{{ totalRecoveredAmount | number: '1.0-0' }} recovered against ₹{{
|
||||
gnpaAmount | number: '1.0-0'
|
||||
}}
|
||||
GNPA outstanding
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Mode</th>
|
||||
<th class="num col-in">Recovered Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of paged(recoveryRows); track r.accountNo + r.date + r.amount) {
|
||||
<tr>
|
||||
<td>{{ r.date }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ r.customerName }}</td>
|
||||
<td>{{ r.branchName }}</td>
|
||||
<td>{{ r.mode }}</td>
|
||||
<td class="num col-in">₹{{ r.amount | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No recoveries recorded yet.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="5">Total Recovered</td>
|
||||
<td class="num">₹{{ totalRecoveredAmount | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@if (recoveryRows.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(recoveryRows) }}–{{ rangeEnd(recoveryRows) }} of {{ recoveryRows.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, recoveryRows)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(recoveryRows) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(recoveryRows)"
|
||||
(click)="goToPage(currentPage + 1, recoveryRows)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== 04 NPA SUMMARY REPORT ===================== -->
|
||||
@if (activeReport === 'summary') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Stage-wise Summary — {{ scopeName }}</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Classification</th>
|
||||
<th class="num">Accounts</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num col-out">Provision Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of stageSummary; track row.stage) {
|
||||
<tr>
|
||||
<td>
|
||||
<span [class]="badgeClass(row.stage)">{{ row.label }}</span>
|
||||
</td>
|
||||
<td class="num">{{ row.count }}</td>
|
||||
<td class="num">₹{{ row.outstanding | number: '1.0-0' }}</td>
|
||||
<td class="num col-out">₹{{ row.provisionReq | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td>Total</td>
|
||||
<td class="num">{{ grandTotalAccounts }}</td>
|
||||
<td class="num">₹{{ grandTotalOutstanding | number: '1.0-0' }}</td>
|
||||
<td class="num">₹{{ grandTotalProvision | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (isHeadOffice && !branchFilter) {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Branch-wise NPA Summary — All Branches</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Branch</th>
|
||||
<th class="num">Total Accounts</th>
|
||||
<th class="num">Total Advances</th>
|
||||
<th class="num">NPA Accounts</th>
|
||||
<th class="num col-out">GNPA Outstanding</th>
|
||||
<th class="num col-out">Provision Required</th>
|
||||
<th class="num">GNPA %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of branchSummary; track row.branchCode) {
|
||||
<tr>
|
||||
<td>
|
||||
<a href="javascript:void(0)" (click)="setBranch(row.branchCode)">{{ row.branchName }}</a>
|
||||
</td>
|
||||
<td class="num">{{ row.totalAccounts }}</td>
|
||||
<td class="num">₹{{ row.totalAdvances | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ row.npaCount }}</td>
|
||||
<td class="num col-out">₹{{ row.npaOutstanding | number: '1.0-0' }}</td>
|
||||
<td class="num col-out">₹{{ row.provisionReq | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ row.gnpaRatio | number: '1.2-2' }}%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ===================== 05 NPA ACCOUNT DETAIL REPORT ===================== -->
|
||||
@if (activeReport === 'account-detail') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Search NPA Account</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field npa-form-field--full">
|
||||
<span>Account No / Customer</span>
|
||||
<select [(ngModel)]="selectedAccountNo">
|
||||
@for (a of npaAccounts; track a.accountNo) {
|
||||
<option [value]="a.accountNo">{{ a.accountNo }} — {{ a.customerName }} ({{ a.branchName }})</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="searchAccount()">View Details</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (selectedAccount; as a) {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
{{ a.accountNo }} — {{ a.customerName }}
|
||||
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
|
||||
</div>
|
||||
<dl class="npa-detail-grid">
|
||||
<div class="npa-detail-field">
|
||||
<dt>Branch</dt>
|
||||
<dd>{{ a.branchName }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Product</dt>
|
||||
<dd>{{ a.productName }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Secured</dt>
|
||||
<dd>{{ a.secured ? 'Yes' : 'No' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Guarantor</dt>
|
||||
<dd>{{ a.guarantorName }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Sanctioned Amount</dt>
|
||||
<dd>₹{{ a.sanctionedAmount | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Disbursement Date</dt>
|
||||
<dd>{{ a.disbursementDate }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Interest Rate</dt>
|
||||
<dd>{{ a.interestRate }}%</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Outstanding Balance</dt>
|
||||
<dd>₹{{ a.outstandingBalance | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Overdue Amount</dt>
|
||||
<dd>₹{{ a.overdueAmount | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Overdue Days</dt>
|
||||
<dd>{{ a.overdueDays }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>NPA Since</dt>
|
||||
<dd>{{ npaSince(a) }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Restructured</dt>
|
||||
<dd>{{ a.isRestructured ? 'Yes' : 'No' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Last Payment Date</dt>
|
||||
<dd>{{ a.lastPaymentDate }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Collateral Value</dt>
|
||||
<dd>₹{{ a.collateralValue | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Provisioning Rate</dt>
|
||||
<dd>{{ provisionRate(a) }}%</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Provision Required</dt>
|
||||
<dd>₹{{ provisionReq(a) | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Provision Held</dt>
|
||||
<dd>₹{{ provisionHeldAmt(a) | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Interest Reversed</dt>
|
||||
<dd>₹{{ interestRev(a) | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Total Recovered</dt>
|
||||
<dd>₹{{ recovered(a) | number: '1.0-0' }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Movement (vs Last Quarter)</dt>
|
||||
<dd>{{ movement(a) }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>NPA Reason</dt>
|
||||
<dd>{{ a.npaReason }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Recovery Officer</dt>
|
||||
<dd>{{ a.recoveryOfficer }}</dd>
|
||||
</div>
|
||||
<div class="npa-detail-field">
|
||||
<dt>Legal Action Status</dt>
|
||||
<dd [class]="legalActionClass(a.legalActionStatus)">{{ legalActionLabel(a.legalActionStatus) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@if (isNpaStageOf(a)) {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Recovery Action — Update Legal Action Status</div>
|
||||
<div class="inv-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="a.legalActionStatus === 'NOTICE_ISSUED'"
|
||||
(click)="updateLegalAction(a, 'NOTICE_ISSUED')"
|
||||
>
|
||||
Issue Demand Notice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="a.legalActionStatus === 'SARFAESI_INITIATED'"
|
||||
(click)="updateLegalAction(a, 'SARFAESI_INITIATED')"
|
||||
>
|
||||
Initiate SARFAESI Action
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="a.legalActionStatus === 'SUIT_FILED'"
|
||||
(click)="updateLegalAction(a, 'SUIT_FILED')"
|
||||
>
|
||||
File Recovery Suit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="a.legalActionStatus === 'OTS_NEGOTIATION'"
|
||||
(click)="updateLegalAction(a, 'OTS_NEGOTIATION')"
|
||||
>
|
||||
Mark OTS Negotiation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="a.legalActionStatus === 'NONE'"
|
||||
(click)="updateLegalAction(a, 'NONE')"
|
||||
>
|
||||
Reset to No Action
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Recovery History</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Mode</th>
|
||||
<th class="num col-in">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of a.recoveries; track r.date + r.amount) {
|
||||
<tr>
|
||||
<td>{{ r.date }}</td>
|
||||
<td>{{ r.mode }}</td>
|
||||
<td class="num col-in">₹{{ r.amount | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="3" class="pg-empty">No recoveries recorded against this account.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ===================== 06 NPA MOVEMENT REPORT ===================== -->
|
||||
@if (activeReport === 'movement') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Classification Movement — Current Quarter vs Previous Quarter</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Previous Stage</th>
|
||||
<th>Current Stage</th>
|
||||
<th>Movement</th>
|
||||
<th class="num">Outstanding</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of paged(movedAccounts); track a.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ a.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ a.customerName }}</td>
|
||||
<td>{{ a.branchName }}</td>
|
||||
<td>
|
||||
<span [class]="badgeClass(prevStage(a))">{{ stageLabel(prevStage(a)) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
|
||||
</td>
|
||||
<td>{{ movement(a) }}</td>
|
||||
<td class="num">₹{{ a.outstandingBalance | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="pg-empty">No classification movement in this branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@if (movedAccounts.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(movedAccounts) }}–{{ rangeEnd(movedAccounts) }} of {{ movedAccounts.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, movedAccounts)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(movedAccounts) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(movedAccounts)"
|
||||
(click)="goToPage(currentPage + 1, movedAccounts)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== 07 NPA WITH PROVISION AND REQUIRED PROVISION ===================== -->
|
||||
@if (activeReport === 'provision') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Provision Held vs Required — Full Loan Book</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Stage</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Provision Required</th>
|
||||
<th class="num">Provision Held</th>
|
||||
<th class="num">Shortfall / (Excess)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of paged(provisionRows); track a.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ a.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ a.customerName }}</td>
|
||||
<td>{{ a.branchName }}</td>
|
||||
<td>
|
||||
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
|
||||
</td>
|
||||
<td class="num">₹{{ a.outstandingBalance | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ provisionRate(a) }}%</td>
|
||||
<td class="num">₹{{ provisionReq(a) | number: '1.0-0' }}</td>
|
||||
<td class="num">₹{{ provisionHeldAmt(a) | number: '1.0-0' }}</td>
|
||||
<td class="num" [class.col-out]="shortfall(a) > 0" [class.col-in]="shortfall(a) < 0">
|
||||
₹{{ shortfall(a) | number: '1.0-0' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="6">Total ({{ provisionRows.length }} accounts)</td>
|
||||
<td class="num">₹{{ provisionRequiredTotal | number: '1.0-0' }}</td>
|
||||
<td class="num">₹{{ provisionHeldTotal | number: '1.0-0' }}</td>
|
||||
<td class="num">₹{{ provisionShortfallTotal | number: '1.0-0' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@if (provisionRows.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(provisionRows) }}–{{ rangeEnd(provisionRows) }} of {{ provisionRows.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, provisionRows)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(provisionRows) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(provisionRows)"
|
||||
(click)="goToPage(currentPage + 1, provisionRows)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== 08 PROBABLE NPA ===================== -->
|
||||
@if (activeReport === 'probable') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">
|
||||
Probable NPA — SMA-2 Accounts Likely to Slip Next Quarter (61–90 Days Overdue)
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Product</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num">Overdue Amount</th>
|
||||
<th class="num">Overdue Days</th>
|
||||
<th>Last Payment</th>
|
||||
<th>Follow-up</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of paged(probableNpaAccounts); track a.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ a.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ a.customerName }}</td>
|
||||
<td>{{ a.branchName }}</td>
|
||||
<td>{{ a.productName }}</td>
|
||||
<td class="num">₹{{ a.outstandingBalance | number: '1.0-0' }}</td>
|
||||
<td class="num col-out">₹{{ a.overdueAmount | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ a.overdueDays }}</td>
|
||||
<td>{{ a.lastPaymentDate }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="npa-badge"
|
||||
[class.npa-badge--sma2]="a.overdueDays > 75"
|
||||
[class.npa-badge--sma0]="a.overdueDays <= 75"
|
||||
>
|
||||
{{ a.overdueDays > 75 ? 'Urgent' : 'Monitor' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="9" class="pg-empty">No SMA-2 accounts in this branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="4">Total ({{ probableNpaAccounts.length }} accounts)</td>
|
||||
<td class="num">₹{{ sumOutstanding(probableNpaAccounts) | number: '1.0-0' }}</td>
|
||||
<td colspan="4"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@if (probableNpaAccounts.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(probableNpaAccounts) }}–{{ rangeEnd(probableNpaAccounts) }} of
|
||||
{{ probableNpaAccounts.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, probableNpaAccounts)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(probableNpaAccounts) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(probableNpaAccounts)"
|
||||
(click)="goToPage(currentPage + 1, probableNpaAccounts)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ===================== 09 SMA REPORT ===================== -->
|
||||
@if (activeReport === 'sma') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Special Mention Accounts — SMA-0 (1–30d) / SMA-1 (31–60d) / SMA-2 (61–90d)</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Customer</th>
|
||||
<th>Branch</th>
|
||||
<th>Product</th>
|
||||
<th>Stage</th>
|
||||
<th class="num">Outstanding</th>
|
||||
<th class="num">Overdue Amount</th>
|
||||
<th class="num">Overdue Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of paged(smaAccounts); track a.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ a.accountNo }}</span>
|
||||
</td>
|
||||
<td>{{ a.customerName }}</td>
|
||||
<td>{{ a.branchName }}</td>
|
||||
<td>{{ a.productName }}</td>
|
||||
<td>
|
||||
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
|
||||
</td>
|
||||
<td class="num">₹{{ a.outstandingBalance | number: '1.0-0' }}</td>
|
||||
<td class="num col-out">₹{{ a.overdueAmount | number: '1.0-0' }}</td>
|
||||
<td class="num">{{ a.overdueDays }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="8" class="pg-empty">No SMA accounts in this branch.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td colspan="5">Total ({{ smaAccounts.length }} accounts)</td>
|
||||
<td class="num">₹{{ sumOutstanding(smaAccounts) | number: '1.0-0' }}</td>
|
||||
<td class="num">₹{{ sumOverdue(smaAccounts) | number: '1.0-0' }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@if (smaAccounts.length > pageSize) {
|
||||
<div class="pg-pagination">
|
||||
<span class="pg-pagination__info"
|
||||
>Showing {{ rangeStart(smaAccounts) }}–{{ rangeEnd(smaAccounts) }} of {{ smaAccounts.length }}</span
|
||||
>
|
||||
<div class="pg-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === 1"
|
||||
(click)="goToPage(currentPage - 1, smaAccounts)"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(smaAccounts) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[disabled]="currentPage === pageCount(smaAccounts)"
|
||||
(click)="goToPage(currentPage + 1, smaAccounts)"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* 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, inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||
import {
|
||||
LOAN_ACCOUNTS,
|
||||
BRANCHES,
|
||||
REPORT_META,
|
||||
GROUP_LABELS,
|
||||
DEFAULT_REPORT_ID,
|
||||
LoanAccount,
|
||||
NpaStage,
|
||||
ReportGroupId,
|
||||
STAGE_ORDER,
|
||||
STAGE_LABELS,
|
||||
currentStage,
|
||||
previousStage,
|
||||
isNpaStage,
|
||||
isSmaStage,
|
||||
provisionRatePct,
|
||||
provisionRequired,
|
||||
provisionHeld,
|
||||
interestReversed,
|
||||
totalRecovered,
|
||||
movementLabel,
|
||||
stageBadgeClass,
|
||||
npaSinceDate,
|
||||
todayDDMMYYYY,
|
||||
LegalActionStatus,
|
||||
LEGAL_ACTION_LABELS
|
||||
} from '../npa-data';
|
||||
|
||||
interface StageSummaryRow {
|
||||
stage: NpaStage;
|
||||
label: string;
|
||||
count: number;
|
||||
outstanding: number;
|
||||
provisionReq: number;
|
||||
}
|
||||
|
||||
interface BranchSummaryRow {
|
||||
branchCode: string;
|
||||
branchName: string;
|
||||
totalAccounts: number;
|
||||
totalAdvances: number;
|
||||
npaCount: number;
|
||||
npaOutstanding: number;
|
||||
provisionReq: number;
|
||||
gnpaRatio: number;
|
||||
}
|
||||
|
||||
interface RecoveryRow {
|
||||
accountNo: string;
|
||||
customerName: string;
|
||||
branchName: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy "NPA" mega-menu — 9 screens across NPA Reports / NPA Analysis / NPA Classification —
|
||||
* collapsed into one route-param-driven component so every menu item lands on its own URL
|
||||
* (/npa/reports/:reportId) with nothing left unwired. Every figure is derived from the single
|
||||
* LOAN_ACCOUNTS book in npa-data.ts, so a count on one report always agrees with another.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-npa-reports',
|
||||
templateUrl: './npa-reports.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../../investment/investment-shared.scss',
|
||||
'../npa-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class NpaReportsComponent implements OnInit, OnDestroy {
|
||||
readonly reportMeta = REPORT_META;
|
||||
readonly groupLabels = GROUP_LABELS;
|
||||
readonly branches = BRANCHES;
|
||||
readonly stageOrder = STAGE_ORDER;
|
||||
readonly stageLabels = STAGE_LABELS;
|
||||
asOnDate = todayDDMMYYYY();
|
||||
|
||||
activeGroup: ReportGroupId = 'reports';
|
||||
activeReport = DEFAULT_REPORT_ID;
|
||||
|
||||
/** True for a Head Office login — sees a branch selector + the consolidated "All Branches"
|
||||
* view with a branch-wise break-up. A branch-level login is locked to its own branch, the
|
||||
* same split used by the Cash/Clearing/Transfer Summary report. */
|
||||
isHeadOffice = false;
|
||||
/** The logged-in user's own branch/office name, from real session credentials. */
|
||||
currentOfficeName = 'Branch Office';
|
||||
/** Set when a branch-level login's officeName matches one of the demo branches below. */
|
||||
private ownBranchCode: string | null = null;
|
||||
|
||||
/** '' = All Branches (Head Office consolidated view); otherwise a BranchMaster.code. */
|
||||
branchFilter = '';
|
||||
|
||||
/** Pagination — shared across whichever report is currently on screen, since only one
|
||||
* report's table is ever visible at a time. Reset to page 1 whenever the underlying list
|
||||
* can change size (report switch, branch switch, Generate). */
|
||||
readonly pageSize = 20;
|
||||
currentPage = 1;
|
||||
|
||||
selectedAccountNo = '';
|
||||
|
||||
newRecovery = {
|
||||
accountNo: '',
|
||||
date: this.asOnDate,
|
||||
amount: null as number | null,
|
||||
mode: 'Cash Deposit'
|
||||
};
|
||||
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
private sub?: Subscription;
|
||||
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly auth = inject(AuthenticationService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sub = this.route.paramMap.subscribe((params) => {
|
||||
const id = params.get('reportId') || DEFAULT_REPORT_ID;
|
||||
const meta = this.reportMeta.find((m) => m.id === id) || this.reportMeta[3];
|
||||
this.activeReport = meta.id;
|
||||
this.activeGroup = meta.group;
|
||||
this.currentPage = 1;
|
||||
});
|
||||
|
||||
const creds = this.auth.getCredentials();
|
||||
this.isHeadOffice = this.auth.isHeadOfficeUser();
|
||||
this.currentOfficeName = creds?.officeName || 'Branch Office';
|
||||
if (this.isHeadOffice) {
|
||||
// Head Office starts on the consolidated view and can drill into any branch.
|
||||
this.branchFilter = '';
|
||||
} else {
|
||||
// Match the logged-in office's real name to one of the demo branches, so each branch's
|
||||
// login sees only its own book. Falls back to the first demo branch (still labelled with
|
||||
// the user's real office name) if no branch name matches.
|
||||
this.ownBranchCode =
|
||||
this.branches.find((b) => this.currentOfficeName.toLowerCase().includes(b.name.toLowerCase()))?.code || null;
|
||||
this.branchFilter = this.ownBranchCode || this.branches[0].code;
|
||||
}
|
||||
|
||||
const firstNpa = this.npaAccounts[0];
|
||||
this.selectedAccountNo = firstNpa ? firstNpa.accountNo : this.allAccounts[0]?.accountNo || '';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub?.unsubscribe();
|
||||
}
|
||||
|
||||
/** Name of the branch/scope currently being viewed, for display in the header. */
|
||||
get scopeName(): string {
|
||||
if (!this.isHeadOffice) {
|
||||
return this.currentOfficeName;
|
||||
}
|
||||
return this.branchFilter
|
||||
? this.branches.find((b) => b.code === this.branchFilter)?.name || ''
|
||||
: 'All Branches (Consolidated)';
|
||||
}
|
||||
|
||||
setBranch(code: string): void {
|
||||
this.branchFilter = code;
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
// -------------------- Pagination (shared by every report's table) --------------------
|
||||
paged<T>(items: T[]): T[] {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
return items.slice(start, start + this.pageSize);
|
||||
}
|
||||
|
||||
pageCount(items: unknown[]): number {
|
||||
return Math.max(1, Math.ceil(items.length / this.pageSize));
|
||||
}
|
||||
|
||||
rangeStart(items: unknown[]): number {
|
||||
return items.length === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1;
|
||||
}
|
||||
|
||||
rangeEnd(items: unknown[]): number {
|
||||
return Math.min(this.currentPage * this.pageSize, items.length);
|
||||
}
|
||||
|
||||
goToPage(page: number, items: unknown[]): void {
|
||||
this.currentPage = Math.min(Math.max(1, page), this.pageCount(items));
|
||||
}
|
||||
|
||||
goToReport(reportId: string): void {
|
||||
this.router.navigate([
|
||||
'/npa/reports',
|
||||
reportId
|
||||
]);
|
||||
}
|
||||
|
||||
groupOf(group: string) {
|
||||
return this.reportMeta.filter((m) => m.group === group);
|
||||
}
|
||||
|
||||
reportLabel(id: string): string {
|
||||
return this.reportMeta.find((m) => m.id === id)?.label || id;
|
||||
}
|
||||
|
||||
stage(a: LoanAccount): NpaStage {
|
||||
return currentStage(a);
|
||||
}
|
||||
|
||||
prevStage(a: LoanAccount): NpaStage {
|
||||
return previousStage(a);
|
||||
}
|
||||
|
||||
stageLabel(stage: NpaStage): string {
|
||||
return STAGE_LABELS[stage];
|
||||
}
|
||||
|
||||
badgeClass(stage: NpaStage): string {
|
||||
return stageBadgeClass(stage);
|
||||
}
|
||||
|
||||
provisionRate(a: LoanAccount): number {
|
||||
return provisionRatePct(this.stage(a), a.secured);
|
||||
}
|
||||
|
||||
provisionReq(a: LoanAccount): number {
|
||||
return provisionRequired(a);
|
||||
}
|
||||
|
||||
provisionHeldAmt(a: LoanAccount): number {
|
||||
return provisionHeld(a);
|
||||
}
|
||||
|
||||
interestRev(a: LoanAccount): number {
|
||||
return interestReversed(a);
|
||||
}
|
||||
|
||||
recovered(a: LoanAccount): number {
|
||||
return totalRecovered(a);
|
||||
}
|
||||
|
||||
movement(a: LoanAccount): string {
|
||||
return movementLabel(this.prevStage(a), this.stage(a));
|
||||
}
|
||||
|
||||
npaSince(a: LoanAccount): string {
|
||||
return npaSinceDate(a);
|
||||
}
|
||||
|
||||
legalActionLabel(status: LegalActionStatus): string {
|
||||
return LEGAL_ACTION_LABELS[status];
|
||||
}
|
||||
|
||||
legalActionClass(status: LegalActionStatus): string {
|
||||
return status === 'SARFAESI_INITIATED' || status === 'SUIT_FILED' ? 'col-out' : '';
|
||||
}
|
||||
|
||||
updateLegalAction(a: LoanAccount, status: LegalActionStatus): void {
|
||||
a.legalActionStatus = status;
|
||||
this.toast(`${this.legalActionLabel(status)} — recorded for ${a.accountNo}.`);
|
||||
}
|
||||
|
||||
isNpaStageOf(a: LoanAccount): boolean {
|
||||
return isNpaStage(this.stage(a));
|
||||
}
|
||||
|
||||
get allAccounts(): LoanAccount[] {
|
||||
return this.branchFilter ? LOAN_ACCOUNTS.filter((a) => a.branchCode === this.branchFilter) : LOAN_ACCOUNTS;
|
||||
}
|
||||
|
||||
get npaAccounts(): LoanAccount[] {
|
||||
return this.allAccounts.filter((a) => isNpaStage(this.stage(a)));
|
||||
}
|
||||
|
||||
get smaAccounts(): LoanAccount[] {
|
||||
return this.allAccounts.filter((a) => isSmaStage(this.stage(a)));
|
||||
}
|
||||
|
||||
get probableNpaAccounts(): LoanAccount[] {
|
||||
return this.allAccounts.filter((a) => this.stage(a) === 'SMA2').sort((a, b) => b.overdueDays - a.overdueDays);
|
||||
}
|
||||
|
||||
get movedAccounts(): LoanAccount[] {
|
||||
return this.allAccounts
|
||||
.filter((a) => this.prevStage(a) !== this.stage(a))
|
||||
.sort((a, b) => STAGE_ORDER.indexOf(this.stage(b)) - STAGE_ORDER.indexOf(this.stage(a)));
|
||||
}
|
||||
|
||||
get recoveryAccounts(): LoanAccount[] {
|
||||
return this.npaAccounts.filter((a) => a.recoveries.length > 0);
|
||||
}
|
||||
|
||||
get recoveryOptions(): LoanAccount[] {
|
||||
return this.npaAccounts;
|
||||
}
|
||||
|
||||
get recoveryRows(): RecoveryRow[] {
|
||||
return this.recoveryAccounts
|
||||
.flatMap((a) =>
|
||||
a.recoveries.map((r) => ({
|
||||
accountNo: a.accountNo,
|
||||
customerName: a.customerName,
|
||||
branchName: a.branchName,
|
||||
date: r.date,
|
||||
amount: r.amount,
|
||||
mode: r.mode
|
||||
}))
|
||||
)
|
||||
.sort((a, b) => b.amount - a.amount);
|
||||
}
|
||||
|
||||
// -------------------- Portfolio-wide KPIs (shown on every screen) --------------------
|
||||
get totalAdvances(): number {
|
||||
return this.allAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
}
|
||||
|
||||
get gnpaAmount(): number {
|
||||
return this.npaAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
}
|
||||
|
||||
get gnpaRatio(): number {
|
||||
return this.totalAdvances ? (this.gnpaAmount / this.totalAdvances) * 100 : 0;
|
||||
}
|
||||
|
||||
get totalNpaProvisionHeld(): number {
|
||||
return this.npaAccounts.reduce((s, a) => s + this.provisionHeldAmt(a), 0);
|
||||
}
|
||||
|
||||
get netNpaAmount(): number {
|
||||
return Math.max(0, this.gnpaAmount - this.totalNpaProvisionHeld);
|
||||
}
|
||||
|
||||
get nnpaRatio(): number {
|
||||
return this.totalAdvances ? (this.netNpaAmount / this.totalAdvances) * 100 : 0;
|
||||
}
|
||||
|
||||
get totalRecoveredAmount(): number {
|
||||
return this.npaAccounts.reduce((s, a) => s + this.recovered(a), 0);
|
||||
}
|
||||
|
||||
get restructuredAccounts(): LoanAccount[] {
|
||||
return this.allAccounts.filter((a) => a.isRestructured);
|
||||
}
|
||||
|
||||
get restructuredOutstanding(): number {
|
||||
return this.restructuredAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
}
|
||||
|
||||
// -------------------- NPA Summary Report: stage-wise + branch-wise cuts --------------------
|
||||
get stageSummary(): StageSummaryRow[] {
|
||||
return STAGE_ORDER.map((stage) => {
|
||||
const accts = this.allAccounts.filter((a) => this.stage(a) === stage);
|
||||
return {
|
||||
stage,
|
||||
label: STAGE_LABELS[stage],
|
||||
count: accts.length,
|
||||
outstanding: accts.reduce((s, a) => s + a.outstandingBalance, 0),
|
||||
provisionReq: accts.reduce((s, a) => s + this.provisionReq(a), 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get branchSummary(): BranchSummaryRow[] {
|
||||
return this.branches.map((b) => {
|
||||
const all = LOAN_ACCOUNTS.filter((a) => a.branchCode === b.code);
|
||||
const npa = all.filter((a) => isNpaStage(this.stage(a)));
|
||||
const totalAdvances = all.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
const npaOutstanding = npa.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
return {
|
||||
branchCode: b.code,
|
||||
branchName: b.name,
|
||||
totalAccounts: all.length,
|
||||
totalAdvances,
|
||||
npaCount: npa.length,
|
||||
npaOutstanding,
|
||||
provisionReq: npa.reduce((s, a) => s + this.provisionReq(a), 0),
|
||||
gnpaRatio: totalAdvances ? (npaOutstanding / totalAdvances) * 100 : 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get grandTotalAccounts(): number {
|
||||
return this.allAccounts.length;
|
||||
}
|
||||
|
||||
get grandTotalOutstanding(): number {
|
||||
return this.allAccounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
}
|
||||
|
||||
get grandTotalProvision(): number {
|
||||
return this.allAccounts.reduce((s, a) => s + this.provisionReq(a), 0);
|
||||
}
|
||||
|
||||
get bankWideTotalAccounts(): number {
|
||||
return LOAN_ACCOUNTS.length;
|
||||
}
|
||||
|
||||
// -------------------- NPA with Provision and Required Provision --------------------
|
||||
get provisionRows(): LoanAccount[] {
|
||||
return [...this.allAccounts].sort((a, b) => this.provisionReq(b) - this.provisionReq(a));
|
||||
}
|
||||
|
||||
get provisionRequiredTotal(): number {
|
||||
return this.allAccounts.reduce((s, a) => s + this.provisionReq(a), 0);
|
||||
}
|
||||
|
||||
get provisionHeldTotal(): number {
|
||||
return this.allAccounts.reduce((s, a) => s + this.provisionHeldAmt(a), 0);
|
||||
}
|
||||
|
||||
get provisionShortfallTotal(): number {
|
||||
return this.provisionRequiredTotal - this.provisionHeldTotal;
|
||||
}
|
||||
|
||||
shortfall(a: LoanAccount): number {
|
||||
return this.provisionReq(a) - this.provisionHeldAmt(a);
|
||||
}
|
||||
|
||||
// -------------------- NPA Account Detail Report --------------------
|
||||
get selectedAccount(): LoanAccount | undefined {
|
||||
return LOAN_ACCOUNTS.find((a) => a.accountNo === this.selectedAccountNo);
|
||||
}
|
||||
|
||||
searchAccount(): void {
|
||||
const a = this.selectedAccount;
|
||||
if (!a) {
|
||||
this.toast('No account found with that number.');
|
||||
return;
|
||||
}
|
||||
this.toast(`Loaded full NPA details for ${a.accountNo} — ${a.customerName}.`);
|
||||
}
|
||||
|
||||
// -------------------- NPA Recovery Report: add recovery entry --------------------
|
||||
addRecovery(): void {
|
||||
const acc = LOAN_ACCOUNTS.find((a) => a.accountNo === this.newRecovery.accountNo);
|
||||
if (!acc || !this.newRecovery.amount || this.newRecovery.amount <= 0) {
|
||||
this.toast('Select an NPA account and enter a valid recovery amount.');
|
||||
return;
|
||||
}
|
||||
acc.recoveries = [
|
||||
{ date: this.newRecovery.date || this.asOnDate, amount: this.newRecovery.amount, mode: this.newRecovery.mode },
|
||||
...acc.recoveries
|
||||
];
|
||||
this.toast(`Recovery of ₹${this.newRecovery.amount.toLocaleString('en-IN')} recorded against ${acc.accountNo}.`);
|
||||
this.newRecovery = { accountNo: '', date: this.asOnDate, amount: null, mode: 'Cash Deposit' };
|
||||
}
|
||||
|
||||
sumOutstanding(accounts: LoanAccount[]): number {
|
||||
return accounts.reduce((s, a) => s + a.outstandingBalance, 0);
|
||||
}
|
||||
|
||||
sumOverdue(accounts: LoanAccount[]): number {
|
||||
return accounts.reduce((s, a) => s + a.overdueAmount, 0);
|
||||
}
|
||||
|
||||
sumInterest(accounts: LoanAccount[]): number {
|
||||
return accounts.reduce((s, a) => s + this.interestRev(a), 0);
|
||||
}
|
||||
|
||||
generate(): void {
|
||||
this.currentPage = 1;
|
||||
this.toast(`${this.reportLabel(this.activeReport)} generated as on ${this.asOnDate}.`);
|
||||
}
|
||||
|
||||
private toast(msg: string): void {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
if (this.toastTimer) {
|
||||
clearTimeout(this.toastTimer);
|
||||
}
|
||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { NpaReportsComponent } from './npa-reports/npa-reports.component';
|
||||
import { DEFAULT_REPORT_ID } from './npa-data';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
{
|
||||
path: 'npa',
|
||||
data: { title: 'NPA', breadcrumb: 'NPA' },
|
||||
children: [
|
||||
{
|
||||
path: 'reports',
|
||||
redirectTo: `reports/${DEFAULT_REPORT_ID}`,
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'reports/:reportId',
|
||||
component: NpaReportsComponent,
|
||||
data: { title: 'NPA Reports', breadcrumb: 'NPA Reports' }
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class NpaRoutingModule {}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// NPA-specific status colouring layered on top of ../cashier/cashier-shared.scss (pg-*, cctr-*)
|
||||
// and ../investment/investment-shared.scss (inv-kpis, inv-form-*, inv-toast — reused as-is since
|
||||
// they are the same generic "KPI tile / entry form / toast" primitives, not Investment-specific).
|
||||
|
||||
.npa-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
|
||||
&--standard {
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
&--sma0 {
|
||||
background: #fefce8;
|
||||
color: #854d0e;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
&--sma1 {
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
&--sma2 {
|
||||
background: #fff1f0;
|
||||
color: #b91c1c;
|
||||
border: 1px solid #fecdd3;
|
||||
}
|
||||
|
||||
&--substandard {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&--doubtful1 {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
&--doubtful2 {
|
||||
background: #fdf2f8;
|
||||
color: #9d174d;
|
||||
border: 1px solid #fbcfe8;
|
||||
}
|
||||
|
||||
&--doubtful3 {
|
||||
background: #f5f3ff;
|
||||
color: #5b21b6;
|
||||
border: 1px solid #ddd6fe;
|
||||
}
|
||||
|
||||
&--loss {
|
||||
background: #1f2937;
|
||||
color: #f9fafb;
|
||||
border: 1px solid #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
.npa-kpis--ratio {
|
||||
.inv-kpi__value {
|
||||
&.npa-warn {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.npa-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px 20px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.npa-detail-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
dt {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps tables with more columns than comfortably fit — .pg-card clips overflow with
|
||||
// border-radius, so the scroll container has to live one level in, around the <table> only.
|
||||
.pg-table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
// Lets a single field span the whole .inv-form-grid row instead of being squeezed into one
|
||||
// ~220px column — needed for the Account Detail search box, whose "code — name (branch)"
|
||||
// option text is long enough to overflow a narrow column even with ellipsis truncation.
|
||||
.npa-form-field--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.pg-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__page {
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NpaRoutingModule } from './npa-routing.module';
|
||||
import { NpaReportsComponent } from './npa-reports/npa-reports.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
NpaRoutingModule,
|
||||
NpaReportsComponent
|
||||
]
|
||||
})
|
||||
export class NpaModule {}
|
||||
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
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="container aml-rules-container">
|
||||
<mat-card class="aml-card">
|
||||
<mat-card-header class="aml-card-header">
|
||||
<div class="aml-header-content">
|
||||
<mat-icon class="header-icon">shield</mat-icon>
|
||||
<div>
|
||||
<mat-card-title class="header-title">AML Rules Master</mat-card-title>
|
||||
<mat-card-subtitle class="header-subtitle">
|
||||
Database-driven rules for client risk categorization and AML compliance checks
|
||||
</mat-card-subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content class="aml-card-content">
|
||||
<table mat-table [dataSource]="rules" class="aml-table mat-elevation-z1">
|
||||
<!-- Order Position Column -->
|
||||
<ng-container matColumnDef="orderPosition">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">Rule Srno</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-center">{{ element.orderPosition }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Rule Name Column -->
|
||||
<ng-container matColumnDef="ruleName">
|
||||
<th mat-header-cell *matHeaderCellDef>Rule Description</th>
|
||||
<td mat-cell *matCellDef="let element" class="rule-name-cell">{{ element.ruleName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Risk Category Column -->
|
||||
<ng-container matColumnDef="riskCategory">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">Risk Category</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-center">
|
||||
<span class="risk-badge" [ngClass]="getRiskColorClass(element.riskCategory)">
|
||||
<span class="dot"></span>
|
||||
{{ getRiskCategoryLabel(element.riskCategory) }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Reason Column -->
|
||||
<ng-container matColumnDef="reason">
|
||||
<th mat-header-cell *matHeaderCellDef>Rule Reason / Criteria</th>
|
||||
<td mat-cell *matCellDef="let element" class="reason-cell">{{ element.reason }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- AML Check Status Column -->
|
||||
<ng-container matColumnDef="amlCheckStatus">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">AML Check Status</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-center">
|
||||
<span class="compliance-badge" [ngClass]="'status-' + (element.amlCheckStatus || 'clear').toLowerCase()">
|
||||
{{ element.amlCheckStatus || 'CLEAR' }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Banned Entity Match Column -->
|
||||
<ng-container matColumnDef="bannedEntityMatch">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">Banned Match</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-center">
|
||||
<span class="compliance-badge" [ngClass]="'match-' + (element.bannedEntityMatch || 'no').toLowerCase()">
|
||||
{{ element.bannedEntityMatch || 'NO' }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="rule-row"></tr>
|
||||
</table>
|
||||
|
||||
<!-- Mat Paginator for standard pagination -->
|
||||
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons class="aml-paginator"></mat-paginator>
|
||||
|
||||
<div *ngIf="rules.data.length === 0" class="no-rules-message">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
<span>No AML rules found in the database.</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.aml-rules-container {
|
||||
padding: 24px;
|
||||
background-color: var(--bg-neutral-light, #f8fafc);
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.aml-card {
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 4px 20px -2px rgb(0 0 0 / 5%),
|
||||
0 2px 8px -1px rgb(0 0 0 / 3%);
|
||||
border: 1px solid rgb(226 232 240 / 80%);
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aml-card-header {
|
||||
background: linear-gradient(135deg, #1e293b, #0f172a);
|
||||
color: #fff;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.aml-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.header-icon {
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.aml-card-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.aml-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
th {
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
padding: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.rule-row {
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col-center {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rule-name-cell {
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.reason-cell {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* Badges for Risk Category */
|
||||
.risk-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.risk-high {
|
||||
background-color: #fef2f2;
|
||||
color: #ef4444;
|
||||
border: 1px solid #fee2e2;
|
||||
|
||||
.dot {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&.risk-medium {
|
||||
background-color: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fef3c7;
|
||||
|
||||
.dot {
|
||||
background-color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
&.risk-low {
|
||||
background-color: #f0fdf4;
|
||||
color: #16a34a;
|
||||
border: 1px solid #dcfce7;
|
||||
|
||||
.dot {
|
||||
background-color: #16a34a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Badges for Compliance status */
|
||||
.compliance-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
/* AML Statuses */
|
||||
&.status-clear {
|
||||
background-color: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&.status-flagged {
|
||||
background-color: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
&.status-blocked {
|
||||
background-color: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
/* Banned Matches */
|
||||
&.match-no {
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
&.match-yes {
|
||||
background-color: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
&.match-partial_match {
|
||||
background-color: #ffedd5;
|
||||
color: #c2410c;
|
||||
}
|
||||
}
|
||||
|
||||
.no-rules-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: #64748b;
|
||||
gap: 12px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.aml-paginator {
|
||||
background: #fff;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 { Component, OnInit, inject, ViewChild } from '@angular/core';
|
||||
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../../clients/clients.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-aml-rules',
|
||||
templateUrl: './aml-rules.component.html',
|
||||
styleUrls: ['./aml-rules.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatTableModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatPaginatorModule
|
||||
]
|
||||
})
|
||||
export class AmlRulesComponent implements OnInit {
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
rules = new MatTableDataSource<any>([]);
|
||||
displayedColumns: string[] = [
|
||||
'orderPosition',
|
||||
'ruleName',
|
||||
'riskCategory',
|
||||
'reason',
|
||||
'amlCheckStatus',
|
||||
'bannedEntityMatch'
|
||||
];
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientsService.getClientRiskRules().subscribe({
|
||||
next: (data) => {
|
||||
this.rules.data = data || [];
|
||||
this.rules.paginator = this.paginator;
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
getRiskCategoryLabel(category: number): string {
|
||||
switch (category) {
|
||||
case 1:
|
||||
return 'Low Risk';
|
||||
case 2:
|
||||
return 'Medium Risk';
|
||||
case 3:
|
||||
return 'High Risk';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
getRiskColorClass(category: number): string {
|
||||
switch (category) {
|
||||
case 1:
|
||||
return 'risk-low';
|
||||
case 2:
|
||||
return 'risk-medium';
|
||||
case 3:
|
||||
return 'risk-high';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Route } from '../core/route/route.service';
|
||||
import { RbiCircularsComponent } from './rbi-circulars.component';
|
||||
import { AmlRulesComponent } from './aml-rules/aml-rules.component';
|
||||
|
||||
const routes: Routes = [
|
||||
Route.withShell([
|
||||
@@ -18,6 +19,11 @@ const routes: Routes = [
|
||||
path: 'rbi-circulars',
|
||||
component: RbiCircularsComponent,
|
||||
data: { title: 'RBI Circulars', breadcrumb: 'RBI Circulars' }
|
||||
},
|
||||
{
|
||||
path: 'rbi/aml',
|
||||
component: AmlRulesComponent,
|
||||
data: { title: 'AML Rules Master', breadcrumb: 'AML Rules Master' }
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
+11
-11
@@ -141,11 +141,11 @@ export interface SftInterestDividendRecord {
|
||||
}
|
||||
|
||||
export const SFT_BANK = {
|
||||
name: 'Bharat Urban Bank Ltd',
|
||||
name: 'CBSNexora Bank Ltd',
|
||||
branch: 'Head Office',
|
||||
filerPan: 'AABCB1100M',
|
||||
filerTan: 'MUMR99001E',
|
||||
itdReportingPortalId: 'BHARATURBAN-285BA',
|
||||
itdReportingPortalId: 'CBSNEXORA-285BA',
|
||||
fy: '2025-26',
|
||||
filingDueDate: '31-05-2026',
|
||||
principalOfficer: 'Karthik Kamath',
|
||||
@@ -159,7 +159,7 @@ export const SFT_BANK = {
|
||||
branchStdCode: '022',
|
||||
branchPhone: '24938000',
|
||||
branchMobile: '9892000100',
|
||||
branchEmail: 'compliance@bharaturbanbank.example'
|
||||
branchEmail: 'compliance@cbsnexorabank.example'
|
||||
};
|
||||
|
||||
/** ── SFT-001 / 002 / 004 — Person-wise records ── */
|
||||
@@ -202,7 +202,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
|
||||
cashAmountReceivedFromPerson: 1500000,
|
||||
amountPaidToPerson: 0,
|
||||
relatedAccountNumber: '110001101000838',
|
||||
relatedInstituteName: 'BHARAT URBAN BANK',
|
||||
relatedInstituteName: 'CBSNEXORA BANK',
|
||||
relatedInstituteRefNumber: 'YESB0BHRT',
|
||||
sftCode: 'SFT-001'
|
||||
},
|
||||
@@ -243,7 +243,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
|
||||
cashAmountReceivedFromPerson: 1100000,
|
||||
amountPaidToPerson: 0,
|
||||
relatedAccountNumber: '110001352000357',
|
||||
relatedInstituteName: 'BHARAT URBAN BANK',
|
||||
relatedInstituteName: 'CBSNEXORA BANK',
|
||||
relatedInstituteRefNumber: 'YESB0BHRT',
|
||||
sftCode: 'SFT-001'
|
||||
},
|
||||
@@ -285,7 +285,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
|
||||
cashAmountReceivedFromPerson: 2500000,
|
||||
amountPaidToPerson: 0,
|
||||
relatedAccountNumber: '110001352000420',
|
||||
relatedInstituteName: 'BHARAT URBAN BANK',
|
||||
relatedInstituteName: 'CBSNEXORA BANK',
|
||||
relatedInstituteRefNumber: 'YESB0BHRT',
|
||||
sftCode: 'SFT-002'
|
||||
},
|
||||
@@ -326,7 +326,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
|
||||
cashAmountReceivedFromPerson: 1800000,
|
||||
amountPaidToPerson: 0,
|
||||
relatedAccountNumber: '110001352000511',
|
||||
relatedInstituteName: 'BHARAT URBAN BANK',
|
||||
relatedInstituteName: 'CBSNEXORA BANK',
|
||||
relatedInstituteRefNumber: 'YESB0BHRT',
|
||||
sftCode: 'SFT-002'
|
||||
},
|
||||
@@ -368,7 +368,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
|
||||
cashAmountReceivedFromPerson: 1250000,
|
||||
amountPaidToPerson: 0,
|
||||
relatedAccountNumber: '110001101001145',
|
||||
relatedInstituteName: 'BHARAT URBAN BANK',
|
||||
relatedInstituteName: 'CBSNEXORA BANK',
|
||||
relatedInstituteRefNumber: 'YESB0BHRT',
|
||||
sftCode: 'SFT-004'
|
||||
},
|
||||
@@ -409,7 +409,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
|
||||
cashAmountReceivedFromPerson: 2150000,
|
||||
amountPaidToPerson: 0,
|
||||
relatedAccountNumber: '110001101001458',
|
||||
relatedInstituteName: 'BHARAT URBAN BANK',
|
||||
relatedInstituteName: 'CBSNEXORA BANK',
|
||||
relatedInstituteRefNumber: 'YESB0BHRT',
|
||||
sftCode: 'SFT-004'
|
||||
}
|
||||
@@ -436,7 +436,7 @@ export const SFT_ACCOUNT_RECORDS: SftAccountRecord[] = [
|
||||
branchMobile: '9892000100',
|
||||
branchFaxStd: '022',
|
||||
branchFaxPhone: '24938011',
|
||||
branchEmail: 'compliance@bharaturbanbank.example',
|
||||
branchEmail: 'compliance@cbsnexorabank.example',
|
||||
aggGrossCashCredited: 6500000,
|
||||
aggGrossCashDebited: 4200000,
|
||||
amountCreditedBeforeDemonetization: 0,
|
||||
@@ -485,7 +485,7 @@ export const SFT_ACCOUNT_RECORDS: SftAccountRecord[] = [
|
||||
branchMobile: '9892000100',
|
||||
branchFaxStd: '022',
|
||||
branchFaxPhone: '24938011',
|
||||
branchEmail: 'compliance@bharaturbanbank.example',
|
||||
branchEmail: 'compliance@cbsnexorabank.example',
|
||||
aggGrossCashCredited: 5800000,
|
||||
aggGrossCashDebited: 3450000,
|
||||
amountCreditedBeforeDemonetization: 0,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260731',
|
||||
'hash': 'ca85bae4'
|
||||
'hash': 'fd28788d'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
@@ -1017,6 +1017,22 @@ mat-card {
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Full-Screen Dialog Panel (e.g. FD/RD Entry) — edge-to-edge, no rounded corners ─── */
|
||||
.fdrd-fullscreen-panel {
|
||||
max-width: 100vw !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
|
||||
.mat-mdc-dialog-container {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-surface {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── CIC DQR Dialog Backdrop — ensure outside clicks register ─── */
|
||||
.cic-dlg-backdrop {
|
||||
background: rgb(15 23 42 / 45%);
|
||||
|
||||
Reference in New Issue
Block a user