feat: edit client — save per tab, DOB re-KYC alert, audit logs, KYC improvements

- Edit button per tab with Save & Next confirmation dialog
- DOB change triggers re-KYC alert, resets PAN verification, auto-fills remarks
- Mobile/email changes logged as audit notes via client notes API
- Redesigned confirmation dialog with icon + title, type-based theming
- KYC re-edit flow: remarks mandatory for VERIFIED records, re-edit API command
- Load existing photo & signature in edit KYC, immediate upload on change
- Populate all KYC fields (document, AML, form60/15) from API on edit
- Non-individual PAN verification uses same flow as individual
- Fix table overflow when sidebar panel is open
- Add fr unit to stylelint allowed-list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-07 15:08:07 +05:30
co-authored by Claude Opus 4.6
parent b890d24bb7
commit 79e455c4ed
14 changed files with 1699 additions and 398 deletions
@@ -1,5 +1,9 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
@@ -54,77 +58,88 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
kycId: string = '';
/** Whether existing KYC data has been loaded (edit mode) */
kycLoaded = false;
/** KYC status from API: 100=PENDING, 200=IN_PROGRESS, 300=VERIFIED */
kycStatus: number = 0;
/** Whether the KYC record is in VERIFIED state (needs re-edit command) */
get isKycVerified(): boolean {
return this.kycStatus === 300;
}
/** KYC Document Types — populated from API template, falls back to defaults */
kycDocumentTypes: { name: string; code: string; mandatory: boolean }[] = [];
/** Fallback document types when template API is unavailable */
private defaultDocumentTypes = [
'Aadhaar Card', 'Passport', 'PAN Card', 'Driving Licence',
'Voter ID', 'NREGA Job Card', 'National Population Register Letter'
'Aadhaar Card',
'Passport',
'PAN Card',
'Driving Licence',
'Voter ID',
'NREGA Job Card',
'National Population Register Letter'
];
/** Constitution-based KYC document types for non-individual clients */
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
'HUF': [
HUF: [
{ name: 'PAN Card of HUF', mandatory: true },
{ name: 'Declaration by Karta (with name, address & HUF member details)', mandatory: true },
{ name: 'KYC of Karta (OVDs + PAN + Photo)', mandatory: true },
{ name: 'Proof of Address of HUF', mandatory: false }
],
'PRIVATE_LIMITED': [
PRIVATE_LIMITED: [
{ name: 'Certificate of Incorporation', mandatory: true },
{ name: 'Memorandum & Articles of Association', mandatory: true },
{ name: 'PAN of Company', mandatory: true },
{ name: 'Board Resolution (authorizing signatories)', mandatory: true },
{ name: 'KYC of Authorized Signatories', mandatory: true }
],
'PUBLIC_LIMITED': [
PUBLIC_LIMITED: [
{ name: 'Certificate of Incorporation', mandatory: true },
{ name: 'Memorandum & Articles of Association', mandatory: true },
{ name: 'PAN of Company', mandatory: true },
{ name: 'Board Resolution (authorizing signatories)', mandatory: true },
{ name: 'KYC of Authorized Signatories', mandatory: true }
],
'LLP': [
LLP: [
{ name: 'Certificate of Incorporation', mandatory: true },
{ name: 'LLP Agreement', mandatory: true },
{ name: 'PAN of LLP', mandatory: true },
{ name: 'Designated Partners Resolution', mandatory: true },
{ name: 'KYC of Designated Partners / Authorized Signatories', mandatory: true }
],
'PARTNERSHIP': [
PARTNERSHIP: [
{ name: 'Registration Certificate', mandatory: false },
{ name: 'Partnership Deed', mandatory: true },
{ name: 'PAN of Firm', mandatory: true },
{ name: 'KYC of Authorized Signatories', mandatory: true }
],
'TRUST': [
TRUST: [
{ name: 'Registration Certificate', mandatory: true },
{ name: 'Trust Deed', mandatory: true },
{ name: 'PAN of Trust', mandatory: true },
{ name: 'KYC of Trustees / Authorized Signatories', mandatory: true }
],
'SOCIETY': [
SOCIETY: [
{ name: 'Registration Certificate', mandatory: true },
{ name: 'PAN of Society', mandatory: true },
{ name: 'KYC of Authorized Signatories', mandatory: true }
],
'PROPRIETORSHIP': [
PROPRIETORSHIP: [
{ name: 'Proof of Business Existence (GST Certificate / Shop License)', mandatory: true },
{ name: 'PAN of Proprietor', mandatory: true },
{ name: "Proprietor's KYC (OVDs + PAN + Photo)", mandatory: true }
],
'ASSOCIATION': [
ASSOCIATION: [
{ name: 'Registration Certificate', mandatory: true },
{ name: 'PAN', mandatory: true },
{ name: 'Resolution authorizing signatories', mandatory: true },
{ name: 'KYC of Authorized Signatories', mandatory: true }
],
'GOVT_PSU': [
GOVT_PSU: [
{ name: 'Government Notification / Order', mandatory: true },
{ name: 'PAN (if applicable)', mandatory: false },
{ name: 'Authorized Signatory KYC', mandatory: true }
],
'COOPERATIVE': [
COOPERATIVE: [
{ name: 'Registration Certificate', mandatory: true },
{ name: 'PAN', mandatory: true },
{ name: 'Resolution authorizing signatories', mandatory: true },
@@ -161,7 +176,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
aadhaarOtpReferenceId = '';
/** OTP digit boxes */
otpDigits: string[] = ['', '', '', '', '', ''];
otpDigits: string[] = [
'',
'',
'',
'',
'',
''
];
/** Resend countdown */
resendCountdown = 0;
private resendTimer: any = null;
@@ -221,7 +243,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
}
resetOtpDigits(): void {
this.otpDigits = ['', '', '', '', '', ''];
this.otpDigits = [
'',
'',
'',
'',
'',
''
];
this.aadhaarOtp = '';
}
@@ -254,25 +283,73 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
photographPreview: string | null = null;
signatureFile: File | null = null;
signaturePreview: string | null = null;
/** Existing signature document ID (for re-upload/delete) */
existingSignatureDocId: string = '';
/** KYC Form */
kycForm: UntypedFormGroup = this.formBuilder.group({
hasPan: [true],
panNumber: ['', [Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/), Validators.maxLength(10)]],
aadhaarNumber: ['', [Validators.pattern(/^\d{12}$/), Validators.maxLength(12)]],
panNumber: [
'',
[
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/),
Validators.maxLength(10)
]
],
aadhaarNumber: [
'',
[
Validators.pattern(/^\d{12}$/),
Validators.maxLength(12)
]
],
panAadhaarSeedingStatus: [''],
gstinNumber: ['', [Validators.pattern(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/), Validators.maxLength(15)]],
enterprisePan: ['', [Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/), Validators.maxLength(10)]],
cinNumber: ['', [Validators.pattern(/^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/), Validators.maxLength(21)]],
gstinNumber: [
'',
[
Validators.pattern(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/),
Validators.maxLength(15)
]
],
enterprisePan: [
'',
[
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/),
Validators.maxLength(10)
]
],
cinNumber: [
'',
[
Validators.pattern(/^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/),
Validators.maxLength(21)
]
],
incorporationDate: [''],
kycDocumentName: ['', Validators.required],
kycDocumentNumber: ['', [Validators.required, Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/), Validators.maxLength(30)]],
kycDocumentName: [
'',
Validators.required
],
kycDocumentNumber: [
'',
[
Validators.required,
Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/),
Validators.maxLength(30)
]
],
kycDocIssueDate: [''],
kycDocExpiryDate: [''],
riskCategory: [1],
remarks: [''],
amlCheckStatus: ['CLEAR', Validators.required],
bannedEntityMatch: ['NO', Validators.required],
amlCheckStatus: [
'CLEAR',
Validators.required
],
bannedEntityMatch: [
'NO',
Validators.required
],
willfulDefaulter: ['NO'],
ecgcSalFlag: ['NO'],
dormantFlag: ['ACTIVE'],
@@ -320,7 +397,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
private updateDocTypesForConstitution(): void {
const constitutionValue = this.constitutionMeta?.value;
if (constitutionValue && this.constitutionDocTypes[constitutionValue]) {
this.kycDocumentTypes = this.constitutionDocTypes[constitutionValue].map(doc => ({
this.kycDocumentTypes = this.constitutionDocTypes[constitutionValue].map((doc) => ({
name: doc.name,
code: doc.name,
mandatory: doc.mandatory
@@ -339,13 +416,51 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.kycLoaded = true;
if (data) {
this.kycId = data.id?.toString() || '';
this.kycStatus = data.status?.id || data.kycStatus || 0;
// Helper to parse API date arrays [y,m,d] or strings into Date objects
const parseDate = (val: any): Date | string => {
if (Array.isArray(val) && val.length >= 3) {
return new Date(val[0], val[1] - 1, val[2]);
}
if (val && typeof val === 'string') {
const d = new Date(val);
return isNaN(d.getTime()) ? '' : d;
}
return '';
};
this.kycForm.patchValue({
panNumber: data.panNumber || '',
aadhaarNumber: data.aadhaarNumberMasked || '',
gstinNumber: data.gstinNumber || '',
enterprisePan: data.enterprisePan || '',
cinNumber: data.cinNumber || '',
incorporationDate: parseDate(data.incorporationDate),
// KYC Document
kycDocumentName: data.kycDocumentName || '',
kycDocumentNumber: data.kycDocumentNumber || '',
kycDocIssueDate: parseDate(data.kycDocIssueDate),
kycDocExpiryDate: parseDate(data.kycDocExpiryDate),
// Risk & Remarks
riskCategory: data.riskCategory || 1,
remarks: data.remarks || '',
hasPan: !!data.panNumber
hasPan: !!data.panNumber,
// AML & Compliance
amlCheckStatus: data.amlCheckStatus || 'CLEAR',
bannedEntityMatch: data.bannedEntityMatch || 'NO',
willfulDefaulter: data.willfulDefaulter || 'NO',
ecgcSalFlag: data.ecgcSalFlag || 'NO',
dormantFlag: data.dormantFlag || 'ACTIVE',
// Account Status & Freeze
accountStatus: data.accountStatus || 'ACTIVE',
freezeReason: data.freezeReason || '',
freezeAuthority: data.freezeAuthority || '',
// Form 60 / Form 15
form60Type: data.form60Type || 'FORM_60',
form60Date: parseDate(data.form60Date),
form15Applicable: data.form15Applicable || false,
form15Type: data.form15Type || '15G',
form15FY: data.form15FY || ''
});
// Set verification statuses from existing data
if (data.panVerified) {
@@ -366,6 +481,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
}
}
// Load existing photo & signature
this.loadExistingPhoto();
this.loadExistingSignature();
},
error: () => {
// No existing KYC — that's fine for new clients
@@ -374,6 +492,45 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
});
}
/** Fetch existing client profile photo */
private loadExistingPhoto(): void {
if (!this.clientId) return;
this.clientsService.getClientProfileImage(this.clientId).subscribe({
next: (imageData: any) => {
if (imageData) {
this.photographPreview = imageData;
}
},
error: () => {}
});
}
/** Fetch existing client signature from documents */
private loadExistingSignature(): void {
if (!this.clientId) return;
this.clientsService.getClientDocuments(this.clientId).subscribe({
next: (documents: any[]) => {
const sigDoc = documents?.find((d: any) => d.name === 'clientSignature');
if (sigDoc?.id) {
this.existingSignatureDocId = sigDoc.id.toString();
this.clientsService.getClientSignatureImage(this.clientId, this.existingSignatureDocId).subscribe({
next: async (blob: any) => {
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
this.signaturePreview = 'data:' + blob.type + ';base64,' + btoa(binary);
},
error: () => {}
});
}
},
error: () => {}
});
}
/** Load KYC template to get required documents based on client type */
private loadKycTemplate(): void {
if (!this.clientId || this.kycTemplateLoaded) return;
@@ -388,7 +545,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
mandatory: doc.mandatory ?? false
}));
// Update kycDocumentName validator based on mandatory docs
const hasMandatory = this.kycDocumentTypes.some(d => d.mandatory);
const hasMandatory = this.kycDocumentTypes.some((d) => d.mandatory);
if (hasMandatory) {
this.kycForm.get('kycDocumentName')?.setValidators(Validators.required);
}
@@ -398,7 +555,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
error: () => {
// Fallback to defaults if template API unavailable
this.kycTemplateLoaded = true;
this.kycDocumentTypes = this.defaultDocumentTypes.map(name => ({
this.kycDocumentTypes = this.defaultDocumentTypes.map((name) => ({
name,
code: name,
mandatory: false
@@ -410,19 +567,48 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
/** Save or update KYC, then execute callback */
private saveKycAndDo(payload: any, callback: () => void, onError: (msg: string) => void) {
if (this.kycId) {
// KYC already exists — use PUT to update (strip create-only fields)
const { kycType, ...updatePayload } = payload;
this.clientsService.updateClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
next: () => callback(),
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to update KYC.')
});
// If KYC is VERIFIED, remarks are mandatory — use re-edit command
if (this.isKycVerified) {
const remarks = this.kycForm.get('remarks')?.value?.trim();
if (!remarks) {
onError('Remarks are mandatory when updating a verified KYC record.');
this.kycForm.get('remarks')?.markAsTouched();
return;
}
updatePayload.remarks = remarks;
this.clientsService.reEditClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
next: () => {
// Status transitions to IN_PROGRESS, PAN verification is invalidated
this.kycStatus = 200;
this.panVerifyStatus = 'NONE';
this.panVerifyName = '';
this.panVerifiedOn = '';
callback();
},
error: (err: any) =>
onError(
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Failed to re-edit KYC.'
)
});
} else {
// Normal update for non-verified records
this.clientsService.updateClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
next: () => callback(),
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to update KYC.')
});
}
} else {
// First time — use POST to create
this.clientsService.saveClientKyc(this.clientId, payload).subscribe({
next: (res: any) => {
this.kycId = res?.resourceId?.toString() || '';
if (this.kycId) { callback(); }
else { onError('Failed to save KYC.'); }
if (this.kycId) {
callback();
} else {
onError('Failed to save KYC.');
}
},
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to save KYC.')
});
@@ -436,7 +622,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
return;
}
const pan = this.kycForm.value.panNumber;
if (!pan) { this.panVerifyError = 'Enter PAN number first.'; return; }
if (!pan) {
this.panVerifyError = 'Enter PAN number first.';
return;
}
this.panVerifying = true;
this.panVerifyError = '';
@@ -451,107 +640,136 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
aadhaarNumberMasked: aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : 'XXXXXXXX0000'
};
this.saveKycAndDo(panPayload, () => {
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
next: (res: any) => {
this.panVerifying = false;
if (res?.changes?.panVerified === true) {
// Full name from API
const resolvedName = res.changes?.panFullName
|| res.changes?.panHolderName
|| [res.changes?.panFirstName, res.changes?.panLastName].filter(Boolean).join(' ')
|| '';
this.saveKycAndDo(
panPayload,
() => {
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
next: (res: any) => {
this.panVerifying = false;
if (res?.changes?.panVerified === true) {
// Full name from API
const resolvedName =
res.changes?.panFullName ||
res.changes?.panHolderName ||
[
res.changes?.panFirstName,
res.changes?.panLastName
]
.filter(Boolean)
.join(' ') ||
'';
// If name is empty, the client name/DOB didn't match PAN holder
if (!resolvedName || !resolvedName.trim()) {
this.panVerifyStatus = 'FAILED';
this.showPanErrorAlert('PAN verification failed: Client name or date of birth does not match the PAN card holder. Please ensure the client\'s name and date of birth exactly match the PAN card details.');
return;
}
// If name is empty, the client name/DOB didn't match PAN holder
if (!resolvedName || !resolvedName.trim()) {
this.panVerifyStatus = 'FAILED';
this.showPanErrorAlert(
"PAN verification failed: Client name or date of birth does not match the PAN card holder. Please ensure the client's name and date of birth exactly match the PAN card details."
);
return;
}
this.panVerifyStatus = 'VERIFIED';
this.panVerifyName = resolvedName;
// Date — could be string or array [y,m,d]
const rawDate = res.changes?.panVerifiedOn;
if (Array.isArray(rawDate)) {
this.panVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
} else if (rawDate) {
this.panVerifiedOn = rawDate;
this.panVerifyStatus = 'VERIFIED';
this.panVerifyName = resolvedName;
// Date — could be string or array [y,m,d]
const rawDate = res.changes?.panVerifiedOn;
if (Array.isArray(rawDate)) {
this.panVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
} else if (rawDate) {
this.panVerifiedOn = rawDate;
} else {
const now = new Date();
this.panVerifiedOn = `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
}
// Seeding status
if (res.changes?.panAadhaarSeedingStatus) {
this.panAadhaarSeedingStatus = res.changes.panAadhaarSeedingStatus;
}
} else {
const now = new Date();
this.panVerifiedOn = `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
this.panVerifyStatus = 'FAILED';
this.showPanErrorAlert(res?.changes?.panVerificationMessage || 'PAN verification failed.');
}
// Seeding status
if (res.changes?.panAadhaarSeedingStatus) {
this.panAadhaarSeedingStatus = res.changes.panAadhaarSeedingStatus;
}
} else {
},
error: (err: any) => {
this.panVerifying = false;
this.panVerifyStatus = 'FAILED';
this.showPanErrorAlert(res?.changes?.panVerificationMessage || 'PAN verification failed.');
const msg =
err?.error?.errors?.[0]?.defaultUserMessage ||
err?.error?.defaultUserMessage ||
'PAN verification failed.';
this.showPanErrorAlert(msg);
}
},
error: (err: any) => {
this.panVerifying = false;
this.panVerifyStatus = 'FAILED';
const msg = err?.error?.errors?.[0]?.defaultUserMessage
|| err?.error?.defaultUserMessage
|| 'PAN verification failed.';
this.showPanErrorAlert(msg);
}
});
}, (msg) => { this.panVerifying = false; this.showPanErrorAlert(msg); });
});
},
(msg) => {
this.panVerifying = false;
this.showPanErrorAlert(msg);
}
);
}
/** Verify Enterprise PAN (non-individual) — uses same verify-pan API */
/**
* Verify Enterprise PAN (non-individual) — same flow as individual PAN verification.
* Maps: Company PAN → panNumber, Company Name → client name, Date of Incorporation → DOB
*/
verifyEnterprisePan() {
if (!this.clientId) return;
const pan = this.kycForm.value.enterprisePan;
if (!pan || this.kycForm.get('enterprisePan')?.invalid) return;
this.entPanVerifying = true;
const dateFormat = this.settingsService.dateFormat;
const incorpDate = this.kycForm.value.incorporationDate;
// Same payload structure as individual — backend uses panNumber + client name/DOB for verification
const payload: any = {
locale: 'en',
kycType: 1,
riskCategory: this.kycForm.value.riskCategory || 1,
panNumber: pan,
dateFormat: this.settingsService.dateFormat
panNumber: pan, // Company PAN → panNumber
dateFormat,
aadhaarNumberMasked: 'XXXXXXXX0000' // Placeholder — not applicable for entities
};
if (incorpDate) {
payload.dateOfIncorporation = incorpDate instanceof Date
? this.dateUtils.formatDate(incorpDate, this.settingsService.dateFormat)
: incorpDate;
}
if (this.entityName) {
payload.companyFullName = this.entityName;
}
this.saveKycAndDo(payload, () => {
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
next: (res: any) => {
this.entPanVerifying = false;
if (res?.changes?.panVerified === true) {
this.entPanVerifyStatus = 'VERIFIED';
this.entPanVerifyName = res.changes?.panFullName || res.changes?.panHolderName || '';
const rawDate = res.changes?.panVerifiedOn;
if (Array.isArray(rawDate)) {
this.entPanVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
this.saveKycAndDo(
payload,
() => {
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
next: (res: any) => {
this.entPanVerifying = false;
if (res?.changes?.panVerified === true) {
this.entPanVerifyStatus = 'VERIFIED';
this.entPanVerifyName = res.changes?.panFullName || res.changes?.panHolderName || '';
const rawDate = res.changes?.panVerifiedOn;
if (Array.isArray(rawDate)) {
this.entPanVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
} else {
const now = new Date();
this.entPanVerifiedOn =
rawDate ||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
}
} else {
const now = new Date();
this.entPanVerifiedOn = rawDate || `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
this.entPanVerifyStatus = 'FAILED';
this.showPanErrorAlert(
res?.changes?.panVerificationMessage ||
'Enterprise PAN verification failed. Ensure company name and date of incorporation match the PAN records.'
);
}
} else {
},
error: (err: any) => {
this.entPanVerifying = false;
this.entPanVerifyStatus = 'FAILED';
this.showPanErrorAlert(res?.changes?.panVerificationMessage || 'Enterprise PAN verification failed.');
this.showPanErrorAlert(
err?.error?.errors?.[0]?.defaultUserMessage || 'Enterprise PAN verification failed.'
);
}
},
error: (err: any) => {
this.entPanVerifying = false;
this.entPanVerifyStatus = 'FAILED';
this.showPanErrorAlert(err?.error?.errors?.[0]?.defaultUserMessage || 'Enterprise PAN verification failed.');
}
});
}, (msg) => { this.entPanVerifying = false; this.showPanErrorAlert(msg); });
});
},
(msg) => {
this.entPanVerifying = false;
this.showPanErrorAlert(msg);
}
);
}
/** Verify CIN (dummy — no real API) */
@@ -563,7 +781,12 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.cinProgressValue = 0;
this.cinCurrentStep = 0;
const stepDurations = [800, 1000, 1200, 800];
const stepDurations = [
800,
1000,
1200,
800
];
const totalDuration = stepDurations.reduce((a, b) => a + b, 0);
let elapsed = 0;
const interval = 50;
@@ -573,7 +796,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
let accum = 0;
for (let i = 0; i < stepDurations.length; i++) {
accum += stepDurations[i];
if (elapsed <= accum) { this.cinCurrentStep = i; break; }
if (elapsed <= accum) {
this.cinCurrentStep = i;
break;
}
}
this.cinProgressValue = Math.min(Math.round((elapsed / totalDuration) * 100), 100);
@@ -598,7 +824,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
return;
}
const aadhaar = this.kycForm.value.aadhaarNumber;
if (!aadhaar || aadhaar.length !== 12) { this.aadhaarVerifyError = 'Enter valid 12-digit Aadhaar first.'; return; }
if (!aadhaar || aadhaar.length !== 12) {
this.aadhaarVerifyError = 'Enter valid 12-digit Aadhaar first.';
return;
}
this.aadhaarVerifying = true;
this.aadhaarVerifyError = '';
@@ -611,23 +840,32 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
aadhaarNumberMasked: aadhaarMasked
};
const panVal = this.kycForm.value.panNumber;
if (panVal) { aadhaarPayload.panNumber = panVal; }
if (panVal) {
aadhaarPayload.panNumber = panVal;
}
this.saveKycAndDo(aadhaarPayload, () => {
this.clientsService.sendAadhaarOtp(this.clientId, this.kycId, aadhaar).subscribe({
next: (res: any) => {
this.aadhaarVerifying = false;
this.aadhaarOtpSent = true;
this.aadhaarOtpReferenceId = res?.changes?.aadhaarRefNumber || '';
this.resetOtpDigits();
this.startResendCountdown();
},
error: (err: any) => {
this.aadhaarVerifying = false;
this.aadhaarVerifyError = err?.error?.defaultUserMessage || 'Failed to send OTP.';
}
});
}, (msg) => { this.aadhaarVerifying = false; this.aadhaarVerifyError = msg; });
this.saveKycAndDo(
aadhaarPayload,
() => {
this.clientsService.sendAadhaarOtp(this.clientId, this.kycId, aadhaar).subscribe({
next: (res: any) => {
this.aadhaarVerifying = false;
this.aadhaarOtpSent = true;
this.aadhaarOtpReferenceId = res?.changes?.aadhaarRefNumber || '';
this.resetOtpDigits();
this.startResendCountdown();
},
error: (err: any) => {
this.aadhaarVerifying = false;
this.aadhaarVerifyError = err?.error?.defaultUserMessage || 'Failed to send OTP.';
}
});
},
(msg) => {
this.aadhaarVerifying = false;
this.aadhaarVerifyError = msg;
}
);
}
/** Verify Aadhaar OTP */
@@ -636,25 +874,27 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.aadhaarOtpVerifying = true;
this.aadhaarVerifyError = '';
this.clientsService.verifyAadhaarOtp(this.clientId, this.kycId, this.aadhaarOtp, this.aadhaarOtpReferenceId).subscribe({
next: (res: any) => {
this.aadhaarOtpVerifying = false;
if (res?.changes?.aadhaarVerified === true) {
this.aadhaarVerifyStatus = 'VERIFIED';
this.aadhaarVerifyName = res.changes?.aadhaarHolderName || 'Aadhaar Verified';
this.aadhaarOtpSent = false;
if (res.changes?.panAadhaarSeedingStatus) {
this.panAadhaarSeedingStatus = res.changes.panAadhaarSeedingStatus;
this.clientsService
.verifyAadhaarOtp(this.clientId, this.kycId, this.aadhaarOtp, this.aadhaarOtpReferenceId)
.subscribe({
next: (res: any) => {
this.aadhaarOtpVerifying = false;
if (res?.changes?.aadhaarVerified === true) {
this.aadhaarVerifyStatus = 'VERIFIED';
this.aadhaarVerifyName = res.changes?.aadhaarHolderName || 'Aadhaar Verified';
this.aadhaarOtpSent = false;
if (res.changes?.panAadhaarSeedingStatus) {
this.panAadhaarSeedingStatus = res.changes.panAadhaarSeedingStatus;
}
} else {
this.aadhaarVerifyError = res?.changes?.aadhaarVerificationMessage || 'OTP verification failed';
}
} else {
this.aadhaarVerifyError = res?.changes?.aadhaarVerificationMessage || 'OTP verification failed';
},
error: (err: any) => {
this.aadhaarOtpVerifying = false;
this.aadhaarVerifyError = err?.error?.defaultUserMessage || 'OTP verification failed.';
}
},
error: (err: any) => {
this.aadhaarOtpVerifying = false;
this.aadhaarVerifyError = err?.error?.defaultUserMessage || 'OTP verification failed.';
}
});
});
}
private showPanErrorAlert(message: string) {
@@ -667,17 +907,34 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
getRenewalYears(): number {
switch (this.kycForm.value.riskCategory) {
case 1: return 10;
case 2: return 8;
case 3: return 2;
default: return 10;
case 1:
return 10;
case 2:
return 8;
case 3:
return 2;
default:
return 10;
}
}
getKycExpiryDate(years: number): string {
const d = new Date();
d.setFullYear(d.getFullYear() + years);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
return `${String(d.getDate()).padStart(2, '0')} ${months[d.getMonth()]} ${d.getFullYear()}`;
}
@@ -688,11 +945,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
input.value = '';
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
width: '680px', maxWidth: '95vw',
width: '680px',
maxWidth: '95vw',
data: { file, title: 'Crop Client Photograph', aspectRatio: 1, maxWidth: 300, maxHeight: 300 }
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) { this.photographFile = result.croppedFile; this.photographPreview = result.previewUrl; }
if (result) {
this.photographFile = result.croppedFile;
this.photographPreview = result.previewUrl;
// In edit mode, upload immediately
if (this.clientId) {
this.clientsService
.uploadClientProfileImage(this.clientId, result.croppedFile)
.subscribe({ error: () => {} });
}
}
});
}
@@ -703,16 +970,70 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
input.value = '';
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
width: '680px', maxWidth: '95vw',
width: '680px',
maxWidth: '95vw',
data: { file, title: 'Crop Specimen Signature', aspectRatio: 2.5, maxWidth: 400, maxHeight: 160 }
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) { this.signatureFile = result.croppedFile; this.signaturePreview = result.previewUrl; }
if (result) {
this.signatureFile = result.croppedFile;
this.signaturePreview = result.previewUrl;
// In edit mode, delete old signature doc then upload new one
if (this.clientId) {
if (this.existingSignatureDocId) {
this.clientsService.deleteClientDocument(this.clientId, this.existingSignatureDocId).subscribe({
next: () => {
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
},
error: () => {
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
}
});
} else {
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
}
}
}
});
}
removePhoto() { this.photographFile = null; this.photographPreview = null; }
removeSignature() { this.signatureFile = null; this.signaturePreview = null; }
removePhoto() {
this.photographFile = null;
this.photographPreview = null;
// In edit mode, delete from server
if (this.clientId) {
this.clientsService.deleteClientProfileImage(this.clientId).subscribe({ error: () => {} });
}
}
removeSignature() {
this.signatureFile = null;
this.signaturePreview = null;
// In edit mode, delete from server
if (this.clientId && this.existingSignatureDocId) {
this.clientsService.deleteClientDocument(this.clientId, this.existingSignatureDocId).subscribe({
next: () => {
this.existingSignatureDocId = '';
},
error: () => {}
});
}
}
get kycData(): any {
const formVal = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
@@ -720,15 +1041,16 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : null;
const dateFormat = this.settingsService.dateFormat;
const v = formVal;
const formatDate = (d: any) => d instanceof Date ? this.dateUtils.formatDate(d, dateFormat) : (d || null);
const formatDate = (d: any) => (d instanceof Date ? this.dateUtils.formatDate(d, dateFormat) : d || null);
return {
locale: 'en',
dateFormat,
kycType: 1,
riskCategory: v.riskCategory || 1,
panNumber: v.hasPan ? (v.panNumber || null) : null,
aadhaarNumberMasked: aadhaarMasked,
// For non-individual: Company PAN is used as panNumber for verification
panNumber: this.isNonIndividual ? v.enterprisePan || null : v.hasPan ? v.panNumber || null : null,
aadhaarNumberMasked: this.isNonIndividual ? 'XXXXXXXX0000' : aadhaarMasked,
panAadhaarSeedingStatus: this.panAadhaarSeedingStatus || null,
gstinNumber: v.gstinNumber || null,
enterprisePan: v.enterprisePan || null,
@@ -761,7 +1083,15 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
get hasKycData(): boolean {
const v = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
return !!(v.panNumber || v.aadhaarNumber || v.gstinNumber || v.enterprisePan || v.cinNumber || v.kycDocumentName || v.form15Applicable);
return !!(
v.panNumber ||
v.aadhaarNumber ||
v.gstinNumber ||
v.enterprisePan ||
v.cinNumber ||
v.kycDocumentName ||
v.form15Applicable
);
}
}
@@ -784,22 +1114,44 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
</mat-dialog-actions>
</div>
`,
styles: [`
.pan-err-dialog { max-width: 460px; }
.pan-err-header {
display: flex; align-items: center; gap: 10px;
padding: 16px 24px 12px; border-bottom: 1px solid #fecaca;
background: #fef2f2; font-size: 16px; font-weight: 600; color: #991b1b;
}
.pan-err-icon { color: #dc2626; }
.pan-err-body { padding: 18px 24px !important; font-size: 14px; line-height: 1.6; color: #475569; }
.pan-err-actions { padding: 10px 24px 16px !important; margin: 0 !important; }
`],
imports: [MatIcon, MatDialogContent, MatDialogActions, STANDALONE_SHARED_IMPORTS],
styles: [
`
.pan-err-dialog {
max-width: 460px;
}
.pan-err-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 24px 12px;
border-bottom: 1px solid #fecaca;
background: #fef2f2;
font-size: 16px;
font-weight: 600;
color: #991b1b;
}
.pan-err-icon {
color: #dc2626;
}
.pan-err-body {
padding: 18px 24px !important;
font-size: 14px;
line-height: 1.6;
color: #475569;
}
.pan-err-actions {
padding: 10px 24px 16px !important;
margin: 0 !important;
}
`
],
imports: [
MatIcon,
MatDialogContent,
MatDialogActions,
STANDALONE_SHARED_IMPORTS
]
})
export class PanErrorDialogComponent {
constructor(
@Inject(MAT_DIALOG_DATA) public data: { message: string }
) {}
data = inject<{ message: string }>(MAT_DIALOG_DATA);
}