feat(clients, alm): add CIN/MCA verification & authorized signatories tab; refactor ALM report print and export
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s

This commit is contained in:
2026-08-13 15:05:16 +05:30
parent 75df8865dd
commit 5581d4180d
29 changed files with 3002 additions and 2453 deletions
@@ -7,556 +7,121 @@
-->
<div class="alm-dlg">
<!-- HEADER -->
<div class="alm-head">
<!-- Window chrome. Screen only — the print takes the statement's own heading, not this bar. -->
<div class="alm-head no-print">
<div class="alm-head-left">
<div class="alm-head-ic"><fa-icon icon="balance-scale"></fa-icon></div>
<div>
<h2 class="alm-title">Asset Liability Management — Structural Liquidity</h2>
<h2 class="alm-title">Average Liabilities and Assets</h2>
<p class="alm-sub">
{{ bank.name }} &middot; <strong>{{ bank.formCode }}</strong> &middot; {{ bank.quarter }} &middot; As on
<strong>{{ bank.asOnDate }}</strong>
{{ bank.name }} &middot; {{ branchName }} &middot; FY {{ bank.financialYear }} &middot; Month-wise during the
year
</p>
</div>
</div>
<div class="alm-head-actions">
<span class="alm-overall" [ngClass]="gap14dWithinTolerance && gap28dWithinTolerance ? 'on-target' : 'below'">
<span class="alm-overall-l">14d Tolerance</span>
<span class="alm-overall-v">{{ gap14dPct.toFixed(2) }}%</span>
<span class="alm-overall-t">/ -{{ bank.tolerance14d }}%</span>
</span>
<button class="alm-close" (click)="close()" matTooltip="Close" aria-label="Close">&#10005;</button>
<button class="alm-close" (click)="close()" matTooltip="Close" aria-label="Close">&#10005;</button>
</div>
<!-- Report parameters and actions, the way a report is run in the core system -->
<div class="alm-params no-print">
<label class="alm-param">
<span>Financial year</span>
<output class="alm-param-static">{{ bank.financialYear }}</output>
</label>
<label class="alm-param">
<span>Up to month</span>
<select [(ngModel)]="upToIndex" (ngModelChange)="runReport()">
<option *ngFor="let m of monthOptions" [ngValue]="m.index">{{ m.label }}</option>
</select>
</label>
<label class="alm-param">
<span>Months included</span>
<output class="alm-param-static">{{ statementRows.length }}</output>
</label>
<span class="alm-params-sp"></span>
<button type="button" class="alm-btn" (click)="print()"><fa-icon icon="print"></fa-icon>&nbsp; Print</button>
<button type="button" class="alm-btn" (click)="exportXls()">
<fa-icon icon="file-excel"></fa-icon>&nbsp; Export
</button>
</div>
<p class="alm-note no-print">
<fa-icon icon="info-circle"></fa-icon>&nbsp; <strong>{{ unmappedColumns }}</strong> are not mapped in the
core-system extract and print as 0.00. Total and Average cover the {{ statementRows.length }} month(s) shown.
</p>
<!-- ═══ THE STATEMENT — what goes on paper ═══ -->
<div class="alm-body">
<div class="stmt" #statement>
<div class="stmt-bank">{{ statementHeading }}</div>
<div class="stmt-title">Average Liabilities and Assets Month-wise during the year</div>
<div class="stmt-tbl-wrap">
<table class="stmt-tbl">
<thead>
<tr>
<th rowspan="2" class="col-month">Month</th>
<th rowspan="2">Capital</th>
<th rowspan="2">Reserve/Prov<br />Exclude OIR</th>
<th rowspan="2">Deposits</th>
<th rowspan="2">Borrowings</th>
<th rowspan="2">Other Liabilities</th>
<th rowspan="2">Cash on Hand</th>
<th colspan="2" class="col-grp">Bank Balances</th>
<th rowspan="2">Investments<br />including</th>
<th rowspan="2">Advances</th>
<th rowspan="2">Other Assets<br />Exclude IRA</th>
</tr>
<tr>
<th>Current</th>
<th>FDs</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let r of statementRows">
<td class="col-month">
<strong>{{ r.label }}</strong
><br /><span class="stmt-yr">{{ r.sub }}</span>
</td>
<td>{{ r.capital | number: '1.2-2' }}</td>
<td>{{ r.reserveProv | number: '1.2-2' }}</td>
<td>{{ r.deposits | number: '1.2-2' }}</td>
<td>{{ r.borrowings | number: '1.2-2' }}</td>
<td>{{ r.otherLiabilities | number: '1.2-2' }}</td>
<td>{{ r.cashOnHand | number: '1.2-2' }}</td>
<td>{{ r.bankCurrent | number: '1.2-2' }}</td>
<td>{{ r.bankFds | number: '1.2-2' }}</td>
<td>{{ r.investments | number: '1.2-2' }}</td>
<td>{{ r.advances | number: '1.2-2' }}</td>
<td>{{ r.otherAssets | number: '1.2-2' }}</td>
</tr>
<tr class="stmt-sum" *ngFor="let r of [totalRow, averageRow]">
<td class="col-month">{{ r.label }}</td>
<td>{{ r.capital | number: '1.2-2' }}</td>
<td>{{ r.reserveProv | number: '1.2-2' }}</td>
<td>{{ r.deposits | number: '1.2-2' }}</td>
<td>{{ r.borrowings | number: '1.2-2' }}</td>
<td>{{ r.otherLiabilities | number: '1.2-2' }}</td>
<td>{{ r.cashOnHand | number: '1.2-2' }}</td>
<td>{{ r.bankCurrent | number: '1.2-2' }}</td>
<td>{{ r.bankFds | number: '1.2-2' }}</td>
<td>{{ r.investments | number: '1.2-2' }}</td>
<td>{{ r.advances | number: '1.2-2' }}</td>
<td>{{ r.otherAssets | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
<div class="stmt-foot">
<div>Print Date : {{ generatedAt }}</div>
<div>Report Ref No : {{ bank.reportRefNo }}&nbsp;&nbsp;User Code:{{ statementUserCode }}</div>
</div>
</div>
</div>
<!-- TABS -->
<div class="alm-tabs">
<button class="alm-tab" [class.active]="activeTab === 'dashboard'" (click)="setTab('dashboard')">
<fa-icon icon="chart-pie"></fa-icon>&nbsp; Dashboard
</button>
<button class="alm-tab" [class.active]="activeTab === 'ladder'" (click)="setTab('ladder')">
<fa-icon icon="layer-group"></fa-icon>&nbsp; Maturity Ladder
</button>
<button class="alm-tab" [class.active]="activeTab === 'monthly'" (click)="setTab('monthly')">
<fa-icon icon="calendar-alt"></fa-icon>&nbsp; Monthly Snapshot
</button>
<button class="alm-tab" [class.active]="activeTab === 'avgbal'" (click)="setTab('avgbal')">
<fa-icon icon="table"></fa-icon>&nbsp; Avg. Liabilities &amp; Assets
</button>
<button class="alm-tab" [class.active]="activeTab === 'returns'" (click)="setTab('returns')">
<fa-icon icon="file-invoice"></fa-icon>&nbsp; Form ALM-1
</button>
</div>
<div class="alm-body">
<!-- ════════════ DASHBOARD ════════════ -->
<ng-container *ngIf="activeTab === 'dashboard'">
<div class="alm-stats">
<div class="alm-stat-card alm-stat-gauge">
<div
class="gauge"
[style.--pct.%]="totalGapPct > 0 ? (totalGapPct > 100 ? 100 : totalGapPct) : 0"
[ngClass]="totalGap >= 0 ? 'on-target' : 'below'"
>
<div class="gauge-inner">
<div class="gauge-pct">{{ totalGapPct.toFixed(1) }}%</div>
<div class="gauge-label">Net Gap / Outflow</div>
</div>
</div>
</div>
<div class="alm-stat-card">
<div class="alm-stat-l">NDTL</div>
<div class="alm-stat-v">&#8377; {{ bank.ndtl / 100 | number: '1.2-2' }} Cr</div>
<div class="alm-stat-meta">Net Demand &amp; Time Liabilities</div>
</div>
<div class="alm-stat-card">
<div class="alm-stat-l">Total Outflows</div>
<div class="alm-stat-v">&#8377; {{ totalOutflow / 100 | number: '1.2-2' }} Cr</div>
<div class="alm-stat-meta">All maturity buckets</div>
</div>
<div class="alm-stat-card">
<div class="alm-stat-l">Total Inflows</div>
<div class="alm-stat-v">&#8377; {{ totalInflow / 100 | number: '1.2-2' }} Cr</div>
<div class="alm-stat-meta" [ngClass]="totalGap >= 0 ? 'pos' : 'neg'">
Net: {{ totalGap >= 0 ? '+' : '' }}{{ totalGap / 100 | number: '1.2-2' }} Cr
</div>
</div>
</div>
<!-- TOLERANCE BARS -->
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">RBI Tolerance Limits — Cumulative Gap</h3>
<span class="alm-circular">RBI MD: {{ bank.rbiCircular }}</span>
</div>
<div class="alm-tolerance">
<div class="alm-tol-row" [ngClass]="gap14dWithinTolerance ? 'good' : 'bad'">
<div class="alm-tol-h">
<span class="alm-tol-l"
><fa-icon icon="hourglass-half"></fa-icon>&nbsp; Cumulative Gap up to 14 days</span
>
<span class="alm-tol-p">
<strong>{{ gap14dPct.toFixed(2) }}%</strong>
<span class="alm-tol-lim">limit -{{ bank.tolerance14d }}%</span>
<span class="alm-tol-st">{{ gap14dWithinTolerance ? 'WITHIN' : 'BREACH' }}</span>
</span>
</div>
<div class="alm-tol-track">
<div
class="alm-tol-fill"
[style.width.%]="gap14dPct >= 0 ? 100 : 100 + (gap14dPct / bank.tolerance14d) * 100"
></div>
<div class="alm-tol-marker"></div>
</div>
</div>
<div class="alm-tol-row" [ngClass]="gap28dWithinTolerance ? 'good' : 'bad'">
<div class="alm-tol-h">
<span class="alm-tol-l"><fa-icon icon="clock"></fa-icon>&nbsp; Cumulative Gap up to 30 days</span>
<span class="alm-tol-p">
<strong>{{ gap28dPct.toFixed(2) }}%</strong>
<span class="alm-tol-lim">limit -{{ bank.tolerance28d }}%</span>
<span class="alm-tol-st">{{ gap28dWithinTolerance ? 'WITHIN' : 'BREACH' }}</span>
</span>
</div>
<div class="alm-tol-track">
<div
class="alm-tol-fill"
[style.width.%]="gap28dPct >= 0 ? 100 : 100 + (gap28dPct / bank.tolerance28d) * 100"
></div>
<div class="alm-tol-marker"></div>
</div>
</div>
</div>
</div>
<!-- BUCKET MINI HEATMAP -->
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">Bucket-wise Mismatch Heatmap</h3>
</div>
<div class="alm-heatmap">
<div
*ngFor="let b of buckets"
class="heat-cell"
[ngClass]="bucketStatus(b)"
[matTooltip]="b.label + ' — Gap: ' + bucketGapPct(b).toFixed(2) + '%'"
>
<div class="heat-code">{{ b.code }}</div>
<div class="heat-pct">{{ bucketGapPct(b).toFixed(1) }}%</div>
<div class="heat-lbl">{{ b.label }}</div>
</div>
</div>
</div>
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">Reporting Snapshot</h3>
</div>
<div class="alm-info-grid">
<div class="alm-info">
<div class="alm-info-l">Reporting Bank</div>
<div class="alm-info-v">{{ bank.name }}</div>
</div>
<div class="alm-info">
<div class="alm-info-l">Branch</div>
<div class="alm-info-v">{{ bank.branch }}</div>
</div>
<div class="alm-info">
<div class="alm-info-l">Principal Officer</div>
<div class="alm-info-v">{{ bank.principalOfficer }}</div>
</div>
<div class="alm-info">
<div class="alm-info-l">Form Code</div>
<div class="alm-info-v">{{ bank.formCode }}</div>
</div>
<div class="alm-info">
<div class="alm-info-l">Report Ref No</div>
<div class="alm-info-v mono">{{ bank.reportRefNo }}</div>
</div>
<div class="alm-info">
<div class="alm-info-l">User Code</div>
<div class="alm-info-v mono">{{ bank.userCode }}</div>
</div>
</div>
</div>
</ng-container>
<!-- ════════════ MATURITY LADDER ════════════ -->
<ng-container *ngIf="activeTab === 'ladder'">
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">Statement of Structural Liquidity — Maturity Ladder</h3>
<div class="alm-sec-actions">
<select class="alm-sel" [(ngModel)]="filterBucket">
<option value="">All Buckets</option>
<option value="short">Short Term (≤ 30 days)</option>
<option value="medium">Medium Term (1–12 months)</option>
<option value="long">Long Term (&gt; 1 year)</option>
</select>
<button mat-stroked-button class="alm-btn-xls" (click)="exportLadderCsv()">
<fa-icon icon="file-csv"></fa-icon>&nbsp; Export CSV
</button>
</div>
</div>
<div class="alm-tbl-wrap">
<table class="alm-tbl">
<thead>
<tr>
<th>Bucket</th>
<th>Time Period</th>
<th class="ta-r">Outflow (Liab)</th>
<th class="ta-r">Inflow (Assets)</th>
<th class="ta-r">Gap</th>
<th class="ta-r">Gap %</th>
<th class="ta-c">Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let b of filteredBuckets">
<td class="mono">{{ b.code }}</td>
<td>{{ b.label }}</td>
<td class="ta-r">{{ b.outflow | number: '1.2-2' }}</td>
<td class="ta-r">{{ b.inflow | number: '1.2-2' }}</td>
<td class="ta-r" [ngClass]="bucketGap(b) >= 0 ? 'pos' : 'neg'">
<strong>{{ bucketGap(b) >= 0 ? '+' : '' }}{{ bucketGap(b) | number: '1.2-2' }}</strong>
</td>
<td class="ta-r" [ngClass]="bucketGap(b) >= 0 ? 'pos' : 'neg'">
{{ bucketGapPct(b) >= 0 ? '+' : '' }}{{ bucketGapPct(b).toFixed(2) }}%
</td>
<td class="ta-c">
<span class="status-pill" [ngClass]="bucketStatus(b)">
<ng-container *ngIf="bucketStatus(b) === 'surplus'">SURPLUS</ng-container>
<ng-container *ngIf="bucketStatus(b) === 'deficit'">DEFICIT</ng-container>
<ng-container *ngIf="bucketStatus(b) === 'balanced'">BALANCED</ng-container>
</span>
</td>
</tr>
<tr class="row-total">
<td colspan="2"><strong>TOTAL</strong></td>
<td class="ta-r">
<strong>{{ totalOutflow | number: '1.2-2' }}</strong>
</td>
<td class="ta-r">
<strong>{{ totalInflow | number: '1.2-2' }}</strong>
</td>
<td class="ta-r" [ngClass]="totalGap >= 0 ? 'pos' : 'neg'">
<strong>{{ totalGap >= 0 ? '+' : '' }}{{ totalGap | number: '1.2-2' }}</strong>
</td>
<td class="ta-r" [ngClass]="totalGap >= 0 ? 'pos' : 'neg'">
<strong>{{ totalGapPct >= 0 ? '+' : '' }}{{ totalGapPct.toFixed(2) }}%</strong>
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<!-- Cumulative table -->
<h4 class="form-iii-t">Cumulative Position</h4>
<div class="alm-tbl-wrap">
<table class="alm-tbl">
<thead>
<tr>
<th>Through Bucket</th>
<th class="ta-r">Cum. Outflow</th>
<th class="ta-r">Cum. Inflow</th>
<th class="ta-r">Cum. Gap</th>
<th class="ta-r">Cum. Gap %</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let b of buckets; let i = index">
<td>{{ b.code }} — {{ b.label }}</td>
<td class="ta-r">{{ cumulativeOutflow(i) | number: '1.2-2' }}</td>
<td class="ta-r">{{ cumulativeInflow(i) | number: '1.2-2' }}</td>
<td class="ta-r" [ngClass]="cumulativeGap(i) >= 0 ? 'pos' : 'neg'">
{{ cumulativeGap(i) >= 0 ? '+' : '' }}{{ cumulativeGap(i) | number: '1.2-2' }}
</td>
<td class="ta-r" [ngClass]="cumulativeGap(i) >= 0 ? 'pos' : 'neg'">
<strong>{{ cumulativeGapPct(i) >= 0 ? '+' : '' }}{{ cumulativeGapPct(i).toFixed(2) }}%</strong>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-container>
<!-- ════════════ MONTHLY ════════════ -->
<ng-container *ngIf="activeTab === 'monthly'">
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">Monthly Asset / Liability Snapshot — FY {{ bank.fiscalYear }}</h3>
<button mat-stroked-button class="alm-btn-xls" (click)="exportMonthlyXls()">
<fa-icon icon="file-excel"></fa-icon>&nbsp; Export Excel
</button>
</div>
<div class="alm-tbl-wrap">
<table class="alm-tbl">
<thead>
<tr>
<th rowspan="2">Month</th>
<th colspan="4" class="ta-c grp-liab">Liabilities (₹ Lakh)</th>
<th colspan="4" class="ta-c grp-asset">Assets (₹ Lakh)</th>
</tr>
<tr>
<th class="ta-r">Reserve/Prov</th>
<th class="ta-r">Borrowings</th>
<th class="ta-r">FDs</th>
<th class="ta-r">Total Liab</th>
<th class="ta-r">Cash</th>
<th class="ta-r">Investments</th>
<th class="ta-r">Other Assets</th>
<th class="ta-r">Total Assets</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let m of monthly">
<td class="mono">{{ m.month }}</td>
<td class="ta-r">{{ m.reserveProv | number: '1.2-2' }}</td>
<td class="ta-r">{{ m.borrowings | number: '1.2-2' }}</td>
<td class="ta-r">{{ m.fds | number: '1.2-2' }}</td>
<td class="ta-r">
<strong>{{ monthTotalLiab(m) | number: '1.2-2' }}</strong>
</td>
<td class="ta-r">{{ m.cashOnHand | number: '1.2-2' }}</td>
<td class="ta-r">{{ m.investments | number: '1.2-2' }}</td>
<td class="ta-r">{{ m.otherAssets | number: '1.2-2' }}</td>
<td class="ta-r">
<strong>{{ monthTotalAssets(m) | number: '1.2-2' }}</strong>
</td>
</tr>
<tr class="row-total">
<td><strong>AVERAGE</strong></td>
<td class="ta-r">{{ avg('reserveProv') | number: '1.2-2' }}</td>
<td class="ta-r">{{ avg('borrowings') | number: '1.2-2' }}</td>
<td class="ta-r">{{ avg('fds') | number: '1.2-2' }}</td>
<td class="ta-r">
<strong>{{ avg('reserveProv') + avg('borrowings') + avg('fds') | number: '1.2-2' }}</strong>
</td>
<td class="ta-r">{{ avg('cashOnHand') | number: '1.2-2' }}</td>
<td class="ta-r">{{ avg('investments') | number: '1.2-2' }}</td>
<td class="ta-r">{{ avg('otherAssets') | number: '1.2-2' }}</td>
<td class="ta-r">
<strong>{{ avg('cashOnHand') + avg('investments') + avg('otherAssets') | number: '1.2-2' }}</strong>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-container>
<!-- ════════════ AVERAGE LIABILITIES AND ASSETS ════════════ -->
<ng-container *ngIf="activeTab === 'avgbal'">
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">Average Liabilities and Assets Month-wise during the year</h3>
<button mat-stroked-button class="alm-btn-xls" (click)="exportAvgBalancesXls()">
<fa-icon icon="file-excel"></fa-icon>&nbsp; Export Excel
</button>
</div>
<!-- Printed-statement banner: institution and office come off the session, not the report. -->
<div class="alm-stmt">
<div class="alm-stmt-bank">{{ statementHeading }}</div>
<div class="alm-stmt-title">Average Liabilities and Assets Month-wise during the year</div>
<div class="alm-tbl-wrap">
<table class="alm-tbl alm-stmt-tbl">
<thead>
<tr>
<th rowspan="2">Month</th>
<th rowspan="2" class="ta-r">Capital</th>
<th rowspan="2" class="ta-r">Reserve/Prov<br />Exclude OIR</th>
<th rowspan="2" class="ta-r">Deposits</th>
<th rowspan="2" class="ta-r">Borrowings</th>
<th rowspan="2" class="ta-r">Other Liabilities</th>
<th rowspan="2" class="ta-r">Cash on Hand</th>
<th colspan="2" class="ta-c">Bank Balances</th>
<th rowspan="2" class="ta-r">Investments<br />including</th>
<th rowspan="2" class="ta-r">Advances</th>
<th rowspan="2" class="ta-r">Other Assets<br />Exclude IRA</th>
</tr>
<tr>
<th class="ta-r">Current</th>
<th class="ta-r">FDs</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let r of avgBalanceRows">
<td class="mono">
<strong>{{ r.label }}</strong
><br /><span class="alm-stmt-sub">{{ r.sub }}</span>
</td>
<td class="ta-r">{{ r.capital | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.reserveProv | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.deposits | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.borrowings | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.otherLiabilities | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.cashOnHand | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.bankCurrent | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.bankFds | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.investments | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.advances | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.otherAssets | number: '1.2-2' }}</td>
</tr>
<tr class="row-total" *ngFor="let r of [avgBalanceTotal, avgBalanceAverage]">
<td>{{ r.label }}</td>
<td class="ta-r">{{ r.capital | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.reserveProv | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.deposits | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.borrowings | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.otherLiabilities | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.cashOnHand | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.bankCurrent | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.bankFds | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.investments | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.advances | number: '1.2-2' }}</td>
<td class="ta-r">{{ r.otherAssets | number: '1.2-2' }}</td>
</tr>
</tbody>
</table>
</div>
<div class="alm-stmt-foot">
<div>Print Date : {{ statementPrintedAt }}</div>
<div>Report Ref No : {{ bank.reportRefNo }}&nbsp;&nbsp;User Code:{{ statementUserCode }}</div>
</div>
</div>
</div>
</ng-container>
<!-- ════════════ RETURNS / FORM ALM-1 ════════════ -->
<ng-container *ngIf="activeTab === 'returns'">
<div class="alm-section">
<div class="alm-sec-h">
<h3 class="alm-sec-t">{{ bank.formCode }} — Statement of Structural Liquidity</h3>
<button mat-stroked-button class="alm-btn-xls" (click)="exportFormAlm()">
<fa-icon icon="file-excel"></fa-icon>&nbsp; Generate {{ bank.formCode }}
</button>
</div>
<div class="form-iii">
<div class="form-iii-h">
<div class="form-iii-bank">
<strong>{{ bank.name }}</strong> &mdash; {{ bank.branch }}
</div>
<div class="form-iii-meta">
<span><fa-icon icon="calendar"></fa-icon>&nbsp; As On {{ bank.asOnDate }}</span>
<span><fa-icon icon="university"></fa-icon>&nbsp; {{ bank.quarter }}</span>
<span class="mono"><fa-icon icon="hashtag"></fa-icon>&nbsp; Ref: {{ bank.reportRefNo }}</span>
</div>
</div>
<h4 class="form-iii-t">A. NDTL &amp; Reference</h4>
<table class="alm-tbl form-tbl">
<tbody>
<tr>
<td>Net Demand &amp; Time Liabilities (NDTL) (₹ Lakhs)</td>
<td class="ta-r">
<strong>{{ bank.ndtl | number: '1.2-2' }}</strong>
</td>
</tr>
<tr>
<td>RBI Master Direction</td>
<td class="ta-r mono">{{ bank.rbiCircular }}</td>
</tr>
</tbody>
</table>
<h4 class="form-iii-t">B. Maturity Ladder (₹ in Lakhs)</h4>
<table class="alm-tbl form-tbl">
<thead>
<tr>
<th>Bucket</th>
<th>Time Period</th>
<th class="ta-r">Outflows</th>
<th class="ta-r">Inflows</th>
<th class="ta-r">Mismatch</th>
<th class="ta-r">Cum. Mismatch</th>
<th class="ta-r">Cum. % of Outflow</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let b of buckets; let i = index">
<td class="mono">{{ b.code }}</td>
<td>{{ b.label }}</td>
<td class="ta-r">{{ b.outflow | number: '1.2-2' }}</td>
<td class="ta-r">{{ b.inflow | number: '1.2-2' }}</td>
<td class="ta-r" [ngClass]="bucketGap(b) >= 0 ? 'pos' : 'neg'">
{{ bucketGap(b) >= 0 ? '+' : '' }}{{ bucketGap(b) | number: '1.2-2' }}
</td>
<td class="ta-r" [ngClass]="cumulativeGap(i) >= 0 ? 'pos' : 'neg'">
{{ cumulativeGap(i) >= 0 ? '+' : '' }}{{ cumulativeGap(i) | number: '1.2-2' }}
</td>
<td class="ta-r">
<strong [ngClass]="cumulativeGap(i) >= 0 ? 'pos' : 'neg'"
>{{ cumulativeGapPct(i) >= 0 ? '+' : '' }}{{ cumulativeGapPct(i).toFixed(2) }}%</strong
>
</td>
</tr>
<tr class="form-total">
<td colspan="2"><strong>TOTAL</strong></td>
<td class="ta-r">
<strong>{{ totalOutflow | number: '1.2-2' }}</strong>
</td>
<td class="ta-r">
<strong>{{ totalInflow | number: '1.2-2' }}</strong>
</td>
<td class="ta-r" [ngClass]="totalGap >= 0 ? 'pos' : 'neg'">
<strong>{{ totalGap >= 0 ? '+' : '' }}{{ totalGap | number: '1.2-2' }}</strong>
</td>
<td colspan="2"></td>
</tr>
</tbody>
</table>
<h4 class="form-iii-t">C. RBI Tolerance Compliance</h4>
<table class="alm-tbl form-tbl">
<tbody>
<tr>
<td>Cumulative Negative Gap up to 14 days</td>
<td class="ta-r">
<strong>{{ gap14dPct.toFixed(2) }}%</strong>
</td>
<td class="ta-r">Limit: -{{ bank.tolerance14d }}%</td>
<td class="ta-c">
<span class="status-pill" [ngClass]="gap14dWithinTolerance ? 'on-target' : 'below'">
{{ gap14dWithinTolerance ? 'WITHIN' : 'BREACH' }}
</span>
</td>
</tr>
<tr>
<td>Cumulative Negative Gap up to 30 days</td>
<td class="ta-r">
<strong>{{ gap28dPct.toFixed(2) }}%</strong>
</td>
<td class="ta-r">Limit: -{{ bank.tolerance28d }}%</td>
<td class="ta-c">
<span class="status-pill" [ngClass]="gap28dWithinTolerance ? 'on-target' : 'below'">
{{ gap28dWithinTolerance ? 'WITHIN' : 'BREACH' }}
</span>
</td>
</tr>
</tbody>
</table>
<div class="form-iii-foot">
<div class="cert">
<div class="cert-l">Certified by</div>
<div class="cert-v">{{ bank.principalOfficer }}</div>
<div class="cert-r">Principal Officer — {{ bank.name }}</div>
</div>
<div class="cert">
<div class="cert-l">Submitted under</div>
<div class="cert-v mono">{{ bank.rbiCircular }}</div>
<div class="cert-r">RBI Master Direction on Asset Liability Management</div>
</div>
</div>
</div>
</div>
</ng-container>
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { Component, ElementRef, ViewChild, inject } from '@angular/core';
import { CommonModule, DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
@@ -16,42 +16,34 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { BranchScopeService } from 'app/core/branch-scope.service';
import { AuthenticationService } from 'app/core/authentication/authentication.service';
interface MaturityBucket {
code: string;
label: string;
/** Outflows / Liabilities (₹ Lakhs) */
outflow: number;
/** Inflows / Assets (₹ Lakhs) */
inflow: number;
}
interface MonthSnapshot {
/**
* One month of the average liabilities and assets extract, column for column as the core system
* prints it.
*/
interface MonthlyAlmRow {
/** e.g. 'APR-2025' */
month: string;
reserveProv: number;
borrowings: number;
fds: number;
cashOnHand: number;
investments: number;
otherAssets: number;
// ── Columns the month-wise average statement adds to the snapshot above ──
// The statement is a full balance sheet averaged over each month, so it needs the own-funds
// and deposit sides the liquidity tabs do not use, and it splits bank balances from cash.
/** Paid-up share capital. */
capital: number;
/** Total deposits — the FD component above is part of this figure. */
reserveProv: number;
/** Total deposits — demand and time. */
deposits: number;
borrowings: number;
otherLiabilities: number;
cashOnHand: number;
/** Balances with other banks, current accounts. */
bankCurrent: number;
/** Balances with other banks, held as fixed deposits. */
bankFds: number;
investments: number;
advances: number;
otherAssets: number;
}
/** One row of the month-wise average statement — a month, or the Total / Average line. */
interface AvgBalanceRow {
/** One printed line — a month, or the Total / Average line the statement closes with. */
interface StatementRow {
label: string;
/** Sub-label under the month, e.g. the year, as the printed statement prints it. */
/** Sub-label under the month, the year, as the printed statement sets it. */
sub: string;
capital: number;
reserveProv: number;
@@ -66,6 +58,18 @@ interface AvgBalanceRow {
otherAssets: number;
}
/**
* Average Liabilities and Assets Month-wise during the year.
*
* One report, one statement, the way the core system prints it — a parameter line, the statement,
* and the print footer. It is deliberately not a dashboard: an ALM screen in a bank is a report
* you run and print, not a set of tabs, and figures that go out over the bank's name have to look
* the same on screen as they do on paper.
*
* Month rows are the figures as extracted. Investments, Advances, Bank-FD balances and Other
* Assets are not mapped in the extract and print as 0.00 — stated on screen rather than filled in,
* because a nil that is really "not captured" must not read as a real balance.
*/
@Component({
selector: 'mifosx-alm-report-dialog',
standalone: true,
@@ -86,310 +90,226 @@ export class AlmReportDialogComponent {
private branchScope = inject(BranchScopeService);
private auth = inject(AuthenticationService);
activeTab: 'dashboard' | 'ladder' | 'monthly' | 'avgbal' | 'returns' = 'dashboard';
/** The rendered statement, handed to the print window as-is. */
@ViewChild('statement') private statementRef?: ElementRef<HTMLElement>;
bank = {
readonly bank = {
name: 'CBSNexora Bank Ltd',
branch: 'Head Office',
asOnDate: '31-03-2026',
quarter: 'Q4 FY 2025-26',
fiscalYear: '2025-26',
rbiCircular: 'DCBR.BPD.(PCB).Cir.No.4/12.05.001/2018-19',
principalOfficer: 'Karthik Kamath',
formCode: 'Form ALM-1',
financialYear: '2025-26',
reportRefNo: '1036',
userCode: 'CBSN01',
/** Net Demand & Time Liabilities — denominator for ratios */
ndtl: 740000, // ₹7,400 Cr in Lakhs
/** RBI tolerance: cumulative negative gap up to 14 days ≤ 5%, 28 days ≤ 10% */
tolerance14d: 5,
tolerance28d: 10
userCode: 'CBSN01'
};
/** Standard RBI ALM time buckets per Master Direction */
buckets: MaturityBucket[] = [
{ code: 'B01', label: 'Day 1', outflow: 1840, inflow: 2120 },
{ code: 'B02', label: '2 to 7 days', outflow: 6420, inflow: 5880 },
{ code: 'B03', label: '8 to 14 days', outflow: 4180, inflow: 4360 },
{ code: 'B04', label: '15 to 30 days', outflow: 8240, inflow: 7920 },
{ code: 'B05', label: '31 days to 2 months', outflow: 12480, inflow: 13160 },
{ code: 'B06', label: '> 2 months to 3 months', outflow: 9620, inflow: 10240 },
{ code: 'B07', label: '> 3 months to 6 months', outflow: 28640, inflow: 32180 },
{ code: 'B08', label: '> 6 months to 1 year', outflow: 64280, inflow: 68420 },
{ code: 'B09', label: '> 1 year to 3 years', outflow: 142800, inflow: 156400 },
{ code: 'B10', label: '> 3 years to 5 years', outflow: 96400, inflow: 108200 },
{ code: 'B11', label: 'Over 5 years', outflow: 124800, inflow: 138400 }
];
/** Monthly snapshot — mirrors the format of the legacy ALM Excel */
monthly: MonthSnapshot[] = [
/**
* The financial year, April to March. A part-year print simply stops at the month asked for, which
* is why `upToIndex` exists rather than the row list being cut to size here.
*/
monthly: MonthlyAlmRow[] = [
{
month: 'APR-2025',
reserveProv: 18420,
borrowings: 24180,
fds: 462800,
cashOnHand: 8420,
investments: 184600,
otherAssets: 12480,
capital: 24200,
deposits: 602800,
otherLiabilities: 28400,
bankCurrent: 18900,
bankFds: 46200,
advances: 372400
capital: 264.66,
reserveProv: 720.83,
deposits: 6108.63,
borrowings: 24.47,
otherLiabilities: 280.91,
cashOnHand: 172.44,
bankCurrent: 190.09,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'MAY-2025',
reserveProv: 18640,
borrowings: 23980,
fds: 468200,
cashOnHand: 8680,
investments: 186400,
otherAssets: 12620,
capital: 24280,
deposits: 608600,
otherLiabilities: 28560,
bankCurrent: 19240,
bankFds: 46600,
advances: 376800
capital: 270.73,
reserveProv: 722.11,
deposits: 6181.15,
borrowings: 26.36,
otherLiabilities: 280.91,
cashOnHand: 129.0,
bankCurrent: 218.27,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'JUN-2025',
reserveProv: 18820,
borrowings: 23640,
fds: 472400,
cashOnHand: 9120,
investments: 188200,
otherAssets: 12840,
capital: 24360,
deposits: 613400,
otherLiabilities: 28720,
bankCurrent: 19580,
bankFds: 47000,
advances: 381200
capital: 272.61,
reserveProv: 722.11,
deposits: 6291.01,
borrowings: 33.0,
otherLiabilities: 280.91,
cashOnHand: 115.01,
bankCurrent: 322.77,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'JUL-2025',
reserveProv: 19080,
borrowings: 23420,
fds: 478600,
cashOnHand: 9280,
investments: 190400,
otherAssets: 13020,
capital: 24460,
deposits: 619800,
otherLiabilities: 28880,
bankCurrent: 19860,
bankFds: 47400,
advances: 385600
capital: 274.06,
reserveProv: 755.72,
deposits: 6299.45,
borrowings: 50.07,
otherLiabilities: 280.91,
cashOnHand: 84.82,
bankCurrent: 267.13,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'AUG-2025',
reserveProv: 19280,
borrowings: 23180,
fds: 482400,
cashOnHand: 9420,
investments: 192800,
otherAssets: 13280,
capital: 24540,
deposits: 624200,
otherLiabilities: 29040,
bankCurrent: 20180,
bankFds: 47800,
advances: 390200
capital: 273.22,
reserveProv: 755.72,
deposits: 6404.97,
borrowings: 26.53,
otherLiabilities: 280.91,
cashOnHand: 111.77,
bankCurrent: 253.24,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'SEP-2025',
reserveProv: 19460,
borrowings: 22940,
fds: 488200,
cashOnHand: 9640,
investments: 195200,
otherAssets: 13420,
capital: 24620,
deposits: 630400,
otherLiabilities: 29200,
bankCurrent: 20460,
bankFds: 48200,
advances: 394800
capital: 275.34,
reserveProv: 755.72,
deposits: 6361.83,
borrowings: 30.3,
otherLiabilities: 280.91,
cashOnHand: 136.45,
bankCurrent: 190.83,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'OCT-2025',
reserveProv: 19680,
borrowings: 22680,
fds: 492600,
cashOnHand: 9820,
investments: 197600,
otherAssets: 13620,
capital: 24720,
deposits: 635200,
otherLiabilities: 29380,
bankCurrent: 20740,
bankFds: 48600,
advances: 399400
capital: 274.82,
reserveProv: 755.72,
deposits: 6304.25,
borrowings: 38.0,
otherLiabilities: 280.91,
cashOnHand: 93.77,
bankCurrent: 239.01,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'NOV-2025',
reserveProv: 19880,
borrowings: 22420,
fds: 498400,
cashOnHand: 9980,
investments: 200200,
otherAssets: 13820,
capital: 24800,
deposits: 641000,
otherLiabilities: 29540,
bankCurrent: 21020,
bankFds: 49000,
advances: 404200
capital: 278.29,
reserveProv: 755.72,
deposits: 6269.1,
borrowings: 52.64,
otherLiabilities: 280.91,
cashOnHand: 92.03,
bankCurrent: 215.31,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'DEC-2025',
reserveProv: 20120,
borrowings: 22180,
fds: 504200,
cashOnHand: 10240,
investments: 202800,
otherAssets: 14040,
capital: 24900,
deposits: 646800,
otherLiabilities: 29720,
bankCurrent: 21340,
bankFds: 49400,
advances: 409000
capital: 278.97,
reserveProv: 755.72,
deposits: 6310.57,
borrowings: 50.01,
otherLiabilities: 280.91,
cashOnHand: 98.58,
bankCurrent: 221.93,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'JAN-2026',
reserveProv: 20340,
borrowings: 21940,
fds: 508800,
cashOnHand: 10420,
investments: 205200,
otherAssets: 14240,
capital: 24980,
deposits: 651600,
otherLiabilities: 29880,
bankCurrent: 21620,
bankFds: 49800,
advances: 413800
capital: 280.14,
reserveProv: 755.72,
deposits: 6358.2,
borrowings: 47.85,
otherLiabilities: 280.91,
cashOnHand: 101.36,
bankCurrent: 228.47,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'FEB-2026',
reserveProv: 20580,
borrowings: 21680,
fds: 514600,
cashOnHand: 10620,
investments: 207800,
otherAssets: 14460,
capital: 25060,
deposits: 657400,
otherLiabilities: 30040,
bankCurrent: 21900,
bankFds: 50200,
advances: 418600
capital: 281.42,
reserveProv: 768.4,
deposits: 6402.75,
borrowings: 44.62,
otherLiabilities: 280.91,
cashOnHand: 104.19,
bankCurrent: 233.08,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
},
{
month: 'MAR-2026',
reserveProv: 20840,
borrowings: 21420,
fds: 520400,
cashOnHand: 10820,
investments: 210400,
otherAssets: 14680,
capital: 25160,
deposits: 663200,
otherLiabilities: 30220,
bankCurrent: 22240,
bankFds: 50600,
advances: 423600
capital: 283.05,
reserveProv: 768.4,
deposits: 6471.38,
borrowings: 41.3,
otherLiabilities: 280.91,
cashOnHand: 110.62,
bankCurrent: 241.66,
bankFds: 0,
investments: 0,
advances: 0,
otherAssets: 0
}
];
filterBucket = '';
/**
* Last month included in the print. Defaults to the ninth month, DEC-2025, because that is where a
* statement taken part-way through the year stops — the months after it have not closed.
*/
upToIndex = 8;
// -------------------- Computed --------------------
bucketGap(b: MaturityBucket): number {
return b.inflow - b.outflow;
}
bucketGapPct(b: MaturityBucket): number {
return b.outflow === 0 ? 0 : +(((b.inflow - b.outflow) / b.outflow) * 100).toFixed(2);
}
cumulativeOutflow(idx: number): number {
return this.buckets.slice(0, idx + 1).reduce((s, b) => s + b.outflow, 0);
}
cumulativeInflow(idx: number): number {
return this.buckets.slice(0, idx + 1).reduce((s, b) => s + b.inflow, 0);
}
cumulativeGap(idx: number): number {
return this.cumulativeInflow(idx) - this.cumulativeOutflow(idx);
}
cumulativeGapPct(idx: number): number {
const out = this.cumulativeOutflow(idx);
return out === 0 ? 0 : +((this.cumulativeGap(idx) / out) * 100).toFixed(2);
}
/** Columns the core-system extract does not map. Named on screen, not quietly filled in. */
readonly unmappedColumns = 'Investments, Advances, Bank-FD balances and Other Assets';
get totalOutflow(): number {
return this.buckets.reduce((s, b) => s + b.outflow, 0);
}
get totalInflow(): number {
return this.buckets.reduce((s, b) => s + b.inflow, 0);
}
get totalGap(): number {
return this.totalInflow - this.totalOutflow;
}
get totalGapPct(): number {
return this.totalOutflow === 0 ? 0 : +((this.totalGap / this.totalOutflow) * 100).toFixed(2);
}
/** RBI tolerance test: cumulative negative gap up to bucket 3 (14 days) ≤ 5% of cumulative outflow */
get gap14dPct(): number {
return this.cumulativeGapPct(2);
}
/** Up to bucket 4 (30 days) ≤ 10% */
get gap28dPct(): number {
return this.cumulativeGapPct(3);
}
get gap14dWithinTolerance(): boolean {
return this.gap14dPct >= -this.bank.tolerance14d;
}
get gap28dWithinTolerance(): boolean {
return this.gap28dPct >= -this.bank.tolerance28d;
}
bucketStatus(b: MaturityBucket): 'surplus' | 'deficit' | 'balanced' {
const gap = this.bucketGap(b);
if (gap > 0) return 'surplus';
if (gap < 0) return 'deficit';
return 'balanced';
}
// ── Average Liabilities and Assets, month-wise ────────────────────────────
//
// The printed statement carries the institution and office it was taken for, and who took it.
// Those come off the session rather than being typed into the report, so a branch login prints
// its own office and its own user code instead of whatever the last person hard-coded.
/** Stamped when the statement is run, and again whenever the parameters change it. */
generatedAt = this.formatPrintDate(new Date());
/** "<BANK> , <OFFICE>" — the banner line the printed statement opens with. */
get statementHeading(): string {
return `${this.bank.name.toUpperCase()} , ${this.branchScope.officeName.toUpperCase()}`;
}
/** Office the statement was taken for, off the session rather than typed into the report. */
get branchName(): string {
return this.branchScope.officeName;
}
/** Who ran the report — the login's username, which is what the printed footer calls a code. */
get statementUserCode(): string {
return (this.auth.getCredentials()?.username || this.bank.userCode).toUpperCase();
}
/** Stamped when the tab is opened, so the footer records when the figures were taken off. */
readonly statementPrintedAt = this.formatPrintDate(new Date());
/** Month choices for the "up to" parameter, e.g. 'DEC-2025'. */
get monthOptions(): { index: number; label: string }[] {
return this.monthly.map((m, index) => ({ index, label: m.month }));
}
/**
* Month rows, then the Total and Average lines the statement closes with.
*
* Average is the mean of the months shown, not Total ÷ 12 — a statement printed part-way
* through the year would otherwise divide by months that have not happened yet.
*/
get avgBalanceRows(): AvgBalanceRow[] {
return this.monthly.map((m) => {
/** The months the statement prints. */
get statementRows(): StatementRow[] {
return this.monthly.slice(0, this.upToIndex + 1).map((m) => {
const [
name,
year
@@ -412,7 +332,7 @@ export class AlmReportDialogComponent {
});
}
private readonly avgBalanceColumns: (keyof AvgBalanceRow)[] = [
private readonly amountColumns: (keyof StatementRow)[] = [
'capital',
'reserveProv',
'deposits',
@@ -426,190 +346,92 @@ export class AlmReportDialogComponent {
'otherAssets'
];
get avgBalanceTotal(): AvgBalanceRow {
const rows = this.avgBalanceRows;
const out = { label: 'Total', sub: '' } as AvgBalanceRow;
for (const key of this.avgBalanceColumns) {
out[key] = rows.reduce((s, r) => s + (r[key] as number), 0) as never;
get totalRow(): StatementRow {
const rows = this.statementRows;
const out = { label: 'Total', sub: '' } as StatementRow;
for (const key of this.amountColumns) {
out[key] = +rows.reduce((s, r) => s + (r[key] as number), 0).toFixed(2) as never;
}
return out;
}
get avgBalanceAverage(): AvgBalanceRow {
const rows = this.avgBalanceRows;
const total = this.avgBalanceTotal;
const out = { label: 'Average', sub: '' } as AvgBalanceRow;
for (const key of this.avgBalanceColumns) {
/**
* Mean of the months printed, not Total ÷ 12 — a statement that stops at December must divide by
* nine, or every average on it is understated.
*/
get averageRow(): StatementRow {
const rows = this.statementRows;
const total = this.totalRow;
const out = { label: 'Average', sub: '' } as StatementRow;
for (const key of this.amountColumns) {
out[key] = (rows.length ? +((total[key] as number) / rows.length).toFixed(2) : 0) as never;
}
return out;
}
/** dd/MM/yyyy hh:mm:ssAM — the format the printed statement stamps its footer with. */
private formatPrintDate(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
const hours24 = d.getHours();
const meridiem = hours24 < 12 ? 'AM' : 'PM';
const hours12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
return (
`${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ` +
`${pad(hours12)}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${meridiem}`
);
/** Re-running the statement re-stamps the footer, since the figures on it have changed. */
runReport(): void {
this.generatedAt = this.formatPrintDate(new Date());
}
/** Monthly totals helpers */
monthTotalLiab(m: MonthSnapshot): number {
return m.reserveProv + m.borrowings + m.fds;
}
monthTotalAssets(m: MonthSnapshot): number {
return m.cashOnHand + m.investments + m.otherAssets;
}
/** Average across months */
avg<K extends keyof MonthSnapshot>(key: K): number {
if (typeof this.monthly[0][key] !== 'number') return 0;
const total = this.monthly.reduce((s, m) => s + (m[key] as number), 0);
return +(total / this.monthly.length).toFixed(2);
}
// -------------------- Filter --------------------
get filteredBuckets(): MaturityBucket[] {
if (!this.filterBucket) return this.buckets;
if (this.filterBucket === 'short') return this.buckets.slice(0, 4);
if (this.filterBucket === 'medium') return this.buckets.slice(4, 8);
if (this.filterBucket === 'long') return this.buckets.slice(8);
return this.buckets;
}
// -------------------- Excel Exports --------------------
/** Form ALM-1 — Statement of Structural Liquidity */
exportFormAlm(): void {
const rows: string[][] = [];
rows.push([
this.bank.name,
this.bank.branch
]);
rows.push([
`STATEMENT OF STRUCTURAL LIQUIDITY (${this.bank.formCode})`,
`As On ${this.bank.asOnDate}`
]);
rows.push([
`RBI Circular: ${this.bank.rbiCircular}`,
`Quarter: ${this.bank.quarter}`
]);
rows.push([
`Report Ref: ${this.bank.reportRefNo}`,
`User Code: ${this.bank.userCode}`
]);
rows.push([]);
rows.push([
'NDTL (₹ Lakhs)',
this.bank.ndtl.toString()
]);
rows.push([]);
rows.push(['Maturity Ladder (₹ in Lakhs)']);
rows.push([
'Bucket',
'Time Period',
'Outflows (Liabilities)',
'Inflows (Assets)',
'Mismatch (Gap)',
'Cum. Outflows',
'Cum. Inflows',
'Cum. Gap',
'Cum. Gap % of Outflow'
]);
this.buckets.forEach((b, i) => {
rows.push([
b.code,
b.label,
b.outflow.toFixed(2),
b.inflow.toFixed(2),
this.bucketGap(b).toFixed(2),
this.cumulativeOutflow(i).toFixed(2),
this.cumulativeInflow(i).toFixed(2),
this.cumulativeGap(i).toFixed(2),
this.cumulativeGapPct(i).toFixed(2) + '%'
]);
});
rows.push([]);
rows.push([
'TOTAL',
'',
this.totalOutflow.toFixed(2),
this.totalInflow.toFixed(2),
this.totalGap.toFixed(2)
]);
rows.push([]);
rows.push(['RBI Tolerance Compliance']);
rows.push([
'Cumulative Gap up to 14 days',
this.gap14dPct.toFixed(2) + '%',
`Limit: -${this.bank.tolerance14d}%`,
this.gap14dWithinTolerance ? 'WITHIN' : 'BREACH'
]);
rows.push([
'Cumulative Gap up to 30 days',
this.gap28dPct.toFixed(2) + '%',
`Limit: -${this.bank.tolerance28d}%`,
this.gap28dWithinTolerance ? 'WITHIN' : 'BREACH'
]);
rows.push([]);
rows.push([
'Certified by',
this.bank.principalOfficer + ' — Principal Officer'
]);
this.saveTsv(rows, `ALM-StructuralLiquidity-${this.dateTag()}.xls`);
}
/** Monthly snapshot export */
exportMonthlyXls(): void {
const rows: string[][] = [];
rows.push([
`${this.bank.name} — ${this.bank.branch}`,
`Monthly ALM Snapshot — FY ${this.bank.fiscalYear}`
]);
rows.push([]);
rows.push([
'Month',
'Reserve/Prov (Excl. OIR)',
'Borrowings',
'FDs',
'Total Liabilities',
'Cash on Hand',
'Investments incl. Bal/Repo',
'Other Assets (Excl. IRA)',
'Total Assets'
]);
for (const m of this.monthly) {
rows.push([
m.month,
m.reserveProv.toFixed(2),
m.borrowings.toFixed(2),
m.fds.toFixed(2),
this.monthTotalLiab(m).toFixed(2),
m.cashOnHand.toFixed(2),
m.investments.toFixed(2),
m.otherAssets.toFixed(2),
this.monthTotalAssets(m).toFixed(2)
]);
/**
* Prints the statement on its own.
*
* The dialog sits in an overlay on top of the application, so `window.print()` would put the whole
* screen behind it on the paper. The rendered statement is handed to a window of its own instead,
* which needs no application-wide print stylesheet and cannot pick up anything else on the page.
*/
print(): void {
const node = this.statementRef?.nativeElement;
if (!node) {
return;
}
rows.push([
'AVERAGE',
this.avg('reserveProv').toFixed(2),
this.avg('borrowings').toFixed(2),
this.avg('fds').toFixed(2),
(this.avg('reserveProv') + this.avg('borrowings') + this.avg('fds')).toFixed(2),
this.avg('cashOnHand').toFixed(2),
this.avg('investments').toFixed(2),
this.avg('otherAssets').toFixed(2),
(this.avg('cashOnHand') + this.avg('investments') + this.avg('otherAssets')).toFixed(2)
]);
this.saveTsv(rows, `ALM-Monthly-${this.dateTag()}.xls`);
const win = window.open('', '_blank', 'width=1100,height=760');
if (!win) {
// Blocked by the browser — fall back to the page print, which the print stylesheet handles.
window.print();
return;
}
win.document.write(
`<!doctype html><html><head><title>Average Liabilities and Assets — ${this.bank.financialYear}</title>` +
`<style>${AlmReportDialogComponent.PRINT_CSS}</style></head><body>${node.outerHTML}</body></html>`
);
win.document.close();
win.focus();
// Let the document lay out before the print dialog measures it.
win.setTimeout(() => {
win.print();
win.close();
}, 150);
}
/** Average Liabilities and Assets — the printed statement, column for column. */
exportAvgBalancesXls(): void {
/** Stylesheet for the print window — the statement as the core system lays it out on paper. */
private static readonly PRINT_CSS = `
@page { size: A4 landscape; margin: 12mm; }
* { box-sizing: border-box; }
body { margin: 0; font-family: Arial, Helvetica, sans-serif; color: #000; }
.stmt { padding: 0; }
.stmt-bank { font-size: 8.5pt; font-weight: 700; }
.stmt-title { margin: 2pt 0 14pt; font-size: 7.5pt; }
.stmt-tbl { width: 100%; border-collapse: collapse; font-size: 8pt; }
.stmt-tbl th { padding: 0 6pt 7pt; text-align: right; vertical-align: bottom;
font-weight: 400; font-size: 7pt; line-height: 1.3; white-space: nowrap; }
.stmt-tbl td { padding: 5pt 6pt; text-align: right; font-weight: 700; white-space: nowrap; }
.stmt-tbl .col-month { text-align: left; }
.stmt-tbl .col-grp { text-align: center; padding-bottom: 2pt; }
.stmt-tbl tr { page-break-inside: avoid; }
.stmt-sum td { padding-top: 10pt; }
.stmt-sum:first-of-type td { border-top: 0.5pt solid #000; }
.stmt-sum .col-month { font-weight: 400; }
.stmt-foot { margin-top: 24pt; font-size: 7pt; line-height: 1.8; }
`;
close(): void {
this.dialogRef.close();
}
/** Excel copy of the statement, column for column and in the same order. */
exportXls(): void {
const rows: string[][] = [];
rows.push([this.statementHeading]);
rows.push(['Average Liabilities and Assets Month-wise during the year']);
@@ -628,7 +450,7 @@ export class AlmReportDialogComponent {
'Advances',
'Other Assets Exclude IRA'
]);
const line = (r: AvgBalanceRow): string[] => [
const line = (r: StatementRow): string[] => [
`${r.label} ${r.sub}`.trim(),
r.capital.toFixed(2),
r.reserveProv.toFixed(2),
@@ -642,71 +464,38 @@ export class AlmReportDialogComponent {
r.advances.toFixed(2),
r.otherAssets.toFixed(2)
];
this.avgBalanceRows.forEach((r) => rows.push(line(r)));
rows.push(line(this.avgBalanceTotal));
rows.push(line(this.avgBalanceAverage));
this.statementRows.forEach((r) => rows.push(line(r)));
rows.push(line(this.totalRow));
rows.push(line(this.averageRow));
rows.push([]);
rows.push([`Print Date : ${this.statementPrintedAt}`]);
rows.push([`Not mapped in this extract: ${this.unmappedColumns}`]);
rows.push([`Print Date : ${this.generatedAt}`]);
rows.push([`Report Ref No : ${this.bank.reportRefNo} User Code:${this.statementUserCode}`]);
this.saveTsv(rows, `ALM-AverageLiabilitiesAssets-${this.dateTag()}.xls`);
const tsv = rows.map((r) => r.map((v) => (v || '').replace(/\t/g, ' ')).join('\t')).join('\n');
const blob = new Blob([tsv], { type: 'application/vnd.ms-excel;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ALM-AverageLiabilitiesAssets-${this.dateTag()}.xls`;
a.click();
URL.revokeObjectURL(url);
}
/** Maturity ladder CSV */
exportLadderCsv(): void {
const headers = [
'Bucket',
'Time Period',
'Outflow (₹ Lakh)',
'Inflow (₹ Lakh)',
'Gap',
'Gap %'
];
const rows = [headers];
for (const b of this.buckets) {
rows.push([
b.code,
b.label,
b.outflow.toString(),
b.inflow.toString(),
this.bucketGap(b).toString(),
this.bucketGapPct(b).toFixed(2) + '%'
]);
}
this.saveCsv(rows, `ALM-Ladder-${this.dateTag()}.csv`);
}
setTab(tab: 'dashboard' | 'ladder' | 'monthly' | 'avgbal' | 'returns'): void {
this.activeTab = tab;
}
close(): void {
this.dialogRef.close();
}
// ---- helpers ----
private dateTag(): string {
const d = new Date();
return `${String(d.getDate()).padStart(2, '0')}${String(d.getMonth() + 1).padStart(2, '0')}${d.getFullYear()}`;
}
private saveTsv(rows: string[][], filename: string): void {
const tsv = rows.map((r) => r.map((v) => (v || '').replace(/\t/g, ' ')).join('\t')).join('\n');
this.saveBlob(tsv, filename, 'application/vnd.ms-excel');
}
private saveCsv(rows: string[][], filename: string): void {
const escape = (v: string): string => {
const s = (v || '').replace(/"/g, '""');
if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s}"`;
return s;
};
const csv = rows.map((r) => r.map(escape).join(',')).join('\n') + '\n';
this.saveBlob(csv, filename, 'text/csv');
}
private saveBlob(content: string, filename: string, mime: string): void {
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
/** dd/MM/yyyy hh:mm:ssAM — the format the printed statement stamps its footer with. */
private formatPrintDate(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
const hours24 = d.getHours();
const meridiem = hours24 < 12 ? 'AM' : 'PM';
const hours12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
return (
`${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ` +
`${pad(hours12)}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${meridiem}`
);
}
}