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..72cf485b2 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,25 @@
Remarks
+
+
+ @if (isSpouse) {
+ Children
+
+ Does this family member have a child/children?
+ @if (familyMemberForm.get('hasChildren')?.value) {
+
+ edit Edit Children Details ({{ children.length }})
+
+ }
+
+ }
}
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..2b941792b 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,32 @@ 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 +253,102 @@ 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 a circular dependency between the two dialogs.
+ 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 +603,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/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/edit-client/edit-client.component.html b/src/app/clients/edit-client/edit-client.component.html
index 92740d87c..ab2a69972 100644
--- a/src/app/clients/edit-client/edit-client.component.html
+++ b/src/app/clients/edit-client/edit-client.component.html
@@ -84,14 +84,70 @@
phone_iphone
Mobile Number
+
+
+ Old mobile number not accessible (lost / deactivated SIM)
+
+
+
+
+
+
+
+
+
+ Month
+ Generated On
+ Records
+ Status
+ Submitted On
+
+
+
+ @for (r of amlCtrReports; track r.id) {
+
+ {{ r.reportMonth }}
+ {{ r.generatedOn | date: 'medium' }}
+ {{ r.recordCount }}
+
+ {{ r.status }}
+
+ {{ r.submittedOn ? (r.submittedOn | date: 'medium') : '—' }}
+
+ } @empty {
+
+ No CTR reports generated yet.
+
+ }
+
+
+
}
@@ -307,6 +354,13 @@
Live AML Alerts (Awaiting Triage)
Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate
+
+ All statuses
+ Raised
+ Under Review
+ Whitelisted
+ Escalated
+
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html
index d2db28c86..639cfbe47 100644
--- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html
+++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html
@@ -115,6 +115,51 @@
}
+
+
+
+
+ @for (row of paymentChannelToFundSourceMappings.controls; track $index; let i = $index) {
+
+
+
+
+ Payment Type
+
+ @for (paymentType of paymentTypeOptions; track paymentType.id) {
+ {{ paymentType.name }}
+ }
+
+
+
+
+
+
+ Fund Source
+
+ @for (glAccount of fundSourceAccountOptions; track glAccount.id) {
+
+ {{ '(' + glAccount.glCode + ') ' + glAccount.name }}
+
+ }
+
+
+
+
+
+
+
+
+ }
+
+
}
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts
index 88e88a51d..986472df6 100644
--- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts
+++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts
@@ -50,6 +50,11 @@ type OtherGlMappingValue = {
glAccountId: number | null;
};
+type PaymentFundSourceValue = {
+ paymentTypeId: number | null;
+ fundSourceAccountId: number | null;
+};
+
@Component({
selector: 'mifosx-saving-product-accounting-step',
templateUrl: './saving-product-accounting-step.component.html',
@@ -78,6 +83,11 @@ export class SavingProductAccountingStepComponent implements OnInit {
expenseAccountOptions: GLAccount[] = [];
incomeAccountOptions: GLAccount[] = [];
assetAccountOptions: GLAccount[] = [];
+ paymentTypeOptions: any[] = [];
+ /** A payment-type Fund Source override can legitimately be a suspense/clearing Liability account
+ * (e.g. an existing "Sundry" account bridging to another module) rather than only a cash-like
+ * Asset account, so this field accepts either. */
+ fundSourceAccountOptions: GLAccount[] = [];
accountingSectionConfigs: AccountingSectionConfig[] = [];
constructor() {
@@ -98,6 +108,8 @@ export class SavingProductAccountingStepComponent implements OnInit {
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
+ this.paymentTypeOptions = this.savingProductsTemplate?.paymentTypeOptions || [];
+ this.fundSourceAccountOptions = this.assetAccountOptions.concat(this.liabilityAccountOptions);
this.buildAccountingSectionConfigs();
this.savingProductAccountingForm.patchValue({
accountingRule: accountingRuleId
@@ -105,6 +117,7 @@ export class SavingProductAccountingStepComponent implements OnInit {
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
+ this.patchPaymentFundSourceMappings(this.savingProductsTemplate?.paymentChannelToFundSourceMappings || []);
this.setOverdraftControl();
this.setDormancyControl();
}
@@ -115,10 +128,48 @@ export class SavingProductAccountingStepComponent implements OnInit {
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
- incomeOtherGlMappings: this.formBuilder.array([])
+ incomeOtherGlMappings: this.formBuilder.array([]),
+ paymentChannelToFundSourceMappings: this.formBuilder.array([])
});
}
+ get paymentChannelToFundSourceMappings(): UntypedFormArray {
+ return this.savingProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray;
+ }
+
+ private createPaymentFundSourceGroup(value: Partial = {}) {
+ return this.formBuilder.group({
+ paymentTypeId: [
+ value.paymentTypeId ?? this.paymentTypeOptions[0]?.id ?? null,
+ Validators.required
+ ],
+ fundSourceAccountId: [
+ value.fundSourceAccountId ?? this.fundSourceAccountOptions[0]?.id ?? null,
+ Validators.required
+ ]
+ });
+ }
+
+ private patchPaymentFundSourceMappings(mappings: any[]): void {
+ this.paymentChannelToFundSourceMappings.clear();
+ (mappings || []).forEach((mapping) => {
+ this.paymentChannelToFundSourceMappings.push(
+ this.createPaymentFundSourceGroup({
+ paymentTypeId: mapping.paymentType?.id,
+ fundSourceAccountId: mapping.fundSourceAccount?.id
+ })
+ );
+ });
+ }
+
+ addPaymentFundSourceMapping(): void {
+ this.paymentChannelToFundSourceMappings.push(this.createPaymentFundSourceGroup());
+ }
+
+ removePaymentFundSourceMapping(index: number): void {
+ this.paymentChannelToFundSourceMappings.removeAt(index);
+ }
+
private setConditionalControls() {
this.savingProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => {
this.syncAccountingControls(accountingRule);
diff --git a/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.ts b/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.ts
index cd20da7bc..14693833d 100644
--- a/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.ts
+++ b/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.ts
@@ -74,6 +74,12 @@ export class ShareProductAccountingStepComponent implements OnInit {
liabilityAccountData: GLAccount[] = [];
incomeAccountData: GLAccount[] = [];
equityAccountData: GLAccount[] = [];
+ /** "Share Reference" is where a redemption's credit side lands pending settlement — some banks
+ * model that as a receivable-like Asset (e.g. "Cash Vault", this field's original default),
+ * others as a payable-like Liability suspense account (e.g. an existing "Sundry" account
+ * already used elsewhere as Shares Suspense). Both are valid conventions, so this field accepts
+ * either instead of Asset-only. */
+ referenceAccountData: GLAccount[] = [];
constructor() {
this.createShareProductAccountingForm();
@@ -85,6 +91,7 @@ export class ShareProductAccountingStepComponent implements OnInit {
this.liabilityAccountData = opts.liabilityAccountOptions || [];
this.incomeAccountData = opts.incomeAccountOptions || [];
this.equityAccountData = opts.equityAccountOptions || [];
+ this.referenceAccountData = this.assetAccountData.concat(this.liabilityAccountData);
this.buildAccountingSectionConfigs();
@@ -135,10 +142,10 @@ export class ShareProductAccountingStepComponent implements OnInit {
rows: [
{
key: 'shareReferenceId',
- label: 'Share Reference (Cash Vault)',
+ label: 'Share Reference (Cash Vault or Suspense)',
inputLabel: 'Share reference',
controlName: 'shareReferenceId',
- options: this.assetAccountData,
+ options: this.referenceAccountData,
required: true
}
],
diff --git a/src/app/shares/share-holders/share-holders.component.html b/src/app/shares/share-holders/share-holders.component.html
index daae3fe22..15adb5d11 100644
--- a/src/app/shares/share-holders/share-holders.component.html
+++ b/src/app/shares/share-holders/share-holders.component.html
@@ -20,11 +20,11 @@
-
+
-
+
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..588b29b94 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: #fff;
+ border: 1px solid #e2e8f0;
+ box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
+ 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,22 +51,19 @@
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;
+ opacity: 0.7;
}
&:nth-child(3) {
@@ -75,53 +78,156 @@
}
.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: #fff !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;
+ }
+ }
+
+ [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: #fff;
.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: #fff;
+}
diff --git a/src/app/shares/shares-account-view/shares-account-view.component.html b/src/app/shares/shares-account-view/shares-account-view.component.html
index 7b1d31e33..006ee30a5 100644
--- a/src/app/shares/shares-account-view/shares-account-view.component.html
+++ b/src/app/shares/shares-account-view/shares-account-view.component.html
@@ -152,6 +152,16 @@
>
{{ 'labels.inputs.Transactions' | translate }}
+
+ Passbook
+
My Share Requests),
+ // never a direct one-step action from this view.
];
break;
case 'Submitted and pending approval':
diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html
index f3941f52b..769d76d05 100644
--- a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html
+++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html
@@ -54,6 +54,25 @@
+
+ Share Balance
+ {{ transaction.runningBalance }}
+
+
+
+ Resolution No. / Meeting Date
+
+ @if (transaction.type.value === 'Redeem') {
+ {{ transaction.resolutionNumber || '—' }}
+ @if (transaction.meetingDate) {
+ ({{ transaction.meetingDate | dateFormat }})
+ }
+ } @else {
+ —
+ }
+
+
+
diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts
index c28640324..46a7e3db0 100644
--- a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts
+++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts
@@ -56,14 +56,18 @@ export class TransactionsTabComponent implements OnInit {
transactionsData: any;
/** Data source for transactions table. */
dataSource: MatTableDataSource
;
- /** Columns to be displayed in transactions table. */
+ /** Columns to be displayed in transactions table — a share account's passbook: every
+ * purchase/redeem entry plus the running share balance and, for a withdrawal, the Board
+ * Resolution Number / Date of Meeting that authorised it. */
displayedColumns: string[] = [
'transactionDate',
'transactionType',
'totalShares',
'purchasedOrRedeemedPrice',
'chargeAmount',
- 'amountRecievedOrReturned'
+ 'amountRecievedOrReturned',
+ 'runningBalance',
+ 'resolutionMeeting'
];
/**
@@ -73,9 +77,19 @@ export class TransactionsTabComponent implements OnInit {
constructor() {
this.route.parent.data.subscribe((data: { sharesAccountData: any }) => {
this.shareAccountData = data.sharesAccountData;
- this.transactionsData = this.shareAccountData.purchasedShares.filter(
- (transaction: any) => transaction.type.value !== 'Charge Payment'
- );
+ this.transactionsData = this.shareAccountData.purchasedShares
+ .filter((transaction: any) => transaction.type.value !== 'Charge Payment')
+ .slice()
+ .sort((a: any, b: any) => new Date(a.purchasedDate).getTime() - new Date(b.purchasedDate).getTime());
+
+ let runningBalance = 0;
+ this.transactionsData.forEach((transaction: any) => {
+ const shares = Number(transaction.numberOfShares) || 0;
+ runningBalance += transaction.type.value === 'Redeem' ? -shares : shares;
+ transaction.runningBalance = runningBalance;
+ });
+ // Passbook reads newest-first.
+ this.transactionsData.reverse();
});
}
diff --git a/src/app/shares/shares-routing.module.ts b/src/app/shares/shares-routing.module.ts
index 4c5193aef..d12979e83 100644
--- a/src/app/shares/shares-routing.module.ts
+++ b/src/app/shares/shares-routing.module.ts
@@ -26,6 +26,7 @@ import { ShareAccountActionsResolver } from './common-resolvers/share-account-ac
import { SharesAccountTemplateResolver } from './common-resolvers/shares-account-template.resolver';
import { SharesAccountAndTemplateResolver } from './common-resolvers/share-account-and-template.resolver';
import { GeneralTabComponent } from './shares-account-view/general-tab/general-tab.component';
+import { SharesPassbookTabComponent } from './shares-account-view/passbook-tab/passbook-tab.component';
/** Shares Routes */
const routes: Routes = [
@@ -75,6 +76,11 @@ const routes: Routes = [
component: TransactionsTabComponent,
data: { title: 'Shares Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false }
},
+ {
+ path: 'passbook',
+ component: SharesPassbookTabComponent,
+ data: { title: 'Shares Account Passbook', breadcrumb: 'Passbook', routeParamBreadcrumb: false }
+ },
{
path: 'charges',
component: ChargesTabComponent,
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..4942b051f 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,29 @@
Actions
+
+
+
+ {{ edit.emailSent ? 'Sent' : 'Not Sent' }}
+
+ @if (edit.emailSent) {
+
+
+ {{ edit.emailOpenedAt ? 'Seen' : 'Not Seen' }}
+
+ }
+
+
Compare
@@ -137,6 +160,16 @@
Reject
}
+ @if (edit.emailSent) {
+
+ View Email
+
+ }
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..642138ffd 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,45 @@ $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..40ba63a9d 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,52 @@ 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();
}