kyc risk and AML & Compliance Checks automate on client creation
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AML transaction-monitoring API: live alerts raised by the rule engine, the
|
||||||
|
* two-tier AML Officer / Principal Officer review workflow, STR cases (with
|
||||||
|
* their 7-working-day FIU filing deadline), monthly CTR reports, and a
|
||||||
|
* client's risk-category change history.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AmlService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
// ═══ Alerts (AML Officer) ═══
|
||||||
|
|
||||||
|
getAlerts(status?: string): Observable<any[]> {
|
||||||
|
const params = status ? new HttpParams().set('status', status) : undefined;
|
||||||
|
return this.http.get<any[]>('/aml/alerts', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** disposition: 'WHITELIST' | 'ESCALATE' */
|
||||||
|
reviewAlert(alertId: number, disposition: 'WHITELIST' | 'ESCALATE', notes: string): Observable<any> {
|
||||||
|
return this.http.post(`/aml/alerts/${alertId}/review`, { disposition, notes });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ STR cases (Principal Officer) ═══
|
||||||
|
|
||||||
|
getStrCases(): Observable<any[]> {
|
||||||
|
return this.http.get<any[]>('/aml/str-cases');
|
||||||
|
}
|
||||||
|
|
||||||
|
getStrCase(strCaseId: number): Observable<any> {
|
||||||
|
return this.http.get(`/aml/str-cases/${strCaseId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWeeklyDigest(): Observable<any[]> {
|
||||||
|
return this.http.get<any[]>('/aml/str-cases/weekly-digest');
|
||||||
|
}
|
||||||
|
|
||||||
|
assignPrincipalOfficer(strCaseId: number, principalOfficerUserId: number): Observable<any> {
|
||||||
|
return this.http.post(`/aml/str-cases/${strCaseId}/assign`, { principalOfficerUserId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** conclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS' */
|
||||||
|
concludeStrCase(
|
||||||
|
strCaseId: number,
|
||||||
|
conclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS',
|
||||||
|
investigationNotes: string,
|
||||||
|
sourceOfFundsNotes: string
|
||||||
|
): Observable<any> {
|
||||||
|
return this.http.post(`/aml/str-cases/${strCaseId}/conclude`, {
|
||||||
|
conclusion,
|
||||||
|
investigationNotes,
|
||||||
|
sourceOfFundsNotes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStr(strCaseId: number, referenceNo: string): Observable<any> {
|
||||||
|
return this.http.post(`/aml/str-cases/${strCaseId}/file`, { referenceNo });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ CTR reports (mechanical, threshold-based) ═══
|
||||||
|
|
||||||
|
getCtrReports(): Observable<any[]> {
|
||||||
|
return this.http.get<any[]>('/aml/ctr-reports');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** month format: yyyy-MM */
|
||||||
|
generateCtrReport(month: string): Observable<any> {
|
||||||
|
return this.http.post('/aml/ctr-reports/generate', { month });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ Client risk-category history ═══
|
||||||
|
|
||||||
|
getRiskCategoryHistory(clientId: number): Observable<any[]> {
|
||||||
|
return this.http.get<any[]>(`/clients/${clientId}/risk-category-history`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-4
@@ -2126,7 +2126,15 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
return generalDetails;
|
return generalDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
get calculatedRisk(): { category: number; reason: string } {
|
get calculatedRisk(): {
|
||||||
|
category: number;
|
||||||
|
reason: string;
|
||||||
|
amlCheckStatus?: string;
|
||||||
|
bannedEntityMatch?: string;
|
||||||
|
willfulDefaulter?: string;
|
||||||
|
ecgcSalFlag?: string;
|
||||||
|
dormantFlag?: string;
|
||||||
|
} {
|
||||||
const legalFormId = this.createClientForm.get('legalFormId')?.value || 1; // 1 = Person, 2 = Entity
|
const legalFormId = this.createClientForm.get('legalFormId')?.value || 1; // 1 = Person, 2 = Entity
|
||||||
const occCatId = this.createClientForm.get('occupationCategoryCvId')?.value;
|
const occCatId = this.createClientForm.get('occupationCategoryCvId')?.value;
|
||||||
const occId = this.createClientForm.get('occupationCvId')?.value;
|
const occId = this.createClientForm.get('occupationCvId')?.value;
|
||||||
@@ -2143,6 +2151,21 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
// Evaluate rules from DB (ordered by orderPosition ASC)
|
// Evaluate rules from DB (ordered by orderPosition ASC)
|
||||||
if (this.riskRules && this.riskRules.length > 0) {
|
if (this.riskRules && this.riskRules.length > 0) {
|
||||||
for (const rule of this.riskRules) {
|
for (const rule of this.riskRules) {
|
||||||
|
// Skip transaction/AML rules that have no KYC criteria
|
||||||
|
if (
|
||||||
|
rule.legalFormId == null &&
|
||||||
|
rule.occupationCategoryCvId == null &&
|
||||||
|
rule.occupationCvId == null &&
|
||||||
|
rule.minMonthlyIncome == null &&
|
||||||
|
rule.maxMonthlyIncome == null &&
|
||||||
|
rule.minAnnualIncome == null &&
|
||||||
|
rule.maxAnnualIncome == null &&
|
||||||
|
rule.minAnnualTurnover == null &&
|
||||||
|
rule.maxAnnualTurnover == null
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Match legal form
|
// Match legal form
|
||||||
if (rule.legalFormId != null && rule.legalFormId !== legalFormId) {
|
if (rule.legalFormId != null && rule.legalFormId !== legalFormId) {
|
||||||
continue;
|
continue;
|
||||||
@@ -2183,15 +2206,39 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All matched!
|
// All matched!
|
||||||
return { category: rule.riskCategory, reason: rule.reason };
|
return {
|
||||||
|
category: rule.riskCategory,
|
||||||
|
reason: rule.reason,
|
||||||
|
amlCheckStatus: rule.amlCheckStatus || (rule.riskCategory === 3 ? 'FLAGGED' : 'CLEAR'),
|
||||||
|
bannedEntityMatch: rule.bannedEntityMatch || 'NO',
|
||||||
|
willfulDefaulter: rule.willfulDefaulter || 'NO',
|
||||||
|
ecgcSalFlag: rule.ecgcSalFlag || 'NO',
|
||||||
|
dormantFlag: rule.dormantFlag || 'ACTIVE'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default Fallbacks
|
// Default Fallbacks
|
||||||
if (legalFormId !== LegalFormId.PERSON) {
|
if (legalFormId !== LegalFormId.PERSON) {
|
||||||
return { category: 2, reason: 'Non-Individual / Entity profile (Default Medium Risk)' };
|
return {
|
||||||
|
category: 2,
|
||||||
|
reason: 'Non-Individual / Entity profile (Default Medium Risk)',
|
||||||
|
amlCheckStatus: 'CLEAR',
|
||||||
|
bannedEntityMatch: 'NO',
|
||||||
|
willfulDefaulter: 'NO',
|
||||||
|
ecgcSalFlag: 'NO',
|
||||||
|
dormantFlag: 'ACTIVE'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { category: 1, reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)' };
|
return {
|
||||||
|
category: 1,
|
||||||
|
reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)',
|
||||||
|
amlCheckStatus: 'CLEAR',
|
||||||
|
bannedEntityMatch: 'NO',
|
||||||
|
willfulDefaulter: 'NO',
|
||||||
|
ecgcSalFlag: 'NO',
|
||||||
|
dormantFlag: 'ACTIVE'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1251,6 +1251,12 @@
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
|
<div class="checkbox-field override-checkbox-wrapper full-width">
|
||||||
|
<mat-checkbox formControlName="allowAmlOverride" [disabled]="readOnly">
|
||||||
|
Override Auto-calculated AML & Compliance Checks
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
<mat-label>AML Check Status</mat-label>
|
<mat-label>AML Check Status</mat-label>
|
||||||
<mat-select required formControlName="amlCheckStatus">
|
<mat-select required formControlName="amlCheckStatus">
|
||||||
|
|||||||
@@ -70,7 +70,15 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@Input() entityName: string = '';
|
@Input() entityName: string = '';
|
||||||
/** Read-only mode — disables form editing until user clicks edit icon */
|
/** Read-only mode — disables form editing until user clicks edit icon */
|
||||||
@Input() readOnly = false;
|
@Input() readOnly = false;
|
||||||
@Input() calculatedRisk: { category: number; reason: string } | null = null;
|
@Input() calculatedRisk: {
|
||||||
|
category: number;
|
||||||
|
reason: string;
|
||||||
|
amlCheckStatus?: string;
|
||||||
|
bannedEntityMatch?: string;
|
||||||
|
willfulDefaulter?: string;
|
||||||
|
ecgcSalFlag?: string;
|
||||||
|
dormantFlag?: string;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
today = new Date();
|
today = new Date();
|
||||||
kycId: string = '';
|
kycId: string = '';
|
||||||
@@ -457,6 +465,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
kycDocExpiryDate: [''],
|
kycDocExpiryDate: [''],
|
||||||
riskCategory: [1],
|
riskCategory: [1],
|
||||||
allowRiskOverride: [false],
|
allowRiskOverride: [false],
|
||||||
|
allowAmlOverride: [false],
|
||||||
remarks: [''],
|
remarks: [''],
|
||||||
amlCheckStatus: [
|
amlCheckStatus: [
|
||||||
'CLEAR',
|
'CLEAR',
|
||||||
@@ -534,9 +543,35 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.kycForm.get('allowAmlOverride')?.valueChanges.subscribe((allowed: boolean) => {
|
||||||
|
const amlControls = [
|
||||||
|
this.kycForm.get('amlCheckStatus'),
|
||||||
|
this.kycForm.get('bannedEntityMatch'),
|
||||||
|
this.kycForm.get('willfulDefaulter'),
|
||||||
|
this.kycForm.get('ecgcSalFlag'),
|
||||||
|
this.kycForm.get('dormantFlag')
|
||||||
|
];
|
||||||
|
if (allowed) {
|
||||||
|
if (!this.readOnly) {
|
||||||
|
amlControls.forEach((ctrl) => ctrl?.enable());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
amlControls.forEach((ctrl) => ctrl?.disable());
|
||||||
|
this.applyAutoCalculatedAmlFlags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
||||||
this.kycForm.get('riskCategory')?.disable();
|
this.kycForm.get('riskCategory')?.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.kycForm.get('allowAmlOverride')?.value) {
|
||||||
|
this.kycForm.get('amlCheckStatus')?.disable();
|
||||||
|
this.kycForm.get('bannedEntityMatch')?.disable();
|
||||||
|
this.kycForm.get('willfulDefaulter')?.disable();
|
||||||
|
this.kycForm.get('ecgcSalFlag')?.disable();
|
||||||
|
this.kycForm.get('dormantFlag')?.disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
|
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
|
||||||
@@ -644,6 +679,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
if (!this.kycForm.get('allowRiskOverride')?.value) {
|
||||||
this.kycForm.get('riskCategory')?.disable({ emitEvent: false });
|
this.kycForm.get('riskCategory')?.disable({ emitEvent: false });
|
||||||
}
|
}
|
||||||
|
if (!this.kycForm.get('allowAmlOverride')?.value) {
|
||||||
|
this.kycForm.get('amlCheckStatus')?.disable({ emitEvent: false });
|
||||||
|
this.kycForm.get('bannedEntityMatch')?.disable({ emitEvent: false });
|
||||||
|
this.kycForm.get('willfulDefaulter')?.disable({ emitEvent: false });
|
||||||
|
this.kycForm.get('ecgcSalFlag')?.disable({ emitEvent: false });
|
||||||
|
this.kycForm.get('dormantFlag')?.disable({ emitEvent: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changes['calculatedRisk']) {
|
if (changes['calculatedRisk']) {
|
||||||
@@ -656,6 +698,11 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
riskControl.setValue(newRisk.category);
|
riskControl.setValue(newRisk.category);
|
||||||
riskControl.markAsDirty();
|
riskControl.markAsDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowAmlOverride = this.kycForm.get('allowAmlOverride')?.value;
|
||||||
|
if (!allowAmlOverride) {
|
||||||
|
this.applyAutoCalculatedAmlFlags(newRisk);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.riskCalculationReason = '';
|
this.riskCalculationReason = '';
|
||||||
}
|
}
|
||||||
@@ -870,6 +917,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// Risk & Remarks
|
// Risk & Remarks
|
||||||
riskCategory: data.riskCategory || 1,
|
riskCategory: data.riskCategory || 1,
|
||||||
allowRiskOverride: this.calculatedRisk ? this.calculatedRisk.category !== (data.riskCategory || 1) : false,
|
allowRiskOverride: this.calculatedRisk ? this.calculatedRisk.category !== (data.riskCategory || 1) : false,
|
||||||
|
allowAmlOverride: this.calculatedRisk
|
||||||
|
? this.calculatedRisk.amlCheckStatus !== (data.amlCheckStatus || 'CLEAR') ||
|
||||||
|
this.calculatedRisk.bannedEntityMatch !== (data.bannedEntityMatch || 'NO') ||
|
||||||
|
this.calculatedRisk.willfulDefaulter !== (data.willfulDefaulter || 'NO') ||
|
||||||
|
this.calculatedRisk.ecgcSalFlag !== (data.ecgcSalFlag || 'NO') ||
|
||||||
|
this.calculatedRisk.dormantFlag !== (data.dormantFlag || 'ACTIVE')
|
||||||
|
: false,
|
||||||
remarks: data.remarks || '',
|
remarks: data.remarks || '',
|
||||||
hasPan: !!data.panNumber,
|
hasPan: !!data.panNumber,
|
||||||
// AML & Compliance
|
// AML & Compliance
|
||||||
@@ -1809,6 +1863,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
v.form15Applicable
|
v.form15Applicable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyAutoCalculatedAmlFlags(risk = this.calculatedRisk): void {
|
||||||
|
if (!risk) return;
|
||||||
|
this.kycForm.get('amlCheckStatus')?.setValue(risk.amlCheckStatus || 'CLEAR');
|
||||||
|
this.kycForm.get('bannedEntityMatch')?.setValue(risk.bannedEntityMatch || 'NO');
|
||||||
|
this.kycForm.get('willfulDefaulter')?.setValue(risk.willfulDefaulter || 'NO');
|
||||||
|
this.kycForm.get('ecgcSalFlag')?.setValue(risk.ecgcSalFlag || 'NO');
|
||||||
|
this.kycForm.get('dormantFlag')?.setValue(risk.dormantFlag || 'ACTIVE');
|
||||||
|
|
||||||
|
this.kycForm.get('amlCheckStatus')?.markAsDirty();
|
||||||
|
this.kycForm.get('bannedEntityMatch')?.markAsDirty();
|
||||||
|
this.kycForm.get('willfulDefaulter')?.markAsDirty();
|
||||||
|
this.kycForm.get('ecgcSalFlag')?.markAsDirty();
|
||||||
|
this.kycForm.get('dormantFlag')?.markAsDirty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -392,6 +392,22 @@ export class AuthenticationService {
|
|||||||
return roleNames.some((n) => n === 'clerk');
|
return roleNames.some((n) => n === 'clerk');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true when the logged-in user has the "Principal Officer" role (STR conclusion/filing authority). */
|
||||||
|
isPrincipalOfficerUser(): boolean {
|
||||||
|
const creds = this.getCredentials();
|
||||||
|
if (!creds || !creds.roles) return false;
|
||||||
|
const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim());
|
||||||
|
return roleNames.some((n) => n === 'principal officer');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the logged-in user has the "AML Officer" role (first-level alert review). */
|
||||||
|
isAmlOfficerUser(): boolean {
|
||||||
|
const creds = this.getCredentials();
|
||||||
|
if (!creds || !creds.roles) return false;
|
||||||
|
const roleNames: string[] = (creds.roles as any[]).map((r) => (r.name || r.role || '').toLowerCase().trim());
|
||||||
|
return roleNames.some((n) => n === 'aml officer');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true ONLY for regular cashier role (not Head Cashier).
|
* Returns true ONLY for regular cashier role (not Head Cashier).
|
||||||
* Regular cashier → full-screen transaction entry, no nav.
|
* Regular cashier → full-screen transaction entry, no nav.
|
||||||
|
|||||||
@@ -1167,10 +1167,6 @@
|
|||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0">
|
||||||
<fa-icon icon="chart-bar" class="menu-item-icon"></fa-icon> Depreciation Register
|
<fa-icon icon="chart-bar" class="menu-item-icon"></fa-icon> Depreciation Register
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/fiu" tabindex="0">
|
|
||||||
<fa-icon icon="shield-alt" class="menu-item-icon" style="color: #dc2626"></fa-icon> Financial Intelligence
|
|
||||||
Unit (FIU-IND)
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Investment -->
|
<!-- Investment -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
@@ -1571,7 +1567,8 @@
|
|||||||
<a
|
<a
|
||||||
class="gl-link"
|
class="gl-link"
|
||||||
[class.hidden]="!matchesRbiSearch('CTR / STR')"
|
[class.hidden]="!matchesRbiSearch('CTR / STR')"
|
||||||
(click)="rbiMenuOpen = false; openCtrStrDialog()"
|
routerLink="/fiu"
|
||||||
|
(click)="rbiMenuOpen = false"
|
||||||
>
|
>
|
||||||
<fa-icon icon="calculator" class="gl-icon"></fa-icon> CTR / STR
|
<fa-icon icon="calculator" class="gl-icon"></fa-icon> CTR / STR
|
||||||
</a>
|
</a>
|
||||||
@@ -1668,8 +1665,13 @@
|
|||||||
>
|
>
|
||||||
<fa-icon icon="bell" class="gl-icon"></fa-icon> Daily Position Notice
|
<fa-icon icon="bell" class="gl-icon"></fa-icon> Daily Position Notice
|
||||||
</a>
|
</a>
|
||||||
<a class="gl-link" [class.hidden]="!matchesRbiSearch('R/SLR')" (click)="rbiMenuOpen = false">
|
<a
|
||||||
<fa-icon icon="lock" class="gl-icon"></fa-icon> R/SLR
|
class="gl-link"
|
||||||
|
[class.hidden]="!matchesRbiSearch('AML')"
|
||||||
|
routerLink="/rbi/aml"
|
||||||
|
(click)="rbiMenuOpen = false"
|
||||||
|
>
|
||||||
|
<fa-icon icon="shield-alt" class="gl-icon"></fa-icon> AML
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- OSS Reports -->
|
<!-- OSS Reports -->
|
||||||
|
|||||||
@@ -280,23 +280,6 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the CTR & STR Compliance Center dialog.
|
|
||||||
*/
|
|
||||||
async openCtrStrDialog(): Promise<void> {
|
|
||||||
const { CtrStrDialogComponent } = await import('../../home/dashboard/ctr-str-dialog/ctr-str-dialog.component');
|
|
||||||
this.dialog.open(CtrStrDialogComponent, {
|
|
||||||
width: '95vw',
|
|
||||||
maxWidth: '1280px',
|
|
||||||
maxHeight: '92vh',
|
|
||||||
panelClass: 'crr-slr-dialog-panel',
|
|
||||||
autoFocus: false,
|
|
||||||
disableClose: false,
|
|
||||||
hasBackdrop: true,
|
|
||||||
backdropClass: 'ckyc-rpt-dlg-backdrop'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the CIMS / XBRL Returns dialog. The initial form (OSS-2, PSA, RAL, RCP-UCB)
|
* Opens the CIMS / XBRL Returns dialog. The initial form (OSS-2, PSA, RAL, RCP-UCB)
|
||||||
* is selected based on the menu link the user clicked; user can switch between
|
* is selected based on the menu link the user clicked; user can switch between
|
||||||
|
|||||||
@@ -129,9 +129,130 @@
|
|||||||
<!-- CTR -->
|
<!-- CTR -->
|
||||||
@if (activeTab === 'ctr') {
|
@if (activeTab === 'ctr') {
|
||||||
<div class="fiu-section">
|
<div class="fiu-section">
|
||||||
|
<!-- LIVE: cash-related AML alerts awaiting AML Officer review -->
|
||||||
<div class="fiu-form-meta">
|
<div class="fiu-form-meta">
|
||||||
<div class="fiu-form-title">
|
<div class="fiu-form-title">
|
||||||
<h3>CTR — Cash Transaction Report</h3>
|
<h3>Live Cash Alerts</h3>
|
||||||
|
<p>
|
||||||
|
Raised by the transaction-monitoring rule engine — review and whitelist or escalate to Principal Officer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<select class="fiu-inp" [(ngModel)]="amlAlertStatusFilter" (ngModelChange)="loadAmlAlerts()">
|
||||||
|
<option value="">All statuses</option>
|
||||||
|
<option value="RAISED">Raised</option>
|
||||||
|
<option value="UNDER_REVIEW">Under Review</option>
|
||||||
|
<option value="WHITELISTED">Whitelisted</option>
|
||||||
|
<option value="ESCALATED">Escalated</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="fiu-tbl-wrap">
|
||||||
|
<table class="fiu-tbl">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Client</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Rule</th>
|
||||||
|
<th class="ta-r">Amount</th>
|
||||||
|
<th>Raised On</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (alert of amlAlerts; track alert.id) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ alert.id }}</td>
|
||||||
|
<td>{{ alert.clientDisplayName }}</td>
|
||||||
|
<td>{{ alert.transactionType }}</td>
|
||||||
|
<td>{{ alert.ruleCode }} — {{ alert.ruleDescription }}</td>
|
||||||
|
<td class="ta-r mono">₹{{ alert.amount | number: '1.0-0' }}</td>
|
||||||
|
<td>{{ alert.raisedOn | date: 'medium' }}</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="amlStatusClass(alert.status)">{{ alert.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') {
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="fiu-inp"
|
||||||
|
placeholder="Review notes"
|
||||||
|
[(ngModel)]="amlReviewNotes[alert.id]"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<span class="muted">{{ alert.reviewNotes }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (alert.status === 'RAISED' || alert.status === 'UNDER_REVIEW') {
|
||||||
|
<div class="fiu-action-group" *mifosxHasPermission="'REVIEW_AML_ALERT'">
|
||||||
|
<button class="fiu-btn fiu-btn-g" (click)="whitelistAlert(alert)">Whitelist</button>
|
||||||
|
<button class="fiu-btn fiu-btn-xml" (click)="escalateAlert(alert)">Escalate</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="fiu-empty">No live alerts.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LIVE: monthly CTR reports (mechanical, threshold-based) -->
|
||||||
|
<div class="fiu-form-meta">
|
||||||
|
<div class="fiu-form-title">
|
||||||
|
<h3>Monthly CTR Reports</h3>
|
||||||
|
<p>
|
||||||
|
Threshold: <strong>₹{{ bank.cashThreshold | number }}</strong> per single txn / connected aggregate
|
||||||
|
per month
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="fiu-action-group">
|
||||||
|
<input class="fiu-inp" type="month" [(ngModel)]="ctrGenerateMonth" />
|
||||||
|
<button class="fiu-btn fiu-btn-g" *mifosxHasPermission="'REVIEW_AML_ALERT'" (click)="generateCtrReport()">
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fiu-tbl-wrap">
|
||||||
|
<table class="fiu-tbl">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Month</th>
|
||||||
|
<th>Generated On</th>
|
||||||
|
<th class="ta-c">Records</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Submitted On</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of amlCtrReports; track r.id) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.reportMonth }}</td>
|
||||||
|
<td>{{ 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>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="fiu-empty">No CTR reports generated yet.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FINnet export template (separate concern — TC1/TC2 file-format generation) -->
|
||||||
|
<div class="fiu-form-meta">
|
||||||
|
<div class="fiu-form-title">
|
||||||
|
<h3>FINnet TC2 Export Template</h3>
|
||||||
<p>
|
<p>
|
||||||
All cash transactions ≥ ₹{{ bank.cashThreshold | number }} (single or aggregate per month per account)
|
All cash transactions ≥ ₹{{ bank.cashThreshold | number }} (single or aggregate per month per account)
|
||||||
</p>
|
</p>
|
||||||
@@ -221,9 +342,107 @@
|
|||||||
<!-- STR -->
|
<!-- STR -->
|
||||||
@if (activeTab === 'str') {
|
@if (activeTab === 'str') {
|
||||||
<div class="fiu-section">
|
<div class="fiu-section">
|
||||||
|
<!-- LIVE: STR cases with full Principal Officer actions -->
|
||||||
<div class="fiu-form-meta">
|
<div class="fiu-form-meta">
|
||||||
<div class="fiu-form-title">
|
<div class="fiu-form-title">
|
||||||
<h3>STR — Suspicious Transaction Report</h3>
|
<h3>Live STR Cases</h3>
|
||||||
|
<p>
|
||||||
|
Escalated by an AML Officer; Principal Officer investigates, concludes and files with FIU-IND within 7
|
||||||
|
working days
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fiu-tbl-wrap">
|
||||||
|
<table class="fiu-tbl">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Client</th>
|
||||||
|
<th class="ta-c">Cumulative Cases</th>
|
||||||
|
<th>Assigned PO</th>
|
||||||
|
<th>Conclusion</th>
|
||||||
|
<th>FIU Due Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (sc of amlStrCases; track sc.id) {
|
||||||
|
<tr [class.selected-row]="selectedStrCase?.id === sc.id">
|
||||||
|
<td>{{ sc.id }}</td>
|
||||||
|
<td>{{ sc.clientDisplayName }}</td>
|
||||||
|
<td class="ta-c" [class.escalation-warning]="sc.cumulativeStrCaseCountForClient >= 3">
|
||||||
|
{{ sc.cumulativeStrCaseCountForClient }}
|
||||||
|
</td>
|
||||||
|
<td>{{ sc.assignedPoUsername || '—' }}</td>
|
||||||
|
<td>{{ sc.conclusion || '—' }}</td>
|
||||||
|
<td>{{ sc.fiuDueDate || '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="amlStatusClass(sc.status)">{{ sc.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td><button class="fiu-btn fiu-btn-g" (click)="selectStrCase(sc)">Investigate</button></td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="fiu-empty">No STR cases yet.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</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 }} ·
|
||||||
|
{{ 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-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>
|
||||||
|
<div class="form-row conclusion-row">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="fiuConclusion" value="NOT_SUSPICIOUS" [(ngModel)]="concludeConclusion" />
|
||||||
|
Not suspicious (whitelist)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="fiuConclusion" value="SUSPICIOUS" [(ngModel)]="concludeConclusion" />
|
||||||
|
Suspicious (file STR)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button class="fiu-btn fiu-btn-xml" (click)="concludeStrCase()">Conclude</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>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- FINnet export template (separate concern — STR XML file-format generation) -->
|
||||||
|
<div class="fiu-form-meta">
|
||||||
|
<div class="fiu-form-title">
|
||||||
|
<h3>FINnet STR XML Export Template</h3>
|
||||||
<p>Detected by AML rules engine + branch compliance officers; filed within 7 days</p>
|
<p>Detected by AML rules engine + branch compliance officers; filed within 7 days</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="fiu-btn fiu-btn-xml" (click)="exportStrXml()">
|
<button class="fiu-btn fiu-btn-xml" (click)="exportStrXml()">
|
||||||
|
|||||||
@@ -579,6 +579,41 @@
|
|||||||
background: var(--f-red-l);
|
background: var(--f-red-l);
|
||||||
color: var(--f-red);
|
color: var(--f-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.status-raised {
|
||||||
|
background: var(--f-amber-l);
|
||||||
|
color: var(--f-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-under-review {
|
||||||
|
background: var(--f-blue-l);
|
||||||
|
color: var(--f-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-whitelisted,
|
||||||
|
&.status-concluded-clear,
|
||||||
|
&.status-generated,
|
||||||
|
&.status-submitted {
|
||||||
|
background: var(--f-green-l);
|
||||||
|
color: var(--f-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-escalated,
|
||||||
|
&.status-open,
|
||||||
|
&.status-concluded-suspicious {
|
||||||
|
background: var(--f-red-l);
|
||||||
|
color: var(--f-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-filed {
|
||||||
|
background: var(--f-purple-l);
|
||||||
|
color: var(--f-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-draft {
|
||||||
|
background: var(--f-g100);
|
||||||
|
color: var(--f-g500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-pill {
|
.risk-pill {
|
||||||
@@ -892,3 +927,85 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-row {
|
||||||
|
background: var(--f-blue-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.escalation-warning {
|
||||||
|
color: var(--f-red);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.escalation-badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--f-amber-l);
|
||||||
|
color: var(--f-amber);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.str-case-detail {
|
||||||
|
border: 1px solid var(--f-g200);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--f-g300);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--f-g500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-row {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-str-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px dashed var(--f-g300);
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--f-g500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { AmlService } from 'app/aml/aml.service';
|
||||||
|
import { AuthenticationService } from 'app/core/authentication/authentication.service';
|
||||||
import {
|
import {
|
||||||
CtrTransaction,
|
CtrTransaction,
|
||||||
CTR_TXNS,
|
CTR_TXNS,
|
||||||
@@ -33,7 +35,9 @@ import {
|
|||||||
FormsModule
|
FormsModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class FiuComponent {
|
export class FiuComponent implements OnInit {
|
||||||
|
private amlService = inject(AmlService);
|
||||||
|
private authenticationService = inject(AuthenticationService);
|
||||||
/** Active tab */
|
/** Active tab */
|
||||||
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
|
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
|
||||||
|
|
||||||
@@ -57,6 +61,126 @@ export class FiuComponent {
|
|||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: any;
|
private toastTimer: any;
|
||||||
|
|
||||||
|
// -------------------- Live AML monitoring (real backend, replaces mock CTR/STR tables) --------------------
|
||||||
|
amlAlerts: any[] = [];
|
||||||
|
amlAlertStatusFilter = '';
|
||||||
|
amlReviewNotes: Record<number, string> = {};
|
||||||
|
|
||||||
|
amlCtrReports: any[] = [];
|
||||||
|
ctrGenerateMonth = '';
|
||||||
|
|
||||||
|
amlStrCases: any[] = [];
|
||||||
|
selectedStrCase: any = null;
|
||||||
|
concludeConclusion: 'NOT_SUSPICIOUS' | 'SUSPICIOUS' = 'NOT_SUSPICIOUS';
|
||||||
|
concludeInvestigationNotes = '';
|
||||||
|
concludeSourceOfFundsNotes = '';
|
||||||
|
fileReferenceNo = '';
|
||||||
|
|
||||||
|
get isPrincipalOfficer(): boolean {
|
||||||
|
return this.authenticationService.isPrincipalOfficerUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAmlOfficer(): boolean {
|
||||||
|
return this.authenticationService.isAmlOfficerUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadAmlAlerts();
|
||||||
|
this.loadAmlCtrReports();
|
||||||
|
this.loadAmlStrCases();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAmlAlerts(): void {
|
||||||
|
this.amlService
|
||||||
|
.getAlerts(this.amlAlertStatusFilter || undefined)
|
||||||
|
.subscribe((data) => (this.amlAlerts = data || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAmlCtrReports(): void {
|
||||||
|
this.amlService.getCtrReports().subscribe((data) => (this.amlCtrReports = data || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAmlStrCases(): void {
|
||||||
|
this.amlService.getStrCases().subscribe((data) => (this.amlStrCases = data || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistAlert(alert: any): void {
|
||||||
|
this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe(() => {
|
||||||
|
this.toast(`Alert #${alert.id} whitelisted`);
|
||||||
|
this.loadAmlAlerts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCtrReport(): void {
|
||||||
|
if (!this.ctrGenerateMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.amlService.generateCtrReport(this.ctrGenerateMonth).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toast(`CTR report generated for ${this.ctrGenerateMonth}`);
|
||||||
|
this.loadAmlCtrReports();
|
||||||
|
},
|
||||||
|
error: (err) => this.toast(err?.error?.defaultUserMessage || 'CTR report generation failed')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectStrCase(strCase: any): void {
|
||||||
|
this.selectedStrCase = strCase;
|
||||||
|
this.concludeConclusion = 'NOT_SUSPICIOUS';
|
||||||
|
this.concludeInvestigationNotes = strCase.investigationNotes || '';
|
||||||
|
this.concludeSourceOfFundsNotes = strCase.sourceOfFundsNotes || '';
|
||||||
|
this.fileReferenceNo = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeStrCaseDetail(): void {
|
||||||
|
this.selectedStrCase = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
concludeStrCase(): void {
|
||||||
|
if (!this.selectedStrCase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.amlService
|
||||||
|
.concludeStrCase(
|
||||||
|
this.selectedStrCase.id,
|
||||||
|
this.concludeConclusion,
|
||||||
|
this.concludeInvestigationNotes,
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStrCase(): void {
|
||||||
|
if (!this.selectedStrCase || !this.fileReferenceNo) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
amlStatusClass(status: string): string {
|
||||||
|
return 'status-pill status-' + (status || '').toLowerCase().replace(/_/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
|
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
|
||||||
this.activeTab = tab;
|
this.activeTab = tab;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
<!--
|
|
||||||
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/.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="ctr-str-dlg">
|
|
||||||
<!-- Dialog Header -->
|
|
||||||
<div class="dlg-header">
|
|
||||||
<div class="dlg-title-area">
|
|
||||||
<fa-icon icon="shield-alt" class="header-icon"></fa-icon>
|
|
||||||
<div>
|
|
||||||
<h2 class="title">CTR & STR Compliance Control Center</h2>
|
|
||||||
<p class="subtitle">Anti-Money Laundering (AML) & CFT Regulatory Reporting Terminal</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button mat-icon-button [mat-dialog-close]="true" class="close-btn">
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab Navigators -->
|
|
||||||
<div class="tab-navs">
|
|
||||||
<button class="tab-btn" [class.active]="activeTab === 'dashboard'" (click)="activeTab = 'dashboard'">
|
|
||||||
<fa-icon icon="chart-bar" class="tab-icon"></fa-icon> Compliance Dashboard
|
|
||||||
</button>
|
|
||||||
<button class="tab-btn" [class.active]="activeTab === 'guidelines'" (click)="activeTab = 'guidelines'">
|
|
||||||
<fa-icon icon="university" class="tab-icon"></fa-icon> RBI Regulatory Guidelines
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dialog Body (Tabs) -->
|
|
||||||
<div class="dlg-body">
|
|
||||||
<!-- TAB 1: COMPLIANCE DASHBOARD -->
|
|
||||||
@if (activeTab === 'dashboard') {
|
|
||||||
<div class="tab-content dashboard-tab">
|
|
||||||
<!-- SECTION 1: CTR MANAGMENT -->
|
|
||||||
<div class="dashboard-section card-glass">
|
|
||||||
<div class="section-hdr">
|
|
||||||
<div>
|
|
||||||
<h3>Cash Transaction Reports (CTR - Monthly Cash Registry)</h3>
|
|
||||||
<p class="section-sub">
|
|
||||||
Scans and compiles cash transactions exceeding INR 10 Lakhs limit (Single or monthly cumulative cash
|
|
||||||
deposits/withdrawals).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="action-btn primary" [disabled]="isGeneratingCtr" (click)="generateCtrReport()">
|
|
||||||
@if (isGeneratingCtr) {
|
|
||||||
<span class="spinner"></span> Generating Report...
|
|
||||||
} @else {
|
|
||||||
<fa-icon icon="sync-alt"></fa-icon> Generate Monthly CTR
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-actions">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search CTR files by month or name..."
|
|
||||||
[(ngModel)]="ctrFilter"
|
|
||||||
class="search-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container">
|
|
||||||
<table class="compliance-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Report Payload Name</th>
|
|
||||||
<th>Reporting Period</th>
|
|
||||||
<th>Record Count</th>
|
|
||||||
<th>Generated By</th>
|
|
||||||
<th>Generated Date</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (report of filteredCtrReports; track report.id) {
|
|
||||||
<tr>
|
|
||||||
<td class="bold-text"><fa-icon icon="file-code" class="xml-icon"></fa-icon> {{ report.name }}</td>
|
|
||||||
<td>{{ report.month }}</td>
|
|
||||||
<td class="center-text">
|
|
||||||
<span class="cnt-badge">{{ report.recordCount }} txns</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ report.generatedBy }}</td>
|
|
||||||
<td>{{ report.generatedAt | date: 'medium' }}</td>
|
|
||||||
<td><span class="status-badge ready">Ready</span></td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
class="icon-action-btn"
|
|
||||||
matTooltip="Download FIU-IND XML Payload"
|
|
||||||
(click)="downloadCtrFile(report.name)"
|
|
||||||
>
|
|
||||||
<fa-icon icon="download"></fa-icon> Download XML
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
} @empty {
|
|
||||||
<tr>
|
|
||||||
<td colspan="7" class="empty-state">No CTR reports found matching your search.</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SECTION 2: STR ALERTS PIPELINE -->
|
|
||||||
<div class="dashboard-section card-glass mt-20">
|
|
||||||
<div class="section-hdr">
|
|
||||||
<div>
|
|
||||||
<h3>Suspicious Transaction Activity Pipeline (STR Management)</h3>
|
|
||||||
<p class="section-sub">
|
|
||||||
Behavioral & structural AML alerts generated by KYC profile rules engine. Investigator actions update
|
|
||||||
state in real-time.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="action-btn secondary" (click)="downloadStrReportTemplate()">
|
|
||||||
<fa-icon icon="file-excel"></fa-icon> Export Active Alerts (CSV)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-actions">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search alerts by Client, CIF, or alert type..."
|
|
||||||
[(ngModel)]="strFilter"
|
|
||||||
class="search-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alerts-grid">
|
|
||||||
@for (alert of filteredStrAlerts; track alert.id) {
|
|
||||||
<div class="alert-card" [class]="alert.status.toLowerCase().replace(' ', '-')">
|
|
||||||
<div class="card-hdr">
|
|
||||||
<div class="client-info">
|
|
||||||
<h4>{{ alert.clientName }}</h4>
|
|
||||||
<span class="cif-number">{{ alert.clientCif }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="status-badge" [class]="alert.status.toLowerCase().replace(' ', '-')">{{
|
|
||||||
alert.status
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="amount-line">
|
|
||||||
<span class="label">Amount Highlighted:</span>
|
|
||||||
<span class="amount">₹{{ alert.amount | number: '1.2-2' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="type-line">
|
|
||||||
<span class="label">Alert Flag:</span>
|
|
||||||
<span class="type-value">{{ alert.alertType }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="desc">{{ alert.description }}</p>
|
|
||||||
<span class="date-stamp"
|
|
||||||
><fa-icon icon="clock"></fa-icon> Flagged Date: {{ alert.date | date: 'mediumDate' }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-footer">
|
|
||||||
@if (alert.status === 'Open') {
|
|
||||||
<button class="card-btn investigate" (click)="changeAlertStatus(alert.id, 'Investigating')">
|
|
||||||
<fa-icon icon="search"></fa-icon> Investigate
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (alert.status === 'Open' || alert.status === 'Investigating') {
|
|
||||||
<button class="card-btn file-str" (click)="changeAlertStatus(alert.id, 'Reported STR')">
|
|
||||||
<fa-icon icon="flag"></fa-icon> File STR
|
|
||||||
</button>
|
|
||||||
<button class="card-btn close-alert" (click)="changeAlertStatus(alert.id, 'Closed')">
|
|
||||||
<fa-icon icon="ban"></fa-icon> Dismiss
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (alert.status === 'Closed' || alert.status === 'Reported STR') {
|
|
||||||
<button class="card-btn reset-alert" (click)="changeAlertStatus(alert.id, 'Open')">
|
|
||||||
<fa-icon icon="redo-alt"></fa-icon> Reopen Alert
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} @empty {
|
|
||||||
<div class="empty-alerts">No Suspicious Transaction alerts matching your criteria.</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- TAB 2: RBI REGULATORY GUIDELINES -->
|
|
||||||
@if (activeTab === 'guidelines') {
|
|
||||||
<div class="tab-content scroll-y">
|
|
||||||
<div class="info-layout">
|
|
||||||
<!-- CTR Rules Card -->
|
|
||||||
<div class="info-card card-glass">
|
|
||||||
<div class="card-title-badge ctr-badge-color">
|
|
||||||
<fa-icon icon="coins" class="badge-icon"></fa-icon> CTR - Cash Transaction Report
|
|
||||||
</div>
|
|
||||||
<div class="rules-body">
|
|
||||||
<p class="lead-desc">
|
|
||||||
As mandated by Section 12 of the **Prevention of Money Laundering Act (PMLA), 2002** and RBI KYC
|
|
||||||
guidelines, banks must record and report all high-value cash transactions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="rule-point">
|
|
||||||
<h5>1. Transaction Threshold</h5>
|
|
||||||
<p>
|
|
||||||
All cash transactions (either deposits or withdrawals) of **value more than INR 10 Lakhs** (or its
|
|
||||||
equivalent in foreign currency) in a single transaction.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-point">
|
|
||||||
<h5>2. Monthly Aggregation Rule (Structuring check)</h5>
|
|
||||||
<p>
|
|
||||||
A series of cash transactions in a calendar month that are **integrally connected** and accumulate to
|
|
||||||
a total value exceeding **INR 10 Lakhs** must be compiled and reported in the monthly CTR.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-point">
|
|
||||||
<h5>3. Reporting Timeline</h5>
|
|
||||||
<p>
|
|
||||||
CTRs must be generated and uploaded to the FIU-IND gateway **by the 15th day of the succeeding month**
|
|
||||||
(e.g., June transactions must be uploaded by July 15th).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-point text-warning-border">
|
|
||||||
<strong>Important Note:</strong> Transactions where the net cash balance does not change but high volume
|
|
||||||
is rotated (e.g., cash withdrawals immediately deposited back) must be reported under CTR as separate
|
|
||||||
debit and credit records.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- STR Rules Card -->
|
|
||||||
<div class="info-card card-glass">
|
|
||||||
<div class="card-title-badge str-badge-color">
|
|
||||||
<fa-icon icon="flag" class="badge-icon"></fa-icon> STR - Suspicious Transaction Report
|
|
||||||
</div>
|
|
||||||
<div class="rules-body">
|
|
||||||
<p class="lead-desc">
|
|
||||||
Every reporting entity must maintain vigilance over account transactions. If there are reasonable
|
|
||||||
grounds to suspect transactions involve proceeds of crime, an STR must be generated.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="rule-point">
|
|
||||||
<h5>1. What Constitutes Suspicion?</h5>
|
|
||||||
<p>
|
|
||||||
Transactions that are complex, show unusual patterns, mismatch the customer’s business/risk profile,
|
|
||||||
have no apparent economic rationale, or involve suspected proceeds of crime or terror funding.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-point">
|
|
||||||
<h5>2. No Minimum Threshold Limit</h5>
|
|
||||||
<p>
|
|
||||||
**There is no minimum monetary limit** for STR. An alert should be generated for any transaction,
|
|
||||||
regardless of size, if suspicious behavior is flagged.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-point">
|
|
||||||
<h5>3. Reporting Timeline</h5>
|
|
||||||
<p>
|
|
||||||
The STR must be filed to FIU-IND **within 7 days** of the Principal Compliance Officer confirming the
|
|
||||||
suspicion.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-point text-danger-border">
|
|
||||||
<strong>Strict Tipping-off Restriction:</strong> It is illegal to disclose or "tip-off" to the client
|
|
||||||
that their account is being monitored, investigated, or reported under an STR. Complete confidentiality
|
|
||||||
must be maintained.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,750 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.ctr-str-dlg {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 90vh;
|
|
||||||
max-height: 820px;
|
|
||||||
background: #f8fafc;
|
|
||||||
color: #1e293b;
|
|
||||||
font-family: Inter, sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header
|
|
||||||
.dlg-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
|
|
||||||
.dlg-title-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
|
|
||||||
.header-icon {
|
|
||||||
font-size: 26px;
|
|
||||||
color: #4f46e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
margin: 2px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
color: #64748b;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation Tabs
|
|
||||||
.tab-navs {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
background: #fff;
|
|
||||||
padding: 8px 24px;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
.tab-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: #e0e7ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll layout
|
|
||||||
.scroll-y {
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dialog Body
|
|
||||||
.dlg-body {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 24px;
|
|
||||||
background: #f1f5f9;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&.scroll-y {
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 6px;
|
|
||||||
|
|
||||||
/* Custom scrollbar */
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #cbd5e1;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #94a3b8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dashboard Tab specific layout
|
|
||||||
.dashboard-tab {
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 6px;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #cbd5e1;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card Style Container
|
|
||||||
.card-glass {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgb(0 0 0 / 5%),
|
|
||||||
0 2px 4px -1px rgb(0 0 0 / 2%);
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.section-hdr {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-sub {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-20 {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-25 {
|
|
||||||
margin-top: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search input
|
|
||||||
.table-actions {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 380px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
color: #1e293b;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #4f46e5;
|
|
||||||
box-shadow: 0 0 0 2px rgb(79 70 229 / 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button styles
|
|
||||||
.action-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: #4f46e5;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover:not([disabled]) {
|
|
||||||
background: #4338ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background: #94a3b8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
background: #0ea5e9;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #0284c7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spinner animation
|
|
||||||
.spinner {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid rgb(255 255 255 / 30%);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: #fff;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tables styling
|
|
||||||
.table-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compliance-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: #f8fafc;
|
|
||||||
color: #475569;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
color: #334155;
|
|
||||||
|
|
||||||
&.bold-text {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.center-text {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cnt-badge {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #475569;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xml-icon {
|
|
||||||
color: #ea580c;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
padding: 24px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon Action Button
|
|
||||||
.icon-action-btn {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
color: #475569;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert Cards Grid
|
|
||||||
.alerts-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
transition: box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left boundary color codes based on status
|
|
||||||
&.open {
|
|
||||||
border-left: 4px solid #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.investigating {
|
|
||||||
border-left: 4px solid #ea580c;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reported-str {
|
|
||||||
border-left: 4px solid #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.closed {
|
|
||||||
border-left: 4px solid #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-hdr {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cif-number {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #475569;
|
|
||||||
|
|
||||||
.amount-line,
|
|
||||||
.type-line {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-value {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4f46e5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
margin: 8px 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
background: #f8fafc;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px dashed #e2e8f0;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-stamp {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #94a3b8;
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: 14px;
|
|
||||||
border-top: 1px solid #f1f5f9;
|
|
||||||
padding-top: 12px;
|
|
||||||
|
|
||||||
.card-btn {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 75px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&.investigate {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #d97706;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #fde68a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.file-str {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #16a34a;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #bbf7d0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.close-alert {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #dc2626;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #fecaca;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reset-alert {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #475569;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Badges
|
|
||||||
.status-badge {
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&.ready {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.investigating {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reported-str {
|
|
||||||
background: #dbeafe;
|
|
||||||
color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.closed {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-alerts {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
text-align: center;
|
|
||||||
padding: 36px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #64748b;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab 2: Guidelines layout
|
|
||||||
.info-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
@media (width >= 900px) {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.card-title-badge {
|
|
||||||
align-self: flex-start;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 14px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctr-badge-color {
|
|
||||||
background: #ea580c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.str-badge-color {
|
|
||||||
background: #be185d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-body {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #334155;
|
|
||||||
|
|
||||||
.lead-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #0f172a;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-point {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-warning-border {
|
|
||||||
border-left: 3px solid #eab308;
|
|
||||||
background: #fef9c3;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 0 6px 6px 0;
|
|
||||||
color: #854d0e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-danger-border {
|
|
||||||
border-left: 3px solid #ef4444;
|
|
||||||
background: #fee2e2;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 0 6px 6px 0;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab 3: Architecture & Schema
|
|
||||||
.arch-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
margin: 4px 0 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.arch-section {
|
|
||||||
h4 {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-sub {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-snippet {
|
|
||||||
background: #0f172a;
|
|
||||||
color: #e2e8f0;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
overflow-x: auto;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
&.plain {
|
|
||||||
background: #1e293b;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Routes
|
|
||||||
.route-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.method {
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
min-width: 45px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&.get {
|
|
||||||
background: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.put {
|
|
||||||
background: #ea580c;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.post {
|
|
||||||
background: #2563eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.path {
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.route-desc {
|
|
||||||
color: #475569;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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, inject } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { FaIconComponent, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
|
||||||
import {
|
|
||||||
faShieldAlt,
|
|
||||||
faChartBar,
|
|
||||||
faUniversity,
|
|
||||||
faCode,
|
|
||||||
faSyncAlt,
|
|
||||||
faFileExcel,
|
|
||||||
faFileCode,
|
|
||||||
faDownload,
|
|
||||||
faClock,
|
|
||||||
faSearch,
|
|
||||||
faFlag,
|
|
||||||
faBan,
|
|
||||||
faRedoAlt,
|
|
||||||
faCoins
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
interface CtrReport {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
month: string;
|
|
||||||
recordCount: number;
|
|
||||||
generatedBy: string;
|
|
||||||
generatedAt: Date;
|
|
||||||
status: 'Ready' | 'Generating';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StrAlert {
|
|
||||||
id: number;
|
|
||||||
clientName: string;
|
|
||||||
clientCif: string;
|
|
||||||
alertType: string;
|
|
||||||
amount: number;
|
|
||||||
status: 'Open' | 'Investigating' | 'Reported STR' | 'Closed';
|
|
||||||
description: string;
|
|
||||||
date: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'mifosx-ctr-str-dialog',
|
|
||||||
standalone: true,
|
|
||||||
templateUrl: './ctr-str-dialog.component.html',
|
|
||||||
styleUrls: ['./ctr-str-dialog.component.scss'],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
MatDialogModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatTabsModule,
|
|
||||||
MatTooltipModule,
|
|
||||||
MatIconModule,
|
|
||||||
FaIconComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class CtrStrDialogComponent {
|
|
||||||
dialogRef = inject(MatDialogRef<CtrStrDialogComponent>);
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const library = inject(FaIconLibrary);
|
|
||||||
library.addIcons(
|
|
||||||
faShieldAlt,
|
|
||||||
faChartBar,
|
|
||||||
faUniversity,
|
|
||||||
faCode,
|
|
||||||
faSyncAlt,
|
|
||||||
faFileExcel,
|
|
||||||
faFileCode,
|
|
||||||
faDownload,
|
|
||||||
faClock,
|
|
||||||
faSearch,
|
|
||||||
faFlag,
|
|
||||||
faBan,
|
|
||||||
faRedoAlt,
|
|
||||||
faCoins
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeTab: 'dashboard' | 'guidelines' = 'dashboard';
|
|
||||||
|
|
||||||
// Mock State
|
|
||||||
isGeneratingCtr = false;
|
|
||||||
ctrFilter = '';
|
|
||||||
strFilter = '';
|
|
||||||
|
|
||||||
ctrReports: CtrReport[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'CTR_REPORT_JUNE_2026.xml',
|
|
||||||
month: 'June 2026',
|
|
||||||
recordCount: 14,
|
|
||||||
generatedBy: 'Compliance_Officer_1',
|
|
||||||
generatedAt: new Date(2026, 6, 5, 10, 30),
|
|
||||||
status: 'Ready'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'CTR_REPORT_MAY_2026.xml',
|
|
||||||
month: 'May 2026',
|
|
||||||
recordCount: 22,
|
|
||||||
generatedBy: 'Compliance_Officer_1',
|
|
||||||
generatedAt: new Date(2026, 5, 4, 14, 15),
|
|
||||||
status: 'Ready'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'CTR_REPORT_APRIL_2026.xml',
|
|
||||||
month: 'April 2026',
|
|
||||||
recordCount: 9,
|
|
||||||
generatedBy: 'Compliance_Officer_2',
|
|
||||||
generatedAt: new Date(2026, 4, 3, 11, 45),
|
|
||||||
status: 'Ready'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
strAlerts: StrAlert[] = [
|
|
||||||
{
|
|
||||||
id: 101,
|
|
||||||
clientName: 'Aarav Sharma',
|
|
||||||
clientCif: 'CIF982348',
|
|
||||||
alertType: 'Profile Mismatch',
|
|
||||||
amount: 450000,
|
|
||||||
status: 'Open',
|
|
||||||
description:
|
|
||||||
'Student account received a large inward transfer (INR 4.5L) mismatching declared zero/student income category.',
|
|
||||||
date: new Date(2026, 6, 26)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 102,
|
|
||||||
clientName: 'Karan Malhotra',
|
|
||||||
clientCif: 'CIF229482',
|
|
||||||
alertType: 'Structuring (Smurfing)',
|
|
||||||
amount: 980000,
|
|
||||||
status: 'Investigating',
|
|
||||||
description:
|
|
||||||
'Multiple cash deposits of INR 2.45L, 2.45L, 2.45L, 2.45L within 10 days, totaling INR 9.8L cash (deliberately staying below the 10L single cash transaction CTR threshold).',
|
|
||||||
date: new Date(2026, 6, 24)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 103,
|
|
||||||
clientName: 'Nexora Real Estate Ltd',
|
|
||||||
clientCif: 'CIF500481',
|
|
||||||
alertType: 'Turnover Outlier',
|
|
||||||
amount: 15000000,
|
|
||||||
status: 'Open',
|
|
||||||
description:
|
|
||||||
'Entity account registered an outward wire of INR 1.5Cr when maximum monthly turnover limit is set to INR 50L.',
|
|
||||||
date: new Date(2026, 6, 20)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 104,
|
|
||||||
clientName: 'Priyanka Patel',
|
|
||||||
clientCif: 'CIF774920',
|
|
||||||
alertType: 'Dormant Account Reactivation',
|
|
||||||
amount: 600000,
|
|
||||||
status: 'Reported STR',
|
|
||||||
description:
|
|
||||||
'Account inactive for 18 months suddenly received a cash deposit of INR 6L, followed by immediate online transfers.',
|
|
||||||
date: new Date(2026, 6, 15)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
get filteredCtrReports() {
|
|
||||||
return this.ctrReports.filter(
|
|
||||||
(r) =>
|
|
||||||
r.name.toLowerCase().includes(this.ctrFilter.toLowerCase()) ||
|
|
||||||
r.month.toLowerCase().includes(this.ctrFilter.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get filteredStrAlerts() {
|
|
||||||
return this.strAlerts.filter(
|
|
||||||
(a) =>
|
|
||||||
a.clientName.toLowerCase().includes(this.strFilter.toLowerCase()) ||
|
|
||||||
a.clientCif.toLowerCase().includes(this.strFilter.toLowerCase()) ||
|
|
||||||
a.alertType.toLowerCase().includes(this.strFilter.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
generateCtrReport() {
|
|
||||||
if (this.isGeneratingCtr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isGeneratingCtr = true;
|
|
||||||
|
|
||||||
// Simulate API call to Spring Boot batch job
|
|
||||||
setTimeout(() => {
|
|
||||||
const date = new Date();
|
|
||||||
const newReport: CtrReport = {
|
|
||||||
id: this.ctrReports.length + 1,
|
|
||||||
name: `CTR_REPORT_JULY_2026_DRAFT.xml`,
|
|
||||||
month: 'July 2026 (Draft)',
|
|
||||||
recordCount: 8,
|
|
||||||
generatedBy: 'Current_Admin',
|
|
||||||
generatedAt: date,
|
|
||||||
status: 'Ready'
|
|
||||||
};
|
|
||||||
this.ctrReports.unshift(newReport);
|
|
||||||
this.isGeneratingCtr = false;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadCtrFile(reportName: string) {
|
|
||||||
const dummyXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<FIU-IND-CTR-Report xmlns="http://fiuindia.gov.in/ctr/v1">
|
|
||||||
<Header>
|
|
||||||
<ReportType>CTR</ReportType>
|
|
||||||
<ReportingEntityName>CBS Nexora Bank</ReportingEntityName>
|
|
||||||
<ReportingPeriod>2026-06</ReportingPeriod>
|
|
||||||
</Header>
|
|
||||||
<ReportData>
|
|
||||||
<Record>
|
|
||||||
<TransactionID>TXN88924824</TransactionID>
|
|
||||||
<Date>2026-06-15</Date>
|
|
||||||
<Mode>CASH</Mode>
|
|
||||||
<Amount>1250000.00</Amount>
|
|
||||||
<AccountType>SAVINGS</AccountType>
|
|
||||||
<CustomerName>Aarav Sharma</CustomerName>
|
|
||||||
<CifNumber>CIF982348</CifNumber>
|
|
||||||
</Record>
|
|
||||||
</ReportData>
|
|
||||||
</FIU-IND-CTR-Report>`;
|
|
||||||
|
|
||||||
const blob = new Blob([dummyXml], { type: 'application/xml' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = reportName;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeAlertStatus(alertId: number, newStatus: 'Open' | 'Investigating' | 'Reported STR' | 'Closed') {
|
|
||||||
const alert = this.strAlerts.find((a) => a.id === alertId);
|
|
||||||
if (alert) {
|
|
||||||
alert.status = newStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadStrReportTemplate() {
|
|
||||||
const dummyCsv =
|
|
||||||
`AlertID,ClientName,CIF,AlertType,Amount,Status,Description\n` +
|
|
||||||
this.strAlerts
|
|
||||||
.map(
|
|
||||||
(a) =>
|
|
||||||
`"${a.id}","${a.clientName}","${a.clientCif}","${a.alertType}","${a.amount}","${a.status}","${a.description.replace(/"/g, '""')}"`
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const blob = new Blob([dummyCsv], { type: 'text/csv' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `STR_AML_ALERTS_EXPORT.csv`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<!--
|
||||||
|
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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="container aml-rules-container">
|
||||||
|
<mat-card class="aml-card">
|
||||||
|
<mat-card-header class="aml-card-header">
|
||||||
|
<div class="aml-header-content">
|
||||||
|
<mat-icon class="header-icon">shield</mat-icon>
|
||||||
|
<div>
|
||||||
|
<mat-card-title class="header-title">AML Rules Master</mat-card-title>
|
||||||
|
<mat-card-subtitle class="header-subtitle">
|
||||||
|
Database-driven rules for client risk categorization and AML compliance checks
|
||||||
|
</mat-card-subtitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content class="aml-card-content">
|
||||||
|
<table mat-table [dataSource]="rules" class="aml-table mat-elevation-z1">
|
||||||
|
<!-- Order Position Column -->
|
||||||
|
<ng-container matColumnDef="orderPosition">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="col-center">Rule Srno</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="col-center">{{ element.orderPosition }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Rule Name Column -->
|
||||||
|
<ng-container matColumnDef="ruleName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Rule Description</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="rule-name-cell">{{ element.ruleName }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Risk Category Column -->
|
||||||
|
<ng-container matColumnDef="riskCategory">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="col-center">Risk Category</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="col-center">
|
||||||
|
<span class="risk-badge" [ngClass]="getRiskColorClass(element.riskCategory)">
|
||||||
|
<span class="dot"></span>
|
||||||
|
{{ getRiskCategoryLabel(element.riskCategory) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Reason Column -->
|
||||||
|
<ng-container matColumnDef="reason">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Rule Reason / Criteria</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="reason-cell">{{ element.reason }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- AML Check Status Column -->
|
||||||
|
<ng-container matColumnDef="amlCheckStatus">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="col-center">AML Check Status</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="col-center">
|
||||||
|
<span class="compliance-badge" [ngClass]="'status-' + (element.amlCheckStatus || 'clear').toLowerCase()">
|
||||||
|
{{ element.amlCheckStatus || 'CLEAR' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Banned Entity Match Column -->
|
||||||
|
<ng-container matColumnDef="bannedEntityMatch">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="col-center">Banned Match</th>
|
||||||
|
<td mat-cell *matCellDef="let element" class="col-center">
|
||||||
|
<span class="compliance-badge" [ngClass]="'match-' + (element.bannedEntityMatch || 'no').toLowerCase()">
|
||||||
|
{{ element.bannedEntityMatch || 'NO' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="rule-row"></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Mat Paginator for standard pagination -->
|
||||||
|
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons class="aml-paginator"></mat-paginator>
|
||||||
|
|
||||||
|
<div *ngIf="rules.data.length === 0" class="no-rules-message">
|
||||||
|
<mat-icon>info_outline</mat-icon>
|
||||||
|
<span>No AML rules found in the database.</span>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.aml-rules-container {
|
||||||
|
padding: 24px;
|
||||||
|
background-color: var(--bg-neutral-light, #f8fafc);
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aml-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 20px -2px rgb(0 0 0 / 5%),
|
||||||
|
0 2px 8px -1px rgb(0 0 0 / 3%);
|
||||||
|
border: 1px solid rgb(226 232 240 / 80%);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aml-card-header {
|
||||||
|
background: linear-gradient(135deg, #1e293b, #0f172a);
|
||||||
|
color: #fff;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aml-header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aml-card-content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aml-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border-bottom: 1px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #334155;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-row {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-center {
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-name-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e293b;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-cell {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges for Risk Category */
|
||||||
|
.risk-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.risk-high {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fee2e2;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.risk-medium {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fef3c7;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
background-color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.risk-low {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
border: 1px solid #dcfce7;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
background-color: #16a34a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges for Compliance status */
|
||||||
|
.compliance-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
|
||||||
|
/* AML Statuses */
|
||||||
|
&.status-clear {
|
||||||
|
background-color: #dcfce7;
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-flagged {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-blocked {
|
||||||
|
background-color: #fee2e2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banned Matches */
|
||||||
|
&.match-no {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.match-yes {
|
||||||
|
background-color: #fee2e2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.match-partial_match {
|
||||||
|
background-color: #ffedd5;
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-rules-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
color: #64748b;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aml-paginator {
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Angular Imports */
|
||||||
|
import { Component, OnInit, inject, ViewChild } from '@angular/core';
|
||||||
|
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { ClientsService } from '../../clients/clients.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-aml-rules',
|
||||||
|
templateUrl: './aml-rules.component.html',
|
||||||
|
styleUrls: ['./aml-rules.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatPaginatorModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AmlRulesComponent implements OnInit {
|
||||||
|
private clientsService = inject(ClientsService);
|
||||||
|
|
||||||
|
rules = new MatTableDataSource<any>([]);
|
||||||
|
displayedColumns: string[] = [
|
||||||
|
'orderPosition',
|
||||||
|
'ruleName',
|
||||||
|
'riskCategory',
|
||||||
|
'reason',
|
||||||
|
'amlCheckStatus',
|
||||||
|
'bannedEntityMatch'
|
||||||
|
];
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.clientsService.getClientRiskRules().subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.rules.data = data || [];
|
||||||
|
this.rules.paginator = this.paginator;
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRiskCategoryLabel(category: number): string {
|
||||||
|
switch (category) {
|
||||||
|
case 1:
|
||||||
|
return 'Low Risk';
|
||||||
|
case 2:
|
||||||
|
return 'Medium Risk';
|
||||||
|
case 3:
|
||||||
|
return 'High Risk';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRiskColorClass(category: number): string {
|
||||||
|
switch (category) {
|
||||||
|
case 1:
|
||||||
|
return 'risk-low';
|
||||||
|
case 2:
|
||||||
|
return 'risk-medium';
|
||||||
|
case 3:
|
||||||
|
return 'risk-high';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { Route } from '../core/route/route.service';
|
import { Route } from '../core/route/route.service';
|
||||||
import { RbiCircularsComponent } from './rbi-circulars.component';
|
import { RbiCircularsComponent } from './rbi-circulars.component';
|
||||||
|
import { AmlRulesComponent } from './aml-rules/aml-rules.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -18,6 +19,11 @@ const routes: Routes = [
|
|||||||
path: 'rbi-circulars',
|
path: 'rbi-circulars',
|
||||||
component: RbiCircularsComponent,
|
component: RbiCircularsComponent,
|
||||||
data: { title: 'RBI Circulars', breadcrumb: 'RBI Circulars' }
|
data: { title: 'RBI Circulars', breadcrumb: 'RBI Circulars' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'rbi/aml',
|
||||||
|
component: AmlRulesComponent,
|
||||||
|
data: { title: 'AML Rules Master', breadcrumb: 'AML Rules Master' }
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user