feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API

- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview
- Hide Financial & Classification section for non-individual entities in personal data and preview
- Fix legal form column in clients list — fetch legalForm from client API for correct display
- Add person/business icon before client name in list rows
- Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API
- Add registration number format hint (mat-hint) based on constitution type
- Add spacing between verification row and constitution type dropdowns
- Reduce table cell padding and prevent action buttons from being clipped
- Add missing MPL-2.0 license headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:10:51 +05:30
co-authored by Claude Opus 4.6
parent ab01111424
commit b890d24bb7
522 changed files with 51991 additions and 21885 deletions
@@ -15,46 +15,81 @@ import { ClientsService } from 'app/clients/clients.service';
</h2>
<mat-dialog-content>
@if (verifying) {
<mat-progress-bar mode="indeterminate" color="primary"></mat-progress-bar>
<p class="sanctions-msg">
Checking <strong>{{ clientName }}</strong> against sanctions list<br />
<strong>UN Security Council</strong> &amp;
<strong>Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231)</strong>
</p>
}
@if (!verifying && !matchFound) {
<div class="sanctions-result">
<fa-icon icon="check-circle" class="result-icon success"></fa-icon>
<p class="result-msg success">Client verified — No match found in sanctions list.</p>
</div>
}
@if (!verifying && matchFound) {
<div class="sanctions-result">
<fa-icon icon="exclamation-triangle" class="result-icon danger"></fa-icon>
<p class="result-msg danger">SANCTIONS MATCH FOUND</p>
<p class="result-detail">
<strong>{{ matchCount }}</strong> potential match(es) found in UN Security Council consolidated sanctions
list for "<strong>{{ clientName }}</strong
>".
</p>
<div class="match-list">
@for (match of matches; track match.id) {
<div class="match-item">
<div class="match-name">{{ match.fullName }}</div>
<div class="match-meta">
<span class="match-type">{{ match.entryType }}</span>
<span class="match-ref">{{ match.referenceNumber }}</span>
@if (match.unListType) {
<span class="match-list-type">{{ match.unListType }}</span>
}
</div>
@if (match.aliases && match.aliases.length > 0) {
<div class="match-aliases"><em>Also known as:</em> {{ match.aliases.join(', ') }}</div>
}
<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="block-msg">This client cannot be onboarded. Account opening is blocked due to sanctions match.</p>
<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) {
@@ -67,10 +102,15 @@ import { ClientsService } from 'app/clients/clients.service';
</mat-dialog-content>
<mat-dialog-actions align="end">
@if (!verifying && !matchFound && !errorOccurred) {
<button mat-raised-button color="primary" (click)="close(true)">OK</button>
<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)">Close</button>
<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>
@@ -101,17 +141,250 @@ import { ClientsService } from 'app/clients/clients.service';
mat-dialog-content {
padding: 16px 24px;
max-height: 60vh;
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: 16px 0 0;
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;
@@ -160,8 +433,8 @@ import { ClientsService } from 'app/clients/clients.service';
.match-list {
width: 100%;
max-height: 200px;
overflow-y: auto;
max-height: unset;
overflow: visible;
margin: 8px 0;
}
@@ -239,33 +512,113 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
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.verifying = false;
this.dialogRef.disableClose = false;
if (result.matchFound) {
this.matchFound = true;
this.matchCount = result.matchCount || 0;
this.matches = result.matches || [];
}
this.apiResult = result;
this.apiDone = true;
},
error: (err: any) => {
this.verifying = false;
this.dialogRef.disableClose = false;
this.errorOccurred = true;
this.errorMessage = err?.error?.message || err?.message || 'Unable to connect to sanctions screening service.';
this.apiError = err;
this.apiDone = true;
}
});
// Animate progress through steps
this.runProgress();
}
ngOnDestroy() {}
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);
this.dialogRef.close({ verified, screeningLogId: this.screeningLogId });
}
}