KYC management: stage-wise notices, pagination, RD base chart

This commit is contained in:
2026-10-20 17:23:42 +05:30
parent 3e5cc26e49
commit 357f666421
108 changed files with 15001 additions and 2721 deletions
+2
View File
@@ -54,6 +54,8 @@ export interface CtrTransaction {
declaration: string;
/** Customer name resolved from KC1 (UI convenience, not in actual TC2 schema) */
customerName?: string;
/** True for a transaction that has crossed the threshold but is not yet in a generated CTR. */
pendingFiling?: boolean;
/**
* CTR does not judge suspicion. It records only whether the source of funds was
* evidenced at the counter (CTR filed, nothing further) or could not be, in which
+232 -140
View File
@@ -223,6 +223,15 @@
<div class="fiu-note">Loading the generated CTR lines…</div>
} @else if (ctrLinesError) {
<div class="fiu-note fiu-note--warn"><fa-icon icon="exclamation-triangle"></fa-icon> {{ ctrLinesError }}</div>
} @else if (ctrPendingCount > 0) {
<div class="fiu-note">
<fa-icon icon="info-circle"></fa-icon>
{{ ctrPendingCount }} cash transaction{{ ctrPendingCount > 1 ? 's' : '' }} of ₹10,00,000 or more
{{ ctrPendingCount > 1 ? 'are' : 'is' }} listed below automatically as soon as
{{ ctrPendingCount > 1 ? 'they are' : 'it is' }} posted.
{{ ctrPendingCount > 1 ? 'They are' : 'It is' }} not in a filed report yet — pick the month under “Generate
CTR” and run it to include {{ ctrPendingCount > 1 ? 'them' : 'it' }}.
</div>
} @else if (!ctrTxns.length) {
<div class="fiu-note fiu-note--warn">
<fa-icon icon="exclamation-triangle"></fa-icon>
@@ -342,27 +351,103 @@
<th class="ta-c">Records</th>
<th>Status</th>
<th>Submitted On</th>
<th class="ta-c">Customers</th>
</tr>
</thead>
<tbody>
@for (r of amlCtrReports; track r.id) {
<tr>
<tr style="cursor: pointer" (click)="openCtrReport(r)">
<td>{{ r.reportMonth }}</td>
<td>{{ r.generatedOn | date: 'medium' }}</td>
<td>{{ r.generatedOn ? (asDate(r.generatedOn) | date: 'medium') : '—' }}</td>
<td class="ta-c">{{ r.recordCount }}</td>
<td>
<span [class]="amlStatusClass(r.status)">{{ r.status }}</span>
</td>
<td>{{ r.submittedOn ? (r.submittedOn | date: 'medium') : '—' }}</td>
<td>{{ r.submittedOn ? (asDate(r.submittedOn) | date: 'medium') : '—' }}</td>
<td class="ta-c" (click)="$event.stopPropagation()">
<button class="fiu-btn fiu-btn-g" (click)="openCtrReport(r)">View</button>
<button class="fiu-btn fiu-btn-g" (click)="exportCtrReport(r)">Excel</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="fiu-empty">No CTR reports generated yet.</td>
<td colspan="6" class="fiu-empty">No CTR reports generated yet.</td>
</tr>
}
</tbody>
</table>
</div>
<!-- The customers and transactions one CTR report names. Opened from a row above; declared here so it keeps this
component's state and styles. -->
<ng-template #ctrReportDialog>
<div class="fiu-dialog">
@if (ctrReportView; as report) {
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>CTR report — {{ report.reportMonth }}</h3>
<p>
{{ report.status }} · {{ ctrReportLines.length }} record{{ ctrReportLines.length === 1 ? '' : 's' }} ·
total ₹{{ ctrReportTotal | number: '1.2-2' }}
</p>
</div>
<div class="fiu-action-group">
<button
class="fiu-btn fiu-btn-xml"
[disabled]="ctrReportLinesLoading"
(click)="exportCtrReport(report)"
>
Export Excel
</button>
<button class="fiu-btn fiu-btn-g" (click)="closeCtrReport()">Close</button>
</div>
</div>
@if (ctrReportLinesLoading) {
<div class="fiu-note">Loading the customers in this report…</div>
} @else {
<div class="fiu-tbl-wrap">
<table class="fiu-tbl">
<thead>
<tr>
<th>Customer</th>
<th>Customer ID / CKYC</th>
<th>Account</th>
<th>Branch</th>
<th>Date</th>
<th>Direction</th>
<th>Type</th>
<th class="ta-r">Amount</th>
</tr>
</thead>
<tbody>
@for (l of ctrReportLines; track $index) {
<tr>
<td>
{{ l.clientName }}
<div style="font-size: 11px; color: var(--f-g500)">
{{ l.individual === false ? 'Entity' : 'Individual' }}
</div>
</td>
<td class="mono">{{ l.ckycNumber || l.clientExternalId || '—' }}</td>
<td class="mono">{{ l.accountNo || '—' }}</td>
<td>{{ l.branchName || '—' }}</td>
<td>{{ l.transactionDate ? (asDate(l.transactionDate) | date: 'dd MMM yyyy') : '—' }}</td>
<td>{{ l.direction || '—' }}</td>
<td>{{ l.connectedGroup ? 'Connected group (month total)' : 'Single transaction' }}</td>
<td class="ta-r">₹{{ l.amount | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="fiu-empty">This report names no customers.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</div>
</ng-template>
</div>
}
@@ -581,146 +666,153 @@
</div>
}
@if (selectedStrCase) {
<div class="str-case-detail">
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>STR Case #{{ selectedStrCase.id }} — {{ selectedStrCase.clientDisplayName }}</h3>
<p>
Alert #{{ selectedStrCase.alertId }} &middot;
{{ selectedStrCase.cumulativeStrCaseCountForClient }} cumulative STR case(s) for this client
@if (selectedStrCase.cumulativeStrCaseCountForClient >= 3) {
<span class="escalation-badge">Risk category auto-escalated</span>
<!-- The case investigation. Opened as a popup by selectStrCase(); declared here so it keeps this component's state. -->
<ng-template #strCaseDialog>
<div class="fiu-dialog">
@if (selectedStrCase) {
<div class="str-case-detail">
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>STR Case #{{ selectedStrCase.id }} — {{ selectedStrCase.clientDisplayName }}</h3>
<p>
Alert #{{ selectedStrCase.alertId }} &middot;
{{ selectedStrCase.cumulativeStrCaseCountForClient }} cumulative STR case(s) for this client
@if (selectedStrCase.cumulativeStrCaseCountForClient >= 3) {
<span class="escalation-badge">Risk category auto-escalated</span>
}
</p>
</div>
<button class="fiu-btn fiu-btn-g" (click)="closeStrCaseDetail()">Close</button>
</div>
<div
class="case-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
>
<textarea id="fiuInvestigationNotes" rows="3" [(ngModel)]="concludeInvestigationNotes"></textarea>
</div>
<div class="form-row">
<label for="fiuSourceOfFundsNotes">Source-of-funds notes</label>
<textarea id="fiuSourceOfFundsNotes" rows="2" [(ngModel)]="concludeSourceOfFundsNotes"></textarea>
</div>
<!-- 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>
<!-- 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>
@if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') {
<div class="file-str-row">
<label for="fiuFileReferenceNo">FIU-IND acknowledgement / reference number</label>
<input id="fiuFileReferenceNo" class="fiu-inp" type="text" [(ngModel)]="fileReferenceNo" />
<button class="fiu-btn fiu-btn-xml" (click)="fileStrCase()">Mark STR Filed</button>
</div>
}
</p>
</div>
<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>
<textarea id="fiuInvestigationNotes" rows="3" [(ngModel)]="concludeInvestigationNotes"></textarea>
</div>
<div class="form-row">
<label for="fiuSourceOfFundsNotes">Source-of-funds notes</label>
<textarea id="fiuSourceOfFundsNotes" rows="2" [(ngModel)]="concludeSourceOfFundsNotes"></textarea>
</div>
<!-- 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>
<!-- 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>
@if (selectedStrCase.status === 'CONCLUDED_SUSPICIOUS') {
<div class="file-str-row">
<label for="fiuFileReferenceNo">FIU-IND acknowledgement / reference number</label>
<input id="fiuFileReferenceNo" class="fiu-inp" type="text" [(ngModel)]="fileReferenceNo" />
<button class="fiu-btn fiu-btn-xml" (click)="fileStrCase()">Mark STR Filed</button>
</div>
}
</div>
}
</div>
}
</ng-template>
<!-- FINnet export template (separate concern — STR XML file-format generation) -->
<div class="fiu-form-meta">
+7 -1
View File
@@ -6,7 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
:host {
:host,
.fiu-dialog {
--f-navy: #0b1e3d;
--f-blue: #1d4ed8;
--f-blue-l: #eff6ff;
@@ -1858,3 +1859,8 @@
white-space: normal;
font-size: 12.5px;
}
/* The STR case popup renders in the overlay, outside :host, so it carries the page's colour variables itself. */
.fiu-dialog {
padding: 4px;
}
+174 -6
View File
@@ -6,7 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { Component, OnInit, inject, ViewChild, TemplateRef } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { forkJoin, of } from 'rxjs';
import { exportWorkbook } from 'app/cashier/cashier-export';
import { catchError } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -131,6 +136,134 @@ export class FiuComponent implements OnInit {
private sanctionsLogsService = inject(SanctionsLogsService);
private authenticationService = inject(AuthenticationService);
private route = inject(ActivatedRoute);
private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar);
/** The STR case investigation, shown in a popup rather than a panel pushed in under the case list. */
@ViewChild('strCaseDialog') strCaseDialogTpl!: TemplateRef<unknown>;
private strCaseDialogRef: MatDialogRef<unknown> | null = null;
/** The CTR report whose customers are open in the popup, and the lines behind it. */
@ViewChild('ctrReportDialog') ctrReportDialogTpl!: TemplateRef<unknown>;
private ctrReportDialogRef: MatDialogRef<unknown> | null = null;
ctrReportView: any = null;
ctrReportLines: any[] = [];
ctrReportLinesLoading = false;
/** Opens a generated CTR report with every customer and transaction it names. */
openCtrReport(report: any): void {
this.ctrReportView = report;
this.ctrReportLines = [];
this.ctrReportLinesLoading = true;
this.amlService.getCtrReportLines(report.id).subscribe({
next: (lines) => {
this.ctrReportLines = lines || [];
this.ctrReportLinesLoading = false;
},
error: () => {
this.ctrReportLinesLoading = false;
this.toast(`Could not load the customers in the ${report.reportMonth} report`);
}
});
this.ctrReportDialogRef?.close();
this.ctrReportDialogRef = this.dialog.open(this.ctrReportDialogTpl, {
width: '1280px',
maxWidth: '96vw',
maxHeight: '92vh',
autoFocus: false
});
this.ctrReportDialogRef.afterClosed().subscribe(() => {
this.ctrReportView = null;
this.ctrReportDialogRef = null;
});
}
closeCtrReport(): void {
this.ctrReportDialogRef?.close();
}
/** Total of the amounts in the open report, so the popup states what the filing adds up to. */
get ctrReportTotal(): number {
return this.ctrReportLines.reduce((sum, l) => sum + (Number(l.amount) || 0), 0);
}
/**
* Excel workbook of a CTR report with the customer behind every line. Fetches the lines itself unless the popup
* already holds them, so it works straight from the reports table as well as from inside the popup.
*/
exportCtrReport(report: any): void {
const build = (lines: any[]) => this.buildCtrReportWorkbook(report, lines);
if (this.ctrReportView?.id === report.id && !this.ctrReportLinesLoading) {
build(this.ctrReportLines);
return;
}
this.amlService.getCtrReportLines(report.id).subscribe({
next: (lines) => build(lines || []),
error: () => this.toast(`Could not load the ${report.reportMonth} report to export it`)
});
}
private async buildCtrReportWorkbook(report: any, lines: any[]): Promise<void> {
const total = lines.reduce((sum, l) => sum + (Number(l.amount) || 0), 0);
await exportWorkbook(
[
{
sheetName: `CTR ${report.reportMonth}`,
title: `Cash Transaction Report — ${report.reportMonth}`,
subtitle: `${report.status} · ${lines.length} record${lines.length === 1 ? '' : 's'} · total ₹${total.toLocaleString('en-IN')} · generated ${new Date().toLocaleString('en-IN')}`,
headers: [
'Report Month',
'Customer',
'Customer ID / CKYC Ref',
'CKYC Number',
'Customer Type',
'Account No',
'Branch',
'Account Status',
'Account Opened',
'Transaction ID',
'Transaction Date',
'Direction',
'Line Type',
'Amount (₹)'
],
rows: lines.map((l) => [
l.reportMonth || report.reportMonth,
l.clientName || '',
l.clientExternalId || '',
l.ckycNumber || '',
l.individual === false ? 'Entity' : 'Individual',
l.accountNo || '',
l.branchName || '',
l.accountStatus || '',
toIsoDate(l.accountOpenedOn) || '',
l.transactionId ?? null,
toIsoDate(l.transactionDate) || '',
l.direction || '',
l.connectedGroup ? 'Connected group — month total' : 'Single transaction',
Number(l.amount) || 0
]),
widths: [
12,
28,
22,
18,
14,
20,
18,
16,
16,
14,
16,
12,
28,
18
]
}
],
`CTR-${report.reportMonth}`
);
}
/** Active tab */
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
@@ -145,6 +278,8 @@ export class FiuComponent implements OnInit {
*/
ctrTxns: CtrTransaction[] = [];
ctrLinesLoading = false;
/** Reportable cash transactions already posted but not yet in a generated CTR, listed automatically. */
ctrPendingCount = 0;
ctrLinesError = '';
/**
* ATM / card-rail cash, which feeds the same CTR as the counter transactions in TC2.
@@ -682,10 +817,23 @@ export class FiuComponent implements OnInit {
loadCtrLines(): void {
this.ctrLinesLoading = true;
this.ctrLinesError = '';
this.amlService.getCtrReportLines().subscribe({
next: (lines) => {
// The pending list is an extra: if it cannot be read, the generated lines still show.
forkJoin({
generated: this.amlService.getCtrReportLines(),
pending: this.amlService.getCtrPendingLines().pipe(catchError(() => of([] as any[])))
}).subscribe({
next: ({ generated, pending }) => {
this.ctrLinesLoading = false;
this.ctrTxns = (lines || []).map((l: any, i: number) => this.toCtrTransaction(l, i + 1));
const generatedRows = (generated || []).map((l: any, i: number) => this.toCtrTransaction(l, i + 1));
const pendingRows = (pending || []).map((l: any, i: number) => ({
...this.toCtrTransaction(l, generatedRows.length + i + 1),
pendingFiling: true
}));
this.ctrPendingCount = pendingRows.length;
this.ctrTxns = [
...generatedRows,
...pendingRows
];
// Cached so the compliance flags an officer sets below survive a reload, keyed on the rows
// that actually came back rather than on a seeded set.
this.applyStoredCtrCompliance();
@@ -693,6 +841,7 @@ export class FiuComponent implements OnInit {
error: (err: any) => {
this.ctrLinesLoading = false;
this.ctrTxns = [];
this.ctrPendingCount = 0;
this.ctrLinesError =
err?.error?.defaultUserMessage ||
'Could not load the generated CTR lines. Generate a report for the month first.';
@@ -820,6 +969,17 @@ export class FiuComponent implements OnInit {
this.concludeSourceOfFundsNotes = strCase.sourceOfFundsNotes || '';
this.fileReferenceNo = strCase.strReferenceNo || '';
this.resetChecklist();
this.strCaseDialogRef?.close();
this.strCaseDialogRef = this.dialog.open(this.strCaseDialogTpl, {
width: '1100px',
maxWidth: '96vw',
maxHeight: '92vh',
autoFocus: false
});
this.strCaseDialogRef.afterClosed().subscribe(() => {
this.selectedStrCase = null;
this.strCaseDialogRef = null;
});
}
/** Plain-language outcome for a live STR case, shown in the investigation panel. */
@@ -837,6 +997,7 @@ export class FiuComponent implements OnInit {
}
closeStrCaseDetail(): void {
this.strCaseDialogRef?.close();
this.selectedStrCase = null;
}
@@ -871,8 +1032,9 @@ export class FiuComponent implements OnInit {
? `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();
// Concluded: close the popup so the case's new status is what the officer sees in the list.
this.closeStrCaseDetail();
},
error: this.apiError(`Concluding STR case #${this.selectedStrCase.id}`)
});
@@ -889,8 +1051,8 @@ export class FiuComponent implements OnInit {
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();
this.closeStrCaseDetail();
},
error: this.apiError(`Filing STR case #${this.selectedStrCase.id}`)
});
@@ -1784,6 +1946,12 @@ export class FiuComponent implements OnInit {
}
toast(msg: string): void {
// The page's own toast sits behind the popup's backdrop, so while the STR case popup is open the message would be
// there but unseen - which read as the action having done nothing. A snackbar is drawn above the popup.
if (this.strCaseDialogRef || this.ctrReportDialogRef) {
this.snackBar.open(msg, 'OK', { duration: 5000 });
return;
}
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) clearTimeout(this.toastTimer);