feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
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

This commit is contained in:
2026-08-11 11:11:18 +05:30
parent 01e86173c1
commit 7b895a0e2b
77 changed files with 10046 additions and 1130 deletions
@@ -40,6 +40,9 @@
<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>
@@ -344,6 +347,87 @@
</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">
@@ -597,6 +597,58 @@
}
}
/* AVERAGE LIABILITIES AND ASSETS — laid out as the statement prints */
.alm-stmt {
padding: 16px;
border: 1px solid var(--alm-g200);
border-radius: 10px;
background: #fff;
}
.alm-stmt-bank {
font-size: 13px;
font-weight: 700;
color: var(--alm-g900);
}
.alm-stmt-title {
margin: 2px 0 14px;
font-size: 12px;
color: var(--alm-g500);
}
.alm-stmt-tbl {
th,
td {
padding: 7px 10px;
font-size: 12px;
}
th {
text-transform: none;
letter-spacing: 0;
white-space: normal;
}
td.ta-r {
font-family: 'DM Mono', monospace;
}
}
.alm-stmt-sub {
font-size: 10px;
color: var(--alm-g500);
}
.alm-stmt-foot {
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid var(--alm-g200);
font-size: 11px;
line-height: 1.7;
color: var(--alm-g500);
}
.status-pill {
display: inline-block;
padding: 3px 10px;
@@ -13,6 +13,8 @@ import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
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;
@@ -31,6 +33,37 @@ interface MonthSnapshot {
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. */
deposits: number;
otherLiabilities: number;
/** Balances with other banks, current accounts. */
bankCurrent: number;
/** Balances with other banks, held as fixed deposits. */
bankFds: number;
advances: number;
}
/** One row of the month-wise average statement — a month, or the Total / Average line. */
interface AvgBalanceRow {
label: string;
/** Sub-label under the month, e.g. the year, as the printed statement prints it. */
sub: string;
capital: number;
reserveProv: number;
deposits: number;
borrowings: number;
otherLiabilities: number;
cashOnHand: number;
bankCurrent: number;
bankFds: number;
investments: number;
advances: number;
otherAssets: number;
}
@Component({
@@ -50,8 +83,10 @@ interface MonthSnapshot {
})
export class AlmReportDialogComponent {
private dialogRef = inject(MatDialogRef<AlmReportDialogComponent>);
private branchScope = inject(BranchScopeService);
private auth = inject(AuthenticationService);
activeTab: 'dashboard' | 'ladder' | 'monthly' | 'returns' = 'dashboard';
activeTab: 'dashboard' | 'ladder' | 'monthly' | 'avgbal' | 'returns' = 'dashboard';
bank = {
name: 'CBSNexora Bank Ltd',
@@ -95,7 +130,13 @@ export class AlmReportDialogComponent {
fds: 462800,
cashOnHand: 8420,
investments: 184600,
otherAssets: 12480
otherAssets: 12480,
capital: 24200,
deposits: 602800,
otherLiabilities: 28400,
bankCurrent: 18900,
bankFds: 46200,
advances: 372400
},
{
month: 'MAY-2025',
@@ -104,7 +145,13 @@ export class AlmReportDialogComponent {
fds: 468200,
cashOnHand: 8680,
investments: 186400,
otherAssets: 12620
otherAssets: 12620,
capital: 24280,
deposits: 608600,
otherLiabilities: 28560,
bankCurrent: 19240,
bankFds: 46600,
advances: 376800
},
{
month: 'JUN-2025',
@@ -113,7 +160,13 @@ export class AlmReportDialogComponent {
fds: 472400,
cashOnHand: 9120,
investments: 188200,
otherAssets: 12840
otherAssets: 12840,
capital: 24360,
deposits: 613400,
otherLiabilities: 28720,
bankCurrent: 19580,
bankFds: 47000,
advances: 381200
},
{
month: 'JUL-2025',
@@ -122,7 +175,13 @@ export class AlmReportDialogComponent {
fds: 478600,
cashOnHand: 9280,
investments: 190400,
otherAssets: 13020
otherAssets: 13020,
capital: 24460,
deposits: 619800,
otherLiabilities: 28880,
bankCurrent: 19860,
bankFds: 47400,
advances: 385600
},
{
month: 'AUG-2025',
@@ -131,7 +190,13 @@ export class AlmReportDialogComponent {
fds: 482400,
cashOnHand: 9420,
investments: 192800,
otherAssets: 13280
otherAssets: 13280,
capital: 24540,
deposits: 624200,
otherLiabilities: 29040,
bankCurrent: 20180,
bankFds: 47800,
advances: 390200
},
{
month: 'SEP-2025',
@@ -140,7 +205,13 @@ export class AlmReportDialogComponent {
fds: 488200,
cashOnHand: 9640,
investments: 195200,
otherAssets: 13420
otherAssets: 13420,
capital: 24620,
deposits: 630400,
otherLiabilities: 29200,
bankCurrent: 20460,
bankFds: 48200,
advances: 394800
},
{
month: 'OCT-2025',
@@ -149,7 +220,13 @@ export class AlmReportDialogComponent {
fds: 492600,
cashOnHand: 9820,
investments: 197600,
otherAssets: 13620
otherAssets: 13620,
capital: 24720,
deposits: 635200,
otherLiabilities: 29380,
bankCurrent: 20740,
bankFds: 48600,
advances: 399400
},
{
month: 'NOV-2025',
@@ -158,7 +235,13 @@ export class AlmReportDialogComponent {
fds: 498400,
cashOnHand: 9980,
investments: 200200,
otherAssets: 13820
otherAssets: 13820,
capital: 24800,
deposits: 641000,
otherLiabilities: 29540,
bankCurrent: 21020,
bankFds: 49000,
advances: 404200
},
{
month: 'DEC-2025',
@@ -167,7 +250,13 @@ export class AlmReportDialogComponent {
fds: 504200,
cashOnHand: 10240,
investments: 202800,
otherAssets: 14040
otherAssets: 14040,
capital: 24900,
deposits: 646800,
otherLiabilities: 29720,
bankCurrent: 21340,
bankFds: 49400,
advances: 409000
},
{
month: 'JAN-2026',
@@ -176,7 +265,13 @@ export class AlmReportDialogComponent {
fds: 508800,
cashOnHand: 10420,
investments: 205200,
otherAssets: 14240
otherAssets: 14240,
capital: 24980,
deposits: 651600,
otherLiabilities: 29880,
bankCurrent: 21620,
bankFds: 49800,
advances: 413800
},
{
month: 'FEB-2026',
@@ -185,7 +280,13 @@ export class AlmReportDialogComponent {
fds: 514600,
cashOnHand: 10620,
investments: 207800,
otherAssets: 14460
otherAssets: 14460,
capital: 25060,
deposits: 657400,
otherLiabilities: 30040,
bankCurrent: 21900,
bankFds: 50200,
advances: 418600
},
{
month: 'MAR-2026',
@@ -194,7 +295,13 @@ export class AlmReportDialogComponent {
fds: 520400,
cashOnHand: 10820,
investments: 210400,
otherAssets: 14680
otherAssets: 14680,
capital: 25160,
deposits: 663200,
otherLiabilities: 30220,
bankCurrent: 22240,
bankFds: 50600,
advances: 423600
}
];
@@ -256,6 +363,100 @@ export class AlmReportDialogComponent {
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.
/** "<BANK> , <OFFICE>" — the banner line the printed statement opens with. */
get statementHeading(): string {
return `${this.bank.name.toUpperCase()} , ${this.branchScope.officeName.toUpperCase()}`;
}
/** 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 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) => {
const [
name,
year
] = m.month.split('-');
return {
label: name,
sub: year,
capital: m.capital,
reserveProv: m.reserveProv,
deposits: m.deposits,
borrowings: m.borrowings,
otherLiabilities: m.otherLiabilities,
cashOnHand: m.cashOnHand,
bankCurrent: m.bankCurrent,
bankFds: m.bankFds,
investments: m.investments,
advances: m.advances,
otherAssets: m.otherAssets
};
});
}
private readonly avgBalanceColumns: (keyof AvgBalanceRow)[] = [
'capital',
'reserveProv',
'deposits',
'borrowings',
'otherLiabilities',
'cashOnHand',
'bankCurrent',
'bankFds',
'investments',
'advances',
'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;
}
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) {
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}`
);
}
/** Monthly totals helpers */
monthTotalLiab(m: MonthSnapshot): number {
return m.reserveProv + m.borrowings + m.fds;
@@ -407,6 +608,49 @@ export class AlmReportDialogComponent {
this.saveTsv(rows, `ALM-Monthly-${this.dateTag()}.xls`);
}
/** Average Liabilities and Assets — the printed statement, column for column. */
exportAvgBalancesXls(): void {
const rows: string[][] = [];
rows.push([this.statementHeading]);
rows.push(['Average Liabilities and Assets Month-wise during the year']);
rows.push([]);
rows.push([
'Month',
'Capital',
'Reserve/Prov Exclude OIR',
'Deposits',
'Borrowings',
'Other Liabilities',
'Cash on Hand',
'Bank Balances Current',
'Bank Balances FDs',
'Investments including',
'Advances',
'Other Assets Exclude IRA'
]);
const line = (r: AvgBalanceRow): string[] => [
`${r.label} ${r.sub}`.trim(),
r.capital.toFixed(2),
r.reserveProv.toFixed(2),
r.deposits.toFixed(2),
r.borrowings.toFixed(2),
r.otherLiabilities.toFixed(2),
r.cashOnHand.toFixed(2),
r.bankCurrent.toFixed(2),
r.bankFds.toFixed(2),
r.investments.toFixed(2),
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));
rows.push([]);
rows.push([`Print Date : ${this.statementPrintedAt}`]);
rows.push([`Report Ref No : ${this.bank.reportRefNo} User Code:${this.statementUserCode}`]);
this.saveTsv(rows, `ALM-AverageLiabilitiesAssets-${this.dateTag()}.xls`);
}
/** Maturity ladder CSV */
exportLadderCsv(): void {
const headers = [
@@ -431,7 +675,7 @@ export class AlmReportDialogComponent {
this.saveCsv(rows, `ALM-Ladder-${this.dateTag()}.csv`);
}
setTab(tab: 'dashboard' | 'ladder' | 'monthly' | 'returns'): void {
setTab(tab: 'dashboard' | 'ladder' | 'monthly' | 'avgbal' | 'returns'): void {
this.activeTab = tab;
}
close(): void {
@@ -7,7 +7,16 @@
-->
<h1 mat-dialog-title>{{ 'labels.heading.Session Timeout' | translate }}</h1>
<mat-dialog-content>{{ 'labels.text.Session timed out after a period of inactivity' | translate }}.</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button color="primary" mat-dialog-close>{{ 'labels.buttons.Close' | translate }}</button>
<mat-dialog-content>
<div style="display: flex; flex-direction: column; gap: 8px; padding-top: 8px">
<p>{{ 'labels.text.Session timed out after a period of inactivity' | translate }}.</p>
<p>
For your security, you will be logged out in
<strong style="color: #f44336; font-size: 1.1em">{{ countdown }}</strong> seconds.
</p>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" style="gap: 8px">
<button mat-button color="warn" (click)="logoutNow()">Logout</button>
<button mat-raised-button color="primary" (click)="keepSessionActive()">Stay Logged In</button>
</mat-dialog-actions>
@@ -6,13 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { Component, inject, OnInit, OnDestroy } from '@angular/core';
import {
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
MatDialogClose,
MAT_DIALOG_DATA
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -30,6 +31,43 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatDialogClose
]
})
export class SessionTimeoutDialogComponent {
export class SessionTimeoutDialogComponent implements OnInit, OnDestroy {
dialogRef = inject<MatDialogRef<SessionTimeoutDialogComponent>>(MatDialogRef);
data = inject<{ countdownSeconds?: number }>(MAT_DIALOG_DATA, { optional: true });
countdown = this.data?.countdownSeconds ?? 60;
private intervalId: any;
ngOnInit() {
this.startCountdown();
}
ngOnDestroy() {
this.clearTimer();
}
private startCountdown() {
this.intervalId = setInterval(() => {
this.countdown--;
if (this.countdown <= 0) {
this.clearTimer();
this.dialogRef.close('logout');
}
}, 1000);
}
private clearTimer() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
}
keepSessionActive() {
this.dialogRef.close('keep-alive');
}
logoutNow() {
this.dialogRef.close('logout');
}
}