KYC management: stage-wise notices, pagination, RD base chart
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user