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
@@ -9,10 +9,12 @@
/** Angular Imports */
import { Component, QueryList, ViewChild, ViewChildren, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
/** Custom Services */
import { ClientsService } from '../clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
/** Custom Components */
import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component';
@@ -60,6 +62,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export class EditClientComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private dialog = inject(MatDialog);
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
@@ -79,6 +82,9 @@ export class EditClientComponent {
/** Track which tabs are in edit mode (false = read-only, true = editable) */
tabEditMode: Record<number, boolean> = {};
/** Flag: DOB was changed — requires re-KYC (PAN re-verification) */
dobChanged = false;
/** Client data and template (merged object from resolver) */
clientDataAndTemplate: any;
/** Client ID */
@@ -139,8 +145,13 @@ export class EditClientComponent {
return areValids;
}
/** Whether the current tab has unsaved edits */
isCurrentTabEdited(): boolean {
return this.tabEditMode[this.selectedTabIndex] === true;
}
/**
* Next button handler — just navigates between tabs (client already exists in edit mode).
* Next button handler — saves changes via API if the current tab was edited, then moves to next tab.
*/
onNext() {
if (this.selectedTabIndex === 0) {
@@ -155,9 +166,271 @@ export class EditClientComponent {
return;
}
}
// If tab was edited, confirm and save before moving on
if (this.isCurrentTabEdited()) {
this.confirmAndSaveTab();
} else {
this.selectedTabIndex = this.selectedTabIndex + 1;
}
}
/**
* Show confirmation dialog and save the current tab's data via API.
* Shows a special warning if DOB was changed (re-KYC required).
*/
private confirmAndSaveTab() {
let heading = 'Confirm Edit';
let dialogContext = 'Are you sure you want to save the changes';
let dialogType = 'basic';
// Check if DOB changed on the General tab — warn about re-KYC
if (this.selectedTabIndex === 0) {
const changes = this.detectSensitiveFieldChanges(this.clientDataAndTemplate);
if (changes.dobChanged) {
heading = 'Re-KYC Required';
dialogContext =
'Date of birth has been changed. This will require PAN re-verification in the KYC section. A remark will be added automatically. Do you want to proceed';
dialogType = 'warn';
}
}
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: { heading, dialogContext, type: dialogType }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response?.confirm) {
this.saveCurrentTab();
}
});
}
/**
* Save the current tab's data via API, then advance to the next tab.
*/
private saveCurrentTab() {
const tabIndex = this.selectedTabIndex;
// General Details tab (always index 0)
if (tabIndex === 0) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const clientData = { ...this.client, dateFormat, locale };
delete clientData.officeId;
// Detect changes to sensitive fields before saving
const originalData = this.clientDataAndTemplate;
const changedFields = this.detectSensitiveFieldChanges(originalData);
this.savingClient = true;
this.clientsService.updateClient(this.clientId, clientData).subscribe({
next: () => {
this.savingClient = false;
this.tabEditMode[tabIndex] = false;
// Log mobile/email changes as client notes for risk audit
this.logFieldChangeNotes(changedFields);
// If DOB changed, flag re-KYC requirement and reset PAN verification
if (changedFields.dobChanged) {
this.dobChanged = true;
this.tabEditMode[1] = true; // auto-enable KYC tab for re-verification
this.flagReKycRequired(changedFields.oldDob, changedFields.newDob);
}
// Update original data snapshot so subsequent edits compare against latest
if (changedFields.mobileChanged) {
originalData.mobileNo = changedFields.newMobile;
}
if (changedFields.emailChanged) {
originalData.emailAddress = changedFields.newEmail;
}
if (changedFields.dobChanged) {
originalData.dateOfBirth = changedFields.newDob;
}
this.selectedTabIndex = this.selectedTabIndex + 1;
},
error: () => {
this.savingClient = false;
}
});
return;
}
// KYC tab (always index 1)
if (tabIndex === 1) {
this.savingClient = true;
// Upload photo if selected
if (this.clientKycStep?.photographFile) {
this.clientsService
.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile)
.subscribe({ error: () => {} });
}
// Upload signature if selected
if (this.clientKycStep?.signatureFile) {
this.clientsService
.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile)
.subscribe({ error: () => {} });
}
// Save KYC data — use correct API based on KYC state
if (this.clientKycStep?.hasKycData) {
const kycData = this.clientKycStep.kycData;
const kycId = this.clientKycStep.kycId;
const isVerified = this.clientKycStep.isKycVerified;
// Validate remarks are present for verified KYC
if (isVerified && !kycData.remarks?.trim()) {
this.savingClient = false;
this.clientKycStep.kycForm.get('remarks')?.markAsTouched();
return;
}
let saveObs;
if (kycId && isVerified) {
// VERIFIED → re-edit command
const { kycType, ...updatePayload } = kycData;
saveObs = this.clientsService.reEditClientKyc(this.clientId, kycId, updatePayload);
} else if (kycId) {
// Existing but not verified → normal PUT
const { kycType, ...updatePayload } = kycData;
saveObs = this.clientsService.updateClientKyc(this.clientId, kycId, updatePayload);
} else {
// New → POST
saveObs = this.clientsService.saveClientKyc(this.clientId, kycData);
}
saveObs.subscribe({
next: (res: any) => {
this.savingClient = false;
this.tabEditMode[tabIndex] = false;
// If re-edit, update KYC component state
if (isVerified && this.clientKycStep) {
this.clientKycStep.kycStatus = 200;
this.clientKycStep.panVerifyStatus = 'NONE';
this.clientKycStep.panVerifyName = '';
this.clientKycStep.panVerifiedOn = '';
}
if (!kycId && res?.resourceId) {
this.clientKycStep.kycId = res.resourceId.toString();
}
this.selectedTabIndex = this.selectedTabIndex + 1;
},
error: () => {
this.savingClient = false;
}
});
} else {
this.savingClient = false;
this.tabEditMode[tabIndex] = false;
this.selectedTabIndex = this.selectedTabIndex + 1;
}
return;
}
// Address tab
const addressTabIdx = this.legalFormType === 2 ? 3 : 2;
if (tabIndex === addressTabIdx) {
this.savingClient = true;
if (this.clientIndiaAddressStep?.hasAddressData) {
this.saveClientAddresses(this.clientId);
}
this.savingClient = false;
this.tabEditMode[tabIndex] = false;
this.selectedTabIndex = this.selectedTabIndex + 1;
return;
}
// Family tab
const familyTabIdx = this.legalFormType === 2 ? 4 : 3;
if (tabIndex === familyTabIdx) {
this.savingClient = true;
if (this.clientFamilyMembersStep?.hasFamilyMembers) {
this.saveClientFamilyMembers(this.clientId);
}
this.savingClient = false;
this.tabEditMode[tabIndex] = false;
this.selectedTabIndex = this.selectedTabIndex + 1;
return;
}
// Default: just advance
this.tabEditMode[tabIndex] = false;
this.selectedTabIndex = this.selectedTabIndex + 1;
}
/**
* Compare current form values with original data to detect changes in DOB, mobile, email.
*/
private detectSensitiveFieldChanges(originalData: any) {
const form = this.clientGeneralForm;
const newMobile = form?.get('mobileNo')?.value || '';
const newEmail = form?.get('emailAddress')?.value || '';
const newDobRaw = form?.get('dateOfBirth')?.value;
const newDob = newDobRaw instanceof Date ? newDobRaw.toISOString().split('T')[0] : newDobRaw || '';
const oldMobile = originalData.mobileNo || '';
const oldEmail = originalData.emailAddress || '';
const oldDobRaw = originalData.dateOfBirth;
const oldDob = Array.isArray(oldDobRaw)
? `${oldDobRaw[0]}-${String(oldDobRaw[1]).padStart(2, '0')}-${String(oldDobRaw[2]).padStart(2, '0')}`
: oldDobRaw instanceof Date
? oldDobRaw.toISOString().split('T')[0]
: oldDobRaw || '';
return {
mobileChanged: newMobile !== oldMobile,
emailChanged: newEmail !== oldEmail,
dobChanged: newDob !== oldDob,
oldMobile,
newMobile,
oldEmail,
newEmail,
oldDob,
newDob
};
}
/**
* Save audit notes for mobile/email changes via the client notes API.
*/
private logFieldChangeNotes(changes: any) {
const timestamp = new Date().toISOString();
if (changes.mobileChanged) {
const note = `[AUDIT] Mobile number changed from "${changes.oldMobile}" to "${changes.newMobile}" on ${timestamp}`;
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
}
if (changes.emailChanged) {
const note = `[AUDIT] Email address changed from "${changes.oldEmail}" to "${changes.newEmail}" on ${timestamp}`;
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
}
}
/**
* When DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC.
*/
private flagReKycRequired(oldDob: string, newDob: string) {
// Reset PAN verification status so user must re-verify
if (this.clientKycStep) {
this.clientKycStep.panVerifyStatus = 'NONE';
this.clientKycStep.panVerifyName = '';
this.clientKycStep.panVerifiedOn = '';
// Pre-fill remarks with re-KYC note
const remarkText = `[Re-KYC Required] Date of birth changed from ${oldDob} to ${newDob}. PAN re-verification is mandatory.`;
this.clientKycStep.kycForm.get('remarks')?.setValue(remarkText);
}
// Also log DOB change as an audit note
const timestamp = new Date().toISOString();
const note = `[AUDIT] Date of birth changed from "${oldDob}" to "${newDob}" on ${timestamp}. Re-KYC (PAN re-verification) required.`;
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
}
legalFormChange(eventData: { legalForm: number }) {
this.legalFormType = eventData.legalForm;
}
@@ -185,12 +458,16 @@ export class EditClientComponent {
// Upload photo if selected
if (this.clientKycStep?.photographFile) {
this.clientsService.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile).subscribe({ error: () => {} });
this.clientsService
.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile)
.subscribe({ error: () => {} });
}
// Upload signature if selected
if (this.clientKycStep?.signatureFile) {
this.clientsService.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile).subscribe({ error: () => {} });
this.clientsService
.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile)
.subscribe({ error: () => {} });
}
// Save addresses from India address step
@@ -205,7 +482,20 @@ export class EditClientComponent {
// Save/update KYC data if available
if (this.clientKycStep?.hasKycData) {
this.clientsService.saveClientKyc(this.clientId, this.clientKycStep.kycData).subscribe({
const kycData = this.clientKycStep.kycData;
const kycId = this.clientKycStep.kycId;
const isVerified = this.clientKycStep.isKycVerified;
let kycObs;
if (kycId && isVerified) {
const { kycType, ...updatePayload } = kycData;
kycObs = this.clientsService.reEditClientKyc(this.clientId, kycId, updatePayload);
} else if (kycId) {
const { kycType, ...updatePayload } = kycData;
kycObs = this.clientsService.updateClientKyc(this.clientId, kycId, updatePayload);
} else {
kycObs = this.clientsService.saveClientKyc(this.clientId, kycData);
}
kycObs.subscribe({
next: () => navigateBack(),
error: () => navigateBack()
});
@@ -225,16 +515,36 @@ export class EditClientComponent {
private saveClientAddresses(clientId: string) {
const addressData = this.clientIndiaAddressStep.addressData;
const typeNameMap: Record<string, string[]> = {
permanent: ['Permanent', 'Permanent Address'],
current: ['Residential', 'Current', 'Current / Residential', 'Communication'],
office: ['Office', 'Office / Work', 'Business', 'Work'],
correspondence: ['Correspondence', 'Mailing', 'Postal']
permanent: [
'Permanent',
'Permanent Address'
],
current: [
'Residential',
'Current',
'Current / Residential',
'Communication'
],
office: [
'Office',
'Office / Work',
'Business',
'Work'
],
correspondence: [
'Correspondence',
'Mailing',
'Postal'
]
};
this.clientsService.getClientAddressTemplate().subscribe({
next: (template: any) => {
const typeOptions = template?.addressTypeIdOptions || [];
for (const [tab, addr] of Object.entries(addressData)) {
for (const [
tab,
addr
] of Object.entries(addressData)) {
if (!addr || !(addr as any).addressLine1) continue;
const possibleNames = typeNameMap[tab] || [tab];
@@ -259,8 +569,8 @@ export class EditClientComponent {
};
if ((addr as any).state && template?.stateProvinceIdOptions?.length) {
const stateMatch = template.stateProvinceIdOptions.find((s: any) =>
s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
const stateMatch = template.stateProvinceIdOptions.find(
(s: any) => s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
);
if (stateMatch) payload.stateProvinceId = stateMatch.id;
}