Client-stepper: pre-fill existing KYC/address proofs in edit, fix identifier saves

Edit-mode now shows the previously selected KYC document type + uploaded file thumbnail
(loads from /clients/{id}/identifiers + /client_identifiers/{id}/documents). Same for
address proof on the India address step. Closes the gap where editing a client showed
an empty file picker even when proof had been uploaded on create.

Identifier-save hardened across all three callsites (create-client, edit-client,
identities-tab.createIdentifierWithDocs) - the KYC chip picker proofId is a CODE
string ('AADHAAR'), but backend ClientIdentifierRequest.documentTypeId is Long, which
Spring rejects with HttpMessageNotReadableException -> 400. resolveDocumentTypeId
helper looks up the code against /kyc/proof-types?type=id and substitutes the numeric
CodeValue id before posting. Skips silently for constitution-specific non-individual
docs that have no CodeValue mapping.

KYC payload normalises panNumber / enterprisePan / gstinNumber / cinNumber via
toUpperCase + whitespace strip so user-typed lowercase or trailing spaces don't trip
the backend regex (validation.msg.CLIENTKYC.panNumber.does.not.match.regexp).

State name match in client-address save now normalises both sides (lowercase, collapse
underscores + spaces) so legacy MADHYA_PRADESH resolves to 'Madhya Pradesh' returned
by api.postalpincode.in. Backed by tenant migration 0297.

Identities-tab resolver flattened - was returning identifiers before the per-row
forkJoin(getClientIdentificationDocuments) completed, so the view rendered empty
documents arrays. Now uses switchMap + forkJoin so the resolver awaits completion.

Also bundles concurrent work-in-progress on letter-mgmt, organization, users,
bank-calendar, shell, nominee/notes/identifier dialogs, and theming.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-20 12:40:58 +05:30
co-authored by Claude Opus 4.7
parent 325228c514
commit 83ea57ccf0
84 changed files with 10281 additions and 1194 deletions
@@ -16,6 +16,9 @@ import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
/** Custom Components */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { EditAddressDialogComponent } from './edit-address-dialog/edit-address-dialog.component';
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
import { ErrorDialogComponent } from 'app/shared/error-dialog/error-dialog.component';
/** Custom Services */
import { TranslateService } from '@ngx-translate/core';
@@ -87,29 +90,42 @@ export class AddressTabComponent {
* Adds a client address.
*/
addAddress() {
const data = {
title:
this.translateService.instant('labels.buttons.Add') +
' ' +
this.translateService.instant('labels.catalogs.Client') +
' ' +
this.translateService.instant('labels.heading.Address'),
formfields: this.getAddressFormFields('add'),
layout: { columns: 2, columnWidth: 350 }
};
const addAddressDialogRef = this.dialog.open(FormDialogComponent, { data });
addAddressDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
this.clientService
.createClientAddress(this.clientId, response.data.value.addressType, response.data.value)
.subscribe((res: any) => {
const addressData = response.data.value;
addressData.addressId = res.resourceId;
addressData.addressType = this.getSelectedValue('addressTypeIdOptions', addressData.addressType).name;
addressData.isActive = false;
this.clientAddressData.push(addressData);
});
}
// Use the Nexora dialog with PIN lookup + "Copy from existing address" picker.
const ref = this.dialog.open(EditAddressDialogComponent, {
data: {
mode: 'add',
title:
this.translateService.instant('labels.buttons.Add') +
' ' +
this.translateService.instant('labels.catalogs.Client') +
' ' +
this.translateService.instant('labels.heading.Address'),
address: null,
template: this.clientAddressTemplate,
fieldConfig: this.clientAddressFieldConfig,
existingAddresses: this.clientAddressData || []
},
panelClass: 'ead-panel',
autoFocus: false
});
ref.afterClosed().subscribe((response: any) => {
if (!response?.value) return;
const payload = { ...response.value };
const typeName = this.getSelectedValue('addressTypeIdOptions', payload.addressType)?.name;
this.clientService.createClientAddress(this.clientId, payload.addressType, payload).subscribe({
next: (res: any) => {
payload.addressId = res.resourceId;
payload.addressType = typeName;
payload.isActive = false;
this.clientAddressData.push(payload);
this.showSuccess('Address Added', `${typeName || 'Address'} has been added to this client's profile.`);
},
error: (err: any) => {
const apiMsg =
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Please try again.';
this.showError(`Failed to add address. ${apiMsg}`);
}
});
});
}
@@ -119,31 +135,54 @@ export class AddressTabComponent {
* @param {number} index address index
*/
editAddress(address: any, index: number) {
const data = {
title:
this.translateService.instant('labels.buttons.Edit') +
' ' +
this.translateService.instant('labels.catalogs.Client') +
' ' +
this.translateService.instant('labels.heading.Address'),
formfields: this.getAddressFormFields('edit', address),
layout: { columns: 2, columnWidth: 350, addButtonText: 'Edit' }
};
const editAddressDialogRef = this.dialog.open(FormDialogComponent, { data });
editAddressDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const addressData = response.data.value;
addressData.addressId = address.addressId;
addressData.isActive = address.isActive;
this.clientService
.editClientAddress(this.clientId, address.addressTypeId, addressData)
.subscribe((res: any) => {
addressData.addressTypeId = address.addressTypeId;
addressData.addressType = address.addressType;
this.clientAddressData[index] = addressData;
});
}
// Open the Nexora edit dialog with PIN lookup (mirrors the stepper).
const ref = this.dialog.open(EditAddressDialogComponent, {
data: {
title:
this.translateService.instant('labels.buttons.Edit') +
' ' +
this.translateService.instant('labels.catalogs.Client') +
' ' +
this.translateService.instant('labels.heading.Address'),
address,
template: this.clientAddressTemplate,
fieldConfig: this.clientAddressFieldConfig
},
panelClass: 'ead-panel',
autoFocus: false
});
ref.afterClosed().subscribe((response: any) => {
if (!response?.value) return;
const addressData = { ...response.value };
addressData.addressId = address.addressId;
addressData.isActive = address.isActive;
this.clientService.editClientAddress(this.clientId, address.addressTypeId, addressData).subscribe({
next: () => {
addressData.addressTypeId = address.addressTypeId;
addressData.addressType = address.addressType;
this.clientAddressData[index] = addressData;
this.showSuccess('Address Updated', `${address.addressType || 'Address'} has been updated successfully.`);
},
error: (err: any) => {
const apiMsg =
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Please try again.';
this.showError(`Failed to update address. ${apiMsg}`);
}
});
});
}
/** Polished success modal — matches the rest of the Nexora UI. */
private showSuccess(title: string, message: string): void {
this.dialog.open(SuccessDialogComponent, {
data: { title, message, buttonLabel: 'OK' },
panelClass: 'sd-panel'
});
}
/** Polished error modal — single string body. */
private showError(message: string): void {
this.dialog.open(ErrorDialogComponent, { data: message });
}
/**
@@ -297,7 +336,9 @@ export class AddressTabComponent {
this.isFieldEnabled('countyDistrict')
? new InputBase({
controlName: 'countyDistrict',
label: this.translateService.instant('labels.inputs.State / Province'),
// Was mistakenly labeled "State / Province" — that label belongs to
// the stateProvinceId select above. This control holds the District.
label: this.translateService.instant('labels.inputs.County / District'),
value: address ? address.countyDistrict : '',
type: 'text',
order: 11