This commit is contained in:
2026-09-23 15:51:10 +05:30
parent cda7c52d11
commit 08a427467a
123 changed files with 20687 additions and 4156 deletions
+51 -59
View File
@@ -7,6 +7,7 @@
*/
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -25,8 +26,7 @@ import {
KycIndividual,
PoFinding,
StrStatus,
StrTransaction,
STR_TXNS
StrTransaction
} from './fiu-data';
/** A FINnet file rendered as a grid — the same rows that go into the CSV. */
@@ -130,6 +130,7 @@ export class FiuComponent implements OnInit {
private amlService = inject(AmlService);
private sanctionsLogsService = inject(SanctionsLogsService);
private authenticationService = inject(AuthenticationService);
private route = inject(ActivatedRoute);
/** Active tab */
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
@@ -154,7 +155,16 @@ export class FiuComponent implements OnInit {
* is honestly empty rather than populated with rows that cannot be filed.
*/
tc1Txns: CardTransaction[] = [];
strTxns: StrTransaction[] = STR_TXNS;
/**
* The STR transaction file.
*
* Empty, for the same reason TC1 and the customer cards are. The four rows that sat here named customers this bank
* has never onboarded, against account numbers it does not hold, for sums it never received — on the screen an
* officer files suspicious-transaction reports from. A filing built on invented rows is not a draft; it is a false
* report to FIU-IND. The live alerts the engine has actually escalated are listed on this tab already, and the
* file fills from those when the STR export is wired to them.
*/
strTxns: StrTransaction[] = [];
/**
* KC1 / KC2 — the customer cards that travel with the CTR bundle.
*
@@ -210,13 +220,29 @@ export class FiuComponent implements OnInit {
*/
sanctionsStrCases: any[] = [];
sanctionsStrFilter = '';
amlAlertStatusFilter = '';
amlReviewNotes: Record<number, string> = {};
amlCtrReports: any[] = [];
ctrGenerateMonth = '';
amlStrCases: any[] = [];
/**
* The cases still carrying work, which is what "Live" means here.
*
* <p>
* A case that has been filed with FIU-IND is finished: the report is gone, the reference number is on it, and there
* is nothing left for the Principal Officer to do. Leaving it in the live queue is how a working list stops being
* one — every filed case stays on it for good, and an officer opening the tab has to tell the outstanding work from
* the closed by reading a status column. Filed cases are listed below, where they can still be looked up.
*/
get liveStrCases(): any[] {
return this.amlStrCases.filter((c) => c?.status !== 'FILED');
}
/** Cases already filed with FIU-IND, kept for the record rather than as work. */
get filedStrCases(): any[] {
return this.amlStrCases.filter((c) => c?.status === 'FILED');
}
selectedStrCase: any = null;
concludeConclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS' = 'NOT_SUSPICIOUS';
concludeInvestigationNotes = '';
@@ -398,6 +424,14 @@ export class FiuComponent implements OnInit {
this.isHeadOffice = this.authenticationService.isHeadOfficeUser();
this.currentOfficeName = creds?.officeName || 'Branch Office';
// Opened on a particular tab when another screen sends the officer here for a reason - the Risk
// Review Report links a case that is with the Principal Officer straight to the STR queue, so
// landing on the dashboard would make them find it themselves.
const wantedTab = this.route.snapshot.queryParamMap.get('tab');
if (wantedTab === 'str' || wantedTab === 'ctr' || wantedTab === 'customers' || wantedTab === 'files') {
this.activeTab = wantedTab;
}
// The header named a hard-coded officer and a hard-coded month — "Karthik Kamath" and
// "January 2026" — on a screen whose whole purpose is to say which month is being filed and
// by whom. Both now come from the session and the clock.
@@ -615,10 +649,9 @@ export class FiuComponent implements OnInit {
}
/**
* Always fetches every status. The triage table and the cleared register are two views of the
* same set, so filtering server-side would empty one whenever the other was narrowed — and the
* CTR tab reads this list too, which a status filter chosen on the STR tab should not touch.
* amlAlertStatusFilter is applied client-side in {@link amlSuspiciousAlerts} instead.
* Always fetches every status. This tab shows what was cleared and what became an STR case, which are two views of
* the same set, and the CTR tab reads the list as well — so narrowing it server-side for one view would empty
* another.
*/
loadAmlAlerts(): void {
this.amlService.getAlerts().subscribe((data) => (this.amlAlerts = data || []));
@@ -762,48 +795,6 @@ export class FiuComponent implements OnInit {
return (err: any) => this.toast(err?.error?.defaultUserMessage || `${action} failed — please retry`);
}
/**
* Clearing or escalating an alert has to carry a reason: review_notes is the only audit record of
* why a flagged transaction was disposed of, and an empty string leaves no trace of the decision.
*/
private reviewNotesOf(alert: any): string | null {
const notes = (this.amlReviewNotes[alert.id] || '').trim();
if (!notes) {
this.toast(`Enter review notes before disposing of alert #${alert.id}`);
return null;
}
return notes;
}
whitelistAlert(alert: any): void {
const notes = this.reviewNotesOf(alert);
if (!notes) {
return;
}
this.amlService.reviewAlert(alert.id, 'WHITELIST', notes).subscribe({
next: () => {
this.toast(`Alert #${alert.id} whitelisted — no STR raised`);
this.loadAmlAlerts();
},
error: this.apiError(`Whitelisting alert #${alert.id}`)
});
}
escalateAlert(alert: any): void {
const notes = this.reviewNotesOf(alert);
if (!notes) {
return;
}
this.amlService.reviewAlert(alert.id, 'ESCALATE', notes).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}`)
});
}
generateCtrReport(): void {
if (!this.ctrGenerateMonth) {
this.toast('Pick a reporting month first');
@@ -991,9 +982,16 @@ export class FiuComponent implements OnInit {
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. */
/**
* Still needs PO action: anything neither filed with FIU-IND nor concluded clear.
*
* Counted by what is unfinished rather than by naming the statuses that are. Adding up OPEN and CONCLUDED_SUSPICIOUS
* gives the same figure today, but it is the shape that fails quietly: a case in any status those two do not name
* drops out of the KPI while still sitting in the queue below it, and the dashboard then under-reports the work.
* That is exactly how five undecided alerts hid behind a pending count of 1 on the flagged-transactions screen.
*/
get liveStrPending(): number {
return this.liveStrOpen + this.liveStrAwaitingFiling;
return this.amlStrCases.filter((c) => c?.status !== 'FILED' && c?.status !== 'CONCLUDED_CLEAR').length;
}
get totalCustomers(): number {
return this.kc1.length + this.kc2.length;
@@ -1022,12 +1020,6 @@ export class FiuComponent implements OnInit {
* The STR triage queue: still to decide, or already escalated. Whitelisted alerts are closed
* with no report, so they are not STR business — they live in {@link amlWhitelistedAlerts}.
*/
get amlSuspiciousAlerts(): any[] {
return this.behaviouralAlerts.filter(
(a) => a.status !== 'WHITELISTED' && (!this.amlAlertStatusFilter || a.status === this.amlAlertStatusFilter)
);
}
/** Cleared register: alerts closed as false positives, kept for audit with their evidence. */
get amlWhitelistedAlerts(): any[] {
return this.behaviouralAlerts.filter((a) => a.status === 'WHITELISTED');