oss file updated, profitl loss as per the day month and also with date select
This commit is contained in:
+344
-30
@@ -20,6 +20,7 @@ import {
|
||||
KC2_ENTITIES,
|
||||
KycEntity,
|
||||
KycIndividual,
|
||||
PoFinding,
|
||||
StrStatus,
|
||||
StrTransaction,
|
||||
STR_TXNS
|
||||
@@ -36,6 +37,9 @@ import {
|
||||
]
|
||||
})
|
||||
export class FiuComponent implements OnInit {
|
||||
/** Bump when CTR_TXNS / STR_TXNS change so cached rows are re-seeded. */
|
||||
private static readonly SEED_VERSION = '2026-01-cash-threshold';
|
||||
|
||||
private amlService = inject(AmlService);
|
||||
private authenticationService = inject(AuthenticationService);
|
||||
/** Active tab */
|
||||
@@ -56,6 +60,9 @@ export class FiuComponent implements OnInit {
|
||||
strFilterStatus: '' | StrStatus = '';
|
||||
strFilterRisk: string = '';
|
||||
|
||||
/** Ref of the STR card to highlight after jumping across from the CTR tab */
|
||||
highlightedStrRef: number | null = null;
|
||||
|
||||
/** Toast */
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
@@ -76,6 +83,89 @@ export class FiuComponent implements OnInit {
|
||||
concludeSourceOfFundsNotes = '';
|
||||
fileReferenceNo = '';
|
||||
|
||||
/**
|
||||
* The Principal Officer's verification checklist. Each point is answered
|
||||
* "Genuine" or "Not satisfied"; the case conclusion is derived from the answers
|
||||
* rather than picked by hand, so a case with an unresolved finding cannot be
|
||||
* whitelisted by accident.
|
||||
*/
|
||||
readonly strChecklist: { key: string; label: string; hint: string }[] = [
|
||||
{
|
||||
key: 'sourceOfFunds',
|
||||
label: 'Source of funds established and documented',
|
||||
hint: 'Salary slips, sale deed, business receipts or other credible evidence on file'
|
||||
},
|
||||
{
|
||||
key: 'profileConsistent',
|
||||
label: 'Transaction consistent with the customer’s declared profile',
|
||||
hint: 'Value and frequency in line with declared income, occupation and turnover'
|
||||
},
|
||||
{
|
||||
key: 'kycCurrent',
|
||||
label: 'KYC records complete and within re-KYC validity',
|
||||
hint: 'Identity, address and beneficial-ownership records verified and current'
|
||||
},
|
||||
{
|
||||
key: 'screeningClear',
|
||||
label: 'Sanctions, PEP and adverse-media screening clear',
|
||||
hint: 'No UNSCC / MHA listing, no PEP linkage, no negative news on the customer'
|
||||
},
|
||||
{
|
||||
key: 'patternConsistent',
|
||||
label: 'Pattern consistent with the account’s own history',
|
||||
hint: 'No structuring, sudden dormancy revival or rapid pass-through observed'
|
||||
},
|
||||
{
|
||||
key: 'counterpartyKnown',
|
||||
label: 'Counterparty identifiable and legitimate',
|
||||
hint: 'Remitter / beneficiary traceable and holding a plausible commercial relationship'
|
||||
},
|
||||
{
|
||||
key: 'documentsGenuine',
|
||||
label: 'Supporting documents verified as genuine',
|
||||
hint: 'Invoices, contracts and bank statements independently corroborated'
|
||||
},
|
||||
{
|
||||
key: 'noLawEnforcement',
|
||||
label: 'No law-enforcement or regulatory reference pending',
|
||||
hint: 'No summons, freeze order or LEA request outstanding against the account'
|
||||
}
|
||||
];
|
||||
|
||||
/** null = not yet answered, true = genuine, false = not satisfied. */
|
||||
strFindings: Record<string, boolean | null> = {};
|
||||
|
||||
private resetChecklist(): void {
|
||||
this.strFindings = {};
|
||||
for (const item of this.strChecklist) {
|
||||
this.strFindings[item.key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
setFinding(key: string, genuine: boolean): void {
|
||||
this.strFindings[key] = genuine;
|
||||
this.concludeConclusion = this.derivedConclusion;
|
||||
}
|
||||
|
||||
get answeredCount(): number {
|
||||
return this.strChecklist.filter((i) => this.strFindings[i.key] !== null).length;
|
||||
}
|
||||
get allAnswered(): boolean {
|
||||
return this.answeredCount === this.strChecklist.length;
|
||||
}
|
||||
/** Points the officer marked as not satisfied — these are what force an STR. */
|
||||
get failedFindings(): { key: string; label: string; hint: string }[] {
|
||||
return this.strChecklist.filter((i) => this.strFindings[i.key] === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything genuine means the case is cleared; a single unsatisfied point
|
||||
* means it must be filed as suspicious.
|
||||
*/
|
||||
get derivedConclusion(): 'NOT_SUSPICIOUS' | 'SUSPICIOUS' {
|
||||
return this.failedFindings.length > 0 ? 'SUSPICIOUS' : 'NOT_SUSPICIOUS';
|
||||
}
|
||||
|
||||
get isPrincipalOfficer(): boolean {
|
||||
return this.authenticationService.isPrincipalOfficerUser();
|
||||
}
|
||||
@@ -88,6 +178,84 @@ export class FiuComponent implements OnInit {
|
||||
this.loadAmlAlerts();
|
||||
this.loadAmlCtrReports();
|
||||
this.loadAmlStrCases();
|
||||
|
||||
// Bumping SEED_VERSION discards previously cached rows so a new data set takes effect.
|
||||
if (localStorage.getItem('fiu_seed_version') !== FiuComponent.SEED_VERSION) {
|
||||
localStorage.setItem('fiu_seed_version', FiuComponent.SEED_VERSION);
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
return;
|
||||
}
|
||||
|
||||
const storedCtr = localStorage.getItem('fiu_ctr_txns');
|
||||
if (storedCtr) {
|
||||
this.ctrTxns = JSON.parse(storedCtr);
|
||||
} else {
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
}
|
||||
|
||||
const storedStr = localStorage.getItem('fiu_str_cases');
|
||||
if (storedStr) {
|
||||
this.strTxns = JSON.parse(storedStr);
|
||||
} else {
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
}
|
||||
}
|
||||
|
||||
whitelistCtrTxn(txn: CtrTransaction): void {
|
||||
txn.complianceStatus = 'WHITELISTED';
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
this.toast(`Transaction for ${txn.customerName} whitelisted — stays in the CTR report only`);
|
||||
}
|
||||
|
||||
flagCtrTxnSuspicious(txn: CtrTransaction): void {
|
||||
txn.complianceStatus = 'SUSPICIOUS';
|
||||
localStorage.setItem('fiu_ctr_txns', JSON.stringify(this.ctrTxns));
|
||||
|
||||
const newRef = Math.max(100, ...this.strTxns.map((s) => s.reportRefNo)) + 1;
|
||||
const newStrCase: StrTransaction = {
|
||||
...txn,
|
||||
reportType: 'STR',
|
||||
reportRefNo: newRef,
|
||||
detectedOn: new Date().toLocaleDateString('en-GB').replace(/\//g, '-'),
|
||||
flaggedBy: 'AML Officer',
|
||||
riskCategory: 'unusual-pattern',
|
||||
narrative:
|
||||
`Cash ${txn.depositOrWithdrawal.toLowerCase()} of ₹${txn.txnAmount.toLocaleString('en-IN')} breached the ` +
|
||||
`₹${this.bank.cashThreshold.toLocaleString('en-IN')} CTR limit and was flagged as suspicious by the AML Officer. ` +
|
||||
`Referred to the Principal Officer to establish whether the source of funds is legitimate or linked to terror financing.`,
|
||||
status: 'po-review',
|
||||
poFinding: undefined
|
||||
};
|
||||
|
||||
this.strTxns.unshift(newStrCase);
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
this.toast(`STR-${newRef} opened for ${txn.customerName} — awaiting Principal Officer investigation`);
|
||||
}
|
||||
|
||||
/** Principal Officer closes an STR case as legitimate, or confirms it and files an STR. */
|
||||
concludeLocalStr(rec: StrTransaction, finding: PoFinding): void {
|
||||
rec.poFinding = finding;
|
||||
if (finding === 'CLEAN') {
|
||||
rec.status = 'rejected';
|
||||
rec.poRemarks =
|
||||
'Source-of-funds documentation reviewed and verified by the Principal Officer. No link to terror financing or ' +
|
||||
'any other predicate offence. Case closed — no STR filed with FIU-IND.';
|
||||
this.toast(`STR-${rec.reportRefNo} closed as clean — no STR filed`);
|
||||
} else {
|
||||
rec.status = 'filed';
|
||||
rec.poRemarks =
|
||||
'Principal Officer investigation could not establish a legitimate source of funds. STR filed with FIU-IND under ' +
|
||||
'PMLA s.12 within the 7-day window; account placed under enhanced monitoring and reported to the Nodal Officer.';
|
||||
this.toast(`STR-${rec.reportRefNo} confirmed suspicious — filed with FIU-IND`);
|
||||
}
|
||||
localStorage.setItem('fiu_str_cases', JSON.stringify(this.strTxns));
|
||||
}
|
||||
|
||||
poFindingLabel(finding: PoFinding): string {
|
||||
return finding === 'CLEAN'
|
||||
? 'Clean — source of funds verified as legitimate. No STR filed.'
|
||||
: 'Suspicious — STR filed with FIU-IND and account escalated.';
|
||||
}
|
||||
|
||||
loadAmlAlerts(): void {
|
||||
@@ -104,23 +272,35 @@ export class FiuComponent implements OnInit {
|
||||
this.amlService.getStrCases().subscribe((data) => (this.amlStrCases = data || []));
|
||||
}
|
||||
|
||||
/** Surfaces a failed backend call — without this a rejected request looks like a dead button. */
|
||||
private apiError(action: string) {
|
||||
return (err: any) => this.toast(err?.error?.defaultUserMessage || `${action} failed — please retry`);
|
||||
}
|
||||
|
||||
whitelistAlert(alert: any): void {
|
||||
this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe(() => {
|
||||
this.toast(`Alert #${alert.id} whitelisted`);
|
||||
this.loadAmlAlerts();
|
||||
this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe({
|
||||
next: () => {
|
||||
this.toast(`Alert #${alert.id} whitelisted — no STR raised`);
|
||||
this.loadAmlAlerts();
|
||||
},
|
||||
error: this.apiError(`Whitelisting alert #${alert.id}`)
|
||||
});
|
||||
}
|
||||
|
||||
escalateAlert(alert: any): void {
|
||||
this.amlService.reviewAlert(alert.id, 'ESCALATE', this.amlReviewNotes[alert.id] || '').subscribe(() => {
|
||||
this.toast(`Alert #${alert.id} escalated to Principal Officer`);
|
||||
this.loadAmlAlerts();
|
||||
this.loadAmlStrCases();
|
||||
this.amlService.reviewAlert(alert.id, 'ESCALATE', this.amlReviewNotes[alert.id] || '').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');
|
||||
return;
|
||||
}
|
||||
this.amlService.generateCtrReport(this.ctrGenerateMonth).subscribe({
|
||||
@@ -128,16 +308,32 @@ export class FiuComponent implements OnInit {
|
||||
this.toast(`CTR report generated for ${this.ctrGenerateMonth}`);
|
||||
this.loadAmlCtrReports();
|
||||
},
|
||||
error: (err) => this.toast(err?.error?.defaultUserMessage || 'CTR report generation failed')
|
||||
error: this.apiError('CTR report generation')
|
||||
});
|
||||
}
|
||||
|
||||
selectStrCase(strCase: any): void {
|
||||
this.selectedStrCase = strCase;
|
||||
this.concludeConclusion = 'NOT_SUSPICIOUS';
|
||||
// Preserve an existing verdict instead of silently resetting it to NOT_SUSPICIOUS.
|
||||
this.concludeConclusion = strCase.conclusion === 'SUSPICIOUS' ? 'SUSPICIOUS' : 'NOT_SUSPICIOUS';
|
||||
this.concludeInvestigationNotes = strCase.investigationNotes || '';
|
||||
this.concludeSourceOfFundsNotes = strCase.sourceOfFundsNotes || '';
|
||||
this.fileReferenceNo = '';
|
||||
this.fileReferenceNo = strCase.strReferenceNo || '';
|
||||
this.resetChecklist();
|
||||
}
|
||||
|
||||
/** Plain-language outcome for a live STR case, shown in the investigation panel. */
|
||||
strCaseOutcome(strCase: any): string {
|
||||
switch (strCase?.status) {
|
||||
case 'CONCLUDED_CLEAR':
|
||||
return 'Concluded not suspicious — case closed, nothing filed with FIU-IND.';
|
||||
case 'CONCLUDED_SUSPICIOUS':
|
||||
return `Concluded suspicious — STR must be filed with FIU-IND by ${strCase.fiuDueDate || 'the due date'}.`;
|
||||
case 'FILED':
|
||||
return `STR filed with FIU-IND${strCase.strReferenceNo ? ` under reference ${strCase.strReferenceNo}` : ''}.`;
|
||||
default:
|
||||
return 'Open — awaiting the Principal Officer’s conclusion.';
|
||||
}
|
||||
}
|
||||
|
||||
closeStrCaseDetail(): void {
|
||||
@@ -148,32 +344,60 @@ export class FiuComponent implements OnInit {
|
||||
if (!this.selectedStrCase) {
|
||||
return;
|
||||
}
|
||||
if (!this.allAnswered) {
|
||||
const pending = this.strChecklist.length - this.answeredCount;
|
||||
this.toast(`Answer all verification points first — ${pending} still pending`);
|
||||
return;
|
||||
}
|
||||
if (!this.concludeInvestigationNotes.trim()) {
|
||||
this.toast('Record your investigation notes before concluding');
|
||||
return;
|
||||
}
|
||||
// The verdict always follows the checklist, never a stale radio selection.
|
||||
this.concludeConclusion = this.derivedConclusion;
|
||||
// Fold the unsatisfied points into the notes so the STR carries its own reasons.
|
||||
const failed = this.failedFindings;
|
||||
const notes = failed.length
|
||||
? `${this.concludeInvestigationNotes.trim()}\n\nUnsatisfied verification points:\n` +
|
||||
failed.map((f) => `• ${f.label}`).join('\n')
|
||||
: `${this.concludeInvestigationNotes.trim()}\n\nAll ${this.strChecklist.length} verification points found genuine.`;
|
||||
|
||||
this.amlService
|
||||
.concludeStrCase(
|
||||
this.selectedStrCase.id,
|
||||
this.concludeConclusion,
|
||||
this.concludeInvestigationNotes,
|
||||
notes,
|
||||
this.concludeSourceOfFundsNotes
|
||||
)
|
||||
.subscribe((updated) => {
|
||||
this.toast(
|
||||
this.concludeConclusion === 'SUSPICIOUS'
|
||||
? `STR case #${updated.id} concluded suspicious — FIU due date ${updated.fiuDueDate}`
|
||||
: `STR case #${updated.id} concluded not suspicious`
|
||||
);
|
||||
this.selectedStrCase = updated;
|
||||
this.loadAmlStrCases();
|
||||
.subscribe({
|
||||
next: (updated) => {
|
||||
this.toast(
|
||||
this.concludeConclusion === 'SUSPICIOUS'
|
||||
? `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();
|
||||
},
|
||||
error: this.apiError(`Concluding STR case #${this.selectedStrCase.id}`)
|
||||
});
|
||||
}
|
||||
|
||||
fileStrCase(): void {
|
||||
if (!this.selectedStrCase || !this.fileReferenceNo) {
|
||||
if (!this.selectedStrCase) {
|
||||
return;
|
||||
}
|
||||
this.amlService.fileStr(this.selectedStrCase.id, this.fileReferenceNo).subscribe((updated) => {
|
||||
this.toast(`STR case #${updated.id} recorded as filed (ref ${updated.strReferenceNo})`);
|
||||
this.selectedStrCase = updated;
|
||||
this.loadAmlStrCases();
|
||||
if (!this.fileReferenceNo.trim()) {
|
||||
this.toast('Enter the FIU-IND acknowledgement number first');
|
||||
return;
|
||||
}
|
||||
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();
|
||||
},
|
||||
error: this.apiError(`Filing STR case #${this.selectedStrCase.id}`)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,10 +405,57 @@ export class FiuComponent implements OnInit {
|
||||
return 'status-pill status-' + (status || '').toLowerCase().replace(/_/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a live AML alert will end up once triaged — drives the "Routes To" column.
|
||||
* `isCtrRule` distinguishes mechanical CTR-threshold alerts (whitelisting just keeps them
|
||||
* in the CTR file) from behavioral alerts (whitelisting clears them with no report at all).
|
||||
*/
|
||||
alertRouteLabel(status: string, isCtrRule: boolean = true): string {
|
||||
switch (status) {
|
||||
case 'WHITELISTED':
|
||||
return isCtrRule ? 'CTR only' : 'Cleared';
|
||||
case 'ESCALATED':
|
||||
return '→ STR';
|
||||
default:
|
||||
return 'Pending';
|
||||
}
|
||||
}
|
||||
|
||||
alertRouteClass(status: string, isCtrRule: boolean = true): string {
|
||||
switch (status) {
|
||||
case 'WHITELISTED':
|
||||
return isCtrRule ? 'report-pill report-ctr' : 'report-pill report-cleared';
|
||||
case 'ESCALATED':
|
||||
return 'report-pill report-str';
|
||||
default:
|
||||
return 'report-pill report-pending';
|
||||
}
|
||||
}
|
||||
|
||||
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump from a flagged CTR row to its STR card. Clears any status/risk filter that
|
||||
* would hide the case, then scrolls it into view and highlights it.
|
||||
*/
|
||||
openStrCase(txn: CtrTransaction): void {
|
||||
const match = this.strTxns.find((s) => s.txnId === txn.txnId);
|
||||
if (!match) {
|
||||
this.toast('No STR case found for this transaction');
|
||||
return;
|
||||
}
|
||||
this.activeTab = 'str';
|
||||
this.resetStrFilters();
|
||||
this.highlightedStrRef = match.reportRefNo;
|
||||
setTimeout(() => {
|
||||
document
|
||||
.getElementById(`str-card-${match.reportRefNo}`)
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- KPIs --------------------
|
||||
get totalReports(): number {
|
||||
return new Set(this.ctrTxns.map((t) => t.reportRefNo)).size;
|
||||
@@ -201,10 +472,39 @@ export class FiuComponent implements OnInit {
|
||||
get filedStr(): number {
|
||||
return this.strTxns.filter((s) => s.status === 'filed').length;
|
||||
}
|
||||
|
||||
// ── Live STR case counts (backend `amlStrCases`) — these are the cases the
|
||||
// Principal Officer actually works through the Investigate → Conclude → File flow.
|
||||
get liveStrOpen(): number {
|
||||
return this.amlStrCases.filter((c) => c.status === 'OPEN').length;
|
||||
}
|
||||
get liveStrAwaitingFiling(): number {
|
||||
return this.amlStrCases.filter((c) => c.status === 'CONCLUDED_SUSPICIOUS').length;
|
||||
}
|
||||
get liveStrCleared(): number {
|
||||
return this.amlStrCases.filter((c) => c.status === 'CONCLUDED_CLEAR').length;
|
||||
}
|
||||
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. */
|
||||
get liveStrPending(): number {
|
||||
return this.liveStrOpen + this.liveStrAwaitingFiling;
|
||||
}
|
||||
get totalCustomers(): number {
|
||||
return this.kc1.length + this.kc2.length;
|
||||
}
|
||||
|
||||
get ctrAlerts(): any[] {
|
||||
const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L'];
|
||||
return this.amlAlerts.filter((a) => ctrRuleCodes.includes(a.ruleCode));
|
||||
}
|
||||
|
||||
get amlSuspiciousAlerts(): any[] {
|
||||
const ctrRuleCodes = ['CTR_THRESHOLD_10L', 'STRUCTURED_CTR_10L_MONTHLY', 'LARGE_CASH_TXN_25L'];
|
||||
return this.amlAlerts.filter((a) => !ctrRuleCodes.includes(a.ruleCode));
|
||||
}
|
||||
|
||||
// -------------------- Filtered lists --------------------
|
||||
get filteredCtr(): CtrTransaction[] {
|
||||
const acc = this.ctrFilterAccount.trim();
|
||||
@@ -224,6 +524,14 @@ export class FiuComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cases that actually belong in an STR filing. A case the Principal Officer
|
||||
* concluded clean is not a suspect and must never reach FIU-IND.
|
||||
*/
|
||||
get strSuspects(): StrTransaction[] {
|
||||
return this.filteredStr.filter((s) => s.poFinding !== 'CLEAN' && s.status !== 'rejected');
|
||||
}
|
||||
|
||||
resetCtrFilters(): void {
|
||||
this.ctrFilterMode = '';
|
||||
this.ctrFilterAccount = '';
|
||||
@@ -523,11 +831,11 @@ export class FiuComponent implements OnInit {
|
||||
const rows = [headers];
|
||||
// Build one aggregate row per (reportRefNo, accountNumber) pair
|
||||
const seen = new Set<string>();
|
||||
for (const t of this.ctrTxns) {
|
||||
for (const t of this.filteredCtr) {
|
||||
const key = `${t.reportRefNo}|${t.accountNumber}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const sameKey = this.ctrTxns.filter(
|
||||
const sameKey = this.filteredCtr.filter(
|
||||
(x) => x.reportRefNo === t.reportRefNo && x.accountNumber === t.accountNumber
|
||||
);
|
||||
const debits = sameKey.filter((x) => x.depositOrWithdrawal === 'Withdrawal');
|
||||
@@ -576,7 +884,7 @@ export class FiuComponent implements OnInit {
|
||||
];
|
||||
const rows = [headers];
|
||||
const seen = new Set<string>();
|
||||
for (const t of this.ctrTxns) {
|
||||
for (const t of this.filteredCtr) {
|
||||
const key = `${t.reportRefNo}|${t.accountNumber}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
@@ -599,12 +907,17 @@ export class FiuComponent implements OnInit {
|
||||
|
||||
/** STR XML stub (FINnet 2.0 STR XML) */
|
||||
exportStrXml(): void {
|
||||
const suspects = this.strSuspects;
|
||||
if (!suspects.length) {
|
||||
this.toast('No suspect cases to export — nothing matches the current filters');
|
||||
return;
|
||||
}
|
||||
const xml: string[] = [];
|
||||
xml.push('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
xml.push(`<STR xmlns="http://fiuind.gov.in/finnet2/str" reportingMonth="${this.bank.reportingMonth}">`);
|
||||
xml.push(` <REBank buid="${this.bank.reBuid}" name="${this.bank.name}" />`);
|
||||
xml.push(` <PrincipalOfficer name="${this.bank.principalOfficer}" email="${this.bank.principalOfficerEmail}" />`);
|
||||
for (const s of this.filteredStr) {
|
||||
for (const s of suspects) {
|
||||
xml.push(` <Suspect ref="${s.reportRefNo}" status="${s.status}">`);
|
||||
xml.push(` <Account>${s.accountNumber}</Account>`);
|
||||
xml.push(
|
||||
@@ -624,10 +937,10 @@ export class FiuComponent implements OnInit {
|
||||
a.download = `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
this.toast(`STR XML exported (${this.filteredStr.length} suspects)`);
|
||||
this.toast(`STR XML exported (${suspects.length} suspects)`);
|
||||
}
|
||||
|
||||
/** Bundle: all 6 CSVs zipped — emitted as separate downloads sequentially */
|
||||
/** Bundle: the 6 CSVs plus the STR XML — emitted as separate downloads sequentially */
|
||||
exportAll(): void {
|
||||
setTimeout(() => this.exportTc1(), 0);
|
||||
setTimeout(() => this.exportTc2(), 200);
|
||||
@@ -635,6 +948,7 @@ export class FiuComponent implements OnInit {
|
||||
setTimeout(() => this.exportKc2(), 600);
|
||||
setTimeout(() => this.exportAccountDetail(), 800);
|
||||
setTimeout(() => this.exportAccountPersonRelation(), 1000);
|
||||
setTimeout(() => this.exportStrXml(), 1200);
|
||||
}
|
||||
|
||||
private escapeXml(s: string): string {
|
||||
|
||||
Reference in New Issue
Block a user