@@ -27,10 +27,17 @@
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
||||
<fa-icon [icon]="isEditMode ? 'times' : 'edit'" class="m-r-10"></fa-icon>
|
||||
{{ isEditMode ? ('labels.buttons.Cancel' | translate) : ('labels.buttons.Edit' | translate) }}
|
||||
</button>
|
||||
<div class="kyc-header-actions">
|
||||
@if (!isEditMode && kycData) {
|
||||
<button mat-stroked-button class="kyc-print-btn" type="button" (click)="printSelfDeclaration()">
|
||||
<fa-icon icon="file-pdf" class="m-r-6"></fa-icon> Self Declaration
|
||||
</button>
|
||||
}
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
||||
<fa-icon [icon]="isEditMode ? 'times' : 'edit'" class="m-r-10"></fa-icon>
|
||||
{{ isEditMode ? ('labels.buttons.Cancel' | translate) : ('labels.buttons.Edit' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KYC Summary (read-only when not in edit mode) -->
|
||||
@@ -58,12 +65,22 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber" (click)="verifyPAN()">
|
||||
@if (verifying.pan) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Re-Verify }
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber"
|
||||
(click)="verifyPAN()"
|
||||
>
|
||||
@if (verifying.pan) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Re-Verify
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -81,11 +98,18 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
@if (!showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked" (click)="startAadhaarReVerify()">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked"
|
||||
(click)="startAadhaarReVerify()"
|
||||
>
|
||||
Re-Verify
|
||||
</button>
|
||||
}
|
||||
@@ -106,12 +130,22 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber" (click)="verifyGSTIN()">
|
||||
@if (verifying.gstin) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Re-Verify }
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber"
|
||||
(click)="verifyGSTIN()"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Re-Verify
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -131,7 +165,9 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
}
|
||||
@@ -195,12 +231,27 @@
|
||||
<div class="kv-otp-bar">
|
||||
<mat-form-field appearance="outline" class="kv-otp-input">
|
||||
<mat-label>Enter 12-digit Aadhaar</mat-label>
|
||||
<input matInput [(ngModel)]="aadhaarFullNumber" [ngModelOptions]="{standalone: true}" maxlength="12" type="password" />
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarFullNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="kv-otp-btn"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar" (click)="sendAadhaarOtp()">
|
||||
@if (verifying.aadhaar) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Send OTP }
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="kv-otp-btn"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
@if (verifying.aadhaar) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Send OTP
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -209,32 +260,69 @@
|
||||
<span class="kv-otp-label"><mat-icon>sms</mat-icon> OTP sent</span>
|
||||
<mat-form-field appearance="outline" class="kv-otp-input">
|
||||
<mat-label>Enter OTP</mat-label>
|
||||
<input matInput [(ngModel)]="aadhaarOtp" [ngModelOptions]="{standalone: true}" maxlength="6" autocomplete="one-time-code" />
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarOtp"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="kv-otp-btn"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying" (click)="verifyAadhaarOtp()">
|
||||
@if (aadhaarOtpVerifying) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Verify }
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="kv-otp-btn"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
||||
(click)="verifyAadhaarOtp()"
|
||||
>
|
||||
@if (aadhaarOtpVerifying) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Verify
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="kv-otp-btn"
|
||||
[disabled]="verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
<button mat-stroked-button type="button" class="kv-otp-btn" [disabled]="verifying.aadhaar" (click)="sendAadhaarOtp()">Resend</button>
|
||||
</div>
|
||||
}
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg"><mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}</div>
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}
|
||||
</div>
|
||||
}
|
||||
@if (verificationError.pan) {
|
||||
<div class="kyc-error-msg"><mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.pan }}</div>
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.pan }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- KYC Meta Grid -->
|
||||
<div class="kv-meta-grid">
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">KYC Type</span>
|
||||
<span class="kv-meta-value">{{ kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' }}</span>
|
||||
<span class="kv-meta-value">{{
|
||||
kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Risk Category</span>
|
||||
<span class="kv-meta-value">{{ kycData.riskCategory === 1 ? 'Low' : kycData.riskCategory === 2 ? 'Medium' : kycData.riskCategory === 3 ? 'High' : '—' }}</span>
|
||||
<span class="kv-meta-value">{{
|
||||
kycData.riskCategory === 1
|
||||
? 'Low'
|
||||
: kycData.riskCategory === 2
|
||||
? 'Medium'
|
||||
: kycData.riskCategory === 3
|
||||
? 'High'
|
||||
: '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Initiated On</span>
|
||||
@@ -255,6 +343,105 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Re-KYC: Upload Signed Declaration & Complete -->
|
||||
<div class="rekyc-section">
|
||||
<div class="rekyc-header">
|
||||
<div class="rekyc-icon-wrap">
|
||||
<fa-icon icon="sync-alt" class="rekyc-icon"></fa-icon>
|
||||
</div>
|
||||
<div class="rekyc-header-text">
|
||||
<h4 class="rekyc-title">Re-KYC Completion</h4>
|
||||
<p class="rekyc-desc">Print self-declaration, get it signed, upload & complete re-KYC.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1 -->
|
||||
<div class="rekyc-timeline">
|
||||
<div class="rekyc-tl-item">
|
||||
<div class="rekyc-tl-dot">
|
||||
<span class="rekyc-tl-num">1</span>
|
||||
<div class="rekyc-tl-line"></div>
|
||||
</div>
|
||||
<div class="rekyc-tl-content">
|
||||
<div class="rekyc-tl-label">Download Self Declaration Form</div>
|
||||
<div class="rekyc-tl-sub">System-generated PDF with PAN, Aadhaar & KYC details</div>
|
||||
<button mat-flat-button class="rekyc-btn-print" type="button" (click)="printSelfDeclaration()">
|
||||
<fa-icon icon="file-pdf"></fa-icon> Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="rekyc-tl-item">
|
||||
<div class="rekyc-tl-dot">
|
||||
<span class="rekyc-tl-num">2</span>
|
||||
<div class="rekyc-tl-line"></div>
|
||||
</div>
|
||||
<div class="rekyc-tl-content">
|
||||
<div class="rekyc-tl-label">Upload Signed Declaration</div>
|
||||
<div class="rekyc-tl-sub">Get the form signed by the client and upload the scanned copy</div>
|
||||
<label class="rekyc-upload-zone" [class.has-file]="selectedDeclarationFile">
|
||||
<input type="file" accept=".pdf,.jpg,.jpeg,.png" (change)="onDeclarationFileSelected($event)" hidden />
|
||||
@if (!selectedDeclarationFile) {
|
||||
<mat-icon class="upload-zone-icon">cloud_upload</mat-icon>
|
||||
<span class="upload-zone-text">Click to upload signed form</span>
|
||||
<span class="upload-zone-hint">PDF, JPG or PNG</span>
|
||||
} @else {
|
||||
<fa-icon icon="check-circle" class="upload-zone-check"></fa-icon>
|
||||
<span class="upload-zone-filename">{{ selectedDeclarationFile.name }}</span>
|
||||
<span class="upload-zone-hint">Click to change file</span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="rekyc-tl-item rekyc-tl-item--last">
|
||||
<div class="rekyc-tl-dot">
|
||||
<span class="rekyc-tl-num">3</span>
|
||||
</div>
|
||||
<div class="rekyc-tl-content">
|
||||
<div class="rekyc-tl-label">Complete Re-KYC</div>
|
||||
<div class="rekyc-tl-sub">Updates Last Verified date and sets new Expiry Date as per RBI norms</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="rekyc-btn-complete"
|
||||
type="button"
|
||||
[disabled]="!selectedDeclarationFile || reKycCompleting || !kycId"
|
||||
(click)="completeReKyc()"
|
||||
>
|
||||
@if (reKycCompleting) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
Completing...
|
||||
} @else {
|
||||
<fa-icon icon="check-circle"></fa-icon> Complete Re-KYC
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (reKycSuccess) {
|
||||
<div class="rekyc-result rekyc-result--success">
|
||||
<fa-icon icon="check-circle" class="rekyc-result-icon"></fa-icon>
|
||||
<div>
|
||||
<strong>Re-KYC completed successfully</strong>
|
||||
<p>Last Verified and Expiry Date have been updated.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (reKycError) {
|
||||
<div class="rekyc-result rekyc-result--error">
|
||||
<mat-icon class="rekyc-result-icon">error_outline</mat-icon>
|
||||
<div>
|
||||
<strong>Re-KYC failed</strong>
|
||||
<p>{{ reKycError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- No KYC Data State -->
|
||||
|
||||
@@ -164,6 +164,24 @@
|
||||
color: #757575;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.linked {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&.not-linked {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.exempt {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.status-chip-icon {
|
||||
@@ -375,26 +393,6 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
&.linked {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&.not-linked {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.exempt {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.pan-aadhaar-detail {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
@@ -468,10 +466,17 @@
|
||||
padding: 0;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { background: #f8fafc; }
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--link { background: #fafbfc; }
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&--link {
|
||||
background: #fafbfc;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-table__col {
|
||||
@@ -486,10 +491,25 @@
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
&--num { flex: 1; }
|
||||
&--status { flex: 0 0 150px; }
|
||||
&--date { flex: 0 0 150px; color: #64748b; font-size: 12px; }
|
||||
&--action { flex: 0 0 110px; text-align: right; }
|
||||
|
||||
&--num {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--status {
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
|
||||
&--date {
|
||||
flex: 0 0 150px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--action {
|
||||
flex: 0 0 110px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-doc-icon {
|
||||
@@ -520,10 +540,25 @@
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
&--ok { background: #ecfdf5; color: #059669; }
|
||||
&--pending { background: #f8fafc; color: #94a3b8; }
|
||||
&--fail { background: #fef2f2; color: #dc2626; }
|
||||
&--info { background: #eff6ff; color: #2563eb; }
|
||||
&--ok {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&--fail {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-action-btn {
|
||||
@@ -558,14 +593,21 @@
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
mat-icon { font-size: 16px !important; width: 16px !important; height: 16px !important; }
|
||||
mat-icon {
|
||||
font-size: 16px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-otp-input {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix { padding: 6px 0 !important; min-height: 36px !important; }
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 6px 0 !important;
|
||||
min-height: 36px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-otp-btn {
|
||||
@@ -594,8 +636,14 @@
|
||||
border-right: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:nth-child(5n) { border-right: none; }
|
||||
&--full { grid-column: 1 / -1; border-right: none; }
|
||||
&:nth-child(5n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-meta-label {
|
||||
@@ -612,20 +660,332 @@
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
@media (width <= 960px) {
|
||||
.kv-table__col {
|
||||
&--doc { flex: 0 0 120px; }
|
||||
&--date { display: none; }
|
||||
&--action { flex: 0 0 90px; }
|
||||
&--doc {
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
&--date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--action {
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-meta-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.kv-meta-item:nth-child(5n) {
|
||||
border-right: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.kv-meta-item:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
.kv-meta-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.kv-meta-item:nth-child(5n) { border-right: 1px solid #f1f5f9; }
|
||||
.kv-meta-item:nth-child(3n) { border-right: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.kv-table__row { flex-wrap: wrap; }
|
||||
.kv-table__col { &--status, &--action { flex: 0 0 50%; } }
|
||||
.kv-meta-grid { grid-template-columns: 1fr 1fr; }
|
||||
.kv-otp-bar { flex-wrap: wrap; }
|
||||
@media (width <= 640px) {
|
||||
.kv-table__row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kv-table__col {
|
||||
&--status,
|
||||
&--action {
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-meta-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.kv-otp-bar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Header Actions ─── */
|
||||
.kyc-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.kyc-print-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #1074b9 !important;
|
||||
border-color: #93c5fd !important;
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ─── Re-KYC Section ─── */
|
||||
.rekyc-section {
|
||||
margin-top: 20px;
|
||||
background: linear-gradient(135deg, #fafbff 0%, #f0f4ff 100%);
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.rekyc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.rekyc-icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #1074b9, #0c5da0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rekyc-icon {
|
||||
font-size: 17px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rekyc-header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rekyc-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.rekyc-desc {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ─── Timeline Layout ─── */
|
||||
.rekyc-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.rekyc-tl-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.rekyc-tl-dot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.rekyc-tl-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #1074b9;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rekyc-tl-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
background: #c7d8ed;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.rekyc-tl-item--last .rekyc-tl-dot {
|
||||
.rekyc-tl-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rekyc-tl-content {
|
||||
flex: 1;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.rekyc-tl-item--last .rekyc-tl-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.rekyc-tl-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rekyc-tl-sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ─── Buttons ─── */
|
||||
.rekyc-btn-print {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #1074b9 !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #bfdbfe !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 6px 16px !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff !important;
|
||||
border-color: #93c5fd !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rekyc-btn-complete {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 7px 20px !important;
|
||||
}
|
||||
|
||||
/* ─── Upload Zone ─── */
|
||||
.rekyc-upload-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 16px 24px;
|
||||
max-width: 280px;
|
||||
background: #fff;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
&.has-file {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-zone-icon {
|
||||
font-size: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.upload-zone-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.upload-zone-hint {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.upload-zone-check {
|
||||
font-size: 24px;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.upload-zone-filename {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
max-width: 230px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ─── Result Banners ─── */
|
||||
.rekyc-result {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
|
||||
p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
|
||||
.rekyc-result-icon {
|
||||
color: #059669;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
|
||||
.rekyc-result-icon {
|
||||
color: #dc2626;
|
||||
font-size: 20px !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.rekyc-upload-zone {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { catchError, of, Subject, takeUntil } from 'rxjs';
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
@@ -51,6 +52,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
clientId: string;
|
||||
kycId: string | null = null;
|
||||
kycData: any = null;
|
||||
clientData: any = null;
|
||||
clientDateOfBirth: string | null = null;
|
||||
isNonIndividual = false;
|
||||
loading = true;
|
||||
@@ -58,6 +60,13 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
isEditMode = false;
|
||||
errorMessage: string | null = null;
|
||||
|
||||
/** Re-KYC flow state */
|
||||
reKycUploading = false;
|
||||
reKycCompleting = false;
|
||||
reKycSuccess = false;
|
||||
reKycError: string | null = null;
|
||||
selectedDeclarationFile: File | null = null;
|
||||
|
||||
verifying: { pan: boolean; aadhaar: boolean; gstin: boolean } = {
|
||||
pan: false,
|
||||
aadhaar: false,
|
||||
@@ -154,6 +163,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
/** Load client's date of birth and legal form from parent route resolved data */
|
||||
loadClientDateOfBirth(): void {
|
||||
this.route.parent.data.subscribe((data: any) => {
|
||||
this.clientData = data?.clientViewData || null;
|
||||
if (data?.clientViewData?.dateOfBirth) {
|
||||
const dob = data.clientViewData.dateOfBirth;
|
||||
if (Array.isArray(dob) && dob.length >= 3) {
|
||||
@@ -437,13 +447,264 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
|
||||
getPanAadhaarStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'Y': return 'Linked (Seeded)';
|
||||
case 'N': return 'Not Linked';
|
||||
case 'E': return 'Exempt';
|
||||
default: return 'Pending Verification';
|
||||
case 'Y':
|
||||
return 'Linked (Seeded)';
|
||||
case 'N':
|
||||
return 'Not Linked';
|
||||
case 'E':
|
||||
return 'Exempt';
|
||||
default:
|
||||
return 'Pending Verification';
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate Self Declaration PDF with KYC details */
|
||||
printSelfDeclaration(): void {
|
||||
const client = this.clientData || {};
|
||||
const kyc = this.kycData || {};
|
||||
const clientName = client.displayName || client.fullname || '';
|
||||
const cifNumber = client.accountNo || client.cifNumber || this.clientId;
|
||||
const dob = this.clientDateOfBirth ? this.formatDate(client.dateOfBirth) : '—';
|
||||
const officeName = client.officeName || '';
|
||||
const today = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'long', year: 'numeric' });
|
||||
|
||||
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const margin = 20;
|
||||
const contentWidth = pageWidth - margin * 2;
|
||||
let y = 20;
|
||||
|
||||
// Helper
|
||||
const addLine = (label: string, value: string, yPos: number): number => {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(10);
|
||||
doc.text(label, margin, yPos);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(value || '—', margin + 55, yPos);
|
||||
return yPos + 7;
|
||||
};
|
||||
|
||||
const drawHr = (yPos: number): number => {
|
||||
doc.setDrawColor(200, 200, 200);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(margin, yPos, pageWidth - margin, yPos);
|
||||
return yPos + 5;
|
||||
};
|
||||
|
||||
// ── Header ──
|
||||
doc.setFillColor(15, 23, 42);
|
||||
doc.rect(0, 0, pageWidth, 30, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(16);
|
||||
doc.text('Self Declaration Form', pageWidth / 2, 13, { align: 'center' });
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('KYC / Re-KYC Compliance — As per RBI Master Direction on KYC', pageWidth / 2, 21, { align: 'center' });
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
y = 40;
|
||||
|
||||
// ── Branch / Date ──
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`Branch: ${officeName}`, margin, y);
|
||||
doc.text(`Date: ${today}`, pageWidth - margin, y, { align: 'right' });
|
||||
y += 8;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Section: Customer Details ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('Customer Details', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
y = addLine('Customer Name:', clientName, y);
|
||||
y = addLine('CIF Number:', cifNumber, y);
|
||||
y = addLine('Date of Birth:', dob, y);
|
||||
if (client.mobileNo) y = addLine('Mobile:', client.mobileNo, y);
|
||||
if (client.emailAddress) y = addLine('Email:', client.emailAddress, y);
|
||||
y += 3;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Section: KYC Documents ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('KYC Document Details', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
// PAN
|
||||
y = addLine('PAN Number:', kyc.panNumber || '—', y);
|
||||
y = addLine('PAN Status:', kyc.panVerified ? 'Verified' : 'Pending', y);
|
||||
if (kyc.panVerifiedOn) y = addLine('PAN Verified On:', this.formatDate(kyc.panVerifiedOn), y);
|
||||
|
||||
// Aadhaar
|
||||
y = addLine('Aadhaar Number:', kyc.aadhaarNumberMasked || '—', y);
|
||||
y = addLine('Aadhaar Status:', kyc.aadhaarVerified ? 'Verified' : 'Pending', y);
|
||||
if (kyc.aadhaarVerifiedOn) y = addLine('Aadhaar Verified On:', this.formatDate(kyc.aadhaarVerifiedOn), y);
|
||||
|
||||
// PAN-Aadhaar Link
|
||||
const seedingStatus =
|
||||
kyc.panAadhaarSeedingStatus === 'Y'
|
||||
? 'Linked'
|
||||
: kyc.panAadhaarSeedingStatus === 'N'
|
||||
? 'Not Linked'
|
||||
: kyc.panAadhaarSeedingStatus === 'E'
|
||||
? 'Exempt'
|
||||
: 'Pending';
|
||||
y = addLine('PAN-Aadhaar Link:', seedingStatus, y);
|
||||
|
||||
// GSTIN (Non-Individual)
|
||||
if (this.isNonIndividual && kyc.gstinNumber) {
|
||||
y = addLine('GSTIN:', kyc.gstinNumber, y);
|
||||
y = addLine('GSTIN Status:', kyc.gstinVerified ? 'Verified' : 'Pending', y);
|
||||
}
|
||||
|
||||
// Enterprise PAN / CIN
|
||||
if (this.isNonIndividual && kyc.enterprisePan) {
|
||||
y = addLine('Enterprise PAN:', kyc.enterprisePan, y);
|
||||
}
|
||||
if (this.isNonIndividual && kyc.cinNumber) {
|
||||
y = addLine('CIN:', kyc.cinNumber, y);
|
||||
}
|
||||
|
||||
y += 3;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Section: KYC Summary ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('KYC Summary', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
y = addLine('KYC Type:', kyc.kycType === 1 ? 'Individual' : kyc.kycType === 2 ? 'Non-Individual' : '—', y);
|
||||
y = addLine(
|
||||
'Risk Category:',
|
||||
kyc.riskCategory === 1 ? 'Low' : kyc.riskCategory === 2 ? 'Medium' : kyc.riskCategory === 3 ? 'High' : '—',
|
||||
y
|
||||
);
|
||||
y = addLine('Initiated On:', this.formatDate(kyc.initiatedOn), y);
|
||||
y = addLine('Last Verified:', this.formatDate(kyc.verifiedOn), y);
|
||||
y = addLine('Expiry Date:', this.formatDate(kyc.expiryDate), y);
|
||||
|
||||
y += 3;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Declaration Text ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('Declaration', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9.5);
|
||||
const declarationText = `I, ${clientName || '_______________'}, hereby declare that the information provided above is true, correct and complete to the best of my knowledge and belief. I understand that furnishing false information is punishable under applicable laws. I authorize the Bank to verify the above details through appropriate channels as per RBI Master Direction on KYC (DBR.AML.BC.No.81/14.01.001/2015-16) and its subsequent amendments.
|
||||
|
||||
I further confirm that:
|
||||
1. I am not a Politically Exposed Person (PEP) / relative of a PEP (unless declared otherwise).
|
||||
2. The source of funds in my account is from legitimate sources.
|
||||
3. I shall promptly notify the Bank of any change in the above information.
|
||||
4. I have read and understood the Bank's terms and conditions regarding KYC compliance.`;
|
||||
|
||||
const lines = doc.splitTextToSize(declarationText, contentWidth);
|
||||
doc.text(lines, margin, y);
|
||||
y += lines.length * 4.5 + 10;
|
||||
|
||||
// ── Signature section ──
|
||||
y = drawHr(y);
|
||||
y += 5;
|
||||
|
||||
// Left: Client signature
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9);
|
||||
doc.text('________________________', margin, y + 15);
|
||||
doc.text('Signature of Customer', margin, y + 21);
|
||||
doc.text(`Name: ${clientName}`, margin, y + 27);
|
||||
doc.text(`Date: _______________`, margin, y + 33);
|
||||
doc.text(`Place: _______________`, margin, y + 39);
|
||||
|
||||
// Right: Bank official
|
||||
doc.text('________________________', pageWidth - margin - 50, y + 15);
|
||||
doc.text('Authorized Bank Official', pageWidth - margin - 50, y + 21);
|
||||
doc.text('Name: _______________', pageWidth - margin - 50, y + 27);
|
||||
doc.text('Designation: _______________', pageWidth - margin - 50, y + 33);
|
||||
doc.text('Stamp & Seal', pageWidth - margin - 50, y + 39);
|
||||
|
||||
// ── Footer ──
|
||||
const footerY = doc.internal.pageSize.getHeight() - 10;
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text('This is a system-generated document. Generated from CBS Nexora — KYC Module.', pageWidth / 2, footerY, {
|
||||
align: 'center'
|
||||
});
|
||||
|
||||
// Save
|
||||
doc.save(`Self_Declaration_${cifNumber}_${clientName.replace(/\s/g, '_')}.pdf`);
|
||||
}
|
||||
|
||||
/** Handle file selection for signed declaration upload */
|
||||
onDeclarationFileSelected(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files?.length) {
|
||||
this.selectedDeclarationFile = input.files[0];
|
||||
this.reKycError = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Upload signed declaration and complete re-KYC */
|
||||
completeReKyc(): void {
|
||||
if (!this.selectedDeclarationFile || !this.clientId || !this.kycId) return;
|
||||
|
||||
this.reKycCompleting = true;
|
||||
this.reKycError = null;
|
||||
this.reKycSuccess = false;
|
||||
|
||||
// Step 1: Upload the signed declaration as a client document
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedDeclarationFile);
|
||||
formData.append('name', 'KYC Self Declaration');
|
||||
formData.append(
|
||||
'description',
|
||||
`Signed self-declaration form uploaded on ${new Date().toLocaleDateString('en-IN')}`
|
||||
);
|
||||
|
||||
this.clientsService.uploadClientDocument(this.clientId, formData).subscribe({
|
||||
next: () => {
|
||||
// Step 2: Call re-KYC completion API to update verified/expiry dates
|
||||
this.clientsService.completeReKyc(this.clientId, this.kycId!).subscribe({
|
||||
next: () => {
|
||||
this.reKycCompleting = false;
|
||||
this.reKycSuccess = true;
|
||||
this.selectedDeclarationFile = null;
|
||||
this.loadKycData(); // Refresh to show updated dates
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.reKycCompleting = false;
|
||||
this.reKycError =
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
'Failed to complete re-KYC. Please try again.';
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.reKycCompleting = false;
|
||||
this.reKycError =
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
'Failed to upload declaration. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
Reference in New Issue
Block a user