From a982083b148868cfe510e2c1e25d0a1707bdbaeb Mon Sep 17 00:00:00 2001
From: Anshul Rathore
Date: Fri, 31 Jul 2026 15:08:35 +0530
Subject: [PATCH 1/2] chore: stop tracking auto-generated .env.ts file
---
src/environments/.env.ts | 17 -----------------
1 file changed, 17 deletions(-)
delete mode 100644 src/environments/.env.ts
diff --git a/src/environments/.env.ts b/src/environments/.env.ts
deleted file mode 100644
index f94dce0fc..000000000
--- a/src/environments/.env.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * 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/.
- */
-// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
-/* tslint:disable */
-export default {
- 'mifos_x': {
- 'version': '260731',
- 'hash': 'ca85bae4'
- },
- 'allow_switching_backend_instance': true
-};
-/* tslint:enable */
From 983c613b8e2d5927830793f3e9b6c7d0050d2c95 Mon Sep 17 00:00:00 2001
From: Anshul Rathore
Date: Thu, 6 Aug 2026 10:41:28 +0530
Subject: [PATCH 2/2] feat: update changes
---
angular.json | 4 +-
...-family-member-child-dialog.component.html | 90 +++++++
...nt-family-member-child-dialog.component.ts | 98 ++++++++
...client-family-member-dialog.component.html | 28 +++
.../client-family-member-dialog.component.ts | 117 +++++++++-
.../client-kyc-step.component.ts | 15 +-
.../welcome-kit-dialog.component.html | 29 ++-
.../welcome-kit-dialog.component.ts | 24 ++
src/app/clients/clients.module.ts | 2 +
src/app/clients/clients.service.ts | 18 ++
.../edit-client/edit-client.component.html | 46 +++-
.../edit-client/edit-client.component.ts | 131 ++++++++---
.../share-holders.component.html | 10 +-
.../share-holders.component.scss | 204 +++++++++++-----
.../client-edit-email-dialog.component.html | 84 +++++++
.../client-edit-email-dialog.component.scss | 219 ++++++++++++++++++
.../client-edit-email-dialog.component.ts | 65 ++++++
.../pending-client-edits.component.html | 25 ++
.../pending-client-edits.component.scss | 38 +++
.../pending-client-edits.component.ts | 43 +++-
.../users/dsc/dsc-list/dsc-list.component.ts | 4 +-
21 files changed, 1189 insertions(+), 105 deletions(-)
create mode 100644 src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.html
create mode 100644 src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.ts
create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.html
create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.scss
create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.ts
diff --git a/angular.json b/angular.json
index 1fe5293cf..fd43e003d 100644
--- a/angular.json
+++ b/angular.json
@@ -69,8 +69,8 @@
"budgets": [
{
"type": "initial",
- "maximumWarning": "13mb",
- "maximumError": "14mb"
+ "maximumWarning": "15mb",
+ "maximumError": "16mb"
},
{
"type": "anyComponentStyle",
diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.html
new file mode 100644
index 000000000..a681b04e5
--- /dev/null
+++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.ts
new file mode 100644
index 000000000..6e457afbb
--- /dev/null
+++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component.ts
@@ -0,0 +1,98 @@
+import { Component, Inject, OnInit } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
+
+import { MatIcon } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'cbs-client-family-member-child-dialog',
+ templateUrl: './client-family-member-child-dialog.component.html',
+ styleUrls: ['../client-family-member-dialog/client-family-member-dialog.component.scss'],
+ standalone: true,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ MatDialogModule,
+ MatIcon,
+ MatButtonModule,
+ MatInputModule,
+ MatSelectModule,
+ MatDatepickerModule,
+ MatTooltipModule
+ ]
+})
+export class ClientFamilyMemberChildDialogComponent implements OnInit {
+
+ childrenForm: UntypedFormGroup;
+ genderIdOptions: any[];
+ maxDate: Date = new Date();
+
+ constructor(
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: any,
+ private formBuilder: UntypedFormBuilder
+ ) {
+ this.genderIdOptions = data.genderIdOptions;
+
+ this.childrenForm = this.formBuilder.group({
+ children: this.formBuilder.array([])
+ });
+ }
+
+ ngOnInit() {
+ if (this.data.children && this.data.children.length > 0) {
+ this.data.children.forEach((child: any) => {
+ this.children.push(this.formBuilder.group({
+ id: [child.id],
+ fullName: [child.fullName, Validators.required],
+ genderId: [child.genderId, Validators.required],
+ dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
+ nationality: [child.nationality || 'INDIAN'],
+ aadhaarNumber: [child.aadhaarNumber || ''],
+ panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
+ }));
+ });
+ } else {
+ this.addChild();
+ }
+ }
+
+ get children(): UntypedFormArray {
+ return this.childrenForm.get('children') as UntypedFormArray;
+ }
+
+ createChildFormGroup(): UntypedFormGroup {
+ return this.formBuilder.group({
+ id: [''],
+ fullName: ['', Validators.required],
+ genderId: ['', Validators.required],
+ dateOfBirth: [''],
+ nationality: ['INDIAN'],
+ aadhaarNumber: [''],
+ panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
+ });
+ }
+
+ addChild(): void {
+ this.children.push(this.createChildFormGroup());
+ }
+
+ removeChild(index: number): void {
+ this.children.removeAt(index);
+ }
+
+ submit(): void {
+ if (this.childrenForm.valid) {
+ this.dialogRef.close(this.children.value);
+ }
+ }
+
+}
diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html
index d5b1e8492..6b3c5ea75 100644
--- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html
+++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html
@@ -139,6 +139,21 @@
{{ familyMemberForm.get('remarks')?.value }}
}
+ @if (isSpouse && familyMemberForm.get('children')?.value?.length > 0) {
+
+ }
}
@@ -447,6 +462,19 @@
Remarks
+
+
+ @if (isSpouse) {
+ Children
+
+ Does this family member have a child/children?
+ @if (familyMemberForm.get('hasChildren')?.value) {
+
+ }
+
+ }
}
diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts
index 552c39daf..49930b556 100644
--- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts
+++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts
@@ -17,7 +17,7 @@ import {
MatDialogClose,
MatDialogContent
} from '@angular/material/dialog';
-import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
+import { UntypedFormGroup, UntypedFormBuilder, UntypedFormArray, Validators } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { ClientsService } from 'app/clients/clients.service';
@@ -58,6 +58,8 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
private dialog = inject(MatDialog);
maxDate = new Date();
+
+ hasChildrenChecked = false;
familyMemberForm: UntypedFormGroup;
/** Client search */
@@ -197,6 +199,23 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
// Re-fetch linked client details (profile, signature, KYC status)
this.fetchLinkedClientData(m.linkedClientId.toString());
}
+ if (m.children && m.children.length > 0) {
+ this.hasChildrenChecked = true;
+ this.familyMemberForm.patchValue({ hasChildren: true });
+ m.children.forEach((child: any) => {
+ this.children.push(
+ this.formBuilder.group({
+ id: [child.id],
+ fullName: [child.fullName, Validators.required],
+ genderId: [child.genderId, Validators.required],
+ dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
+ nationality: [child.nationality || 'INDIAN'],
+ aadhaarNumber: [child.aadhaarNumber || ''],
+ panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
+ })
+ );
+ });
+ }
if (this.isViewMode) {
this.familyMemberForm.disable();
}
@@ -225,8 +244,83 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
isAlive: [true],
marriageDate: [''],
marriageAuthority: [''],
- linkedClientId: ['']
+ linkedClientId: [''],
+ hasChildren: [false],
+ children: this.formBuilder.array([])
});
+
+ this.familyMemberForm.get('hasChildren')?.valueChanges.subscribe(value => {
+ if (value && !this.hasChildrenChecked) {
+ this.openChildrenDialog();
+ } else if (!value) {
+ this.hasChildrenChecked = false;
+ this.children.clear();
+ }
+ });
+ }
+
+ get children(): UntypedFormArray {
+ return this.familyMemberForm.get('children') as UntypedFormArray;
+ }
+
+ async openChildrenDialog() {
+ // dynamically import the dialog to avoid circular dependency issues if any
+ const { ClientFamilyMemberChildDialogComponent } = await import('../client-family-member-child-dialog/client-family-member-child-dialog.component');
+
+ const dialogRef = this.dialog.open(ClientFamilyMemberChildDialogComponent, {
+ data: {
+ children: this.children.value,
+ genderIdOptions: this.data.options.genderIdOptions
+ },
+ width: '500px'
+ });
+
+ dialogRef.afterClosed().subscribe((result: any[]) => {
+ if (result) {
+ this.children.clear();
+ result.forEach(child => {
+ this.children.push(this.formBuilder.group({
+ id: [child.id],
+ fullName: [child.fullName, Validators.required],
+ genderId: [child.genderId, Validators.required],
+ dateOfBirth: [child.dateOfBirth],
+ nationality: [child.nationality || 'INDIAN'],
+ aadhaarNumber: [child.aadhaarNumber || ''],
+ panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
+ }));
+ });
+ this.hasChildrenChecked = true;
+ this.familyMemberForm.patchValue({ hasChildren: true }, { emitEvent: false });
+ } else {
+ if (this.children.length === 0) {
+ this.hasChildrenChecked = false;
+ this.familyMemberForm.patchValue({ hasChildren: false }, { emitEvent: false });
+ }
+ }
+ });
+ }
+
+ createChildFormGroup(): UntypedFormGroup {
+ return this.formBuilder.group({
+ id: [''],
+ fullName: ['', Validators.required],
+ genderId: ['', Validators.required],
+ dateOfBirth: [''],
+ nationality: ['INDIAN'],
+ aadhaarNumber: [''],
+ panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
+ });
+ }
+
+ addChild(): void {
+ this.children.push(this.createChildFormGroup());
+ }
+
+ removeChild(index: number): void {
+ this.children.removeAt(index);
+ if (this.children.length === 0) {
+ this.familyMemberForm.patchValue({ hasChildren: false });
+ }
}
/** Handle marriage certificate file selection */
@@ -481,6 +575,25 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
member._linkedClient = this.selectedClient;
}
+ // Format children dates
+ if (this.isSpouse && member.children && member.children.length > 0) {
+ member.children = member.children.map((child: any) => {
+ const c = { ...child };
+ if (c.dateOfBirth instanceof Date) {
+ c.dateOfBirth = this.dateUtils.formatDate(c.dateOfBirth, dateFormat);
+ } else if (!c.dateOfBirth) {
+ delete c.dateOfBirth;
+ }
+ if (c.panNumber) {
+ c.panNumber = c.panNumber.toUpperCase();
+ }
+ return c;
+ });
+ } else {
+ delete member.children;
+ }
+ delete member.hasChildren;
+
// Attach marriage certificate file if present
if (this.marriageCertFile) {
member._marriageCertFile = this.marriageCertFile;
diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
index 3ebaea616..c062d834f 100644
--- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
+++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
@@ -144,7 +144,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
kycDocumentName: 'KYC Document Type',
kycDocumentNumber: 'KYC Document Number',
amlCheckStatus: 'AML Check Status',
- bannedEntityMatch: 'Banned Entity List Match'
+ bannedEntityMatch: 'Banned Entity List Match',
+ remarks: 'Reason for Re-edit'
};
/** Names every currently-invalid `kycForm` control (required-but-empty, or an entered value
@@ -415,8 +416,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
aadhaarNumber: [
'',
[
- Validators.pattern(/^\d{12}$/),
- Validators.maxLength(12)
+ Validators.pattern(/^(\d{12}|[X•*\s\d]{12,14})$/),
+ Validators.maxLength(14)
]
],
panAadhaarSeedingStatus: [''],
@@ -890,6 +891,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
form15Type: data.form15Type || '15G',
form15FY: data.form15FY || ''
});
+
+ // Set remarks as required if KYC is verified
+ if (this.isKycVerified) {
+ this.kycForm.get('remarks')?.setValidators(Validators.required);
+ this.kycForm.get('remarks')?.updateValueAndValidity();
+ this.kycForm.get('remarks')?.markAsTouched(); // Force red underline so user knows it's blocking
+ }
+
// Sync chip-picker selection with the prefilled document name
if (data.kycDocumentName) {
this.selectedKycDoc = this.kycDocProofs.find((p) => p.name === data.kycDocumentName) || null;
diff --git a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html
index d7f979973..2ad771dcb 100644
--- a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html
+++ b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html
@@ -36,7 +36,34 @@
Status:
{{ dispatch.status }}
-
+
+
+
+
+
+
+
+
+
0 && welcomeKitsData?.canDispatchNew" />
diff --git a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts
index b3d72602b..0ef5ca2d5 100644
--- a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts
+++ b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts
@@ -165,6 +165,30 @@ export class WelcomeKitDialogComponent implements OnInit {
});
}
+ canUpdateStatus(status: string): boolean {
+ return status === 'DISPATCHED' || status === 'IN_TRANSIT';
+ }
+
+ updateStatus(dispatch: any, newStatus: string) {
+ const label = newStatus === 'DELIVERED' ? 'Delivered' : 'Returned';
+ const confirmed = confirm(`Are you sure you want to mark this as ${label}? This action cannot be undone from the UI.`);
+ if (!confirmed) {
+ return;
+ }
+
+ this.isLoading = true;
+ this.clientsService.updateWelcomeKitStatus(dispatch.id, newStatus).subscribe({
+ next: () => {
+ this.snackBar.open(`Welcome Kit marked as ${label} successfully`, 'Close', { duration: 3000 });
+ this.fetchData();
+ },
+ error: () => {
+ this.isLoading = false;
+ this.snackBar.open(`Failed to mark Welcome Kit as ${label}`, 'Close', { duration: 3000 });
+ }
+ });
+ }
+
getDisplayAddress(dispatch: any): string {
const address = dispatch.deliveryAddress || '';
const name = this.welcomeKitsData?.clientName || '';
diff --git a/src/app/clients/clients.module.ts b/src/app/clients/clients.module.ts
index 5171c0131..bd13c7eca 100644
--- a/src/app/clients/clients.module.ts
+++ b/src/app/clients/clients.module.ts
@@ -62,6 +62,7 @@ import { ClientFamilyMembersStepComponent } from './client-stepper/client-family
import { ClientPreviewStepComponent } from './client-stepper/client-preview-step/client-preview-step.component';
import { ClientAddressStepComponent } from './client-stepper/client-address-step/client-address-step.component';
import { ClientFamilyMemberDialogComponent } from './client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
+import { ClientFamilyMemberChildDialogComponent } from './client-stepper/client-family-members-step/client-family-member-child-dialog/client-family-member-child-dialog.component';
import { CaptureImageDialogComponent } from './clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component';
import { AddClientCollateralComponent } from './clients-view/client-actions/add-client-collateral/add-client-collateral.component';
import { ClientDatatableStepComponent } from './client-stepper/client-datatable-step/client-datatable-step.component';
@@ -127,6 +128,7 @@ import { Form60TabComponent } from './clients-view/form60-tab/form60-tab.compone
ClientPreviewStepComponent,
ClientAddressStepComponent,
ClientFamilyMemberDialogComponent,
+ ClientFamilyMemberChildDialogComponent,
CaptureImageDialogComponent,
AddClientCollateralComponent,
ClientDatatableStepComponent,
diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts
index 14a2efc49..619316799 100644
--- a/src/app/clients/clients.service.ts
+++ b/src/app/clients/clients.service.ts
@@ -123,6 +123,20 @@ export class ClientsService {
return this.http.post(`/clients/${clientId}/edit/reject`, { rejectReason: rejectReason || '' });
}
+ /** Fetches the exact email content (subject, body, sentAt, openedAt) for a given pending edit.
+ * Only populated after the Checker has approved and the send succeeded. */
+ getClientEditEmailPreview(clientId: string | number, pendingEditId: string | number) {
+ return this.http.get<{
+ emailSubject: string | null;
+ emailBody: string | null;
+ emailSent: boolean;
+ emailSentAt: string | null;
+ emailOpenedAt: string | null;
+ emailFrom: string | null;
+ emailTo: string | null;
+ }>(`/clients/${clientId}/edit/${pendingEditId}/email-preview`);
+ }
+
getClientDataAndTemplate(clientId: string) {
const httpParams = new HttpParams().set('template', 'true').set('staffInSelectedOfficeOnly', 'true');
return this.http.get(`/clients/${clientId}`, { params: httpParams });
@@ -958,4 +972,8 @@ export class ClientsService {
dispatchWelcomeKit(clientId: string | number, data: any): Observable {
return this.http.post(`/clients/${clientId}/welcomekit`, data);
}
+
+ updateWelcomeKitStatus(welcomeKitId: number, status: string): Observable {
+ return this.http.put(`/welcomekit/${welcomeKitId}`, { status });
+ }
}
diff --git a/src/app/clients/edit-client/edit-client.component.html b/src/app/clients/edit-client/edit-client.component.html
index 92740d87c..748eba1fa 100644
--- a/src/app/clients/edit-client/edit-client.component.html
+++ b/src/app/clients/edit-client/edit-client.component.html
@@ -84,14 +84,46 @@
phone_iphone
Mobile Number
+
+
+ Old mobile number not accessible (lost / deactivated SIM)
+
+
-
+
-
+
All Branches
@@ -33,7 +33,7 @@
-
+
All Products
@@ -42,7 +42,7 @@
-
+
All Status
Active
@@ -53,7 +53,7 @@
-
+
diff --git a/src/app/shares/share-holders/share-holders.component.scss b/src/app/shares/share-holders/share-holders.component.scss
index 6d41b20a0..27c01a151 100644
--- a/src/app/shares/share-holders/share-holders.component.scss
+++ b/src/app/shares/share-holders/share-holders.component.scss
@@ -5,37 +5,43 @@
* 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/.
*/
+
+:host {
+ display: block;
+ width: 100%;
+}
+
.container {
- margin: 1.5rem;
- padding: 0;
- border-radius: var(--radius-xl);
- background: var(--bg-card);
- box-shadow: var(--shadow-sm);
+ margin: 1.5rem auto;
+ max-width: 1400px;
+ padding: 20px 24px;
+ border-radius: 16px;
+ background: #ffffff;
+ border: 1px solid #e2e8f0;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+ box-sizing: border-box;
}
.table-header {
- padding: 1.5rem;
- border-bottom: 1px solid var(--border-light);
+ margin-bottom: 20px;
.table-header-row {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 1.5rem;
- flex-wrap: wrap;
- gap: 1rem;
+ margin-bottom: 16px;
}
.table-title-group {
display: flex;
align-items: center;
- gap: 1rem;
+ gap: 12px;
.table-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
- color: var(--text-main);
+ color: #0f172a;
display: flex;
align-items: center;
gap: 0.75rem;
@@ -45,83 +51,175 @@
width: 24px;
height: 24px;
position: relative;
- display: flex;
- flex-wrap: wrap;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
gap: 2px;
padding: 2px;
- background: var(--bg-hover);
+ background: #f1f5f9;
border-radius: 6px;
span {
- width: 8px;
- height: 8px;
border-radius: 2px;
- background: var(--primary-main);
- opacity: 0.8;
+ background: #1353a0;
- &:nth-child(2) {
- opacity: 0.6;
- }
-
- &:nth-child(3) {
- opacity: 0.4;
- }
-
- &:nth-child(4) {
- opacity: 0.2;
- }
+ &:nth-child(2) { opacity: 0.7; }
+ &:nth-child(3) { opacity: 0.4; }
+ &:nth-child(4) { opacity: 0.2; }
}
}
}
.table-count {
- padding: 0.25rem 0.75rem;
- background: var(--bg-hover);
- color: var(--text-muted);
- border-radius: var(--radius-full);
+ padding: 0.2rem 0.65rem;
+ background: #f1f5f9;
+ color: #64748b;
+ border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
- letter-spacing: 0.02em;
+ font-family: 'Roboto Mono', monospace;
}
}
+ /* Pill-styled filter bar */
.table-filters {
display: flex;
- gap: 1rem;
align-items: center;
+ gap: 10px;
flex-wrap: wrap;
+ padding: 8px 12px;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
- mat-form-field {
- width: 240px;
- margin-bottom: -1.25em;
+ .table-search {
+ flex: 1 1 200px;
+ min-width: 180px;
+ max-width: 280px;
+ margin: 0;
+ }
+
+ .table-filter {
+ flex: 0 1 150px;
+ min-width: 130px;
+ max-width: 170px;
+ margin: 0;
+ }
+
+ /* Override Material Form Field Heights & Subscript Spacing */
+ ::ng-deep {
+ .mat-mdc-form-field {
+ margin: 0;
+ }
+
+ .mat-mdc-form-field-subscript-wrapper {
+ display: none !important;
+ height: 0 !important;
+ }
+
+ .mat-mdc-form-field-flex {
+ height: 38px !important;
+ background: #ffffff !important;
+ align-items: center;
+ }
+
+ .mat-mdc-form-field-infix {
+ padding: 6px 0 !important;
+ min-height: 38px !important;
+ }
+
+ .mdc-notched-outline__leading {
+ border-radius: 8px 0 0 8px !important;
+ }
+
+ .mdc-notched-outline__trailing {
+ border-radius: 0 8px 8px 0 !important;
+ }
+
+ .mat-mdc-select-value,
+ .mat-mdc-select-placeholder,
+ input {
+ font-size: 13px !important;
+ }
+ }
+
+ button[mat-icon-button] {
+ color: #64748b;
+ width: 36px;
+ height: 36px;
+
+ &:hover {
+ color: #0f172a;
+ background: #e2e8f0;
+ }
}
}
}
.table-container {
- position: relative;
- overflow: auto;
- border-radius: 0 0 var(--radius-xl) var(--radius-xl);
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ overflow: hidden;
+ background: #ffffff;
.table-loader {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- z-index: 10;
+ height: 2px;
}
}
.modern-table {
width: 100%;
- background: transparent;
+ border-collapse: separate;
+ border-spacing: 0;
- tr.mat-row {
- cursor: pointer;
- transition: background-color 0.2s ease;
+ ::ng-deep {
+ .mat-mdc-header-row {
+ background: #f8fafc;
+ min-height: 38px;
+ height: 38px;
+ }
- &:hover {
- background-color: var(--bg-hover);
+ .mat-mdc-header-cell {
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: #64748b;
+ border-bottom: 1px solid #e2e8f0;
+ padding: 0 16px;
+ white-space: nowrap;
+ }
+
+ .mat-mdc-row {
+ min-height: 42px;
+ height: 42px;
+ border-bottom: 1px solid #f1f5f9;
+ transition: background-color 0.15s ease;
+ cursor: pointer;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:nth-child(even) {
+ background: #fafbfc;
+ }
+
+ &:hover {
+ background-color: #f1f5f9 !important;
+ }
+ }
+
+ .mat-mdc-cell {
+ font-size: 13px;
+ font-weight: 400;
+ color: #0f172a;
+ padding: 0 16px;
+ white-space: nowrap;
}
}
}
+
+mat-paginator {
+ border-top: 1px solid #e2e8f0;
+ background: #ffffff;
+}
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.html
new file mode 100644
index 000000000..a4b311eee
--- /dev/null
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.html
@@ -0,0 +1,84 @@
+
+
+
+ ✉
+ Approval Email
+
+
+ @if (data.emailSent) {
+
+ Sent
+
+ } @else {
+
+ Not sent
+
+ }
+
+ @if (data.emailOpenedAt) {
+
+ Seen
+
+ } @else {
+
+ Not yet seen
+
+ }
+
+
+
+
+
+
+
+
+
+ @if (data.emailBody) {
+
+ } @else {
+
+
No email content available.
+ @if (!data.emailSent) {
+
The email has not been sent yet for this edit.
+ }
+
+ }
+
+
+
+
+
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.scss
new file mode 100644
index 000000000..f50f1cd82
--- /dev/null
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.scss
@@ -0,0 +1,219 @@
+/**
+ * Copyright since 2025 Mifos Initiative
+ * MPL-2.0
+ */
+
+:host {
+ display: block;
+}
+
+.ced-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 1.15rem;
+ font-weight: 600;
+ color: #1a237e;
+ position: relative; /* For aligning badges to the right */
+
+ &__icon {
+ font-size: 1.3rem;
+ }
+}
+
+.ced-content {
+ min-width: 560px;
+ max-width: 760px;
+ max-height: 72vh;
+ overflow-y: auto;
+ padding: 0 4px;
+}
+
+/* ── Gmail-Style Email Header ─────────────────────────────────────────────────── */
+.ced-gmail-header {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 30px;
+ padding: 0 4px;
+ font-family: 'Roboto', 'Inter', sans-serif; /* Gmail-like typography */
+}
+
+.ced-gmail-subject {
+ font-size: 1.35rem;
+ font-weight: 400;
+ color: #202124;
+ margin: 0 0 24px 0;
+ line-height: 1.4;
+}
+
+.ced-gmail-meta {
+ display: flex;
+ align-items: center; /* Avatar vertically centered with text */
+ gap: 12px;
+}
+
+.ced-gmail-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: #f06292;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.ced-gmail-avatar-letter {
+ color: white;
+ font-size: 1.25rem;
+ font-weight: 500;
+}
+
+.ced-gmail-meta-content {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: center;
+ gap: 2px;
+}
+
+.ced-gmail-meta-top {
+ display: flex;
+ align-items: baseline;
+ width: 100%;
+}
+
+.ced-gmail-spacer {
+ flex-grow: 1;
+}
+
+.ced-gmail-sender-name {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #202124;
+ margin-right: 6px;
+}
+
+.ced-gmail-sender-email {
+ color: #5f6368;
+ font-size: 0.8rem;
+}
+
+.ced-gmail-date {
+ color: #5f6368;
+ font-size: 0.75rem;
+ white-space: nowrap;
+}
+
+.ced-gmail-meta-bottom {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.ced-gmail-to {
+ font-size: 0.75rem;
+ color: #5f6368;
+}
+
+.ced-gmail-caret {
+ font-size: 0.75rem;
+ color: #5f6368;
+ cursor: pointer;
+}
+
+.ced-status-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-left: auto; /* Pushes badges to the right in the flex container */
+}
+
+/* ── Status chips ───────────────────────────────────────────────────────── */
+.ced-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px 4px 10px;
+ border-radius: 20px;
+ font-size: 0.82rem;
+ font-weight: 500;
+
+ &__dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ }
+
+ &__icon {
+ font-size: 0.82rem;
+ }
+
+ &--sent {
+ background: #e8f5e9;
+ color: #1b5e20;
+ .ced-chip__dot { background: #43a047; }
+ }
+
+ &--notsent {
+ background: #f5f5f5;
+ color: #757575;
+ .ced-chip__dot { background: #bdbdbd; }
+ }
+
+ &--seen {
+ background: #e3f2fd;
+ color: #0d47a1;
+ }
+
+ &--notseen {
+ background: #f5f5f5;
+ color: #9e9e9e;
+ }
+}
+
+/* ── Divider ────────────────────────────────────────────────────────────── */
+.ced-divider {
+ border: none;
+ border-top: 1px solid #eeeeee;
+ margin: 12px 0;
+}
+
+/* ── Email body iframe-like container ───────────────────────────────────── */
+.ced-email-body {
+ background: #fafafa;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 20px 24px;
+ font-family: Arial, sans-serif;
+ font-size: 0.93rem;
+ line-height: 1.6;
+ color: #333;
+
+ // Reset any Angular Material overrides inside the rendered HTML
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+
+ td, th {
+ padding: 6px 12px;
+ border: 1px solid #e0e0e0;
+ }
+
+ p { margin: 0 0 10px; }
+ ul { padding-left: 20px; }
+}
+
+/* ── Empty state ────────────────────────────────────────────────────────── */
+.ced-empty {
+ text-align: center;
+ padding: 32px 0;
+ color: #9e9e9e;
+
+ &__hint {
+ font-size: 0.85rem;
+ margin-top: 6px;
+ }
+}
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.ts
new file mode 100644
index 000000000..e2a18cb2d
--- /dev/null
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/client-edit-email-dialog/client-edit-email-dialog.component.ts
@@ -0,0 +1,65 @@
+/**
+ * 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 { Component, inject } from '@angular/core';
+import {
+ MAT_DIALOG_DATA,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose
+} from '@angular/material/dialog';
+import { CdkScrollable } from '@angular/cdk/scrolling';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+
+export interface ClientEditEmailDialogData {
+ emailSubject: string | null;
+ emailBody: string | null;
+ emailSent: boolean;
+ emailSentAt: string | null;
+ emailOpenedAt: string | null;
+ emailFrom: string | null;
+ emailTo: string | null;
+}
+
+/** Displays the exact confirmation email sent to the client when their pending edit was approved.
+ * Shows subject, HTML body (rendered), sent timestamp, and read/seen status. */
+@Component({
+ selector: 'mifosx-client-edit-email-dialog',
+ templateUrl: './client-edit-email-dialog.component.html',
+ styleUrls: ['./client-edit-email-dialog.component.scss'],
+ standalone: true,
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ MatDialogTitle,
+ CdkScrollable,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose,
+ FaIconComponent
+ ]
+})
+export class ClientEditEmailDialogComponent {
+ data = inject(MAT_DIALOG_DATA);
+ private sanitizer = inject(DomSanitizer);
+
+ get safeBody(): SafeHtml {
+ return this.sanitizer.bypassSecurityTrustHtml(this.data.emailBody ?? '');
+ }
+
+ formatDate(isoDate: string | null | undefined): string {
+ if (!isoDate) return '—';
+ const d = new Date(isoDate);
+ return d.toLocaleString('en-IN', {
+ day: '2-digit', month: 'short', year: 'numeric',
+ hour: '2-digit', minute: '2-digit', hour12: true
+ });
+ }
+}
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.html
index 98f557aed..19470635b 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.html
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.html
@@ -126,6 +126,25 @@
Actions |
+
+
+
+ {{ edit.emailSent ? 'Sent' : 'Not Sent' }}
+
+ @if (edit.emailSent) {
+
+
+ {{ edit.emailOpenedAt ? 'Seen' : 'Not Seen' }}
+
+ }
+
+
@@ -137,6 +156,12 @@
Reject
}
+ @if (edit.emailSent) {
+
+ }
|
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.scss
index 61ffa8119..22992daa7 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.scss
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.scss
@@ -346,3 +346,41 @@ $text-muted: #64748b;
line-height: 1.5;
}
}
+
+/* ── Email badges ───────────────────────────────────────────────────────── */
+.pce-email-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.72rem;
+ font-weight: 600;
+ margin-right: 6px;
+ white-space: nowrap;
+ vertical-align: middle;
+
+ fa-icon { font-size: 0.7rem; }
+
+ &--sent {
+ background: #e8f5e9;
+ color: #1b5e20;
+ border: 1px solid #c8e6c9;
+ }
+ &--nosent {
+ background: #f5f5f5;
+ color: #9e9e9e;
+ border: 1px solid #e0e0e0;
+ }
+ &--seen {
+ background: #e3f2fd;
+ color: #0d47a1;
+ border: 1px solid #bbdefb;
+ }
+ &--notseen {
+ background: #fff3e0;
+ color: #e65100;
+ border: 1px solid #ffe0b2;
+ }
+}
+
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.ts
index c3fdc1c3b..a9dbab2fa 100644
--- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.ts
+++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-client-edits/pending-client-edits.component.ts
@@ -34,6 +34,7 @@ import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { NON_COMPARABLE_KEYS, PENDING_EDIT_FIELD_LABELS } from 'app/clients/shared/client-edit-diff.util';
import { ClientEditCompareDialogComponent } from './client-edit-compare-dialog/client-edit-compare-dialog.component';
+import { ClientEditEmailDialogComponent } from './client-edit-email-dialog/client-edit-email-dialog.component';
/** Maps a payload key that holds a CodeValue/template id to the options array it should be
* resolved against — populated from /clients/template plus the same per-code-value lookups the
@@ -97,7 +98,8 @@ const NATIONALITY_LABELS: Record = {
MatRowDef,
MatRow,
MatIcon,
- MatPaginatorModule
+ MatPaginatorModule,
+ ClientEditEmailDialogComponent
]
})
export class PendingClientEditsComponent implements OnInit {
@@ -423,6 +425,45 @@ export class PendingClientEditsComponent implements OnInit {
});
}
+ /** Opens the email preview dialog for an edit row. Fetches the email content first (subject,
+ * body, sent/opened timestamps) via the email-preview endpoint, then opens the dialog. */
+ viewEmail(edit: any): void {
+ this.clientsService.getClientEditEmailPreview(edit.clientId, edit.id).subscribe({
+ next: (preview) => {
+ try {
+ this.dialog.open(ClientEditEmailDialogComponent, {
+ width: '800px',
+ maxWidth: '96vw',
+ data: {
+ emailSubject: preview?.emailSubject || null,
+ emailBody: preview?.emailBody || null,
+ emailSent: preview?.emailSent || false,
+ emailSentAt: preview?.emailSentAt || null,
+ emailOpenedAt: preview?.emailOpenedAt || null,
+ emailFrom: preview?.emailFrom || null,
+ emailTo: preview?.emailTo || null
+ }
+ });
+ } catch (e: any) {
+ console.error('Dialog open failed (next): ', e);
+ }
+ },
+ error: () => {
+ try {
+ // Fallback: open dialog with no-content state
+ this.dialog.open(ClientEditEmailDialogComponent, {
+ width: '800px',
+ maxWidth: '96vw',
+ data: { emailSubject: null, emailBody: null, emailSent: false,
+ emailSentAt: null, emailOpenedAt: null, emailFrom: null, emailTo: null }
+ });
+ } catch (e: any) {
+ console.error('Dialog open failed (error): ', e);
+ }
+ }
+ });
+ }
+
reload(): void {
this.load();
}
diff --git a/src/app/users/dsc/dsc-list/dsc-list.component.ts b/src/app/users/dsc/dsc-list/dsc-list.component.ts
index 429c65c72..aa2fb82e3 100644
--- a/src/app/users/dsc/dsc-list/dsc-list.component.ts
+++ b/src/app/users/dsc/dsc-list/dsc-list.component.ts
@@ -21,7 +21,7 @@ import {
MatRow
} from '@angular/material/table';
import { MatButton, MatIconButton } from '@angular/material/button';
-import { MatIcon } from '@angular/material/icon';
+import { MatIconModule } from '@angular/material/icon';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatTooltip } from '@angular/material/tooltip';
import { MatProgressBar } from '@angular/material/progress-bar';
@@ -52,7 +52,7 @@ import { RevokeDscDialogComponent } from '../revoke-dsc-dialog/revoke-dsc-dialog
MatRow,
MatButton,
MatIconButton,
- MatIcon,
+ MatIconModule,
MatMenu,
MatMenuItem,
MatMenuTrigger,