Files
cbsnexora/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts
T
Nitesh YadavandClaude Sonnet 4.6 d367e76104 merging code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:26:25 +05:30

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> &amp;
<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 &amp; 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 });
}
}