Compare commits

..
1 Commits
Author SHA1 Message Date
nitesh 8c7d8287f9 Merge branch 'uate' into dev — sync branches to same level
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
2026-08-04 11:35:08 +05:30
88 changed files with 734 additions and 24656 deletions
+2 -2
View File
@@ -69,8 +69,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "15mb",
"maximumError": "17mb"
"maximumWarning": "13mb",
"maximumError": "14mb"
},
{
"type": "anyComponentStyle",
-8
View File
@@ -64,11 +64,7 @@ import { BankCalendarModule } from './bank-calendar/bank-calendar.module';
import { DeafModule } from './deaf/deaf.module';
import { DicgcModule } from './dicgc/dicgc.module';
import { FiuModule } from './fiu/fiu.module';
import { GlReportsModule } from './gl-reports/gl-reports.module';
import { SftModule } from './sft/sft.module';
import { InvestmentModule } from './investment/investment.module';
import { NpaModule } from './npa/npa.module';
import { ReconciliationModule } from './reconciliation/reconciliation.module';
import { DigitalModule } from './digital/digital.module';
import { LetterMgmtModule } from './letter-mgmt/letter-mgmt.module';
import { AccountsModule } from './accounts/accounts.module';
@@ -175,11 +171,7 @@ export function HttpLoaderFactory(http: HttpClient) {
DeafModule,
DicgcModule,
FiuModule,
GlReportsModule,
SftModule,
InvestmentModule,
NpaModule,
ReconciliationModule,
DigitalModule,
LetterMgmtModule,
AccountsModule,
@@ -1,266 +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="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>
@@ -1,73 +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/.
*/
.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;
}
}
-11
View File
@@ -179,17 +179,6 @@ 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|CBSNEXORA 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|BHARAT URBAN 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|CBSNEXORA 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|BHARAT URBAN 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|CBSNEXORA 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|BHARAT URBAN 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||||
@@ -533,8 +533,6 @@ 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
@@ -711,12 +709,6 @@ 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,
@@ -751,7 +743,6 @@ 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;
@@ -963,43 +954,6 @@ 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 });
});
}
/**
@@ -2236,26 +2190,26 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
}
// Match monthly income ranges
if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0 && monthlyIncome < rule.minMonthlyIncome) {
if (rule.minMonthlyIncome != null && monthlyIncome < rule.minMonthlyIncome) {
continue;
}
if (rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0 && monthlyIncome > rule.maxMonthlyIncome) {
if (rule.maxMonthlyIncome != null && monthlyIncome > rule.maxMonthlyIncome) {
continue;
}
// Match annual income ranges (evaluated against totalAnnualIncome)
if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0 && totalAnnualIncome < rule.minAnnualIncome) {
if (rule.minAnnualIncome != null && totalAnnualIncome < rule.minAnnualIncome) {
continue;
}
if (rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0 && totalAnnualIncome > rule.maxAnnualIncome) {
if (rule.maxAnnualIncome != null && totalAnnualIncome > rule.maxAnnualIncome) {
continue;
}
// Match turnover ranges
if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0 && annualTurnover < rule.minAnnualTurnover) {
if (rule.minAnnualTurnover != null && annualTurnover < rule.minAnnualTurnover) {
continue;
}
if (rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0 && annualTurnover > rule.maxAnnualTurnover) {
if (rule.maxAnnualTurnover != null && annualTurnover > rule.maxAnnualTurnover) {
continue;
}
@@ -440,18 +440,6 @@ 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.
*
+53 -246
View File
@@ -362,10 +362,6 @@
<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>
@@ -496,15 +492,6 @@
<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>
@@ -666,7 +653,6 @@
<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
>
@@ -715,11 +701,8 @@
</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; 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('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('DDS Entry')" (click)="subLedgerOpen = false"
><fa-icon icon="coins" class="gl-icon"></fa-icon> DDS Entry</a
@@ -825,32 +808,25 @@
<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')"
routerLink="/gl/reports/trial-balance"
(click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="gl-icon" style="color: #b45309"></fa-icon> Trial Balance</a
<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('Profit and Loss')"
routerLink="/gl/reports/profit-and-loss"
(click)="glMenuOpen = false"
(click)="glMenuOpen = false; openAccountingReportsDialog('PNL')"
><fa-icon icon="chart-line" class="gl-icon" style="color: #b45309"></fa-icon> Profit and Loss</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Consolidate Balance Sheet')"
routerLink="/gl/reports/balance-sheet"
(click)="glMenuOpen = false"
><fa-icon icon="file-alt" class="gl-icon" style="color: #b45309"></fa-icon> Consolidated Balance Sheet</a
(click)="glMenuOpen = false; openAccountingReportsDialog('BALANCE-SHEET')"
><fa-icon icon="file-alt" class="gl-icon" style="color: #b45309"></fa-icon> Consolidate Balance Sheet</a
>
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Comparative Balance Sheet')"
routerLink="/gl/reports/comparative-balance-sheet"
(click)="glMenuOpen = false"
(click)="glMenuOpen = false; openAccountingReportsDialog('COMPARATIVE-BS')"
><fa-icon icon="balance-scale" class="gl-icon" style="color: #b45309"></fa-icon> Comparative Balance
Sheet</a
>
@@ -875,8 +851,7 @@
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('GL Wise DR CR')"
routerLink="/gl/reports/gl-wise-dr-cr"
(click)="glMenuOpen = false"
(click)="glMenuOpen = false; openAccountingReportsDialog('GL-DR-CR')"
><fa-icon icon="project-diagram" class="gl-icon" style="color: #b45309"></fa-icon> GL Wise DR / CR
Report</a
>
@@ -889,13 +864,8 @@
(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 Liabilities')"
routerLink="/gl/reports/contingent-liabilities"
(click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="gl-icon" style="color: #b45309"></fa-icon> Contingent
Liabilities</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('Banker Head Report')" (click)="glMenuOpen = false"
><fa-icon icon="university" class="gl-icon"></fa-icon> Banker Head Report</a
@@ -912,8 +882,7 @@
<a
class="gl-link"
[class.hidden]="!matchesGlSearch('Misc Expense Ledger')"
routerLink="/gl/reports/misc-expense-ledger"
(click)="glMenuOpen = false"
(click)="glMenuOpen = false; openAccountingReportsDialog('MISC-EXP')"
><fa-icon icon="receipt" class="gl-icon" style="color: #b45309"></fa-icon> Misc Expense Ledger</a
>
</div>
@@ -1186,7 +1155,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 routerLink="/investment/bonds" tabindex="0">
<button mat-menu-item 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()">
@@ -1202,13 +1171,13 @@
<!-- Investment -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Investment</div>
<button mat-menu-item routerLink="/investment/dashboard" tabindex="0">
<button mat-menu-item tabindex="0">
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
</button>
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
<button mat-menu-item tabindex="0">
<fa-icon icon="dollar-sign" class="menu-item-icon"></fa-icon> Total Int Received &amp; Receivable Report
</button>
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
<button mat-menu-item 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">
@@ -1239,41 +1208,39 @@
<!-- NPA Reports -->
<div class="deposit-menu-group">
<div class="deposit-group-title">NPA Reports</div>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/interest">
<button mat-menu-item tabindex="0">
<fa-icon icon="chart-line" class="menu-item-icon"></fa-icon> NPA Interest
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/status">
<button mat-menu-item tabindex="0">
<fa-icon icon="file-alt" class="menu-item-icon"></fa-icon> NPA Status Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/recovery">
<button mat-menu-item tabindex="0">
<fa-icon icon="undo" class="menu-item-icon"></fa-icon> NPA Recovery Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/summary">
<button mat-menu-item tabindex="0">
<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" routerLink="/npa/reports/account-detail">
<button mat-menu-item tabindex="0">
<fa-icon icon="list-alt" class="menu-item-icon"></fa-icon> NPA Account Detail Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/movement">
<button mat-menu-item tabindex="0">
<fa-icon icon="exchange" class="menu-item-icon"></fa-icon> NPA Movement Report
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/provision">
<button mat-menu-item tabindex="0">
<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" routerLink="/npa/reports/probable">
<button mat-menu-item tabindex="0">
<fa-icon icon="exclamation-circle" class="menu-item-icon"></fa-icon> Probable NPA
</button>
<button mat-menu-item tabindex="0" routerLink="/npa/reports/sma">
<fa-icon icon="flag" class="menu-item-icon"></fa-icon> SMA Report
</button>
<button mat-menu-item tabindex="0"><fa-icon icon="flag" class="menu-item-icon"></fa-icon> SMA Report</button>
</div>
</div>
</mat-menu>
@@ -1306,27 +1273,27 @@
<!-- Core / Books -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Core &amp; Books</div>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/voucher">
<button mat-menu-item tabindex="0">
<fa-icon icon="receipt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Voucher Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/sundry-dr-cr">
<button mat-menu-item tabindex="0">
<fa-icon icon="random" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Sundry Dr / Cr Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/pnl-impact">
<button mat-menu-item tabindex="0">
<fa-icon icon="chart-line" class="menu-item-icon" style="color: #0d9488"></fa-icon>
P&amp;L Impact Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/gl-reconciliation">
<button mat-menu-item tabindex="0">
<fa-icon icon="book-open" class="menu-item-icon" style="color: #0d9488"></fa-icon>
GL Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/fixed-assets">
<button mat-menu-item tabindex="0">
<fa-icon icon="cube" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Fixed Assets Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/inter-company">
<button mat-menu-item tabindex="0">
<fa-icon icon="sitemap" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Inter-Company Reconciliation
</button>
@@ -1335,19 +1302,19 @@
<!-- Bank & Clearing -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Bank &amp; Clearing</div>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/bank-reco">
<button mat-menu-item tabindex="0">
<fa-icon icon="university" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Bank Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/api-bank-reco">
<button mat-menu-item tabindex="0">
<fa-icon icon="code-branch" class="menu-item-icon" style="color: #0d9488"></fa-icon>
API Based Bank Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/neft-rtgs">
<button mat-menu-item tabindex="0">
<fa-icon icon="exchange" class="menu-item-icon" style="color: #0d9488"></fa-icon>
NEFT / RTGS Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/clearing-reco">
<button mat-menu-item tabindex="0">
<fa-icon icon="hand-holding-usd" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Clearing Reconciliation
</button>
@@ -1356,15 +1323,15 @@
<!-- Digital / Payments -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Digital &amp; Payments</div>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/udir">
<button mat-menu-item tabindex="0">
<fa-icon icon="mobile-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
UDIR (IMPS / UPI / ATM)
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/three-way-digital">
<button mat-menu-item tabindex="0">
<fa-icon icon="project-diagram" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Three-way Digital Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/nach-reco">
<button mat-menu-item tabindex="0">
<fa-icon icon="sync-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
NACH Reconciliation
</button>
@@ -1373,11 +1340,11 @@
<!-- Instruments & Members -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Instruments &amp; Members</div>
<button mat-menu-item [routerLink]="['/sponsor-dd-reconciliation']" tabindex="0" class="menu-item-live">
<button mat-menu-item [routerLink]="['/sponsor-dd-reconciliation']" tabindex="0">
<fa-icon icon="money-check-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
DD / Pay Order Reconciliation
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/share-member">
<button mat-menu-item tabindex="0">
<fa-icon icon="coins" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Share Member Reconciliation
</button>
@@ -1386,11 +1353,11 @@
<!-- Compliance & Financials -->
<div class="deposit-menu-group">
<div class="deposit-group-title">Compliance &amp; Financials</div>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/invoice-payment">
<button mat-menu-item tabindex="0">
<fa-icon icon="file-invoice" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Invoice Payment
</button>
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/tax-reco">
<button mat-menu-item tabindex="0">
<fa-icon icon="percentage" class="menu-item-icon" style="color: #0d9488"></fa-icon>
Tax Reconciliation
</button>
@@ -1710,29 +1677,24 @@
<!-- 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')"
routerLink="/rbi/oss1"
(click)="rbiMenuOpen = false"
>
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 1 XBRL/CIMS
<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>
<a
class="gl-link"
[class.hidden]="!matchesRbiSearch('OSS 2 XBRL')"
routerLink="/rbi/oss2"
(click)="rbiMenuOpen = false"
(click)="rbiMenuOpen = false; openCimsXbrlDialog('OSS-2')"
>
<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')"
routerLink="/rbi/oss3"
(click)="rbiMenuOpen = false"
>
<fa-icon icon="file-alt" class="gl-icon" style="color: #0891b2"></fa-icon> OSS 3 XBRL/CIMS
<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>
<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>
<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>
</div>
<!-- CIMS / XBRL Returns -->
@@ -1863,161 +1825,6 @@
</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 &amp; 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 &amp; 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>
-34
View File
@@ -677,26 +677,6 @@ $gray-700: #334155;
opacity: 0.7;
}
// Marks a `mat-menu-item` as actually wired to a working screen/dialog (vs a still-unwired
// placeholder) — same "green dot" convention as .gl-link--live, positioned absolutely since
// mat-menu-item's internal layout isn't a plain flex row we control directly.
[mat-menu-item].menu-item-live {
position: relative;
&::after {
content: '';
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: #16a34a;
box-shadow: 0 0 0 2px rgb(22 163 74 / 15%);
}
}
// ===== Nav Dropdown Menus =====
::ng-deep .nav-dropdown {
border-radius: 8px !important;
@@ -876,20 +856,6 @@ $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;
+2 -33
View File
@@ -98,10 +98,6 @@ 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. */
@@ -120,11 +116,6 @@ 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[] {
@@ -251,7 +242,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' | 'CONTINGENT-LIAB' | 'GL-DR-CR' | 'MISC-EXP' = 'PNL'
initialReport: 'PNL' | 'BALANCE-SHEET' | 'COMPARATIVE-BS' | 'GL-DR-CR' | 'MISC-EXP' = 'PNL'
): Promise<void> {
const { AccountingReportsDialogComponent } =
await import('../../home/dashboard/accounting-reports-dialog/accounting-reports-dialog.component');
@@ -294,9 +285,7 @@ export class ShellComponent implements OnInit, OnDestroy {
* 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-4' | 'OSS-5' | 'OSS-6' | 'OSS-7' = 'OSS-2'
): Promise<void> {
async openCimsXbrlDialog(initialForm: 'OSS-2' | 'PSA' | 'RAL' | 'RCP-UCB' = 'OSS-2'): Promise<void> {
const { CimsXbrlDialogComponent } =
await import('../../home/dashboard/cims-xbrl-dialog/cims-xbrl-dialog.component');
this.dialog.open(CimsXbrlDialogComponent, {
@@ -312,26 +301,6 @@ 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)="navigateMenu('/cashier/cash-clearing-transfer-summary'); subLedgerOpen = false"
(click)="subLedgerOpen = false"
>
<fa-icon icon="money-bill" class="subledger-icon"></fa-icon> Cash / Clearing / Transfer
</a>
@@ -403,26 +403,17 @@
<div class="subledger-columns">
<div class="subledger-group">
<div class="subledger-group-title">Financial Statements</div>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Trial Balance')"
routerLink="/gl/reports/trial-balance"
(click)="glMenuOpen = false"
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Trial Balance')" (click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="subledger-icon"></fa-icon> Trial Balance</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Profit and Loss')"
routerLink="/gl/reports/profit-and-loss"
(click)="glMenuOpen = false"
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Profit and Loss')" (click)="glMenuOpen = false"
><fa-icon icon="chart-line" class="subledger-icon"></fa-icon> Profit and Loss</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Consolidated Balance Sheet')"
routerLink="/gl/reports/balance-sheet"
[class.hidden]="!matchesGlSearch('Consolidate Balance Sheet')"
(click)="glMenuOpen = false"
><fa-icon icon="file-alt" class="subledger-icon"></fa-icon> Consolidated Balance Sheet</a
><fa-icon icon="file-alt" class="subledger-icon"></fa-icon> Consolidate Balance Sheet</a
>
</div>
<div class="subledger-group">
@@ -457,12 +448,8 @@
(click)="glMenuOpen = false"
><fa-icon icon="users" class="subledger-icon"></fa-icon> Client wise Balance Report</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Contingent Liabilities')"
routerLink="/gl/reports/contingent-liabilities"
(click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="subledger-icon"></fa-icon> Contingent Liabilities</a
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Contingent Expense')" (click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="subledger-icon"></fa-icon> Contingent Expense</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Banker Head Report')" (click)="glMenuOpen = false"
><fa-icon icon="university" class="subledger-icon"></fa-icon> Banker Head Report</a
@@ -365,21 +365,6 @@
</mat-form-field>
</form>
}
<!-- Compliance / AML Action Triage -->
<div class="compliance-triage" style="margin-top: 16px; padding: 12px; background: #fff8f8; border: 1px dashed #f5c2c2; border-radius: 8px;">
<h4 style="margin: 0 0 8px; font-weight: 600; color: #b91c1c; display: flex; align-items: center; gap: 6px;">
Compliance & AML Triage
</h4>
<div style="display: flex; flex-direction: column; gap: 8px;">
<mat-checkbox [(ngModel)]="flagAsSuspicious" [ngModelOptions]="{standalone: true}">
Flag as suspicious (escalate to STR cases under RBI compliance)
</mat-checkbox>
<mat-checkbox [(ngModel)]="markAsWhitelisted" [ngModelOptions]="{standalone: true}">
Mark as whitelisted (prevent future AML alerts for this account)
</mat-checkbox>
</div>
</div>
}
<div #lightboxRoot style="display: none"></div>
@@ -131,90 +131,6 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
/** 'any' = mode of operation lets any one signer sign (Either/Anyone or Survivor); 'all' = every mandatory signer must sign. */
signerSelectionMode: 'any' | 'all' = 'any';
manualOverrideChecked = false;
flagAsSuspicious = false;
markAsWhitelisted = false;
handleComplianceActions(): void {
if (this.flagAsSuspicious) {
const existingStrData = localStorage.getItem('fiu_str_cases');
let cases: any[] = [];
if (existingStrData) {
try {
cases = JSON.parse(existingStrData);
} catch (e) {
cases = [];
}
}
const newRef = 101 + cases.length;
const newCase: any = {
reportType: 'STR',
reportRefNo: newRef,
branchCode: this.item.presentingBranchCode || '1',
accountNumber: this.item.accountNo || this.item.accountId || '110001101000838',
txnBranchCode: this.item.presentingBranchCode || '1',
depositOrWithdrawal: 'Withdrawal',
txnId: this.item.id,
txnDate: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'),
txnTime: new Date().toLocaleTimeString('en-US', { hour12: false }),
txnAmount: this.item.amount,
instrumentType: 'Cheque',
instrumentId: this.item.chequeNumber,
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: this.item.clientName || 'UNKNOWN CLIENT',
detectedOn: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'),
flaggedBy: 'CTS Clearing Officer',
riskCategory: 'unusual-pattern',
narrative: `CTS Cheque #${this.item.chequeNumber} for amount ₹${this.item.amount} flagged as suspicious by Clearing Officer during inward presentment review. Action: ${this.mode}. Reason/Notes: ${this.mode === 'RETURN' ? this.returnForm.value.reason : this.passForm.value.note || 'Manual compliance escalation'}`,
status: 'po-review'
};
cases.unshift(newCase);
localStorage.setItem('fiu_str_cases', JSON.stringify(cases));
const storedCtr = localStorage.getItem('fiu_ctr_txns');
if (storedCtr) {
try {
const ctrList = JSON.parse(storedCtr);
const matching = ctrList.find((t: any) => t.accountNumber === newCase.accountNumber);
if (matching) {
matching.complianceStatus = 'SUSPICIOUS';
localStorage.setItem('fiu_ctr_txns', JSON.stringify(ctrList));
}
} catch (e) {}
}
} else if (this.markAsWhitelisted) {
const existingWhitelist = localStorage.getItem('compliance_whitelisted_accounts');
let whitelisted: string[] = [];
if (existingWhitelist) {
try {
whitelisted = JSON.parse(existingWhitelist);
} catch (e) {
whitelisted = [];
}
}
const account = this.item.accountNo || this.item.accountId;
if (account && !whitelisted.includes(account.toString())) {
whitelisted.push(account.toString());
localStorage.setItem('compliance_whitelisted_accounts', JSON.stringify(whitelisted));
}
const storedCtr = localStorage.getItem('fiu_ctr_txns');
if (storedCtr) {
try {
const ctrList = JSON.parse(storedCtr);
const matching = ctrList.find((t: any) => t.accountNumber === (account ? account.toString() : ''));
if (matching) {
matching.complianceStatus = 'WHITELISTED';
localStorage.setItem('fiu_ctr_txns', JSON.stringify(ctrList));
}
} catch (e) {}
}
}
}
/** Every automatic validation rule InwardClearingValidationService already computes, shown so the clerk sees WHY, not just a decision. */
readonly validationRules: { key: string; label: string; goodWhenTrue: boolean }[] = [
@@ -736,7 +652,6 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
this.ctsService.passInwardItem(this.item.id, payload).subscribe({
next: () => {
this.busy = false;
this.handleComplianceActions();
this.notify('Cheque passed');
this.dialogRef.close({ actioned: true });
},
@@ -761,7 +676,6 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
this.ctsService.returnInwardItem(this.item.id, payload).subscribe({
next: () => {
this.busy = false;
this.handleComplianceActions();
this.notify('Cheque returned');
this.dialogRef.close({ actioned: true });
},
+3 -3
View File
@@ -81,7 +81,7 @@ export interface DeafEligibleAccount {
/** Form I — Dec 2025 cycle */
export const FORM_I: DeafForm = {
bankName: 'CBSNexora Bank Ltd, Mumbai Main Branch',
bankName: 'Bharat Urban 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: 'CBSNexora Bank Ltd, Head Office',
bankName: 'Bharat Urban 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: 'CBSNexora Bank Ltd',
bankName: 'Bharat Urban Bank Ltd',
bankDeafCode: '1100',
currentAcWithRbi: '03010200099001',
sponsorBank: 'State Bank of India',
+1 -1
View File
@@ -53,7 +53,7 @@ export interface SahayDepositor {
}
export const DICGC_BANK = {
name: 'CBSNexora Bank Ltd',
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
reportAsOn: '29-01-2026',
printDate: '29-01-2026 04:19 PM',
+267 -344
View File
@@ -42,12 +42,8 @@ export interface CtrTransaction {
declaration: string;
/** Customer name resolved from KC1 (UI convenience, not in actual TC2 schema) */
customerName?: string;
complianceStatus?: 'PENDING' | 'WHITELISTED' | 'SUSPICIOUS';
}
/** Outcome of the Principal Officer's investigation into an STR case */
export type PoFinding = 'CLEAN' | 'SUSPICIOUS';
/** STR record — workflow extension on top of TC2 */
export interface StrTransaction extends CtrTransaction {
detectedOn: string;
@@ -62,8 +58,6 @@ export interface StrTransaction extends CtrTransaction {
narrative: string;
status: StrStatus;
poRemarks?: string;
/** Set once the Principal Officer concludes the investigation */
poFinding?: PoFinding;
}
/** KC1 — Individual customer card (subset of the 70+ fields, only what UI shows) */
@@ -120,25 +114,20 @@ export interface KycEntity {
}
export const FIU_BANK = {
name: 'CBSNexora Bank Ltd',
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
reBankingFlag: 'SCB',
reportingMonth: 'January 2026',
reBuid: 'REBANK-2031',
principalOfficer: 'Karthik Kamath',
principalOfficerEmail: 'po.fiu@cbsnexorabank.example',
principalOfficerEmail: 'po.fiu@bharaturbanbank.example',
filerUserId: 'PO-001',
cashThreshold: 1000000, // ₹10 L per single cash txn
monthlyAggThreshold: 1000000, // aggregate ≥ ₹10 L in a month
cbwtrThreshold: 500000 // CBWTR threshold ₹5 L
};
/**
* ── CTR transactions (FINnet TC2-style) ──
* Only CASH transactions that breach the RBI/PMLA reporting limit of ₹10,00,000
* (single txn, or connected transactions aggregating to ₹10 L in a calendar month)
* belong here. Nothing below the threshold and no non-cash instruments.
*/
/** ── Sample CTR transactions (FINnet TC2-style) ── */
export const CTR_TXNS: CtrTransaction[] = [
{
reportType: 'CTR',
@@ -146,489 +135,423 @@ export const CTR_TXNS: CtrTransaction[] = [
branchCode: '1',
accountNumber: '110001101000838',
txnBranchCode: '1',
depositOrWithdrawal: 'Deposit',
depositOrWithdrawal: 'Withdrawal',
txnId: 1,
txnDate: '05-01-2026',
txnTime: '10:22:41',
txnAmount: 1450000,
instrumentType: 'Cash',
instrumentId: 'CASH',
txnDate: '20-01-2026',
txnTime: '16:51:28',
txnAmount: 1500000,
instrumentType: 'Cheque',
instrumentId: '24304',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'ANAND KRISHNAN'
customerName: 'RAVI KUMAR SHARMA'
},
{
reportType: 'CTR',
reportRefNo: 1,
branchCode: '1',
accountNumber: '110001101000838',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 2,
txnDate: '27-01-2026',
txnTime: '16:14:22',
txnAmount: 932000,
instrumentType: 'Cheque',
instrumentId: '24305',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'RAVI KUMAR SHARMA'
},
{
reportType: 'CTR',
reportRefNo: 2,
branchCode: '3',
accountNumber: '110001204000712',
txnBranchCode: '3',
depositOrWithdrawal: 'Deposit',
txnId: 2,
txnDate: '09-01-2026',
txnTime: '12:47:09',
txnAmount: 3200000,
instrumentType: 'Cash',
instrumentId: 'CASH',
branchCode: '1',
accountNumber: '110001352000357',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 3,
txnDate: '02-01-2026',
txnTime: '15:05:36',
txnAmount: 600000,
instrumentType: 'Cheque',
instrumentId: '100039',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'MEERA SUNDARAM'
customerName: 'PRIYA PATEL'
},
{
reportType: 'CTR',
reportRefNo: 2,
branchCode: '1',
accountNumber: '110001352000357',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 4,
txnDate: '07-01-2026',
txnTime: '14:14:17',
txnAmount: 700000,
instrumentType: 'Cheque',
instrumentId: '100040',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'PRIYA PATEL'
},
{
reportType: 'CTR',
reportRefNo: 3,
branchCode: '6',
accountNumber: '110001307000455',
txnBranchCode: '6',
depositOrWithdrawal: 'Withdrawal',
txnId: 3,
txnDate: '13-01-2026',
txnTime: '15:03:55',
txnAmount: 1075000,
branchCode: '7',
accountNumber: '110001352000420',
txnBranchCode: '7',
depositOrWithdrawal: 'Deposit',
txnId: 5,
txnDate: '12-01-2026',
txnTime: '11:28:50',
txnAmount: 1200000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'HARPREET SINGH BEDI'
customerName: 'VIKRAM REDDY'
},
{
reportType: 'CTR',
reportRefNo: 4,
branchCode: '2',
accountNumber: '110001408000933',
txnBranchCode: '2',
branchCode: '4',
accountNumber: '110001352000511',
txnBranchCode: '4',
depositOrWithdrawal: 'Deposit',
txnId: 4,
txnDate: '16-01-2026',
txnTime: '11:19:27',
txnAmount: 5600000,
txnId: 6,
txnDate: '18-01-2026',
txnTime: '10:14:05',
txnAmount: 2500000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'FAROOQ ABDULLAH KHAN'
customerName: 'NIRAV SHAH'
},
{
reportType: 'CTR',
reportRefNo: 5,
branchCode: '9',
accountNumber: '110001509000268',
txnBranchCode: '9',
branchCode: '11',
accountNumber: '110001352000604',
txnBranchCode: '11',
depositOrWithdrawal: 'Withdrawal',
txnId: 5,
txnDate: '20-01-2026',
txnTime: '14:35:12',
txnAmount: 2850000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'DEEPIKA RANGANATHAN'
},
{
reportType: 'CTR',
reportRefNo: 6,
branchCode: '5',
accountNumber: '110001610000841',
txnBranchCode: '5',
depositOrWithdrawal: 'Deposit',
txnId: 6,
txnDate: '23-01-2026',
txnTime: '09:58:33',
txnAmount: 1000000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'RAJENDRA PRASAD YADAV'
},
{
reportType: 'CTR',
reportRefNo: 7,
branchCode: '12',
accountNumber: '110001711000527',
txnBranchCode: '12',
depositOrWithdrawal: 'Deposit',
txnId: 7,
txnDate: '27-01-2026',
txnTime: '16:41:08',
txnAmount: 4300000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: 'NCR-014',
txnDate: '24-01-2026',
txnTime: '13:42:11',
txnAmount: 1100000,
instrumentType: 'Cheque',
instrumentId: '88123',
nonCustomerRef: 'NCR-001',
relationshipFlag: 'Non-Customer',
thirdPartyPan: 'BXKPT7788M',
thirdPartyPan: 'AKHPV2244R',
declaration: 'NA',
customerName: 'SHABANA TARIQ MIRZA'
customerName: 'KAVITA VERMA'
}
];
/**
* ── STR cases ──
* Raised either by the AML rules engine or by an officer flagging a CTR row as
* suspicious. Each case is investigated by the Principal Officer, who concludes
* whether the funds are legitimate (CLEAN) or linked to terror financing.
*/
/** ── Sample STR transactions ── */
export const STR_TXNS: StrTransaction[] = [
{
reportType: 'STR',
reportRefNo: 101,
branchCode: '8',
accountNumber: '110001812000390',
txnBranchCode: '8',
branchCode: '1',
accountNumber: '110001101000841',
txnBranchCode: '1',
depositOrWithdrawal: 'Deposit',
txnId: 101,
txnDate: '11-01-2026',
txnTime: '13:08:21',
txnAmount: 2750000,
txnId: 8,
txnDate: '15-01-2026',
txnTime: '11:32:00',
txnAmount: 950000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'IMRAN SHEIKH QURESHI',
detectedOn: '12-01-2026',
flaggedBy: 'AML Engine — Rule R-031',
riskCategory: 'sanctions-hit',
customerName: 'ARJUN DESHMUKH',
detectedOn: '16-01-2026',
flaggedBy: 'AML Engine — Rule R-007',
riskCategory: 'structuring',
narrative:
'Cash deposit of ₹27.5L followed within 48 hours by outward remittances to three beneficiaries whose names partially match the UAPA Schedule IV designated-persons list. Awaiting Principal Officer investigation.',
'Three sub-₹10L cash deposits within 5 days totalling ₹28.4L; pattern indicative of structuring to avoid CTR threshold.',
status: 'po-review'
},
{
reportType: 'STR',
reportRefNo: 102,
branchCode: '4',
accountNumber: '110001915000174',
txnBranchCode: '4',
branchCode: '7',
accountNumber: '110001101000922',
txnBranchCode: '7',
depositOrWithdrawal: 'Deposit',
txnId: 102,
txnDate: '07-01-2026',
txnTime: '10:44:52',
txnAmount: 1980000,
instrumentType: 'Cash',
instrumentId: 'CASH',
txnId: 9,
txnDate: '21-01-2026',
txnTime: '14:18:30',
txnAmount: 4800000,
instrumentType: 'NEFT',
instrumentId: 'N1264821',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'GOPAL VENKATARAMAN',
detectedOn: '08-01-2026',
customerName: 'LAKSHMI IYER',
detectedOn: '22-01-2026',
flaggedBy: 'Branch Compliance Officer',
riskCategory: 'unusual-pattern',
riskCategory: 'dormant-revival',
narrative:
'Declared annual income ₹6L but deposited ₹19.8L cash in a single visit. Customer produced sale deed for ancestral agricultural land.',
status: 'draft',
poFinding: 'CLEAN',
poRemarks:
'Source of funds verified against registered sale deed dated 02-01-2026 and buyer bank statement. Legitimate agricultural land sale — no adverse findings. Case closed, no STR filed with FIU-IND.'
'Account dormant for 18 months suddenly received ₹48L NEFT followed by 4 immediate cash withdrawals of ₹2L each.',
status: 'filed',
poRemarks: 'STR filed with FIU-IND. Acknowledgement no: STR-2026-9821.'
},
{
reportType: 'STR',
reportRefNo: 103,
branchCode: '10',
accountNumber: '110002016000628',
txnBranchCode: '10',
branchCode: '2',
accountNumber: '110001302001012',
txnBranchCode: '2',
depositOrWithdrawal: 'Withdrawal',
txnId: 103,
txnDate: '19-01-2026',
txnTime: '16:27:40',
txnAmount: 3400000,
instrumentType: 'Cash',
instrumentId: 'CASH',
txnId: 10,
txnDate: '23-01-2026',
txnTime: '15:42:18',
txnAmount: 1800000,
instrumentType: 'RTGS',
instrumentId: 'R889013',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'ABDUL RAHMAN BAIG',
detectedOn: '20-01-2026',
flaggedBy: 'AML Engine — Rule R-045',
customerName: 'SAMEER KAPOOR',
detectedOn: '24-01-2026',
flaggedBy: 'AML Engine — Rule R-014',
riskCategory: 'high-risk-country',
narrative:
'Account credited by 14 small inward remittances from a FATF high-risk jurisdiction over 9 days, then emptied via ₹34L cash withdrawal at a border-district branch. Classic terror-funding placement-and-dispersal pattern.',
status: 'filed',
poFinding: 'SUSPICIOUS',
poRemarks:
'Investigation confirmed layered inward remittances from a designated high-risk corridor with no commercial rationale. Beneficial owner could not evidence source. STR filed with FIU-IND under PMLA s.12; acknowledgement STR-2026-10457. Account frozen and reported to the Nodal Officer.'
'RTGS transfer to beneficiary in jurisdiction listed under FATF grey list (Cayman Islands intermediary). KYC documentation incomplete.',
status: 'draft'
},
{
reportType: 'STR',
reportRefNo: 104,
branchCode: '1',
accountNumber: '110002117000315',
txnBranchCode: '1',
branchCode: '13',
accountNumber: '110001352001145',
txnBranchCode: '13',
depositOrWithdrawal: 'Deposit',
txnId: 104,
txnDate: '24-01-2026',
txnTime: '11:52:14',
txnAmount: 2300000,
txnId: 11,
txnDate: '08-01-2026',
txnTime: '12:55:42',
txnAmount: 6500000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'SUNITA CHAUDHARY',
detectedOn: '25-01-2026',
flaggedBy: 'AML Engine — Rule R-018',
riskCategory: 'structuring',
customerName: 'MANMOHAN SINGH GILL',
detectedOn: '09-01-2026',
flaggedBy: 'AML Engine — Rule R-022',
riskCategory: 'unusual-pattern',
narrative:
'Six cash deposits of ₹3.8L each across four branches on the same day, aggregating ₹23L — deliberate splitting to stay under the ₹10L single-transaction CTR limit.',
'Account avg monthly turnover ₹1.5L; received ₹65L cash in single deposit, source-of-funds documentation pending.',
status: 'po-review'
}
];
/** ── KC1 individual customer cards (one per CTR report reference) ── */
/** ── KC1 sample individual customers ── */
export const KC1_INDIVIDUALS: KycIndividual[] = [
{
reportRefNo: 1,
ucic: '30115',
pan: 'AFTPK4471B',
ckycNumber: '60031115447722',
firstName: 'ANAND',
middleName: '',
lastName: 'KRISHNAN',
fatherName: 'SUBRAMANIAN KRISHNAN',
motherName: 'VASANTHA KRISHNAN',
ucic: '10078',
pan: 'AAQPS8560J',
ckycNumber: '60011225033445',
firstName: 'RAVI',
middleName: 'KUMAR',
lastName: 'SHARMA',
fatherName: 'RAJESH KUMAR SHARMA',
motherName: 'SUNITA DEVI',
gender: 'Male',
dob: '14-06-1979',
mobileNumber: '91-9840133015',
email: 'anand.krishnan@example.com',
primaryAddress: 'NO 18 SECOND STREET ADYAR',
state: 'TN',
city: 'CHENNAI',
pincode: '600020',
dob: '10-08-1985',
mobileNumber: '91-9892011001',
email: 'ravi.sharma@example.com',
primaryAddress: 'FLAT 402 SUNRISE TOWERS MAROL ANDHERI EAST',
state: 'MH',
city: 'MUMBAI',
pincode: '400069',
customerType: 'Individual',
annualIncome: 2200000,
occupation: 'Business',
onboardingDate: '22-07-2016',
lastKycDate: '18-06-2025',
annualIncome: 750000,
occupation: 'Service',
onboardingDate: '15-03-2020',
lastKycDate: '12-04-2025',
riskLevel: 'Medium',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX4471'
otherIdentifierNumber: 'XXXXXXXX1234'
},
{
reportRefNo: 2,
ucic: '30228',
pan: 'AJSPS6612D',
ckycNumber: '60032228661133',
firstName: 'MEERA',
ucic: '10079',
pan: 'AKLPS9933P',
ckycNumber: '60022144558899',
firstName: 'PRIYA',
middleName: '',
lastName: 'SUNDARAM',
fatherName: 'RAGHAVAN SUNDARAM',
motherName: 'KALYANI SUNDARAM',
lastName: 'PATEL',
fatherName: 'HARISH PATEL',
motherName: 'MEENA PATEL',
gender: 'Female',
dob: '29-11-1986',
mobileNumber: '91-9886044028',
email: 'meera.sundaram@example.com',
primaryAddress: 'FLAT 9C BRIGADE HERITAGE WHITEFIELD',
state: 'KA',
city: 'BENGALURU',
pincode: '560066',
dob: '22-03-1990',
mobileNumber: '91-9822055002',
email: 'priya.patel@example.com',
primaryAddress: 'B-12 SHIVAJI NAGAR FC ROAD',
state: 'MH',
city: 'PUNE',
pincode: '411005',
customerType: 'Individual',
annualIncome: 3600000,
occupation: 'Business',
onboardingDate: '11-01-2019',
lastKycDate: '05-05-2025',
riskLevel: 'High',
annualIncome: 480000,
occupation: 'Self-Employed',
onboardingDate: '10-09-2023',
lastKycDate: '15-03-2025',
riskLevel: 'Low',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX6612'
otherIdentifierNumber: 'XXXXXXXX2210'
},
{
reportRefNo: 3,
ucic: '30334',
pan: 'AMBPB9027F',
ucic: '20101',
pan: 'AAEPR9352L',
ckycNumber: '',
firstName: 'HARPREET',
middleName: 'SINGH',
lastName: 'BEDI',
fatherName: 'GURMEET SINGH BEDI',
motherName: 'JASWINDER KAUR',
firstName: 'VIKRAM',
middleName: '',
lastName: 'REDDY',
fatherName: 'SURESH REDDY',
motherName: 'LAKSHMI REDDY',
gender: 'Male',
dob: '03-02-1974',
mobileNumber: '91-9815066034',
dob: '07-07-1982',
mobileNumber: '91-9845022004',
email: '',
primaryAddress: 'HOUSE 221 SECTOR 34A CHANDIGARH ROAD',
state: 'PB',
city: 'LUDHIANA',
pincode: '141001',
primaryAddress: 'NO 23 4TH CROSS JAYANAGAR 5TH BLOCK',
state: 'KA',
city: 'BENGALURU',
pincode: '560041',
customerType: 'Individual',
annualIncome: 1450000,
occupation: 'Business',
onboardingDate: '30-08-2012',
lastKycDate: '14-03-2025',
annualIncome: 950000,
occupation: 'Profession',
onboardingDate: '12-11-2021',
lastKycDate: '20-02-2025',
riskLevel: 'Medium',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX9027'
otherIdentifierNumber: 'XXXXXXXX6622'
},
{
reportRefNo: 4,
ucic: '30447',
pan: 'ANQPK3358H',
ckycNumber: '60034447335844',
firstName: 'FAROOQ',
middleName: 'ABDULLAH',
lastName: 'KHAN',
fatherName: 'ISHTIAQ AHMED KHAN',
motherName: 'NASREEN BEGUM',
ucic: '40301',
pan: 'AHKPS5544Z',
ckycNumber: '60040301229988',
firstName: 'NIRAV',
middleName: '',
lastName: 'SHAH',
fatherName: 'JAYESH SHAH',
motherName: 'RITA SHAH',
gender: 'Male',
dob: '17-09-1981',
mobileNumber: '91-9701077047',
email: 'farooq.khan@example.com',
primaryAddress: '12-2-417 MEHDIPATNAM MAIN ROAD',
state: 'TG',
city: 'HYDERABAD',
pincode: '500028',
dob: '18-05-1992',
mobileNumber: '91-9824012010',
email: 'nirav.shah@example.com',
primaryAddress: 'A-401 ATLANTIS APARTMENTS SATELLITE ROAD',
state: 'GJ',
city: 'AHMEDABAD',
pincode: '380015',
customerType: 'Individual',
annualIncome: 890000,
occupation: 'Business',
onboardingDate: '05-04-2020',
lastKycDate: '09-01-2025',
annualIncome: 1800000,
occupation: 'Service',
onboardingDate: '14-02-2018',
lastKycDate: '20-12-2024',
riskLevel: 'High',
reportType: 'CTR',
otherIdentifierName: 'PASSPORT',
otherIdentifierNumber: 'P3358214'
otherIdentifierNumber: 'M5544321'
},
{
reportRefNo: 5,
ucic: '30552',
pan: 'APLPR7783C',
ckycNumber: '60035552778355',
firstName: 'DEEPIKA',
ucic: '50402',
pan: 'AKHPV2244R',
ckycNumber: '',
firstName: 'KAVITA',
middleName: '',
lastName: 'RANGANATHAN',
fatherName: 'MOHAN RANGANATHAN',
motherName: 'SHOBHA RANGANATHAN',
lastName: 'VERMA',
fatherName: 'DEEPAK VERMA',
motherName: 'USHA VERMA',
gender: 'Female',
dob: '08-12-1991',
mobileNumber: '91-9739088052',
email: 'deepika.r@example.com',
primaryAddress: 'VILLA 7 PALM MEADOWS SARJAPUR ROAD',
state: 'KA',
city: 'BENGALURU',
pincode: '560035',
customerType: 'Individual',
annualIncome: 2750000,
occupation: 'Profession',
onboardingDate: '19-06-2021',
lastKycDate: '27-07-2025',
riskLevel: 'Low',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX7783'
},
{
reportRefNo: 6,
ucic: '30669',
pan: 'AQWPY2194K',
ckycNumber: '',
firstName: 'RAJENDRA',
middleName: 'PRASAD',
lastName: 'YADAV',
fatherName: 'BHOLA NATH YADAV',
motherName: 'KAMLA DEVI',
gender: 'Male',
dob: '25-05-1968',
mobileNumber: '91-9451099069',
dob: '01-04-1988',
mobileNumber: '91-9415055014',
email: '',
primaryAddress: '78 GOMTI NAGAR VIBHUTI KHAND',
primaryAddress: '45 CIVIL LINES MALL ROAD',
state: 'UP',
city: 'LUCKNOW',
pincode: '226010',
city: 'KANPUR',
pincode: '208001',
customerType: 'Individual',
annualIncome: 640000,
occupation: 'Agriculture',
onboardingDate: '17-10-2014',
lastKycDate: '22-11-2024',
annualIncome: 540000,
occupation: 'Service',
onboardingDate: '08-09-2022',
lastKycDate: '15-09-2024',
riskLevel: 'Low',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX2194'
},
{
reportRefNo: 7,
ucic: '30774',
pan: 'BXKPT7788M',
ckycNumber: '',
firstName: 'SHABANA',
middleName: 'TARIQ',
lastName: 'MIRZA',
fatherName: 'TARIQ HUSSAIN MIRZA',
motherName: 'RUKHSANA MIRZA',
gender: 'Female',
dob: '11-03-1989',
mobileNumber: '91-9958100074',
email: '',
primaryAddress: '54 ZAKIR NAGAR OKHLA',
state: 'DL',
city: 'NEW DELHI',
pincode: '110025',
customerType: 'Individual',
annualIncome: 720000,
occupation: 'Self-Employed',
onboardingDate: '02-02-2023',
lastKycDate: '16-08-2025',
riskLevel: 'High',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX7788'
otherIdentifierNumber: 'XXXXXXXX2244'
}
];
/** ── KC2 entity customer cards ── */
/** ── KC2 sample entity customers ── */
export const KC2_ENTITIES: KycEntity[] = [
{
reportRefNo: 2,
ucic: 'E30228',
entityName: 'SUNDARAM BULLION TRADERS',
panNumber: 'AAGFS3391Q',
gstin: '29AAGFS3391Q1ZP',
mobileNumber: '91-9886044028',
address: 'SHOP 14 COMMERCIAL STREET SHIVAJI NAGAR',
state: 'KA',
city: 'BENGALURU',
pincode: '560001',
dateOfIncorporation: '11-01-2019',
reportRefNo: 11,
ucic: 'E10101',
entityName: 'BHARAT TRADING COMPANY',
panNumber: 'ABCFB1234N',
gstin: '27ABCFB1234N1ZT',
mobileNumber: '91-9892011010',
address: 'GALA 22 LOWER PAREL',
state: 'MH',
city: 'MUMBAI',
pincode: '400013',
dateOfIncorporation: '24-03-2008',
customerType: 'Sole Proprietor',
riskLevel: 'High',
onboardingDate: '11-01-2019',
lastKycDate: '05-05-2025',
onboardingDate: '24-03-2008',
lastKycDate: '03-09-2025',
isSoleProprietor: true,
ownerUcic: '30228',
ownerName: 'MEERA SUNDARAM',
ownerUcic: '10078',
ownerName: 'RAVI KUMAR SHARMA',
reportType: 'CTR'
},
{
reportRefNo: 4,
ucic: 'E30447',
entityName: 'DECCAN SCRAP & METALS PVT LTD',
panNumber: 'AABCD8842R',
gstin: '36AABCD8842R1ZN',
mobileNumber: '91-9701077047',
address: 'PLOT 61 IDA JEEDIMETLA PHASE II',
state: 'TG',
city: 'HYDERABAD',
pincode: '500055',
dateOfIncorporation: '05-04-2020',
reportRefNo: 12,
ucic: 'E10102',
entityName: 'NEXTGEN TECH PVT LTD',
panNumber: 'AABCN5566K',
gstin: '27AABCN5566K1Z2',
mobileNumber: '91-9824012011',
address: 'TOWER B SUNRISE BUSINESS PARK ANDHERI EAST',
state: 'MH',
city: 'MUMBAI',
pincode: '400093',
dateOfIncorporation: '18-09-2015',
customerType: 'Private Limited',
riskLevel: 'High',
onboardingDate: '05-04-2020',
lastKycDate: '09-01-2025',
riskLevel: 'Medium',
onboardingDate: '18-09-2015',
lastKycDate: '12-08-2025',
isSoleProprietor: false,
ownerUcic: '',
ownerName: '',
+26 -273
View File
@@ -71,19 +71,17 @@
<div class="fiu-kpi fiu-kpi--amber">
<div class="fiu-kpi-ic"><fa-icon icon="exclamation-triangle"></fa-icon></div>
<div>
<div class="fiu-kpi-n">{{ liveStrPending | number }}</div>
<div class="fiu-kpi-n">{{ pendingStr | number }}</div>
<div class="fiu-kpi-l">STR Pending</div>
<div class="fiu-kpi-sub">
{{ liveStrOpen }} to investigate &middot; {{ liveStrAwaitingFiling }} awaiting filing
</div>
<div class="fiu-kpi-sub">draft / PO review</div>
</div>
</div>
<div class="fiu-kpi fiu-kpi--green">
<div class="fiu-kpi-ic"><fa-icon icon="check-circle"></fa-icon></div>
<div>
<div class="fiu-kpi-n">{{ liveStrFiled | number }}</div>
<div class="fiu-kpi-n">{{ filedStr | number }}</div>
<div class="fiu-kpi-l">STR Filed</div>
<div class="fiu-kpi-sub">{{ liveStrCleared }} concluded clear</div>
<div class="fiu-kpi-sub">acknowledged by FIU</div>
</div>
</div>
<div class="fiu-kpi fiu-kpi--purple">
@@ -114,12 +112,7 @@
</div>
<div class="dc-meta">Filing window: <strong>within 7 days of detection</strong></div>
<div class="dc-meta">
To investigate: <strong>{{ liveStrOpen }}</strong> &middot; Awaiting filing:
<strong>{{ liveStrAwaitingFiling }}</strong>
</div>
<div class="dc-meta">
Filed: <strong>{{ liveStrFiled }}</strong> &middot; Concluded clear:
<strong>{{ liveStrCleared }}</strong>
Pending: <strong>{{ pendingStr }}</strong> &middot; Filed: <strong>{{ filedStr }}</strong>
</div>
<button class="dc-btn" (click)="showTab('str')"><fa-icon icon="external-link-alt"></fa-icon> Open</button>
</div>
@@ -136,12 +129,12 @@
<!-- CTR -->
@if (activeTab === 'ctr') {
<div class="fiu-section">
<!-- LIVE: cash-related CTR threshold alerts -->
<!-- LIVE: cash-related AML alerts awaiting AML Officer review -->
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>Live CTR Threshold Alerts</h3>
<h3>Live Cash Alerts</h3>
<p>
Mechanical cash transaction threshold breaches — review and whitelist or escalate to Principal Officer
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()">
@@ -163,13 +156,12 @@
<th class="ta-r">Amount</th>
<th>Raised On</th>
<th>Status</th>
<th class="ta-c">Routes To</th>
<th>Notes</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@for (alert of ctrAlerts; track alert.id) {
@for (alert of amlAlerts; track alert.id) {
<tr>
<td>{{ alert.id }}</td>
<td>{{ alert.clientDisplayName }}</td>
@@ -180,9 +172,6 @@
<td>
<span [class]="amlStatusClass(alert.status)">{{ alert.status }}</span>
</td>
<td class="ta-c">
<span [class]="alertRouteClass(alert.status)">{{ alertRouteLabel(alert.status) }}</span>
</td>
<td>
@if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') {
<input
@@ -206,17 +195,12 @@
</tr>
} @empty {
<tr>
<td colspan="10" class="fiu-empty">No live alerts.</td>
<td colspan="9" class="fiu-empty">No live alerts.</td>
</tr>
}
</tbody>
</table>
</div>
<p class="fiu-tbl-legend">
<span class="report-pill report-pending">Pending</span> awaiting Whitelist/Escalate decision &middot;
<span class="report-pill report-ctr">CTR only</span> whitelisted — cleared, no STR needed &middot;
<span class="report-pill report-str">→ STR</span> escalated to Principal Officer as an STR case
</p>
<!-- LIVE: monthly CTR reports (mechanical, threshold-based) -->
<div class="fiu-form-meta">
@@ -311,8 +295,6 @@
<th>Instrument</th>
<th>Branch</th>
<th class="ta-c">Relationship</th>
<th class="ta-c">Filed Under</th>
<th>Compliance Action</th>
</tr>
</thead>
<tbody>
@@ -345,131 +327,21 @@
t.relationshipFlag
}}</span>
</td>
<td class="ta-c">
<span class="report-pill report-ctr" title="Always included in the monthly CTR / TC2 file">CTR</span>
@if (t.complianceStatus === 'SUSPICIOUS') {
<span class="report-pill report-str" title="Also escalated to an STR case for Principal Officer review">+ STR</span>
}
</td>
<td>
@if (!t.complianceStatus || t.complianceStatus === 'PENDING') {
<div class="fiu-action-group" style="gap: 4px;">
<button class="fiu-btn" style="padding: 4px 8px; font-size: 11px; color: var(--f-green); border-color: var(--f-green); background: var(--f-green-l);" (click)="whitelistCtrTxn(t)">
Whitelist
</button>
<button class="fiu-btn" style="padding: 4px 8px; font-size: 11px; color: var(--f-red); border-color: var(--f-red); background: var(--f-red-l);" (click)="flagCtrTxnSuspicious(t)">
Flag STR
</button>
</div>
} @else {
<span [class]="t.complianceStatus === 'WHITELISTED' ? 'status-pill status-whitelisted' : 'status-pill status-escalated'">
{{ t.complianceStatus }}
</span>
@if (t.complianceStatus === 'WHITELISTED') {
<div class="muted" style="margin-top: 3px;">Stays in CTR only</div>
} @else {
<button class="fiu-link-btn" (click)="openStrCase(t)">View in STR tab &rarr;</button>
}
}
</td>
</tr>
} @empty {
<tr>
<td colspan="11" class="fiu-empty">No transactions match the filters.</td>
<td colspan="9" class="fiu-empty">No transactions match the filters.</td>
</tr>
}
</tbody>
</table>
</div>
<p class="fiu-tbl-legend">
<span class="report-pill report-ctr">CTR</span> every cash txn ≥ threshold stays in the monthly CTR / TC2
file &middot;
<span class="report-pill report-str">+ STR</span> also flagged as suspicious and escalated to a separate STR
case (see the STR tab)
</p>
</div>
}
<!-- STR -->
@if (activeTab === 'str') {
<div class="fiu-section">
<!-- LIVE: suspicious AML alerts awaiting AML Officer review (Structuring, PEP, etc.) -->
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>Live AML Alerts (Awaiting Triage)</h3>
<p>
Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate
</p>
</div>
</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 class="ta-c">Routes To</th>
<th>Notes</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@for (alert of amlSuspiciousAlerts; track alert.id) {
<tr>
<td>{{ alert.id }}</td>
<td>{{ alert.clientDisplayName }}</td>
<td>{{ alert.transactionType }}</td>
<td>{{ alert.ruleCode }} &mdash; {{ alert.ruleDescription }}</td>
<td class="ta-r mono">&#8377;{{ alert.amount | number: '1.0-0' }}</td>
<td>{{ alert.raisedOn | date: 'medium' }}</td>
<td>
<span [class]="amlStatusClass(alert.status)">{{ alert.status }}</span>
</td>
<td class="ta-c">
<span [class]="alertRouteClass(alert.status, false)">{{ alertRouteLabel(alert.status, false) }}</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="10" class="fiu-empty">No suspicious behavioral alerts.</td>
</tr>
}
</tbody>
</table>
</div>
<p class="fiu-tbl-legend">
<span class="report-pill report-pending">Pending</span> awaiting Whitelist/Escalate decision &middot;
<span class="report-pill report-cleared">Cleared</span> whitelisted — no report filed &middot;
<span class="report-pill report-str">→ STR</span> escalated to Principal Officer as an STR case
</p>
<hr style="border: 0; border-top: 1px solid var(--f-g200); margin: 8px 0;" />
<!-- LIVE: STR cases with full Principal Officer actions -->
<div class="fiu-form-meta">
<div class="fiu-form-title">
@@ -535,17 +407,6 @@
<button class="fiu-btn fiu-btn-g" (click)="closeStrCaseDetail()">Close</button>
</div>
<div
class="case-outcome"
[class.case-outcome--clear]="selectedStrCase.status === 'CONCLUDED_CLEAR'"
[class.case-outcome--suspicious]="
selectedStrCase.status === 'CONCLUDED_SUSPICIOUS' || selectedStrCase.status === 'FILED'
"
>
<span [class]="amlStatusClass(selectedStrCase.status)">{{ selectedStrCase.status }}</span>
<span>{{ strCaseOutcome(selectedStrCase) }}</span>
</div>
<div class="case-form" *mifosxHasPermission="'CONCLUDE_AML_STR'">
<div class="form-row">
<label for="fiuInvestigationNotes">Investigation notes (source-of-transaction history reviewed)</label>
@@ -555,96 +416,17 @@
<label for="fiuSourceOfFundsNotes">Source-of-funds notes</label>
<textarea id="fiuSourceOfFundsNotes" rows="2" [(ngModel)]="concludeSourceOfFundsNotes"></textarea>
</div>
<!-- Verification checklist — the verdict is derived, not chosen. -->
<div class="checklist">
<div class="checklist-head">
<span>Principal Officer verification — mark each point as found</span>
<span class="checklist-progress" [class.done]="allAnswered">
{{ answeredCount }} / {{ strChecklist.length }} answered
</span>
</div>
<table class="checklist-tbl">
<thead>
<tr>
<th>Verification point</th>
<th class="ta-c col-yes">Genuine</th>
<th class="ta-c col-no">Not satisfied</th>
</tr>
</thead>
<tbody>
@for (item of strChecklist; track item.key) {
<tr
[class.row-yes]="strFindings[item.key] === true"
[class.row-no]="strFindings[item.key] === false"
[class.row-pending]="strFindings[item.key] === null"
>
<td>
<div class="chk-label">{{ item.label }}</div>
<div class="chk-hint">{{ item.hint }}</div>
</td>
<td class="ta-c">
<button
type="button"
class="chk-box chk-box--yes"
[class.on]="strFindings[item.key] === true"
[attr.aria-pressed]="strFindings[item.key] === true"
[attr.aria-label]="'Mark ' + item.label + ' as genuine'"
(click)="setFinding(item.key, true)"
>
<fa-icon icon="check-circle"></fa-icon>
</button>
</td>
<td class="ta-c">
<button
type="button"
class="chk-box chk-box--no"
[class.on]="strFindings[item.key] === false"
[attr.aria-pressed]="strFindings[item.key] === false"
[attr.aria-label]="'Mark ' + item.label + ' as not satisfied'"
(click)="setFinding(item.key, false)"
>
<fa-icon icon="exclamation-triangle"></fa-icon>
</button>
</td>
</tr>
}
</tbody>
</table>
<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>
<!-- Derived verdict -->
<div
class="verdict"
[class.verdict--clear]="allAnswered && derivedConclusion === 'NOT_SUSPICIOUS'"
[class.verdict--suspicious]="allAnswered && derivedConclusion === 'SUSPICIOUS'"
>
@if (!allAnswered) {
<fa-icon icon="info-circle"></fa-icon>
<span>
Answer every verification point to determine the outcome —
{{ strChecklist.length - answeredCount }} still pending.
</span>
} @else if (derivedConclusion === 'NOT_SUSPICIOUS') {
<fa-icon icon="check-circle"></fa-icon>
<span>
<strong>Not suspicious.</strong> All {{ strChecklist.length }} points found genuine — the case will
be closed and nothing filed with FIU-IND.
</span>
} @else {
<fa-icon icon="exclamation-triangle"></fa-icon>
<span>
<strong>Suspicious — STR must be filed.</strong>
{{ failedFindings.length }} of {{ strChecklist.length }} points not satisfied:
{{ failedFindings[0].label }}@if (failedFindings.length > 1) {
<span> and {{ failedFindings.length - 1 }} more</span>
}.
</span>
}
</div>
<button class="fiu-btn fiu-btn-xml" [disabled]="!allAnswered" (click)="concludeStrCase()">
Conclude as {{ allAnswered ? (derivedConclusion === 'SUSPICIOUS' ? 'Suspicious' : 'Not Suspicious') : '…' }}
</button>
<button class="fiu-btn fiu-btn-xml" (click)="concludeStrCase()">Conclude</button>
@if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') {
<div class="file-str-row">
@@ -696,12 +478,7 @@
<div class="fiu-str-list">
@for (s of filteredStr; track s.reportRefNo) {
<div
class="str-card"
[id]="'str-card-' + s.reportRefNo"
[class.str-card--filed]="s.status === 'filed'"
[class.str-card--highlight]="highlightedStrRef === s.reportRefNo"
>
<div class="str-card" [class.str-card--filed]="s.status === 'filed'">
<div class="str-card-h">
<div class="str-card-id">
<span class="str-ref">STR-{{ s.reportRefNo }}</span>
@@ -744,27 +521,6 @@
<p>{{ s.poRemarks }}</p>
</div>
}
@if (s.poFinding) {
<div class="str-finding" [class.str-finding--suspicious]="s.poFinding === 'SUSPICIOUS'">
<label>
<fa-icon [icon]="s.poFinding === 'CLEAN' ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
Principal Officer Finding
</label>
<p>{{ poFindingLabel(s.poFinding) }}</p>
</div>
} @else {
<div class="str-po-action" *mifosxHasPermission="'CONCLUDE_AML_STR'">
<label>Principal Officer investigation — is the source of funds legitimate?</label>
<div class="fiu-action-group">
<button class="fiu-btn fiu-btn-clean" (click)="concludeLocalStr(s, 'CLEAN')">
Clean — close case
</button>
<button class="fiu-btn fiu-btn-xml" (click)="concludeLocalStr(s, 'SUSPICIOUS')">
Suspicious — file STR with FIU-IND
</button>
</div>
</div>
}
</div>
</div>
} @empty {
@@ -870,13 +626,10 @@
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>FINnet 2.0 Submission Files</h3>
<p>
6 CSV files in FIU-IND prescribed schema, plus STR XML. Counts reflect any filters set on the CTR and
STR tabs, so every file in the bundle covers the same transactions.
</p>
<p>6 CSV files in FIU-IND prescribed schema, plus STR XML</p>
</div>
<button class="fiu-btn fiu-btn-xls" (click)="exportAll()">
<fa-icon icon="file-csv"></fa-icon> Download All 7
<fa-icon icon="file-csv"></fa-icon> Download All
</button>
</div>
@@ -888,7 +641,7 @@
</div>
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>TC2.csv</div>
<p>Branch / cash transactions ({{ filteredCtr.length }} rows)</p>
<p>Branch / cash transactions ({{ ctrTxns.length }} rows)</p>
<button class="fiu-btn fiu-btn-csv" (click)="exportTc2()">
<fa-icon icon="file-csv"></fa-icon> Download
</button>
@@ -923,7 +676,7 @@
</div>
<div class="file-card file-card--xml">
<div class="file-card-h"><fa-icon icon="file-alt"></fa-icon>STR.xml</div>
<p>Suspicious Transaction Report ({{ strSuspects.length }} suspects — cases concluded clean are excluded)</p>
<p>Suspicious Transaction Report ({{ filedStr + pendingStr }} suspects)</p>
<button class="fiu-btn fiu-btn-xml" (click)="exportStrXml()">
<fa-icon icon="file-alt"></fa-icon> Download XML
</button>
-341
View File
@@ -386,100 +386,6 @@
align-self: end;
}
.fiu-link-btn {
display: inline-block;
margin-top: 3px;
padding: 0;
border: none;
background: none;
font-family: inherit;
font-size: 11px;
font-weight: 600;
color: var(--f-red);
cursor: pointer;
text-decoration: underline;
&:hover {
color: #9a2f22;
}
&--muted {
color: var(--f-g500);
&:hover {
color: var(--f-red);
}
}
}
/* PRINCIPAL OFFICER INVESTIGATION */
.str-po-action {
background: var(--f-amber-l);
border: 1px dashed #fcd34d;
border-radius: 8px;
padding: 10px 14px;
display: flex;
flex-direction: column;
gap: 8px;
> label {
font-size: 10px;
font-weight: 700;
color: #92400e;
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
.fiu-btn-clean {
background: var(--f-green);
border-color: var(--f-green);
color: #fff;
&:hover {
background: #166534;
border-color: #166534;
color: #fff;
}
}
.str-finding {
background: var(--f-green-l);
border-radius: 8px;
padding: 10px 14px;
label {
font-size: 10px;
font-weight: 700;
color: var(--f-green);
text-transform: uppercase;
letter-spacing: 0.05em;
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 4px;
}
p {
margin: 0;
font-size: 13px;
color: #14532d;
line-height: 1.5;
}
&--suspicious {
background: var(--f-red-l);
label {
color: var(--f-red);
}
p {
color: #7f1d1d;
}
}
}
/* INPUTS */
.fiu-inp {
width: 100%;
@@ -632,50 +538,6 @@
}
}
.report-pill {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
&.report-ctr {
background: var(--f-blue-l);
color: var(--f-blue);
}
&.report-str {
background: var(--f-red-l);
color: var(--f-red);
margin-left: 4px;
}
&.report-pending {
background: var(--f-amber-l);
color: var(--f-amber);
}
&.report-cleared {
background: var(--f-green-l);
color: var(--f-green);
}
}
.fiu-tbl-legend {
margin: -6px 2px 0;
font-size: 11px;
color: var(--f-g500);
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
.report-pill {
margin-left: 0;
}
}
.rel-pill {
display: inline-block;
padding: 2px 9px;
@@ -835,11 +697,6 @@
&--filed {
border-color: #86efac;
}
&--highlight {
border-color: var(--f-red);
box-shadow: 0 0 0 3px rgb(192 57 43 / 20%);
}
}
.str-card-h {
@@ -1099,29 +956,6 @@
margin-bottom: 16px;
}
.case-outcome {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin: 12px 0;
padding: 10px 14px;
border-radius: 8px;
background: var(--f-g100);
font-size: 13px;
color: var(--f-g700);
&--clear {
background: var(--f-green-l);
color: #14532d;
}
&--suspicious {
background: var(--f-red-l);
color: #7f1d1d;
}
}
.case-form {
display: flex;
flex-direction: column;
@@ -1161,181 +995,6 @@
}
}
/* ── Principal Officer verification checklist ───────────────────────────── */
.checklist {
border: 1px solid var(--f-g200);
border-radius: 10px;
overflow: hidden;
background: #fff;
}
.checklist-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--f-g50);
border-bottom: 1px solid var(--f-g200);
font-size: 12px;
font-weight: 700;
color: var(--f-g700);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.checklist-progress {
font-size: 11px;
font-weight: 700;
padding: 2px 10px;
border-radius: 999px;
background: var(--f-amber-l);
color: var(--f-amber);
text-transform: none;
letter-spacing: 0;
white-space: nowrap;
&.done {
background: var(--f-green-l);
color: var(--f-green);
}
}
.checklist-tbl {
width: 100%;
border-collapse: collapse;
th {
padding: 8px 12px;
font-size: 10px;
font-weight: 700;
color: var(--f-g500);
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--f-g50);
border-bottom: 1px solid var(--f-g200);
text-align: left;
white-space: nowrap;
}
th.col-yes {
color: var(--f-green);
width: 96px;
}
th.col-no {
color: var(--f-red);
width: 112px;
}
td {
padding: 9px 12px;
border-bottom: 1px solid var(--f-g100);
vertical-align: middle;
}
tr:last-child td {
border-bottom: none;
}
}
/* Answered rows tint so an unresolved finding is obvious at a glance. */
.row-yes td {
background: rgb(220 252 231 / 45%);
}
.row-no td {
background: rgb(253 236 234 / 60%);
}
.row-pending td {
background: #fff;
}
.chk-label {
font-size: 13px;
font-weight: 600;
color: var(--f-g900);
}
.chk-hint {
font-size: 11px;
color: var(--f-g500);
margin-top: 1px;
}
/* Two-sided toggle: one box on the genuine side, one on the not-satisfied side. */
.chk-box {
width: 30px;
height: 30px;
border-radius: 7px;
border: 2px solid var(--f-g300);
background: #fff;
color: var(--f-g300);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.15s;
&:hover {
transform: scale(1.08);
}
&--yes:hover {
border-color: var(--f-green);
color: var(--f-green);
}
&--no:hover {
border-color: var(--f-red);
color: var(--f-red);
}
&--yes.on {
border-color: var(--f-green);
background: var(--f-green);
color: #fff;
}
&--no.on {
border-color: var(--f-red);
background: var(--f-red);
color: #fff;
}
}
/* Derived verdict banner */
.verdict {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 15px;
border-radius: 9px;
font-size: 13px;
background: var(--f-g100);
border: 1px solid var(--f-g200);
color: var(--f-g700);
&--clear {
background: var(--f-green-l);
border-color: #86efac;
color: #14532d;
}
&--suspicious {
background: var(--f-red-l);
border-color: #fca5a5;
color: #7f1d1d;
}
}
.case-form .fiu-btn-xml:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.file-str-row {
display: flex;
align-items: center;
+30 -344
View File
@@ -20,7 +20,6 @@ import {
KC2_ENTITIES,
KycEntity,
KycIndividual,
PoFinding,
StrStatus,
StrTransaction,
STR_TXNS
@@ -37,9 +36,6 @@ import {
]
})
export class FiuComponent implements OnInit {
/** Bump when CTR_TXNS / STR_TXNS change so cached rows are re-seeded. */
private static readonly SEED_VERSION = '2026-01-cash-threshold';
private amlService = inject(AmlService);
private authenticationService = inject(AuthenticationService);
/** Active tab */
@@ -60,9 +56,6 @@ export class FiuComponent implements OnInit {
strFilterStatus: '' | StrStatus = '';
strFilterRisk: string = '';
/** Ref of the STR card to highlight after jumping across from the CTR tab */
highlightedStrRef: number | null = null;
/** Toast */
toastMsg = '';
toastVisible = false;
@@ -83,89 +76,6 @@ export class FiuComponent implements OnInit {
concludeSourceOfFundsNotes = '';
fileReferenceNo = '';
/**
* The Principal Officer's verification checklist. Each point is answered
* "Genuine" or "Not satisfied"; the case conclusion is derived from the answers
* rather than picked by hand, so a case with an unresolved finding cannot be
* whitelisted by accident.
*/
readonly strChecklist: { key: string; label: string; hint: string }[] = [
{
key: 'sourceOfFunds',
label: 'Source of funds established and documented',
hint: 'Salary slips, sale deed, business receipts or other credible evidence on file'
},
{
key: 'profileConsistent',
label: 'Transaction consistent with the customer’s declared profile',
hint: 'Value and frequency in line with declared income, occupation and turnover'
},
{
key: 'kycCurrent',
label: 'KYC records complete and within re-KYC validity',
hint: 'Identity, address and beneficial-ownership records verified and current'
},
{
key: 'screeningClear',
label: 'Sanctions, PEP and adverse-media screening clear',
hint: 'No UNSCC / MHA listing, no PEP linkage, no negative news on the customer'
},
{
key: 'patternConsistent',
label: 'Pattern consistent with the account’s own history',
hint: 'No structuring, sudden dormancy revival or rapid pass-through observed'
},
{
key: 'counterpartyKnown',
label: 'Counterparty identifiable and legitimate',
hint: 'Remitter / beneficiary traceable and holding a plausible commercial relationship'
},
{
key: 'documentsGenuine',
label: 'Supporting documents verified as genuine',
hint: 'Invoices, contracts and bank statements independently corroborated'
},
{
key: 'noLawEnforcement',
label: 'No law-enforcement or regulatory reference pending',
hint: 'No summons, freeze order or LEA request outstanding against the account'
}
];
/** null = not yet answered, true = genuine, false = not satisfied. */
strFindings: Record<string, boolean | null> = {};
private resetChecklist(): void {
this.strFindings = {};
for (const item of this.strChecklist) {
this.strFindings[item.key] = null;
}
}
setFinding(key: string, genuine: boolean): void {
this.strFindings[key] = genuine;
this.concludeConclusion = this.derivedConclusion;
}
get answeredCount(): number {
return this.strChecklist.filter((i) => this.strFindings[i.key] !== null).length;
}
get allAnswered(): boolean {
return this.answeredCount === this.strChecklist.length;
}
/** Points the officer marked as not satisfied — these are what force an STR. */
get failedFindings(): { key: string; label: string; hint: string }[] {
return this.strChecklist.filter((i) => this.strFindings[i.key] === false);
}
/**
* Everything genuine means the case is cleared; a single unsatisfied point
* means it must be filed as suspicious.
*/
get derivedConclusion(): 'NOT_SUSPICIOUS' | 'SUSPICIOUS' {
return this.failedFindings.length > 0 ? 'SUSPICIOUS' : 'NOT_SUSPICIOUS';
}
get isPrincipalOfficer(): boolean {
return this.authenticationService.isPrincipalOfficerUser();
}
@@ -178,84 +88,6 @@ export class FiuComponent implements OnInit {
this.loadAmlAlerts();
this.loadAmlCtrReports();
this.loadAmlStrCases();
// Bumping SEED_VERSION discards previously cached rows so a new data set takes effect.
if (localStorage.getItem('fiu_seed_version') !== FiuComponent.SEED_VERSION) {
localStorage.setItem('fiu_seed_version', FiuComponent.SEED_VERSION);
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
return;
}
const storedCtr = localStorage.getItem('fiu_ctr_txns');
if (storedCtr) {
this.ctrTxns = JSON.parse(storedCtr);
} else {
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
}
const storedStr = localStorage.getItem('fiu_str_cases');
if (storedStr) {
this.strTxns = JSON.parse(storedStr);
} else {
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
}
}
whitelistCtrTxn(txn: CtrTransaction): void {
txn.complianceStatus = 'WHITELISTED';
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
this.toast(`Transaction for ${txn.customerName} whitelisted — stays in the CTR report only`);
}
flagCtrTxnSuspicious(txn: CtrTransaction): void {
txn.complianceStatus = 'SUSPICIOUS';
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
const newRef = Math.max(100, ...this.strTxns.map((s) => s.reportRefNo)) + 1;
const newStrCase: StrTransaction = {
...txn,
reportType: 'STR',
reportRefNo: newRef,
detectedOn: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'),
flaggedBy: 'AML Officer',
riskCategory: 'unusual-pattern',
narrative:
`Cash ${txn.depositOrWithdrawal.toLowerCase()} of ₹${txn.txnAmount.toLocaleString('en-IN')} breached the ` +
`₹${this.bank.cashThreshold.toLocaleString('en-IN')} CTR limit and was flagged as suspicious by the AML Officer. ` +
`Referred to the Principal Officer to establish whether the source of funds is legitimate or linked to terror financing.`,
status: 'po-review',
poFinding: undefined
};
this.strTxns.unshift(newStrCase);
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
this.toast(`STR-${newRef} opened for ${txn.customerName} — awaiting Principal Officer investigation`);
}
/** Principal Officer closes an STR case as legitimate, or confirms it and files an STR. */
concludeLocalStr(rec: StrTransaction, finding: PoFinding): void {
rec.poFinding = finding;
if (finding === 'CLEAN') {
rec.status = 'rejected';
rec.poRemarks =
'Source-of-funds documentation reviewed and verified by the Principal Officer. No link to terror financing or ' +
'any other predicate offence. Case closed — no STR filed with FIU-IND.';
this.toast(`STR-${rec.reportRefNo} closed as clean — no STR filed`);
} else {
rec.status = 'filed';
rec.poRemarks =
'Principal Officer investigation could not establish a legitimate source of funds. STR filed with FIU-IND under ' +
'PMLA s.12 within the 7-day window; account placed under enhanced monitoring and reported to the Nodal Officer.';
this.toast(`STR-${rec.reportRefNo} confirmed suspicious — filed with FIU-IND`);
}
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
}
poFindingLabel(finding: PoFinding): string {
return finding === 'CLEAN'
? 'Clean — source of funds verified as legitimate. No STR filed.'
: 'Suspicious — STR filed with FIU-IND and account escalated.';
}
loadAmlAlerts(): void {
@@ -272,35 +104,23 @@ export class FiuComponent implements OnInit {
this.amlService.getStrCases().subscribe((data) => (this.amlStrCases = data || []));
}
/** Surfaces a failed backend call — without this a rejected request looks like a dead button. */
private apiError(action: string) {
return (err: any) => this.toast(err?.error?.defaultUserMessage || `${action} failed — please retry`);
}
whitelistAlert(alert: any): void {
this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe({
next: () => {
this.toast(`Alert #${alert.id} whitelisted — no STR raised`);
this.loadAmlAlerts();
},
error: this.apiError(`Whitelisting alert #${alert.id}`)
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({
next: () => {
this.toast(`Alert #${alert.id} escalated — now an STR case for the Principal Officer`);
this.loadAmlAlerts();
this.loadAmlStrCases();
},
error: this.apiError(`Escalating alert #${alert.id}`)
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) {
this.toast('Pick a reporting month first');
return;
}
this.amlService.generateCtrReport(this.ctrGenerateMonth).subscribe({
@@ -308,32 +128,16 @@ export class FiuComponent implements OnInit {
this.toast(`CTR report generated for ${this.ctrGenerateMonth}`);
this.loadAmlCtrReports();
},
error: this.apiError('CTR report generation')
error: (err) => this.toast(err?.error?.defaultUserMessage || 'CTR report generation failed')
});
}
selectStrCase(strCase: any): void {
this.selectedStrCase = strCase;
// Preserve an existing verdict instead of silently resetting it to NOT_SUSPICIOUS.
this.concludeConclusion = strCase.conclusion === 'SUSPICIOUS' ? 'SUSPICIOUS' : 'NOT_SUSPICIOUS';
this.concludeConclusion = 'NOT_SUSPICIOUS';
this.concludeInvestigationNotes = strCase.investigationNotes || '';
this.concludeSourceOfFundsNotes = strCase.sourceOfFundsNotes || '';
this.fileReferenceNo = strCase.strReferenceNo || '';
this.resetChecklist();
}
/** Plain-language outcome for a live STR case, shown in the investigation panel. */
strCaseOutcome(strCase: any): string {
switch (strCase?.status) {
case 'CONCLUDED_CLEAR':
return 'Concluded not suspicious — case closed, nothing filed with FIU-IND.';
case 'CONCLUDED_SUSPICIOUS':
return `Concluded suspicious — STR must be filed with FIU-IND by ${strCase.fiuDueDate || 'the due date'}.`;
case 'FILED':
return `STR filed with FIU-IND${strCase.strReferenceNo ? ` under reference ${strCase.strReferenceNo}` : ''}.`;
default:
return 'Open — awaiting the Principal Officer’s conclusion.';
}
this.fileReferenceNo = '';
}
closeStrCaseDetail(): void {
@@ -344,60 +148,32 @@ export class FiuComponent implements OnInit {
if (!this.selectedStrCase) {
return;
}
if (!this.allAnswered) {
const pending = this.strChecklist.length - this.answeredCount;
this.toast(`Answer all verification points first — ${pending} still pending`);
return;
}
if (!this.concludeInvestigationNotes.trim()) {
this.toast('Record your investigation notes before concluding');
return;
}
// The verdict always follows the checklist, never a stale radio selection.
this.concludeConclusion = this.derivedConclusion;
// Fold the unsatisfied points into the notes so the STR carries its own reasons.
const failed = this.failedFindings;
const notes = failed.length
? `${this.concludeInvestigationNotes.trim()}\n\nUnsatisfied verification points:\n` +
failed.map((f) => `• ${f.label}`).join('\n')
: `${this.concludeInvestigationNotes.trim()}\n\nAll ${this.strChecklist.length} verification points found genuine.`;
this.amlService
.concludeStrCase(
this.selectedStrCase.id,
this.concludeConclusion,
notes,
this.concludeInvestigationNotes,
this.concludeSourceOfFundsNotes
)
.subscribe({
next: (updated) => {
this.toast(
this.concludeConclusion === 'SUSPICIOUS'
? `STR case #${updated.id} concluded suspicious — file with FIU-IND by ${updated.fiuDueDate}`
: `STR case #${updated.id} concluded not suspicious — no filing required`
);
this.selectedStrCase = updated;
this.loadAmlStrCases();
},
error: this.apiError(`Concluding STR case #${this.selectedStrCase.id}`)
.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) {
if (!this.selectedStrCase || !this.fileReferenceNo) {
return;
}
if (!this.fileReferenceNo.trim()) {
this.toast('Enter the FIU-IND acknowledgement number first');
return;
}
this.amlService.fileStr(this.selectedStrCase.id, this.fileReferenceNo).subscribe({
next: (updated) => {
this.toast(`STR case #${updated.id} recorded as filed (ref ${updated.strReferenceNo})`);
this.selectedStrCase = updated;
this.loadAmlStrCases();
},
error: this.apiError(`Filing STR case #${this.selectedStrCase.id}`)
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();
});
}
@@ -405,57 +181,10 @@ export class FiuComponent implements OnInit {
return 'status-pill status-' + (status || '').toLowerCase().replace(/_/g, '-');
}
/**
* Where a live AML alert will end up once triaged — drives the "Routes To" column.
* `isCtrRule` distinguishes mechanical CTR-threshold alerts (whitelisting just keeps them
* in the CTR file) from behavioral alerts (whitelisting clears them with no report at all).
*/
alertRouteLabel(status: string, isCtrRule: boolean = true): string {
switch (status) {
case 'WHITELISTED':
return isCtrRule ? 'CTR only' : 'Cleared';
case 'ESCALATED':
return '→ STR';
default:
return 'Pending';
}
}
alertRouteClass(status: string, isCtrRule: boolean = true): string {
switch (status) {
case 'WHITELISTED':
return isCtrRule ? 'report-pill report-ctr' : 'report-pill report-cleared';
case 'ESCALATED':
return 'report-pill report-str';
default:
return 'report-pill report-pending';
}
}
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
this.activeTab = tab;
}
/**
* Jump from a flagged CTR row to its STR card. Clears any status/risk filter that
* would hide the case, then scrolls it into view and highlights it.
*/
openStrCase(txn: CtrTransaction): void {
const match = this.strTxns.find((s) => s.txnId === txn.txnId);
if (!match) {
this.toast('No STR case found for this transaction');
return;
}
this.activeTab = 'str';
this.resetStrFilters();
this.highlightedStrRef = match.reportRefNo;
setTimeout(() => {
document
.getElementById(`str-card-${match.reportRefNo}`)
?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
// -------------------- KPIs --------------------
get totalReports(): number {
return new Set(this.ctrTxns.map((t) => t.reportRefNo)).size;
@@ -472,39 +201,10 @@ export class FiuComponent implements OnInit {
get filedStr(): number {
return this.strTxns.filter((s) => s.status === 'filed').length;
}
// ── Live STR case counts (backend `amlStrCases`) — these are the cases the
// Principal Officer actually works through the Investigate → Conclude → File flow.
get liveStrOpen(): number {
return this.amlStrCases.filter((c) => c.status === 'OPEN').length;
}
get liveStrAwaitingFiling(): number {
return this.amlStrCases.filter((c) => c.status === 'CONCLUDED_SUSPICIOUS').length;
}
get liveStrCleared(): number {
return this.amlStrCases.filter((c) => c.status === 'CONCLUDED_CLEAR').length;
}
get liveStrFiled(): number {
return this.amlStrCases.filter((c) => c.status === 'FILED').length;
}
/** Still needs PO action: untouched, or concluded suspicious but not yet filed with FIU-IND. */
get liveStrPending(): number {
return this.liveStrOpen + this.liveStrAwaitingFiling;
}
get totalCustomers(): number {
return this.kc1.length + this.kc2.length;
}
get ctrAlerts(): any[] {
const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L'];
return this.amlAlerts.filter((a) => ctrRuleCodes.includes(a.ruleCode));
}
get amlSuspiciousAlerts(): any[] {
const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L'];
return this.amlAlerts.filter((a) => !ctrRuleCodes.includes(a.ruleCode));
}
// -------------------- Filtered lists --------------------
get filteredCtr(): CtrTransaction[] {
const acc = this.ctrFilterAccount.trim();
@@ -524,14 +224,6 @@ export class FiuComponent implements OnInit {
});
}
/**
* Cases that actually belong in an STR filing. A case the Principal Officer
* concluded clean is not a suspect and must never reach FIU-IND.
*/
get strSuspects(): StrTransaction[] {
return this.filteredStr.filter((s) => s.poFinding !== 'CLEAN' && s.status !== 'rejected');
}
resetCtrFilters(): void {
this.ctrFilterMode = '';
this.ctrFilterAccount = '';
@@ -831,11 +523,11 @@ export class FiuComponent implements OnInit {
const rows = [headers];
// Build one aggregate row per (reportRefNo, accountNumber) pair
const seen = new Set<string>();
for (const t of this.filteredCtr) {
for (const t of this.ctrTxns) {
const key = `${t.reportRefNo}|${t.accountNumber}`;
if (seen.has(key)) continue;
seen.add(key);
const sameKey = this.filteredCtr.filter(
const sameKey = this.ctrTxns.filter(
(x) => x.reportRefNo === t.reportRefNo && x.accountNumber === t.accountNumber
);
const debits = sameKey.filter((x) => x.depositOrWithdrawal === 'Withdrawal');
@@ -884,7 +576,7 @@ export class FiuComponent implements OnInit {
];
const rows = [headers];
const seen = new Set<string>();
for (const t of this.filteredCtr) {
for (const t of this.ctrTxns) {
const key = `${t.reportRefNo}|${t.accountNumber}`;
if (seen.has(key)) continue;
seen.add(key);
@@ -907,17 +599,12 @@ export class FiuComponent implements OnInit {
/** STR XML stub (FINnet 2.0 STR XML) */
exportStrXml(): void {
const suspects = this.strSuspects;
if (!suspects.length) {
this.toast('No suspect cases to export — nothing matches the current filters');
return;
}
const xml: string[] = [];
xml.push('<?xml version="1.0" encoding="UTF-8"?>');
xml.push(`<STR xmlns="http://fiuind.gov.in/finnet2/str" reportingMonth="${this.bank.reportingMonth}">`);
xml.push(` <REBank buid="${this.bank.reBuid}" name="${this.bank.name}" />`);
xml.push(` <PrincipalOfficer name="${this.bank.principalOfficer}" email="${this.bank.principalOfficerEmail}" />`);
for (const s of suspects) {
for (const s of this.filteredStr) {
xml.push(` <Suspect ref="${s.reportRefNo}" status="${s.status}">`);
xml.push(` <Account>${s.accountNumber}</Account>`);
xml.push(
@@ -937,10 +624,10 @@ export class FiuComponent implements OnInit {
a.download = `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`;
a.click();
URL.revokeObjectURL(url);
this.toast(`STR XML exported (${suspects.length} suspects)`);
this.toast(`STR XML exported (${this.filteredStr.length} suspects)`);
}
/** Bundle: the 6 CSVs plus the STR XML — emitted as separate downloads sequentially */
/** Bundle: all 6 CSVs zipped — emitted as separate downloads sequentially */
exportAll(): void {
setTimeout(() => this.exportTc1(), 0);
setTimeout(() => this.exportTc2(), 200);
@@ -948,7 +635,6 @@ export class FiuComponent implements OnInit {
setTimeout(() => this.exportKc2(), 600);
setTimeout(() => this.exportAccountDetail(), 800);
setTimeout(() => this.exportAccountPersonRelation(), 1000);
setTimeout(() => this.exportStrXml(), 1200);
}
private escapeXml(s: string): string {
-496
View File
@@ -1,496 +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/.
*/
/**
* Data layer for the General Ledger report suite.
*
* Rather than hard-coding seven finished reports, this file holds a chart of
* accounts plus a generated ledger of double-entry postings. Every report is
* then computed from those postings, which is what makes the day / month /
* branch filters genuinely recompute instead of just re-labelling a static table.
*
* The generator is deterministic — a fixed-seed LCG, never Math.random — so the
* figures are identical on every reload and across users. Amounts are in rupees.
*/
export type GlAccountType = 'ASSET' | 'LIABILITY' | 'INCOME' | 'EXPENSE' | 'CONTINGENT';
export interface GlAccount {
code: string;
name: string;
type: GlAccountType;
/** Balance-sheet / P&L grouping used as a sub-heading in the reports. */
group: string;
/** Included in the Misc Expense Ledger report. */
miscExpense?: boolean;
}
export interface GlBranch {
code: string;
name: string;
}
/** One side of a double-entry voucher. */
export interface GlPosting {
/** ISO yyyy-MM-dd — the posting (value) date. */
date: string;
branchCode: string;
accountCode: string;
debit: number;
credit: number;
voucherNo: string;
narration: string;
}
export const GL_BRANCHES: GlBranch[] = [
{ code: 'HO', name: 'Head Office' },
{ code: '0002', name: 'Andheri (E)' },
{ code: '0003', name: 'Bandra (W)' },
{ code: '0004', name: 'Borivali' },
{ code: '0005', name: 'Dadar' },
{ code: '0006', name: 'Thane' }
];
export const GL_ACCOUNTS: GlAccount[] = [
// ── Assets ────────────────────────────────────────────────────────────────
{ code: '110101', name: 'Cash in Hand', type: 'ASSET', group: 'Cash & Bank Balances' },
{ code: '110102', name: 'Balance with RBI', type: 'ASSET', group: 'Cash & Bank Balances' },
{ code: '110103', name: 'Balance with Other Banks — Current', type: 'ASSET', group: 'Cash & Bank Balances' },
{ code: '110104', name: 'Balance with Other Banks — Fixed', type: 'ASSET', group: 'Cash & Bank Balances' },
{ code: '120101', name: 'SLR Investments — Govt Securities', type: 'ASSET', group: 'Investments' },
{ code: '120102', name: 'SLR Investments — State Govt Securities', type: 'ASSET', group: 'Investments' },
{ code: '120201', name: 'Non-SLR Investments — Bonds', type: 'ASSET', group: 'Investments' },
{ code: '120202', name: 'Non-SLR Investments — Mutual Funds', type: 'ASSET', group: 'Investments' },
{ code: '130101', name: 'Cash Credit & Overdrafts', type: 'ASSET', group: 'Loans & Advances' },
{ code: '130102', name: 'Term Loans — Short Term', type: 'ASSET', group: 'Loans & Advances' },
{ code: '130103', name: 'Term Loans — Medium Term', type: 'ASSET', group: 'Loans & Advances' },
{ code: '130104', name: 'Term Loans — Long Term', type: 'ASSET', group: 'Loans & Advances' },
{ code: '130105', name: 'Housing Loans', type: 'ASSET', group: 'Loans & Advances' },
{ code: '130106', name: 'Gold Loans', type: 'ASSET', group: 'Loans & Advances' },
{ code: '140101', name: 'Interest Receivable', type: 'ASSET', group: 'Other Assets' },
{ code: '140102', name: 'Premises (net of depreciation)', type: 'ASSET', group: 'Fixed Assets' },
{ code: '140103', name: 'Furniture & Fixtures (net)', type: 'ASSET', group: 'Fixed Assets' },
{ code: '140104', name: 'Sundry Debtors', type: 'ASSET', group: 'Other Assets' },
{ code: '140105', name: 'Advance Tax & TDS', type: 'ASSET', group: 'Other Assets' },
{ code: '140199', name: 'Opening Balance Suspense (unreconciled)', type: 'ASSET', group: 'Other Assets' },
// ── Liabilities & Capital ─────────────────────────────────────────────────
{ code: '210101', name: 'Share Capital — Individuals', type: 'LIABILITY', group: 'Capital & Reserves' },
{ code: '210201', name: 'Statutory Reserve Fund', type: 'LIABILITY', group: 'Capital & Reserves' },
{ code: '210202', name: 'Building Fund', type: 'LIABILITY', group: 'Capital & Reserves' },
{ code: '210203', name: 'Investment Depreciation Reserve', type: 'LIABILITY', group: 'Capital & Reserves' },
{ code: '210204', name: 'BDDR / Special BDDR', type: 'LIABILITY', group: 'Capital & Reserves' },
{ code: '220101', name: 'Savings Bank Deposits', type: 'LIABILITY', group: 'Deposits' },
{ code: '220102', name: 'Current Deposits', type: 'LIABILITY', group: 'Deposits' },
{ code: '220103', name: 'Term Deposits', type: 'LIABILITY', group: 'Deposits' },
{ code: '220104', name: 'Recurring Deposits', type: 'LIABILITY', group: 'Deposits' },
{ code: '230101', name: 'Interest Payable on Deposits', type: 'LIABILITY', group: 'Other Liabilities' },
{ code: '230102', name: 'Bills Payable', type: 'LIABILITY', group: 'Other Liabilities' },
{ code: '230103', name: 'Sundry Creditors', type: 'LIABILITY', group: 'Other Liabilities' },
{ code: '230104', name: 'TDS / GST Payable', type: 'LIABILITY', group: 'Other Liabilities' },
{ code: '230105', name: 'Overdue Interest Reserve', type: 'LIABILITY', group: 'Other Liabilities' },
// ── Income ────────────────────────────────────────────────────────────────
{ code: '310101', name: 'Interest on Loans & Advances', type: 'INCOME', group: 'Interest Income' },
{ code: '310102', name: 'Interest on Investments', type: 'INCOME', group: 'Interest Income' },
{ code: '310103', name: 'Interest on Inter-bank Deposits', type: 'INCOME', group: 'Interest Income' },
{ code: '320101', name: 'Commission & Exchange', type: 'INCOME', group: 'Other Income' },
{ code: '320102', name: 'Locker Rent', type: 'INCOME', group: 'Other Income' },
{ code: '320103', name: 'Processing Fees', type: 'INCOME', group: 'Other Income' },
{ code: '320104', name: 'Profit on Sale of Securities', type: 'INCOME', group: 'Other Income' },
{ code: '320105', name: 'Miscellaneous Income', type: 'INCOME', group: 'Other Income' },
// ── Expenditure ───────────────────────────────────────────────────────────
{ code: '410101', name: 'Interest on Savings Deposits', type: 'EXPENSE', group: 'Interest Expended' },
{ code: '410102', name: 'Interest on Term Deposits', type: 'EXPENSE', group: 'Interest Expended' },
{ code: '410103', name: 'Interest on Borrowings', type: 'EXPENSE', group: 'Interest Expended' },
{ code: '420101', name: 'Salaries & Allowances', type: 'EXPENSE', group: 'Operating Expenses' },
{ code: '420102', name: "Directors' Fees", type: 'EXPENSE', group: 'Operating Expenses' },
{ code: '420103', name: "Auditors' Fees", type: 'EXPENSE', group: 'Operating Expenses' },
{ code: '420104', name: 'Rent, Taxes & Lighting', type: 'EXPENSE', group: 'Operating Expenses' },
{ code: '420105', name: 'Depreciation on Bank Property', type: 'EXPENSE', group: 'Operating Expenses' },
{ code: '430101', name: 'Printing & Stationery', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430102', name: 'Postage, Telegram & Telephone', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430103', name: 'Advertisement & Publicity', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430104', name: 'Repairs & Maintenance', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430105', name: 'Travelling & Conveyance', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430106', name: 'Computer & Software Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430107', name: 'Legal & Professional Charges', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430108', name: 'Insurance (DICGC & Other)', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430109', name: 'Bank Charges & Commission Paid', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430110', name: 'AGM & Meeting Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430111', name: 'Electricity Charges', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
{ code: '430112', name: 'Other Miscellaneous Expenses', type: 'EXPENSE', group: 'Miscellaneous Expenses', miscExpense: true },
// ── Contingent (memoranda, not part of the balance sheet totals) ──────────
{ code: '910101', name: 'Guarantees Issued — Financial', type: 'CONTINGENT', group: 'Guarantees' },
{ code: '910102', name: 'Guarantees Issued — Performance', type: 'CONTINGENT', group: 'Guarantees' },
{ code: '910103', name: 'Letters of Credit Outstanding', type: 'CONTINGENT', group: 'Letters of Credit' },
{ code: '910104', name: 'Bills for Collection', type: 'CONTINGENT', group: 'Bills & Claims' },
{ code: '910105', name: 'Claims Against Bank Not Acknowledged as Debt', type: 'CONTINGENT', group: 'Bills & Claims' },
{ code: '910106', name: 'Forward Exchange Contracts', type: 'CONTINGENT', group: 'Other Commitments' },
{ code: '910107', name: 'Capital Commitments Not Provided For', type: 'CONTINGENT', group: 'Other Commitments' }
];
/** Financial year covered by the generated ledger. */
export const GL_PERIOD = {
/** FY 2026-27 opens here. */
fyStart: '2026-04-01',
/** Postings run to the day before the app's "today" (01 Aug 2026). */
fyEnd: '2026-07-31',
label: 'FY 2026-27'
};
/**
* Opening balances as at 01-Apr-2026, keyed by account code. Positive means the
* account opens on its natural side (debit for assets/expenses, credit for the
* rest). Head Office carries the investments and reserves; branches carry the
* customer-facing book.
*/
const OPENING_BALANCES: Record<string, number> = {
'110101': 9_996_000,
'110102': 12_400_000,
'110103': 13_667_000,
// Surplus deposits parked with correspondent banks. Sized so the opening
// balance sheet tallies exactly — see the assertion in glPostings().
'110104': 54_206_000,
'120101': 63_839_000,
'120102': 147_895_000,
'120201': 22_500_000,
'120202': 24_234_000,
'130101': 166_614_000,
'130102': 125_082_000,
'130103': 70_236_000,
'130104': 29_356_000,
'130105': 10_241_000,
'130106': 8_450_000,
'140101': 33_541_000,
'140102': 3_185_000,
'140103': 1_763_000,
'140104': 1_240_000,
'140105': 657_000,
'210101': 27_278_000,
'210201': 31_149_000,
'210202': 13_642_000,
'210203': 3_750_000,
'210204': 24_341_000,
'220101': 208_116_000,
'220102': 19_592_000,
'220103': 406_601_000,
'220104': 24_880_000,
'230101': 731_000,
'230102': 291_000,
'230103': 5_515_000,
'230104': 1_120_000,
'230105': 32_096_000,
'910101': 1_450_000,
'910102': 980_000,
'910103': 620_000,
'910104': 1_180_000,
'910105': 2_638_000,
'910106': 0,
'910107': 340_000
};
/** Deterministic linear congruential generator — same figures on every load. */
function lcg(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
function isoDate(d: Date): string {
const y = d.getUTCFullYear();
const m = `${d.getUTCMonth() + 1}`.padStart(2, '0');
const day = `${d.getUTCDate()}`.padStart(2, '0');
return `${y}-${m}-${day}`;
}
/** Every posting date in the ledger period, Sundays excluded (bank holiday). */
export function ledgerDates(): string[] {
const out: string[] = [];
const cur = new Date(`${GL_PERIOD.fyStart}T00:00:00Z`);
const end = new Date(`${GL_PERIOD.fyEnd}T00:00:00Z`);
while (cur <= end) {
if (cur.getUTCDay() !== 0) {
out.push(isoDate(cur));
}
cur.setUTCDate(cur.getUTCDate() + 1);
}
return out;
}
/** Templates for the vouchers the generator posts each day. */
const VOUCHER_TEMPLATES: {
debit: string;
credit: string;
narration: string;
/** Rough rupee scale of the voucher. */
scale: number;
/** Chance the voucher is raised on any given branch-day. */
frequency: number;
}[] = [
{ debit: '110101', credit: '220101', narration: 'Cash deposit — savings accounts', scale: 900_000, frequency: 1 },
{ debit: '220101', credit: '110101', narration: 'Cash withdrawal — savings accounts', scale: 780_000, frequency: 1 },
{ debit: '110101', credit: '220103', narration: 'Term deposit received', scale: 1_400_000, frequency: 0.75 },
{ debit: '220103', credit: '110101', narration: 'Term deposit matured and paid', scale: 1_150_000, frequency: 0.6 },
{ debit: '110101', credit: '220102', narration: 'Current account deposit', scale: 620_000, frequency: 0.8 },
{ debit: '220102', credit: '110101', narration: 'Current account withdrawal', scale: 560_000, frequency: 0.8 },
{ debit: '110101', credit: '220104', narration: 'Recurring deposit instalment', scale: 180_000, frequency: 0.7 },
{ debit: '130101', credit: '110101', narration: 'Cash credit disbursed', scale: 1_250_000, frequency: 0.55 },
{ debit: '110101', credit: '130101', narration: 'Cash credit repayment', scale: 1_100_000, frequency: 0.6 },
{ debit: '130102', credit: '110101', narration: 'Short term loan disbursed', scale: 850_000, frequency: 0.4 },
{ debit: '110101', credit: '130102', narration: 'Short term loan repayment', scale: 760_000, frequency: 0.5 },
{ debit: '130103', credit: '110101', narration: 'Medium term loan disbursed', scale: 640_000, frequency: 0.3 },
{ debit: '110101', credit: '130104', narration: 'Long term loan repayment', scale: 410_000, frequency: 0.35 },
{ debit: '130105', credit: '110101', narration: 'Housing loan disbursed', scale: 520_000, frequency: 0.2 },
{ debit: '130106', credit: '110101', narration: 'Gold loan disbursed', scale: 260_000, frequency: 0.4 },
// Interest on advances is the dominant earning line — sized so the bank runs a
// ~13% net margin rather than the loss the first-cut figures produced.
{ debit: '110101', credit: '310101', narration: 'Interest recovered on advances', scale: 430_000, frequency: 1 },
{ debit: '140101', credit: '310102', narration: 'Interest accrued on investments', scale: 148_000, frequency: 0.5 },
{ debit: '110103', credit: '310103', narration: 'Interest on inter-bank deposits', scale: 42_000, frequency: 0.25 },
{ debit: '110101', credit: '320101', narration: 'Commission and exchange collected', scale: 26_000, frequency: 0.9 },
{ debit: '110101', credit: '320102', narration: 'Locker rent collected', scale: 9_000, frequency: 0.3 },
{ debit: '110101', credit: '320103', narration: 'Loan processing fees', scale: 18_000, frequency: 0.5 },
{ debit: '110101', credit: '320105', narration: 'Miscellaneous income', scale: 7_500, frequency: 0.4 },
{ debit: '410101', credit: '230101', narration: 'Interest provided on savings deposits', scale: 128_000, frequency: 0.8 },
{ debit: '410102', credit: '230101', narration: 'Interest provided on term deposits', scale: 265_000, frequency: 0.9 },
{ debit: '420101', credit: '110101', narration: 'Salaries and allowances paid', scale: 210_000, frequency: 0.35 },
{ debit: '420104', credit: '110101', narration: 'Rent, taxes and lighting', scale: 46_000, frequency: 0.3 },
{ debit: '430101', credit: '110101', narration: 'Printing and stationery purchased', scale: 9_400, frequency: 0.3 },
{ debit: '430102', credit: '110101', narration: 'Postage and telephone charges', scale: 6_200, frequency: 0.35 },
{ debit: '430103', credit: '110101', narration: 'Advertisement and publicity', scale: 12_500, frequency: 0.15 },
{ debit: '430104', credit: '110101', narration: 'Repairs and maintenance', scale: 15_800, frequency: 0.25 },
{ debit: '430105', credit: '110101', narration: 'Travelling and conveyance', scale: 7_100, frequency: 0.3 },
{ debit: '430106', credit: '110101', narration: 'Computer and software expenses', scale: 22_400, frequency: 0.2 },
{ debit: '430107', credit: '110101', narration: 'Legal and professional charges', scale: 11_300, frequency: 0.15 },
{ debit: '430109', credit: '110101', narration: 'Bank charges and commission paid', scale: 4_800, frequency: 0.3 },
{ debit: '430111', credit: '110101', narration: 'Electricity charges', scale: 13_600, frequency: 0.25 },
{ debit: '430112', credit: '110101', narration: 'Other miscellaneous expenses', scale: 8_900, frequency: 0.3 },
{ debit: '110102', credit: '110101', narration: 'Cash remitted to RBI currency chest', scale: 1_800_000, frequency: 0.2 },
{ debit: '110101', credit: '110103', narration: 'Funds drawn from correspondent bank', scale: 1_400_000, frequency: 0.2 }
];
/** Head-Office-only vouchers — investments, reserves and centralised charges. */
const HO_TEMPLATES: typeof VOUCHER_TEMPLATES = [
{ debit: '120101', credit: '110102', narration: 'Purchase of Govt securities', scale: 2_400_000, frequency: 0.12 },
{ debit: '120102', credit: '110102', narration: 'Purchase of State Govt securities', scale: 1_900_000, frequency: 0.12 },
{ debit: '110102', credit: '320104', narration: 'Profit on sale of securities', scale: 78_000, frequency: 0.1 },
{ debit: '420102', credit: '110101', narration: "Directors' sitting fees", scale: 32_000, frequency: 0.08 },
{ debit: '420103', credit: '110101', narration: "Statutory auditors' fees", scale: 45_000, frequency: 0.06 },
{ debit: '420105', credit: '140103', narration: 'Depreciation charged on assets', scale: 64_000, frequency: 0.12 },
{ debit: '430108', credit: '110101', narration: 'DICGC premium and other insurance', scale: 38_000, frequency: 0.1 },
{ debit: '430110', credit: '110101', narration: 'AGM and meeting expenses', scale: 26_000, frequency: 0.06 },
{ debit: '410103', credit: '230101', narration: 'Interest on borrowings', scale: 54_000, frequency: 0.1 },
{ debit: '230104', credit: '110102', narration: 'TDS and GST remitted', scale: 145_000, frequency: 0.15 }
];
/**
* Contingent-account movements are memoranda, each posted single-sided.
* Guarantees, LCs and collection bills are raised at the counter, so these run
* at every branch — not just Head Office.
*/
const CONTINGENT_TEMPLATES: { account: string; narration: string; scale: number; frequency: number }[] = [
{ account: '910101', narration: 'Financial guarantee issued', scale: 320_000, frequency: 0.1 },
{ account: '910102', narration: 'Performance guarantee issued', scale: 240_000, frequency: 0.08 },
{ account: '910103', narration: 'Letter of credit opened', scale: 180_000, frequency: 0.07 },
{ account: '910104', narration: 'Bills lodged for collection', scale: 95_000, frequency: 0.12 },
{ account: '910105', narration: 'Claim lodged against the bank', scale: 140_000, frequency: 0.03 },
{ account: '910107', narration: 'Capital commitment contracted', scale: 85_000, frequency: 0.04 }
];
/**
* How the opening contingent book is spread across the network. Head Office
* carries the capital commitments and large claims; the branches carry the
* counter-issued guarantees. Weights are applied per account and the rounding
* remainder is absorbed by Head Office so each account's total stays exact.
*/
const CONTINGENT_BRANCH_WEIGHTS: Record<string, number> = {
HO: 0.2,
'0002': 0.18,
'0003': 0.16,
'0004': 0.16,
'0005': 0.15,
'0006': 0.15
};
let cachedPostings: GlPosting[] | null = null;
/**
* The full generated ledger. Built once and memoised — every report reads from
* this array, so the reports can never disagree with one another.
*/
export function glPostings(): GlPosting[] {
if (cachedPostings) {
return cachedPostings;
}
const rand = lcg(20260401);
const out: GlPosting[] = [];
const dates = ledgerDates();
let voucherSeq = 1;
// Opening entries, posted at Head Office on day one so the books start in balance.
const openingVoucher = `OB/${GL_PERIOD.label.replace(/\s/g, '')}`;
let openingDebit = 0;
let openingCredit = 0;
for (const [code, amount] of Object.entries(OPENING_BALANCES)) {
if (!amount) {
continue;
}
const account = GL_ACCOUNTS.find((a) => a.code === code);
if (!account) {
continue;
}
const debitSide = account.type === 'ASSET' || account.type === 'EXPENSE' || account.type === 'CONTINGENT';
// Contingent accounts are single-sided memoranda held at the issuing branch,
// so their opening book is spread across the network rather than sat at HO.
if (account.type === 'CONTINGENT') {
let allocated = 0;
for (const branch of GL_BRANCHES) {
if (branch.code === 'HO') {
continue;
}
const share = Math.round((amount * (CONTINGENT_BRANCH_WEIGHTS[branch.code] ?? 0)) / 100) * 100;
if (!share) {
continue;
}
allocated += share;
out.push({
date: GL_PERIOD.fyStart,
branchCode: branch.code,
accountCode: code,
debit: share,
credit: 0,
voucherNo: openingVoucher,
narration: 'Opening balance brought forward'
});
}
// Head Office takes its own share plus whatever rounding left behind.
out.push({
date: GL_PERIOD.fyStart,
branchCode: 'HO',
accountCode: code,
debit: amount - allocated,
credit: 0,
voucherNo: openingVoucher,
narration: 'Opening balance brought forward'
});
continue;
}
out.push({
date: GL_PERIOD.fyStart,
branchCode: 'HO',
accountCode: code,
debit: debitSide ? amount : 0,
credit: debitSide ? 0 : amount,
voucherNo: openingVoucher,
narration: 'Opening balance brought forward'
});
if (debitSide) {
openingDebit += amount;
} else {
openingCredit += amount;
}
}
// If OPENING_BALANCES is ever edited so the two sides diverge, park the residual
// in a visibly-named suspense account rather than letting the trial balance drift.
const openingResidual = openingCredit - openingDebit;
if (openingResidual !== 0) {
out.push({
date: GL_PERIOD.fyStart,
branchCode: 'HO',
accountCode: '140199',
debit: openingResidual > 0 ? openingResidual : 0,
credit: openingResidual < 0 ? -openingResidual : 0,
voucherNo: openingVoucher,
narration: 'Unreconciled opening balance difference'
});
}
const round = (n: number): number => Math.round(n / 100) * 100;
for (const date of dates) {
for (const branch of GL_BRANCHES) {
// Head Office posts fewer counter transactions than the branches.
const weight = branch.code === 'HO' ? 0.45 : 1;
const templates = branch.code === 'HO' ? [...VOUCHER_TEMPLATES, ...HO_TEMPLATES] : VOUCHER_TEMPLATES;
for (const tpl of templates) {
if (rand() > tpl.frequency * weight) {
continue;
}
// 0.55x .. 1.45x of the template scale keeps daily figures varied but plausible.
const amount = round(tpl.scale * (0.55 + rand() * 0.9));
if (amount <= 0) {
continue;
}
const voucherNo = `${branch.code}/${date.slice(5, 7)}/${`${voucherSeq++}`.padStart(5, '0')}`;
out.push({
date,
branchCode: branch.code,
accountCode: tpl.debit,
debit: amount,
credit: 0,
voucherNo,
narration: tpl.narration
});
out.push({
date,
branchCode: branch.code,
accountCode: tpl.credit,
debit: 0,
credit: amount,
voucherNo,
narration: tpl.narration
});
}
// Contingent memoranda are raised at every branch, weighted by its size.
const contingentWeight = CONTINGENT_BRANCH_WEIGHTS[branch.code] ?? 0.15;
for (const tpl of CONTINGENT_TEMPLATES) {
// Claims against the bank and capital commitments are Head Office matters.
const headOfficeOnly = tpl.account === '910105' || tpl.account === '910107';
if (headOfficeOnly && branch.code !== 'HO') {
continue;
}
const frequency = headOfficeOnly ? tpl.frequency : tpl.frequency * (contingentWeight / 0.2);
if (rand() > frequency) {
continue;
}
const amount = round(tpl.scale * (0.55 + rand() * 0.9));
out.push({
date,
branchCode: branch.code,
accountCode: tpl.account,
debit: amount,
credit: 0,
voucherNo: `CNT/${branch.code}/${date.slice(5, 7)}/${`${voucherSeq++}`.padStart(5, '0')}`,
narration: tpl.narration
});
}
}
}
cachedPostings = out;
return out;
}
export function glAccount(code: string): GlAccount | undefined {
return GL_ACCOUNTS.find((a) => a.code === code);
}
export function glBranchName(code: string): string {
return GL_BRANCHES.find((b) => b.code === code)?.name ?? code;
}
@@ -1,34 +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 { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service';
const routes: Routes = [
Route.withShell([
{
path: 'gl/reports',
// Lazy — the generated ledger and ExcelJS should not weigh down the eager bundle.
loadComponent: () => import('./gl-reports.component').then((m) => m.GlReportsComponent),
data: { title: 'General Ledger Reports', breadcrumb: 'GL Reports' }
},
{
path: 'gl/reports/:report',
loadComponent: () => import('./gl-reports.component').then((m) => m.GlReportsComponent),
data: { title: 'General Ledger Reports', breadcrumb: 'GL Reports' }
}
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class GlReportsRoutingModule {}
@@ -1,523 +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="glr-root">
<!-- HEADER -->
<div class="glr-head">
<div class="glr-head-left">
<fa-icon icon="book-open" class="glr-head-ic"></fa-icon>
<div>
<h2 class="glr-head-t">General Ledger — {{ reportLabel }}</h2>
<div class="glr-head-s">
CBSNexora Bank Ltd &middot; {{ branchLabel }} &middot; Period <strong>{{ rangeLabel }}</strong> &middot;
{{ ledgerPeriod.label }}
</div>
</div>
</div>
<button class="glr-btn glr-btn-xls" [disabled]="generating" (click)="exportExcel()">
<fa-icon icon="file-csv"></fa-icon>
{{ generating ? 'Exporting…' : 'Export to Excel' }}
</button>
</div>
<!-- FILTER BAR -->
<div class="glr-filters">
<div class="glr-filter">
<label>Period</label>
<div class="glr-seg">
<button [class.active]="periodMode === 'day'" (click)="periodMode = 'day'">Day-wise</button>
<button [class.active]="periodMode === 'month'" (click)="periodMode = 'month'">Month-wise</button>
<button [class.active]="periodMode === 'range'" (click)="periodMode = 'range'">Date Range</button>
</div>
</div>
@if (periodMode === 'day') {
<div class="glr-filter">
<label>Date</label>
<input
class="glr-inp"
type="date"
[(ngModel)]="filterDay"
[min]="ledgerPeriod.fyStart"
[max]="ledgerPeriod.fyEnd"
/>
</div>
}
@if (periodMode === 'month') {
<div class="glr-filter">
<label>Month</label>
<input class="glr-inp" type="month" [(ngModel)]="filterMonth" min="2026-04" max="2026-07" />
</div>
}
@if (periodMode === 'range') {
<div class="glr-filter">
<label>From</label>
<input
class="glr-inp"
type="date"
[(ngModel)]="filterFrom"
[min]="ledgerPeriod.fyStart"
[max]="ledgerPeriod.fyEnd"
/>
</div>
<div class="glr-filter">
<label>To</label>
<input
class="glr-inp"
type="date"
[(ngModel)]="filterTo"
[min]="ledgerPeriod.fyStart"
[max]="ledgerPeriod.fyEnd"
/>
</div>
}
<div class="glr-filter">
<label>Branch</label>
<select class="glr-inp" [(ngModel)]="branchCode">
<option value="">All Branches (Consolidated)</option>
@for (b of branches; track b.code) {
<option [value]="b.code">{{ b.code }} — {{ b.name }}</option>
}
</select>
</div>
<button class="glr-btn" (click)="resetFilters()">Reset</button>
</div>
<!-- REPORT SELECTOR -->
<div class="glr-tabs">
@for (r of reports; track r.code) {
<button class="glr-tab" [class.active]="activeReport === r.code" (click)="showReport(r.code)">
<fa-icon [icon]="r.icon"></fa-icon> {{ r.label }}
</button>
}
</div>
<!-- ══════════════ TRIAL BALANCE ══════════════ -->
@if (activeReport === 'trial-balance') {
<div class="glr-section">
<div class="glr-balance" [class.glr-balance--bad]="!tbBalanced">
<fa-icon [icon]="tbBalanced ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
Closing Dr <strong>&#8377;{{ tbClosingDebit | number: '1.2-2' }}</strong> &middot; Closing Cr
<strong>&#8377;{{ tbClosingCredit | number: '1.2-2' }}</strong> —
{{ tbBalanced ? 'trial balance tallies' : 'OUT OF BALANCE — investigate before closing the period' }}
</span>
</div>
<div class="glr-tbl-wrap">
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Account</th>
<th>Group</th>
<th class="ta-r">Opening Dr</th>
<th class="ta-r">Opening Cr</th>
<th class="ta-r">Period Dr</th>
<th class="ta-r">Period Cr</th>
<th class="ta-r">Closing Dr</th>
<th class="ta-r">Closing Cr</th>
</tr>
</thead>
<tbody>
@for (r of data.trialBalance; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}</td>
<td class="muted">{{ r.group }}</td>
<td class="ta-r mono">{{ r.openingDebit ? (r.openingDebit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono">{{ r.openingCredit ? (r.openingCredit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono">{{ r.periodDebit ? (r.periodDebit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono">{{ r.periodCredit ? (r.periodCredit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono">{{ r.closingDebit ? (r.closingDebit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono">{{ r.closingCredit ? (r.closingCredit | number: '1.2-2') : '' }}</td>
</tr>
} @empty {
<tr>
<td colspan="9" class="glr-empty">No ledger activity for this period and branch.</td>
</tr>
}
<tr class="total-row">
<td colspan="3">TOTAL ({{ data.trialBalance.length }} accounts)</td>
<td class="ta-r mono">{{ tbOpeningDebit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ tbOpeningCredit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ tbPeriodDebit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ tbPeriodCredit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ tbClosingDebit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ tbClosingCredit | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
}
<!-- ══════════════ PROFIT AND LOSS ══════════════ -->
@if (activeReport === 'profit-and-loss') {
<div class="glr-section">
<div class="glr-kpis">
<div class="glr-kpi glr-kpi--teal">
<div class="glr-kpi-n">&#8377;{{ totalIncome | number: '1.2-2' }}</div>
<div class="glr-kpi-l">Total Income</div>
</div>
<div class="glr-kpi glr-kpi--amber">
<div class="glr-kpi-n">&#8377;{{ totalExpense | number: '1.2-2' }}</div>
<div class="glr-kpi-l">Total Expenditure</div>
</div>
<div class="glr-kpi" [class.glr-kpi--green]="netProfit >= 0" [class.glr-kpi--red]="netProfit < 0">
<div class="glr-kpi-n">&#8377;{{ netProfit | number: '1.2-2' }}</div>
<div class="glr-kpi-l">{{ netProfit >= 0 ? 'Net Profit' : 'Net Loss' }}</div>
<div class="glr-kpi-sub">{{ netProfitPct | number: '1.2-2' }}% of income</div>
</div>
</div>
<div class="glr-two-col">
<div class="glr-tbl-wrap">
<div class="glr-tbl-h">Expenditure</div>
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Particulars</th>
<th class="ta-r">Amount (&#8377;)</th>
</tr>
</thead>
<tbody>
@for (r of pnlExpense; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="3" class="glr-empty">No expenditure posted in this period.</td>
</tr>
}
<tr class="total-row">
<td colspan="2">Total Expenditure</td>
<td class="ta-r mono">{{ totalExpense | number: '1.2-2' }}</td>
</tr>
@if (netProfit >= 0) {
<tr class="total-row glr-profit-row">
<td colspan="2">Net Profit carried to Balance Sheet</td>
<td class="ta-r mono">{{ netProfit | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
</div>
<div class="glr-tbl-wrap">
<div class="glr-tbl-h">Income</div>
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Particulars</th>
<th class="ta-r">Amount (&#8377;)</th>
</tr>
</thead>
<tbody>
@for (r of pnlIncome; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="3" class="glr-empty">No income posted in this period.</td>
</tr>
}
<tr class="total-row">
<td colspan="2">Total Income</td>
<td class="ta-r mono">{{ totalIncome | number: '1.2-2' }}</td>
</tr>
@if (netProfit < 0) {
<tr class="total-row glr-loss-row">
<td colspan="2">Net Loss carried to Balance Sheet</td>
<td class="ta-r mono">{{ -netProfit | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
<!-- ══════════════ CONSOLIDATED BALANCE SHEET ══════════════ -->
@if (activeReport === 'balance-sheet') {
<div class="glr-section">
<div class="glr-balance" [class.glr-balance--bad]="!bsBalanced">
<fa-icon [icon]="bsBalanced ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
Total Assets <strong>&#8377;{{ bsTotalAssets | number: '1.2-2' }}</strong> &middot; Total Liabilities
<strong>&#8377;{{ bsTotalLiabilities | number: '1.2-2' }}</strong> —
{{ bsBalanced ? 'balance sheet tallies' : 'OUT OF BALANCE' }}
</span>
</div>
<div class="glr-two-col">
<div class="glr-tbl-wrap">
<div class="glr-tbl-h">Capital &amp; Liabilities &mdash; as at {{ range.to }}</div>
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Particulars</th>
<th class="ta-r">Amount (&#8377;)</th>
</tr>
</thead>
<tbody>
@for (r of bsLiabilities; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
</tr>
}
<tr>
<td class="mono">—</td>
<td>Surplus for the period<span class="muted"> · Profit &amp; Loss</span></td>
<td class="ta-r mono">{{ bsSurplus | number: '1.2-2' }}</td>
</tr>
<tr class="total-row">
<td colspan="2">Total Capital &amp; Liabilities</td>
<td class="ta-r mono">{{ bsTotalLiabilities | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
<div class="glr-tbl-wrap">
<div class="glr-tbl-h">Assets &mdash; as at {{ range.to }}</div>
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Particulars</th>
<th class="ta-r">Amount (&#8377;)</th>
</tr>
</thead>
<tbody>
@for (r of bsAssets; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
</tr>
}
<tr class="total-row">
<td colspan="2">Total Assets</td>
<td class="ta-r mono">{{ bsTotalAssets | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
}
<!-- ══════════════ COMPARATIVE BALANCE SHEET ══════════════ -->
@if (activeReport === 'comparative-balance-sheet') {
<div class="glr-section">
<p class="glr-note">
Compares the closing position as at <strong>{{ range.to }}</strong> against the position immediately before
this period began. Change the filter above to compare a different span.
</p>
@for (block of [{ title: 'Capital & Liabilities', rows: compLiabilities }, { title: 'Assets', rows: compAssets }]; track block.title) {
<div class="glr-tbl-wrap">
<div class="glr-tbl-h">{{ block.title }}</div>
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Particulars</th>
<th class="ta-r">Current (&#8377;)</th>
<th class="ta-r">Previous (&#8377;)</th>
<th class="ta-r">Variance (&#8377;)</th>
<th class="ta-r">Variance %</th>
</tr>
</thead>
<tbody>
@for (r of block.rows; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}<span class="muted"> · {{ r.group }}</span></td>
<td class="ta-r mono">{{ r.current | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ r.previous | number: '1.2-2' }}</td>
<td class="ta-r mono" [class.var-up]="r.variance > 0" [class.var-down]="r.variance < 0">
{{ r.variance | number: '1.2-2' }}
</td>
<td class="ta-r mono" [class.var-up]="r.variance > 0" [class.var-down]="r.variance < 0">
{{ r.variancePct === null ? '—' : (r.variancePct | number: '1.2-2') + '%' }}
</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="glr-empty">Nothing to compare for this period and branch.</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
}
<!-- ══════════════ CONTINGENT LIABILITIES ══════════════ -->
@if (activeReport === 'contingent-liabilities') {
<div class="glr-section">
<p class="glr-note">
Off-balance-sheet commitments outstanding as at <strong>{{ range.to }}</strong>. These are memoranda accounts
and are deliberately excluded from the Trial Balance and Balance Sheet totals.
</p>
<div class="glr-tbl-wrap">
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Particulars</th>
<th>Group</th>
<th class="ta-r">Amount (&#8377;)</th>
</tr>
</thead>
<tbody>
@for (r of data.contingent; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}</td>
<td class="muted">{{ r.group }}</td>
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="4" class="glr-empty">No contingent exposures outstanding for this selection.</td>
</tr>
}
<tr class="total-row">
<td colspan="3">Total Contingent Liabilities</td>
<td class="ta-r mono">{{ contingentTotal | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
}
<!-- ══════════════ GL WISE DR / CR ══════════════ -->
@if (activeReport === 'gl-wise-dr-cr') {
<div class="glr-section">
<p class="glr-note">
Debit and credit turnover posted between <strong>{{ rangeLabel }}</strong> for {{ branchLabel }}. Every
voucher is double-entry, so the debit and credit totals below must match.
</p>
<div class="glr-tbl-wrap">
<table class="glr-tbl">
<thead>
<tr>
<th>GL Code</th>
<th>Account</th>
<th>Type</th>
<th>Group</th>
<th class="ta-r">Debit (&#8377;)</th>
<th class="ta-r">Credit (&#8377;)</th>
<th class="ta-r">Net (Dr &minus; Cr)</th>
<th class="ta-c">Vouchers</th>
</tr>
</thead>
<tbody>
@for (r of data.glWise; track r.code) {
<tr>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}</td>
<td>
<span class="type-pill" [class]="'type-' + r.type.toLowerCase()">{{ r.type }}</span>
</td>
<td class="muted">{{ r.group }}</td>
<td class="ta-r mono">{{ r.debit ? (r.debit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono">{{ r.credit ? (r.credit | number: '1.2-2') : '' }}</td>
<td class="ta-r mono" [class.var-up]="r.net > 0" [class.var-down]="r.net < 0">
{{ r.net | number: '1.2-2' }}
</td>
<td class="ta-c mono">{{ r.vouchers }}</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="glr-empty">No postings for this period and branch.</td>
</tr>
}
<tr class="total-row">
<td colspan="4">TOTAL ({{ data.glWise.length }} accounts)</td>
<td class="ta-r mono">{{ glWiseDebit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ glWiseCredit | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ glWiseDebit - glWiseCredit | number: '1.2-2' }}</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
}
<!-- ══════════════ MISC EXPENSE LEDGER ══════════════ -->
@if (activeReport === 'misc-expense-ledger') {
<div class="glr-section">
<p class="glr-note">
Every miscellaneous-expense voucher posted between <strong>{{ rangeLabel }}</strong> for {{ branchLabel }}
&mdash; {{ miscRows.length | number }} entries.
</p>
<div class="glr-tbl-wrap glr-tbl-wrap--tall">
<table class="glr-tbl">
<thead>
<tr>
<th>Date</th>
<th>Voucher No</th>
<th>Branch</th>
<th>GL Code</th>
<th>Account</th>
<th>Narration</th>
<th class="ta-r">Amount (&#8377;)</th>
</tr>
</thead>
<tbody>
@for (r of miscRows; track r.voucherNo + r.code) {
<tr>
<td class="mono">{{ r.date }}</td>
<td class="mono">{{ r.voucherNo }}</td>
<td>{{ branchName(r.branchCode) }}</td>
<td class="mono">{{ r.code }}</td>
<td>{{ r.name }}</td>
<td class="muted">{{ r.narration }}</td>
<td class="ta-r mono">{{ r.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="7" class="glr-empty">No miscellaneous expenses posted for this selection.</td>
</tr>
}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="6">Total Miscellaneous Expenses</td>
<td class="ta-r mono">{{ miscTotal | number: '1.2-2' }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
}
<!-- TOAST -->
<div class="glr-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
@@ -1,506 +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/.
*/
:host {
--g-navy: #1a237e;
--g-blue: #1d4ed8;
--g-blue-l: #eff6ff;
--g-teal: #0e7e6b;
--g-teal-l: #e0f4f0;
--g-green: #15803d;
--g-green-l: #dcfce7;
--g-amber: #b45309;
--g-amber-l: #fef3c7;
--g-red: #c0392b;
--g-red-l: #fdecea;
--g-g50: #f8fafc;
--g-g100: #f1f5f9;
--g-g200: #e2e8f0;
--g-g300: #cbd5e1;
--g-g500: #64748b;
--g-g700: #334155;
--g-g900: #0f172a;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
background: var(--g-g50);
color: var(--g-g900);
font-size: 14px;
line-height: 1.5;
}
.glr-root {
padding: 18px 22px;
max-width: 1700px;
margin: 0 auto;
}
/* HEADER */
.glr-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.glr-head-left {
display: flex;
align-items: center;
gap: 14px;
}
.glr-head-ic {
font-size: 28px;
color: var(--g-navy);
}
.glr-head-t {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.glr-head-s {
font-size: 12px;
color: var(--g-g500);
}
/* BUTTONS */
.glr-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 16px;
border: 1px solid var(--g-g300);
background: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
color: var(--g-g700);
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
white-space: nowrap;
&:hover:not(:disabled) {
background: var(--g-g50);
border-color: var(--g-g500);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.glr-btn-xls {
background: #16a34a;
border-color: #16a34a;
color: #fff;
&:hover:not(:disabled) {
background: #15803d;
border-color: #15803d;
color: #fff;
}
}
/* FILTER BAR */
.glr-filters {
background: #fff;
border: 1px solid var(--g-g200);
border-radius: 12px;
padding: 14px;
display: flex;
align-items: flex-end;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.glr-filter {
display: flex;
flex-direction: column;
gap: 5px;
label {
font-size: 11px;
font-weight: 700;
color: var(--g-g500);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
.glr-inp {
padding: 8px 12px;
border: 1px solid var(--g-g200);
border-radius: 8px;
font-size: 13px;
background: #fff;
font-family: inherit;
min-width: 190px;
&:focus {
outline: none;
border-color: var(--g-navy);
box-shadow: 0 0 0 3px rgb(26 35 126 / 12%);
}
}
/* Day / Month / Range segmented control */
.glr-seg {
display: inline-flex;
border: 1px solid var(--g-g200);
border-radius: 8px;
overflow: hidden;
button {
padding: 8px 14px;
border: none;
background: #fff;
font-size: 13px;
font-weight: 600;
color: var(--g-g500);
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
+ button {
border-left: 1px solid var(--g-g200);
}
&:hover {
background: var(--g-g50);
}
&.active {
background: var(--g-navy);
color: #fff;
}
}
}
/* REPORT SELECTOR */
.glr-tabs {
display: flex;
gap: 4px;
background: #fff;
border: 1px solid var(--g-g200);
border-radius: 10px;
padding: 4px;
margin-bottom: 16px;
overflow-x: auto;
}
.glr-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 500;
color: var(--g-g500);
cursor: pointer;
border-radius: 7px;
white-space: nowrap;
transition: all 0.15s;
font-family: inherit;
&:hover {
color: var(--g-g900);
}
&.active {
background: var(--g-navy);
color: #fff;
box-shadow: 0 2px 6px rgb(26 35 126 / 25%);
}
}
.glr-section {
display: flex;
flex-direction: column;
gap: 16px;
}
/* BALANCE BANNER */
.glr-balance {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-radius: 10px;
background: var(--g-green-l);
border: 1px solid #86efac;
color: #14532d;
font-size: 13px;
&--bad {
background: var(--g-red-l);
border-color: #fca5a5;
color: #7f1d1d;
}
}
.glr-note {
margin: 0;
font-size: 12px;
color: var(--g-g500);
}
/* KPIs */
.glr-kpis {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.glr-kpi {
background: #fff;
border: 1px solid var(--g-g200);
border-left: 4px solid var(--g-g300);
border-radius: 12px;
padding: 14px 18px;
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
&--teal {
border-left-color: var(--g-teal);
}
&--amber {
border-left-color: var(--g-amber);
}
&--green {
border-left-color: var(--g-green);
}
&--red {
border-left-color: var(--g-red);
}
}
.glr-kpi-n {
font-size: 20px;
font-weight: 700;
font-family: 'DM Mono', monospace;
color: var(--g-g900);
}
.glr-kpi-l {
font-size: 12px;
color: var(--g-g700);
font-weight: 600;
margin-top: 2px;
}
.glr-kpi-sub {
font-size: 11px;
color: var(--g-g500);
}
/* TABLES */
.glr-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: start;
}
.glr-tbl-wrap {
background: #fff;
border: 1px solid var(--g-g200);
border-radius: 12px;
overflow: auto;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
&--tall {
max-height: 70vh;
}
}
.glr-tbl-h {
background: var(--g-blue-l);
color: var(--g-navy);
padding: 10px 16px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1px solid var(--g-g200);
position: sticky;
top: 0;
z-index: 2;
}
.glr-tbl {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
padding: 9px 12px;
font-size: 11px;
font-weight: 700;
color: var(--g-g500);
text-transform: uppercase;
letter-spacing: 0.04em;
background: var(--g-g50);
border-bottom: 1px solid var(--g-g200);
white-space: nowrap;
}
td {
padding: 8px 12px;
border-bottom: 1px solid var(--g-g100);
font-size: 13px;
color: var(--g-g700);
}
tbody tr:hover td {
background: var(--g-g50);
}
tfoot td {
position: sticky;
bottom: 0;
}
}
.total-row td {
background: var(--g-g100) !important;
font-weight: 700;
color: var(--g-g900);
border-top: 2px solid var(--g-g300);
}
.glr-profit-row td {
background: var(--g-green-l) !important;
color: #14532d;
}
.glr-loss-row td {
background: var(--g-red-l) !important;
color: #7f1d1d;
}
.ta-c {
text-align: center;
}
.ta-r {
text-align: right;
}
.mono {
font-family: 'DM Mono', monospace;
font-size: 12px;
}
.muted {
color: var(--g-g500);
font-size: 11px;
}
.var-up {
color: var(--g-green);
font-weight: 600;
}
.var-down {
color: var(--g-red);
font-weight: 600;
}
.glr-empty {
text-align: center;
padding: 34px;
color: var(--g-g500);
font-style: italic;
}
/* Account-type pills on the GL Wise report */
.type-pill {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.03em;
background: var(--g-g100);
color: var(--g-g700);
&.type-asset {
background: var(--g-blue-l);
color: var(--g-blue);
}
&.type-liability {
background: var(--g-teal-l);
color: var(--g-teal);
}
&.type-income {
background: var(--g-green-l);
color: var(--g-green);
}
&.type-expense {
background: var(--g-amber-l);
color: var(--g-amber);
}
&.type-contingent {
background: var(--g-red-l);
color: var(--g-red);
}
}
/* TOAST */
.glr-toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--g-navy);
color: #fff;
padding: 12px 22px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 6px 20px rgb(0 0 0 / 25%);
opacity: 0;
pointer-events: none;
transition: all 0.25s;
z-index: 1500;
&.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@media (width <= 1200px) {
.glr-two-col {
grid-template-columns: 1fr;
}
.glr-kpis {
grid-template-columns: 1fr;
}
}
-470
View File
@@ -1,470 +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, OnInit, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import * as ExcelJS from 'exceljs';
import { GL_BRANCHES, GL_PERIOD, glBranchName } from './gl-reports-data';
import {
BalanceSheetRow,
ComparativeRow,
GlDateRange,
GlReportSet,
MiscExpenseRow,
PnlRow,
buildReports,
clampToLedger,
sum
} from './gl-reports.compute';
export type GlReportCode =
| 'trial-balance'
| 'profit-and-loss'
| 'balance-sheet'
| 'comparative-balance-sheet'
| 'contingent-liabilities'
| 'gl-wise-dr-cr'
| 'misc-expense-ledger';
type PeriodMode = 'day' | 'month' | 'range';
@Component({
selector: 'mifosx-gl-reports',
templateUrl: './gl-reports.component.html',
styleUrls: ['./gl-reports.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class GlReportsComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
readonly reports: { code: GlReportCode; label: string; icon: string }[] = [
{ code: 'trial-balance', label: 'Trial Balance', icon: 'balance-scale' },
{ code: 'profit-and-loss', label: 'Profit and Loss', icon: 'chart-line' },
{ code: 'balance-sheet', label: 'Consolidated Balance Sheet', icon: 'file-alt' },
{ code: 'comparative-balance-sheet', label: 'Comparative Balance Sheet', icon: 'balance-scale' },
{ code: 'contingent-liabilities', label: 'Contingent Liabilities', icon: 'exclamation-circle' },
{ code: 'gl-wise-dr-cr', label: 'GL Wise DR / CR', icon: 'project-diagram' },
{ code: 'misc-expense-ledger', label: 'Misc Expense Ledger', icon: 'receipt' }
];
activeReport: GlReportCode = 'trial-balance';
branches = GL_BRANCHES;
ledgerPeriod = GL_PERIOD;
// ── Filter state ───────────────────────────────────────────────────────────
periodMode: PeriodMode = 'month';
/** Last full month of the generated ledger. */
filterDay = '2026-07-31';
filterMonth = '2026-07';
filterFrom = GL_PERIOD.fyStart;
filterTo = GL_PERIOD.fyEnd;
branchCode = '';
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: any;
ngOnInit(): void {
const param = this.route.snapshot.paramMap.get('report') as GlReportCode | null;
if (param && this.reports.some((r) => r.code === param)) {
this.activeReport = param;
}
}
showReport(code: GlReportCode): void {
this.activeReport = code;
// Keep the URL deep-linkable so the GL menu items land on the right report.
this.router.navigate(['/gl/reports', code], { replaceUrl: true });
}
get reportLabel(): string {
return this.reports.find((r) => r.code === this.activeReport)?.label ?? '';
}
// ── Filter resolution ──────────────────────────────────────────────────────
/** Turns the filter controls into the concrete date range the reports use. */
get range(): GlDateRange {
if (this.periodMode === 'day') {
return clampToLedger({ from: this.filterDay, to: this.filterDay });
}
if (this.periodMode === 'month') {
const [
y,
m
] = this.filterMonth.split('-').map(Number);
const last = new Date(Date.UTC(y, m, 0)).getUTCDate();
return clampToLedger({ from: `${this.filterMonth}-01`, to: `${this.filterMonth}-${`${last}`.padStart(2, '0')}` });
}
return clampToLedger({ from: this.filterFrom, to: this.filterTo });
}
get rangeLabel(): string {
const r = this.range;
return r.from === r.to ? r.from : `${r.from} to ${r.to}`;
}
get branchLabel(): string {
return this.branchCode ? glBranchName(this.branchCode) : 'All Branches (Consolidated)';
}
resetFilters(): void {
this.periodMode = 'month';
this.filterMonth = '2026-07';
this.filterDay = '2026-07-31';
this.filterFrom = GL_PERIOD.fyStart;
this.filterTo = GL_PERIOD.fyEnd;
this.branchCode = '';
}
/** All seven reports for the current filter — memoised inside buildReports. */
get data(): GlReportSet {
return buildReports(this.range, this.branchCode);
}
// ── Trial Balance ──────────────────────────────────────────────────────────
get tbOpeningDebit(): number {
return sum(this.data.trialBalance, (r) => r.openingDebit);
}
get tbOpeningCredit(): number {
return sum(this.data.trialBalance, (r) => r.openingCredit);
}
get tbPeriodDebit(): number {
return sum(this.data.trialBalance, (r) => r.periodDebit);
}
get tbPeriodCredit(): number {
return sum(this.data.trialBalance, (r) => r.periodCredit);
}
get tbClosingDebit(): number {
return sum(this.data.trialBalance, (r) => r.closingDebit);
}
get tbClosingCredit(): number {
return sum(this.data.trialBalance, (r) => r.closingCredit);
}
/** A trial balance that does not tally is the single most important thing to surface. */
get tbBalanced(): boolean {
return Math.round(this.tbClosingDebit) === Math.round(this.tbClosingCredit);
}
// ── Profit and Loss ────────────────────────────────────────────────────────
get pnlIncome(): PnlRow[] {
return this.data.pnl.income;
}
get pnlExpense(): PnlRow[] {
return this.data.pnl.expense;
}
get totalIncome(): number {
return sum(this.pnlIncome, (r) => r.amount);
}
get totalExpense(): number {
return sum(this.pnlExpense, (r) => r.amount);
}
get netProfit(): number {
return this.totalIncome - this.totalExpense;
}
get netProfitPct(): number {
return this.totalIncome ? (this.netProfit / this.totalIncome) * 100 : 0;
}
// ── Balance Sheet ──────────────────────────────────────────────────────────
get bsAssets(): BalanceSheetRow[] {
return this.data.balanceSheet.assets;
}
get bsLiabilities(): BalanceSheetRow[] {
return this.data.balanceSheet.liabilities;
}
get bsSurplus(): number {
return this.data.balanceSheet.surplus;
}
get bsTotalAssets(): number {
return sum(this.bsAssets, (r) => r.amount);
}
get bsTotalLiabilities(): number {
return sum(this.bsLiabilities, (r) => r.amount) + this.bsSurplus;
}
get bsBalanced(): boolean {
return Math.round(this.bsTotalAssets) === Math.round(this.bsTotalLiabilities);
}
// ── Comparative Balance Sheet ──────────────────────────────────────────────
get compAssets(): ComparativeRow[] {
return this.data.comparative.filter((r) => r.side === 'ASSET');
}
get compLiabilities(): ComparativeRow[] {
return this.data.comparative.filter((r) => r.side === 'LIABILITY');
}
// ── Contingent Liabilities ─────────────────────────────────────────────────
get contingentTotal(): number {
return sum(this.data.contingent, (r) => r.amount);
}
// ── GL Wise DR/CR ──────────────────────────────────────────────────────────
get glWiseDebit(): number {
return sum(this.data.glWise, (r) => r.debit);
}
get glWiseCredit(): number {
return sum(this.data.glWise, (r) => r.credit);
}
// ── Misc Expense Ledger ────────────────────────────────────────────────────
get miscRows(): MiscExpenseRow[] {
return this.data.miscExpense;
}
get miscTotal(): number {
return sum(this.miscRows, (r) => r.amount);
}
branchName(code: string): string {
return glBranchName(code);
}
// ── Excel export ───────────────────────────────────────────────────────────
/** Exports whichever report is on screen, carrying the active filter into the header. */
async exportExcel(): Promise<void> {
this.generating = true;
try {
const wb = new ExcelJS.Workbook();
wb.creator = 'CBSNexora';
wb.created = new Date();
const sheet = wb.addWorksheet(this.reportLabel.slice(0, 31));
const { headers, rows, widths } = this.exportShape();
sheet.columns = widths.map((w) => ({ width: w }));
const title = sheet.addRow([this.reportLabel]);
sheet.mergeCells(title.number, 1, title.number, headers.length);
title.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } };
title.alignment = { horizontal: 'center', vertical: 'middle' };
title.height = 24;
title.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1A237E' } };
const meta = sheet.addRow([`Period: ${this.rangeLabel} · Branch: ${this.branchLabel}`]);
sheet.mergeCells(meta.number, 1, meta.number, headers.length);
meta.font = { italic: true, size: 10 };
sheet.addRow([]);
const head = sheet.addRow(headers);
head.font = { bold: true };
head.alignment = { wrapText: true, horizontal: 'center' };
head.eachCell((c) => {
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } };
c.border = { bottom: { style: 'thin' } };
});
for (const r of rows) {
const row = sheet.addRow(r);
for (let c = 1; c <= headers.length; c++) {
if (typeof r[c - 1] === 'number') {
row.getCell(c).numFmt = '#,##0.00';
}
}
}
sheet.views = [{ state: 'frozen', ySplit: 4 }];
const buffer = await wb.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${this.reportLabel.replace(/[^\w]+/g, '-')}-${this.range.from}-to-${this.range.to}.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.toast(`${this.reportLabel} exported for ${this.rangeLabel}`);
} catch {
this.toast('Could not generate the workbook — please retry');
} finally {
this.generating = false;
}
}
/** Header row, data rows and column widths for the active report. */
private exportShape(): { headers: string[]; rows: (string | number | null)[][]; widths: number[] } {
switch (this.activeReport) {
case 'trial-balance':
return {
headers: [
'GL Code',
'Account',
'Group',
'Opening Dr',
'Opening Cr',
'Period Dr',
'Period Cr',
'Closing Dr',
'Closing Cr'
],
widths: [12, 42, 26, 16, 16, 16, 16, 16, 16],
rows: [
...this.data.trialBalance.map((r) => [
r.code,
r.name,
r.group,
r.openingDebit,
r.openingCredit,
r.periodDebit,
r.periodCredit,
r.closingDebit,
r.closingCredit
]),
[
'TOTAL',
'',
'',
this.tbOpeningDebit,
this.tbOpeningCredit,
this.tbPeriodDebit,
this.tbPeriodCredit,
this.tbClosingDebit,
this.tbClosingCredit
]
]
};
case 'profit-and-loss':
return {
headers: [
'Side',
'GL Code',
'Account',
'Group',
'Amount'
],
widths: [14, 12, 42, 26, 18],
rows: [
...this.pnlExpense.map((r) => ['Expenditure', r.code, r.name, r.group, r.amount]),
['Total Expenditure', '', '', '', this.totalExpense],
...this.pnlIncome.map((r) => ['Income', r.code, r.name, r.group, r.amount]),
['Total Income', '', '', '', this.totalIncome],
['Net Profit', '', '', '', this.netProfit]
]
};
case 'balance-sheet':
return {
headers: [
'Side',
'GL Code',
'Account',
'Group',
'Amount'
],
widths: [14, 12, 42, 26, 18],
rows: [
...this.bsLiabilities.map((r) => ['Liability', r.code, r.name, r.group, r.amount]),
['Liability', '', 'Surplus for the period', 'Profit & Loss', this.bsSurplus],
['Total Liabilities', '', '', '', this.bsTotalLiabilities],
...this.bsAssets.map((r) => ['Asset', r.code, r.name, r.group, r.amount]),
['Total Assets', '', '', '', this.bsTotalAssets]
]
};
case 'comparative-balance-sheet':
return {
headers: [
'Side',
'GL Code',
'Account',
'Current',
'Previous',
'Variance',
'Variance %'
],
widths: [12, 12, 42, 18, 18, 18, 14],
rows: this.data.comparative.map((r) => [
r.side,
r.code,
r.name,
r.current,
r.previous,
r.variance,
r.variancePct === null ? '—' : Number(r.variancePct.toFixed(2))
])
};
case 'contingent-liabilities':
return {
headers: [
'GL Code',
'Particulars',
'Group',
'Amount'
],
widths: [12, 48, 26, 18],
rows: [
...this.data.contingent.map((r) => [r.code, r.name, r.group, r.amount]),
['TOTAL', '', '', this.contingentTotal]
]
};
case 'gl-wise-dr-cr':
return {
headers: [
'GL Code',
'Account',
'Type',
'Group',
'Debit',
'Credit',
'Net (Dr-Cr)',
'Vouchers'
],
widths: [12, 42, 14, 26, 18, 18, 18, 12],
rows: [
...this.data.glWise.map((r) => [r.code, r.name, r.type, r.group, r.debit, r.credit, r.net, r.vouchers]),
['TOTAL', '', '', '', this.glWiseDebit, this.glWiseCredit, this.glWiseDebit - this.glWiseCredit, null]
]
};
case 'misc-expense-ledger':
default:
return {
headers: [
'Date',
'Voucher No',
'Branch',
'GL Code',
'Account',
'Narration',
'Amount'
],
widths: [14, 20, 18, 12, 36, 46, 18],
rows: [
...this.miscRows.map((r) => [
r.date,
r.voucherNo,
this.branchName(r.branchCode),
r.code,
r.name,
r.narration,
r.amount
]),
['TOTAL', '', '', '', '', '', this.miscTotal]
]
};
}
}
private toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
}
}
-379
View File
@@ -1,379 +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/.
*/
/**
* Derives all seven General Ledger reports from the posting ledger.
*
* Each report is a pure function of (postings, date range, branch), so changing
* the day / month / branch filter genuinely recomputes the figures. Results are
* memoised per filter key because the ledger runs to roughly twenty thousand
* postings and Angular re-evaluates template getters on every change detection.
*/
import {
GL_ACCOUNTS,
GL_PERIOD,
GlAccount,
GlPosting,
glAccount,
glPostings
} from './gl-reports-data';
export interface GlDateRange {
from: string;
to: string;
}
export interface TrialBalanceRow {
code: string;
name: string;
group: string;
openingDebit: number;
openingCredit: number;
periodDebit: number;
periodCredit: number;
closingDebit: number;
closingCredit: number;
}
export interface PnlRow {
code: string;
name: string;
group: string;
amount: number;
}
export interface BalanceSheetRow {
code: string;
name: string;
group: string;
amount: number;
}
export interface ComparativeRow {
code: string;
name: string;
group: string;
side: 'ASSET' | 'LIABILITY';
current: number;
previous: number;
variance: number;
variancePct: number | null;
}
export interface GlWiseRow {
code: string;
name: string;
group: string;
type: string;
debit: number;
credit: number;
net: number;
vouchers: number;
}
export interface MiscExpenseRow {
date: string;
voucherNo: string;
branchCode: string;
code: string;
name: string;
narration: string;
amount: number;
}
/** Natural side of an account — assets and expenses sit on the debit side. */
function isDebitSide(account: GlAccount): boolean {
return account.type === 'ASSET' || account.type === 'EXPENSE' || account.type === 'CONTINGENT';
}
function matchesBranch(p: GlPosting, branchCode: string): boolean {
return !branchCode || p.branchCode === branchCode;
}
/** Signed balance on the account's natural side, for postings up to and including `to`. */
function cumulativeBalances(to: string, branchCode: string): Map<string, number> {
const out = new Map<string, number>();
for (const p of glPostings()) {
if (p.date > to || !matchesBranch(p, branchCode)) {
continue;
}
const account = glAccount(p.accountCode);
if (!account) {
continue;
}
const delta = isDebitSide(account) ? p.debit - p.credit : p.credit - p.debit;
out.set(p.accountCode, (out.get(p.accountCode) ?? 0) + delta);
}
return out;
}
/** Debit and credit turnover strictly inside the range. */
function periodTurnover(range: GlDateRange, branchCode: string): Map<string, { debit: number; credit: number; vouchers: Set<string> }> {
const out = new Map<string, { debit: number; credit: number; vouchers: Set<string> }>();
for (const p of glPostings()) {
if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) {
continue;
}
let entry = out.get(p.accountCode);
if (!entry) {
entry = { debit: 0, credit: 0, vouchers: new Set<string>() };
out.set(p.accountCode, entry);
}
entry.debit += p.debit;
entry.credit += p.credit;
entry.vouchers.add(p.voucherNo);
}
return out;
}
/** The day before `date`, ISO formatted — used for opening balances. */
function previousDay(date: string): string {
const d = new Date(`${date}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() - 1);
return d.toISOString().slice(0, 10);
}
// ── Reports ──────────────────────────────────────────────────────────────────
export function trialBalance(range: GlDateRange, branchCode: string): TrialBalanceRow[] {
const opening = cumulativeBalances(previousDay(range.from), branchCode);
const turnover = periodTurnover(range, branchCode);
const rows: TrialBalanceRow[] = [];
for (const account of GL_ACCOUNTS) {
// Contingent accounts are memoranda and never appear in the trial balance.
if (account.type === 'CONTINGENT') {
continue;
}
const open = opening.get(account.code) ?? 0;
const move = turnover.get(account.code);
const periodDebit = move?.debit ?? 0;
const periodCredit = move?.credit ?? 0;
if (!open && !periodDebit && !periodCredit) {
continue;
}
const debitSide = isDebitSide(account);
const closingNatural = open + (debitSide ? periodDebit - periodCredit : periodCredit - periodDebit);
const closingSigned = debitSide ? closingNatural : -closingNatural;
rows.push({
code: account.code,
name: account.name,
group: account.group,
openingDebit: debitSide && open > 0 ? open : !debitSide && open < 0 ? -open : 0,
openingCredit: !debitSide && open > 0 ? open : debitSide && open < 0 ? -open : 0,
periodDebit,
periodCredit,
closingDebit: closingSigned > 0 ? closingSigned : 0,
closingCredit: closingSigned < 0 ? -closingSigned : 0
});
}
return rows;
}
export function profitAndLoss(range: GlDateRange, branchCode: string): { income: PnlRow[]; expense: PnlRow[] } {
const turnover = periodTurnover(range, branchCode);
const income: PnlRow[] = [];
const expense: PnlRow[] = [];
for (const account of GL_ACCOUNTS) {
if (account.type !== 'INCOME' && account.type !== 'EXPENSE') {
continue;
}
const move = turnover.get(account.code);
if (!move) {
continue;
}
const amount =
account.type === 'INCOME' ? move.credit - move.debit : move.debit - move.credit;
if (!amount) {
continue;
}
const row: PnlRow = { code: account.code, name: account.name, group: account.group, amount };
(account.type === 'INCOME' ? income : expense).push(row);
}
return { income, expense };
}
/**
* Balance sheet as at the period end. The current-period surplus is folded in as
* a liability line so the two sides tally the way the printed statement does.
*/
export function balanceSheet(range: GlDateRange, branchCode: string): {
assets: BalanceSheetRow[];
liabilities: BalanceSheetRow[];
surplus: number;
} {
const closing = cumulativeBalances(range.to, branchCode);
const assets: BalanceSheetRow[] = [];
const liabilities: BalanceSheetRow[] = [];
let income = 0;
let expense = 0;
for (const account of GL_ACCOUNTS) {
const bal = closing.get(account.code) ?? 0;
if (account.type === 'INCOME') {
income += bal;
continue;
}
if (account.type === 'EXPENSE') {
expense += bal;
continue;
}
if (account.type === 'CONTINGENT' || !bal) {
continue;
}
const row: BalanceSheetRow = { code: account.code, name: account.name, group: account.group, amount: bal };
(account.type === 'ASSET' ? assets : liabilities).push(row);
}
return { assets, liabilities, surplus: income - expense };
}
/** Current period against the equivalent span immediately before it. */
export function comparativeBalanceSheet(range: GlDateRange, branchCode: string): ComparativeRow[] {
const priorTo = previousDay(range.from);
const current = cumulativeBalances(range.to, branchCode);
const previous = cumulativeBalances(priorTo, branchCode);
const rows: ComparativeRow[] = [];
for (const account of GL_ACCOUNTS) {
if (account.type !== 'ASSET' && account.type !== 'LIABILITY') {
continue;
}
const cur = current.get(account.code) ?? 0;
const prev = previous.get(account.code) ?? 0;
if (!cur && !prev) {
continue;
}
const variance = cur - prev;
rows.push({
code: account.code,
name: account.name,
group: account.group,
side: account.type,
current: cur,
previous: prev,
variance,
variancePct: prev ? (variance / Math.abs(prev)) * 100 : null
});
}
return rows;
}
export function contingentLiabilities(range: GlDateRange, branchCode: string): BalanceSheetRow[] {
const closing = cumulativeBalances(range.to, branchCode);
return GL_ACCOUNTS.filter((a) => a.type === 'CONTINGENT')
.map((a) => ({ code: a.code, name: a.name, group: a.group, amount: closing.get(a.code) ?? 0 }))
.filter((r) => r.amount !== 0);
}
export function glWiseDrCr(range: GlDateRange, branchCode: string): GlWiseRow[] {
const turnover = periodTurnover(range, branchCode);
const rows: GlWiseRow[] = [];
for (const account of GL_ACCOUNTS) {
const move = turnover.get(account.code);
if (!move || (!move.debit && !move.credit)) {
continue;
}
rows.push({
code: account.code,
name: account.name,
group: account.group,
type: account.type,
debit: move.debit,
credit: move.credit,
net: move.debit - move.credit,
vouchers: move.vouchers.size
});
}
return rows;
}
export function miscExpenseLedger(range: GlDateRange, branchCode: string): MiscExpenseRow[] {
const miscCodes = new Set(GL_ACCOUNTS.filter((a) => a.miscExpense).map((a) => a.code));
const rows: MiscExpenseRow[] = [];
for (const p of glPostings()) {
if (p.date < range.from || p.date > range.to || !matchesBranch(p, branchCode)) {
continue;
}
if (!miscCodes.has(p.accountCode) || !p.debit) {
continue;
}
rows.push({
date: p.date,
voucherNo: p.voucherNo,
branchCode: p.branchCode,
code: p.accountCode,
name: glAccount(p.accountCode)?.name ?? p.accountCode,
narration: p.narration,
amount: p.debit
});
}
return rows.sort((a, b) => (a.date === b.date ? a.voucherNo.localeCompare(b.voucherNo) : a.date.localeCompare(b.date)));
}
// ── Memoisation ──────────────────────────────────────────────────────────────
export interface GlReportSet {
trialBalance: TrialBalanceRow[];
pnl: { income: PnlRow[]; expense: PnlRow[] };
balanceSheet: { assets: BalanceSheetRow[]; liabilities: BalanceSheetRow[]; surplus: number };
comparative: ComparativeRow[];
contingent: BalanceSheetRow[];
glWise: GlWiseRow[];
miscExpense: MiscExpenseRow[];
}
let cacheKey = '';
let cached: GlReportSet | null = null;
/** All seven reports for one filter selection, computed once per selection. */
export function buildReports(range: GlDateRange, branchCode: string): GlReportSet {
const key = `${range.from}|${range.to}|${branchCode}`;
if (cached && cacheKey === key) {
return cached;
}
cached = {
trialBalance: trialBalance(range, branchCode),
pnl: profitAndLoss(range, branchCode),
balanceSheet: balanceSheet(range, branchCode),
comparative: comparativeBalanceSheet(range, branchCode),
contingent: contingentLiabilities(range, branchCode),
glWise: glWiseDrCr(range, branchCode),
miscExpense: miscExpenseLedger(range, branchCode)
};
cacheKey = key;
return cached;
}
/** Clamp a range to the generated ledger so filters cannot query empty space. */
export function clampToLedger(range: GlDateRange): GlDateRange {
return {
from: range.from < GL_PERIOD.fyStart ? GL_PERIOD.fyStart : range.from,
to: range.to > GL_PERIOD.fyEnd ? GL_PERIOD.fyEnd : range.to
};
}
export function sum<T>(rows: T[], pick: (row: T) => number): number {
return rows.reduce((acc, r) => acc + pick(r), 0);
}
/** Groups rows by their `group` field, preserving chart-of-accounts order. */
export function groupBy<T extends { group: string }>(rows: T[]): { group: string; rows: T[]; total: number }[] {
const order: string[] = [];
const map = new Map<string, T[]>();
for (const r of rows) {
if (!map.has(r.group)) {
map.set(r.group, []);
order.push(r.group);
}
map.get(r.group)!.push(r);
}
return order.map((g) => ({ group: g, rows: map.get(g)!, total: 0 }));
}
-20
View File
@@ -1,20 +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 { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { GlReportsRoutingModule } from './gl-reports-routing.module';
@NgModule({
imports: [
SharedModule,
GlReportsRoutingModule
]
})
export class GlReportsModule {}
@@ -397,64 +397,6 @@
</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">&#8377; {{ 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 &mdash; As on {{ bank.asOnDate }} (₹ in Lakhs)</h3>
<button mat-stroked-button class="acr-btn-xls" (click)="exportContingentLiabCsv()">
<fa-icon icon="file-csv"></fa-icon>&nbsp; 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: &#8377; {{ contingentLiab.current | number: '1.2-2' }} Lakh</strong>
</td>
</tr>
</tfoot>
</table>
</div>
<p class="acr-note">
<fa-icon icon="info-circle"></fa-icon>&nbsp; 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">
@@ -318,16 +318,6 @@
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;
@@ -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' | 'CONTINGENT-LIAB' | 'GL-DR-CR' | 'MISC-EXP';
export type AcctReportCode = 'PNL' | 'BALANCE-SHEET' | 'COMPARATIVE-BS' | 'GL-DR-CR' | 'MISC-EXP';
export interface AcctReportsDialogData {
initialReport?: AcctReportCode;
@@ -47,11 +47,6 @@ interface GlLine {
closing: number;
}
interface ContingentLiabItem {
code: string;
label: string;
}
interface MiscExpRow {
txnDate: string;
branch: string;
@@ -86,7 +81,7 @@ export class AccountingReportsDialogComponent {
activeReport: AcctReportCode;
bank = {
name: 'CBSNexora Bank Ltd',
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
asOnDate: '31-03-2026',
period: '01-04-2025 to 31-03-2026',
@@ -99,7 +94,7 @@ export class AccountingReportsDialogComponent {
poDesignation: 'Chief Finance Officer',
auditor: 'M/s Sharma & Associates, Chartered Accountants',
reportRefNo: '157/2',
userCode: 'CBSN01',
userCode: 'BUBL01',
branchCount: 15
};
@@ -125,13 +120,6 @@ 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',
@@ -148,192 +136,309 @@ export class AccountingReportsDialogComponent {
}
];
// -------------------- P&L Data (₹ in Lakhs) — FY 2025-26 audited figures --------------------
// -------------------- P&L Data (₹ in Lakhs) --------------------
pnl: PnlLine[] = [
{ 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-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-03',
group: 'INCOME',
subgroup: 'Other Income',
label: 'Commission, Exchange & Brokerage',
amount: 144.2
subgroup: 'Interest Earned',
label: 'Interest on Balances with RBI / Other Banks',
amount: 1240
},
{ code: 'I-04', group: 'INCOME', subgroup: 'Other Income', label: 'Dividend Income', amount: 4.11 },
{
code: 'I-05',
code: 'I-04',
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
label: 'Commission, Exchange & Brokerage',
amount: 1820
},
{ 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 Paid on NABARD Borrowings',
amount: 47083.71
label: 'Interest on RBI / Inter-Bank Borrowings',
amount: 980
},
{
code: 'E-03',
group: 'EXPENDITURE',
subgroup: 'Operating Expenses',
label: 'Operating Expenses (Staff, Admin, Establishment)',
amount: 14503.79
label: 'Payments to & Provisions for Employees',
amount: 8420
},
{
code: 'E-04',
group: 'EXPENDITURE',
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
subgroup: 'Operating Expenses',
label: 'Rent, Taxes & Lighting',
amount: 1240
},
{ code: 'E-05', group: 'EXPENDITURE', subgroup: 'Operating Expenses', label: 'Printing & Stationery', amount: 184 },
{
code: 'E-06',
group: 'EXPENDITURE',
subgroup: 'Provisions',
label: 'Bad Debts Written Off',
amount: 906.44
subgroup: 'Operating Expenses',
label: 'Depreciation on Bank Property',
amount: 642
},
{
code: 'E-07',
group: 'EXPENDITURE',
subgroup: 'Provisions',
label: 'Provision for Staff Training Reserve',
amount: 245.06
subgroup: 'Operating Expenses',
label: 'Auditors Fees & Expenses',
amount: 124
},
{ code: 'E-08', group: 'EXPENDITURE', subgroup: 'Provisions', label: 'Provision for Income Tax', amount: 4600 }
{
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
},
{ code: 'E-11', group: 'EXPENDITURE', subgroup: 'Provisions', label: 'Provisions for Tax', amount: 2120 }
];
// -------------------- Balance Sheet (₹ in Lakhs) — as on 31-03-2026, audited --------------------
// -------------------- Balance Sheet (₹ in Lakhs) --------------------
bs: BsLine[] = [
{
code: 'L-01',
side: 'CAPITAL-LIAB',
group: 'Capital & Reserves',
label: 'Capital (Paid-up Share Capital)',
amount: 92837.18,
prevAmount: 88842.87
amount: 22000,
prevAmount: 20800
},
{
code: 'L-02',
side: 'CAPITAL-LIAB',
group: 'Capital & Reserves',
label: 'Reserves & Surplus',
amount: 155847.97,
prevAmount: 145524.37
amount: 32400,
prevAmount: 28640
},
{
code: 'L-03',
side: 'CAPITAL-LIAB',
group: 'Deposits',
label: 'Deposits',
amount: 1154054.96,
prevAmount: 997239.51
label: 'Demand Deposits — Current Accounts',
amount: 84200,
prevAmount: 78420
},
{
code: 'L-04',
side: 'CAPITAL-LIAB',
group: 'Borrowings',
label: 'Borrowings (NABARD / Other Banks)',
amount: 1113954.48,
prevAmount: 993301.8
group: 'Deposits',
label: 'Savings Bank Deposits',
amount: 218600,
prevAmount: 198400
},
{
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: 121037.61,
prevAmount: 124706.3
amount: 5940,
prevAmount: 5420
},
{
code: 'A-01',
side: 'ASSETS',
group: 'Cash & Bank Balances',
label: 'Cash & Balances with RBI',
amount: 32627.16,
prevAmount: 33361.93
label: 'Cash on Hand',
amount: 8420,
prevAmount: 7820
},
{
code: 'A-02',
side: 'ASSETS',
group: 'Money at Call',
label: 'Balances with Other Banks & Money at Call/Short Notice',
amount: 604619.45,
prevAmount: 549599.47
group: 'Cash & Bank Balances',
label: 'Balances with RBI',
amount: 38400,
prevAmount: 34200
},
{
code: 'A-03',
side: 'ASSETS',
group: 'Investments',
label: 'Investments',
amount: 316953.61,
prevAmount: 248274.61
group: 'Cash & Bank Balances',
label: 'Balances with Other Banks — Current',
amount: 14800,
prevAmount: 12400
},
{
code: 'A-04',
side: 'ASSETS',
group: 'Advances',
label: 'Loans & Advances',
amount: 1627711.4,
prevAmount: 1466401.79
group: 'Cash & Bank Balances',
label: 'Balances with Other Banks — Term Deposits',
amount: 10000,
prevAmount: 9200
},
{
code: 'A-05',
side: 'ASSETS',
group: 'Fixed Assets',
label: 'Fixed Assets (Property, Plant & Equipment)',
amount: 33791.75,
prevAmount: 33727.66
group: 'Money at Call',
label: 'Money at Call & Short Notice',
amount: 12400,
prevAmount: 10800
},
{
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: 'Other Assets',
amount: 22028.83,
prevAmount: 18249.4
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
}
];
// -------------------- 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[] = [
{
@@ -903,7 +1008,6 @@ 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();
}
@@ -1051,49 +1155,6 @@ 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: 'CBSNexora Bank Ltd',
name: 'Bharat Urban 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: 'CBSN01',
userCode: 'BUBL01',
/** 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' | 'OSS-4' | 'OSS-5' | 'OSS-6' | 'OSS-7';
export type CimsFormCode = 'OSS-2' | 'PSA' | 'RAL' | 'RCP-UCB';
export interface CimsXbrlDialogData {
initialForm?: CimsFormCode;
@@ -64,20 +64,20 @@ export class CimsXbrlDialogComponent {
activeTab: 'filing' | 'data' | 'preview' = 'filing';
bank = {
name: 'CBSNexora Bank Ltd',
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
bsrCode: 'CBSN0000123',
ifsc: 'CBSN0000123',
legalEntityId: 'CBSN-LEI-2025',
bsrCode: 'BUBL0000123',
ifsc: 'BUBL0000123',
legalEntityId: 'BUBL-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@cbsnexorabank.in',
poEmail: 'compliance@bharaturbanbank.in',
poMobile: '+91-9988002211',
cimsUserId: 'CBSN_CIMS_001',
cimsUserId: 'BUBL_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: 21.82,
value: 14.82,
unit: 'Percent',
type: 'percent'
},
@@ -105,7 +105,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:CapAdq.Tier1',
section: 'Capital Adequacy',
label: 'Tier-I CRAR',
value: 20.51,
value: 12.18,
unit: 'Percent',
type: 'percent'
},
@@ -113,7 +113,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:CapAdq.Tier2',
section: 'Capital Adequacy',
label: 'Tier-II CRAR',
value: 1.31,
value: 2.64,
unit: 'Percent',
type: 'percent'
},
@@ -121,7 +121,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.Total',
section: 'Asset Classification',
label: 'Total Advances (₹ Lakh)',
value: 4112.04,
value: 416265,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -129,7 +129,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.Standard',
section: 'Asset Classification',
label: 'Standard Assets (₹ Lakh)',
value: 3845.88,
value: 401820,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -137,7 +137,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.SubStd',
section: 'Asset Classification',
label: 'Sub-Standard (₹ Lakh)',
value: 102.41,
value: 6220,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -145,7 +145,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.Doubtful',
section: 'Asset Classification',
label: 'Doubtful (₹ Lakh)',
value: 163.75,
value: 5840,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -153,7 +153,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.Loss',
section: 'Asset Classification',
label: 'Loss (₹ Lakh)',
value: 0,
value: 2385,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -161,7 +161,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.GNPA',
section: 'NPA Position',
label: 'Gross NPA (₹ Lakh)',
value: 266.16,
value: 14445,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -169,7 +169,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.NNPA',
section: 'NPA Position',
label: 'Net NPA (₹ Lakh)',
value: -96.33,
value: 5212,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -177,7 +177,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.GNPAPct',
section: 'NPA Position',
label: 'GNPA %',
value: 6.47,
value: 3.47,
unit: 'Percent',
type: 'percent'
},
@@ -185,7 +185,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.NNPAPct',
section: 'NPA Position',
label: 'NNPA %',
value: -2.6,
value: 1.27,
unit: 'Percent',
type: 'percent'
},
@@ -193,7 +193,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Asset.PCR',
section: 'NPA Position',
label: 'Provision Coverage Ratio',
value: 136.24,
value: 64.13,
unit: 'Percent',
type: 'percent'
},
@@ -201,7 +201,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Profit.NIM',
section: 'Profitability',
label: 'Net Interest Margin',
value: 0.51,
value: 3.21,
unit: 'Percent',
type: 'percent'
},
@@ -209,7 +209,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Profit.ROA',
section: 'Profitability',
label: 'Return on Assets',
value: 0.67,
value: 0.92,
unit: 'Percent',
type: 'percent'
},
@@ -217,7 +217,7 @@ export class CimsXbrlDialogComponent {
ref: 'oss2:Profit.ROE',
section: 'Profitability',
label: 'Return on Equity',
value: 5.38,
value: 11.48,
unit: 'Percent',
type: 'percent'
},
@@ -254,7 +254,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:Reference.ANBC',
section: 'Reference',
label: 'Adjusted Net Bank Credit (₹ Lakh)',
value: 3274.83,
value: 600000,
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: 0,
value: 25000,
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: 2485.67,
value: 241700,
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: 75.9,
value: 40.28,
unit: 'Percent',
type: 'percent'
},
@@ -286,7 +286,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.Agri',
section: 'Sub-targets',
label: 'Agriculture (₹ Lakh)',
value: 39.0,
value: 124000,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -294,7 +294,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.AgriPct',
section: 'Sub-targets',
label: 'Agriculture % of ANBC',
value: 1.19,
value: 20.67,
unit: 'Percent',
type: 'percent'
},
@@ -302,7 +302,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.SMF',
section: 'Sub-targets',
label: 'Small &amp; Marginal Farmers (₹ Lakh)',
value: 0,
value: 72000,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -310,23 +310,23 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.SMFPct',
section: 'Sub-targets',
label: 'SMF % of ANBC',
value: 0,
value: 12.0,
unit: 'Percent',
type: 'percent'
},
{
ref: 'psa:PSL.MicroEnt',
section: 'Sub-targets',
label: 'MSME Enterprises (₹ Lakh)',
value: 2313.83,
label: 'Micro Enterprises (₹ Lakh)',
value: 51000,
unit: 'INR-Lakh',
type: 'monetary'
},
{
ref: 'psa:PSL.MicroPct',
section: 'Sub-targets',
label: 'MSME Enterprises % of ANBC',
value: 70.65,
label: 'Micro Enterprises % of ANBC',
value: 8.5,
unit: 'Percent',
type: 'percent'
},
@@ -334,7 +334,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.Weaker',
section: 'Sub-targets',
label: 'Weaker Sections (₹ Lakh)',
value: 990.28,
value: 85500,
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: 30.22,
value: 14.25,
unit: 'Percent',
type: 'percent'
},
@@ -350,7 +350,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.Housing',
section: 'Other Categories',
label: 'Housing (₹ Lakh)',
value: 115.12,
value: 9200,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -358,7 +358,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.Education',
section: 'Other Categories',
label: 'Education (₹ Lakh)',
value: 17.32,
value: 4200,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -366,7 +366,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.Export',
section: 'Other Categories',
label: 'Export Credit (₹ Lakh)',
value: 0,
value: 3800,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -374,7 +374,7 @@ export class CimsXbrlDialogComponent {
ref: 'psa:PSL.RenEnergy',
section: 'Other Categories',
label: 'Renewable Energy (₹ Lakh)',
value: 0,
value: 2400,
unit: 'INR-Lakh',
type: 'monetary'
}
@@ -395,15 +395,15 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Liab.Capital',
section: 'Liabilities',
label: 'Capital (₹ Lakh)',
value: 272.78,
value: 22000,
unit: 'INR-Lakh',
type: 'monetary'
},
{
ref: 'ral:Liab.Reserves',
section: 'Liabilities',
label: 'Reserves, Surplus &amp; Profit (₹ Lakh)',
value: 712.6,
label: 'Reserves &amp; Surplus (₹ Lakh)',
value: 32400,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -411,7 +411,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Liab.DepDemand',
section: 'Liabilities',
label: 'Demand Deposits (₹ Lakh)',
value: 195.92,
value: 84200,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -419,7 +419,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Liab.DepSavings',
section: 'Liabilities',
label: 'Savings Deposits (₹ Lakh)',
value: 2081.16,
value: 218600,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -427,7 +427,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Liab.DepTerm',
section: 'Liabilities',
label: 'Term Deposits (₹ Lakh)',
value: 4066.01,
value: 462800,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -435,7 +435,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Liab.Borrow',
section: 'Liabilities',
label: 'Borrowings (₹ Lakh)',
value: 0,
value: 24180,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -443,7 +443,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Liab.Other',
section: 'Liabilities',
label: 'Other Liabilities &amp; Provisions (₹ Lakh)',
value: 780.73,
value: 18420,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -451,7 +451,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Asset.Cash',
section: 'Assets',
label: 'Cash on Hand (₹ Lakh)',
value: 99.96,
value: 8420,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -459,15 +459,15 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Asset.RBI',
section: 'Assets',
label: 'Balances with RBI (₹ Lakh)',
value: 0,
value: 38400,
unit: 'INR-Lakh',
type: 'monetary'
},
{
ref: 'ral:Asset.Banks',
section: 'Assets',
label: 'Balances with Banks &amp; Other Institutions (₹ Lakh)',
value: 1349.53,
label: 'Balances with Banks (₹ Lakh)',
value: 24800,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -475,7 +475,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Asset.Money',
section: 'Assets',
label: 'Money at Call &amp; Short Notice (₹ Lakh)',
value: 0,
value: 12400,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -483,7 +483,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Asset.Invest',
section: 'Assets',
label: 'Investments (₹ Lakh)',
value: 2342.97,
value: 184600,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -491,7 +491,7 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Asset.Adv',
section: 'Assets',
label: 'Advances (₹ Lakh)',
value: 3912.88,
value: 416265,
unit: 'INR-Lakh',
type: 'monetary'
},
@@ -499,23 +499,23 @@ export class CimsXbrlDialogComponent {
ref: 'ral:Asset.Fixed',
section: 'Assets',
label: 'Fixed Assets (₹ Lakh)',
value: 49.48,
value: 14200,
unit: 'INR-Lakh',
type: 'monetary'
},
{
ref: 'ral:Asset.Other',
section: 'Assets',
label: 'Other Assets &amp; Interest Receivable (₹ Lakh)',
value: 354.38,
label: 'Other Assets (₹ Lakh)',
value: 12480,
unit: 'INR-Lakh',
type: 'monetary'
},
{
ref: 'ral:OBE.Guar',
section: 'Off-Balance Sheet',
label: 'Contingent Liabilities (₹ Lakh)',
value: 26.38,
label: 'Bank Guarantees (₹ Lakh)',
value: 18420,
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: 0,
value: 6580,
unit: 'INR-Lakh',
type: 'monetary'
}
@@ -653,293 +653,6 @@ export class CimsXbrlDialogComponent {
type: 'monetary'
}
]
},
'OSS-4': {
code: 'OSS-4',
name: 'OSS-4',
fullName: 'Connected Lending &amp; 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 &amp; 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 &gt; 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 &amp; 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 &amp; 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 &amp; 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 &gt; 30 Days',
value: 0,
unit: 'Number',
type: 'numeric'
}
]
}
};
@@ -947,11 +660,7 @@ export class CimsXbrlDialogComponent {
'OSS-2',
'PSA',
'RAL',
'RCP-UCB',
'OSS-4',
'OSS-5',
'OSS-6',
'OSS-7'
'RCP-UCB'
];
data = inject<CimsXbrlDialogData | null>(MAT_DIALOG_DATA, { optional: true });
@@ -76,7 +76,7 @@ export class CkycReportsDialogComponent {
activeReport: CkycReportCode;
bank = {
name: 'CBSNexora Bank Ltd',
name: 'Bharat Urban 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: 'CBSN01',
userCode: 'BUBL01',
fyStart: '01-04-2025',
fyEnd: '31-03-2026'
};
@@ -121,7 +121,7 @@ export class CkycReportsDialogComponent {
rekyc: RekycRow[] = [
{
ucode: 'CBSN000201',
ucode: 'BUBL000201',
name: 'Ravi Kumar Sharma',
customerType: 'INDIVIDUAL',
risk: 'LOW',
@@ -132,7 +132,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN000418',
ucode: 'BUBL000418',
name: 'Priya Patel',
customerType: 'INDIVIDUAL',
risk: 'MEDIUM',
@@ -143,7 +143,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN000522',
ucode: 'BUBL000522',
name: 'Arjun Deshmukh',
customerType: 'INDIVIDUAL',
risk: 'LOW',
@@ -154,7 +154,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN000711',
ucode: 'BUBL000711',
name: 'Vikram Reddy',
customerType: 'INDIVIDUAL',
risk: 'HIGH',
@@ -165,7 +165,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN000814',
ucode: 'BUBL000814',
name: 'Lakshmi Iyer',
customerType: 'INDIVIDUAL',
risk: 'LOW',
@@ -176,7 +176,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN001012',
ucode: 'BUBL001012',
name: 'Nirav Shah',
customerType: 'INDIVIDUAL',
risk: 'MEDIUM',
@@ -187,7 +187,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN001145',
ucode: 'BUBL001145',
name: 'Kavita Verma',
customerType: 'INDIVIDUAL',
risk: 'HIGH',
@@ -198,7 +198,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN001458',
ucode: 'BUBL001458',
name: 'Sameer Kapoor',
customerType: 'INDIVIDUAL',
risk: 'MEDIUM',
@@ -209,7 +209,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN001721',
ucode: 'BUBL001721',
name: 'Karthik Subramanian',
customerType: 'INDIVIDUAL',
risk: 'LOW',
@@ -220,7 +220,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN002012',
ucode: 'BUBL002012',
name: 'Abhishek Banerjee',
customerType: 'INDIVIDUAL',
risk: 'MEDIUM',
@@ -231,7 +231,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN002314',
ucode: 'BUBL002314',
name: 'Gaurav Thakur',
customerType: 'INDIVIDUAL',
risk: 'LOW',
@@ -242,7 +242,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN002615',
ucode: 'BUBL002615',
name: 'Manmohan Singh Gill',
customerType: 'INDIVIDUAL',
risk: 'HIGH',
@@ -253,7 +253,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN002918',
ucode: 'BUBL002918',
name: 'Sunita Devi',
customerType: 'INDIVIDUAL',
risk: 'LOW',
@@ -264,7 +264,7 @@ export class CkycReportsDialogComponent {
segment: 'BELOW-25-LACS'
},
{
ucode: 'CBSN003221',
ucode: 'BUBL003221',
name: 'Deepak Tiwari',
customerType: 'INDIVIDUAL',
risk: 'MEDIUM',
@@ -275,7 +275,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN003524',
ucode: 'BUBL003524',
name: 'Harpreet Kaur',
customerType: 'INDIVIDUAL',
risk: 'MEDIUM',
@@ -286,7 +286,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN003812',
ucode: 'BUBL003812',
name: 'Saraswati Mahila Co-op Society',
customerType: 'SOCIETY',
risk: 'MEDIUM',
@@ -297,7 +297,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN004115',
ucode: 'BUBL004115',
name: 'Aditya Enterprises',
customerType: 'FIRM',
risk: 'HIGH',
@@ -308,7 +308,7 @@ export class CkycReportsDialogComponent {
segment: 'ABOVE-25-LACS'
},
{
ucode: 'CBSN004318',
ucode: 'BUBL004318',
name: 'Vasudha Trading HUF',
customerType: 'HUF',
risk: 'MEDIUM',
@@ -332,7 +332,7 @@ export class CkycReportsDialogComponent {
branchCode: '01',
branch: 'Mumbai Main',
acType: 'SB',
clientId: 'CBSN000201',
clientId: 'BUBL000201',
customerName: 'Ravi Kumar Sharma',
oldRisk: 'LOW',
newRisk: 'LOW',
@@ -345,7 +345,7 @@ export class CkycReportsDialogComponent {
branchCode: '02',
branch: 'Pune FC Road',
acType: 'CA',
clientId: 'CBSN000418',
clientId: 'BUBL000418',
customerName: 'Priya Patel',
oldRisk: 'LOW',
newRisk: 'MEDIUM',
@@ -358,7 +358,7 @@ export class CkycReportsDialogComponent {
branchCode: '03',
branch: 'Nagpur Dharampeth',
acType: 'TL',
clientId: 'CBSN000522',
clientId: 'BUBL000522',
customerName: 'Arjun Deshmukh',
oldRisk: 'MEDIUM',
newRisk: 'LOW',
@@ -371,7 +371,7 @@ export class CkycReportsDialogComponent {
branchCode: '04',
branch: 'Bengaluru Jayanagar',
acType: 'OD',
clientId: 'CBSN000711',
clientId: 'BUBL000711',
customerName: 'Vikram Reddy',
oldRisk: 'MEDIUM',
newRisk: 'HIGH',
@@ -384,7 +384,7 @@ export class CkycReportsDialogComponent {
branchCode: '05',
branch: 'Bengaluru Indiranagar',
acType: 'SB',
clientId: 'CBSN000814',
clientId: 'BUBL000814',
customerName: 'Lakshmi Iyer',
oldRisk: 'LOW',
newRisk: 'LOW',
@@ -397,7 +397,7 @@ export class CkycReportsDialogComponent {
branchCode: '06',
branch: 'Ahmedabad Satellite',
acType: 'CC',
clientId: 'CBSN001012',
clientId: 'BUBL001012',
customerName: 'Nirav Shah',
oldRisk: 'LOW',
newRisk: 'MEDIUM',
@@ -410,7 +410,7 @@ export class CkycReportsDialogComponent {
branchCode: '07',
branch: 'Lucknow Gomti Nagar',
acType: 'CA',
clientId: 'CBSN001145',
clientId: 'BUBL001145',
customerName: 'Kavita Verma',
oldRisk: 'MEDIUM',
newRisk: 'HIGH',
@@ -423,7 +423,7 @@ export class CkycReportsDialogComponent {
branchCode: '08',
branch: 'Delhi Greater Kailash',
acType: 'OD',
clientId: 'CBSN001458',
clientId: 'BUBL001458',
customerName: 'Sameer Kapoor',
oldRisk: 'MEDIUM',
newRisk: 'MEDIUM',
@@ -436,7 +436,7 @@ export class CkycReportsDialogComponent {
branchCode: '09',
branch: 'Chennai Valasaravakkam',
acType: 'SB',
clientId: 'CBSN001721',
clientId: 'BUBL001721',
customerName: 'Karthik Subramanian',
oldRisk: 'LOW',
newRisk: 'LOW',
@@ -449,7 +449,7 @@ export class CkycReportsDialogComponent {
branchCode: '10',
branch: 'Kolkata Salt Lake',
acType: 'TL',
clientId: 'CBSN002012',
clientId: 'BUBL002012',
customerName: 'Abhishek Banerjee',
oldRisk: 'LOW',
newRisk: 'MEDIUM',
@@ -462,7 +462,7 @@ export class CkycReportsDialogComponent {
branchCode: '11',
branch: 'Shimla Sector 4',
acType: 'SB',
clientId: 'CBSN002314',
clientId: 'BUBL002314',
customerName: 'Gaurav Thakur',
oldRisk: 'LOW',
newRisk: 'LOW',
@@ -475,7 +475,7 @@ export class CkycReportsDialogComponent {
branchCode: '12',
branch: 'Amritsar Model Town',
acType: 'CC',
clientId: 'CBSN002615',
clientId: 'BUBL002615',
customerName: 'Manmohan Singh Gill',
oldRisk: 'HIGH',
newRisk: 'HIGH',
@@ -488,7 +488,7 @@ export class CkycReportsDialogComponent {
branchCode: '13',
branch: 'Bhopal Arera Colony',
acType: 'SB',
clientId: 'CBSN002918',
clientId: 'BUBL002918',
customerName: 'Sunita Devi',
oldRisk: 'LOW',
newRisk: 'LOW',
@@ -501,7 +501,7 @@ export class CkycReportsDialogComponent {
branchCode: '14',
branch: 'Indore Vijay Nagar',
acType: 'OD',
clientId: 'CBSN003221',
clientId: 'BUBL003221',
customerName: 'Deepak Tiwari',
oldRisk: 'MEDIUM',
newRisk: 'HIGH',
@@ -514,7 +514,7 @@ export class CkycReportsDialogComponent {
branchCode: '15',
branch: 'Ludhiana Sarabha Nagar',
acType: 'TL',
clientId: 'CBSN003524',
clientId: 'BUBL003524',
customerName: 'Harpreet Kaur',
oldRisk: 'MEDIUM',
newRisk: 'MEDIUM',
@@ -165,12 +165,12 @@
<div class="da-compare">
<div class="da-item">
<div class="da-label">Total Deposits</div>
<div class="da-value deposit">&#8377; 11,540.55 Cr</div>
<div class="da-bar"><div class="da-fill deposit-fill" style="width: 71%"></div></div>
<div class="da-value deposit">&#8377; 3,264.42 Cr</div>
<div class="da-bar"><div class="da-fill deposit-fill" style="width: 78%"></div></div>
</div>
<div class="da-item">
<div class="da-label">Total Advances</div>
<div class="da-value advance">&#8377; 16,277.11 Cr</div>
<div class="da-value advance">&#8377; 4,162.65 Cr</div>
<div class="da-bar"><div class="da-fill advance-fill" style="width: 100%"></div></div>
</div>
</div>
@@ -1,169 +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="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>&nbsp;{{ bank.name }} </span>
<span class="fdrd-meta"> <fa-icon icon="map-marker-alt"></fa-icon>&nbsp;{{ bank.branch }} </span>
<button class="fdrd-close" (click)="close()" matTooltip="Close" aria-label="Close">&#10005;</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">&#8377; {{ 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">&#8377; {{ 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>&nbsp; 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>&nbsp; Reset
</button>
<button mat-stroked-button class="fdrd-btn-save" [disabled]="!isValid" (click)="save()">
<fa-icon icon="save"></fa-icon>&nbsp; Save
</button>
</div>
</div>
</div>
@@ -1,395 +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/.
*/
: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);
}
}
@@ -1,146 +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, 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: 'CBSNexora Bank Ltd',
name: 'Bharat Urban 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
@@ -1,472 +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="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>
@@ -1,461 +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 } 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);
}
}
@@ -1,198 +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="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">Portfolio Allocation — Target vs Actual</div>
<table class="pg-table">
<thead>
<tr>
<th>Instrument Type</th>
<th class="num">Target %</th>
<th class="num">Actual %</th>
<th class="num">Actual Value</th>
<th class="num">Variance</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (a of allocationVsTarget; track a.type) {
<tr>
<td>{{ a.label }}</td>
<td class="num">{{ a.targetPct }}%</td>
<td class="num">{{ a.actualPct }}%</td>
<td class="num">₹{{ a.actualValue | number: '1.2-2' }}</td>
<td
class="num"
[class.col-out]="a.statusLabel !== 'Within Target'"
[class.col-in]="a.statusLabel === 'Within Target'"
>
{{ a.variancePct > 0 ? '+' : '' }}{{ a.variancePct }}%
</td>
<td>
<span
class="inv-badge"
[class.inv-badge--active]="a.statusLabel === 'Within Target'"
[class.inv-badge--sold]="a.statusLabel === 'Over-Allocated'"
[class.inv-badge--written_off]="a.statusLabel === 'Under-Allocated'"
>
{{ a.statusLabel }}
</span>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td>Total</td>
<td class="num">100%</td>
<td class="num">100%</td>
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
<td colspan="2"></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 &amp; 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>
@@ -1,155 +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 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import {
ALL_HOLDINGS,
INSTRUMENT_LABELS,
InstrumentType,
InvestmentHolding,
TARGET_ALLOCATION_PCT,
accruedInterest,
daysToMaturity,
maturityAmount
} from '../investment-data';
interface InstrumentSummary {
type: InstrumentType;
label: string;
route: string;
count: number;
faceValue: number;
accruedInterest: number;
maturityValue: number;
}
interface AllocationRow {
type: InstrumentType;
label: string;
targetPct: number;
actualPct: number;
actualValue: number;
variancePct: number;
statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
}
/** 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)
};
});
}
/** Board-approved policy target % per instrument type vs the portfolio's actual allocation
* (by invested/face value) — a ±2 percentage-point band is treated as "Within Target" since
* a treasury desk can't rebalance to the exact decimal every day. */
get allocationVsTarget(): AllocationRow[] {
const total = this.totalFaceValue;
return this.types.map(({ type }) => {
const value = this.holdingsOf(type).reduce((s, h) => s + h.faceValue, 0);
const actualPct = total ? (value / total) * 100 : 0;
const targetPct = TARGET_ALLOCATION_PCT[type];
const variancePct = +(actualPct - targetPct).toFixed(1);
const statusLabel: AllocationRow['statusLabel'] =
Math.abs(variancePct) <= 2 ? 'Within Target' : variancePct > 0 ? 'Over-Allocated' : 'Under-Allocated';
return {
type,
label: INSTRUMENT_LABELS[type],
targetPct,
actualPct: +actualPct.toFixed(1),
actualValue: value,
variancePct,
statusLabel
};
});
}
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
@@ -1,566 +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="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">
Interest Receivable, FD and Govt./Bond interest calculation-and-post batch runs, Interest Receive, 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 === 'receivable'"
(click)="setTab('receivable')"
>
Interest Receivable
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'fdCalc'"
(click)="setTab('fdCalc')"
>
01 FD Interest Calculation &amp; Post
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'bondCalc'"
(click)="setTab('bondCalc')"
>
02 Govt./Bond Int. Calculation &amp; Post
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'receive'"
(click)="setTab('receive')"
>
Interest Receive
</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>
<!-- INTEREST RECEIVABLE -->
@if (activeTab === 'receivable') {
<div class="pg-card">
<div class="pos-section-label">
Interest Receivable — Posted but Not Yet Collected (₹{{ totalReceivable | number: '1.2-2' }})
</div>
<table class="pg-table">
<thead>
<tr>
<th>Cert No</th>
<th>Script / Party</th>
<th>Type</th>
<th>Status</th>
<th class="num">Interest Posted</th>
<th class="num">Interest Received</th>
<th class="num col-in">Receivable Balance</th>
</tr>
</thead>
<tbody>
@for (h of receivableRows; track h.certNo) {
<tr>
<td>
<span class="pg-chip">{{ h.certNo }}</span>
</td>
<td>{{ h.scriptName || h.party }}</td>
<td>{{ instrumentLabel(h.instrumentType) }}</td>
<td>
<span [class]="provisionBadgeClass(h)">{{ provisionLabel(h) }}</span>
</td>
<td class="num">₹{{ posted(h) | number: '1.2-2' }}</td>
<td class="num">₹{{ received(h) | number: '1.2-2' }}</td>
<td class="num col-in">₹{{ receivableBalance(h) | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="7" class="pg-empty">No outstanding interest receivable.</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td colspan="6">Total Receivable</td>
<td class="num">₹{{ totalReceivable | number: '1.2-2' }}</td>
</tr>
</tfoot>
</table>
</div>
@if (suspenseRows.length > 0) {
<div class="pg-card">
<div class="pos-section-label">
Non-Performing Investments — Interest Held in Suspense (₹{{ totalSuspense | number: '1.2-2' }})
</div>
<p class="pg-sub" style="padding: 0 16px">
These holdings are classified Non-Performing (Sub-Standard/Doubtful/Loss). Per RBI income-recognition norms,
interest is not posted as an accrual receivable — it accrues in memo only, and is recognised as income solely
on actual cash receipt via the Interest Receive tab.
</p>
<table class="pg-table">
<thead>
<tr>
<th>Cert No</th>
<th>Script / Party</th>
<th>Type</th>
<th>Status</th>
<th class="num">Provision Required</th>
<th class="num col-out">Interest in Suspense</th>
</tr>
</thead>
<tbody>
@for (h of suspenseRows; track h.certNo) {
<tr>
<td>
<span class="pg-chip">{{ h.certNo }}</span>
</td>
<td>{{ h.scriptName || h.party }}</td>
<td>{{ instrumentLabel(h.instrumentType) }}</td>
<td>
<span [class]="provisionBadgeClass(h)">{{ provisionLabel(h) }}</span>
</td>
<td class="num">₹{{ provisionRequired(h) | number: '1.2-2' }}</td>
<td class="num col-out">₹{{ suspense(h) | number: '1.2-2' }}</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td colspan="5">Total Suspense</td>
<td class="num">₹{{ totalSuspense | number: '1.2-2' }}</td>
</tr>
</tfoot>
</table>
</div>
}
}
<!-- 01 FD INTEREST CALCULATION -->
@if (activeTab === 'fdCalc') {
<div class="pg-card">
<div class="pos-section-label">Investment - FD Interest Calculation &amp; Post</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()">Calculate &amp; Post</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>
@if (fdCalcExcludedNpi.length > 0) {
<p class="pg-sub" style="padding: 12px 16px 16px">
{{ fdCalcExcludedNpi.length }} certificate(s) excluded — classified Non-Performing, interest is not posted on
accrual basis (see Interest Receivable tab for suspense amount).
</p>
}
</div>
}
<!-- 02 GOVT./BOND INT. CALCULATION -->
@if (activeTab === 'bondCalc') {
<div class="pg-card">
<div class="pos-section-label">Investment - Govt./Bond Interest Calculation &amp; Post</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()">
Calculate &amp; Post
</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>
@if (bondCalcExcludedNpi.length > 0) {
<p class="pg-sub" style="padding: 12px 16px 16px">
{{ bondCalcExcludedNpi.length }} holding(s) excluded — classified Non-Performing, interest is not posted on
accrual basis (see Interest Receivable tab for suspense amount).
</p>
}
</div>
}
<!-- INTEREST RECEIVE -->
@if (activeTab === 'receive') {
<div class="pg-card">
<div class="pos-section-label">Interest Receive — Record Actual Cash Receipt</div>
<table class="pg-table">
<thead>
<tr>
<th>Cert No</th>
<th>Script / Party</th>
<th>Type</th>
<th>Status</th>
<th class="num">Outstanding</th>
<th class="num">Amount to Receive</th>
<th></th>
</tr>
</thead>
<tbody>
@for (h of receiveTargets; track h.certNo) {
<tr>
<td>
<span class="pg-chip">{{ h.certNo }}</span>
</td>
<td>{{ h.scriptName || h.party }}</td>
<td>{{ instrumentLabel(h.instrumentType) }}</td>
<td>
<span [class]="provisionBadgeClass(h)">{{ provisionLabel(h) }}</span>
</td>
<td class="num">₹{{ outstandingFor(h) | number: '1.2-2' }}</td>
<td class="num">
<input
type="number"
[(ngModel)]="receiveDrafts[h.certNo]"
[max]="outstandingFor(h)"
style="width: 120px; text-align: right"
/>
</td>
<td>
<button type="button" class="pg-btn pg-btn--ghost" (click)="receiveInterest(h)">Receive</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="7" class="pg-empty">No interest outstanding for collection.</td>
</tr>
}
</tbody>
</table>
</div>
<div class="pg-card">
<div class="pos-section-label">Interest Received Log</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">Amount Received</th>
</tr>
</thead>
<tbody>
@for (t of receivedLog; 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 interest received yet.</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td colspan="4">Total Received</td>
<td class="num">₹{{ receivedLogTotal | 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>
@@ -1,401 +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 } 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,
InvestmentProvisionStatus,
INSTRUMENT_LABELS,
PROVISION_STATUS_LABELS,
accruedInterest,
daysToMaturity,
maturityAmount,
todayDDMMYYYY,
isNpi,
canAccrueInterest,
interestToPost,
interestReceivableBalance,
suspenseInterest,
provisionRequiredInv
} from '../investment-data';
type InterestTab =
| 'receivable'
| 'fdCalc'
| 'bondCalc'
| 'receive'
| 'bondIntRegister'
| 'fdIntRegister'
| 'fdRegister';
/**
* The legacy "Interest" top-menu for Investment — plus the full accrual lifecycle: Interest
* Receivable, 01 FD Interest Calculation, 02 Govt./Bond Int. Calculation, Interest Receive,
* 03 Government Bond Int Register, 04 FD Interest Register, 05 FD Register.
*
* 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.
*
* Provisioning: a holding classified Non-Performing (Sub-Standard/Doubtful/Loss) can never have
* interest calculated/posted on accrual basis (canAccrueInterest() blocks it) — only actual cash
* receipts are recognised for it, straight through Interest Receive, matching RBI's income
* recognition norms for Non-Performing Investments.
*/
@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 = 'receivable';
asOnDate = todayDDMMYYYY();
fdHoldings: InvestmentHolding[] = [...FD_HOLDINGS];
bondHoldings: InvestmentHolding[] = [
...GOVT_SECURITY_HOLDINGS,
...NON_SLR_HOLDINGS,
...BOND_HOLDINGS
];
txns: InvestmentTxn[] = [...ALL_TXNS];
readonly scriptOptions = SCRIPT_MASTER;
/** Draft "amount to receive" per certificate, keyed by certNo, for the Interest Receive tab. */
receiveDrafts: Record<string, number | null> = {};
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);
}
get allInterestHoldings(): InvestmentHolding[] {
return [
...this.fdHoldings,
...this.bondHoldings
].filter((h) => h.status === 'ACTIVE');
}
isNpiHolding(h: InvestmentHolding): boolean {
return isNpi(h);
}
provisionLabel(h: InvestmentHolding): string {
return PROVISION_STATUS_LABELS[h.provisionStatus ?? 'STANDARD'];
}
provisionBadgeClass(h: InvestmentHolding): string {
const status: InvestmentProvisionStatus = h.provisionStatus ?? 'STANDARD';
return `inv-badge inv-badge--${status.toLowerCase()}`;
}
posted(h: InvestmentHolding): number {
return h.interestPosted ?? 0;
}
received(h: InvestmentHolding): number {
return h.interestReceived ?? 0;
}
receivableBalance(h: InvestmentHolding): number {
return interestReceivableBalance(h);
}
suspense(h: InvestmentHolding): number {
return suspenseInterest(h, this.asOnDate);
}
provisionRequired(h: InvestmentHolding): number {
return provisionRequiredInv(h);
}
// -------------------- 01 FD Interest Calculation --------------------
get fdCalcTargets(): InvestmentHolding[] {
const base = this.fdHoldings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
if (this.fdCalcAll) {
return base;
}
return base.filter((h) => !this.fdCalcCertNo || h.certNo === this.fdCalcCertNo);
}
/** NPI FD certificates excluded from calculation/posting — interest recognised on cash basis
* only, via the Interest Receive tab. */
get fdCalcExcludedNpi(): InvestmentHolding[] {
return this.fdHoldings.filter((h) => h.status === 'ACTIVE' && isNpi(h));
}
fdInterest(h: InvestmentHolding): number {
return interestToPost(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.filter((h) => this.fdInterest(h) > 0);
if (targets.length === 0) {
this.toast('No FD holdings have interest due to be posted.');
return;
}
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
const amt = this.fdInterest(h);
h.interestPosted = this.posted(h) + amt;
return {
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: amt,
particulars: `FD interest posted (accrual) - ${h.party}`
};
});
const total = newTxns.reduce((s, t) => s + t.amount, 0);
this.txns = [
...newTxns,
...this.txns
];
this.toast(`FD interest posted for ${targets.length} certificate(s). Total ₹${total.toLocaleString('en-IN')}.`);
}
// -------------------- 02 Govt./Bond Int. Calculation --------------------
get bondCalcTargets(): InvestmentHolding[] {
const base = this.bondHoldings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h));
if (this.bondCalcAll) {
return base;
}
return base.filter((h) => {
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;
});
}
/** NPI Govt/Bond holdings excluded from calculation/posting — interest recognised on cash
* basis only, via the Interest Receive tab. */
get bondCalcExcludedNpi(): InvestmentHolding[] {
return this.bondHoldings.filter((h) => h.status === 'ACTIVE' && isNpi(h));
}
bondInterest(h: InvestmentHolding): number {
return interestToPost(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.filter((h) => this.bondInterest(h) > 0);
if (targets.length === 0) {
this.toast('No Govt/Bond holdings have interest due to be posted.');
return;
}
const newTxns: InvestmentTxn[] = targets.map((h, i) => {
const amt = this.bondInterest(h);
h.interestPosted = this.posted(h) + amt;
return {
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: amt,
particulars: `Coupon/interest posted (accrual) - ${h.scriptName}`
};
});
const total = newTxns.reduce((s, t) => s + t.amount, 0);
this.txns = [
...newTxns,
...this.txns
];
this.toast(`Interest posted for ${targets.length} holding(s). Total ₹${total.toLocaleString('en-IN')}.`);
}
// -------------------- Interest Receivable (all interest-bearing types) --------------------
/** Standard holdings with a posted-but-uncollected balance — a genuine "Interest Receivable"
* GL asset. */
get receivableRows(): InvestmentHolding[] {
return this.allInterestHoldings.filter((h) => canAccrueInterest(h) && this.receivableBalance(h) > 0);
}
/** NPI holdings with interest still accruing in memo/suspense — not a GL asset, shown
* separately so the treasury desk can see what cash-basis income is outstanding. */
get suspenseRows(): InvestmentHolding[] {
return this.allInterestHoldings.filter((h) => isNpi(h) && this.suspense(h) > 0);
}
get totalReceivable(): number {
return this.receivableRows.reduce((s, h) => s + this.receivableBalance(h), 0);
}
get totalSuspense(): number {
return this.suspenseRows.reduce((s, h) => s + this.suspense(h), 0);
}
// -------------------- Interest Receive (cash receipt, all interest-bearing types) --------------------
/** Everything with money outstanding to collect — Standard holdings against their posted
* receivable, and NPI holdings against their notional suspense (cash-basis recognition). */
get receiveTargets(): InvestmentHolding[] {
return this.allInterestHoldings.filter((h) =>
canAccrueInterest(h) ? this.receivableBalance(h) > 0 : this.suspense(h) > 0
);
}
outstandingFor(h: InvestmentHolding): number {
return canAccrueInterest(h) ? this.receivableBalance(h) : this.suspense(h);
}
get receivedLog(): InvestmentTxn[] {
return this.txns.filter((t) => t.txnType === 'INTEREST_RECEIVED');
}
get receivedLogTotal(): number {
return this.receivedLog.reduce((s, t) => s + t.amount, 0);
}
receiveInterest(h: InvestmentHolding): void {
const draft = this.receiveDrafts[h.certNo];
if (!draft || draft <= 0) {
this.toast('Enter a valid amount to receive.');
return;
}
let amt = draft;
let particulars: string;
if (canAccrueInterest(h)) {
amt = Math.min(draft, this.receivableBalance(h));
h.interestReceived = this.received(h) + amt;
particulars = `Interest received against posted receivable - ${h.scriptName || h.party}`;
} else {
h.interestReceived = this.received(h) + amt;
particulars = `Interest received on cash basis (${this.provisionLabel(h)}) - ${h.scriptName || h.party}`;
}
this.txns = [
{
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'INTEREST_RECEIVED',
voucherNo: `INV/INT-RCV/${h.certNo}-${this.receivedLog.length + 1}`,
txnDate: this.asOnDate,
amount: amt,
particulars
},
...this.txns
];
this.toast(`₹${amt.toLocaleString('en-IN')} interest received for ${h.certNo}.`);
delete this.receiveDrafts[h.certNo];
}
// -------------------- 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);
}
}
@@ -1,557 +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="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>
@@ -1,262 +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 } 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);
}
}
@@ -1,694 +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="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 &amp; Registers
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeGroup === 'maturity'"
(click)="setGroup('maturity', 'maturedFd')"
>
Maturity &amp; 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>
@@ -1,247 +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 } 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);
}
}
@@ -1,71 +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 { 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 {}
-262
View File
@@ -1,262 +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/.
*/
// 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;
}
// Investment-provisioning classification (Non-Performing Investment) — same severity ramp as
// the NPA module's stage badges, kept local since it colours a different status field here.
&--standard {
background: #f0fdf4;
color: #15803d;
border: 1px solid #bbf7d0;
}
&--sub_standard {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
&--doubtful {
background: #fdf2f8;
color: #9d174d;
border: 1px solid #fbcfe8;
}
&--loss {
background: #1f2937;
color: #f9fafb;
border: 1px solid #1f2937;
}
}
.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);
}
}
-34
View File
@@ -1,34 +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 { 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 {}
@@ -1,276 +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="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>
@@ -1,263 +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 } 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);
}
}
-619
View File
@@ -1,619 +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/.
*/
// 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';
@@ -1,949 +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="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>
@@ -1,490 +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, 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);
}
}
-41
View File
@@ -1,41 +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 { 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 {}
-153
View File
@@ -1,153 +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/.
*/
// 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;
}
}
-22
View File
@@ -1,22 +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 { 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 {}
@@ -45,12 +45,6 @@
</td>
</ng-container>
<!-- Income / Occupation Limit Column -->
<ng-container matColumnDef="incomeLimit">
<th mat-header-cell *matHeaderCellDef>Income / Occupation Limit</th>
<td mat-cell *matCellDef="let element" class="income-limit-cell">{{ getIncomeLimitLabel(element) }}</td>
</ng-container>
<!-- Reason Column -->
<ng-container matColumnDef="reason">
<th mat-header-cell *matHeaderCellDef>Rule Reason / Criteria</th>
@@ -114,15 +114,6 @@
max-width: 400px;
}
.income-limit-cell {
color: #334155;
font-size: 13px;
font-weight: 500;
white-space: normal;
max-width: 200px;
line-height: 1.35;
}
/* Badges for Risk Category */
.risk-badge {
display: inline-flex;
@@ -42,7 +42,6 @@ export class AmlRulesComponent implements OnInit {
'orderPosition',
'ruleName',
'riskCategory',
'incomeLimit',
'reason',
'amlCheckStatus',
'bannedEntityMatch'
@@ -85,39 +84,4 @@ export class AmlRulesComponent implements OnInit {
return '';
}
}
/** Formats whichever income/turnover/occupation criterion drives this rule's risk mark. */
getIncomeLimitLabel(rule: any): string {
const inr = (n: number) => '₹' + Number(n).toLocaleString('en-IN');
if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0) {
return rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0
? `Monthly Income ${inr(rule.minMonthlyIncome)} - ${inr(rule.maxMonthlyIncome)}`
: `Monthly Income > ${inr(rule.minMonthlyIncome)}`;
}
if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0) {
return rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0
? `Annual Income ${inr(rule.minAnnualIncome)} - ${inr(rule.maxAnnualIncome)}`
: `Annual Income > ${inr(rule.minAnnualIncome)}`;
}
if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0) {
return rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0
? `Annual Turnover ${inr(rule.minAnnualTurnover)} - ${inr(rule.maxAnnualTurnover)}`
: `Annual Turnover > ${inr(rule.minAnnualTurnover)}`;
}
if (rule.occupationName) return `Occupation: ${rule.occupationName}`;
if (rule.occupationCategoryName) return `Occupation Category: ${rule.occupationCategoryName}`;
if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) {
const parts: string[] = [];
if (rule.txnThresholdAmountMediumRisk != null) parts.push(`${inr(rule.txnThresholdAmountMediumRisk)} (Medium Risk)`);
if (rule.txnThresholdAmountHighRisk != null) parts.push(`${inr(rule.txnThresholdAmountHighRisk)} (High Risk)`);
return `Txn Amount ≥ ${parts.join(' / ')}`;
}
if (rule.txnThresholdAmount != null) {
const amt = inr(rule.txnThresholdAmount);
if (rule.txnRuleType === 'THRESHOLD') return `Txn Amount ≥ ${amt}`;
if (rule.txnWindowDays != null) return `${amt} within ${rule.txnWindowDays} days`;
return `Txn Amount ≥ ${amt}`;
}
return 'Behavioral / Transaction Pattern';
}
}
-134
View File
@@ -1,134 +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/.
*/
/**
* Shapes common to every RBI CIMS off-site surveillance return (OSS 1..7).
* Kept free of any ExcelJS import so data files stay dependency-light.
*/
/** One labelled row of a return. */
export interface ReturnLine {
label: string;
/** 0 = numbered head, 1 = roman sub-item, 2 = a/b/c leaf. Drives indentation. */
level: number;
/** null renders as an empty cell, never as 0 — RBI treats blank and zero differently. */
value: number | null;
/** Derived/aggregate row — shaded and bold, like the blue cells in the workbook. */
total?: boolean;
/** Row is a headcount (depositors / borrowers / employees), not a rupee amount. */
count?: boolean;
}
/**
* One row of a multi-column statement. Returns from OSS 3 onwards are matrices
* (risk category x asset class) rather than the single-value lists of OSS 1/2.
*/
export interface MatrixRow {
label: string;
/** 0 = numbered head, 1..n = progressively indented sub-items. */
level: number;
/** One entry per data column; null renders blank, never 0. */
values: (number | null)[];
/** Derived/aggregate row — shaded and bold. */
total?: boolean;
/** Caption-only row that spans the table with no figures of its own. */
header?: boolean;
/** Render values as percentages rather than amounts. */
percent?: boolean;
}
/** A labelled statement made of a header band plus matrix rows. */
export interface MatrixTable {
title: string;
subtitle?: string;
/** Column captions excluding the leading label column. */
columns: string[];
rows: MatrixRow[];
notes?: string[];
unit?: string;
}
/** A signatory block as it appears on the Signatory sheet of every OSS return. */
export interface ReturnSignatory {
heading: string;
signature: string;
name: string;
designation: string;
email: string;
telephoneOffice: string;
telephoneResidence: string;
place: string;
date: string;
}
/** A `label -> value` row on the General Information sheet. */
export interface ReturnGeneralInfoRow {
label: string;
value: string;
}
/** Field order shared by the Signatory sheet of every OSS return. */
export const SIGNATORY_FIELDS: { key: keyof ReturnSignatory; caption: string }[] = [
{ key: 'signature', caption: 'Signature' },
{ key: 'name', caption: 'Name' },
{ key: 'designation', caption: 'Designation' },
{ key: 'email', caption: 'e-mail ID' },
{ key: 'telephoneOffice', caption: 'Tele. No. (O)' },
{ key: 'telephoneResidence', caption: 'Tele. No. (R)' },
{ key: 'place', caption: 'Place' },
{ key: 'date', caption: 'Date' }
];
/**
* Identity of the reporting bank, shared by every OSS return so the General
* Information sheets can never drift apart between OSS 1 and OSS 2.
*/
export const OSS_BANK = {
name: 'CBSNEXORA BANK LTD., HEAD OFFICE',
bankCode: '91007',
roName: 'MUMBAI',
institutionType: 'Urban Cooperative Banks',
reportingFrequency: 'Quarterly',
quarterEndDate: '30-Jun-2026',
auditFlag: 'Un-Audited',
dateOfAudit: '',
returnVersion: 'V1.0',
startDate: '01-Apr-2026',
/** OSS 3 prints dates as DD-MM-YYYY rather than DD-Mon-YYYY. */
startDateNumeric: '01-04-2026',
quarterEndDateNumeric: '30-06-2026'
};
/**
* Signatories for the demo bank. The `.example` domain is reserved for
* documentation, so these addresses can never reach a real inbox.
*/
export const OSS_SIGNATORIES: ReturnSignatory[] = [
{
heading: 'Authorised Reporting Official',
signature: 'ROHAN DESHPANDE',
name: 'ROHAN DESHPANDE',
designation: 'MANAGER — REGULATORY REPORTING',
email: 'rohan.deshpande@cbsnexorabank.example',
telephoneOffice: '022-40015520',
telephoneResidence: '022-40015588',
place: 'MUMBAI',
date: '14/07/2026'
},
{
heading: 'Countersigned By',
signature: 'ANJALI MENON',
name: 'ANJALI MENON',
designation: 'CHIEF EXECUTIVE OFFICER',
email: 'ceo@cbsnexorabank.example',
telephoneOffice: '022-40015500',
telephoneResidence: '022-40015501',
place: 'MUMBAI',
date: '14/07/2026'
}
];
-417
View File
@@ -1,417 +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/.
*/
:host {
/* Mirrors the RBI CIMS workbook palette so the page and the export read alike. */
--o-navy: #002f5f;
--o-blue: #1d4ed8;
--o-blue-l: #eff6ff;
--o-derived: #cfe2f8;
--o-green: #15803d;
--o-green-l: #dcfce7;
--o-amber-l: #ffc;
--o-amber-d: #92400e;
--o-red: #c0392b;
--o-red-l: #fdecea;
--o-g50: #f8fafc;
--o-g100: #f1f5f9;
--o-g200: #e2e8f0;
--o-g300: #cbd5e1;
--o-g500: #64748b;
--o-g700: #334155;
--o-g900: #0f172a;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
background: var(--o-g50);
color: var(--o-g900);
font-size: 14px;
line-height: 1.5;
}
.oss-root {
padding: 18px 22px;
max-width: 1500px;
margin: 0 auto;
}
/* HEADER */
.oss-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.oss-head-left {
display: flex;
align-items: center;
gap: 14px;
}
.oss-head-ic {
font-size: 28px;
color: var(--o-navy);
}
.oss-head-t {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.oss-head-s {
font-size: 12px;
color: var(--o-g500);
}
.oss-head-right {
display: flex;
align-items: center;
gap: 12px;
}
.oss-audit-pill {
padding: 4px 12px;
border-radius: 999px;
background: var(--o-amber-l);
color: var(--o-amber-d);
border: 1px solid #fcd34d;
font-size: 12px;
font-weight: 700;
}
/* BUTTONS */
.oss-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 16px;
border: 1px solid var(--o-g300);
background: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
color: var(--o-g700);
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.oss-btn-xls {
background: #16a34a;
border-color: #16a34a;
color: #fff;
&:hover:not(:disabled) {
background: #15803d;
border-color: #15803d;
}
}
/* BALANCE CHECK */
.oss-balance {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-radius: 10px;
background: var(--o-green-l);
border: 1px solid #86efac;
color: #14532d;
font-size: 13px;
margin-bottom: 14px;
&--bad {
background: var(--o-red-l);
border-color: #fca5a5;
color: #7f1d1d;
}
}
/* TABS */
.oss-tabs {
display: flex;
gap: 4px;
background: #fff;
border: 1px solid var(--o-g200);
border-radius: 10px;
padding: 4px;
margin-bottom: 16px;
overflow-x: auto;
}
.oss-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 500;
color: var(--o-g500);
cursor: pointer;
border-radius: 7px;
white-space: nowrap;
transition: all 0.15s;
font-family: inherit;
&:hover {
color: var(--o-g900);
}
&.active {
background: var(--o-navy);
color: #fff;
box-shadow: 0 2px 6px rgb(0 47 95 / 25%);
}
}
.oss-section {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Full-width banner, matching the dark blue header band in the workbook. */
.oss-sheet-banner {
background: var(--o-navy);
color: #fff;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
text-align: center;
letter-spacing: 0.02em;
}
/* TABLES */
.oss-tbl-wrap {
background: #fff;
border: 1px solid var(--o-g200);
border-radius: 10px;
overflow: auto;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.oss-tbl-h {
background: var(--o-blue-l);
color: var(--o-navy);
padding: 10px 16px;
font-size: 12px;
font-weight: 700;
border-bottom: 1px solid var(--o-g200);
}
.oss-tbl {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
padding: 9px 12px;
font-size: 11px;
font-weight: 700;
color: var(--o-g700);
background: var(--o-g100);
border-bottom: 1px solid var(--o-g200);
border-right: 1px solid var(--o-g200);
}
td {
padding: 8px 12px;
border-bottom: 1px solid var(--o-g100);
border-right: 1px solid var(--o-g100);
font-size: 13px;
color: var(--o-g700);
}
tr:last-child td {
border-bottom: none;
}
tbody tr:hover td {
background: var(--o-g50);
}
}
/* Key/value layout used by General Information and Signatory. */
.oss-tbl--kv {
th {
width: 40%;
background: #d9d9d9;
color: var(--o-g900);
}
td.filled {
background: var(--o-derived);
}
td.audit {
background: var(--o-amber-l);
font-weight: 700;
}
}
/* Derived/aggregate rows — the blue cells in the original sheet. */
.total-row td {
font-weight: 700;
color: var(--o-g900);
}
td.derived {
background: var(--o-derived);
font-weight: 700;
color: var(--o-navy);
}
/* Indentation matching the return's numbered / roman / lettered hierarchy. */
.lvl-0 {
font-weight: 600;
}
.lvl-1 {
padding-left: 30px !important;
}
.lvl-2 {
padding-left: 52px !important;
}
.lvl-3 {
padding-left: 74px !important;
}
/* Caption-only row spanning a statement (e.g. "PRIORITY SECTOR"). */
.section-row td {
background: var(--o-g100);
font-weight: 700;
color: var(--o-g900);
}
/* Wide matrices keep their label column readable while the figures scroll. */
.oss-col-label {
min-width: 320px;
}
.oss-tbl-unit {
float: right;
font-weight: 600;
color: var(--o-g500);
}
.oss-tbl-sub {
padding: 8px 16px;
font-size: 11px;
font-weight: 600;
color: var(--o-g700);
background: var(--o-g100);
border-bottom: 1px solid var(--o-g200);
}
.ta-c {
text-align: center;
}
.ta-r {
text-align: right;
}
.mono {
font-family: 'DM Mono', monospace;
font-size: 12px;
}
/* NOTES */
.oss-notes {
background: var(--o-g100);
border: 1px solid var(--o-g200);
border-radius: 8px;
padding: 10px 16px;
}
.oss-note-line {
font-size: 12px;
font-weight: 600;
color: var(--o-g700);
padding: 2px 0;
}
.oss-foot-note {
margin: -6px 2px 0;
font-size: 11px;
color: var(--o-g500);
}
/* Tie-up status pills (cross-return reconciliation checks). */
.report-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
&.report-ok {
background: var(--o-green-l);
color: var(--o-green);
}
&.report-bad {
background: var(--o-red-l);
color: var(--o-red);
}
}
.oss-sign-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
/* TOAST */
.oss-toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--o-navy);
color: #fff;
padding: 12px 22px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 6px 20px rgb(0 0 0 / 25%);
opacity: 0;
pointer-events: none;
transition: all 0.25s;
z-index: 1500;
&.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@media (width <= 1000px) {
.oss-sign-grid {
grid-template-columns: 1fr;
}
}
-228
View File
@@ -1,228 +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 * as ExcelJS from 'exceljs';
import {
MatrixTable,
ReturnGeneralInfoRow,
ReturnLine,
ReturnSignatory,
SIGNATORY_FIELDS
} from './oss-return.model';
/** Palette lifted from the RBI CIMS workbooks so exports look like the originals. */
export const XL = {
banner: 'FF002F5F',
bannerText: 'FFFFFFFF',
headerGrey: 'FFC0C0C0',
derivedBlue: 'FF9CC3F5',
auditYellow: 'FFFFFFCC',
keyCell: 'FFD9D9D9',
border: 'FF000000'
};
const AMOUNT_FMT = '#,##0.00';
const COUNT_FMT = '#,##0';
export function solidFill(argb: string): ExcelJS.Fill {
return { type: 'pattern', pattern: 'solid', fgColor: { argb } };
}
/** Dark-blue full-width banner that heads every sheet in the RBI templates. */
export function addBanner(sheet: ExcelJS.Worksheet, title: string, lastCol: string): void {
const row = sheet.addRow([title]);
sheet.mergeCells(`A${row.number}:${lastCol}${row.number}`);
row.height = 26;
const cell = row.getCell(1);
cell.fill = solidFill(XL.banner);
cell.font = { bold: true, size: 13, color: { argb: XL.bannerText } };
cell.alignment = { vertical: 'middle', horizontal: 'center' };
sheet.addRow([]);
}
export function boxAllCells(
sheet: ExcelJS.Worksheet,
from: number,
to: number,
lastColIndex: number
): void {
for (let r = from; r <= to; r++) {
for (let c = 1; c <= lastColIndex; c++) {
sheet.getRow(r).getCell(c).border = {
top: { style: 'thin', color: { argb: XL.border } },
left: { style: 'thin', color: { argb: XL.border } },
bottom: { style: 'thin', color: { argb: XL.border } },
right: { style: 'thin', color: { argb: XL.border } }
};
}
}
}
export function addSectionHeader(sheet: ExcelJS.Worksheet, label: string, unitLabel?: string): void {
const row = sheet.addRow([label, unitLabel ?? '']);
row.font = { bold: true };
row.eachCell((cell) => (cell.fill = solidFill(XL.headerGrey)));
row.getCell(2).alignment = { horizontal: 'right' };
}
/** Renders a labelled block; indents by level and shades derived totals. */
export function addLineRows(sheet: ExcelJS.Worksheet, lines: ReturnLine[]): void {
for (const line of lines) {
const row = sheet.addRow([line.label, line.value]);
const labelCell = row.getCell(1);
const valueCell = row.getCell(2);
labelCell.alignment = { wrapText: true, vertical: 'middle', indent: line.level };
if (line.level === 0) {
labelCell.font = { bold: true };
}
valueCell.numFmt = line.count ? COUNT_FMT : AMOUNT_FMT;
valueCell.alignment = { horizontal: 'right' };
if (line.total) {
valueCell.fill = solidFill(XL.derivedBlue);
valueCell.font = { bold: true };
}
}
}
/**
* Writes a multi-column statement: title band, column header row, then the matrix
* rows. Returns the row number after the block so callers can keep stacking.
*/
export function addMatrixTable(sheet: ExcelJS.Worksheet, table: MatrixTable): void {
const colCount = table.columns.length + 1;
const start = sheet.rowCount + 1;
const titleRow = sheet.addRow([table.title, ...(table.unit ? [table.unit] : [])]);
if (!table.unit) {
sheet.mergeCells(titleRow.number, 1, titleRow.number, colCount);
}
titleRow.font = { bold: true };
for (let c = 1; c <= colCount; c++) {
titleRow.getCell(c).fill = solidFill(XL.headerGrey);
}
if (table.subtitle) {
const sub = sheet.addRow([table.subtitle]);
sheet.mergeCells(sub.number, 1, sub.number, colCount);
sub.font = { bold: true, size: 10 };
sub.alignment = { wrapText: true };
sub.getCell(1).fill = solidFill(XL.headerGrey);
}
const head = sheet.addRow(['Particulars', ...table.columns]);
head.font = { bold: true };
head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const row of table.rows) {
const excelRow = sheet.addRow([row.label, ...row.values]);
const labelCell = excelRow.getCell(1);
labelCell.alignment = { wrapText: true, vertical: 'middle', indent: row.level };
if (row.level === 0 || row.header) {
labelCell.font = { bold: true };
}
if (row.header) {
labelCell.fill = solidFill(XL.headerGrey);
continue;
}
for (let c = 2; c <= colCount; c++) {
const cell = excelRow.getCell(c);
cell.numFmt = row.percent ? '0.00"%"' : AMOUNT_FMT;
cell.alignment = { horizontal: 'right' };
if (row.total) {
cell.fill = solidFill(XL.derivedBlue);
cell.font = { bold: true };
}
}
}
boxAllCells(sheet, start, sheet.rowCount, colCount);
for (const note of table.notes ?? []) {
const noteRow = sheet.addRow([note]);
sheet.mergeCells(noteRow.number, 1, noteRow.number, colCount);
noteRow.font = { bold: true, size: 9 };
noteRow.alignment = { wrapText: true };
noteRow.getCell(1).fill = solidFill(XL.headerGrey);
}
sheet.addRow([]);
}
/** The General Information sheet is identical in shape across every OSS return. */
export function buildGeneralInfoSheet(wb: ExcelJS.Workbook, rows: ReturnGeneralInfoRow[]): void {
const sheet = wb.addWorksheet('General Information');
sheet.columns = [
{ width: 26 },
{ width: 58 }
];
addBanner(sheet, 'General Information', 'B');
const first = sheet.rowCount + 1;
for (const item of rows) {
const row = sheet.addRow([item.label, item.value]);
row.getCell(1).fill = solidFill(XL.keyCell);
// The workbook tints the audit flag yellow and leaves unfilled cells grey.
const fill = item.label === 'Audit Flag' ? XL.auditYellow : item.value ? XL.derivedBlue : XL.headerGrey;
row.getCell(2).fill = solidFill(fill);
}
boxAllCells(sheet, first, sheet.rowCount, 2);
}
/** The Signatory sheet is likewise identical across returns. */
export function buildSignatorySheet(
wb: ExcelJS.Workbook,
signatories: ReturnSignatory[],
sheetName = 'Signatory'
): void {
const sheet = wb.addWorksheet(sheetName);
sheet.columns = [
{ width: 44 },
{ width: 24 }
];
addBanner(sheet, sheetName, 'B');
for (const person of signatories) {
const start = sheet.rowCount + 1;
const head = sheet.addRow([person.heading, '']);
head.font = { bold: true };
head.alignment = { horizontal: 'center' };
head.getCell(1).fill = solidFill(XL.keyCell);
for (const f of SIGNATORY_FIELDS) {
const row = sheet.addRow([person[f.key], f.caption]);
row.getCell(2).font = { bold: true };
row.getCell(2).alignment = { horizontal: 'center' };
row.getCell(2).fill = solidFill(XL.keyCell);
}
boxAllCells(sheet, start, sheet.rowCount, 2);
sheet.addRow([]);
}
}
/** Serialises the workbook and triggers a browser download. */
export async function downloadWorkbook(wb: ExcelJS.Workbook, filename: string): Promise<void> {
const buffer = await wb.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export function newWorkbook(): ExcelJS.Workbook {
const wb = new ExcelJS.Workbook();
wb.creator = 'CBSNexora';
wb.created = new Date();
return wb;
}
@@ -1,431 +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/.
*/
/**
* OSS 1 — Return on Assets and Liabilities (RAL), return code R070.
*
* Mirrors the RBI CIMS "Form RAL_CIMS Template1 OSS 1" workbook sheet-for-sheet:
* General Information / Assets & Liabilities / Annexure 1 / Annexure 2 / Signatory
*
* All monetary values are Rs in Thousands, exactly as the return prescribes.
* A `null` value means the source return leaves that cell blank — blank and zero
* are different to RBI, so they must not be collapsed into one another.
*/
import { OSS_BANK, OSS_SIGNATORIES, ReturnGeneralInfoRow, ReturnLine, ReturnSignatory } from '../oss-return.model';
/** One labelled row of the return. */
export type RalLine = ReturnLine;
export interface RalTopDeposit {
srNo: number;
institution: string;
amount: number | null;
rateOfInterest: number | null;
dateOfReceipt: string;
days: number | null;
months: number | null;
years: number | null;
}
export interface RalAgeingRow {
period: string;
noOfDebitEntries: number | null;
amtOfDebitEntries: number | null;
noOfCreditEntries: number | null;
amtOfCreditEntries: number | null;
}
export interface RalUnsecuredGuarantee {
srNo: number;
borrower: string;
beneficiary: string;
amountOutstanding: number | null;
}
export interface RalSundryDebtor {
srNo: number;
dueFromName: string;
amountDue: number | null;
provisionMade: number | null;
}
export type RalSignatory = ReturnSignatory;
/**
* Sheet 1 — General Information.
* Return Name / Return Code are fixed by RBI; everything else is this bank's own
* identity and the quarter being reported.
*/
export const RAL_GENERAL_INFO: ReturnGeneralInfoRow[] = [
{ label: 'Return Name', value: 'Return on Assets and Liabilities (RAL)' },
{ label: 'Return Code', value: 'R070' },
{ label: 'Bank Name', value: OSS_BANK.name },
{ label: 'Bank Code', value: OSS_BANK.bankCode },
{ label: 'RO Name', value: OSS_BANK.roName },
{ label: 'Reporting Frequency', value: OSS_BANK.reportingFrequency },
{ label: 'Quarter End Date', value: OSS_BANK.quarterEndDate },
{ label: 'Audit Flag', value: OSS_BANK.auditFlag },
{ label: 'Date of Audit', value: OSS_BANK.dateOfAudit },
{ label: 'Return Version', value: OSS_BANK.returnVersion },
{ label: 'Start Date', value: OSS_BANK.startDate }
];
/** Sheet 2 — Assets & Liabilities :: Liabilities */
export const RAL_LIABILITIES: RalLine[] = [
{ label: '1. Paid-up Capital', level: 0, value: 27278, total: true },
{ label: 'i. Individuals', level: 1, value: 27278 },
{ label: 'ii. State Government', level: 1, value: null },
{ label: 'iii. Others', level: 1, value: null },
{ label: '2. Reserve Fund and other Reserves', level: 0, value: 60863, total: true },
{ label: 'i. Statutory Reserve Fund', level: 1, value: 31149 },
{ label: 'ii. Building Fund', level: 1, value: 13642 },
{ label: 'iii. Dividend Equalization Fund', level: 1, value: 36 },
{ label: 'iv. Revaluation Reserve', level: 1, value: null },
{ label: 'v. Provision for Standard Assets', level: 1, value: 3750 },
{ label: 'vi. General Provisions (other than provision for standard advances)', level: 1, value: 282 },
{
label: 'vii. Other Funds and Reserves not in the nature of outside liability-(Refer Annexure I)',
level: 1,
value: 12004
},
{ label: '3. Profit (+) / Loss (-)', level: 0, value: 10397, total: true },
{ label: 'i. Surplus Unallocated and Carried over from Previous years', level: 1, value: 5592 },
{ label: 'ii. Accumulated Losses of Previous years', level: 1, value: null },
{ label: 'iii. Operating Surplus in Current year', level: 1, value: 4805 },
{ label: 'iv. Operating Deficit in Current year', level: 1, value: null },
{ label: '4. Total Capital And Reserves', level: 0, value: 98538, total: true },
{ label: '5. Subordinated Debt (Tier II Capital) #', level: 0, value: null },
{ label: '6. Deposits and other Accounts', level: 0, value: 634309, total: true },
{ label: 'Customer Deposits -', level: 1, value: 634309, total: true },
{ label: 'Total No of Depositors', level: 1, value: 8156, total: true, count: true },
{ label: 'A. Resident Deposits', level: 1, value: 634309, total: true },
{
label:
'i. Term Deposits (including recurring deposits, cash certificates, staff provident funds,security deposits and matured term deposits)',
level: 2,
value: 406601
},
{ label: 'No. of Term Depositors', level: 2, value: 3462, count: true },
{ label: 'ii. Savings Bank Deposits', level: 2, value: 208116 },
{ label: 'No of Savings Depositors', level: 2, value: 4455, count: true },
{ label: 'iii. Current Deposits', level: 2, value: 19592 },
{ label: 'No of Current Depositors', level: 2, value: 239, count: true },
{ label: 'B. Non Resident Deposits', level: 1, value: 0, total: true },
{ label: '1. NRI Deposits (Other than deposits in NRO Accounts)', level: 2, value: 0, total: true },
{ label: 'i. FCNR Deposits', level: 2, value: null },
{ label: 'No of FCNR Depositors', level: 2, value: null, count: true },
{ label: 'ii. NRE Deposits', level: 2, value: null },
{ label: 'No of NRE Depositors', level: 2, value: null, count: true },
{ label: '2. NRO Deposits', level: 2, value: null, total: true },
{ label: 'No of NRO Depositors', level: 2, value: null, count: true },
{ label: 'C. Deposit of Banks', level: 1, value: 0, total: true },
{ label: 'i. Commercial Banks', level: 2, value: null },
{ label: 'ii. Scheduled Cooperative Banks', level: 2, value: null },
{ label: 'iii. Other Cooperative Banks', level: 2, value: null },
{ label: '7. Borrowings from', level: 0, value: 0, total: true },
{ label: 'i. RBI / NABARD / SIDBI / NHB', level: 1, value: null },
{ label: 'ii. State/Central Coop. Bank of the State/ District', level: 1, value: 0, total: true },
{ label: 'a. Short-term loans', level: 2, value: null },
{ label: 'b. Medium-term loans', level: 2, value: null },
{ label: 'c. Cash credits and overdrafts', level: 2, value: null },
{ label: 'iii. Other banks (exclude borrowings from call money market)', level: 1, value: null },
{ label: 'iv. Call money market', level: 1, value: null },
{ label: 'v. Long Term Deposits', level: 1, value: null },
{ label: 'vi. Others', level: 1, value: null },
{ label: '8. Bills for collection being bills receivable as per contra', level: 0, value: null },
{ label: '9. Branch Adjustments *', level: 0, value: null },
{ label: '10. Overdue Interest Reserve', level: 0, value: 32096 },
{ label: '11. Interest Payable on', level: 0, value: 731, total: true },
{ label: 'i. Deposits', level: 1, value: 731 },
{ label: 'ii. Borrowings', level: 1, value: null },
{
label:
'12. Risk Provisions Other than netted off assets (BDDR, Spl BDDR, Depreciation on fixed assets etc.,)',
level: 0,
value: 39440,
total: true
},
{ label: 'i. For investments (Investment Depreciation Reserve)', level: 1, value: 3750 },
{ label: 'ii. BDDR, Spl BDDR', level: 1, value: 24341 },
{ label: 'iii. Depreciation on fixed assets', level: 1, value: null },
{ label: 'iv. For other impaired assets', level: 1, value: 11349 },
{ label: 'v. For contingent / non-funded exposures', level: 1, value: null },
{ label: '13. Other Liabilities', level: 0, value: 5806, total: true },
{ label: 'i. Bills payable', level: 1, value: 291 },
{ label: 'ii. Unclaimed dividends', level: 1, value: null },
{ label: 'iii.Provident fund payable to Regional Provident Fund Commissioner', level: 1, value: null },
{ label: 'iv.Others', level: 1, value: 5515 },
{ label: '14. Total Outside Liabilities', level: 0, value: 712382, total: true },
{ label: '15. Total Liabilities', level: 0, value: 810920, total: true },
{ label: '16. Contingent Liabilities', level: 0, value: 2638, total: true },
{ label: 'i. Claims against the bank not acknowledged as debt', level: 1, value: null },
{ label: 'ii. Liabilities for partly paid investments', level: 1, value: null },
{ label: 'iii. Letter of Credit Documentary', level: 1, value: null },
{ label: 'iv. Guarantees - Financial', level: 1, value: null },
{ label: 'v. Guarantees- Others (Indemnity/ Tender/ Bid Bonds)', level: 1, value: null },
{ label: 'vi. Sale and Repurchase Agreement /Asset sales with recourse', level: 1, value: null },
{ label: 'vii. Liability on account of outstanding forward exchange contracts', level: 1, value: null },
{ label: 'viii. Acceptances, endorsements and other obligations', level: 1, value: null },
{ label: 'ix. Others', level: 1, value: 2638 }
];
/** Sheet 2 — Assets & Liabilities :: Assets */
export const RAL_ASSETS: RalLine[] = [
{ label: '1. Cash in Hand', level: 0, value: 9996 },
{ label: '2. I. Due from Banks', level: 0, value: 57600, total: true },
{ label: 'A. Balances with Banks', level: 1, value: 57600, total: true },
{ label: 'i. State/ Central Cooperative Bank of the State/District concerned in', level: 2, value: 32268, total: true },
{ label: 'a. Current', level: 2, value: 5980 },
{ label: 'b. Savings', level: 2, value: null },
{ label: 'c. Fixed', level: 2, value: 26288 },
{ label: 'ii. RBI :', level: 2, value: 0, total: true },
{ label: 'a. Current', level: 2, value: null },
{ label: 'b. Others', level: 2, value: null },
{ label: 'iii. SBI and other notified banks :', level: 2, value: 25332, total: true },
{ label: 'a. Current', level: 2, value: 13667 },
{ label: 'b. Savings', level: 2, value: null },
{ label: 'c. Fixed', level: 2, value: 11665 },
{ label: 'B. Loans and Advances to Banks (including bills)', level: 1, value: 0, total: true },
{ label: 'i. Call and Short Notice Money to Cooperative Banks', level: 2, value: null },
{ label: 'ii. Call and Short Notice Money to Other Banks', level: 2, value: null },
{ label: 'iii. Loans and Advances to Scheduled Cooperative Banks', level: 2, value: null },
{ label: 'iv.Loans and advances to Non scheduled Cooperative Banks', level: 2, value: null },
{ label: 'v. Loans and Advances to Other Banks', level: 2, value: null },
{
label: '2. II. Due from Other institutions (including post office / treasuries / others)',
level: 0,
value: 77353,
total: true
},
{ label: 'a. Current', level: 1, value: 9954 },
{ label: 'b. Fixed', level: 1, value: 67399 },
{ label: 'c. Others', level: 1, value: null },
{ label: '3. Investments', level: 0, value: 234297, total: true },
{
label: 'A. SLR Investments in Government and Other Approved Central and State Govt Securities',
level: 1,
value: 211734,
total: true
},
{ label: 'i. GOI T. Bills', level: 2, value: null },
{ label: 'ii. GOI (dated) Securities', level: 2, value: 63839 },
{ label: 'iii. State Government Securities', level: 2, value: 147895 },
{
label: 'iv. Other Trustee/approved securities including Land Development Bank debentures',
level: 2,
value: null
},
{ label: 'B. Non - SLR Investments', level: 1, value: 22563, total: true },
{ label: 'B1. Other Debt Securities', level: 2, value: 0, total: true },
{ label: 'i. Other State Government Securities', level: 2, value: null },
{ label: 'ii. Bonds of PSUs / All India Financial Institutions', level: 2, value: null },
{ label: 'B2. Equities / Units of Mutual Funds', level: 2, value: 63, total: true },
{ label: 'i. Shares in Cooperative Institutions', level: 2, value: 63 },
{ label: 'ii. Shares of All India Financial Institutions', level: 2, value: null },
{ label: 'iii. Units of Mutual Funds (UTI,etc)', level: 2, value: null },
{ label: 'B3. Investment in Subsidiaries / Equity Participations', level: 2, value: null },
{ label: 'B4. Other Investments', level: 2, value: 22500 },
{ label: '4. Loans and Advances', level: 0, value: 391288, total: true },
{ label: 'A. Gross Loans and Advances: (Other than banks)', level: 1, value: 391288, total: true },
{ label: 'Total No of Borrowers', level: 1, value: 363, total: true, count: true },
{ label: 'i. Short term loans', level: 2, value: 125082, total: true },
{ label: 'No of Borrowers', level: 2, value: 137, count: true },
{ label: 'a. Secured', level: 2, value: 125082 },
{ label: 'b. Unsecured', level: 2, value: null },
{ label: 'ii. Cash credits and overdrafts', level: 2, value: 166614, total: true },
{ label: 'No of Borrowers', level: 2, value: 140, count: true },
{ label: 'a. Secured', level: 2, value: 166614 },
{ label: 'b. Unsecured', level: 2, value: null },
{ label: 'iii. Medium - term loans', level: 2, value: 70236, total: true },
{ label: 'No of Borrowers', level: 2, value: 66, count: true },
{ label: 'a. Secured', level: 2, value: 70070 },
{ label: 'b. Unsecured', level: 2, value: 166 },
{ label: 'iv. Long - term loans', level: 2, value: 29356, total: true },
{ label: 'No of Borrowers', level: 2, value: 20, count: true },
{ label: 'a. Secured', level: 2, value: 29356 },
{ label: 'b. Unsecured', level: 2, value: null },
{ label: 'v. Bills/hundies purchased/discounted', level: 2, value: 0, total: true },
{ label: 'No of Borrowers', level: 2, value: null, count: true },
{ label: 'a. Documentary', level: 2, value: null },
{ label: 'b. Clean', level: 2, value: null },
{ label: 'B. Netting items on Loans and Advances', level: 1, value: 0, total: true },
{ label: 'i. Unrealised Interest in Suspense', level: 2, value: null },
{ label: 'ii. Credit Recoveries in Suspense', level: 2, value: null },
{ label: 'iii. Provisions for Credit Losses ( funded credits)', level: 2, value: null },
{ label: 'C. Loans and Advances (Net)', level: 1, value: 391288, total: true },
{ label: '5. Interest Receivable on', level: 0, value: 33541, total: true },
{ label: 'i. Investments', level: 1, value: 1445 },
{ label: 'ii.Loans and Advances', level: 1, value: 32096 },
{ label: 'Of 5 (ii) above, overdues', level: 1, value: 32096 },
{ label: '6. Bills Receivable being bills for collection as per contra', level: 0, value: null },
{ label: '7. Premises (less depreciation)', level: 0, value: 3185 },
{ label: '8. Furniture and Fixtures (less depreciation)', level: 0, value: 1763 },
{ label: '9. Branch Adjustments **', level: 0, value: null },
{ label: '10. Other Assets', level: 0, value: 1897, total: true },
{ label: 'i. Advance Tax Paid and TDS (net)', level: 1, value: null },
{ label: 'ii. Amounts due from RBI', level: 1, value: null },
{ label: 'iii. Sundry Debtors', level: 1, value: null },
{ label: 'iv. Intangible Assets', level: 1, value: null },
{ label: 'v. All Other', level: 1, value: 1897 },
{ label: '11. Non-banking Assets acquired in satisfaction of claims', level: 0, value: null },
{ label: '12. Total Assets (1 to 11)', level: 0, value: 810920, total: true }
];
/** Sheet 2 — Memo block */
export const RAL_MEMO: RalLine[] = [
{ label: '1.Capital Infusion during Apr to Date', level: 0, value: 0, total: true },
{ label: '1.1 Tier 1 Capital Raised during April to Date', level: 1, value: null },
{ label: '1.2 Tier 2 Capital Raised during April to Date', level: 1, value: null },
{ label: '2. Capital Instruments due for redemption in next twelve months', level: 0, value: null }
];
export const RAL_BALANCE_SHEET_NOTES: string[] = [
'Note : * To be furnished as per the books of the bank.',
'Note : ** Branch adjustments: Net position to be shown as an item of liability / assets, as the case may be.',
'Note: # Capital raised by way of RCPS / RNCPS / PNCPS to be reported under this item'
];
/** Sheet 3 — Annexure 1 :: 1. Break-up of Other Funds and Reserves */
export const RAL_OTHER_FUNDS: RalLine[] = [
{ label: 'i. Gratuity Fund', level: 1, value: null },
{ label: 'ii. Staff Welfare Fund', level: 1, value: 1310 },
{ label: 'iii. Silver Jubilee Fund', level: 1, value: null },
{ label: 'iv. Development Fund', level: 1, value: 36 },
{ label: 'v. Charity Fund', level: 1, value: 1718 },
{ label: 'vi. Any Other Fund/ Disclosed reserves', level: 1, value: 8940 },
{
label:
'vii. Undisclosed Reserves created out of PAT and included in B/S under heads like other liabilities and provisions',
level: 1,
value: null
},
{ label: 'Total', level: 0, value: 12004, total: true }
];
/** Sheet 3 — Annexure 1 :: 2. Top 20 deposits from other banks / institutions */
export const RAL_TOP_20_DEPOSITS: RalTopDeposit[] = Array.from(
{ length: 20 },
(_, i): RalTopDeposit => ({
srNo: i + 1,
institution: '',
amount: null,
rateOfInterest: null,
dateOfReceipt: '',
days: null,
months: null,
years: null
})
);
/** Column captions shared by the two age-wise analyses in Annexure 1. */
export const RAL_AGEING_HEADERS: string[] = [
'Time period',
'No. of debit entries',
'Amt. of debit entries',
'No. of credit entries',
'Amt. of credit entries'
];
/** Sheet 3 — Annexure 1 :: 3. Age-wise Inter Branch Adjustment Account */
export const RAL_INTER_BRANCH_AGEING: RalAgeingRow[] = [
{ period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 }
];
/** Sheet 3 — Annexure 1 :: 4. Age-wise Inter Bank Reconciliation Account */
export const RAL_INTER_BANK_AGEING: RalAgeingRow[] = [
{ period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
{ period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 }
];
/** Sheet 3 — Annexure 1 :: 5. Unsecured guarantees issued by the bank */
export const RAL_UNSECURED_GUARANTEES: RalUnsecuredGuarantee[] = [
{ srNo: 1, borrower: '', beneficiary: '', amountOutstanding: null }
];
export const RAL_UNSECURED_GUARANTEES_TOTAL = 0;
/** Sheet 4 — Annexure 2 :: Sundry Debtors ageing buckets */
export const RAL_SUNDRY_DEBTORS: { title: string; subtitle: string; rows: RalSundryDebtor[] }[] = [
{
title: 'Sundry Debtors Account -1',
subtitle: 'Long Outstanding Large Amounts - Outstanding for over 6 months but less than 1 year*',
rows: [{ srNo: 1, dueFromName: '', amountDue: null, provisionMade: null }]
},
{
title: 'Sundry Debtors Account -2',
subtitle: 'Long Outstanding Large Amounts - Outstanding for over 1 year but less than 3 years*',
rows: [{ srNo: 1, dueFromName: '', amountDue: null, provisionMade: null }]
},
{
title: 'Sundry Debtors Account -3',
subtitle: 'Long Outstanding Large Amounts - Outstanding for over 3 years*',
rows: [{ srNo: 1, dueFromName: '', amountDue: null, provisionMade: null }]
}
];
export const RAL_SUNDRY_DEBTORS_NOTE =
'* Large Amounts: Rs. 2 lakhs for banks having total assets of Rs. 100 lakhs and over Rs. 50,000 for other banks';
/** Sheet 4 — Annexure 2 :: Details of Depositors & Borrowers */
export const RAL_DEPOSITORS_BORROWERS: RalLine[] = [
{ label: '1.Total No of Depositors', level: 0, value: 8156, total: true, count: true },
{ label: 'Total Customer Deposits', level: 0, value: 634309, total: true },
{ label: 'a. i) Of which Total No. of Depositors with Deposits less than Rs. 1 Lakh', level: 1, value: 6568, count: true },
{ label: 'ii) Of which total Deposit Amount', level: 1, value: 109992 },
{ label: 'b. i) Of which Total No. of Depositors with Deposits Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 739, count: true },
{ label: 'ii) Of which total Deposit Amount', level: 1, value: 102475 },
{ label: 'c. i) Of which Total No. of Depositors with Deposits Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 580, count: true },
{ label: 'ii) Of which total Deposit Amount', level: 1, value: 176820 },
{ label: '2.Total No of Borrowers', level: 0, value: 363, total: true, count: true },
{ label: 'Total Loans and Advances (Other than Banks)', level: 0, value: 391288, total: true },
{ label: 'a. i) Of which Total No. of Borrowers with Loans and Advances less than Rs. 1 Lakh', level: 1, value: 55, count: true },
{ label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2061 },
{ label: 'b. i) Of which Total No. of Borrowers with Loans and Advances Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 18, count: true },
{ label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2837 },
{ label: 'c. i) Of which Total No. of Borrowers with Loans and Advances Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 55, count: true },
{ label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 21260 }
];
/** Sheet 4 — Annexure 2 :: Complaints Received from public */
export const RAL_COMPLAINTS: RalLine[] = [
{ label: 'a) No. Of complaints outstanding at the beginning of the quarter', level: 1, value: 0, count: true },
{ label: 'b) No. Of complaints received during the quarter', level: 1, value: 0, count: true },
{ label: 'c) No. of complaints addressed during the quarter', level: 1, value: 0, count: true },
{ label: 'd) No. of complaints outstanding at the end of the quarter', level: 1, value: 0, count: true },
{ label: 'e) No. of complaints outstanding for over 2 quarters', level: 1, value: 0, count: true }
];
/** Sheet 5 — Signatory. Shared across returns so OSS 1 and OSS 2 always agree. */
export const RAL_SIGNATORIES: RalSignatory[] = OSS_SIGNATORIES;
@@ -1,422 +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="oss-root">
<!-- HEADER -->
<div class="oss-head">
<div class="oss-head-left">
<fa-icon icon="university" class="oss-head-ic"></fa-icon>
<div>
<h2 class="oss-head-t">OSS 1 — {{ returnName }}</h2>
<div class="oss-head-s">
{{ bankName }} &middot; Bank Code <strong>{{ bankCode }}</strong> &middot; Return Code
<strong>{{ returnCode }}</strong> &middot; Quarter ending <strong>{{ quarterEndDate }}</strong>
</div>
</div>
</div>
<div class="oss-head-right">
<span class="oss-audit-pill">{{ auditFlag }}</span>
<button class="oss-btn oss-btn-xls" [disabled]="generating" (click)="generateExcel()">
<fa-icon icon="file-csv"></fa-icon>
{{ generating ? 'Generating…' : 'Generate Excel' }}
</button>
</div>
</div>
<!-- BALANCE CHECK -->
<div class="oss-balance" [class.oss-balance--bad]="!isBalanced">
<fa-icon [icon]="isBalanced ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
Total Liabilities <strong>&#8377;{{ totalLiabilities | number: '1.2-2' }}</strong> &middot; Total Assets
<strong>&#8377;{{ totalAssets | number: '1.2-2' }}</strong> (Rs in Thousands) —
{{ isBalanced ? 'balance sheet tallies' : 'MISMATCH — resolve before filing with RBI' }}
</span>
</div>
<!-- TABS -->
<div class="oss-tabs">
<button class="oss-tab" [class.active]="activeTab === 'general'" (click)="showTab('general')">
<fa-icon icon="info-circle"></fa-icon> General Information
</button>
<button class="oss-tab" [class.active]="activeTab === 'balance-sheet'" (click)="showTab('balance-sheet')">
<fa-icon icon="university"></fa-icon> Assets &amp; Liabilities
</button>
<button class="oss-tab" [class.active]="activeTab === 'annexure1'" (click)="showTab('annexure1')">
<fa-icon icon="file-alt"></fa-icon> Annexure 1
</button>
<button class="oss-tab" [class.active]="activeTab === 'annexure2'" (click)="showTab('annexure2')">
<fa-icon icon="file-alt"></fa-icon> Annexure 2
</button>
<button class="oss-tab" [class.active]="activeTab === 'signatory'" (click)="showTab('signatory')">
<fa-icon icon="check-circle"></fa-icon> Signatory
</button>
</div>
<!-- GENERAL INFORMATION -->
@if (activeTab === 'general') {
<div class="oss-section">
<div class="oss-sheet-banner">General Information</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl oss-tbl--kv">
<tbody>
@for (item of generalInfo; track item.label) {
<tr>
<th>{{ item.label }}</th>
<td [class.filled]="item.value" [class.audit]="item.label === 'Audit Flag'">{{ item.value }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- ASSETS & LIABILITIES -->
@if (activeTab === 'balance-sheet') {
<div class="oss-section">
<div class="oss-sheet-banner">Assets &amp; Liabilities</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl">
<thead>
<tr>
<th>Liabilities</th>
<th class="ta-r">Rs in Thousands</th>
</tr>
</thead>
<tbody>
@for (line of liabilities; track $index) {
<tr [class.total-row]="line.total">
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
<td class="ta-r mono" [class.derived]="line.total">
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl">
<thead>
<tr>
<th>Assets</th>
<th class="ta-r">Rs in Thousands</th>
</tr>
</thead>
<tbody>
@for (line of assets; track $index) {
<tr [class.total-row]="line.total">
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
<td class="ta-r mono" [class.derived]="line.total">
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-notes">
@for (note of balanceSheetNotes; track note) {
<div class="oss-note-line">{{ note }}</div>
}
</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl">
<thead>
<tr>
<th>Memo</th>
<th class="ta-r">Rs in Thousands</th>
</tr>
</thead>
<tbody>
@for (line of memo; track $index) {
<tr [class.total-row]="line.total">
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
<td class="ta-r mono" [class.derived]="line.total">
{{ line.value === null ? '' : (line.value | number: '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- ANNEXURE 1 -->
@if (activeTab === 'annexure1') {
<div class="oss-section">
<div class="oss-sheet-banner">Annexure 1</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">1. Break-up of Other Funds and Reserves &mdash; Rs in Thousands</div>
<table class="oss-tbl">
<tbody>
@for (line of otherFunds; track line.label) {
<tr [class.total-row]="line.total">
<td>{{ line.label }}</td>
<td class="ta-r mono" [class.derived]="line.total">
{{ line.value === null ? '' : (line.value | number: '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">
2. Particulars of top 20 deposits accepted from other banks / and institutions as on the date of review
</div>
<table class="oss-tbl">
<thead>
<tr>
<th class="ta-c">Sr. No</th>
<th>Name of Bank / Institution</th>
<th class="ta-r">Amount of deposit</th>
<th class="ta-r">Rate of interest (%)</th>
<th>Date of receipt</th>
<th class="ta-c">Days</th>
<th class="ta-c">Months</th>
<th class="ta-c">Years</th>
</tr>
</thead>
<tbody>
@for (d of topDeposits; track d.srNo) {
<tr>
<td class="ta-c">{{ d.srNo }}</td>
<td>{{ d.institution }}</td>
<td class="ta-r mono">{{ d.amount === null ? '' : (d.amount | number: '1.2-2') }}</td>
<td class="ta-r mono">{{ d.rateOfInterest }}</td>
<td>{{ d.dateOfReceipt }}</td>
<td class="ta-c mono">{{ d.days }}</td>
<td class="ta-c mono">{{ d.months }}</td>
<td class="ta-c mono">{{ d.years }}</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">
3. Age-wise analysis of Inter Branch Adjustment Account &mdash; Position as on: {{ quarterEndDate }}
</div>
<table class="oss-tbl">
<thead>
<tr>
@for (h of ageingHeaders; track h) {
<th [class.ta-r]="!$first">{{ h }}</th>
}
</tr>
</thead>
<tbody>
@for (r of interBranchAgeing; track r.period) {
<tr [class.total-row]="r.period === 'Total'">
<td>{{ r.period }}</td>
<td class="ta-r mono">{{ r.noOfDebitEntries }}</td>
<td class="ta-r mono">
{{ r.amtOfDebitEntries === null ? '' : (r.amtOfDebitEntries | number: '1.2-2') }}
</td>
<td class="ta-r mono">{{ r.noOfCreditEntries }}</td>
<td class="ta-r mono">
{{ r.amtOfCreditEntries === null ? '' : (r.amtOfCreditEntries | number: '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">
4. Age-wise analysis of Inter Bank Reconciliation Account &mdash; Position as on: {{ quarterEndDate }}
</div>
<table class="oss-tbl">
<thead>
<tr>
@for (h of ageingHeaders; track h) {
<th [class.ta-r]="!$first">{{ h }}</th>
}
</tr>
</thead>
<tbody>
@for (r of interBankAgeing; track r.period) {
<tr [class.total-row]="r.period === 'Total'">
<td>{{ r.period }}</td>
<td class="ta-r mono">{{ r.noOfDebitEntries }}</td>
<td class="ta-r mono">
{{ r.amtOfDebitEntries === null ? '' : (r.amtOfDebitEntries | number: '1.2-2') }}
</td>
<td class="ta-r mono">{{ r.noOfCreditEntries }}</td>
<td class="ta-r mono">
{{ r.amtOfCreditEntries === null ? '' : (r.amtOfCreditEntries | number: '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">5. Unsecured guarantees issued by the bank</div>
<table class="oss-tbl">
<thead>
<tr>
<th class="ta-c">Sr. No</th>
<th>Name of Borrower</th>
<th>Name of Beneficiary</th>
<th class="ta-r">Amount Outstanding as on: {{ quarterEndDate }}</th>
</tr>
</thead>
<tbody>
@for (g of unsecuredGuarantees; track g.srNo) {
<tr>
<td class="ta-c">{{ g.srNo }}</td>
<td>{{ g.borrower }}</td>
<td>{{ g.beneficiary }}</td>
<td class="ta-r mono">
{{ g.amountOutstanding === null ? '' : (g.amountOutstanding | number: '1.2-2') }}
</td>
</tr>
}
<tr class="total-row">
<td colspan="3">Total</td>
<td class="ta-r mono derived">{{ unsecuredGuaranteesTotal | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
<p class="oss-foot-note">
Note: * Give name if borrower/beneficiary is director/top management/related entity. Other guarantees may be
consolidated and reported against others.
</p>
</div>
}
<!-- ANNEXURE 2 -->
@if (activeTab === 'annexure2') {
<div class="oss-section">
<div class="oss-sheet-banner">Annexure 2</div>
@for (block of sundryDebtors; track block.title) {
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">{{ block.title }} &mdash; {{ block.subtitle }}</div>
<table class="oss-tbl">
<thead>
<tr>
<th class="ta-c">Sr. No</th>
<th>Due from Name</th>
<th class="ta-r">Amount due</th>
<th class="ta-r">Provision made</th>
</tr>
</thead>
<tbody>
@for (r of block.rows; track r.srNo) {
<tr>
<td class="ta-c">{{ r.srNo }}</td>
<td>{{ r.dueFromName }}</td>
<td class="ta-r mono">{{ r.amountDue === null ? '' : (r.amountDue | number: '1.2-2') }}</td>
<td class="ta-r mono">{{ r.provisionMade === null ? '' : (r.provisionMade | number: '1.2-2') }}</td>
</tr>
}
</tbody>
</table>
</div>
}
<p class="oss-foot-note">{{ sundryDebtorsNote }}</p>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">Details of Depositors &amp; Borrowers &mdash; Rs in Thousands</div>
<table class="oss-tbl">
<tbody>
@for (line of depositorsBorrowers; track $index) {
<tr [class.total-row]="line.total">
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
<td class="ta-r mono" [class.derived]="line.total">
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">Complaints Received from public</div>
<table class="oss-tbl">
<tbody>
@for (line of complaints; track line.label) {
<tr>
<td>{{ line.label }}</td>
<td class="ta-r mono">{{ line.value }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- SIGNATORY -->
@if (activeTab === 'signatory') {
<div class="oss-section">
<div class="oss-sheet-banner">Signatory</div>
<div class="oss-sign-grid">
@for (p of signatories; track p.heading) {
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">{{ p.heading }}</div>
<table class="oss-tbl oss-tbl--kv">
<tbody>
<tr>
<td>{{ p.signature }}</td>
<th>Signature</th>
</tr>
<tr>
<td>{{ p.name }}</td>
<th>Name</th>
</tr>
<tr>
<td>{{ p.designation }}</td>
<th>Designation</th>
</tr>
<tr>
<td>{{ p.email }}</td>
<th>e-mail ID</th>
</tr>
<tr>
<td class="mono">{{ p.telephoneOffice }}</td>
<th>Tele. No. (O)</th>
</tr>
<tr>
<td class="mono">{{ p.telephoneResidence }}</td>
<th>Tele. No. (R)</th>
</tr>
<tr>
<td>{{ p.place }}</td>
<th>Place</th>
</tr>
<tr>
<td>{{ p.date }}</td>
<th>Date</th>
</tr>
</tbody>
</table>
</div>
}
</div>
</div>
}
<!-- TOAST -->
<div class="oss-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
@@ -1,423 +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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import * as ExcelJS from 'exceljs';
import {
addBanner,
addLineRows,
addSectionHeader,
boxAllCells,
buildGeneralInfoSheet,
buildSignatorySheet,
downloadWorkbook,
newWorkbook,
solidFill,
XL
} from '../oss-workbook';
import {
RAL_AGEING_HEADERS,
RAL_ASSETS,
RAL_BALANCE_SHEET_NOTES,
RAL_COMPLAINTS,
RAL_DEPOSITORS_BORROWERS,
RAL_GENERAL_INFO,
RAL_INTER_BANK_AGEING,
RAL_INTER_BRANCH_AGEING,
RAL_LIABILITIES,
RAL_MEMO,
RAL_OTHER_FUNDS,
RAL_SIGNATORIES,
RAL_SUNDRY_DEBTORS,
RAL_SUNDRY_DEBTORS_NOTE,
RAL_TOP_20_DEPOSITS,
RAL_UNSECURED_GUARANTEES,
RAL_UNSECURED_GUARANTEES_TOTAL
} from './oss1-ral-data';
type Oss1Tab = 'general' | 'balance-sheet' | 'annexure1' | 'annexure2' | 'signatory';
@Component({
selector: 'mifosx-oss1-ral',
templateUrl: './oss1-ral.component.html',
styleUrls: ['../oss-return.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class Oss1RalComponent {
activeTab: Oss1Tab = 'general';
generalInfo = RAL_GENERAL_INFO;
liabilities = RAL_LIABILITIES;
assets = RAL_ASSETS;
memo = RAL_MEMO;
balanceSheetNotes = RAL_BALANCE_SHEET_NOTES;
otherFunds = RAL_OTHER_FUNDS;
topDeposits = RAL_TOP_20_DEPOSITS;
interBranchAgeing = RAL_INTER_BRANCH_AGEING;
interBankAgeing = RAL_INTER_BANK_AGEING;
ageingHeaders = RAL_AGEING_HEADERS;
unsecuredGuarantees = RAL_UNSECURED_GUARANTEES;
unsecuredGuaranteesTotal = RAL_UNSECURED_GUARANTEES_TOTAL;
sundryDebtors = RAL_SUNDRY_DEBTORS;
sundryDebtorsNote = RAL_SUNDRY_DEBTORS_NOTE;
depositorsBorrowers = RAL_DEPOSITORS_BORROWERS;
complaints = RAL_COMPLAINTS;
signatories = RAL_SIGNATORIES;
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: any;
showTab(tab: Oss1Tab): void {
this.activeTab = tab;
}
private info(label: string): string {
return this.generalInfo.find((g) => g.label === label)?.value ?? '';
}
get returnName(): string {
return this.info('Return Name');
}
get returnCode(): string {
return this.info('Return Code');
}
get bankName(): string {
return this.info('Bank Name');
}
get bankCode(): string {
return this.info('Bank Code');
}
get quarterEndDate(): string {
return this.info('Quarter End Date');
}
get auditFlag(): string {
return this.info('Audit Flag');
}
/** Total Liabilities must equal Total Assets — surfaced so a mismatch can't be filed unnoticed. */
get totalLiabilities(): number {
return this.liabilities.find((l) => l.label.startsWith('15.'))?.value ?? 0;
}
get totalAssets(): number {
return this.assets.find((a) => a.label.startsWith('12.'))?.value ?? 0;
}
get isBalanced(): boolean {
return this.totalLiabilities === this.totalAssets;
}
// ══════════════ Excel generation ══════════════
/** Rebuilds the RBI workbook: one sheet per section, original layout and styling. */
async generateExcel(): Promise<void> {
this.generating = true;
try {
const wb = newWorkbook();
buildGeneralInfoSheet(wb, this.generalInfo);
this.buildBalanceSheet(wb);
this.buildAnnexure1(wb);
this.buildAnnexure2(wb);
buildSignatorySheet(wb, this.signatories);
await downloadWorkbook(wb, `Form RAL_CIMS ${this.returnCode} OSS 1 - ${this.quarterEndDate}.xlsx`);
this.toast(`${this.returnCode} workbook generated for quarter ending ${this.quarterEndDate}`);
} catch {
this.toast('Could not generate the workbook — please retry');
} finally {
this.generating = false;
}
}
private buildBalanceSheet(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('Assets & Liabilities');
sheet.columns = [
{ width: 78 },
{ width: 20 }
];
addBanner(sheet, 'Assets & Liabilities', 'B');
const first = sheet.rowCount + 1;
addSectionHeader(sheet, 'Liabilities', 'Rs in Thousands');
addLineRows(sheet, this.liabilities);
addSectionHeader(sheet, 'Assets', 'Rs in Thousands');
addLineRows(sheet, this.assets);
boxAllCells(sheet, first, sheet.rowCount, 2);
sheet.addRow([]);
const notesStart = sheet.rowCount + 1;
for (const note of this.balanceSheetNotes) {
const row = sheet.addRow([note]);
sheet.mergeCells(`A${row.number}:B${row.number}`);
row.font = { bold: true, size: 10 };
row.getCell(1).fill = solidFill(XL.headerGrey);
}
boxAllCells(sheet, notesStart, sheet.rowCount, 2);
sheet.addRow([]);
const memoStart = sheet.rowCount + 1;
addSectionHeader(sheet, 'Memo', 'Rs in Thousands');
addLineRows(sheet, this.memo);
boxAllCells(sheet, memoStart, sheet.rowCount, 2);
sheet.views = [{ state: 'frozen', ySplit: 3 }];
}
private buildAnnexure1(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('Annexure 1');
sheet.columns = [
{ width: 10 },
{ width: 46 },
{ width: 20 },
{ width: 18 },
{ width: 16 },
{ width: 12 },
{ width: 12 },
{ width: 12 }
];
addBanner(sheet, 'Annexure 1', 'H');
// 1. Break-up of Other Funds and Reserves
let start = sheet.rowCount + 1;
const fundsTitle = sheet.addRow(['1. Break-up of Other Funds and Reserves', '', 'Rs in Thousands']);
sheet.mergeCells(`A${fundsTitle.number}:B${fundsTitle.number}`);
fundsTitle.font = { bold: true };
fundsTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const line of this.otherFunds) {
const row = sheet.addRow([line.label, '', line.value]);
sheet.mergeCells(`A${row.number}:B${row.number}`);
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
row.getCell(3).numFmt = '#,##0.00';
if (line.total) {
row.font = { bold: true };
row.getCell(3).fill = solidFill(XL.derivedBlue);
row.getCell(1).alignment = { horizontal: 'center' };
}
}
boxAllCells(sheet, start, sheet.rowCount, 3);
sheet.addRow([]);
// 2. Top 20 deposits from other banks / institutions
start = sheet.rowCount + 1;
const depTitle = sheet.addRow([
`2. Particulars of top 20 deposits accepted from other banks / and institutions as on the date of review`
]);
sheet.mergeCells(`A${depTitle.number}:H${depTitle.number}`);
depTitle.font = { bold: true };
depTitle.getCell(1).fill = solidFill(XL.headerGrey);
const depHead = sheet.addRow([
'Sr. No',
'Name of Bank / Institution',
'Amount of deposit',
'Rate of interest (%)',
'Date of receipt',
'Period in Days',
'Period in Months',
'Period in Years'
]);
depHead.font = { bold: true };
depHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
depHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const d of this.topDeposits) {
const row = sheet.addRow([
d.srNo,
d.institution,
d.amount,
d.rateOfInterest,
d.dateOfReceipt,
d.days,
d.months,
d.years
]);
row.getCell(3).numFmt = '#,##0.00';
}
boxAllCells(sheet, start, sheet.rowCount, 8);
sheet.addRow([]);
// 3 & 4 — ageing analyses
start = sheet.rowCount + 1;
this.addAgeingBlock(
sheet,
`3. Age-wise analysis of Inter Branch Adjustment Account - Position as on:${this.quarterEndDate}`,
this.interBranchAgeing
);
sheet.addRow([]);
this.addAgeingBlock(
sheet,
`4. Age-wise analysis of Inter Bank Reconciliation Account - Position as on:${this.quarterEndDate}`,
this.interBankAgeing
);
sheet.addRow([]);
// 5. Unsecured guarantees
start = sheet.rowCount + 1;
const gTitle = sheet.addRow(['5. Unsecured guarantees issued by the bank']);
sheet.mergeCells(`A${gTitle.number}:D${gTitle.number}`);
gTitle.font = { bold: true };
gTitle.getCell(1).fill = solidFill(XL.headerGrey);
const gHead = sheet.addRow([
'Sr. No',
'Name of Borrower',
'Name of Beneficiary',
`Amount Outstanding as on:${this.quarterEndDate}`
]);
gHead.font = { bold: true };
gHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
gHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const g of this.unsecuredGuarantees) {
const row = sheet.addRow([g.srNo, g.borrower, g.beneficiary, g.amountOutstanding]);
row.getCell(4).numFmt = '#,##0.00';
}
const gTotal = sheet.addRow(['Total', '', '', this.unsecuredGuaranteesTotal]);
gTotal.font = { bold: true };
gTotal.getCell(4).numFmt = '#,##0.00';
gTotal.getCell(4).fill = solidFill(XL.derivedBlue);
const gNote = sheet.addRow([
'Note : * Give name if borrower/beneficiary is director/top management/related entity. Other guarantees may be consolidated and reported against others.'
]);
sheet.mergeCells(`A${gNote.number}:D${gNote.number}`);
gNote.font = { bold: true, size: 10 };
gNote.alignment = { wrapText: true };
boxAllCells(sheet, start, sheet.rowCount, 4);
}
private addAgeingBlock(sheet: ExcelJS.Worksheet, title: string, rows: typeof RAL_INTER_BRANCH_AGEING): void {
const start = sheet.rowCount + 1;
const titleRow = sheet.addRow([title]);
sheet.mergeCells(`A${titleRow.number}:E${titleRow.number}`);
titleRow.font = { bold: true };
titleRow.getCell(1).fill = solidFill(XL.headerGrey);
const head = sheet.addRow([...this.ageingHeaders]);
head.font = { bold: true };
head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const r of rows) {
const row = sheet.addRow([
r.period,
r.noOfDebitEntries,
r.amtOfDebitEntries,
r.noOfCreditEntries,
r.amtOfCreditEntries
]);
row.getCell(3).numFmt = '#,##0.00';
row.getCell(5).numFmt = '#,##0.00';
if (r.period === 'Total') {
row.font = { bold: true };
[
2,
3,
4,
5
].forEach(
(c) => (row.getCell(c).fill = solidFill(XL.derivedBlue))
);
}
}
boxAllCells(sheet, start, sheet.rowCount, 5);
}
private buildAnnexure2(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('Annexure 2');
sheet.columns = [
{ width: 10 },
{ width: 62 },
{ width: 20 },
{ width: 20 }
];
addBanner(sheet, 'Annexure 2', 'D');
for (const block of this.sundryDebtors) {
const start = sheet.rowCount + 1;
const t = sheet.addRow([block.title]);
sheet.mergeCells(`A${t.number}:D${t.number}`);
t.font = { bold: true };
t.getCell(1).fill = solidFill(XL.headerGrey);
const s = sheet.addRow([block.subtitle]);
sheet.mergeCells(`A${s.number}:D${s.number}`);
s.font = { bold: true, size: 10 };
s.getCell(1).fill = solidFill(XL.headerGrey);
const head = sheet.addRow(['Sr. No', 'Due from Name', 'Amount due', 'Provision made']);
head.font = { bold: true };
head.alignment = { horizontal: 'center' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const r of block.rows) {
const row = sheet.addRow([r.srNo, r.dueFromName, r.amountDue, r.provisionMade]);
row.getCell(3).numFmt = '#,##0.00';
row.getCell(4).numFmt = '#,##0.00';
}
const note = sheet.addRow([this.sundryDebtorsNote]);
sheet.mergeCells(`A${note.number}:D${note.number}`);
note.font = { bold: true, size: 10 };
note.alignment = { wrapText: true };
boxAllCells(sheet, start, sheet.rowCount, 4);
sheet.addRow([]);
}
// Details of Depositors & Borrowers
let start = sheet.rowCount + 1;
const dbTitle = sheet.addRow(['Details of Depositors & Borrowers', '', 'Rs. In Thousands']);
sheet.mergeCells(`A${dbTitle.number}:B${dbTitle.number}`);
dbTitle.font = { bold: true };
dbTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
dbTitle.getCell(3).alignment = { horizontal: 'right' };
for (const line of this.depositorsBorrowers) {
const row = sheet.addRow([line.label, '', line.value]);
sheet.mergeCells(`A${row.number}:B${row.number}`);
row.getCell(1).alignment = { wrapText: true, vertical: 'middle', indent: line.level };
row.getCell(3).numFmt = line.count ? '#,##0' : '#,##0.00';
if (line.total) {
row.font = { bold: true };
row.getCell(3).fill = solidFill(XL.derivedBlue);
}
}
boxAllCells(sheet, start, sheet.rowCount, 3);
sheet.addRow([]);
// Complaints
start = sheet.rowCount + 1;
const cTitle = sheet.addRow(['Complaints Received from public']);
sheet.mergeCells(`A${cTitle.number}:B${cTitle.number}`);
cTitle.font = { bold: true };
cTitle.getCell(1).fill = solidFill(XL.headerGrey);
for (const line of this.complaints) {
const row = sheet.addRow([line.label, '', line.value]);
sheet.mergeCells(`A${row.number}:B${row.number}`);
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
row.getCell(3).numFmt = '#,##0';
}
boxAllCells(sheet, start, sheet.rowCount, 3);
}
private toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
}
}
@@ -1,203 +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/.
*/
/**
* OSS 2 — Return on Earnings (RoE), return code R071.
*
* Mirrors the RBI CIMS "Form OSS-2_CIMS Template1" workbook sheet-for-sheet:
* General Information / Earnings / Interest Receivable Not Recognized /
* Other Operating Expenses / Signatory Information
*
* All monetary values are Rs in Thousands. A `null` value means the source return
* leaves that cell blank — RBI treats blank and zero differently, so the two are
* never collapsed into one another.
*/
import { OSS_BANK, OSS_SIGNATORIES, ReturnGeneralInfoRow, ReturnLine, ReturnSignatory } from '../oss-return.model';
export type RoeLine = ReturnLine;
export type RoeSignatory = ReturnSignatory;
/** A row on the Interest Receivable Not Recognized sheet. */
export interface RoeInterestNotRecognizedRow {
label: string;
value: number | null;
total?: boolean;
}
/** A line item on the Other Operating Expenses detail sheet. */
export interface RoeOtherExpense {
srNo: number;
particulars: string;
amount: number | null;
}
/** Sheet 1 — General Information */
export const ROE_GENERAL_INFO: ReturnGeneralInfoRow[] = [
{ label: 'Return Name', value: 'Return on Earnings (RoE)' },
{ label: 'Return Code', value: 'R071' },
{ label: 'Name Of Bank', value: OSS_BANK.name },
{ label: 'Bank Code', value: OSS_BANK.bankCode },
{ label: 'RO Name', value: OSS_BANK.roName },
{ label: 'Quarter End Date', value: OSS_BANK.quarterEndDate },
{ label: 'Reporting Frequency', value: OSS_BANK.reportingFrequency },
{ label: 'Audit Flag', value: OSS_BANK.auditFlag },
{ label: 'Date of Audit', value: OSS_BANK.dateOfAudit },
{ label: 'Return Version', value: OSS_BANK.returnVersion },
{ label: 'Start Date', value: OSS_BANK.startDate }
];
/** Sheet 2 — Earnings (items 1 to 33) */
export const ROE_EARNINGS: RoeLine[] = [
{ label: '1. Interest/discount received (2+3+4+5)', level: 0, value: 15069, total: true },
{
label: '2. Interest/discount received on loans and advances (other than from banks)',
level: 0,
value: 12070,
total: true
},
{ label: 'i) Actually received', level: 1, value: 12070 },
{ label: 'ii) Accrued interest on Standard advances', level: 1, value: null },
{ label: '3. Interest on market lending ( to banks, if any)', level: 0, value: null },
{ label: '4. Interest on investments', level: 0, value: 2999 },
{ label: 'i) Interest received /accrued on Standard Investments', level: 1, value: null },
{
label: '5. Interest on additional balance with RBI, inter-bank deposits, placements and credits',
level: 0,
value: null
},
{ label: '6. Other Operating income', level: 0, value: 285, total: true },
{ label: 'i) Fee, Commission, exchange and brokerage', level: 1, value: 51 },
{ label: 'ii) Profit (+) / Loss (-) on forex operations', level: 1, value: null },
{ label: 'iii) Profit (+) / Loss (-) on trading and sale of securities', level: 1, value: null },
{ label: 'iv) Dividend Income', level: 1, value: null },
{ label: 'v) Miscellaneous Income', level: 1, value: 234 },
{ label: '7. Total Operating Income [1+6]', level: 0, value: 15354, total: true },
{ label: '8. Non Operating Income [Capital gains / losses (+)/ (-)]', level: 0, value: 0, total: true },
{ label: 'i) Profit / (Loss) on sale of fixed assets (+) / (-)', level: 1, value: null },
{ label: 'ii) Profit / (Loss) on sale of other assets (+) / (-)', level: 1, value: null },
{ label: 'iii) Other Non-operating Income / (Expenses) (+) / (-)', level: 1, value: null },
{ label: '9. Total income[7+8]', level: 0, value: 15354, total: true },
{ label: '10. Interest paid', level: 0, value: 5659, total: true },
{ label: 'i) On Customer deposits', level: 1, value: 5659 },
{ label: 'ii) On Inter-bank deposits', level: 1, value: null },
{ label: 'iii) On Inter-bank borrowings (including from RBI/FIs)', level: 1, value: null },
{ label: 'iv) Others', level: 1, value: null },
{ label: '11. Operating Expenses', level: 0, value: 3352, total: true },
{ label: 'i) Staff expenses', level: 1, value: 3244 },
{ label: 'ii) Directors fees', level: 1, value: 45 },
{ label: "iii) Auditors' fees", level: 1, value: 63 },
{ label: '12. Other Operating expenses', level: 0, value: 1038, total: true },
{ label: 'i) Rent, taxes and lighting', level: 1, value: null },
{ label: 'ii) Insurance', level: 1, value: 4 },
{ label: 'iii) Law charges', level: 1, value: 0 },
{ label: 'iv) Postage, telegrams and stamps', level: 1, value: 34 },
{ label: 'v) Stationery and printing', level: 1, value: 23 },
{ label: 'vi) Advertisement and publicity', level: 1, value: null },
{ label: "vii) Depreciation on bank's property", level: 1, value: null },
{ label: 'viii) Repairs and maintenance', level: 1, value: null },
{ label: 'ix) Others', level: 1, value: 977, total: true },
{ label: '13. Total operating expenses [10 + 11+ 12]', level: 0, value: 10049, total: true },
{ label: '14. Operating Profit/Loss [7 - 13]', level: 0, value: 5305, total: true },
{
label:
'15. Value adjustment of Securities (marking to market of securities in AFS & HFT) [Only loss is to be reported]',
level: 0,
value: null
},
{ label: '16. Write off', level: 0, value: 0, total: true },
{ label: 'i) Bad Debts written off', level: 1, value: null },
{ label: 'ii) Other assets written off', level: 1, value: null },
{ label: 'iii) Capitalised expenditure written off', level: 1, value: null },
{ label: '17. Provision against risks/ contingencies', level: 0, value: 0, total: true },
{ label: 'i) Provision for loan losses (BDDR, Spl. BDDR, etc.) *', level: 1, value: null },
{ label: 'ii) Provision for depreciation in investments including AFS #', level: 1, value: null },
{
label:
'iii) Other risk provisions towards losses on account of frauds, misappropriations, robberies, claims against the bank, etc.',
level: 1,
value: null
},
{ label: 'iv) Other provisions', level: 1, value: null },
{ label: '18. Net Operating Profit (+)/Loss (-) [14 - 15 - 16 - 17]', level: 0, value: 5305, total: true },
{ label: '19. Non Operating Income [Capital gains]', level: 0, value: 0, total: true },
{ label: '20. Non Operating Income [Capital losses]', level: 0, value: 0, total: true },
{ label: '21. Net Profit (+)/ Loss (-) before taxes - (EBPT) [18 + 19 - 20]', level: 0, value: 5305, total: true },
{ label: '22. Provisions for taxes (if any)', level: 0, value: 500 },
{ label: '23. Net Profit (+)/ Loss (-) after taxes [21 - 22]', level: 0, value: 4805, total: true },
{ label: '24. Balance of previous year’s profit (+) / Accumulated losses (-)', level: 0, value: 5592 },
{ label: '25. Net distributable profit (+) / Accumulated losses (-) [23 + 24]', level: 0, value: 10397, total: true },
{ label: '26. Appropriation of profits', level: 0, value: 10397, total: true },
{ label: 'i) Appropriation towards Statutory Reserves', level: 1, value: 3638 },
{ label: 'ii) Appropriation towards Building Fund', level: 1, value: 519 },
{ label: 'iii) Appropriation towards Dividend Equalisation Fund', level: 1, value: null },
{ label: 'iv) Appropriation towards Other Reserves/ Provisions', level: 1, value: 4058 },
{ label: 'v) Dividend', level: 1, value: 2182 },
{ label: '27. Net interest income', level: 0, value: 9410, total: true },
{ label: '28. Non-interest income', level: 0, value: 285, total: true },
{ label: '29. Non-interest expenditure', level: 0, value: 4390, total: true },
{ label: '30. Average Total Assets', level: 0, value: 796295 },
{ label: '31. Average Earning Assets', level: 0, value: 716407 },
{ label: '32. Working Fund', level: 0, value: 778829 },
{ label: '33. Number of Employee', level: 0, value: 11, count: true }
];
/**
* The two cross-return tie-ups printed at the foot of the Earnings sheet. Both
* reference line items on OSS 1, which is why the page checks them live.
*/
export const ROE_EARNINGS_NOTES: string[] = [
'Note: * The value of {17 (i) Provision for loan losses (BDDR, Spl. BDDR, etc.)} should tally with {12(ii) {BDDR, spl. BDDR} of liabilities in OSS-1.',
'Note: # The value of {17 (ii) Provision for Depreciation in investments including AFS} should tally with {i. For investments (Investment Depreciation Reserve)} of liabilities in OSS-1 .'
];
/** Sheet 3 — Interest Receivable Not Recognized */
export const ROE_INTEREST_NOT_RECOGNIZED: RoeInterestNotRecognizedRow[] = [
{ label: 'i. Interest receivable not recognized during the quarter', value: 2614 },
{ label: 'ii. Balance at the beginning of the quarter', value: 35101 },
{
label:
'iii. Interest received and reversed to income from (i) in the quarter on NPAs upgraded to Performing / accrual basis',
value: 5620
},
{ label: 'Balance at the end of the quarter (i)+(ii)-(iii)', value: 32095, total: true }
];
/** Sheet 4 — Details of Other Operating Expenses (feeds Earnings line 12 ix) */
export const ROE_OTHER_OPERATING_EXPENSES: RoeOtherExpense[] = [
{ srNo: 1, particulars: 'Miscelaneous expenses', amount: 55 },
{ srNo: 2, particulars: 'Entertainment Exps', amount: 24 },
{ srNo: 3, particulars: 'Commission On Mini Deposits', amount: 317 },
{ srNo: 4, particulars: 'AGM expenses', amount: 19 },
{ srNo: 5, particulars: 'Computer Exps', amount: 441 },
{ srNo: 6, particulars: 'Govt sec premium', amount: 0 },
{ srNo: 7, particulars: 'CIC Member Ship', amount: 0 },
{ srNo: 8, particulars: 'transaction expenses', amount: 7 },
{ srNo: 9, particulars: 'Digital Banking', amount: 11 },
{ srNo: 10, particulars: 'GST', amount: 26 },
{ srNo: 11, particulars: 'inventory write off', amount: 0 },
{ srNo: 12, particulars: 'Electricity Expenses', amount: 77 },
{ srNo: 13, particulars: 'Donation to Jila Sahakari', amount: 0 }
];
/** Sheet 5 — Signatory Information. Shared so OSS 1 and OSS 2 always agree. */
export const ROE_SIGNATORIES: RoeSignatory[] = OSS_SIGNATORIES;
@@ -1,278 +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="oss-root">
<!-- HEADER -->
<div class="oss-head">
<div class="oss-head-left">
<fa-icon icon="university" class="oss-head-ic"></fa-icon>
<div>
<h2 class="oss-head-t">OSS 2 — {{ returnName }}</h2>
<div class="oss-head-s">
{{ bankName }} &middot; Bank Code <strong>{{ bankCode }}</strong> &middot; Return Code
<strong>{{ returnCode }}</strong> &middot; Quarter ending <strong>{{ quarterEndDate }}</strong>
</div>
</div>
</div>
<div class="oss-head-right">
<span class="oss-audit-pill">{{ auditFlag }}</span>
<button class="oss-btn oss-btn-xls" [disabled]="generating" (click)="generateExcel()">
<fa-icon icon="file-csv"></fa-icon>
{{ generating ? 'Generating…' : 'Generate Excel' }}
</button>
</div>
</div>
<!-- HEADLINE RESULT -->
<div class="oss-balance">
<fa-icon icon="check-circle"></fa-icon>
<span>
Operating Profit <strong>&#8377;{{ operatingProfit | number: '1.2-2' }}</strong> &middot; Net Profit after tax
<strong>&#8377;{{ netProfitAfterTax | number: '1.2-2' }}</strong> (Rs in Thousands)
</span>
</div>
<!-- TABS -->
<div class="oss-tabs">
<button class="oss-tab" [class.active]="activeTab === 'general'" (click)="showTab('general')">
<fa-icon icon="info-circle"></fa-icon> General Information
</button>
<button class="oss-tab" [class.active]="activeTab === 'earnings'" (click)="showTab('earnings')">
<fa-icon icon="university"></fa-icon> Earnings
</button>
<button class="oss-tab" [class.active]="activeTab === 'interest'" (click)="showTab('interest')">
<fa-icon icon="exclamation-triangle"></fa-icon> Interest Not Recognized
</button>
<button class="oss-tab" [class.active]="activeTab === 'other-expenses'" (click)="showTab('other-expenses')">
<fa-icon icon="file-alt"></fa-icon> Other Operating Expenses
</button>
<button class="oss-tab" [class.active]="activeTab === 'signatory'" (click)="showTab('signatory')">
<fa-icon icon="check-circle"></fa-icon> Signatory
</button>
</div>
<!-- GENERAL INFORMATION -->
@if (activeTab === 'general') {
<div class="oss-section">
<div class="oss-sheet-banner">General Information</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl oss-tbl--kv">
<tbody>
@for (item of generalInfo; track item.label) {
<tr>
<th>{{ item.label }}</th>
<td [class.filled]="item.value" [class.audit]="item.label === 'Audit Flag'">{{ item.value }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- EARNINGS -->
@if (activeTab === 'earnings') {
<div class="oss-section">
<div class="oss-sheet-banner">Earnings</div>
<!-- Cross-return tie-ups the footnotes demand -->
<div class="oss-balance" [class.oss-balance--bad]="!allTieUpsMatch">
<fa-icon [icon]="allTieUpsMatch ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
{{
allTieUpsMatch
? 'Item 17 tallies with OSS-1 liabilities as the footnotes require.'
: 'Item 17 does not tally with OSS-1 liabilities — see the tie-up check below.'
}}
</span>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">OSS-1 tie-up check</div>
<table class="oss-tbl">
<thead>
<tr>
<th>OSS-2 item</th>
<th class="ta-r">OSS-2 value</th>
<th>Must tally with</th>
<th class="ta-r">OSS-1 value</th>
<th class="ta-c">Status</th>
</tr>
</thead>
<tbody>
@for (t of oss1TieUps; track t.label) {
<tr>
<td>{{ t.label }}</td>
<td class="ta-r mono">{{ t.oss2Value === null ? '—' : (t.oss2Value | number: '1.2-2') }}</td>
<td>{{ t.oss1Label }}</td>
<td class="ta-r mono">{{ t.oss1Value === null ? '—' : (t.oss1Value | number: '1.2-2') }}</td>
<td class="ta-c">
<span [class]="t.tallies ? 'report-pill report-ok' : 'report-pill report-bad'">
{{ t.tallies ? 'Tallies' : 'Mismatch' }}
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl">
<thead>
<tr>
<th>Particulars</th>
<th class="ta-r">Rs in Thousand</th>
</tr>
</thead>
<tbody>
@for (line of earnings; track $index) {
<tr [class.total-row]="line.total">
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
<td class="ta-r mono" [class.derived]="line.total">
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-notes">
@for (note of earningsNotes; track note) {
<div class="oss-note-line">{{ note }}</div>
}
</div>
</div>
}
<!-- INTEREST RECEIVABLE NOT RECOGNIZED -->
@if (activeTab === 'interest') {
<div class="oss-section">
<div class="oss-sheet-banner">Interest Receivable Not Recognized</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl">
<thead>
<tr>
<th>Interest receivable on NPAs not recognized as income</th>
<th class="ta-r">{{ quarterEndDate }} (Rs in Thousand)</th>
</tr>
</thead>
<tbody>
@for (r of interestNotRecognized; track r.label) {
<tr [class.total-row]="r.total">
<td>{{ r.label }}</td>
<td class="ta-r mono" [class.derived]="r.total">
{{ r.value === null ? '' : (r.value | number: '1.2-2') }}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- OTHER OPERATING EXPENSES -->
@if (activeTab === 'other-expenses') {
<div class="oss-section">
<div class="oss-sheet-banner">Other Operating Expenses</div>
<div class="oss-balance" [class.oss-balance--bad]="!otherExpensesTally">
<fa-icon [icon]="otherExpensesTally ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
Detail total <strong>&#8377;{{ otherExpensesTotal | number: '1.2-2' }}</strong> vs Earnings line 12 ix)
Others
<strong>&#8377;{{ otherExpensesLineValue === null ? '—' : (otherExpensesLineValue | number: '1.2-2') }}</strong>
— {{ otherExpensesTally ? 'tallies' : 'MISMATCH — resolve before filing' }}
</span>
</div>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">Details of Other Operating Expenses &mdash; Rs in Thousand</div>
<table class="oss-tbl">
<thead>
<tr>
<th class="ta-c">Sr No</th>
<th>Particulars</th>
<th class="ta-r">Amount</th>
</tr>
</thead>
<tbody>
@for (e of otherExpenses; track e.srNo) {
<tr>
<td class="ta-c">{{ e.srNo }}</td>
<td>{{ e.particulars }}</td>
<td class="ta-r mono">{{ e.amount === null ? '' : (e.amount | number: '1.2-2') }}</td>
</tr>
}
<tr class="total-row">
<td class="ta-c"></td>
<td>Total</td>
<td class="ta-r mono derived">{{ otherExpensesTotal | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
<p class="oss-foot-note">Feeds Earnings sheet line 12 ix) Others.</p>
</div>
}
<!-- SIGNATORY -->
@if (activeTab === 'signatory') {
<div class="oss-section">
<div class="oss-sheet-banner">Signatory Information</div>
<div class="oss-sign-grid">
@for (p of signatories; track p.heading) {
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">{{ p.heading }}</div>
<table class="oss-tbl oss-tbl--kv">
<tbody>
<tr>
<td>{{ p.signature }}</td>
<th>Signature</th>
</tr>
<tr>
<td>{{ p.name }}</td>
<th>Name</th>
</tr>
<tr>
<td>{{ p.designation }}</td>
<th>Designation</th>
</tr>
<tr>
<td>{{ p.email }}</td>
<th>e-mail ID</th>
</tr>
<tr>
<td class="mono">{{ p.telephoneOffice }}</td>
<th>Tel. No. (O)</th>
</tr>
<tr>
<td class="mono">{{ p.telephoneResidence }}</td>
<th>Tel. No. (R)</th>
</tr>
<tr>
<td>{{ p.place }}</td>
<th>Place</th>
</tr>
<tr>
<td>{{ p.date }}</td>
<th>Date</th>
</tr>
</tbody>
</table>
</div>
}
</div>
</div>
}
<!-- TOAST -->
<div class="oss-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
@@ -1,283 +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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import * as ExcelJS from 'exceljs';
import {
addBanner,
addLineRows,
addSectionHeader,
boxAllCells,
buildGeneralInfoSheet,
buildSignatorySheet,
downloadWorkbook,
newWorkbook,
solidFill,
XL
} from '../oss-workbook';
import { RAL_LIABILITIES } from '../oss1-ral/oss1-ral-data';
import {
ROE_EARNINGS,
ROE_EARNINGS_NOTES,
ROE_GENERAL_INFO,
ROE_INTEREST_NOT_RECOGNIZED,
ROE_OTHER_OPERATING_EXPENSES,
ROE_SIGNATORIES
} from './oss2-roe-data';
type Oss2Tab = 'general' | 'earnings' | 'interest' | 'other-expenses' | 'signatory';
/** One of the two OSS-1 tie-ups printed at the foot of the Earnings sheet. */
interface TieUp {
label: string;
oss2Value: number | null;
oss1Label: string;
oss1Value: number | null;
tallies: boolean;
}
@Component({
selector: 'mifosx-oss2-roe',
templateUrl: './oss2-roe.component.html',
styleUrls: ['../oss-return.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class Oss2RoeComponent {
activeTab: Oss2Tab = 'general';
generalInfo = ROE_GENERAL_INFO;
earnings = ROE_EARNINGS;
earningsNotes = ROE_EARNINGS_NOTES;
interestNotRecognized = ROE_INTEREST_NOT_RECOGNIZED;
otherExpenses = ROE_OTHER_OPERATING_EXPENSES;
signatories = ROE_SIGNATORIES;
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: any;
showTab(tab: Oss2Tab): void {
this.activeTab = tab;
}
private info(label: string): string {
return this.generalInfo.find((g) => g.label === label)?.value ?? '';
}
get returnName(): string {
return this.info('Return Name');
}
get returnCode(): string {
return this.info('Return Code');
}
get bankName(): string {
return this.info('Name Of Bank');
}
get bankCode(): string {
return this.info('Bank Code');
}
get quarterEndDate(): string {
return this.info('Quarter End Date');
}
get auditFlag(): string {
return this.info('Audit Flag');
}
private earningsValue(prefix: string): number | null {
return this.earnings.find((l) => l.label.startsWith(prefix))?.value ?? null;
}
private liabilityValue(prefix: string): number | null {
return RAL_LIABILITIES.find((l) => l.label.startsWith(prefix))?.value ?? null;
}
get netProfitAfterTax(): number {
return this.earningsValue('23.') ?? 0;
}
get operatingProfit(): number {
return this.earningsValue('14.') ?? 0;
}
/** Sum of the detail sheet — must equal Earnings line 12 ix) Others. */
get otherExpensesTotal(): number {
return this.otherExpenses.reduce((sum, e) => sum + (e.amount ?? 0), 0);
}
get otherExpensesLineValue(): number | null {
return this.earningsValue('ix) Others');
}
get otherExpensesTally(): boolean {
return this.otherExpensesTotal === (this.otherExpensesLineValue ?? 0);
}
/**
* The Earnings sheet carries two footnotes requiring 17(i) and 17(ii) to tally
* with OSS-1 liabilities 12(ii) and 12(i). Checked live rather than left to a
* reader spotting it in the fine print.
*/
get oss1TieUps(): TieUp[] {
const rows: TieUp[] = [
{
label: '17 (i) Provision for loan losses (BDDR, Spl. BDDR, etc.)',
oss2Value: this.earningsValue('i) Provision for loan losses'),
oss1Label: 'OSS-1 liabilities 12 (ii) BDDR, Spl BDDR',
oss1Value: this.liabilityValue('ii. BDDR, Spl BDDR'),
tallies: false
},
{
label: '17 (ii) Provision for depreciation in investments including AFS',
oss2Value: this.earningsValue('ii) Provision for depreciation in investments'),
oss1Label: 'OSS-1 liabilities 12 (i) Investment Depreciation Reserve',
oss1Value: this.liabilityValue('i. For investments (Investment Depreciation Reserve)'),
tallies: false
}
];
return rows.map((r) => ({ ...r, tallies: (r.oss2Value ?? 0) === (r.oss1Value ?? 0) }));
}
get allTieUpsMatch(): boolean {
return this.oss1TieUps.every((t) => t.tallies);
}
// ══════════════ Excel generation ══════════════
/** Rebuilds the RBI workbook: one sheet per section, original layout and styling. */
async generateExcel(): Promise<void> {
this.generating = true;
try {
const wb = newWorkbook();
buildGeneralInfoSheet(wb, this.generalInfo);
this.buildEarningsSheet(wb);
this.buildInterestSheet(wb);
this.buildOtherExpensesSheet(wb);
buildSignatorySheet(wb, this.signatories, 'Signatory Information');
await downloadWorkbook(wb, `Form OSS-2_CIMS ${this.returnCode} - ${this.quarterEndDate}.xlsx`);
this.toast(`${this.returnCode} workbook generated for quarter ending ${this.quarterEndDate}`);
} catch {
this.toast('Could not generate the workbook — please retry');
} finally {
this.generating = false;
}
}
private buildEarningsSheet(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('Earnings');
sheet.columns = [
{ width: 90 },
{ width: 20 }
];
addBanner(sheet, 'Earnings', 'B');
const first = sheet.rowCount + 1;
addSectionHeader(sheet, '', 'Rs in Thousand');
addLineRows(sheet, this.earnings);
boxAllCells(sheet, first, sheet.rowCount, 2);
sheet.addRow([]);
const notesStart = sheet.rowCount + 1;
for (const note of this.earningsNotes) {
const row = sheet.addRow([note]);
sheet.mergeCells(`A${row.number}:B${row.number}`);
row.font = { bold: true, italic: true, size: 10 };
row.alignment = { wrapText: true };
row.getCell(1).fill = solidFill(XL.headerGrey);
}
boxAllCells(sheet, notesStart, sheet.rowCount, 2);
sheet.views = [{ state: 'frozen', ySplit: 3 }];
}
private buildInterestSheet(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('Interest Receivable Not Recog');
sheet.columns = [
{ width: 72 },
{ width: 22 }
];
addBanner(sheet, 'Interest Receivable Not Recognized', 'B');
const start = sheet.rowCount + 1;
const unit = sheet.addRow(['', 'Rs in Thousand']);
unit.font = { bold: true };
unit.alignment = { horizontal: 'center' };
unit.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
const head = sheet.addRow(['Interest receivable on NPAs not recognized as income', this.quarterEndDate]);
head.font = { bold: true };
head.getCell(2).alignment = { horizontal: 'center' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const r of this.interestNotRecognized) {
const row = sheet.addRow([r.label, r.value]);
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
row.getCell(1).fill = solidFill(XL.headerGrey);
row.getCell(2).numFmt = '#,##0.00';
if (r.total) {
row.getCell(2).fill = solidFill(XL.derivedBlue);
row.getCell(2).font = { bold: true };
}
}
boxAllCells(sheet, start, sheet.rowCount, 2);
}
private buildOtherExpensesSheet(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('Other Operating Expenses');
sheet.columns = [
{ width: 10 },
{ width: 56 },
{ width: 20 }
];
addBanner(sheet, 'Other Operating Expenses', 'C');
const start = sheet.rowCount + 1;
const title = sheet.addRow(['Details of Other Operating Expenses', '', 'Rs in Thousand']);
sheet.mergeCells(`A${title.number}:B${title.number}`);
title.font = { bold: true };
title.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
title.getCell(3).alignment = { horizontal: 'center' };
const head = sheet.addRow(['Sr No', 'Particulars', 'Amount']);
head.font = { bold: true };
head.alignment = { horizontal: 'center' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const e of this.otherExpenses) {
const row = sheet.addRow([e.srNo, e.particulars, e.amount]);
row.getCell(3).numFmt = '#,##0.00';
}
const total = sheet.addRow(['Total', '', this.otherExpensesTotal]);
sheet.mergeCells(`A${total.number}:B${total.number}`);
total.font = { bold: true };
total.getCell(1).fill = solidFill(XL.headerGrey);
total.getCell(3).numFmt = '#,##0.00';
total.getCell(3).fill = solidFill(XL.derivedBlue);
boxAllCells(sheet, start, sheet.rowCount, 3);
const ref = sheet.addRow(['', '', 'Refer Earnings Sheet — line 12 ix) Others']);
ref.getCell(3).font = { italic: true, size: 10 };
}
private toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,372 +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="oss-root">
<!-- HEADER -->
<div class="oss-head">
<div class="oss-head-left">
<fa-icon icon="university" class="oss-head-ic"></fa-icon>
<div>
<h2 class="oss-head-t">OSS 3 — {{ returnName }}</h2>
<div class="oss-head-s">
{{ bankName }} &middot; Bank Code <strong>{{ bankCode }}</strong> &middot; Return Code
<strong>{{ returnCode }}</strong> &middot; Quarter ending <strong>{{ reportingEndDate }}</strong>
</div>
</div>
</div>
<div class="oss-head-right">
<span class="oss-audit-pill">{{ reportStatus }}</span>
<button class="oss-btn oss-btn-xls" [disabled]="generating" (click)="generateExcel()">
<fa-icon icon="file-csv"></fa-icon>
{{ generating ? 'Generating…' : 'Generate Excel' }}
</button>
</div>
</div>
<!-- HEADLINE ASSET QUALITY -->
<div class="oss-balance" [class.oss-balance--bad]="!totalsAgree">
<fa-icon [icon]="totalsAgree ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
Gross NPA <strong>&#8377;{{ grossNpa | number: '1.2-2' }}</strong> ({{ grossNpaRatio | number: '1.2-2' }}%)
&middot; Net NPA <strong>&#8377;{{ netNpa | number: '1.2-2' }}</strong> ({{ netNpaRatio | number: '1.2-2' }}%)
&middot;
{{
totalsAgree
? 'Portfolio Analysis and Classification totals agree'
: 'MISMATCH between Portfolio Analysis and Classification totals — resolve before filing'
}}
</span>
</div>
<!-- TABS -->
<div class="oss-tabs">
@for (t of tabs; track t.id) {
<button class="oss-tab" [class.active]="activeTab === t.id" (click)="showTab(t.id)">
<fa-icon [icon]="t.icon"></fa-icon> {{ t.label }}
</button>
}
</div>
<!-- FILING INFORMATION -->
@if (activeTab === 'filing') {
<div class="oss-section">
<div class="oss-sheet-banner">Filing Information</div>
<div class="oss-tbl-wrap">
<table class="oss-tbl oss-tbl--kv">
<tbody>
@for (item of filingInfo; track item.label) {
<tr>
<th>{{ item.label }}</th>
<td [class.filled]="item.value" [class.audit]="item.label === 'Report status'">{{ item.value }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- PORTFOLIO ANALYSIS -->
@if (activeTab === 'portfolio') {
<div class="oss-section">
<div class="oss-sheet-banner">PortfolioAnalysis(loans_advncs) — Portfolio Analysis (By delinquency in Interest Payment)</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: portfolioAnalysis }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: portfolioMemo }"></ng-container>
</div>
}
<!-- CLASSIFICATION OF RISK ASSETS -->
@if (activeTab === 'classification') {
<div class="oss-section">
<div class="oss-sheet-banner">Classifcation_Loans_Adv — Classification of Risk Assets</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: classificationPartA }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: nettingItems1 }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: nettingItems2 }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: classificationMemo }"></ng-container>
</div>
}
<!-- MOVEMENT OF LOANS -->
@if (activeTab === 'movement') {
<div class="oss-section">
<div class="oss-sheet-banner">Movement_of_loans — Change in Asset Quality Profile and Recoveries of NPAs</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: movementPart1 }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: movementPart2 }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: movementPart3 }"></ng-container>
</div>
}
<!-- OTHER RISK ASSETS -->
@if (activeTab === 'other-risk') {
<div class="oss-section">
<div class="oss-sheet-banner">StatementOtherRiskAssets — Other Risk Assets and Exposures</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: otherRiskOnBs }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: otherRiskOffBs }"></ng-container>
</div>
}
<!-- TOP IMPAIRED CREDITS -->
@if (activeTab === 'impaired') {
<div class="oss-section">
<div class="oss-sheet-banner">TopImpairedCredits — Top Impaired Credits</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: capitalMemo }"></ng-container>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs &mdash; Rs in Thousands</div>
<table class="oss-tbl">
<thead>
<tr>
<th>PAN of Borrower</th>
<th>Name of Borrower</th>
<th class="ta-r">Loans &amp; Advances</th>
<th class="ta-r">Funded Exposure</th>
<th class="ta-r">Interest in arrear</th>
<th class="ta-r">% to Total Advances</th>
<th class="ta-r">% to Capital Funds</th>
<th>Asset Classification</th>
<th class="ta-r">Loss Provision held</th>
</tr>
</thead>
<tbody>
@for (r of impairedCredits; track $index) {
<tr>
<td class="mono">{{ r.pan }}</td>
<td>{{ r.name }}</td>
<td class="ta-r mono">{{ r.loansAdvances | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ r.funded | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ r.interestInArrear | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ r.pctTotalAdvances | number: '1.2-2' }}%</td>
<td class="ta-r mono">{{ r.pctCapitalFunds | number: '1.2-2' }}%</td>
<td>
<span class="report-pill" [class.report-bad]="r.assetClassification !== 'Standard'">
{{ r.assetClassification }}
</span>
</td>
<td class="ta-r mono">{{ r.lossProvisionHeld | number: '1.2-2' }}</td>
</tr>
}
<tr class="total-row">
<td colspan="3">Total ({{ impairedCredits.length }} borrowers)</td>
<td class="ta-r mono derived">{{ impairedFundedTotal | number: '1.2-2' }}</td>
<td class="ta-r mono derived">{{ impairedInterestTotal | number: '1.2-2' }}</td>
<td colspan="3"></td>
<td class="ta-r mono derived">{{ impairedProvisionTotal | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
@for (t of recoveryTables; track t.title) {
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: t }"></ng-container>
}
</div>
}
<!-- EXPOSURE LIMITS -->
@if (activeTab === 'exposure') {
<div class="oss-section">
<div class="oss-sheet-banner">ExposureLimits — Single and Group Borrower Exposure Limit</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: capitalPreviousMarch }"></ng-container>
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">
Table 2: Statement on Large Exposure &mdash; Single Borrower (in excess of 15% of Tier 1 capital as on
previous March, or a minimum of 20 large credits)
</div>
<table class="oss-tbl">
<thead>
<tr>
<th>PAN of Borrower</th>
<th>Name of Borrower</th>
<th class="ta-r">Limit sanctioned</th>
<th class="ta-r">% to Total Advances</th>
<th class="ta-r">Outstanding Balance</th>
<th class="ta-r">% to Tier 1 capital</th>
<th>Asset Classification</th>
</tr>
</thead>
<tbody>
@for (r of largeExposures; track $index) {
<tr>
<td class="mono">{{ r.pan }}</td>
<td>{{ r.name }}</td>
<td class="ta-r mono">{{ r.limitSanctioned | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ r.pctTotalAdvances | number: '1.2-2' }}%</td>
<td class="ta-r mono">{{ r.outstandingBalance | number: '1.2-2' }}</td>
<td class="ta-r mono">{{ r.pctTier1Capital | number: '1.2-2' }}%</td>
<td>
<span class="report-pill report-ok">{{ r.assetClassification }}</span>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="oss-notes">
@for (note of exposureNotes; track note) {
<div class="oss-note-line">{{ note }}</div>
}
</div>
</div>
}
<!-- SECURITY PORTFOLIO -->
@if (activeTab === 'securities') {
<div class="oss-section">
<div class="oss-sheet-banner">QualitySecurityPortoflio — Quality of Security Portfolio</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: investmentQuality }"></ng-container>
<div class="oss-sheet-banner">OtherNonSLRSecurity — Other Details of Non-SLR Securities</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: nonSlrSecurities }"></ng-container>
</div>
}
<!-- SECTORAL CREDIT -->
@if (activeTab === 'sectoral') {
<div class="oss-section">
<div class="oss-sheet-banner">SectoralCredit — Sectoral Credit</div>
<div class="oss-balance" [class.oss-balance--bad]="!sectoralAgreesWithClosing">
<fa-icon [icon]="sectoralAgreesWithClosing ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
<span>
@if (sectoralAgreesWithClosing) {
Sectoral totals agree with the Classification statement.
} @else {
Sectoral gross advances <strong>&#8377;{{ sectoralGross | number: '1.2-2' }}</strong> and NPAs
<strong>&#8377;{{ sectoralNpa | number: '1.2-2' }}</strong> do not match the closing position reported by
Classification (<strong>&#8377;{{ classificationTotal | number: '1.2-2' }}</strong> /
<strong>&#8377;{{ grossNpa | number: '1.2-2' }}</strong>).
@if (sectoralMatchesOpening) {
They match the <em>opening</em> balances in Movement Parts 1 and 2 instead — this statement appears to
have been prepared on the previous quarter's book.
}
}
</span>
</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: sectoralCredit }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: sectoralMemo }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: segmentAnalysis }"></ng-container>
</div>
}
<!-- INDUSTRY BREAKUP -->
@if (activeTab === 'industry') {
<div class="oss-section">
<div class="oss-sheet-banner">IndustryBreakup — Industry Breakup</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: industryBreakup }"></ng-container>
</div>
}
<!-- SENSITIVE SECTOR -->
@if (activeTab === 'sensitive') {
<div class="oss-section">
<div class="oss-sheet-banner">SensitiveSector — Sensitive Sector</div>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: capitalMarket }"></ng-container>
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: realEstate }"></ng-container>
</div>
}
<!-- SIGNATORY -->
@if (activeTab === 'signatory') {
<div class="oss-section">
<div class="oss-sheet-banner">Signatory — Authorised Signatory</div>
<div class="oss-sign-grid">
@for (p of signatories; track p.heading) {
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">{{ p.heading }}</div>
<table class="oss-tbl oss-tbl--kv">
<tbody>
<tr>
<td>{{ p.signature }}</td>
<th>Signature</th>
</tr>
<tr>
<td>{{ p.name }}</td>
<th>Name</th>
</tr>
<tr>
<td>{{ p.designation }}</td>
<th>Designation</th>
</tr>
<tr>
<td>{{ p.email }}</td>
<th>e-mail ID</th>
</tr>
<tr>
<td class="mono">{{ p.telephoneOffice }}</td>
<th>Tele. No. (O)</th>
</tr>
<tr>
<td class="mono">{{ p.telephoneResidence }}</td>
<th>Tele. No. (R)</th>
</tr>
<tr>
<td>{{ p.place }}</td>
<th>Place</th>
</tr>
<tr>
<td>{{ p.date }}</td>
<th>Date</th>
</tr>
</tbody>
</table>
</div>
}
</div>
</div>
}
<!-- Shared renderer for every multi-column statement in this return. -->
<ng-template #matrix let-t="t">
<div class="oss-tbl-wrap">
<div class="oss-tbl-h">
{{ t.title }}@if (t.unit) {<span class="oss-tbl-unit">{{ t.unit }}</span>}
</div>
@if (t.subtitle) {
<div class="oss-tbl-sub">{{ t.subtitle }}</div>
}
<table class="oss-tbl">
<thead>
<tr>
<th class="oss-col-label">Particulars</th>
@for (c of t.columns; track $index) {
<th class="ta-r">{{ c }}</th>
}
</tr>
</thead>
<tbody>
@for (row of t.rows; track $index) {
<tr [class.total-row]="row.total" [class.section-row]="row.header">
<td [class]="'lvl-' + row.level">{{ row.label }}</td>
@if (row.header) {
<td [attr.colspan]="t.columns.length"></td>
} @else {
@for (v of row.values; track $index) {
<td class="ta-r mono" [class.derived]="row.total">
{{ v === null ? '' : (v | number: '1.2-2') }}{{ v !== null && row.percent ? '%' : '' }}
</td>
}
}
</tr>
}
</tbody>
</table>
</div>
@if (t.notes?.length) {
<div class="oss-notes">
@for (note of t.notes; track note) {
<div class="oss-note-line">{{ note }}</div>
}
</div>
}
</ng-template>
<!-- TOAST -->
<div class="oss-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
@@ -1,470 +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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import * as ExcelJS from 'exceljs';
import { MatrixTable } from '../oss-return.model';
import {
addBanner,
addMatrixTable,
boxAllCells,
buildGeneralInfoSheet,
buildSignatorySheet,
downloadWorkbook,
newWorkbook,
solidFill,
XL
} from '../oss-workbook';
import {
RAQ_CAPITAL_MARKET,
RAQ_CAPITAL_MEMO,
RAQ_CAPITAL_PREVIOUS_MARCH,
RAQ_CLASSIFICATION_MEMO,
RAQ_CLASSIFICATION_PART_A,
RAQ_EXPOSURE_NOTES,
RAQ_FILING_INFO,
RAQ_IMPAIRED_CREDITS,
RAQ_INDUSTRY_BREAKUP,
RAQ_INVESTMENT_QUALITY,
RAQ_LARGE_EXPOSURES,
RAQ_MOVEMENT_PART_1,
RAQ_MOVEMENT_PART_2,
RAQ_MOVEMENT_PART_3,
RAQ_NETTING_ITEMS_1,
RAQ_NETTING_ITEMS_2,
RAQ_NON_SLR_SECURITIES,
RAQ_OTHER_RISK_OFF_BS,
RAQ_OTHER_RISK_ON_BS,
RAQ_PORTFOLIO_ANALYSIS,
RAQ_PORTFOLIO_MEMO,
RAQ_REAL_ESTATE,
RAQ_RECOVERY_TABLES,
RAQ_SECTORAL_CREDIT,
RAQ_SECTORAL_MEMO,
RAQ_SEGMENT_ANALYSIS,
RAQ_SIGNATORIES
} from './oss3-raq-data';
type Oss3Tab =
| 'filing'
| 'portfolio'
| 'classification'
| 'movement'
| 'other-risk'
| 'impaired'
| 'exposure'
| 'securities'
| 'sectoral'
| 'industry'
| 'sensitive'
| 'signatory';
@Component({
selector: 'mifosx-oss3-raq',
templateUrl: './oss3-raq.component.html',
styleUrls: ['../oss-return.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class Oss3RaqComponent {
activeTab: Oss3Tab = 'filing';
/** Tab strip definition — keeps the template free of a dozen near-identical buttons. */
readonly tabs: { id: Oss3Tab; label: string; icon: string }[] = [
{ id: 'filing', label: 'Filing Information', icon: 'info-circle' },
{ id: 'portfolio', label: 'Portfolio Analysis', icon: 'chart-pie' },
{ id: 'classification', label: 'Classification of Risk Assets', icon: 'university' },
{ id: 'movement', label: 'Movement of Loans', icon: 'exchange-alt' },
{ id: 'other-risk', label: 'Other Risk Assets', icon: 'exclamation-triangle' },
{ id: 'impaired', label: 'Top Impaired Credits', icon: 'file-alt' },
{ id: 'exposure', label: 'Exposure Limits', icon: 'users' },
{ id: 'securities', label: 'Security Portfolio', icon: 'coins' },
{ id: 'sectoral', label: 'Sectoral Credit', icon: 'chart-pie' },
{ id: 'industry', label: 'Industry Breakup', icon: 'file-alt' },
{ id: 'sensitive', label: 'Sensitive Sector', icon: 'shield-alt' },
{ id: 'signatory', label: 'Signatory', icon: 'check-circle' }
];
filingInfo = RAQ_FILING_INFO;
portfolioAnalysis = RAQ_PORTFOLIO_ANALYSIS;
portfolioMemo = RAQ_PORTFOLIO_MEMO;
classificationPartA = RAQ_CLASSIFICATION_PART_A;
nettingItems1 = RAQ_NETTING_ITEMS_1;
nettingItems2 = RAQ_NETTING_ITEMS_2;
classificationMemo = RAQ_CLASSIFICATION_MEMO;
movementPart1 = RAQ_MOVEMENT_PART_1;
movementPart2 = RAQ_MOVEMENT_PART_2;
movementPart3 = RAQ_MOVEMENT_PART_3;
otherRiskOnBs = RAQ_OTHER_RISK_ON_BS;
otherRiskOffBs = RAQ_OTHER_RISK_OFF_BS;
capitalMemo = RAQ_CAPITAL_MEMO;
recoveryTables = RAQ_RECOVERY_TABLES;
impairedCredits = RAQ_IMPAIRED_CREDITS;
capitalPreviousMarch = RAQ_CAPITAL_PREVIOUS_MARCH;
largeExposures = RAQ_LARGE_EXPOSURES;
exposureNotes = RAQ_EXPOSURE_NOTES;
investmentQuality = RAQ_INVESTMENT_QUALITY;
nonSlrSecurities = RAQ_NON_SLR_SECURITIES;
sectoralCredit = RAQ_SECTORAL_CREDIT;
sectoralMemo = RAQ_SECTORAL_MEMO;
segmentAnalysis = RAQ_SEGMENT_ANALYSIS;
industryBreakup = RAQ_INDUSTRY_BREAKUP;
capitalMarket = RAQ_CAPITAL_MARKET;
realEstate = RAQ_REAL_ESTATE;
signatories = RAQ_SIGNATORIES;
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: any;
showTab(tab: Oss3Tab): void {
this.activeTab = tab;
}
private info(label: string): string {
return this.filingInfo.find((g) => g.label === label)?.value ?? '';
}
get returnName(): string {
return this.info('Return Name');
}
get returnCode(): string {
return this.info('Return Code');
}
get bankName(): string {
return this.info('Name of reporting institution');
}
get bankCode(): string {
return this.info('Bank Code');
}
get reportingEndDate(): string {
return this.info('Reporting end date');
}
get reportStatus(): string {
return this.info('Report status');
}
private matrixValue(table: MatrixTable, prefix: string, col: number): number | null {
return table.rows.find((r) => r.label.startsWith(prefix))?.values[col] ?? null;
}
get grossNpa(): number {
return this.matrixValue(this.nettingItems2, 'Gross Non Performing Advances', 0) ?? 0;
}
get grossNpaRatio(): number {
return this.matrixValue(this.nettingItems2, 'Gross Non Performing Advances Ratio', 0) ?? 0;
}
get netNpa(): number {
return this.matrixValue(this.nettingItems2, 'Net Non Performing Advances', 0) ?? 0;
}
get netNpaRatio(): number {
return this.matrixValue(this.nettingItems2, 'Net Non Performing Advances Ratio', 0) ?? 0;
}
/**
* Portfolio Analysis TOTAL must equal Classification TOTAL — the two statements
* describe the same book from different angles and RBI validates them against
* each other, so a divergence is surfaced rather than left to be spotted.
*/
get portfolioTotal(): number {
return this.matrixValue(this.portfolioAnalysis, 'TOTAL (A + B)', 3) ?? 0;
}
get classificationTotal(): number {
return this.matrixValue(this.classificationPartA, 'TOTAL', 0) ?? 0;
}
get totalsAgree(): boolean {
return this.portfolioTotal === this.classificationTotal;
}
/**
* Sectoral Credit reports its own gross advances and NPA figures, which should
* describe the same closing book as the Classification statement. In this filing
* they instead match the *opening* balances from Movement Part 1/2, so the
* divergence is shown rather than buried.
*/
get sectoralGross(): number {
return this.matrixValue(this.sectoralCredit, 'I. Gross Advances', 0) ?? 0;
}
get sectoralNpa(): number {
return this.matrixValue(this.sectoralCredit, 'I. Gross Advances', 1) ?? 0;
}
get movementOpeningGross(): number {
return this.matrixValue(this.movementPart1, 'A. Loans and advances at the beginning', 4) ?? 0;
}
get movementOpeningNpa(): number {
return this.matrixValue(this.movementPart2, 'A. NPAs at the beginning', 3) ?? 0;
}
get sectoralAgreesWithClosing(): boolean {
return this.sectoralGross === this.classificationTotal && this.sectoralNpa === this.grossNpa;
}
get sectoralMatchesOpening(): boolean {
return this.sectoralGross === this.movementOpeningGross && this.sectoralNpa === this.movementOpeningNpa;
}
/** Table 5 footer totals, summed rather than hardcoded so they cannot drift. */
get impairedFundedTotal(): number {
return this.impairedCredits.reduce((s, r) => s + (r.funded ?? 0), 0);
}
get impairedInterestTotal(): number {
return this.impairedCredits.reduce((s, r) => s + (r.interestInArrear ?? 0), 0);
}
get impairedProvisionTotal(): number {
return this.impairedCredits.reduce((s, r) => s + (r.lossProvisionHeld ?? 0), 0);
}
// ══════════════ Excel generation ══════════════
/** Rebuilds the RBI iFILE workbook: one sheet per statement, original layout. */
async generateExcel(): Promise<void> {
this.generating = true;
try {
const wb = newWorkbook();
buildGeneralInfoSheet(wb, this.filingInfo);
this.sheetOf(wb, 'PortfolioAnalysis', [
this.portfolioAnalysis,
this.portfolioMemo
]);
this.sheetOf(wb, 'Classifcation_Loans_Adv', [
this.classificationPartA,
this.nettingItems1,
this.nettingItems2,
this.classificationMemo
]);
this.sheetOf(wb, 'Movement_of_loans', [
this.movementPart1,
this.movementPart2,
this.movementPart3
]);
this.sheetOf(wb, 'StatementOtherRiskAssets', [
this.otherRiskOnBs,
this.otherRiskOffBs
]);
this.buildImpairedCreditsSheet(wb);
this.buildExposureLimitsSheet(wb);
this.sheetOf(wb, 'QualitySecurityPortoflio', [this.investmentQuality]);
this.sheetOf(wb, 'OtherNonSLRSecurity', [this.nonSlrSecurities]);
this.sheetOf(wb, 'SectoralCredit', [
this.sectoralCredit,
this.sectoralMemo,
this.segmentAnalysis
]);
this.sheetOf(wb, 'IndustryBreakup', [this.industryBreakup]);
this.sheetOf(wb, 'SensitiveSector', [
this.capitalMarket,
this.realEstate
]);
buildSignatorySheet(wb, this.signatories, 'Signatory');
await downloadWorkbook(wb, `R32312 OSS 3 ${this.returnCode} - ${this.reportingEndDate}.xlsx`);
this.toast(`${this.returnCode} workbook generated for quarter ending ${this.reportingEndDate}`);
} catch {
this.toast('Could not generate the workbook — please retry');
} finally {
this.generating = false;
}
}
/** One worksheet holding a stack of matrix statements. */
private sheetOf(wb: ExcelJS.Workbook, name: string, tables: MatrixTable[]): void {
const sheet = wb.addWorksheet(name);
const widest = Math.max(...tables.map((t) => t.columns.length));
sheet.columns = [
{ width: 62 },
...Array.from({ length: widest }, () => ({ width: 18 }))
];
addBanner(sheet, name, this.colLetter(widest + 1));
for (const table of tables) {
addMatrixTable(sheet, table);
}
}
private colLetter(index: number): string {
let n = index;
let s = '';
while (n > 0) {
const rem = (n - 1) % 26;
s = String.fromCharCode(65 + rem) + s;
n = Math.floor((n - 1) / 26);
}
return s || 'A';
}
private buildImpairedCreditsSheet(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('TopImpairedCredits');
sheet.columns = [
{ width: 16 },
{ width: 40 },
{ width: 16 },
{ width: 16 },
{ width: 16 },
{ width: 16 },
{ width: 18 },
{ width: 18 },
{ width: 18 }
];
addBanner(sheet, 'TopImpairedCredits - Top Impaired Credits', 'I');
addMatrixTable(sheet, this.capitalMemo);
for (const t of this.recoveryTables) {
addMatrixTable(sheet, t);
}
const start = sheet.rowCount + 1;
const title = sheet.addRow(['Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs']);
sheet.mergeCells(title.number, 1, title.number, 9);
title.font = { bold: true };
title.getCell(1).fill = solidFill(XL.headerGrey);
const head = sheet.addRow([
'PAN of Borrower',
'Name of Borrower',
'Loans & Advances',
'Funded Exposure',
'Interest in arrear',
'% to Total Advances',
'% to Capital Funds of the Bank',
'Asset Classification',
'Loss Provision held'
]);
head.font = { bold: true };
head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const r of this.impairedCredits) {
const row = sheet.addRow([
r.pan,
r.name,
r.loansAdvances,
r.funded,
r.interestInArrear,
r.pctTotalAdvances,
r.pctCapitalFunds,
r.assetClassification,
r.lossProvisionHeld
]);
[
3,
4,
5,
9
].forEach((c) => (row.getCell(c).numFmt = '#,##0.00'));
[
6,
7
].forEach((c) => (row.getCell(c).numFmt = '0.00"%"'));
}
const total = sheet.addRow([
'Total',
'',
null,
this.impairedFundedTotal,
this.impairedInterestTotal,
null,
null,
'',
this.impairedProvisionTotal
]);
total.font = { bold: true };
[
4,
5,
9
].forEach((c) => {
total.getCell(c).numFmt = '#,##0.00';
total.getCell(c).fill = solidFill(XL.derivedBlue);
});
boxAllCells(sheet, start, sheet.rowCount, 9);
}
private buildExposureLimitsSheet(wb: ExcelJS.Workbook): void {
const sheet = wb.addWorksheet('ExposureLimits');
sheet.columns = [
{ width: 16 },
{ width: 46 },
{ width: 18 },
{ width: 18 },
{ width: 20 },
{ width: 22 },
{ width: 20 }
];
addBanner(sheet, 'ExposureLimits - Single and Group Borrower Exposure Limit', 'G');
addMatrixTable(sheet, this.capitalPreviousMarch);
const start = sheet.rowCount + 1;
const title = sheet.addRow(['Table 2: STATEMENT ON LARGE EXPOSURE - SINGLE BORROWER']);
sheet.mergeCells(title.number, 1, title.number, 7);
title.font = { bold: true };
title.getCell(1).fill = solidFill(XL.headerGrey);
const sub = sheet.addRow([
'In excess of 15% of banks Tier 1 capital as on previous March or a minimum of 20 large credits'
]);
sheet.mergeCells(sub.number, 1, sub.number, 7);
sub.font = { bold: true, size: 10 };
sub.getCell(1).fill = solidFill(XL.headerGrey);
const head = sheet.addRow([
'PAN of Borrower',
'Name of Borrower',
'Limit sanctioned',
'% to Total Advances',
'Outstanding Balance',
'% to Tier 1 capital of the Bank',
'Asset Classification'
]);
head.font = { bold: true };
head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
for (const r of this.largeExposures) {
const row = sheet.addRow([
r.pan,
r.name,
r.limitSanctioned,
r.pctTotalAdvances,
r.outstandingBalance,
r.pctTier1Capital,
r.assetClassification
]);
[
3,
5
].forEach((c) => (row.getCell(c).numFmt = '#,##0.00'));
[
4,
6
].forEach((c) => (row.getCell(c).numFmt = '0.00"%"'));
}
boxAllCells(sheet, start, sheet.rowCount, 7);
for (const note of this.exposureNotes) {
const n = sheet.addRow([note]);
sheet.mergeCells(n.number, 1, n.number, 7);
n.font = { bold: true, size: 9 };
n.alignment = { wrapText: true };
n.getCell(1).fill = solidFill(XL.headerGrey);
}
}
private toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
}
}
@@ -24,22 +24,6 @@ const routes: Routes = [
path: 'rbi/aml',
component: AmlRulesComponent,
data: { title: 'AML Rules Master', breadcrumb: 'AML Rules Master' }
},
{
path: 'rbi/oss1',
// Lazy — the ExcelJS workbook builder should not weigh down the eager bundle.
loadComponent: () => import('./oss1-ral/oss1-ral.component').then((m) => m.Oss1RalComponent),
data: { title: 'OSS 1 — Return on Assets and Liabilities', breadcrumb: 'OSS 1 (RAL)' }
},
{
path: 'rbi/oss2',
loadComponent: () => import('./oss2-roe/oss2-roe.component').then((m) => m.Oss2RoeComponent),
data: { title: 'OSS 2 — Return on Earnings', breadcrumb: 'OSS 2 (RoE)' }
},
{
path: 'rbi/oss3',
loadComponent: () => import('./oss3-raq/oss3-raq.component').then((m) => m.Oss3RaqComponent),
data: { title: 'OSS 3 — Return on Asset Quality', breadcrumb: 'OSS 3 (RAQ-UCB)' }
}
])
];
@@ -1,703 +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/.
*/
// Dummy data layer for the "Reconciliation" mega-menu — 16 screens across 5 groups (Core &
// Books / Bank & Clearing / Digital & Payments / Instruments & Members / Compliance &
// Financials). Every reconciliation type shares one generic two-sided model (Side A vs Side B,
// difference, match status) so the UI, KPIs and "Mark as Reconciled" action are all driven by
// the same pure functions below — no report can duplicate/contradict another's numbers.
//
// Branches are the SAME 8 used by the NPA module (Branch 1/Branch 2 + 6 named branches) so a
// real bank demo never shows two different branch lists in two different menus.
import { BRANCHES, BranchMaster } from '../npa/npa-data';
export { BRANCHES };
export type { BranchMaster };
export type ReconGroupId = 'core' | 'bank' | 'digital' | 'instruments' | 'compliance';
export type ReconScope = 'BRANCH' | 'HEAD_OFFICE';
export type ReconStatus = 'MATCHED' | 'UNMATCHED' | 'PENDING' | 'RESOLVED';
export const GROUP_LABELS: Record<ReconGroupId, string> = {
core: 'Core & Books',
bank: 'Bank & Clearing',
digital: 'Digital & Payments',
instruments: 'Instruments & Members',
compliance: 'Compliance & Financials'
};
export interface ReconTypeMeta {
id: string;
group: ReconGroupId;
label: string;
/**
* BRANCH — the underlying activity is booked at a specific branch (vouchers, cheques,
* customer digital transactions, share members, fixed assets), so Head Office sees a branch
* selector + consolidated view and a branch login is locked to its own branch — same pattern
* as the NPA module.
* HEAD_OFFICE — the activity has no branch dimension in real life (bank-wide P&L/GL control,
* treasury's own bank accounts, statutory tax filing, vendor procurement, inter-branch
* consolidation) — only Head Office can open these; a branch login sees a restricted notice.
*/
scope: ReconScope;
sideALabel: string;
sideBLabel: string;
referenceLabel: string;
/** Only set for the one genuinely three-sided report (Three-way Digital Reconciliation). */
extraLabel?: string;
}
export const RECON_TYPES: ReconTypeMeta[] = [
{
id: 'voucher',
group: 'core',
label: 'Voucher Reconciliation',
scope: 'BRANCH',
sideALabel: 'System Voucher Amount',
sideBLabel: 'Physical / Scanned Voucher Amount',
referenceLabel: 'Voucher No'
},
{
id: 'sundry-dr-cr',
group: 'core',
label: 'Sundry Dr / Cr Reconciliation',
scope: 'BRANCH',
sideALabel: 'GL Balance',
sideBLabel: 'Party Ledger Balance',
referenceLabel: 'Party Code'
},
{
id: 'pnl-impact',
group: 'core',
label: 'P&L Impact Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'P&L as per GL',
sideBLabel: 'P&L as per MIS Schedule',
referenceLabel: 'Income / Expense Head'
},
{
id: 'gl-reconciliation',
group: 'core',
label: 'GL Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'Total Debit',
sideBLabel: 'Total Credit',
referenceLabel: 'GL Code'
},
{
id: 'fixed-assets',
group: 'core',
label: 'Fixed Assets Reconciliation',
scope: 'BRANCH',
sideALabel: 'GL Asset Value (WDV)',
sideBLabel: 'Physical Verification Value',
referenceLabel: 'Asset Tag No'
},
{
id: 'inter-company',
group: 'core',
label: 'Inter-Company Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'HO Books (Inter-Branch A/c)',
sideBLabel: 'Branch Books (HO A/c)',
referenceLabel: 'Branch'
},
{
id: 'bank-reco',
group: 'bank',
label: 'Bank Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'Balance per Bank GL (Books)',
sideBLabel: 'Balance per Bank Statement',
referenceLabel: 'Bank Account'
},
{
id: 'api-bank-reco',
group: 'bank',
label: 'API Based Bank Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'Core Banking Balance',
sideBLabel: 'API Feed Balance',
referenceLabel: 'Bank Account (API)'
},
{
id: 'neft-rtgs',
group: 'bank',
label: 'NEFT / RTGS Reconciliation',
scope: 'BRANCH',
sideALabel: 'Amount per CBS',
sideBLabel: 'Amount per RBI Settlement File',
referenceLabel: 'UTR No'
},
{
id: 'clearing-reco',
group: 'bank',
label: 'Clearing Reconciliation',
scope: 'BRANCH',
sideALabel: 'Cheque Amount per CBS',
sideBLabel: 'Amount per Clearing House (CTS)',
referenceLabel: 'Cheque No'
},
{
id: 'udir',
group: 'digital',
label: 'UDIR (IMPS / UPI / ATM)',
scope: 'BRANCH',
sideALabel: 'Disputed Amount (Customer Claim)',
sideBLabel: 'NPCI UDIR Resolution Amount',
referenceLabel: 'UDIR Ticket No'
},
{
id: 'three-way-digital',
group: 'digital',
label: 'Three-way Digital Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'Core Banking Record',
sideBLabel: 'Settlement Bank Record',
extraLabel: 'Switch / NPCI Record',
referenceLabel: 'Transaction Ref (RRN)'
},
{
id: 'nach-reco',
group: 'digital',
label: 'NACH Reconciliation',
scope: 'BRANCH',
sideALabel: 'Mandate Amount per CBS',
sideBLabel: 'Amount per NPCI Response File',
referenceLabel: 'UMRN'
},
{
id: 'share-member',
group: 'instruments',
label: 'Share Member Reconciliation',
scope: 'BRANCH',
sideALabel: 'Share GL Balance',
sideBLabel: 'Member Share Ledger Total',
referenceLabel: 'Folio No'
},
{
id: 'invoice-payment',
group: 'compliance',
label: 'Invoice Payment',
scope: 'HEAD_OFFICE',
sideALabel: 'Invoice Amount (Vendor Bill)',
sideBLabel: 'Amount Paid per GL',
referenceLabel: 'Invoice No'
},
{
id: 'tax-reco',
group: 'compliance',
label: 'Tax Reconciliation',
scope: 'HEAD_OFFICE',
sideALabel: 'Deducted / Collected per Books',
sideBLabel: 'Remitted to Government (Challan)',
referenceLabel: 'Challan No'
}
];
export const DEFAULT_RECON_ID = 'voucher';
export function reconMeta(id: string): ReconTypeMeta {
return RECON_TYPES.find((t) => t.id === id) || RECON_TYPES[0];
}
export interface ReconItem {
id: string;
reconTypeId: string;
branchCode: string;
branchName: string;
groupLabel: string;
reference: string;
description: string;
date: string;
sideAAmount: number;
sideBAmount: number;
extraAmount?: number;
status: ReconStatus;
remarks: string;
}
export function difference(item: ReconItem): number {
return +(item.sideAAmount - item.sideBAmount).toFixed(2);
}
export function isOpen(item: ReconItem): boolean {
return item.status === 'UNMATCHED' || item.status === 'PENDING';
}
// -------------------------------------------------------------------------
// Date helpers (same shape as npa-data.ts, kept local so this module has no
// runtime dependency beyond the shared BRANCHES list)
// -------------------------------------------------------------------------
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;
}
export function todayDDMMYYYY(): string {
return fmt(new Date());
}
// -------------------------------------------------------------------------
// Seeded RNG — deterministic dataset across reloads (own seed, independent of npa-data.ts)
// -------------------------------------------------------------------------
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(20260213);
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);
const VENDOR_NAMES = [
'Om Stationery Suppliers',
'Shubham Printers',
'Shree Balaji AMC Services',
'Metro Security Services',
'Sai Computers & Networking',
'Deccan Furniture Works',
'Unity Housekeeping Services',
'Precision Fire & Safety',
'Konark Electricals',
'Vasant Stationery Mart'
];
const PARTY_NAMES = [
'Kiran Hardware',
'Vishal Electronics',
'Nilesh Electricals',
'Ramdas Plumbing Works',
'Anita Traders',
'Ganesh Enterprises',
'Om Textiles',
'Balaji Agro Traders',
'Sunil Auto Parts',
'Meera Fabricators'
];
const ASSET_NAMES = [
'Office Furniture Set',
'Desktop Computer (Branch Counter)',
'UPS 3 KVA',
'Currency Counting Machine',
'Air Conditioner 1.5 Ton',
'CCTV Camera System',
'Branch Generator 15 KVA',
'Server Rack Cabinet',
'Biometric Attendance Device',
'Two-Wheeler (Recovery Officer)'
];
const GL_HEADS = [
'Interest on Loans & Advances',
'Interest on Investments',
'Interest Paid on Deposits',
'Commission & Exchange',
'Locker Rent Income',
'Establishment Charges',
'Rent, Taxes & Lighting',
'Printing & Stationery',
'Depreciation on Fixed Assets',
'Bad Debts Written Off',
'Audit Fees',
'Sundry Income'
];
const BANK_ACCOUNTS = [
'SBI Current Account - 40012345678',
'HDFC Bank Current Account - 5020012345',
'ICICI Bank Current Account - 000305012345',
'Axis Bank Current Account - 91020012345',
'RBI Current Account (Banking Dept)'
];
interface GenOptions {
reconTypeId: string;
refPrefix: string;
refDigits: number;
descriptions: string[];
minAmt: number;
maxAmt: number;
unmatchedRate: number;
branchScoped: boolean;
countPerBranch?: number;
totalCount?: number;
today: Date;
}
function statusFor(unmatchedRate: number): ReconStatus {
const r = rng();
if (r < unmatchedRate * 0.6) {
return 'UNMATCHED';
}
if (r < unmatchedRate) {
return 'PENDING';
}
return 'MATCHED';
}
function amountsFor(status: ReconStatus, minAmt: number, maxAmt: number): { a: number; b: number } {
const a = round(between(minAmt, maxAmt));
if (status === 'MATCHED') {
return { a, b: a };
}
const gapPct = between(0.02, 0.35);
const sign = rng() < 0.5 ? 1 : -1;
const b = round(a * (1 - sign * gapPct));
return { a, b };
}
function generateGeneric(opts: GenOptions): ReconItem[] {
const items: ReconItem[] = [];
const meta = reconMeta(opts.reconTypeId);
const branchLoop = opts.branchScoped ? BRANCHES : [{ code: 'HO', name: 'Head Office' }];
const perBranch = opts.countPerBranch ?? opts.totalCount ?? 20;
branchLoop.forEach((branch, branchIdx) => {
const count = opts.branchScoped ? perBranch : (opts.totalCount ?? perBranch);
for (let i = 0; i < count; i++) {
const status = statusFor(opts.unmatchedRate);
const { a, b } = amountsFor(status, opts.minAmt, opts.maxAmt);
const daysAgo = round(between(1, 75));
const seq = String(branchIdx * 1000 + i + 1).padStart(opts.refDigits, '0');
items.push({
id: `${opts.reconTypeId}-${branch.code}-${i}`,
reconTypeId: opts.reconTypeId,
branchCode: branch.code,
branchName: branch.name,
groupLabel: GROUP_LABELS[meta.group],
reference: `${opts.refPrefix}${seq}`,
description: pick(opts.descriptions),
date: fmt(addDays(opts.today, -daysAgo)),
sideAAmount: a,
sideBAmount: status === 'MATCHED' ? a : b,
status,
remarks: status === 'MATCHED' ? '' : ''
});
}
});
return items;
}
function generateInterCompany(today: Date): ReconItem[] {
return BRANCHES.map((b): ReconItem => {
const status = statusFor(0.25);
const { a, b: bAmt } = amountsFor(status, 200000, 3500000);
return {
id: `inter-company-${b.code}`,
reconTypeId: 'inter-company',
branchCode: 'HO',
branchName: 'Head Office',
groupLabel: GROUP_LABELS.core,
reference: b.name,
description: `Inter-branch settlement account — ${b.name}`,
date: fmt(addDays(today, -round(between(1, 10)))),
sideAAmount: a,
sideBAmount: status === 'MATCHED' ? a : bAmt,
status,
remarks: ''
};
});
}
function generateBankReco(reconTypeId: string, today: Date, refPrefix: string): ReconItem[] {
const templates = [
'Cheque issued, not yet presented',
'Cheque deposited, not yet credited by bank',
'Bank charges debited, not yet booked in CBS',
'Interest credited by bank, not yet booked in CBS',
'Direct credit by remitter, not yet identified',
'Standing instruction debited by bank, pending booking',
'RTGS outward debited by bank, in-transit in CBS'
];
const items: ReconItem[] = [];
BANK_ACCOUNTS.forEach((acct, idx) => {
const lineCount = reconTypeId === 'api-bank-reco' ? 5 : 6;
for (let i = 0; i < lineCount; i++) {
const status = statusFor(0.55);
const { a, b } = amountsFor(status, 15000, 850000);
items.push({
id: `${reconTypeId}-${idx}-${i}`,
reconTypeId,
branchCode: 'HO',
branchName: 'Head Office',
groupLabel: GROUP_LABELS.bank,
reference: `${refPrefix}${String(idx + 1).padStart(2, '0')}${String(i + 1).padStart(3, '0')}`,
description: `${acct} — ${pick(templates)}`,
date: fmt(addDays(today, -round(between(1, 30)))),
sideAAmount: a,
sideBAmount: status === 'MATCHED' ? a : b,
status,
remarks: ''
});
}
});
return items;
}
function generateThreeWayDigital(today: Date): ReconItem[] {
const channels = [
'IMPS',
'UPI',
'NEFT',
'RTGS',
'ATM (NFS)'
];
const items: ReconItem[] = [];
for (let i = 0; i < 45; i++) {
const status = statusFor(0.12);
const core = round(between(500, 250000));
const switchAmt = status === 'MATCHED' ? core : round(core * (1 - between(0.02, 0.2)));
const settlement = status === 'MATCHED' ? core : round(core * (1 - between(0.02, 0.15)));
items.push({
id: `three-way-digital-${i}`,
reconTypeId: 'three-way-digital',
branchCode: 'HO',
branchName: 'Head Office',
groupLabel: GROUP_LABELS.digital,
reference: `RRN${String(700000 + i)}`,
description: `${pick(channels)} transaction — Core vs Switch vs Settlement Bank`,
date: fmt(addDays(today, -round(between(1, 20)))),
sideAAmount: core,
sideBAmount: settlement,
extraAmount: switchAmt,
status,
remarks: ''
});
}
return items;
}
function generateAll(): ReconItem[] {
const today = new Date();
return [
...generateGeneric({
reconTypeId: 'voucher',
refPrefix: 'JV/26/',
refDigits: 5,
descriptions: [
'Cash deposit voucher',
'Cash withdrawal voucher',
'Journal entry — interest provision',
'Journal entry — expense booking',
'Transfer voucher — inter-GL',
'Debit voucher — charges recovery',
'Credit voucher — refund adjustment'
],
minAmt: 500,
maxAmt: 250000,
unmatchedRate: 0.08,
branchScoped: true,
countPerBranch: 15,
today
}),
...generateGeneric({
reconTypeId: 'sundry-dr-cr',
refPrefix: 'PTY-',
refDigits: 4,
descriptions: PARTY_NAMES,
minAmt: 5000,
maxAmt: 180000,
unmatchedRate: 0.18,
branchScoped: true,
countPerBranch: 10,
today
}),
...generateGeneric({
reconTypeId: 'pnl-impact',
refPrefix: 'GLH-',
refDigits: 3,
descriptions: GL_HEADS,
minAmt: 40000,
maxAmt: 4200000,
unmatchedRate: 0.15,
branchScoped: false,
totalCount: 24,
today
}),
...generateGeneric({
reconTypeId: 'gl-reconciliation',
refPrefix: 'GL-',
refDigits: 6,
descriptions: [
'Suspense account clearance check',
'Control account tally — deposits',
'Control account tally — advances',
'Inter-branch adjustment head',
'Sundry assets/liabilities head',
...GL_HEADS
],
minAmt: 20000,
maxAmt: 5200000,
unmatchedRate: 0.1,
branchScoped: false,
totalCount: 42,
today
}),
...generateGeneric({
reconTypeId: 'fixed-assets',
refPrefix: 'FA-',
refDigits: 4,
descriptions: ASSET_NAMES,
minAmt: 8000,
maxAmt: 650000,
unmatchedRate: 0.12,
branchScoped: true,
countPerBranch: 8,
today
}),
...generateInterCompany(today),
...generateBankReco('bank-reco', today, 'BRS-'),
...generateBankReco('api-bank-reco', today, 'API-'),
...generateGeneric({
reconTypeId: 'neft-rtgs',
refPrefix: 'UTR',
refDigits: 12,
descriptions: [
'RTGS outward — beneficiary bank settlement',
'RTGS inward — remitting bank settlement',
'NEFT outward — beneficiary bank settlement',
'NEFT inward — remitting bank settlement'
],
minAmt: 25000,
maxAmt: 1800000,
unmatchedRate: 0.06,
branchScoped: true,
countPerBranch: 12,
today
}),
...generateGeneric({
reconTypeId: 'clearing-reco',
refPrefix: 'CHQ-',
refDigits: 6,
descriptions: [
'Outward clearing — cheque sent',
'Inward clearing — cheque received',
'CTS grid settlement adjustment',
'Cheque return — insufficient funds'
],
minAmt: 10000,
maxAmt: 620000,
unmatchedRate: 0.09,
branchScoped: true,
countPerBranch: 10,
today
}),
...generateGeneric({
reconTypeId: 'udir',
refPrefix: 'UDIR-',
refDigits: 5,
descriptions: [
'IMPS failed but debited — customer dispute',
'UPI transaction disputed — merchant not credited',
'ATM cash-not-dispensed but account debited',
'UPI duplicate debit dispute'
],
minAmt: 500,
maxAmt: 45000,
unmatchedRate: 0.35,
branchScoped: true,
countPerBranch: 4,
today
}),
...generateThreeWayDigital(today),
...generateGeneric({
reconTypeId: 'nach-reco',
refPrefix: 'UMRN',
refDigits: 10,
descriptions: [
'NACH debit — loan EMI collection',
'NACH debit — gold loan EMI collection',
'NACH credit — salary/DBT disbursement',
'NACH mandate return — insufficient funds'
],
minAmt: 1500,
maxAmt: 85000,
unmatchedRate: 0.14,
branchScoped: true,
countPerBranch: 8,
today
}),
...generateGeneric({
reconTypeId: 'share-member',
refPrefix: 'FOL-',
refDigits: 5,
descriptions: [
'Regular member — share balance',
'Nominal member — share balance',
'Share transfer on account closure',
'Share application pending allotment'
],
minAmt: 500,
maxAmt: 25000,
unmatchedRate: 0.07,
branchScoped: true,
countPerBranch: 9,
today
}),
...generateGeneric({
reconTypeId: 'invoice-payment',
refPrefix: 'INV-',
refDigits: 5,
descriptions: VENDOR_NAMES,
minAmt: 3000,
maxAmt: 380000,
unmatchedRate: 0.13,
branchScoped: false,
totalCount: 30,
today
}),
...generateGeneric({
reconTypeId: 'tax-reco',
refPrefix: 'CHLN-',
refDigits: 4,
descriptions: [
'TDS on FD/RD interest payouts',
'TDS on salary payments',
'TDS on contractor/vendor payments',
'GST on service charges collected',
'GST on locker rent collected',
'TCS on cash transactions'
],
minAmt: 8000,
maxAmt: 620000,
unmatchedRate: 0.11,
branchScoped: false,
totalCount: 18,
today
})
];
}
export const RECON_ITEMS: ReconItem[] = generateAll();
// -------------------------------------------------------------------------
// KPI helpers — always computed from the item list handed in, so a filtered
// (branch-scoped) view and the full book can never disagree on a total.
// -------------------------------------------------------------------------
export function totalSideA(items: ReconItem[]): number {
return items.reduce((s, i) => s + i.sideAAmount, 0);
}
export function totalSideB(items: ReconItem[]): number {
return items.reduce((s, i) => s + i.sideBAmount, 0);
}
export function totalDifference(items: ReconItem[]): number {
return items.reduce((s, i) => s + Math.abs(difference(i)), 0);
}
export function countByStatus(items: ReconItem[], status: ReconStatus): number {
return items.filter((i) => i.status === status).length;
}
export function matchRate(items: ReconItem[]): number {
return items.length ? (countByStatus(items, 'MATCHED') / items.length) * 100 : 100;
}
@@ -1,228 +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="pg">
<header class="pg-head">
<div>
<div class="pg-eyebrow">
RECONCILIATION · {{ groupLabels[activeGroup] | uppercase }}
<span class="recon-scope-chip">{{ meta.scope === 'HEAD_OFFICE' ? 'Head Office Only' : 'Branch-wise' }}</span>
</div>
<h1 class="pg-title">{{ meta.label }}</h1>
<p class="pg-sub">
{{ meta.sideALabel }} vs {{ meta.sideBLabel }}
@if (meta.extraLabel) {
vs {{ meta.extraLabel }}
}
— as on {{ asOnDate }}.
</p>
</div>
<div class="pg-toolbar">
@if (meta.scope === 'BRANCH') {
@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>
}
} @else {
<label class="pg-field">
<span>Scope</span>
<span class="pg-chip">Head Office (Whole Bank)</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>
@if (!isRestricted) {
<div class="inv-kpis">
<div class="inv-kpi">
<span class="inv-kpi__label">{{ meta.sideALabel }}</span>
<span class="inv-kpi__value">₹{{ totalA | number: '1.0-0' }}</span>
<span class="inv-kpi__sub">{{ items.length }} items</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">{{ meta.sideBLabel }}</span>
<span class="inv-kpi__value">₹{{ totalB | number: '1.0-0' }}</span>
<span class="inv-kpi__sub">{{ scopeName }}</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Absolute Difference</span>
<span class="inv-kpi__value" [class.npa-warn]="totalDiff > 0">₹{{ totalDiff | number: '1.0-0' }}</span>
<span class="inv-kpi__sub">{{ matchPct | number: '1.1-1' }}% matched</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Open Items</span>
<span class="inv-kpi__value npa-warn">{{ unmatchedCount + pendingCount }}</span>
<span class="inv-kpi__sub"
>{{ unmatchedCount }} unmatched · {{ pendingCount }} pending · {{ resolvedCount }} resolved</span
>
</div>
</div>
}
<div class="cctr-tabs">
@for (g of groups; 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>
@if (isRestricted) {
<div class="pg-card">
<div class="recon-restricted">
<fa-icon icon="university" class="recon-restricted__icon"></fa-icon>
<div class="recon-restricted__title">Managed Centrally by Head Office</div>
<p class="recon-restricted__body">
{{ meta.label }} has no branch-level dimension in real-world banking — it is reconciled centrally by the Head
Office Accounts/Treasury team. Your login ({{ currentOfficeName }}) does not have access to this screen.
Contact Head Office if you need a figure from this reconciliation.
</p>
</div>
</div>
} @else {
<div class="pg-card">
<div class="pos-section-label">{{ meta.label }} — {{ scopeName }}</div>
<div class="pg-table-scroll">
<table class="pg-table">
<thead>
<tr>
<th>{{ meta.referenceLabel }}</th>
<th>Description</th>
@if (meta.scope === 'BRANCH') {
<th>Branch</th>
}
<th>Date</th>
<th class="num">{{ meta.sideALabel }}</th>
<th class="num">{{ meta.sideBLabel }}</th>
@if (meta.extraLabel) {
<th class="num">{{ meta.extraLabel }}</th>
}
<th class="num">Difference</th>
<th>Status</th>
<th>Remarks / Action</th>
</tr>
</thead>
<tbody>
@for (item of paged(items); track item.id) {
<tr>
<td>
<span class="pg-chip">{{ item.reference }}</span>
</td>
<td>{{ item.description }}</td>
@if (meta.scope === 'BRANCH') {
<td>{{ item.branchName }}</td>
}
<td>{{ item.date }}</td>
<td class="num">₹{{ item.sideAAmount | number: '1.0-0' }}</td>
<td class="num">₹{{ item.sideBAmount | number: '1.0-0' }}</td>
@if (meta.extraLabel) {
<td class="num">₹{{ item.extraAmount | number: '1.0-0' }}</td>
}
<td class="num" [class.col-out]="diff(item) !== 0" [class.col-in]="diff(item) === 0">
₹{{ diff(item) | number: '1.0-0' }}
</td>
<td>
<span [class]="badgeClass(item.status)">{{ item.status }}</span>
</td>
<td>
@if (isItemOpen(item)) {
<div class="recon-action-row">
<input
type="text"
class="recon-remark-input"
[(ngModel)]="remarksDraft[item.id]"
placeholder="Remarks (optional)"
/>
<button type="button" class="pg-btn pg-btn--ghost" (click)="markReconciled(item)">
Mark as Reconciled
</button>
</div>
} @else if (item.status === 'RESOLVED') {
<span class="pg-sub">{{ item.remarks }}</span>
}
</td>
</tr>
} @empty {
<tr>
<td
[attr.colspan]="meta.scope === 'BRANCH' ? (meta.extraLabel ? 10 : 9) : meta.extraLabel ? 9 : 8"
class="pg-empty"
>
No {{ meta.label }} items for this branch.
</td>
</tr>
}
</tbody>
</table>
</div>
@if (items.length > pageSize) {
<div class="pg-pagination">
<span class="pg-pagination__info"
>Showing {{ rangeStart(items) }}–{{ rangeEnd(items) }} of {{ items.length }}</span
>
<div class="pg-pagination__controls">
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="currentPage === 1"
(click)="goToPage(currentPage - 1, items)"
>
‹ Prev
</button>
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(items) }}</span>
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="currentPage === pageCount(items)"
(click)="goToPage(currentPage + 1, items)"
>
Next ›
</button>
</div>
</div>
}
</div>
}
</div>
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
@@ -1,246 +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, 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 {
RECON_TYPES,
RECON_ITEMS,
BRANCHES,
GROUP_LABELS,
DEFAULT_RECON_ID,
ReconGroupId,
ReconTypeMeta,
ReconItem,
ReconStatus,
reconMeta,
difference,
isOpen,
totalSideA,
totalSideB,
totalDifference,
countByStatus,
matchRate,
todayDDMMYYYY
} from '../reconciliation-data';
/**
* The "Reconciliation" mega-menu — 16 screens across 5 groups (Core & Books / Bank & Clearing /
* Digital & Payments / Instruments & Members / Compliance & Financials) — collapsed into one
* route-param-driven component, mirroring the NPA module's pattern. Every screen shares the same
* generic two-sided (Side A vs Side B) reconciliation model in reconciliation-data.ts, and the
* same Head-Office-vs-branch access split used by NPA and Cash/Clearing/Transfer: activity that
* genuinely happens at a branch (vouchers, cheques, digital transactions, share members, fixed
* assets) is branch-scoped; activity with no branch dimension in real life (bank-wide P&L/GL
* control, treasury's own bank accounts, statutory tax filing, vendor procurement, inter-branch
* consolidation) is Head-Office-only.
*/
@Component({
selector: 'mifosx-reconciliation-report',
templateUrl: './reconciliation-report.component.html',
styleUrls: [
'../../cashier/cashier-shared.scss',
'../../investment/investment-shared.scss',
'../../npa/npa-shared.scss',
'../reconciliation-shared.scss'
],
standalone: true,
imports: [
CommonModule,
FormsModule,
RouterModule
]
})
export class ReconciliationReportComponent implements OnInit, OnDestroy {
readonly reportMeta = RECON_TYPES;
readonly groupLabels = GROUP_LABELS;
readonly branches = BRANCHES;
readonly groups: ReconGroupId[] = [
'core',
'bank',
'digital',
'instruments',
'compliance'
];
asOnDate = todayDDMMYYYY();
activeGroup: ReconGroupId = 'core';
activeReport = DEFAULT_RECON_ID;
isHeadOffice = false;
currentOfficeName = 'Branch Office';
private ownBranchCode: string | null = null;
/** '' = All Branches (Head Office consolidated view); otherwise a BranchMaster.code. */
branchFilter = '';
readonly pageSize = 20;
currentPage = 1;
remarksDraft: Record<string, string> = {};
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_RECON_ID;
const meta = this.reportMeta.find((m) => m.id === id) || this.reportMeta[0];
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) {
this.branchFilter = '';
} else {
this.ownBranchCode =
this.branches.find((b) => this.currentOfficeName.toLowerCase().includes(b.name.toLowerCase()))?.code || null;
this.branchFilter = this.ownBranchCode || this.branches[0].code;
}
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
goToReport(reportId: string): void {
this.router.navigate([
'/reconciliation',
reportId
]);
}
groupOf(group: ReconGroupId): ReconTypeMeta[] {
return this.reportMeta.filter((m) => m.group === group);
}
setBranch(code: string): void {
this.branchFilter = code;
this.currentPage = 1;
}
get meta(): ReconTypeMeta {
return reconMeta(this.activeReport);
}
/** True when a branch-level login has opened a Head-Office-only reconciliation type. */
get isRestricted(): boolean {
return this.meta.scope === 'HEAD_OFFICE' && !this.isHeadOffice;
}
get scopeName(): string {
if (this.meta.scope === 'HEAD_OFFICE') {
return 'Head Office';
}
if (!this.isHeadOffice) {
return this.currentOfficeName;
}
return this.branchFilter ? this.branches.find((b) => b.code === this.branchFilter)?.name || '' : 'All Branches';
}
get items(): ReconItem[] {
const all = RECON_ITEMS.filter((i) => i.reconTypeId === this.activeReport);
if (this.meta.scope !== 'BRANCH' || !this.branchFilter) {
return all;
}
return all.filter((i) => i.branchCode === this.branchFilter);
}
diff(item: ReconItem): number {
return difference(item);
}
isItemOpen(item: ReconItem): boolean {
return isOpen(item);
}
badgeClass(status: ReconStatus): string {
return `recon-badge recon-badge--${status.toLowerCase()}`;
}
// -------------------- KPIs (always over the full `items` list, never the paged slice) --------------------
get totalA(): number {
return totalSideA(this.items);
}
get totalB(): number {
return totalSideB(this.items);
}
get totalDiff(): number {
return totalDifference(this.items);
}
get matchedCount(): number {
return countByStatus(this.items, 'MATCHED');
}
get unmatchedCount(): number {
return countByStatus(this.items, 'UNMATCHED');
}
get pendingCount(): number {
return countByStatus(this.items, 'PENDING');
}
get resolvedCount(): number {
return countByStatus(this.items, 'RESOLVED');
}
get matchPct(): number {
return matchRate(this.items);
}
// -------------------- Pagination --------------------
paged<T>(list: T[]): T[] {
const start = (this.currentPage - 1) * this.pageSize;
return list.slice(start, start + this.pageSize);
}
pageCount(list: unknown[]): number {
return Math.max(1, Math.ceil(list.length / this.pageSize));
}
rangeStart(list: unknown[]): number {
return list.length === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1;
}
rangeEnd(list: unknown[]): number {
return Math.min(this.currentPage * this.pageSize, list.length);
}
goToPage(page: number, list: unknown[]): void {
this.currentPage = Math.min(Math.max(1, page), this.pageCount(list));
}
// -------------------- Actions --------------------
markReconciled(item: ReconItem): void {
const remark = (this.remarksDraft[item.id] || '').trim();
item.status = 'RESOLVED';
item.remarks = remark || 'Reconciled — difference verified and squared off.';
delete this.remarksDraft[item.id];
this.toast(`${this.meta.referenceLabel} ${item.reference} marked as reconciled.`);
}
generate(): void {
this.currentPage = 1;
this.toast(`${this.meta.label} refreshed 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);
}
}
@@ -1,41 +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 { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service';
import { ReconciliationReportComponent } from './reconciliation-report/reconciliation-report.component';
import { DEFAULT_RECON_ID } from './reconciliation-data';
const routes: Routes = [
Route.withShell([
{
path: 'reconciliation',
data: { title: 'Reconciliation', breadcrumb: 'Reconciliation' },
children: [
{
path: '',
redirectTo: DEFAULT_RECON_ID,
pathMatch: 'full'
},
{
path: ':reportId',
component: ReconciliationReportComponent,
data: { title: 'Reconciliation', breadcrumb: 'Reconciliation' }
}
]
}
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ReconciliationRoutingModule {}
@@ -1,107 +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/.
*/
// Reconciliation-status colouring layered on top of ../cashier/cashier-shared.scss (pg-*,
// cctr-*), ../investment/investment-shared.scss (inv-kpis, inv-form-*, inv-toast) and
// ../npa/npa-shared.scss (pg-pagination) — reused as-is since they're generic primitives.
.recon-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;
&--matched {
background: #f0fdf4;
color: #15803d;
border: 1px solid #bbf7d0;
}
&--unmatched {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
&--pending {
background: #fefce8;
color: #a16207;
border: 1px solid #fde68a;
}
&--resolved {
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
}
}
.recon-scope-chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
background: #f1f5f9;
color: #475569;
border: 1px solid #e2e8f0;
}
.recon-action-row {
display: flex;
align-items: center;
gap: 8px;
}
.recon-remark-input {
height: 32px;
min-width: 160px;
padding: 0 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 12px;
font-family: inherit;
color: #0d1b2a;
background: #fff;
}
.recon-restricted {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 56px 24px;
text-align: center;
color: #64748b;
&__icon {
font-size: 28px;
color: #94a3b8;
}
&__title {
font-size: 15px;
font-weight: 700;
color: #334155;
}
&__body {
max-width: 440px;
font-size: 13px;
line-height: 1.5;
}
}
@@ -1,22 +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 { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { ReconciliationRoutingModule } from './reconciliation-routing.module';
import { ReconciliationReportComponent } from './reconciliation-report/reconciliation-report.component';
@NgModule({
imports: [
SharedModule,
ReconciliationRoutingModule,
ReconciliationReportComponent
]
})
export class ReconciliationModule {}
+11 -11
View File
@@ -141,11 +141,11 @@ export interface SftInterestDividendRecord {
}
export const SFT_BANK = {
name: 'CBSNexora Bank Ltd',
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
filerPan: 'AABCB1100M',
filerTan: 'MUMR99001E',
itdReportingPortalId: 'CBSNEXORA-285BA',
itdReportingPortalId: 'BHARATURBAN-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@cbsnexorabank.example'
branchEmail: 'compliance@bharaturbanbank.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: 'CBSNEXORA BANK',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-001'
},
@@ -243,7 +243,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
cashAmountReceivedFromPerson: 1100000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001352000357',
relatedInstituteName: 'CBSNEXORA BANK',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-001'
},
@@ -285,7 +285,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
cashAmountReceivedFromPerson: 2500000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001352000420',
relatedInstituteName: 'CBSNEXORA BANK',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-002'
},
@@ -326,7 +326,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
cashAmountReceivedFromPerson: 1800000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001352000511',
relatedInstituteName: 'CBSNEXORA BANK',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-002'
},
@@ -368,7 +368,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
cashAmountReceivedFromPerson: 1250000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001101001145',
relatedInstituteName: 'CBSNEXORA BANK',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-004'
},
@@ -409,7 +409,7 @@ export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
cashAmountReceivedFromPerson: 2150000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001101001458',
relatedInstituteName: 'CBSNEXORA BANK',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-004'
}
@@ -436,7 +436,7 @@ export const SFT_ACCOUNT_RECORDS: SftAccountRecord[] = [
branchMobile: '9892000100',
branchFaxStd: '022',
branchFaxPhone: '24938011',
branchEmail: 'compliance@cbsnexorabank.example',
branchEmail: 'compliance@bharaturbanbank.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@cbsnexorabank.example',
branchEmail: 'compliance@bharaturbanbank.example',
aggGrossCashCredited: 5800000,
aggGrossCashDebited: 3450000,
amountCreditedBeforeDemonetization: 0,
+2 -2
View File
@@ -9,8 +9,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260801',
'hash': '1c125d97'
'version': '260731',
'hash': 'ca85bae4'
},
'allow_switching_backend_instance': true
};
-16
View File
@@ -1017,22 +1017,6 @@ 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%);