From 1d1e76497c0f7389ba53aca9e6e827878d5874fd Mon Sep 17 00:00:00 2001 From: Anshul Rathore Date: Fri, 14 Aug 2026 12:42:29 +0530 Subject: [PATCH] Update client KYC and address handling --- package-lock.json | 26 ++++ package.json | 9 +- .../client-general-step.component.html | 7 +- .../client-general-step.component.ts | 22 ++++ .../client-india-address-step.component.html | 61 +++++++-- .../client-india-address-step.component.ts | 121 ++++++++++++++++-- .../create-client.component.html | 6 + .../edit-client/edit-client.component.html | 6 + src/environments/.env.ts | 4 +- 9 files changed, 236 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3496b303b..4ab0e12c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "@typescript-eslint/eslint-plugin": "^8.48.1", "@typescript-eslint/parser": "^8.48.1", "@vendure/ngx-translate-extract": "^9.4.0", + "cross-env": "^10.1.0", "cypress": "^13.17.0", "eslint": "^9.39.1", "git-describe": "^4.1.1", @@ -3560,6 +3561,13 @@ "url": "https://github.com/sponsors/JounQin" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -11702,6 +11710,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/package.json b/package.json index 124962bd0..d64b90449 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "ng": "ng", "build": "node version.js && npm run env -s && ng build --configuration production --output-hashing=none", "build:prod": "node version.js && node --max-old-space-size=16384 ./node_modules/@angular/cli/bin/ng build --configuration production --output-hashing=none --base-href=/web-app/", - "start": "npm run env -s && ng serve --proxy-config proxy.localhost.conf.js", - "start:share": "npm run env -s && ng serve --host 0.0.0.0 --proxy-config proxy.localhost.conf.js", - "start:demo": "npm run env -s && ng serve --proxy-config proxy.conf.js", - "serve:dev": "npm run env -s && ng serve --source-map --proxy-config proxy.localhost.conf.js", + "start": "npm run env -s && cross-env NODE_OPTIONS=\"--max_old_space_size=16384\" ng serve --proxy-config proxy.localhost.conf.js", + "start:share": "npm run env -s && cross-env NODE_OPTIONS=\"--max_old_space_size=16384\" ng serve --host 0.0.0.0 --proxy-config proxy.localhost.conf.js", + "start:demo": "npm run env -s && cross-env NODE_OPTIONS=\"--max_old_space_size=16384\" ng serve --proxy-config proxy.conf.js", + "serve:dev": "npm run env -s && cross-env NODE_OPTIONS=\"--max_old_space_size=16384\" ng serve --source-map --proxy-config proxy.localhost.conf.js", "serve:sw": "npm run build -s && npx http-server ./dist -p 4200", "lint": "eslint . && stylelint \"src/**/*.scss\" && prettier . --check && htmlhint \"src\" --config .htmlhintrc", "test": "jest --config jest.config.ts", @@ -107,6 +107,7 @@ "@typescript-eslint/eslint-plugin": "^8.48.1", "@typescript-eslint/parser": "^8.48.1", "@vendure/ngx-translate-extract": "^9.4.0", + "cross-env": "^10.1.0", "cypress": "^13.17.0", "eslint": "^9.39.1", "git-describe": "^4.1.1", diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html index 6fbf39851..f5ad0590e 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html @@ -263,6 +263,7 @@ {{ 'labels.inputs.Last Name' | translate }} person + <<<<<<< Updated upstream + ======= + + >>>>>>> Stashed changes @if (createClientForm.controls.lastname.hasError('required')) { Last name is required } @else if (createClientForm.controls.lastname.hasError('pattern')) { @@ -854,7 +858,8 @@
{{ result.displayName }} ID: {{ result.id }} · A/C: {{ result.accountNumber }} · + >ID: {{ result.id }} · A/C: {{ result.accountNumber }} + · {{ result.officeName }} @if (result.dateOfBirth) { · DOB: {{ result.dateOfBirth | dateFormat }} diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index 910651bb5..8ea5bf5e5 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -2590,6 +2590,28 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges dormantFlag: 'ACTIVE' }; } + + /** + * Automatically capitalizes the first letter of each word and lowercases the rest. + * e.g., "sri rAm" -> "Sri Ram" + */ + formatName(controlName: string): void { + const control = this.createClientForm.get(controlName); + if (!control || typeof control.value !== 'string') return; + + const val = control.value; + const formatted = val + .split(' ') + .map((word: string) => { + if (!word) return ''; + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join(' '); + + if (formatted !== val) { + control.setValue(formatted, { emitEvent: false }); + } + } } /** diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html index 0efe524f2..c1922f22c 100644 --- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html +++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html @@ -122,8 +122,41 @@ }
- -
+
+ + @if (isAddressProofLinkedToKyc && !readOnly) { +
+ link + Automatically linked from KYC step + +
+ } +
+
@for (group of addressGroups; track group.label; let last = $last) {
folder_open {{ group.label }}
@@ -131,11 +164,11 @@ @@ -167,14 +200,24 @@ @if (selectedAddrProof.requiresDate) { Issue / bill date - - + + } diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts index 66ac05ee9..90d54787a 100644 --- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts +++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts @@ -55,9 +55,22 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { * NPR/Birth Certificate/School ID) from the proof-of-address picker; a company's registered * office address is evidenced by utility bills, GST/rent documents etc., not a person's ID. */ @Input() isNonIndividual = false; + /** KYC form data passed down from the parent wizard so we can pre-fill address proof */ + @Input() kycData: any; + /** KYC document code for robust matching */ + @Input() kycDocCode: string | undefined; + /** KYC document files passed down to avoid re-uploading */ + @Input() kycDocFiles: { front: File | null; back: File | null } | null = null; + /** Combined preview string for KYC document */ + @Input() kycDocMergedPreview: string | null = null; + /** Existing server documents from KYC step (used in Edit mode) */ + @Input() existingKycDocSides: { front: any | null; back: any | null } | null = null; + /** Identifier ID for existing server documents */ + @Input() existingKycIdentifierId: string | null = null; activeTab = 0; sameAsPermanent = false; + isAddressProofLinkedToKyc = false; permanentForm: UntypedFormGroup = this.fb.group({ addressLine1: [ @@ -240,6 +253,8 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { /** Address type name → tab mapping for loading existing data */ private typeToTab: Record = {}; + private existingAddressProofCheckComplete = false; + ngOnInit(): void { this.loadProofTypes(); if (this.clientId) { @@ -247,6 +262,73 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { } } + private findMatchingProof(targetName: string | undefined): any { + if (!targetName) return undefined; + const cleanStr = (s: string) => (s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); + const n = cleanStr(targetName); + return this.addressProofs.find((p) => { + const pname = cleanStr(p.name); + return pname === n || n.includes(pname) || pname.includes(n); + }); + } + + private tryLinkKycData(): void { + if (!this.kycData || !this.kycData.kycDocumentName) return; + + if (this.clientId && !this.existingAddressProofCheckComplete) return; + + if (this.isAddressProofLinkedToKyc || !this.permanentForm.get('documentKey')?.value) { + let matchedProof = undefined; + if (this.kycDocCode) { + matchedProof = this.addressProofs.find((p) => p.code === this.kycDocCode); + } + if (!matchedProof) { + matchedProof = this.findMatchingProof(this.kycData.kycDocumentName); + } + if (matchedProof) { + this.isAddressProofLinkedToKyc = true; + this.selectAddrProof(matchedProof); + this.permanentForm.patchValue({ + documentKey: this.kycData.kycDocumentNumber, + issueDate: this.kycData.kycDocIssueDate + }); + this.currentForm.patchValue({ + documentKey: this.kycData.kycDocumentNumber, + issueDate: this.kycData.kycDocIssueDate + }); + + // Sync uploaded images if they exist in KYC step + if (this.kycDocFiles && (this.kycDocFiles.front || this.kycDocFiles.back)) { + this.addrFiles = { front: this.kycDocFiles.front || null, back: this.kycDocFiles.back || null }; + if (this.kycDocMergedPreview) { + this.addrMergedPreview = this.kycDocMergedPreview; + } + } else if (this.existingKycDocSides && (this.existingKycDocSides.front || this.existingKycDocSides.back)) { + this.existingAddressProofDocuments = []; + if (this.existingKycDocSides.front) this.existingAddressProofDocuments.push(this.existingKycDocSides.front); + if (this.existingKycDocSides.back) this.existingAddressProofDocuments.push(this.existingKycDocSides.back); + if (this.existingKycDocSides.front?.previewUrl) { + this.addrMergedPreview = this.existingKycDocSides.front.previewUrl; + } else if (this.existingKycDocSides.back?.previewUrl) { + this.addrMergedPreview = this.existingKycDocSides.back.previewUrl; + } + this.existingAddressProofIdentifierId = this.existingKycIdentifierId; + } + } else if (this.isAddressProofLinkedToKyc) { + // If it was linked but the KYC document changed to something not allowed for address (e.g. PAN for non-individuals), unlink. + this.unlinkAddressProof(); + } + } + } + + unlinkAddressProof(): void { + this.isAddressProofLinkedToKyc = false; + this.permanentForm.patchValue({ documentKey: '', issueDate: null }); + this.currentForm.patchValue({ documentKey: '', issueDate: null }); + this.addrFiles = { front: null, back: null }; + this.addrMergedPreview = null; + } + /** * Resets transient address-tab state for a brand-new client. * @@ -277,6 +359,7 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { this.existingAddressProofIdentifierId = null; this.existingAddressProofDocuments = []; this.addressLoaded = false; + this.existingAddressProofCheckComplete = false; // Re-fetch rather than re-filter `addressProofs` in place — if the previous session's // isMinor filtering already reduced it (e.g. down to Aadhaar/Passport only), filtering // that reduced list again can never recover the items it already removed. @@ -293,6 +376,7 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { ); this.addressGroups = groupProofsByCategory(this.addressProofs); this.loadingProofs = false; + this.tryLinkKycData(); }, error: () => { this.loadingProofs = false; @@ -426,6 +510,17 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { } ngOnChanges(changes: SimpleChanges): void { + if (changes['kycData'] || changes['existingKycDocSides']) { + this.tryLinkKycData(); + } + if (changes['existingKycIdentifierId']) { + if ( + this.existingAddressProofIdentifierId && + this.existingAddressProofIdentifierId === this.existingKycIdentifierId + ) { + this.isAddressProofLinkedToKyc = true; + } + } if (changes['readOnly']) { const forms = [ this.permanentForm, @@ -544,19 +639,25 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { } this.clientsService.getClientIdentifiers(this.clientId).subscribe({ next: (identifiers: any[]) => { - if (!identifiers || identifiers.length === 0) return; - const proofNames = new Set(this.addressProofs.map((p) => p.name?.toLowerCase()).filter(Boolean)); - const identifier = identifiers.find((i: any) => { - const n = i?.documentType?.name?.toLowerCase(); - return n && proofNames.has(n); - }); - if (!identifier?.id) return; + if (!identifiers || identifiers.length === 0) { + this.existingAddressProofCheckComplete = true; + this.tryLinkKycData(); + return; + } + const identifier = identifiers.find((i: any) => this.findMatchingProof(i?.documentType?.name)); + if (!identifier?.id) { + this.existingAddressProofCheckComplete = true; + this.tryLinkKycData(); + return; + } + this.existingAddressProofCheckComplete = true; this.existingAddressProofIdentifierId = identifier.id.toString(); + if (this.existingKycIdentifierId && this.existingKycIdentifierId === this.existingAddressProofIdentifierId) { + this.isAddressProofLinkedToKyc = true; + } // Restore selected chip + form values - const matchedProof = this.addressProofs.find( - (p) => p.name?.toLowerCase() === identifier.documentType?.name?.toLowerCase() - ); + const matchedProof = this.findMatchingProof(identifier.documentType?.name); if (matchedProof) { this.selectedAddrProof = matchedProof; this.permanentForm.patchValue({ diff --git a/src/app/clients/create-client/create-client.component.html b/src/app/clients/create-client/create-client.component.html index d87a5eea6..039ada14f 100644 --- a/src/app/clients/create-client/create-client.component.html +++ b/src/app/clients/create-client/create-client.component.html @@ -153,6 +153,12 @@ [isMinor]="clientGeneralStep?.isMinor || false" [isNonIndividual]="legalFormType === 2" [readOnly]="isTabReadOnly(addressTabIdx)" + [kycData]="clientKycStep?.kycForm?.value" + [kycDocCode]="clientKycStep?.selectedKycDoc?.code" + [kycDocFiles]="clientKycStep?.kycDocFiles || null" + [kycDocMergedPreview]="clientKycStep?.kycDocMergedPreview || null" + [existingKycDocSides]="clientKycStep?.existingKycDocSides || null" + [existingKycIdentifierId]="clientKycStep?.existingKycIdentifierId || null" >
diff --git a/src/app/clients/edit-client/edit-client.component.html b/src/app/clients/edit-client/edit-client.component.html index efbc8c74a..01d6bb123 100644 --- a/src/app/clients/edit-client/edit-client.component.html +++ b/src/app/clients/edit-client/edit-client.component.html @@ -289,6 +289,12 @@ [isMinor]="clientGeneralStep?.isMinor || false" [isNonIndividual]="legalFormType === 2" [readOnly]="isTabReadOnly(addressTabIdx)" + [kycData]="clientKyc?.kycForm?.value" + [kycDocCode]="clientKyc?.selectedKycDoc?.code" + [kycDocFiles]="clientKyc?.kycDocFiles || null" + [kycDocMergedPreview]="clientKyc?.kycDocMergedPreview || null" + [existingKycDocSides]="clientKyc?.existingKycDocSides || null" + [existingKycIdentifierId]="clientKyc?.existingKycIdentifierId || null" >
diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 543316953..fd0d2a130 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260814', - 'hash': '997e8920' + 'version': '260813', + 'hash': '5a6fb1f9' }, 'allow_switching_backend_instance': true };