Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
644 lines
16 KiB
TypeScript
644 lines
16 KiB
TypeScript
/**
|
|
* 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, OnInit, OnDestroy, inject } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { MatDialogRef, MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { ClientsService } from 'app/clients/clients.service';
|
|
|
|
@Component({
|
|
selector: 'mifosx-sanctions-verify-dialog',
|
|
template: `
|
|
<h2 mat-dialog-title>
|
|
<fa-icon icon="shield-alt" class="dialog-title-icon"></fa-icon>
|
|
Sanctions Verification
|
|
</h2>
|
|
<mat-dialog-content>
|
|
@if (verifying) {
|
|
<div class="progress-section">
|
|
<mat-progress-bar mode="determinate" [value]="progressValue" color="primary"></mat-progress-bar>
|
|
<div class="progress-percent">{{ progressValue }}%</div>
|
|
<div class="progress-steps">
|
|
@for (step of progressSteps; track step.label; let i = $index) {
|
|
<div class="progress-step" [class.active]="i === currentStep" [class.done]="i < currentStep">
|
|
<fa-icon
|
|
[icon]="i < currentStep ? 'check-circle' : step.icon"
|
|
[class.done-icon]="i < currentStep"
|
|
></fa-icon>
|
|
<span>{{ step.label }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<p class="sanctions-msg">
|
|
Screening <strong>{{ clientName }}</strong> against<br />
|
|
<strong>UN Security Council</strong> &
|
|
<strong>Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231)</strong>
|
|
</p>
|
|
</div>
|
|
}
|
|
@if (!verifying && !matchFound && !errorOccurred) {
|
|
<div class="approve-card">
|
|
<div class="approve-icon-ring">
|
|
<fa-icon icon="shield-alt" class="approve-shield"></fa-icon>
|
|
</div>
|
|
<div class="approve-badge">CLEARED</div>
|
|
<h3 class="approve-title">No Sanctions Match Found</h3>
|
|
<p class="approve-detail">
|
|
<strong>{{ clientName }}</strong> has been screened against the
|
|
<strong>UN Security Council Consolidated List</strong> and <strong>FIU-IND</strong> records. No confirmed
|
|
match was found.
|
|
</p>
|
|
<div class="approve-meta">
|
|
<div class="meta-row">
|
|
<span class="meta-label">Screened On</span>
|
|
<span class="meta-value">{{ screenedDate }}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Status</span>
|
|
<span class="meta-value meta-clear">Customer is Clear</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Database</span>
|
|
<span class="meta-value">UN SC Consolidated List</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (!verifying && matchFound) {
|
|
<div class="reject-card">
|
|
<div class="reject-icon-ring">
|
|
<fa-icon icon="ban" class="reject-shield"></fa-icon>
|
|
</div>
|
|
<div class="reject-badge">CONFIRMED MATCH / REFER</div>
|
|
<h3 class="reject-title">Sanctions Match Found</h3>
|
|
<p class="reject-detail">
|
|
<strong class="reject-name">{{ clientName }}</strong> matched against
|
|
<strong>UN SC Consolidated List</strong>.
|
|
</p>
|
|
<div class="reject-meta">
|
|
<div class="meta-row">
|
|
<span class="meta-label">Screened On</span>
|
|
<span class="meta-value">{{ screenedDate }}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Status</span>
|
|
<span class="meta-value meta-danger">Match Found — Blocked</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Database</span>
|
|
<span class="meta-value">UN SC Consolidated List</span>
|
|
</div>
|
|
</div>
|
|
<div class="escalate-banner">
|
|
<fa-icon icon="exclamation-circle" class="escalate-icon"></fa-icon>
|
|
<span><strong>Escalate to Principal Officer immediately.</strong> Account opening blocked.</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (!verifying && errorOccurred) {
|
|
<div class="sanctions-result">
|
|
<fa-icon icon="exclamation-circle" class="result-icon warning"></fa-icon>
|
|
<p class="result-msg warning">Verification failed. Please try again.</p>
|
|
<p class="result-detail">{{ errorMessage }}</p>
|
|
</div>
|
|
}
|
|
</mat-dialog-content>
|
|
<mat-dialog-actions align="end">
|
|
@if (!verifying && !matchFound && !errorOccurred) {
|
|
<button mat-stroked-button (click)="close(true)" class="approve-btn-secondary">Dismiss</button>
|
|
<button mat-raised-button color="primary" (click)="close(true)" class="approve-btn">
|
|
<fa-icon icon="check"></fa-icon> Mark as Clear
|
|
</button>
|
|
}
|
|
@if (!verifying && matchFound) {
|
|
<button mat-raised-button color="warn" (click)="close(false)" class="reject-btn">
|
|
<fa-icon icon="times"></fa-icon> Flag & Escalate
|
|
</button>
|
|
}
|
|
@if (!verifying && errorOccurred) {
|
|
<button mat-raised-button (click)="close(false)">Close</button>
|
|
}
|
|
</mat-dialog-actions>
|
|
`,
|
|
styles: [
|
|
`
|
|
:host {
|
|
display: block;
|
|
min-width: 500px;
|
|
}
|
|
|
|
h2[mat-dialog-title] {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin: 0;
|
|
padding: 16px 24px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1074b9;
|
|
}
|
|
|
|
.dialog-title-icon {
|
|
font-size: 20px;
|
|
}
|
|
|
|
mat-dialog-content {
|
|
padding: 16px 24px;
|
|
max-height: unset;
|
|
overflow: visible;
|
|
}
|
|
|
|
.progress-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.progress-percent {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #1074b9;
|
|
}
|
|
|
|
.progress-steps {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
width: 100%;
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.progress-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
color: #9ca3af;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.progress-step.active {
|
|
color: #1074b9;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.progress-step.done {
|
|
color: #388e3c;
|
|
}
|
|
|
|
.done-icon {
|
|
color: #388e3c;
|
|
}
|
|
|
|
.sanctions-msg {
|
|
font-size: 14px;
|
|
color: #555;
|
|
line-height: 1.6;
|
|
margin: 4px 0 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.approve-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px 16px;
|
|
gap: 12px;
|
|
}
|
|
|
|
.approve-icon-ring {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #ecfdf5, #d1fae5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 3px solid #a7f3d0;
|
|
}
|
|
|
|
.approve-shield {
|
|
font-size: 28px;
|
|
color: #059669;
|
|
}
|
|
|
|
.approve-badge {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
letter-spacing: 1.5px;
|
|
color: #059669;
|
|
background: #ecfdf5;
|
|
border: 1px solid #a7f3d0;
|
|
padding: 3px 14px;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.approve-title {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
margin: 0;
|
|
}
|
|
|
|
.approve-detail {
|
|
font-size: 13px;
|
|
color: #6b7280;
|
|
text-align: center;
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.approve-meta {
|
|
width: 100%;
|
|
background: #f9fafb;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
padding: 12px 16px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.meta-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 6px 0;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.meta-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.meta-label {
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.meta-value {
|
|
font-size: 12px;
|
|
color: #374151;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.meta-clear {
|
|
color: #059669;
|
|
}
|
|
|
|
.approve-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.approve-btn-secondary {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.reject-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px 16px;
|
|
gap: 12px;
|
|
}
|
|
|
|
.reject-icon-ring {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #fef2f2, #fecaca);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 3px solid #fca5a5;
|
|
}
|
|
|
|
.reject-shield {
|
|
font-size: 28px;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.reject-badge {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
letter-spacing: 1.5px;
|
|
color: #dc2626;
|
|
background: #fef2f2;
|
|
border: 1px solid #fecaca;
|
|
padding: 3px 14px;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.reject-title {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
margin: 0;
|
|
}
|
|
|
|
.reject-detail {
|
|
font-size: 13px;
|
|
color: #6b7280;
|
|
text-align: center;
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
}
|
|
|
|
.reject-name {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.reject-meta {
|
|
width: 100%;
|
|
background: #f9fafb;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.meta-danger {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.escalate-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
background: #dc2626;
|
|
color: #fff;
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.escalate-icon {
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.reject-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.sanctions-result {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 16px 0;
|
|
}
|
|
|
|
.result-icon {
|
|
font-size: 48px;
|
|
}
|
|
|
|
.result-icon.success {
|
|
color: #388e3c;
|
|
}
|
|
.result-icon.danger {
|
|
color: #d32f2f;
|
|
}
|
|
.result-icon.warning {
|
|
color: #f57c00;
|
|
}
|
|
|
|
.result-msg {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.result-msg.success {
|
|
color: #388e3c;
|
|
}
|
|
.result-msg.danger {
|
|
color: #d32f2f;
|
|
}
|
|
.result-msg.warning {
|
|
color: #f57c00;
|
|
}
|
|
|
|
.result-detail {
|
|
font-size: 14px;
|
|
color: #555;
|
|
text-align: center;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.match-list {
|
|
width: 100%;
|
|
max-height: unset;
|
|
overflow: visible;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.match-item {
|
|
background: #fce4ec;
|
|
border-left: 4px solid #d32f2f;
|
|
padding: 8px 12px;
|
|
margin-bottom: 6px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.match-name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
.match-meta {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.match-type,
|
|
.match-ref,
|
|
.match-list-type {
|
|
font-size: 11px;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
background: #ffcdd2;
|
|
color: #b71c1c;
|
|
}
|
|
|
|
.match-aliases {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.block-msg {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #d32f2f;
|
|
background: #ffebee;
|
|
padding: 10px 16px;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
width: 100%;
|
|
}
|
|
|
|
mat-dialog-actions {
|
|
padding: 8px 24px 16px;
|
|
}
|
|
`
|
|
],
|
|
imports: [
|
|
CommonModule,
|
|
MatDialogModule,
|
|
MatProgressBarModule,
|
|
MatButtonModule,
|
|
FaIconComponent
|
|
]
|
|
})
|
|
export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
|
|
private dialogRef = inject(MatDialogRef<SanctionsVerifyDialogComponent>);
|
|
private data: { name: string } = inject(MAT_DIALOG_DATA);
|
|
private clientsService = inject(ClientsService);
|
|
|
|
clientName = '';
|
|
verifying = true;
|
|
matchFound = false;
|
|
matchCount = 0;
|
|
matches: any[] = [];
|
|
errorOccurred = false;
|
|
errorMessage = '';
|
|
screeningLogId: number | null = null;
|
|
|
|
screenedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
|
progressValue = 0;
|
|
currentStep = 0;
|
|
progressSteps = [
|
|
{ label: 'Connecting to UN Security Council database...', icon: 'globe' as any },
|
|
{ label: 'Screening name against consolidated list...', icon: 'search' as any },
|
|
{ label: 'Cross-checking FIU-IND records...', icon: 'database' as any },
|
|
{ label: 'Analyzing results...', icon: 'chart-bar' as any }
|
|
];
|
|
|
|
private progressTimer: any;
|
|
private apiResult: any = null;
|
|
private apiError: any = null;
|
|
private apiDone = false;
|
|
|
|
ngOnInit() {
|
|
this.clientName = this.data?.name || '';
|
|
this.dialogRef.disableClose = true;
|
|
|
|
// Start API call immediately
|
|
this.clientsService.screenSanctions(this.clientName).subscribe({
|
|
next: (result: any) => {
|
|
this.apiResult = result;
|
|
this.apiDone = true;
|
|
},
|
|
error: (err: any) => {
|
|
this.apiError = err;
|
|
this.apiDone = true;
|
|
}
|
|
});
|
|
|
|
// Animate progress through steps
|
|
this.runProgress();
|
|
}
|
|
|
|
private runProgress() {
|
|
const stepDurations = [
|
|
1200,
|
|
1500,
|
|
1800,
|
|
1000
|
|
]; // ms per step
|
|
const totalDuration = stepDurations.reduce((a, b) => a + b, 0);
|
|
let elapsed = 0;
|
|
const interval = 50;
|
|
|
|
this.progressTimer = setInterval(() => {
|
|
elapsed += interval;
|
|
|
|
// Calculate which step we're on
|
|
let accum = 0;
|
|
for (let i = 0; i < stepDurations.length; i++) {
|
|
accum += stepDurations[i];
|
|
if (elapsed <= accum) {
|
|
this.currentStep = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update progress bar
|
|
this.progressValue = Math.min(Math.round((elapsed / totalDuration) * 95), 95);
|
|
|
|
// After all steps are done, wait for API then finish
|
|
if (elapsed >= totalDuration) {
|
|
if (this.apiDone) {
|
|
this.finishVerification();
|
|
} else {
|
|
// API still running — hold at 95% until done
|
|
this.currentStep = stepDurations.length - 1;
|
|
this.progressValue = 95;
|
|
clearInterval(this.progressTimer);
|
|
this.progressTimer = setInterval(() => {
|
|
if (this.apiDone) {
|
|
this.finishVerification();
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
}, interval);
|
|
}
|
|
|
|
private finishVerification() {
|
|
clearInterval(this.progressTimer);
|
|
this.progressValue = 100;
|
|
this.currentStep = this.progressSteps.length;
|
|
|
|
// Small delay after 100% before showing result
|
|
setTimeout(() => {
|
|
this.verifying = false;
|
|
this.dialogRef.disableClose = false;
|
|
|
|
if (this.apiError) {
|
|
this.errorOccurred = true;
|
|
this.errorMessage =
|
|
this.apiError?.error?.message ||
|
|
this.apiError?.message ||
|
|
'Unable to connect to sanctions screening service.';
|
|
} else {
|
|
this.screeningLogId = this.apiResult?.screeningLogId || null;
|
|
if (this.apiResult?.matchFound) {
|
|
this.matchFound = true;
|
|
this.matchCount = this.apiResult.matchCount || 0;
|
|
this.matches = this.apiResult.matches || [];
|
|
}
|
|
}
|
|
}, 400);
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
clearInterval(this.progressTimer);
|
|
}
|
|
|
|
close(verified: boolean) {
|
|
this.dialogRef.close({ verified, screeningLogId: this.screeningLogId });
|
|
}
|
|
}
|