Merge remote-tracking branch 'origin/demo' into demo
This commit is contained in:
@@ -84,14 +84,70 @@
|
||||
<mat-icon>phone_iphone</mat-icon>
|
||||
<span>Mobile Number</span>
|
||||
</div>
|
||||
<div class="contact-verify-panel__exception-row" style="margin-bottom: 12px">
|
||||
<mat-checkbox
|
||||
[checked]="oldMobileNotAccessible"
|
||||
(change)="onOldMobileNotAccessibleToggle($event.checked)"
|
||||
color="primary"
|
||||
>
|
||||
Old mobile number not accessible (lost / deactivated SIM)
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="contact-verify-panel__grid">
|
||||
<mifosx-client-contact-otp-block
|
||||
[clientId]="clientId"
|
||||
channel="MOBILE"
|
||||
[destination]="contactChanges.oldMobile"
|
||||
label="Old Mobile Number"
|
||||
(stateChange)="onOldMobileOtpState($event)"
|
||||
></mifosx-client-contact-otp-block>
|
||||
@if (!oldMobileNotAccessible) {
|
||||
<mifosx-client-contact-otp-block
|
||||
[clientId]="clientId"
|
||||
channel="MOBILE"
|
||||
[destination]="contactChanges.oldMobile"
|
||||
label="Old Mobile Number"
|
||||
(stateChange)="onOldMobileOtpState($event)"
|
||||
></mifosx-client-contact-otp-block>
|
||||
} @else {
|
||||
<div
|
||||
class="fir-upload-box"
|
||||
style="padding: 12px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #f8fafc"
|
||||
>
|
||||
<div style="font-weight: 500; font-size: 13px; margin-bottom: 6px; color: #475569">
|
||||
Upload Police Complaint / FIR Copy for Lost Mobile
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<input
|
||||
type="file"
|
||||
(change)="onFirFileSelected($event)"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
#firFileInput
|
||||
hidden
|
||||
/>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
(click)="firFileInput.click()"
|
||||
[disabled]="uploadingFir"
|
||||
>
|
||||
<mat-icon>upload_file</mat-icon>
|
||||
{{ firFileName ? firFileName : 'Select FIR Document' }}
|
||||
</button>
|
||||
@if (uploadingFir) {
|
||||
<mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
|
||||
}
|
||||
@if (firDocumentId && !uploadingFir) {
|
||||
<span
|
||||
style="
|
||||
color: #16a34a;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
"
|
||||
>
|
||||
<mat-icon style="font-size: 16px; width: 16px; height: 16px">check_circle</mat-icon> FIR
|
||||
Uploaded Successfully
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<mifosx-client-contact-otp-block
|
||||
[clientId]="clientId"
|
||||
channel="MOBILE"
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ContactOtpBlockComponent, ContactOtpState } from './contact-otp-block/c
|
||||
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
@@ -51,6 +52,7 @@ import { ORIGINAL_VALUES_PAYLOAD_KEY, buildOriginalValuesSnapshot } from '../sha
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatTabsModule,
|
||||
MatIconModule,
|
||||
MatCheckboxModule,
|
||||
MatDivider,
|
||||
FaIconComponent,
|
||||
MatProgressSpinner,
|
||||
@@ -108,6 +110,13 @@ export class EditClientComponent {
|
||||
newMobileOtpId: number | null = null;
|
||||
oldMobileVerified = false;
|
||||
newMobileVerified = false;
|
||||
|
||||
// FIR exception flow properties for inaccessible old mobile number
|
||||
oldMobileNotAccessible = false;
|
||||
firFileName = '';
|
||||
firDocumentId: number | null = null;
|
||||
uploadingFir = false;
|
||||
|
||||
oldEmailOtpId: number | null = null;
|
||||
newEmailOtpId: number | null = null;
|
||||
oldEmailVerified = false;
|
||||
@@ -215,11 +224,14 @@ export class EditClientComponent {
|
||||
return this.detectSensitiveFieldChanges(this.clientDataAndTemplate);
|
||||
}
|
||||
|
||||
/** Whether every OTP block required by the fields that actually changed has been verified. */
|
||||
/** Whether every OTP block required by the fields that actually changed has been verified (or FIR uploaded for lost mobile). */
|
||||
get contactVerificationComplete(): boolean {
|
||||
const changes = this.contactChanges;
|
||||
if (changes.mobileChanged && !(this.oldMobileVerified && this.newMobileVerified)) {
|
||||
return false;
|
||||
if (changes.mobileChanged) {
|
||||
const isOldSatisfied = this.oldMobileNotAccessible ? this.firDocumentId != null : this.oldMobileVerified;
|
||||
if (!isOldSatisfied || !this.newMobileVerified) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (changes.emailChanged && !(this.oldEmailVerified && this.newEmailVerified)) {
|
||||
return false;
|
||||
@@ -227,6 +239,41 @@ export class EditClientComponent {
|
||||
return true;
|
||||
}
|
||||
|
||||
onOldMobileNotAccessibleToggle(checked?: boolean): void {
|
||||
if (checked !== undefined) {
|
||||
this.oldMobileNotAccessible = checked;
|
||||
}
|
||||
if (this.oldMobileNotAccessible) {
|
||||
this.oldMobileOtpId = null;
|
||||
this.oldMobileVerified = false;
|
||||
}
|
||||
}
|
||||
|
||||
onFirFileSelected(event: any): void {
|
||||
const file: File = event.target?.files?.[0];
|
||||
if (!file) return;
|
||||
this.firFileName = file.name;
|
||||
this.uploadingFir = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('name', 'FIR - Lost Mobile Number');
|
||||
formData.append('description', 'FIR police complaint copy for lost mobile number verification exception');
|
||||
|
||||
this.clientsService.uploadClientDocument(this.clientId, formData).subscribe({
|
||||
next: (res: any) => {
|
||||
this.uploadingFir = false;
|
||||
this.firDocumentId = res?.resourceId || res?.id || null;
|
||||
this.snackBar.open('FIR document uploaded successfully', 'Close', { duration: 3000 });
|
||||
},
|
||||
error: () => {
|
||||
this.uploadingFir = false;
|
||||
this.firDocumentId = null;
|
||||
this.snackBar.open('Failed to upload FIR document', 'Close', { duration: 4000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onOldMobileOtpState(state: ContactOtpState): void {
|
||||
this.oldMobileOtpId = state.otpId;
|
||||
this.oldMobileVerified = state.verified;
|
||||
@@ -254,6 +301,10 @@ export class EditClientComponent {
|
||||
this.newMobileOtpId = null;
|
||||
this.oldMobileVerified = false;
|
||||
this.newMobileVerified = false;
|
||||
this.oldMobileNotAccessible = false;
|
||||
this.firFileName = '';
|
||||
this.firDocumentId = null;
|
||||
this.uploadingFir = false;
|
||||
this.oldEmailOtpId = null;
|
||||
this.newEmailOtpId = null;
|
||||
this.oldEmailVerified = false;
|
||||
@@ -439,7 +490,11 @@ export class EditClientComponent {
|
||||
// email's when BOTH changed in the same save. See the class-level JSDoc above the OTP
|
||||
// state fields for the full naming rationale.
|
||||
if (changedFields.mobileChanged) {
|
||||
clientData.mobileOldOtpId = this.oldMobileOtpId;
|
||||
if (this.oldMobileNotAccessible && this.firDocumentId) {
|
||||
(clientData as any).oldMobileVerificationExceptionDocumentId = this.firDocumentId;
|
||||
} else {
|
||||
clientData.mobileOldOtpId = this.oldMobileOtpId;
|
||||
}
|
||||
clientData.mobileNewOtpId = this.newMobileOtpId;
|
||||
}
|
||||
if (changedFields.emailChanged) {
|
||||
@@ -630,25 +685,34 @@ export class EditClientComponent {
|
||||
|
||||
private detectSensitiveFieldChanges(originalData: any) {
|
||||
const form = this.clientGeneralForm;
|
||||
// Trimmed so browser autofill adding invisible leading/trailing whitespace never reads as
|
||||
// a "change" — the masked preview shown in the Verify Contact Change panel only reveals the
|
||||
// first couple of characters, so a whitespace-only difference is otherwise invisible to the
|
||||
// user and just looks like the panel appeared for no reason. These trimmed-but-original-case
|
||||
// values are what get returned/sent as the actual OTP destination and saved value — only the
|
||||
// *comparison* (below) is further case-normalized for email, so we never silently rewrite the
|
||||
// customer's email casing into the saved record or the OTP destination.
|
||||
const newMobile = (form?.get('mobileNo')?.value || '').trim();
|
||||
const newEmail = (form?.get('emailAddress')?.value || '').trim();
|
||||
const newDobRaw = form?.get('dateOfBirth')?.value;
|
||||
// Local calendar date, NOT `.toISOString()` — that converts to UTC first, which silently
|
||||
// shifts the date by a day in any positive-UTC-offset timezone (e.g. India, UTC+5:30:
|
||||
// midnight local on the 15th becomes 18:30 UTC on the 14th). That made this comparison
|
||||
// report a DOB "change" — falsely triggering the Re-KYC dialog — even when the user never
|
||||
// touched the date field, purely because the Date object round-tripped through the picker.
|
||||
if (!form || !originalData) {
|
||||
return {
|
||||
nameChanged: false,
|
||||
mobileChanged: false,
|
||||
emailChanged: false,
|
||||
dobChanged: false,
|
||||
oldName: '',
|
||||
newName: '',
|
||||
oldMobile: '',
|
||||
newMobile: '',
|
||||
oldEmail: '',
|
||||
newEmail: '',
|
||||
oldDob: '',
|
||||
newDob: ''
|
||||
};
|
||||
}
|
||||
|
||||
const mobileControl = form.get('mobileNo');
|
||||
const emailControl = form.get('emailAddress');
|
||||
|
||||
// Trimmed so browser autofill adding invisible leading/trailing whitespace never reads as a change
|
||||
const newMobile = (mobileControl?.value || '').toString().trim();
|
||||
const newEmail = (emailControl?.value || '').toString().trim();
|
||||
const newDobRaw = form.get('dateOfBirth')?.value;
|
||||
const newDob = newDobRaw instanceof Date ? this.toLocalDateString(newDobRaw) : newDobRaw || '';
|
||||
|
||||
const oldMobile = (originalData.mobileNo || '').trim();
|
||||
const oldEmail = (originalData.emailAddress || '').trim();
|
||||
const oldMobile = (originalData.mobileNo || '').toString().trim();
|
||||
const oldEmail = (originalData.emailAddress || '').toString().trim();
|
||||
const oldDobRaw = originalData.dateOfBirth;
|
||||
const oldDob = Array.isArray(oldDobRaw)
|
||||
? `${oldDobRaw[0]}-${String(oldDobRaw[1]).padStart(2, '0')}-${String(oldDobRaw[2]).padStart(2, '0')}`
|
||||
@@ -669,10 +733,10 @@ export class EditClientComponent {
|
||||
).trim();
|
||||
|
||||
const newName = buildName({
|
||||
firstname: form?.get('firstname')?.value || '',
|
||||
middlename: form?.get('middlename')?.value || '',
|
||||
lastname: form?.get('lastname')?.value || '',
|
||||
fullname: form?.get('fullname')?.value || ''
|
||||
firstname: form.get('firstname')?.value || '',
|
||||
middlename: form.get('middlename')?.value || '',
|
||||
lastname: form.get('lastname')?.value || '',
|
||||
fullname: form.get('fullname')?.value || ''
|
||||
});
|
||||
const oldName = buildName({
|
||||
firstname: originalData.firstname || '',
|
||||
@@ -681,12 +745,21 @@ export class EditClientComponent {
|
||||
fullname: originalData.fullname || ''
|
||||
});
|
||||
|
||||
// Normalize phone numbers by stripping non-digit characters (+91 9009009009 -> 9009009009)
|
||||
const normPhone = (val: string) => val.replace(/\D/g, '');
|
||||
const newMobileNorm = normPhone(newMobile);
|
||||
const oldMobileNorm = normPhone(oldMobile);
|
||||
|
||||
// Mobile/Email are considered changed ONLY when the control has been edited (dirty) AND normalized values differ
|
||||
const mobileChanged =
|
||||
!!oldMobileNorm && !!newMobileNorm && !!mobileControl?.dirty && newMobileNorm !== oldMobileNorm;
|
||||
const emailChanged =
|
||||
!!oldEmail && !!newEmail && !!emailControl?.dirty && newEmail.toLowerCase() !== oldEmail.toLowerCase();
|
||||
|
||||
return {
|
||||
nameChanged: newName !== oldName,
|
||||
mobileChanged: newMobile !== oldMobile,
|
||||
// Case-insensitive: emails are not case-sensitive in practice, and comparing raw case
|
||||
// would flag a change (surfacing the dual-OTP panel) even when nothing meaningful changed.
|
||||
emailChanged: newEmail.toLowerCase() !== oldEmail.toLowerCase(),
|
||||
mobileChanged,
|
||||
emailChanged,
|
||||
dobChanged: newDob !== oldDob,
|
||||
oldName,
|
||||
newName,
|
||||
|
||||
Reference in New Issue
Block a user