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
@@ -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) {
|
||||
@@ -444,6 +454,231 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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