feat(clients): maker-checker for client edits (frontend)
- New "Pending Client Edits" screen under Checker Inbox & Tasks: server-driven pagination,
date range + Today/Yesterday/All presets, status filter (Pending/Approved/Rejected), search —
mirrors ChequebookRequestsComponent's architecture.
- Compare dialog diffs the maker's proposed values against a point-in-time "before" snapshot
(client-edit-diff.util.ts) captured when the edit page was opened, so only genuinely changed
fields show — works correctly for Pending, Approved, and Rejected edits alike, since it no
longer depends on comparing against the live (possibly already-updated) client record.
- edit-client.component.ts: General tab save now submits for approval instead of applying
immediately, shows a pending-approval banner, and blocks a second submission while one's
outstanding.
- error-handler.interceptor.ts: added the new /clients/{id}/edit/pending check to the existing
"expected 403/404, don't alert" allowlist (same pattern already used for client images/KYC).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { ORIGINAL_VALUES_PAYLOAD_KEY, buildOriginalValuesSnapshot } from '../shared/client-edit-diff.util';
|
||||
|
||||
/**
|
||||
* Edit Client Component — uses the same wizard/stepper layout as Create Client.
|
||||
@@ -116,6 +117,9 @@ export class EditClientComponent {
|
||||
clientDataAndTemplate: any;
|
||||
/** Client ID */
|
||||
clientId: string;
|
||||
/** True while a General-tab edit is staged awaiting checker approval — blocks submitting
|
||||
* another one (the backend enforces this too; this just avoids a round-trip error toast). */
|
||||
hasPendingGeneralEdit = false;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { clientDataAndTemplate: any }) => {
|
||||
@@ -124,6 +128,15 @@ export class EditClientComponent {
|
||||
if (this.clientDataAndTemplate.legalForm) {
|
||||
this.legalFormType = this.clientDataAndTemplate.legalForm.id;
|
||||
}
|
||||
this.loadPendingGeneralEdit();
|
||||
});
|
||||
}
|
||||
|
||||
private loadPendingGeneralEdit(): void {
|
||||
if (!this.clientId) return;
|
||||
this.clientsService.getPendingClientEdit(this.clientId).subscribe({
|
||||
next: (edit: any) => (this.hasPendingGeneralEdit = !!edit && edit.status === 'PENDING'),
|
||||
error: () => (this.hasPendingGeneralEdit = false)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,15 +447,27 @@ export class EditClientComponent {
|
||||
clientData.emailNewOtpId = this.newEmailOtpId;
|
||||
}
|
||||
|
||||
// Point-in-time "before" snapshot for the checker's diff view — captured now, from the
|
||||
// client record loaded when this edit page opened, rather than relying on comparing against
|
||||
// the LIVE client later (which would show nothing changed once a checker approves the edit,
|
||||
// since by then current == proposed).
|
||||
clientData[ORIGINAL_VALUES_PAYLOAD_KEY] = buildOriginalValuesSnapshot(originalData);
|
||||
|
||||
this.savingClient = true;
|
||||
this.clientsService.updateClient(this.clientId, clientData).subscribe({
|
||||
// Maker-checker: stage the edit instead of applying it immediately. The customer-facing
|
||||
// side effects that a direct update would have had immediately — SMS/Email contact-change
|
||||
// notification, re-KYC flagging, PAN re-verification reset — are all deferred until a
|
||||
// different user actually approves the edit (approval replays this exact payload through
|
||||
// the same updateClient() the direct PUT used to call, so the backend-driven notification
|
||||
// still fires correctly, just at approval time instead of now).
|
||||
this.clientsService.submitClientEdit(this.clientId, clientData).subscribe({
|
||||
next: () => {
|
||||
this.savingClient = false;
|
||||
this.tabEditMode[tabIndex] = false;
|
||||
this.hasPendingGeneralEdit = true;
|
||||
|
||||
// Senior Citizen / PWD supporting documents — same pattern as create-client's
|
||||
// post-creation upload, triggered here since these are plain file inputs on the
|
||||
// General step, not part of the JSON update payload.
|
||||
// Senior Citizen / PWD supporting documents — separate upload endpoints, not part of
|
||||
// the staged JSON edit, so these still apply immediately as before.
|
||||
if (this.clientGeneralStep?.seniorCitizenProofFile) {
|
||||
const seniorCitizenProofForm = new FormData();
|
||||
seniorCitizenProofForm.append('file', this.clientGeneralStep.seniorCitizenProofFile);
|
||||
@@ -460,40 +485,13 @@ export class EditClientComponent {
|
||||
this.clientsService.uploadClientDocument(this.clientId, pwdCertificateForm).subscribe({ error: () => {} });
|
||||
}
|
||||
|
||||
// Log mobile/email changes as client notes for risk audit
|
||||
this.logFieldChangeNotes(changedFields);
|
||||
|
||||
// If name or DOB changed, flag re-KYC requirement and reset PAN verification
|
||||
if (changedFields.nameChanged || changedFields.dobChanged) {
|
||||
this.dobChanged = true;
|
||||
this.tabEditMode[1] = true; // auto-enable KYC tab for re-verification
|
||||
this.flagReKycRequired(changedFields);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (changedFields.nameChanged) {
|
||||
const form = this.clientGeneralForm;
|
||||
originalData.firstname = form?.get('firstname')?.value ?? originalData.firstname;
|
||||
originalData.middlename = form?.get('middlename')?.value ?? originalData.middlename;
|
||||
originalData.lastname = form?.get('lastname')?.value ?? originalData.lastname;
|
||||
originalData.fullname = form?.get('fullname')?.value ?? originalData.fullname;
|
||||
}
|
||||
|
||||
// Notification is backend-driven (SMS + Email) — just reassure staff it happened.
|
||||
if (changedFields.mobileChanged || changedFields.emailChanged) {
|
||||
this.snackBar.open('Customer notified via SMS/Email of the contact change.', 'Close', {
|
||||
duration: 4000
|
||||
});
|
||||
}
|
||||
this.snackBar.open(
|
||||
'Edit submitted for approval — a different user must review it before it takes effect.',
|
||||
'Close',
|
||||
{
|
||||
duration: 5000
|
||||
}
|
||||
);
|
||||
this.resetContactVerificationState();
|
||||
|
||||
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||
|
||||
Reference in New Issue
Block a user