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:
co-authored by
Claude Opus 4.6
parent
b890d24bb7
commit
79e455c4ed
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user