feat: add 11 banking compliance & accounting report modules
Adds dialog components and feature modules for RBI/banking compliance, KYC, and accounting reports. Wires them into the shell menu (RBI, General Ledger, CKYC Mgmt, Others, Insurance) and dashboard cards. Dialog components (lazy-loaded MatDialog): - PSL Returns (Form III) — dashboard card + RBI menu - ALM Statement of Structural Liquidity (Form ALM-1) — RBI menu - CIMS / XBRL Returns (OSS-2 / PSA / RAL / RCP-UCB) — RBI menu, shared dialog with form-pill switcher - CIC DQR (CIBIL / Experian / Equifax / CRIF, 72-col TUDF) — Others menu + dashboard card - CKYC Compliance Reports (Re-KYC / Risk Review / CERSAI) — CKYC Mgmt - Accounting Reports (P&L / Balance Sheet / Comparative B/S / GL DR/CR / Misc Expense Ledger) — General Ledger menu Feature modules (Route.withShell): - DEAF (/deaf): Form I, Form II, Annex 4, accounts register - DICGC (/dicgc): Slab Report, SAHAY Depositor List - FIU-IND (/fiu): FINnet 2.0 Statement of Financial Transactions - SFT (/sft): Income Tax Section 285BA SFT - CKYC Reports (/ckyc/ckyc-reports): standalone dashboard UI: - Theme-colored icons on 22 wired menu links across 5 menus — visually distinguishes wired (colored) from placeholder (gray) links. - Backdrop classes per dialog in main.scss. All using dummy data (Bharat Urban Bank Ltd, Karthik Kamath as Principal Officer, dummy customer names) — no real bank or customer references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
634003a75c
commit
12bfc81219
@@ -0,0 +1,547 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import {
|
||||
CtrTransaction,
|
||||
CTR_TXNS,
|
||||
FIU_BANK,
|
||||
KC1_INDIVIDUALS,
|
||||
KC2_ENTITIES,
|
||||
KycEntity,
|
||||
KycIndividual,
|
||||
StrStatus,
|
||||
StrTransaction,
|
||||
STR_TXNS
|
||||
} from './fiu-data';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-fiu',
|
||||
templateUrl: './fiu.component.html',
|
||||
styleUrls: ['./fiu.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class FiuComponent {
|
||||
/** Active tab */
|
||||
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
|
||||
|
||||
bank = FIU_BANK;
|
||||
ctrTxns: CtrTransaction[] = CTR_TXNS;
|
||||
strTxns: StrTransaction[] = STR_TXNS;
|
||||
kc1: KycIndividual[] = KC1_INDIVIDUALS;
|
||||
kc2: KycEntity[] = KC2_ENTITIES;
|
||||
|
||||
/** CTR filters */
|
||||
ctrFilterMode: '' | 'Deposit' | 'Withdrawal' = '';
|
||||
ctrFilterAccount = '';
|
||||
ctrFilterMinAmount = 0;
|
||||
|
||||
/** STR filters */
|
||||
strFilterStatus: '' | StrStatus = '';
|
||||
strFilterRisk: string = '';
|
||||
|
||||
/** Toast */
|
||||
toastMsg = '';
|
||||
toastVisible = false;
|
||||
private toastTimer: any;
|
||||
|
||||
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
// -------------------- KPIs --------------------
|
||||
get totalReports(): number {
|
||||
return new Set(this.ctrTxns.map((t) => t.reportRefNo)).size;
|
||||
}
|
||||
get totalCtrTxns(): number {
|
||||
return this.ctrTxns.length;
|
||||
}
|
||||
get totalCtrAmount(): number {
|
||||
return this.ctrTxns.reduce((s, t) => s + t.txnAmount, 0);
|
||||
}
|
||||
get pendingStr(): number {
|
||||
return this.strTxns.filter((s) => s.status === 'draft' || s.status === 'po-review').length;
|
||||
}
|
||||
get filedStr(): number {
|
||||
return this.strTxns.filter((s) => s.status === 'filed').length;
|
||||
}
|
||||
get totalCustomers(): number {
|
||||
return this.kc1.length + this.kc2.length;
|
||||
}
|
||||
|
||||
// -------------------- Filtered lists --------------------
|
||||
get filteredCtr(): CtrTransaction[] {
|
||||
const acc = this.ctrFilterAccount.trim();
|
||||
return this.ctrTxns.filter((t) => {
|
||||
if (this.ctrFilterMode && t.depositOrWithdrawal !== this.ctrFilterMode) return false;
|
||||
if (acc && !t.accountNumber.includes(acc)) return false;
|
||||
if (this.ctrFilterMinAmount > 0 && t.txnAmount < this.ctrFilterMinAmount) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
get filteredStr(): StrTransaction[] {
|
||||
return this.strTxns.filter((s) => {
|
||||
if (this.strFilterStatus && s.status !== this.strFilterStatus) return false;
|
||||
if (this.strFilterRisk && s.riskCategory !== this.strFilterRisk) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
resetCtrFilters(): void {
|
||||
this.ctrFilterMode = '';
|
||||
this.ctrFilterAccount = '';
|
||||
this.ctrFilterMinAmount = 0;
|
||||
}
|
||||
resetStrFilters(): void {
|
||||
this.strFilterStatus = '';
|
||||
this.strFilterRisk = '';
|
||||
}
|
||||
|
||||
// -------------------- Helpers --------------------
|
||||
statusLabel(s: StrStatus): string {
|
||||
return s === 'draft' ? 'Draft' : s === 'po-review' ? 'PO Review' : s === 'filed' ? 'Filed' : 'Rejected';
|
||||
}
|
||||
riskLabel(c: StrTransaction['riskCategory']): string {
|
||||
const map: Record<StrTransaction['riskCategory'], string> = {
|
||||
structuring: 'Structuring',
|
||||
'dormant-revival': 'Dormant Revival',
|
||||
'high-risk-country': 'High-Risk Country',
|
||||
'unusual-pattern': 'Unusual Pattern',
|
||||
'pep-related': 'PEP Related',
|
||||
'sanctions-hit': 'Sanctions Hit'
|
||||
};
|
||||
return map[c];
|
||||
}
|
||||
|
||||
// -------------------- Exports --------------------
|
||||
/** TC2 — Branch / cash transaction CSV */
|
||||
exportTc2(): void {
|
||||
const headers = [
|
||||
'Report Type',
|
||||
'Report Reference Number',
|
||||
'Branch Code of Account',
|
||||
'Account Number',
|
||||
'Transaction Branch Code',
|
||||
'Deposit/Withdrawal',
|
||||
'Transaction ID',
|
||||
'Transaction Date',
|
||||
'Transaction Time',
|
||||
'Transaction Amount',
|
||||
'Instrument Type',
|
||||
'Instrument ID (Card Number/ Cash/ Cheque/ DD/ Others)',
|
||||
'Non-Customer Reference Number',
|
||||
'Relationship Flag',
|
||||
'Third Party PAN (Non-Customer)',
|
||||
'Declaration (If Third Party PAN (Non-Customer) is not available)'
|
||||
];
|
||||
const rows = [headers];
|
||||
for (const t of this.filteredCtr) {
|
||||
rows.push([
|
||||
t.reportType,
|
||||
t.reportRefNo.toString(),
|
||||
t.branchCode,
|
||||
t.accountNumber,
|
||||
t.txnBranchCode,
|
||||
t.depositOrWithdrawal,
|
||||
t.txnId.toString(),
|
||||
t.txnDate.replace(/-/g, '/'),
|
||||
t.txnTime,
|
||||
t.txnAmount.toString(),
|
||||
t.instrumentType,
|
||||
t.instrumentId,
|
||||
t.nonCustomerRef,
|
||||
t.relationshipFlag,
|
||||
t.thirdPartyPan,
|
||||
t.declaration
|
||||
]);
|
||||
}
|
||||
this.saveCsv(rows, 'TC2.csv');
|
||||
this.toast(`TC2 exported (${this.filteredCtr.length} rows)`);
|
||||
}
|
||||
|
||||
/** TC1 header-only template (UI doesn't track ATM/card txns) */
|
||||
exportTc1(): void {
|
||||
const headers = [
|
||||
'Relationship Flag',
|
||||
'Transaction Status',
|
||||
'Transaction ID',
|
||||
'Transaction Date',
|
||||
'Transaction Time',
|
||||
'Deposit/Withdrawal',
|
||||
'Transaction Amount',
|
||||
'Account Number',
|
||||
'Account Type',
|
||||
'Card Type',
|
||||
'Card Number',
|
||||
'Card Issuing Country',
|
||||
'Card Issuing Bank',
|
||||
'Report Reference Number',
|
||||
'Switch Provider Transaction ID',
|
||||
'ATM ID/ CAM ID',
|
||||
'Branch Code of Account',
|
||||
'Report Type'
|
||||
];
|
||||
this.saveCsv([headers], 'TC1.csv');
|
||||
this.toast('TC1 template exported (header only)');
|
||||
}
|
||||
|
||||
/** KC1 — Individual customers */
|
||||
exportKc1(): void {
|
||||
const headers = [
|
||||
'Report Reference Number',
|
||||
'UCIC',
|
||||
'Customer ID',
|
||||
'PAN',
|
||||
'Declaration (If PAN is not available)',
|
||||
'CKYC Number',
|
||||
'Declaration (If CKYC is not available)',
|
||||
'Passport Number',
|
||||
'Voter ID',
|
||||
'Other Occupation',
|
||||
'Drivers License Number',
|
||||
'NREGA Card',
|
||||
'DIN / DPIN',
|
||||
'First Name',
|
||||
'Middle Name',
|
||||
'Last Name',
|
||||
'Declaration (If Last name is not available)',
|
||||
'Name of Father',
|
||||
'Name of Mother',
|
||||
'Spouse/Partner Name',
|
||||
'Gender',
|
||||
'Date of Birth',
|
||||
'Nationality',
|
||||
'Mobile Number',
|
||||
'Alternate Mobile Number',
|
||||
'Telephone Number',
|
||||
'Email ID',
|
||||
'Primary Address 1',
|
||||
'Primary Address Country',
|
||||
'Primary Address Pin Code',
|
||||
'Primary Address Locality',
|
||||
'Primary Address State',
|
||||
'Primary Address District',
|
||||
'Primary Address City',
|
||||
'Customer Type',
|
||||
'Annual Income (INR)',
|
||||
'Occupation',
|
||||
'Date of Customer On-boarding',
|
||||
'Date of Last KYC / re-KYC',
|
||||
'Customer Risk Level',
|
||||
'Report Type',
|
||||
'Other Identifier Name',
|
||||
'Other Identifier Number',
|
||||
'Primary Entity'
|
||||
];
|
||||
const rows = [headers];
|
||||
for (const k of this.kc1) {
|
||||
rows.push([
|
||||
k.reportRefNo.toString(),
|
||||
k.ucic,
|
||||
k.ucic,
|
||||
k.pan,
|
||||
k.pan ? '' : 'NA',
|
||||
k.ckycNumber,
|
||||
k.ckycNumber ? '' : 'NA',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
k.firstName,
|
||||
k.middleName,
|
||||
k.lastName,
|
||||
'',
|
||||
k.fatherName,
|
||||
k.motherName,
|
||||
'',
|
||||
k.gender,
|
||||
k.dob.replace(/-/g, '/'),
|
||||
'97',
|
||||
k.mobileNumber,
|
||||
'',
|
||||
'',
|
||||
k.email,
|
||||
k.primaryAddress,
|
||||
'95',
|
||||
k.pincode,
|
||||
k.primaryAddress.split(' ').slice(0, 4).join(' '),
|
||||
k.state,
|
||||
'501',
|
||||
k.city,
|
||||
k.customerType,
|
||||
k.annualIncome.toString(),
|
||||
k.occupation,
|
||||
k.onboardingDate.replace(/-/g, '/'),
|
||||
k.lastKycDate.replace(/-/g, '/'),
|
||||
k.riskLevel,
|
||||
k.reportType,
|
||||
k.otherIdentifierName || '',
|
||||
k.otherIdentifierNumber || '',
|
||||
'Yes'
|
||||
]);
|
||||
}
|
||||
this.saveCsv(rows, 'KC1.csv');
|
||||
this.toast(`KC1 exported (${this.kc1.length} individuals)`);
|
||||
}
|
||||
|
||||
/** KC2 — Entity customers */
|
||||
exportKc2(): void {
|
||||
const headers = [
|
||||
'Report Reference Number',
|
||||
'UCIC',
|
||||
'Entity Name',
|
||||
'Company ID Number',
|
||||
'Company ID Type',
|
||||
'PAN Number',
|
||||
'Declaration (If PAN is not available)',
|
||||
'TAN',
|
||||
'GSTIN',
|
||||
'IEC',
|
||||
'Declaration (If IEC is not available)',
|
||||
'No identifier available',
|
||||
'Telephone Number',
|
||||
'Mobile Number',
|
||||
'Email ID',
|
||||
'Company Website',
|
||||
'Country',
|
||||
'Address Line 1',
|
||||
'PIN Code',
|
||||
'Locality',
|
||||
'State',
|
||||
'District',
|
||||
'City / Village / Town',
|
||||
'Date Of Incorporation',
|
||||
'Customer Type',
|
||||
'Last KYC Date',
|
||||
'Onboarding Date',
|
||||
'Customer Risk Level',
|
||||
'Report Type',
|
||||
'Sole Proprietor Firm',
|
||||
'UCIC of Owner of Sole Proprietor Firm',
|
||||
'Name of Owner of Sole Proprietor Firm'
|
||||
];
|
||||
const rows = [headers];
|
||||
for (const k of this.kc2) {
|
||||
rows.push([
|
||||
k.reportRefNo.toString(),
|
||||
k.ucic,
|
||||
k.entityName,
|
||||
'',
|
||||
'',
|
||||
k.panNumber,
|
||||
'',
|
||||
'',
|
||||
k.gstin,
|
||||
'',
|
||||
'NA',
|
||||
'',
|
||||
'',
|
||||
k.mobileNumber,
|
||||
'',
|
||||
'',
|
||||
'95',
|
||||
k.address,
|
||||
k.pincode,
|
||||
k.address.split(' ').slice(0, 4).join(' '),
|
||||
k.state,
|
||||
'501',
|
||||
k.city,
|
||||
k.dateOfIncorporation.replace(/-/g, '/'),
|
||||
k.customerType,
|
||||
k.lastKycDate.replace(/-/g, '/'),
|
||||
k.onboardingDate.replace(/-/g, '/'),
|
||||
k.riskLevel,
|
||||
k.reportType,
|
||||
k.isSoleProprietor ? 'Yes' : 'No',
|
||||
k.ownerUcic,
|
||||
k.ownerName
|
||||
]);
|
||||
}
|
||||
this.saveCsv(rows, 'KC2.csv');
|
||||
this.toast(`KC2 exported (${this.kc2.length} entities)`);
|
||||
}
|
||||
|
||||
/** Account_Detail.csv — per-report account aggregates (12-month) */
|
||||
exportAccountDetail(): void {
|
||||
const headers = [
|
||||
'Report Type',
|
||||
'Report Reference Number',
|
||||
'Account Type',
|
||||
'Account Number',
|
||||
'Branch Code of Account',
|
||||
'Date of Account Opening',
|
||||
'Date of Account Closing',
|
||||
'Account Status',
|
||||
'Reason for Account Freeze',
|
||||
'No Of Debits (In last 12 months)',
|
||||
'Total Debit Amount (In last 12 months)',
|
||||
'No Of Credits (In last 12 months)',
|
||||
'Total Credit Amount (In last 12 months)',
|
||||
'No Of Cash Transaction (In last 12 months)',
|
||||
'Total Cash Deposit (In last 12 months)',
|
||||
'Total Cash Withdrawal (In last 12 months) Amount'
|
||||
];
|
||||
const rows = [headers];
|
||||
// Build one aggregate row per (reportRefNo, accountNumber) pair
|
||||
const seen = new Set<string>();
|
||||
for (const t of this.ctrTxns) {
|
||||
const key = `${t.reportRefNo}|${t.accountNumber}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const sameKey = this.ctrTxns.filter(
|
||||
(x) => x.reportRefNo === t.reportRefNo && x.accountNumber === t.accountNumber
|
||||
);
|
||||
const debits = sameKey.filter((x) => x.depositOrWithdrawal === 'Withdrawal');
|
||||
const credits = sameKey.filter((x) => x.depositOrWithdrawal === 'Deposit');
|
||||
const cashTxns = sameKey.filter((x) => x.instrumentType === 'Cash');
|
||||
rows.push([
|
||||
t.reportType,
|
||||
t.reportRefNo.toString(),
|
||||
'212',
|
||||
t.accountNumber,
|
||||
t.branchCode,
|
||||
'20/05/2018',
|
||||
'',
|
||||
'ACTIVE',
|
||||
'',
|
||||
debits.length.toString(),
|
||||
debits.reduce((s, x) => s + x.txnAmount, 0).toString(),
|
||||
credits.length.toString(),
|
||||
credits.reduce((s, x) => s + x.txnAmount, 0).toString(),
|
||||
cashTxns.length.toString(),
|
||||
cashTxns
|
||||
.filter((x) => x.depositOrWithdrawal === 'Deposit')
|
||||
.reduce((s, x) => s + x.txnAmount, 0)
|
||||
.toString(),
|
||||
cashTxns
|
||||
.filter((x) => x.depositOrWithdrawal === 'Withdrawal')
|
||||
.reduce((s, x) => s + x.txnAmount, 0)
|
||||
.toString()
|
||||
]);
|
||||
}
|
||||
this.saveCsv(rows, 'Account_Detail.csv');
|
||||
this.toast(`Account_Detail exported (${rows.length - 1} accounts)`);
|
||||
}
|
||||
|
||||
/** Account_Person_Relation.csv — account ↔ customer mapping */
|
||||
exportAccountPersonRelation(): void {
|
||||
const headers = [
|
||||
'Report Type',
|
||||
'Report Reference Number',
|
||||
'Branch Code of Account',
|
||||
'Account Number',
|
||||
'Relationship Type',
|
||||
'Unique Reference Number',
|
||||
'Individual / Non-individual',
|
||||
'Name of Non-customer'
|
||||
];
|
||||
const rows = [headers];
|
||||
const seen = new Set<string>();
|
||||
for (const t of this.ctrTxns) {
|
||||
const key = `${t.reportRefNo}|${t.accountNumber}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
// Find matching KC1 by reportRefNo (best-effort dummy mapping)
|
||||
const k = this.kc1.find((x) => x.reportRefNo === t.reportRefNo);
|
||||
rows.push([
|
||||
t.reportType,
|
||||
t.reportRefNo.toString(),
|
||||
t.branchCode,
|
||||
t.accountNumber,
|
||||
'1',
|
||||
k ? k.ucic : '0',
|
||||
'Individual',
|
||||
t.relationshipFlag === 'Non-Customer' ? t.customerName || '' : ''
|
||||
]);
|
||||
}
|
||||
this.saveCsv(rows, 'Account_Person_Relation.csv');
|
||||
this.toast(`Account_Person_Relation exported (${rows.length - 1} rows)`);
|
||||
}
|
||||
|
||||
/** STR XML stub (FINnet 2.0 STR XML) */
|
||||
exportStrXml(): void {
|
||||
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) {
|
||||
xml.push(` <Suspect ref="${s.reportRefNo}" status="${s.status}">`);
|
||||
xml.push(` <Account>${s.accountNumber}</Account>`);
|
||||
xml.push(
|
||||
` <Transaction id="${s.txnId}" date="${s.txnDate}" time="${s.txnTime}" amount="${s.txnAmount}" type="${s.depositOrWithdrawal}" instrument="${s.instrumentType}" />`
|
||||
);
|
||||
xml.push(` <RiskCategory>${s.riskCategory}</RiskCategory>`);
|
||||
xml.push(` <Detection date="${s.detectedOn}" by="${this.escapeXml(s.flaggedBy)}" />`);
|
||||
xml.push(` <Narrative>${this.escapeXml(s.narrative)}</Narrative>`);
|
||||
if (s.poRemarks) xml.push(` <PORemarks>${this.escapeXml(s.poRemarks)}</PORemarks>`);
|
||||
xml.push(' </Suspect>');
|
||||
}
|
||||
xml.push('</STR>');
|
||||
const blob = new Blob([xml.join('\n')], { type: 'application/xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
this.toast(`STR XML exported (${this.filteredStr.length} suspects)`);
|
||||
}
|
||||
|
||||
/** Bundle: all 6 CSVs zipped — emitted as separate downloads sequentially */
|
||||
exportAll(): void {
|
||||
setTimeout(() => this.exportTc1(), 0);
|
||||
setTimeout(() => this.exportTc2(), 200);
|
||||
setTimeout(() => this.exportKc1(), 400);
|
||||
setTimeout(() => this.exportKc2(), 600);
|
||||
setTimeout(() => this.exportAccountDetail(), 800);
|
||||
setTimeout(() => this.exportAccountPersonRelation(), 1000);
|
||||
}
|
||||
|
||||
private escapeXml(s: string): string {
|
||||
return (s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
private saveCsv(rows: string[][], filename: string): void {
|
||||
const escape = (v: string): string => {
|
||||
const s = (v || '').replace(/"/g, '""');
|
||||
if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s}"`;
|
||||
return s;
|
||||
};
|
||||
const csv = rows.map((r) => r.map(escape).join(',')).join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
toast(msg: string): void {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
if (this.toastTimer) clearTimeout(this.toastTimer);
|
||||
this.toastTimer = setTimeout(() => (this.toastVisible = false), 2800);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user