- @if (clientViewData.annualTurnover != null && clientViewData.annualTurnover !== '') {
+ @if (clientViewData.annualTurnover !== null && clientViewData.annualTurnover !== '') {
₹ {{ clientViewData.annualTurnover | number }}
} @else {
-
diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts
index de9b603a2..3b7e201c4 100644
--- a/src/app/clients/clients.service.ts
+++ b/src/app/clients/clients.service.ts
@@ -576,11 +576,20 @@ export class ClientsService {
return this.http.get(`/clients/${clientId}/collaterals/template`);
}
- searchByText(text: string, page: number, pageSize: number, sortAttribute: string = '', sortDirection: string = '') {
+ searchByText(
+ text: string,
+ page: number,
+ pageSize: number,
+ sortAttribute: string = '',
+ sortDirection: string = '',
+ activeOnly: boolean = false
+ ) {
+ const innerRequest: any = { text };
+ if (activeOnly) {
+ innerRequest.activeOnly = true;
+ }
let request: any = {
- request: {
- text
- },
+ request: innerRequest,
page,
size: pageSize
};
diff --git a/src/app/clients/create-client/create-client.component.ts b/src/app/clients/create-client/create-client.component.ts
index 08f539a97..21571913f 100644
--- a/src/app/clients/create-client/create-client.component.ts
+++ b/src/app/clients/create-client/create-client.component.ts
@@ -72,9 +72,9 @@ export class CreateClientComponent {
@ViewChild(ClientGeneralStepComponent) clientGeneralStep: ClientGeneralStepComponent;
@ViewChild('clientAddress') clientAddressStep: ClientAddressStepComponent;
@ViewChild('clientIndiaAddress') clientIndiaAddressStep: ClientIndiaAddressStepComponent;
- @ViewChild('clientEntityKyc') clientEntityKycStep: ClientEntityKycStepComponent;
+ @ViewChild(ClientEntityKycStepComponent) clientEntityKycStep: ClientEntityKycStepComponent;
@ViewChild('clientKyc') clientKycStep: ClientKycStepComponent;
- @ViewChild('clientFamilyMembers') clientFamilyMembersStep: ClientFamilyMembersStepComponent;
+ @ViewChild(ClientFamilyMembersStepComponent) clientFamilyMembersStep: ClientFamilyMembersStepComponent;
@ViewChildren('dtclient') clientDatatables: QueryList;
datatables: any = [];
@@ -250,6 +250,13 @@ export class CreateClientComponent {
// KYC tab
const kycTabIndex = 1;
if (tabIndex === kycTabIndex) {
+ // Photograph + Signature are mandatory before leaving the KYC tab
+ if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
+ this.clientKycStep.markPhotoSigTouched();
+ this.validationError = 'Photograph and Signature are required before continuing.';
+ return;
+ }
+ this.validationError = '';
// Save KYC
if (this.clientKycStep?.hasKycData && !this.clientKycStep.kycId) {
this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({ error: () => {} });
@@ -265,6 +272,8 @@ export class CreateClientComponent {
.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile)
.subscribe({ error: () => {} });
}
+ // Persist the KYC document picker selection + uploaded files as a client identifier
+ this.saveKycDocProof(clientId);
return;
}
@@ -349,6 +358,14 @@ export class CreateClientComponent {
validationError = '';
submit() {
+ // Validate: photograph and signature are mandatory
+ if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
+ this.clientKycStep.markPhotoSigTouched();
+ this.validationError = 'Photograph and Signature are required before submitting.';
+ // Switch to KYC tab
+ this.selectedTabIndex = 1;
+ return;
+ }
// Validate: at least one family member required (Individual clients)
if (this.legalFormType === 1 && this.clientFamilyMembersStep && !this.clientFamilyMembersStep.hasFamilyMembers) {
this.validationError = 'At least one family member is required. Please add a family member before submitting.';
@@ -425,6 +442,9 @@ export class CreateClientComponent {
this.saveClientFamilyMembers(clientId);
}
+ // Persist the KYC document picker selection + uploaded files as a client identifier
+ this.saveKycDocProof(clientId);
+
// Save KYC only if it wasn't already created during the KYC step
if (this.clientKycStep?.hasKycData && !this.clientKycStep.kycId) {
this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({
@@ -514,6 +534,72 @@ export class CreateClientComponent {
},
error: () => {}
});
+
+ // Persist the selected proof of address as a client identifier (with attached files)
+ this.saveAddressProof(clientId);
+ }
+
+ /**
+ * Save the address-proof picked on the permanent tab as a client identifier
+ * + upload each selected file as a document attached to that identifier.
+ * Mirrors identities-tab.createIdentifierWithDocs.
+ */
+ private saveAddressProof(clientId: string) {
+ const payload = this.clientIndiaAddressStep?.addressProofPayload;
+ if (!payload?.proofId) return;
+
+ const proofName = payload.proof?.name || 'Address Proof';
+ const identifierData: any = {
+ documentTypeId: payload.proofId,
+ status: 'Active',
+ documentKey: payload.documentKey || `${proofName} - ${clientId}`,
+ description: payload.issueDate ? `Issue date: ${payload.issueDate}` : ''
+ };
+
+ this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
+ next: (res: any) => {
+ const identifierId = res?.resourceId;
+ if (!identifierId) return;
+ for (const entry of payload.files) {
+ const formData = new FormData();
+ formData.append('name', `${proofName} — ${entry.slot}`);
+ formData.append('file', entry.file);
+ this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
+ }
+ },
+ error: () => {}
+ });
+ }
+
+ /**
+ * Save the KYC document picked on the KYC tab as a client identifier
+ * + upload each selected file as a document attached to that identifier.
+ */
+ private saveKycDocProof(clientId: string) {
+ const payload = this.clientKycStep?.kycDocPayload;
+ if (!payload?.proofId || !payload?.files?.length) return;
+
+ const proofName = payload.proof?.name || 'KYC Document';
+ const identifierData: any = {
+ documentTypeId: payload.proofId,
+ status: 'Active',
+ documentKey: payload.documentKey || `${proofName} - ${clientId}`,
+ description: 'KYC document'
+ };
+
+ this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
+ next: (res: any) => {
+ const identifierId = res?.resourceId;
+ if (!identifierId) return;
+ for (const entry of payload.files) {
+ const formData = new FormData();
+ formData.append('name', `${proofName} — ${entry.slot}`);
+ formData.append('file', entry.file);
+ this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
+ }
+ },
+ error: () => {}
+ });
}
/**
diff --git a/src/app/clients/edit-client/edit-client.component.html b/src/app/clients/edit-client/edit-client.component.html
index 9dcdd46df..c63b1a3cb 100644
--- a/src/app/clients/edit-client/edit-client.component.html
+++ b/src/app/clients/edit-client/edit-client.component.html
@@ -67,8 +67,8 @@
warning
- Re-KYC Required: Date of birth was changed. Please re-verify PAN and update the remarks
- below before proceeding.
+ Re-KYC Required: Client name or date of birth was changed. Please re-verify PAN and
+ update the remarks below before proceeding.
}
@@ -180,6 +180,8 @@
[addressData]="clientIndiaAddressStep?.addressData"
[guardian]="clientGeneralStep?.selectedGuardian"
[guardianRelation]="clientGeneralStep?.createClientForm?.get('guardianDetails.guardianRelation')?.value || ''"
+ [entityClassificationData]="clientEntityKycStep?.classificationData"
+ [familyMembers]="clientFamilyMembersStep?.clientFamilyMembers || []"
[maritalStatusId]="clientGeneralStep?.createClientForm?.get('maritalStatusId')?.value"
[maritalStatusOptions]="clientGeneralStep?.maritalStatusOptions || []"
[educationLevelOptions]="clientGeneralStep?.educationLevelOptions || []"
diff --git a/src/app/clients/edit-client/edit-client.component.ts b/src/app/clients/edit-client/edit-client.component.ts
index 92f579192..c4a09aed4 100644
--- a/src/app/clients/edit-client/edit-client.component.ts
+++ b/src/app/clients/edit-client/edit-client.component.ts
@@ -69,9 +69,9 @@ export class EditClientComponent {
@ViewChild(ClientGeneralStepComponent) clientGeneralStep: ClientGeneralStepComponent;
@ViewChild('clientAddress') clientAddressStep: ClientAddressStepComponent;
@ViewChild('clientIndiaAddress') clientIndiaAddressStep: ClientIndiaAddressStepComponent;
- @ViewChild('clientEntityKyc') clientEntityKycStep: ClientEntityKycStepComponent;
+ @ViewChild(ClientEntityKycStepComponent) clientEntityKycStep: ClientEntityKycStepComponent;
@ViewChild('clientKyc') clientKycStep: ClientKycStepComponent;
- @ViewChild('clientFamilyMembers') clientFamilyMembersStep: ClientFamilyMembersStepComponent;
+ @ViewChild(ClientFamilyMembersStepComponent) clientFamilyMembersStep: ClientFamilyMembersStepComponent;
@ViewChildren('dtclient') clientDatatables: QueryList;
datatables: any = [];
@@ -184,13 +184,13 @@ export class EditClientComponent {
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
+ // Check if name or DOB changed on the General tab — warn about re-KYC
if (this.selectedTabIndex === 0) {
const changes = this.detectSensitiveFieldChanges(this.clientDataAndTemplate);
- if (changes.dobChanged) {
+ if (changes.nameChanged || 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';
+ 'Client name or 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';
}
}
@@ -232,11 +232,11 @@ export class EditClientComponent {
// 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) {
+ // 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.oldDob, changedFields.newDob);
+ this.flagReKycRequired(changedFields);
}
// Update original data snapshot so subsequent edits compare against latest
@@ -249,6 +249,13 @@ export class EditClientComponent {
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;
+ }
this.selectedTabIndex = this.selectedTabIndex + 1;
},
@@ -261,6 +268,11 @@ export class EditClientComponent {
// KYC tab (always index 1)
if (tabIndex === 1) {
+ // Photograph + Signature are mandatory before leaving the KYC tab
+ if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
+ this.clientKycStep.markPhotoSigTouched();
+ return;
+ }
this.savingClient = true;
// Upload photo if selected
@@ -317,6 +329,8 @@ export class EditClientComponent {
if (!kycId && res?.resourceId) {
this.clientKycStep.kycId = res.resourceId.toString();
}
+ // Persist KYC document picker selection + uploaded files as a client identifier
+ this.saveKycDocProof(this.clientId);
this.selectedTabIndex = this.selectedTabIndex + 1;
},
error: () => {
@@ -363,7 +377,7 @@ export class EditClientComponent {
}
/**
- * Compare current form values with original data to detect changes in DOB, mobile, email.
+ * Compare current form values with original data to detect changes in name, DOB, mobile, email.
*/
private detectSensitiveFieldChanges(originalData: any) {
const form = this.clientGeneralForm;
@@ -381,10 +395,38 @@ export class EditClientComponent {
? oldDobRaw.toISOString().split('T')[0]
: oldDobRaw || '';
+ const buildName = (parts: { firstname?: string; middlename?: string; lastname?: string; fullname?: string }) =>
+ (
+ parts.fullname ||
+ [
+ parts.firstname,
+ parts.middlename,
+ parts.lastname
+ ]
+ .filter((p) => p && p.trim())
+ .join(' ')
+ ).trim();
+
+ const newName = buildName({
+ 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 || '',
+ middlename: originalData.middlename || '',
+ lastname: originalData.lastname || '',
+ fullname: originalData.fullname || ''
+ });
+
return {
+ nameChanged: newName !== oldName,
mobileChanged: newMobile !== oldMobile,
emailChanged: newEmail !== oldEmail,
dobChanged: newDob !== oldDob,
+ oldName,
+ newName,
oldMobile,
newMobile,
oldEmail,
@@ -412,22 +454,38 @@ export class EditClientComponent {
}
/**
- * When DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC.
+ * When name or DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC.
*/
- private flagReKycRequired(oldDob: string, newDob: string) {
+ private flagReKycRequired(changes: {
+ nameChanged: boolean;
+ dobChanged: boolean;
+ oldName: string;
+ newName: string;
+ oldDob: string;
+ newDob: string;
+ }) {
+ const reasons: string[] = [];
+ if (changes.nameChanged) {
+ reasons.push(`Client name changed from "${changes.oldName}" to "${changes.newName}"`);
+ }
+ if (changes.dobChanged) {
+ reasons.push(`Date of birth changed from "${changes.oldDob}" to "${changes.newDob}"`);
+ }
+ const reasonText = reasons.join('; ');
+
// 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.`;
+ const remarkText = `[Re-KYC Required] ${reasonText}. PAN re-verification is mandatory.`;
this.clientKycStep.kycForm.get('remarks')?.setValue(remarkText);
}
- // Also log DOB change as an audit note
+ // Also log the 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.`;
+ const note = `[AUDIT] ${reasonText} on ${timestamp}. Re-KYC (PAN re-verification) required.`;
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
}
@@ -439,6 +497,13 @@ export class EditClientComponent {
* Submits the edit client form.
*/
submit() {
+ // Photograph + Signature are mandatory before submitting
+ if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
+ this.clientKycStep.markPhotoSigTouched();
+ this.selectedTabIndex = 1;
+ return;
+ }
+
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const clientData = {
@@ -496,10 +561,14 @@ export class EditClientComponent {
kycObs = this.clientsService.saveClientKyc(this.clientId, kycData);
}
kycObs.subscribe({
- next: () => navigateBack(),
+ next: () => {
+ this.saveKycDocProof(this.clientId);
+ navigateBack();
+ },
error: () => navigateBack()
});
} else {
+ this.saveKycDocProof(this.clientId);
navigateBack();
}
},
@@ -580,6 +649,72 @@ export class EditClientComponent {
},
error: () => {}
});
+
+ // Persist the selected proof of address as a client identifier (with attached files)
+ this.saveAddressProof(clientId);
+ }
+
+ /**
+ * Save the address-proof picked on the permanent tab as a client identifier
+ * + upload each selected file as a document attached to that identifier.
+ * Mirrors identities-tab.createIdentifierWithDocs.
+ */
+ private saveAddressProof(clientId: string) {
+ const payload = this.clientIndiaAddressStep?.addressProofPayload;
+ if (!payload?.proofId) return;
+
+ const proofName = payload.proof?.name || 'Address Proof';
+ const identifierData: any = {
+ documentTypeId: payload.proofId,
+ status: 'Active',
+ documentKey: payload.documentKey || `${proofName} - ${clientId}`,
+ description: payload.issueDate ? `Issue date: ${payload.issueDate}` : ''
+ };
+
+ this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
+ next: (res: any) => {
+ const identifierId = res?.resourceId;
+ if (!identifierId) return;
+ for (const entry of payload.files) {
+ const formData = new FormData();
+ formData.append('name', `${proofName} — ${entry.slot}`);
+ formData.append('file', entry.file);
+ this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
+ }
+ },
+ error: () => {}
+ });
+ }
+
+ /**
+ * Save the KYC document picked on the KYC tab as a client identifier
+ * + upload each selected file as a document attached to that identifier.
+ */
+ private saveKycDocProof(clientId: string) {
+ const payload = this.clientKycStep?.kycDocPayload;
+ if (!payload?.proofId || !payload?.files?.length) return;
+
+ const proofName = payload.proof?.name || 'KYC Document';
+ const identifierData: any = {
+ documentTypeId: payload.proofId,
+ status: 'Active',
+ documentKey: payload.documentKey || `${proofName} - ${clientId}`,
+ description: 'KYC document'
+ };
+
+ this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
+ next: (res: any) => {
+ const identifierId = res?.resourceId;
+ if (!identifierId) return;
+ for (const entry of payload.files) {
+ const formData = new FormData();
+ formData.append('name', `${proofName} — ${entry.slot}`);
+ formData.append('file', entry.file);
+ this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
+ }
+ },
+ error: () => {}
+ });
}
private saveClientFamilyMembers(clientId: string) {
diff --git a/src/app/clients/shared/kyc-proof-types.ts b/src/app/clients/shared/kyc-proof-types.ts
new file mode 100644
index 000000000..b2351fcbc
--- /dev/null
+++ b/src/app/clients/shared/kyc-proof-types.ts
@@ -0,0 +1,167 @@
+/**
+ * Copyright since 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+export interface ProofType {
+ id: string | number;
+ name: string;
+ code?: string;
+ sides?: number;
+ category?: string;
+ documentKeyLabel?: string;
+ documentKeyPlaceholder?: string;
+ requiresDate?: boolean;
+ info?: string;
+}
+
+/**
+ * Client-side enrichment for KYC proof types. Backend currently returns
+ * only { id, name, description (code), position, active, mandatory } — we
+ * derive sides / category / banner text / input labels from the code.
+ */
+export const PROOF_META: Record> = {
+ PAN: {
+ sides: 1,
+ documentKeyLabel: 'PAN number',
+ documentKeyPlaceholder: 'e.g. ABCDE1234F',
+ info: 'PAN card — upload front side only (single copy required).'
+ },
+ AADHAAR: {
+ sides: 2,
+ documentKeyLabel: '12-digit Aadhaar number',
+ documentKeyPlaceholder: 'XXXX XXXX XXXX',
+ category: 'Government-issued ID',
+ info: 'Aadhaar — both front and back copies mandatory as per UIDAI guidelines.'
+ },
+ PASSPORT: {
+ sides: 2,
+ documentKeyLabel: 'Passport number',
+ documentKeyPlaceholder: 'e.g. A1234567',
+ category: 'Government-issued ID',
+ info: 'Passport — upload front (photo page) and back (last page with address).'
+ },
+ VOTER_ID: {
+ sides: 2,
+ documentKeyLabel: 'EPIC number',
+ documentKeyPlaceholder: 'Voter ID number',
+ category: 'Government-issued ID',
+ info: 'Voter ID (EPIC) — both sides required as address appears on the back.'
+ },
+ DRIVING_LICENSE: {
+ sides: 2,
+ documentKeyLabel: 'Driving licence number',
+ documentKeyPlaceholder: 'DL number',
+ category: 'Government-issued ID',
+ info: 'Driving licence — both sides required; vehicle class and validity are on the back.'
+ },
+ NREGA_JOB_CARD: {
+ sides: 1,
+ documentKeyLabel: 'NREGA job card number',
+ documentKeyPlaceholder: 'Job card number',
+ info: 'NREGA job card — ensure government authority signature is visible.'
+ },
+ NPR_LETTER: {
+ sides: 1,
+ documentKeyLabel: 'Reference / letter number',
+ documentKeyPlaceholder: 'NPR reference number',
+ info: 'NPR letter — upload full document.'
+ },
+ UTILITY_BILL: {
+ sides: 1,
+ category: 'Utility bills',
+ requiresDate: true,
+ info: 'Utility bill — must not be older than 3 months.'
+ },
+ ELECTRICITY_BILL: {
+ sides: 1,
+ category: 'Utility bills',
+ requiresDate: true,
+ info: 'Electricity bill — must not be older than 3 months.'
+ },
+ WATER_BILL: {
+ sides: 1,
+ category: 'Utility bills',
+ requiresDate: true,
+ info: 'Water bill — must not be older than 3 months.'
+ },
+ GAS_BILL: {
+ sides: 1,
+ category: 'Utility bills',
+ requiresDate: true,
+ info: 'Gas bill — must not be older than 3 months.'
+ },
+ INTERNET_BILL: {
+ sides: 1,
+ category: 'Utility bills',
+ requiresDate: true,
+ info: 'Internet bill — must not be older than 3 months.'
+ },
+ POSTPAID_MOBILE_BILL: {
+ sides: 1,
+ category: 'Utility bills',
+ requiresDate: true,
+ info: 'Postpaid mobile bill — must not be older than 3 months.'
+ },
+ BANK_STATEMENT: {
+ sides: 1,
+ category: 'Financial & other',
+ requiresDate: true,
+ info: 'Bank statement / passbook — not older than 3 months.'
+ },
+ RENT_AGREEMENT: {
+ sides: 1,
+ category: 'Financial & other',
+ info: 'Registered rent agreement — upload full document. Registration stamp must be visible.'
+ },
+ PROPERTY_TAX_RECEIPT: {
+ sides: 1,
+ category: 'Financial & other',
+ requiresDate: true,
+ info: 'Property tax receipt — latest receipt required.'
+ },
+ EMPLOYER_LETTER: {
+ sides: 1,
+ category: 'Financial & other',
+ info: 'Employer letter — must be on official letterhead with HR/management signature.'
+ },
+ PENSION_PAYMENT_ORDER: {
+ sides: 1,
+ category: 'Financial & other',
+ info: 'Pension payment order — upload full document issued by pension authority.'
+ }
+};
+
+export function normalizeProofType(p: any): ProofType {
+ const code = (p.code || p.description || '').toString().toUpperCase();
+ const meta = PROOF_META[code] || {};
+ return {
+ id: p.id,
+ name: p.name,
+ code,
+ sides: p.sides || meta.sides || 1,
+ category: p.category || meta.category || 'Other',
+ documentKeyLabel: p.documentKeyLabel || meta.documentKeyLabel || 'Document number',
+ documentKeyPlaceholder: p.documentKeyPlaceholder || meta.documentKeyPlaceholder || '',
+ requiresDate: p.requiresDate ?? meta.requiresDate ?? false,
+ info: p.info || meta.info || ''
+ };
+}
+
+export function groupProofsByCategory(proofs: ProofType[]): { label: string; items: ProofType[] }[] {
+ const map = new Map();
+ proofs.forEach((p) => {
+ const key = p.category || 'Other';
+ if (!map.has(key)) map.set(key, []);
+ map.get(key)!.push(p);
+ });
+ return Array.from(map.entries()).map(
+ ([
+ label,
+ items
+ ]) => ({ label, items })
+ );
+}
diff --git a/src/app/clients/shared/kyc-upload-helper.service.ts b/src/app/clients/shared/kyc-upload-helper.service.ts
new file mode 100644
index 000000000..235a7e67b
--- /dev/null
+++ b/src/app/clients/shared/kyc-upload-helper.service.ts
@@ -0,0 +1,102 @@
+/**
+ * Copyright since 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { Injectable, inject } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { ImageCropperDialogComponent } from '../clients-view/custom-dialogs/image-cropper-dialog/image-cropper-dialog.component';
+
+@Injectable({ providedIn: 'root' })
+export class KycUploadHelperService {
+ private dialog = inject(MatDialog);
+
+ /**
+ * If the picked file is an image, open the cropper dialog and resolve with
+ * the cropped File (or null if cancelled). Non-image files (PDFs) are
+ * passed through unchanged.
+ */
+ pickWithCrop(file: File, title: string): Promise {
+ if (!file.type.startsWith('image/')) {
+ return Promise.resolve(file);
+ }
+ return new Promise((resolve) => {
+ const ref = this.dialog.open(ImageCropperDialogComponent, {
+ width: '720px',
+ maxWidth: '95vw',
+ data: { file, title }
+ });
+ ref.afterClosed().subscribe((cropped: File | null) => resolve(cropped || null));
+ });
+ }
+
+ /**
+ * Stack two images (front above back) into one PNG. Resolves with both
+ * the new File (suitable for upload) and a data-URL preview for the UI.
+ * Returns null if either side is not an image (e.g. one is a PDF).
+ */
+ mergeFrontBack(front: File, back: File, name: string): Promise<{ file: File; preview: string } | null> {
+ if (!front.type.startsWith('image/') || !back.type.startsWith('image/')) {
+ return Promise.resolve(null);
+ }
+ return Promise.all([
+ this.fileToImage(front),
+ this.fileToImage(back)
+ ]).then(
+ ([
+ fImg,
+ bImg
+ ]) => {
+ const targetWidth = Math.max(fImg.width, bImg.width);
+ const fScale = targetWidth / fImg.width;
+ const bScale = targetWidth / bImg.width;
+ const fH = fImg.height * fScale;
+ const bH = bImg.height * bScale;
+ const gap = 20;
+ const totalH = Math.round(fH + bH + gap);
+
+ const canvas = document.createElement('canvas');
+ canvas.width = targetWidth;
+ canvas.height = totalH;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return null;
+
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(fImg, 0, 0, targetWidth, fH);
+ ctx.drawImage(bImg, 0, fH + gap, targetWidth, bH);
+
+ return new Promise<{ file: File; preview: string } | null>((resolve) => {
+ canvas.toBlob((blob) => {
+ if (!blob) {
+ resolve(null);
+ return;
+ }
+ const file = new File([blob], `${name}-combined.png`, { type: 'image/png' });
+ const preview = canvas.toDataURL('image/png');
+ resolve({ file, preview });
+ }, 'image/png');
+ });
+ }
+ );
+ }
+
+ private fileToImage(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const url = URL.createObjectURL(file);
+ const img = new Image();
+ img.onload = () => {
+ URL.revokeObjectURL(url);
+ resolve(img);
+ };
+ img.onerror = () => {
+ URL.revokeObjectURL(url);
+ reject(new Error('image load failed'));
+ };
+ img.src = url;
+ });
+ }
+}
diff --git a/src/app/core/authentication/credentials.model.ts b/src/app/core/authentication/credentials.model.ts
index 597517cac..098729cad 100644
--- a/src/app/core/authentication/credentials.model.ts
+++ b/src/app/core/authentication/credentials.model.ts
@@ -23,6 +23,9 @@ export interface Credentials {
roles: any;
userId: number;
username: string;
+ /** Optional contact details — populated when the auth response includes them. */
+ email?: string;
+ mobileNo?: string;
shouldRenewPassword: boolean;
rememberMe?: boolean;
}
diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html
index 72c6c548f..8028aae7a 100644
--- a/src/app/core/shell/shell.component.html
+++ b/src/app/core/shell/shell.component.html
@@ -220,6 +220,9 @@
+
diff --git a/src/app/core/shell/toolbar/toolbar.component.html b/src/app/core/shell/toolbar/toolbar.component.html
index 5e59a8253..f73c1f5f5 100644
--- a/src/app/core/shell/toolbar/toolbar.component.html
+++ b/src/app/core/shell/toolbar/toolbar.component.html
@@ -342,6 +342,12 @@
{{ 'labels.menus.Profile' | translate }}
+