feat: Client & CKYC UI improvements — filters, KYC management, image crop+merge
- Clients list: CKYC icon, CKYC Number column, status/legal form/CKYC filters - CKYC: SFTP upload logs, date formatting, modal z-index fix, non-dismissible dialogs - KYC Management: new standalone module with alert dashboard, notification scheduler, audit inventory, account status - Image cropper: ngx-image-cropper integration for front+back document merge into single PNG - Sidebar: KYC Management menu entry after CKYC Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
79e455c4ed
commit
d72e969dc3
+6
-6
@@ -41,11 +41,11 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 24px 16px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: rgb(0 0 0 / 45%);
|
||||
font-size: 14px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
background: rgb(0 0 0 / 2%);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.12);
|
||||
border: 1px dashed rgb(0 0 0 / 12%);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 18px;
|
||||
@@ -58,7 +58,7 @@
|
||||
.panel-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
color: rgb(0 0 0 / 50%);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
+5
-3
@@ -12,7 +12,7 @@
|
||||
&__left { display: flex; align-items: center; gap: 12px; }
|
||||
&__icon { font-size: 18px; color: #60a5fa; }
|
||||
&__title { font-size: 16px; font-weight: 600; color: #fff; margin: 0; }
|
||||
&__sub { font-size: 11px; color: rgba(255,255,255,0.5); margin: 2px 0 0; }
|
||||
&__sub { font-size: 11px; color: rgb(255 255 255 / 50%); margin: 2px 0 0; }
|
||||
}
|
||||
|
||||
.ec-chips-label {
|
||||
@@ -133,6 +133,7 @@
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
mat-checkbox {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
@@ -197,8 +198,9 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) { .ec-grid-3 { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 900px) { .ec-grid-3 { grid-template-columns: repeat(2, 1fr); } }
|
||||
|
||||
@media (width <= 600px) {
|
||||
.ec-grid-3, .ec-grid-2 { grid-template-columns: 1fr; }
|
||||
.ec-check-grid { grid-template-columns: 1fr; }
|
||||
.ec-card-head { padding: 12px 14px; }
|
||||
|
||||
+8
-2
@@ -55,7 +55,7 @@
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -113,6 +113,7 @@
|
||||
|
||||
&__left { flex-shrink: 0; }
|
||||
&__icon { color: #1074b9; }
|
||||
|
||||
&__photo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -123,6 +124,7 @@
|
||||
&__info { flex: 1; display: flex; flex-direction: column; }
|
||||
&__name { font-size: 14px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 12px; color: #64748b; font-family: 'Roboto Mono', monospace; }
|
||||
|
||||
&__sig {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -139,6 +141,7 @@
|
||||
background: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
@@ -225,6 +228,7 @@
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&.not-verified {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
@@ -266,7 +270,7 @@
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 18%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +313,7 @@
|
||||
width: 100%;
|
||||
|
||||
&__icon { color: #1074b9; font-size: 20px; }
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
@@ -318,6 +323,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
color: #94a3b8 !important;
|
||||
width: 28px !important;
|
||||
|
||||
+8
-5
@@ -1,7 +1,7 @@
|
||||
.zoomable {
|
||||
cursor: zoom-in;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
&:hover { transform: scale(1.08); box-shadow: 0 4px 16px rgba(0,0,0,0.18); }
|
||||
&:hover { transform: scale(1.08); box-shadow: 0 4px 16px rgb(0 0 0 / 18%); }
|
||||
}
|
||||
|
||||
::ng-deep .image-zoom-panel {
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@
|
||||
&--delete:hover mat-icon { color: #ef4444; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.fm-step__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -358,6 +358,7 @@
|
||||
.nom-card__info { flex: 1; }
|
||||
.nom-card__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; }
|
||||
.nom-card__meta { font-size: 12px; color: #64748b; display: block; margin-top: 2px; }
|
||||
|
||||
.nom-card__sig {
|
||||
flex-shrink: 0;
|
||||
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; padding: 2px; }
|
||||
@@ -445,6 +446,7 @@
|
||||
/* Search */
|
||||
.nom-search-wrapper { position: relative; }
|
||||
.nom-search-field { width: 100%; }
|
||||
|
||||
.nom-search-prefix {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -460,7 +462,7 @@
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
margin-top: -12px;
|
||||
@@ -517,6 +519,7 @@
|
||||
.nom-selected__info { flex: 1; }
|
||||
.nom-selected__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; }
|
||||
.nom-selected__meta { font-size: 12px; color: #64748b; display: block; }
|
||||
|
||||
.nom-selected__sig {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 2px; flex-shrink: 0;
|
||||
img { height: 28px; max-width: 70px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; background: #fff; padding: 2px 4px; }
|
||||
@@ -599,7 +602,7 @@
|
||||
button { display: flex; align-items: center; gap: 6px; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.nom-form-fields { grid-template-columns: 1fr; }
|
||||
.nom-guardian-card { flex-wrap: wrap; }
|
||||
.nom-guardian-rel { width: 100%; }
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
<span class="sig-table__col sig-table__col--name">Name</span>
|
||||
<span class="sig-table__col sig-table__col--id">Client ID</span>
|
||||
<span class="sig-table__col sig-table__col--pan">PAN</span>
|
||||
<span class="sig-table__col sig-table__col--din">DIN</span>
|
||||
<span class="sig-table__col sig-table__col--status">Status</span>
|
||||
<span class="sig-table__col sig-table__col--actions">Actions</span>
|
||||
</div>
|
||||
@@ -645,6 +646,17 @@
|
||||
{{ sig.panNumber || '-' }}
|
||||
@if (sig.panVerified) { <mat-icon class="verify-tick-sm">check_circle</mat-icon> }
|
||||
</span>
|
||||
<span class="sig-table__col sig-table__col--din">
|
||||
@if (sig.dinNumber) {
|
||||
{{ sig.dinNumber }}
|
||||
@if (sig.dinVerified) { <mat-icon class="verify-tick-sm">check_circle</mat-icon> }
|
||||
@if (!sig.dinVerified) { <mat-icon class="verify-cross-sm">cancel</mat-icon> }
|
||||
} @else {
|
||||
<button mat-stroked-button class="din-verify-btn" type="button" matTooltip="Verify DIN" (click)="openDinVerifyDialog(sig)">
|
||||
<mat-icon class="din-verify-icon">badge</mat-icon> Verify DIN
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
<span class="sig-table__col sig-table__col--status">
|
||||
<span class="sig-status-chip" [class.active]="sig.status?.value === 'Active'">{{ sig.status?.value || '-' }}</span>
|
||||
</span>
|
||||
|
||||
+47
-14
@@ -13,7 +13,7 @@ form {
|
||||
.section-block {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 6%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -28,7 +28,7 @@ form {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0 0 16px;
|
||||
|
||||
.section-icon {
|
||||
color: var(--md-sys-color-primary, #1074b9);
|
||||
@@ -235,7 +235,7 @@ form {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
.guardian-section__header {
|
||||
@@ -268,7 +268,7 @@ form {
|
||||
|
||||
.guardian-section__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: rgb(255 255 255 / 55%);
|
||||
}
|
||||
|
||||
.guardian-section__age-chips {
|
||||
@@ -280,9 +280,9 @@ form {
|
||||
.guardian-age-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: rgb(255 255 255 / 80%);
|
||||
padding: 3px 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgb(255 255 255 / 10%);
|
||||
border-radius: 10px;
|
||||
|
||||
strong {
|
||||
@@ -638,12 +638,13 @@ form {
|
||||
padding: 0 !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.mdc-dialog__surface {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.guardian-section__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -738,7 +739,7 @@ form {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -834,7 +835,7 @@ form {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
.guardian-card__header {
|
||||
@@ -945,7 +946,7 @@ form {
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 0 1px rgb(0 0 0 / 10%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1030,7 +1031,7 @@ form {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgb(255 255 255 / 15%);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
@@ -1040,7 +1041,7 @@ form {
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
background: rgb(255 255 255 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1201,6 +1202,7 @@ form {
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: #10b981 !important;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #059669 !important;
|
||||
-webkit-text-fill-color: #059669 !important;
|
||||
@@ -1213,6 +1215,7 @@ form {
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: #0ea5e9 !important;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #0284c7 !important;
|
||||
-webkit-text-fill-color: #0284c7 !important;
|
||||
@@ -1225,6 +1228,7 @@ form {
|
||||
.mdc-notched-outline__trailing {
|
||||
border-color: #f59e0b !important;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #d97706 !important;
|
||||
-webkit-text-fill-color: #d97706 !important;
|
||||
@@ -1390,7 +1394,7 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1505,6 +1509,7 @@ form {
|
||||
&--name { flex: 1; font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
||||
&--id { flex: 0 0 100px; font-size: 12px; color: #64748b; }
|
||||
&--pan { flex: 0 0 120px; font-size: 12px; color: #1e293b; display: flex; align-items: center; gap: 3px; }
|
||||
&--din { flex: 0 0 140px; font-size: 12px; color: #1e293b; display: flex; align-items: center; gap: 3px; }
|
||||
&--status { flex: 0 0 80px; }
|
||||
&--actions { flex: 0 0 80px; display: flex; gap: 2px; }
|
||||
}
|
||||
@@ -1534,6 +1539,32 @@ form {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.verify-cross-sm {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.din-verify-btn {
|
||||
font-size: 11px !important;
|
||||
padding: 2px 8px !important;
|
||||
min-height: 24px !important;
|
||||
line-height: 20px !important;
|
||||
border-radius: 6px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #1074b9 !important;
|
||||
border-color: #93c5fd !important;
|
||||
|
||||
.din-verify-icon {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sig-status-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -1561,11 +1592,13 @@ form {
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.mdc-dialog__surface {
|
||||
border-radius: 12px !important;
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
@@ -1625,7 +1658,7 @@ form {
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { SanctionsVerifyDialogComponent } from './sanctions-verify-dialog.component';
|
||||
import { DipVerifyDialogComponent } from './dip-verify-dialog.component';
|
||||
import { DinVerifyDialogComponent } from './din-verify-dialog.component';
|
||||
|
||||
/**
|
||||
* Create Client Component
|
||||
@@ -824,6 +825,49 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.syncSignatoryNames();
|
||||
|
||||
this.fetchClientFullDetails(client.id, sig);
|
||||
this.openDinVerifyDialog(sig);
|
||||
}
|
||||
|
||||
/** Open DIN verification dialog for a signatory */
|
||||
openDinVerifyDialog(sig: any): void {
|
||||
const cid = sig.id?.toString();
|
||||
if (!cid) return;
|
||||
|
||||
const entityName = this.createClientForm.get('fullname')?.value || '';
|
||||
|
||||
const openDialog = (kycId: string, panFromKyc?: string) => {
|
||||
const ref = this.dialog.open(DinVerifyDialogComponent, {
|
||||
width: '620px',
|
||||
maxWidth: '95vw',
|
||||
disableClose: false,
|
||||
autoFocus: true,
|
||||
data: {
|
||||
clientId: cid,
|
||||
kycId,
|
||||
signatoryName: sig.displayName || '',
|
||||
cifNumber: sig.cifNumber || sig.accountNumber || cid,
|
||||
panNumber: sig.panNumber || panFromKyc || '-',
|
||||
entityName
|
||||
}
|
||||
});
|
||||
ref.afterClosed().subscribe((result: any) => {
|
||||
if (result && !result.skipped) {
|
||||
sig.dinNumber = result.dinNumber || '';
|
||||
sig.dinVerified = result.dinVerified || false;
|
||||
sig.directorName = result.directorName || '';
|
||||
sig.dinStatus = result.dinStatus || '';
|
||||
sig.dinDesignation = result.dinDesignation || '';
|
||||
sig.companyMatch = result.companyMatch ?? null;
|
||||
sig.matchedCompany = result.matchedCompany || '';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch KYC ID first, then open dialog
|
||||
this.clientService.getClientKyc(cid).subscribe({
|
||||
next: (kyc: any) => openDialog(kyc?.id?.toString() || '', kyc?.panNumber),
|
||||
error: () => openDialog('')
|
||||
});
|
||||
}
|
||||
|
||||
removeSignatoryAt(index: number): void {
|
||||
|
||||
@@ -0,0 +1,651 @@
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-din-verify-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>
|
||||
<fa-icon icon="id-badge" class="dialog-title-icon"></fa-icon>
|
||||
DIN Verification
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<!-- Step 1: Enter DIN -->
|
||||
@if (!verifying && !verified && !failed && !errorOccurred) {
|
||||
<div class="din-entry">
|
||||
<div class="signatory-info">
|
||||
<fa-icon icon="user-shield" class="sig-icon"></fa-icon>
|
||||
<div class="sig-detail">
|
||||
<span class="sig-name">{{ signatoryName }}</span>
|
||||
<span class="sig-meta">CIF: {{ cifNumber }} · PAN: {{ panNumber }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (entityName) {
|
||||
<div class="entity-info">
|
||||
<fa-icon icon="building" class="entity-icon"></fa-icon>
|
||||
<div class="entity-detail">
|
||||
<span class="entity-label">Entity</span>
|
||||
<span class="entity-name">{{ entityName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="din-info-bar">
|
||||
<fa-icon icon="info-circle" class="info-icon"></fa-icon>
|
||||
<span>Enter the <strong>Director Identification Number (DIN)</strong> issued by the Ministry of Corporate Affairs (MCA) for this authorized signatory. The system will verify the DIN and match the entity name against the director's associated companies.</span>
|
||||
</div>
|
||||
<mat-form-field appearance="outline" class="din-input-field">
|
||||
<mat-label>DIN Number</mat-label>
|
||||
<mat-icon matPrefix class="din-prefix-icon">badge</mat-icon>
|
||||
<input matInput [(ngModel)]="dinNumber" placeholder="e.g. 01234567" maxlength="8"
|
||||
(keyup.enter)="onVerify()" autocomplete="off" />
|
||||
<mat-hint>8-digit DIN number from MCA</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 2: Verifying -->
|
||||
@if (verifying) {
|
||||
<div class="progress-section">
|
||||
<mat-progress-bar mode="determinate" [value]="progress" color="primary"></mat-progress-bar>
|
||||
<div class="progress-percent">{{ progress }}%</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="verify-msg">
|
||||
Verifying DIN <strong>{{ dinNumber }}</strong> for <strong>{{ signatoryName }}</strong>
|
||||
against <strong>MCA (Ministry of Corporate Affairs)</strong> records.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 3a: Verified -->
|
||||
@if (!verifying && verified) {
|
||||
<div class="result-card success-card">
|
||||
<div class="result-icon-ring success-ring">
|
||||
<fa-icon icon="check-circle" class="result-shield success-shield"></fa-icon>
|
||||
</div>
|
||||
<div class="result-badge success-badge">VERIFIED</div>
|
||||
<h3 class="result-title">DIN Verified Successfully</h3>
|
||||
<p class="result-detail">
|
||||
DIN <strong class="din-highlight">{{ dinNumber }}</strong> for
|
||||
<strong>{{ signatoryName }}</strong> has been verified against
|
||||
<strong>MCA</strong> records.
|
||||
</p>
|
||||
<div class="result-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">DIN</span>
|
||||
<span class="meta-value meta-mono">{{ dinNumber }}</span>
|
||||
</div>
|
||||
@if (directorName) {
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Director Name</span>
|
||||
<span class="meta-value">{{ directorName }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Status</span>
|
||||
<span class="meta-value meta-success">{{ dinStatus || 'Active' }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Verified On</span>
|
||||
<span class="meta-value">{{ verifiedDate }}</span>
|
||||
</div>
|
||||
@if (dinDesignation) {
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Designation</span>
|
||||
<span class="meta-value">{{ dinDesignation }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Company Match Section -->
|
||||
@if (companies.length > 0) {
|
||||
<div class="company-match-section">
|
||||
<h4 class="company-match-title">
|
||||
<fa-icon icon="building" class="company-title-icon"></fa-icon>
|
||||
Associated Companies ({{ companies.length }})
|
||||
</h4>
|
||||
|
||||
@if (companyMatch) {
|
||||
<div class="match-banner match-success">
|
||||
<fa-icon icon="check-circle" class="match-banner-icon"></fa-icon>
|
||||
<div>
|
||||
<strong>Entity Match Found</strong>
|
||||
<p><strong>{{ entityName }}</strong> matches <strong>{{ matchedCompany }}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="match-banner match-warning">
|
||||
<fa-icon icon="exclamation-triangle" class="match-banner-icon"></fa-icon>
|
||||
<div>
|
||||
<strong>No Entity Match</strong>
|
||||
<p><strong>{{ entityName }}</strong> does not match any of the director's associated companies.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="company-list">
|
||||
@for (company of companies; track company.companyName || $index) {
|
||||
<div class="company-item" [class.company-matched]="company._matched">
|
||||
@if (company._matched) {
|
||||
<mat-icon class="company-match-icon match">check_circle</mat-icon>
|
||||
} @else {
|
||||
<mat-icon class="company-match-icon no-match">radio_button_unchecked</mat-icon>
|
||||
}
|
||||
<div class="company-info">
|
||||
<span class="company-name">{{ company.companyName }}</span>
|
||||
<span class="company-meta">
|
||||
@if (company.cin) { CIN: {{ company.cin }} }
|
||||
@if (company.cin && company.designation) { · }
|
||||
@if (company.designation) { {{ company.designation }} }
|
||||
@if ((company.cin || company.designation) && company.appointmentDate) { · }
|
||||
@if (company.appointmentDate) { Since: {{ company.appointmentDate }} }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 3b: Verification Failed -->
|
||||
@if (!verifying && failed) {
|
||||
<div class="result-card fail-card">
|
||||
<div class="result-icon-ring fail-ring">
|
||||
<fa-icon icon="times-circle" class="result-shield fail-shield"></fa-icon>
|
||||
</div>
|
||||
<div class="result-badge fail-badge">FAILED</div>
|
||||
<h3 class="result-title">DIN Verification Failed</h3>
|
||||
<p class="result-detail">
|
||||
DIN <strong class="din-highlight-fail">{{ dinNumber }}</strong> could not be verified.
|
||||
</p>
|
||||
@if (failReason) {
|
||||
<div class="fail-reason">
|
||||
<fa-icon icon="exclamation-triangle" class="fail-reason-icon"></fa-icon>
|
||||
<span>{{ failReason }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 3c: Error -->
|
||||
@if (!verifying && errorOccurred) {
|
||||
<div class="result-card error-card">
|
||||
<fa-icon icon="exclamation-circle" class="error-icon-large"></fa-icon>
|
||||
<p class="error-msg">Verification failed. Please try again.</p>
|
||||
<p class="error-detail">{{ errorMessage }}</p>
|
||||
</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
@if (!verifying && !verified && !failed && !errorOccurred) {
|
||||
<button mat-stroked-button (click)="onSkip()">Skip</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!dinNumber || dinNumber.length < 8" (click)="onVerify()">
|
||||
<fa-icon icon="search"></fa-icon> Verify DIN
|
||||
</button>
|
||||
}
|
||||
@if (!verifying && verified) {
|
||||
<button mat-raised-button color="primary" (click)="onClose(true)">
|
||||
<fa-icon icon="check"></fa-icon> Done
|
||||
</button>
|
||||
}
|
||||
@if (!verifying && failed) {
|
||||
<button mat-stroked-button (click)="onRetry()">Retry</button>
|
||||
<button mat-raised-button color="warn" (click)="onClose(false)">
|
||||
<fa-icon icon="times"></fa-icon> Close
|
||||
</button>
|
||||
}
|
||||
@if (!verifying && errorOccurred) {
|
||||
<button mat-stroked-button (click)="onRetry()">Retry</button>
|
||||
<button mat-raised-button (click)="onClose(false)">Close</button>
|
||||
}
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
min-width: 520px;
|
||||
}
|
||||
|
||||
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: 70vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Signatory info */
|
||||
.signatory-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #f0f4f8;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.sig-icon { font-size: 22px; color: #1074b9; }
|
||||
.sig-detail { display: flex; flex-direction: column; }
|
||||
.sig-name { font-size: 15px; font-weight: 700; color: #1a2332; }
|
||||
.sig-meta { font-size: 12px; color: #64748b; margin-top: 2px; }
|
||||
|
||||
/* Entity info */
|
||||
.entity-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
background: #fffbeb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.entity-icon { font-size: 18px; color: #b45309; }
|
||||
.entity-detail { display: flex; flex-direction: column; }
|
||||
.entity-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: #92400e; }
|
||||
.entity-name { font-size: 14px; font-weight: 700; color: #78350f; }
|
||||
|
||||
/* DIN info bar */
|
||||
.din-info-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-icon { font-size: 16px; color: #1074b9; flex-shrink: 0; margin-top: 2px; }
|
||||
|
||||
/* DIN input */
|
||||
.din-input-field { width: 100%; }
|
||||
.din-prefix-icon { color: #64748b; margin-right: 4px; }
|
||||
|
||||
/* Progress */
|
||||
.progress-section { display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
.progress-percent { font-size: 22px; font-weight: 700; color: #1074b9; margin-top: 4px; }
|
||||
|
||||
.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; }
|
||||
|
||||
.verify-msg { font-size: 14px; color: #555; line-height: 1.6; text-align: center; margin: 4px 0 0; }
|
||||
|
||||
/* Result cards */
|
||||
.result-card { display: flex; flex-direction: column; align-items: center; padding: 20px 16px; gap: 12px; }
|
||||
.result-icon-ring { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.success-ring { background: linear-gradient(135deg, #ecfdf5, #d1fae5); border: 3px solid #a7f3d0; }
|
||||
.fail-ring { background: linear-gradient(135deg, #fef2f2, #fecaca); border: 3px solid #fca5a5; }
|
||||
.result-shield { font-size: 28px; }
|
||||
.success-shield { color: #059669; }
|
||||
.fail-shield { color: #dc2626; }
|
||||
|
||||
.result-badge { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 1.5px; padding: 3px 14px; border-radius: 20px; }
|
||||
.success-badge { color: #059669; background: #ecfdf5; border: 1px solid #a7f3d0; }
|
||||
.fail-badge { color: #dc2626; background: #fef2f2; border: 1px solid #fecaca; }
|
||||
|
||||
.result-title { font-size: 17px; font-weight: 700; color: #111827; margin: 0; }
|
||||
.result-detail { font-size: 13px; color: #6b7280; text-align: center; line-height: 1.6; margin: 0; max-width: 400px; }
|
||||
.din-highlight { color: #059669; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
|
||||
.din-highlight-fail { color: #dc2626; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
|
||||
|
||||
.result-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-mono { font-family: 'JetBrains Mono', 'Fira Code', monospace; letter-spacing: 0.5px; }
|
||||
.meta-success { color: #059669; }
|
||||
|
||||
/* ═══ Company Match Section ═══ */
|
||||
.company-match-section {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.company-match-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.company-title-icon { font-size: 14px; color: #64748b; }
|
||||
|
||||
/* Match banner */
|
||||
.match-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.match-banner p { margin: 2px 0 0; font-size: 12px; }
|
||||
|
||||
.match-success {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.match-success .match-banner-icon { color: #059669; font-size: 18px; margin-top: 1px; flex-shrink: 0; }
|
||||
|
||||
.match-warning {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.match-warning .match-banner-icon { color: #d97706; font-size: 18px; margin-top: 1px; flex-shrink: 0; }
|
||||
|
||||
/* Company list */
|
||||
.company-list {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.company-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.company-item:last-child { border-bottom: none; }
|
||||
|
||||
.company-item:hover { background: #f8fafc; }
|
||||
|
||||
.company-matched {
|
||||
background: #f0fdf4 !important;
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
.company-match-icon {
|
||||
font-size: 18px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.company-match-icon.match { color: #22c55e; }
|
||||
.company-match-icon.no-match { color: #d1d5db; }
|
||||
|
||||
.company-info { display: flex; flex-direction: column; min-width: 0; }
|
||||
.company-name { font-size: 13px; font-weight: 600; color: #1e293b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.company-meta { font-size: 11px; color: #94a3b8; margin-top: 2px; }
|
||||
|
||||
/* Fail reason */
|
||||
.fail-reason {
|
||||
display: flex; align-items: center; gap: 10px; width: 100%;
|
||||
background: #fef2f2; border: 1px solid #fecaca; padding: 10px 14px;
|
||||
border-radius: 6px; font-size: 13px; color: #991b1b; line-height: 1.5;
|
||||
}
|
||||
.fail-reason-icon { font-size: 16px; color: #dc2626; flex-shrink: 0; }
|
||||
|
||||
/* Error */
|
||||
.error-icon-large { font-size: 48px; color: #f57c00; }
|
||||
.error-msg { font-size: 16px; font-weight: 600; color: #f57c00; margin: 0; text-align: center; }
|
||||
.error-detail { font-size: 14px; color: #555; text-align: center; }
|
||||
|
||||
mat-dialog-actions { padding: 8px 24px 16px; }
|
||||
mat-dialog-actions button { display: flex; align-items: center; gap: 6px; }
|
||||
`
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatProgressBarModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class DinVerifyDialogComponent implements OnInit, OnDestroy {
|
||||
private dialogRef = inject(MatDialogRef<DinVerifyDialogComponent>);
|
||||
private data: {
|
||||
clientId: string;
|
||||
kycId: string;
|
||||
signatoryName: string;
|
||||
cifNumber: string;
|
||||
panNumber: string;
|
||||
entityName: string;
|
||||
} = inject(MAT_DIALOG_DATA);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
signatoryName = '';
|
||||
cifNumber = '';
|
||||
panNumber = '';
|
||||
entityName = '';
|
||||
dinNumber = '';
|
||||
|
||||
verifying = false;
|
||||
verified = false;
|
||||
failed = false;
|
||||
errorOccurred = false;
|
||||
errorMessage = '';
|
||||
failReason = '';
|
||||
|
||||
directorName = '';
|
||||
dinStatus = '';
|
||||
dinDesignation = '';
|
||||
verifiedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
|
||||
/** Companies list from DIN verification response */
|
||||
companies: any[] = [];
|
||||
/** Whether entity name matched a company */
|
||||
companyMatch = false;
|
||||
/** The matched company name */
|
||||
matchedCompany = '';
|
||||
|
||||
progress = 0;
|
||||
currentStep = 0;
|
||||
progressSteps = [
|
||||
{ label: 'Connecting to MCA portal...', icon: 'globe' as any },
|
||||
{ label: 'Looking up DIN records...', icon: 'search' as any },
|
||||
{ label: 'Matching entity with companies...', icon: 'building' as any },
|
||||
{ label: 'Saving verification...', icon: 'database' as any }
|
||||
];
|
||||
|
||||
private intervalId: any;
|
||||
private apiResponse: any = null;
|
||||
private apiError: any = null;
|
||||
private apiDone = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.signatoryName = this.data?.signatoryName || '';
|
||||
this.cifNumber = this.data?.cifNumber || '';
|
||||
this.panNumber = this.data?.panNumber || '-';
|
||||
this.entityName = this.data?.entityName || '';
|
||||
}
|
||||
|
||||
onVerify() {
|
||||
if (!this.dinNumber || this.dinNumber.length < 8) return;
|
||||
|
||||
this.verifying = true;
|
||||
this.verified = false;
|
||||
this.failed = false;
|
||||
this.errorOccurred = false;
|
||||
this.apiDone = false;
|
||||
this.apiResponse = null;
|
||||
this.apiError = null;
|
||||
this.progress = 0;
|
||||
this.currentStep = 0;
|
||||
this.companies = [];
|
||||
this.companyMatch = false;
|
||||
this.matchedCompany = '';
|
||||
|
||||
this.dialogRef.disableClose = true;
|
||||
|
||||
// Fire API call — pass clientId, kycId, dinNumber and entityName
|
||||
this.clientsService.verifyDIN(this.data.clientId, this.data.kycId, this.dinNumber, this.entityName).subscribe({
|
||||
next: (res: any) => {
|
||||
this.apiResponse = res;
|
||||
this.apiDone = true;
|
||||
this.tryShowResult();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.apiError = err;
|
||||
this.apiDone = true;
|
||||
this.tryShowResult();
|
||||
}
|
||||
});
|
||||
|
||||
// Animate progress
|
||||
this.intervalId = setInterval(() => {
|
||||
this.progress += 2;
|
||||
this.currentStep = Math.min(Math.floor(this.progress / 25), this.progressSteps.length - 1);
|
||||
if (this.progress >= 100) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
this.tryShowResult();
|
||||
}
|
||||
}, 60);
|
||||
}
|
||||
|
||||
private tryShowResult() {
|
||||
if (!this.apiDone || this.progress < 100) return;
|
||||
|
||||
this.verifying = false;
|
||||
this.dialogRef.disableClose = false;
|
||||
|
||||
if (this.apiError) {
|
||||
this.errorOccurred = true;
|
||||
this.errorMessage = this.apiError?.error?.defaultUserMessage || this.apiError?.error?.message || this.apiError?.message || 'Unable to verify DIN. Please try again.';
|
||||
return;
|
||||
}
|
||||
|
||||
const res = this.apiResponse;
|
||||
const changes = res?.changes || res;
|
||||
|
||||
if (changes?.dinVerified || res?.dinVerified) {
|
||||
this.verified = true;
|
||||
this.directorName = changes?.directorName || res?.directorName || '';
|
||||
this.dinStatus = changes?.dinStatus || res?.dinStatus || 'Active';
|
||||
this.dinDesignation = changes?.dinDesignation || res?.dinDesignation || '';
|
||||
this.verifiedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
|
||||
// Process company list from response
|
||||
const rawCompanies = changes?.companies || res?.companies || [];
|
||||
this.companies = rawCompanies.map((c: any) => ({
|
||||
...c,
|
||||
companyName: c.companyName || c.name || '',
|
||||
cin: c.cin || c.cinNumber || '',
|
||||
designation: c.designation || '',
|
||||
appointmentDate: c.appointmentDate || c.dateOfAppointment || '',
|
||||
_matched: false
|
||||
}));
|
||||
|
||||
// Match entity name against company list
|
||||
this.matchCompanyName();
|
||||
} else {
|
||||
this.failed = true;
|
||||
this.failReason = changes?.dinFailReason || res?.failReason || res?.defaultUserMessage || 'DIN could not be verified against MCA records.';
|
||||
}
|
||||
}
|
||||
|
||||
/** Match the entity name against the list of companies from the DIN response */
|
||||
private matchCompanyName() {
|
||||
if (!this.entityName || this.companies.length === 0) return;
|
||||
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const entityNorm = normalize(this.entityName);
|
||||
|
||||
for (const company of this.companies) {
|
||||
const compNorm = normalize(company.companyName);
|
||||
// Exact match or substring containment
|
||||
if (compNorm === entityNorm || compNorm.includes(entityNorm) || entityNorm.includes(compNorm)) {
|
||||
company._matched = true;
|
||||
this.companyMatch = true;
|
||||
this.matchedCompany = company.companyName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSkip() {
|
||||
this.dialogRef.close({ skipped: true });
|
||||
}
|
||||
|
||||
onRetry() {
|
||||
this.verified = false;
|
||||
this.failed = false;
|
||||
this.errorOccurred = false;
|
||||
this.progress = 0;
|
||||
this.currentStep = 0;
|
||||
this.companies = [];
|
||||
this.companyMatch = false;
|
||||
this.matchedCompany = '';
|
||||
}
|
||||
|
||||
onClose(verified: boolean) {
|
||||
this.dialogRef.close({
|
||||
verified,
|
||||
dinNumber: this.dinNumber,
|
||||
directorName: this.directorName,
|
||||
dinStatus: this.dinStatus,
|
||||
dinDesignation: this.dinDesignation,
|
||||
dinVerified: verified,
|
||||
companyMatch: this.companyMatch,
|
||||
matchedCompany: this.matchedCompany,
|
||||
companies: this.companies,
|
||||
response: this.apiResponse
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -38,7 +38,7 @@ $muted: #64748b;
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ $muted: #64748b;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.addr-grid--2, .addr-grid--3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -305,23 +305,85 @@
|
||||
<mat-datepicker #incorpDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- GSTIN (all non-individual) -->
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>GSTIN Number</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="15-character GSTIN"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<!-- GSTIN (all non-individual) with Verify -->
|
||||
<div class="form-field verify-field">
|
||||
<mat-form-field appearance="outline" class="verify-input">
|
||||
<mat-label>GSTIN Number</mat-label>
|
||||
<span matPrefix class="field-prefix-icon"><mat-icon>receipt_long</mat-icon></span>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="15-character GSTIN"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'FAILED') {
|
||||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="verify-action">
|
||||
@if (gstinVerifyStatus === 'NONE' || gstinVerifyStatus === 'FAILED') {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="verify-btn-inline"
|
||||
[disabled]="
|
||||
readOnly ||
|
||||
!kycForm.get('gstinNumber')?.value ||
|
||||
kycForm.get('gstinNumber')?.value.length < 15 ||
|
||||
gstinVerifying ||
|
||||
!clientId
|
||||
"
|
||||
(click)="verifyGstin()"
|
||||
>
|
||||
@if (gstinVerifying) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<fa-icon icon="shield-alt"></fa-icon> Verify GSTIN
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<div class="pan-result-card full-width">
|
||||
<div class="pan-result-header">
|
||||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||||
<span>GSTIN Verified Successfully</span>
|
||||
</div>
|
||||
<div class="pan-result-body">
|
||||
@if (gstinVerifyName) {
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">Trade Name / Legal Name</span>
|
||||
<span class="pan-result-value name">{{ gstinVerifyName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (gstinVerifyState) {
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">State</span>
|
||||
<span class="pan-result-value">{{ gstinVerifyState }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">Verified On</span>
|
||||
<span class="pan-result-value date">{{ gstinVerifiedOn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PAN — Proprietorship uses Individual PAN, others use Enterprise PAN -->
|
||||
@if (constitutionMeta?.value === 'PROPRIETORSHIP') {
|
||||
|
||||
@@ -263,6 +263,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
entPanVerifyName = '';
|
||||
entPanVerifiedOn = '';
|
||||
|
||||
/** GSTIN verification */
|
||||
gstinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
gstinVerifying = false;
|
||||
gstinVerifyName = '';
|
||||
gstinVerifiedOn = '';
|
||||
gstinVerifyState = '';
|
||||
|
||||
/** CIN verification (dummy) */
|
||||
cinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
cinVerifying = false;
|
||||
@@ -480,6 +487,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (data.panAadhaarSeedingStatus) {
|
||||
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
|
||||
}
|
||||
if (data.gstinVerified) {
|
||||
this.gstinVerifyStatus = 'VERIFIED';
|
||||
this.gstinVerifyName = data.gstinTradeName || data.gstinLegalName || '';
|
||||
this.gstinVerifyState = data.gstinState || '';
|
||||
const rawGstinDate = data.gstinVerifiedOn;
|
||||
if (Array.isArray(rawGstinDate) && rawGstinDate.length >= 3) {
|
||||
this.gstinVerifiedOn = `${rawGstinDate[2]?.toString().padStart(2, '0')}/${rawGstinDate[1]?.toString().padStart(2, '0')}/${rawGstinDate[0]}`;
|
||||
} else if (rawGstinDate) {
|
||||
this.gstinVerifiedOn = rawGstinDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load existing photo & signature
|
||||
this.loadExistingPhoto();
|
||||
@@ -772,6 +790,68 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify GSTIN via API */
|
||||
verifyGstin() {
|
||||
if (!this.clientId) return;
|
||||
const gstin = this.kycForm.value.gstinNumber;
|
||||
if (!gstin || this.kycForm.get('gstinNumber')?.invalid) return;
|
||||
|
||||
this.gstinVerifying = true;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
gstinNumber: gstin,
|
||||
dateFormat,
|
||||
panNumber: this.kycForm.value.enterprisePan || this.kycForm.value.panNumber || null,
|
||||
aadhaarNumberMasked: 'XXXXXXXX0000'
|
||||
};
|
||||
|
||||
this.saveKycAndDo(
|
||||
payload,
|
||||
() => {
|
||||
this.clientsService.verifyGSTIN(this.clientId, this.kycId).subscribe({
|
||||
next: (res: any) => {
|
||||
this.gstinVerifying = false;
|
||||
const changes = res?.changes || res;
|
||||
if (changes?.gstinVerified === true) {
|
||||
this.gstinVerifyStatus = 'VERIFIED';
|
||||
this.gstinVerifyName = changes?.gstinTradeName || changes?.gstinLegalName || '';
|
||||
this.gstinVerifyState = changes?.gstinState || '';
|
||||
const rawDate = changes?.gstinVerifiedOn;
|
||||
if (Array.isArray(rawDate)) {
|
||||
this.gstinVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
|
||||
} else {
|
||||
const now = new Date();
|
||||
this.gstinVerifiedOn =
|
||||
rawDate ||
|
||||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||
}
|
||||
} else {
|
||||
this.gstinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
changes?.gstinVerificationMessage || 'GSTIN verification failed. Please check the GSTIN number and try again.'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.gstinVerifying = false;
|
||||
this.gstinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'GSTIN verification failed.'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
(msg) => {
|
||||
this.gstinVerifying = false;
|
||||
this.showPanErrorAlert(msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify CIN (dummy — no real API) */
|
||||
verifyCin() {
|
||||
const cin = this.kycForm.value.cinNumber;
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ $navy: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
.crop-workspace {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
+21
-21
@@ -32,7 +32,7 @@
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
@@ -96,9 +96,9 @@
|
||||
font-weight: 500;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
background: rgb(255 255 255 / 70%);
|
||||
color: #555;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
|
||||
.chip-icon {
|
||||
font-size: 10px;
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
/* ── Info Cards ── */
|
||||
.info-card {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
@@ -132,7 +132,7 @@
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 6%);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
@@ -177,8 +177,8 @@
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 4%);
|
||||
border-right: 1px solid rgb(0 0 0 / 4%);
|
||||
|
||||
&:nth-child(3n) {
|
||||
border-right: none;
|
||||
@@ -195,7 +195,7 @@
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.preview-value {
|
||||
@@ -204,7 +204,7 @@
|
||||
color: #1a1a1a;
|
||||
|
||||
&.no-data {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
color: rgb(0 0 0 / 30%);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -239,7 +239,7 @@
|
||||
border-collapse: collapse;
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 4%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -254,7 +254,7 @@
|
||||
.info-label {
|
||||
width: 40%;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
color: rgb(0 0 0 / 50%);
|
||||
font-weight: 500;
|
||||
padding-right: 16px;
|
||||
}
|
||||
@@ -278,17 +278,17 @@
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
color: rgb(0 0 0 / 55%);
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 4%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -349,7 +349,7 @@
|
||||
.address-block {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px dashed rgb(0 0 0 / 8%);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -381,7 +381,7 @@
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px 0 4px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -394,23 +394,23 @@
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 960px) {
|
||||
@media (width <= 960px) {
|
||||
.preview-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.preview-field {
|
||||
&:nth-child(3n) { border-right: 1px solid rgba(0, 0, 0, 0.04); }
|
||||
&:nth-child(3n) { border-right: 1px solid rgb(0 0 0 / 4%); }
|
||||
&:nth-child(2n) { border-right: none; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.profile-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user