-
MSME
-
Account is categorized under the Micro, Small and Medium Enterprises segment.
+
+
+
MSME
+
Account is categorized under the Micro, Small and Medium Enterprises segment.
+
+
-
-
-
-
Auto Debit Facility
-
Enable automatic debit instructions for recurring payments linked to this account.
+
+
+
Auto Debit Facility
+
Enable automatic debit instructions for recurring payments linked to this account.
+
+
-
-
+ }
diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts
index 9e8e1acd6..89bdb9e11 100644
--- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts
+++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts
@@ -30,6 +30,7 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges {
@Input() primaryClientId: number | null = null;
@Input() isNonIndividualClient = false;
@Input() existingAccount: any = null;
+ @Input() nominationOnly = false;
@ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent;
nominees: any[] = [];
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html
index c040b6031..1cf4ac389 100644
--- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html
+++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html
@@ -104,7 +104,7 @@
Account Type
-
+
@for (opt of accountTypeOptions; track opt) {
{{ opt }}
}
@@ -113,7 +113,11 @@
Customer Segment
-
+
@for (opt of customerSegmentOptions; track opt) {
{{ opt }}
}
@@ -157,7 +161,7 @@
Share Linking Enabled
-
+
Yes
No
@@ -172,6 +176,7 @@
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
"
(introducerChange)="onIntroducerChange($event)"
+ [disabled]="readOnly"
>
}
}
@@ -263,7 +268,7 @@
color="primary"
type="button"
class="soa-add-btn"
- [disabled]="totalJointSharePercentage >= 100"
+ [disabled]="totalJointSharePercentage >= 100 || readOnly"
(click)="openAddJointHolderDialog()"
>
person_add Add Holder
@@ -309,6 +314,7 @@
max="100"
[(ngModel)]="primaryHolderShare"
[ngModelOptions]="{ standalone: true }"
+ [disabled]="readOnly"
placeholder="0"
(input)="onPrimaryShareInput($event)"
(blur)="onPrimaryShareBlur($event)"
@@ -358,7 +364,8 @@
class="soa-sign-toggle"
[class.soa-sign-toggle--mandatory]="h.isMandatory"
[class.soa-sign-toggle--optional]="!h.isMandatory"
- (click)="toggleJointHolderMandatory(h)"
+ [disabled]="readOnly"
+ (click)="!readOnly && toggleJointHolderMandatory(h)"
>
{{ h.isMandatory ? 'lock' : 'lock_open' }}
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
@@ -366,7 +373,13 @@
Co-Holder
-
+
delete_outline
@@ -448,7 +461,8 @@
type="button"
class="soa-mode-card"
[class.soa-mode-card--selected]="modeOfOperation === opt.value"
- (click)="modeOfOperation = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (modeOfOperation = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
@@ -503,8 +517,9 @@
[max]="maxDate"
[(ngModel)]="minorDob"
[ngModelOptions]="{ standalone: true }"
+ [disabled]="readOnly"
/>
-
+
@if (clientDob) {
Auto-filled from client profile
@@ -525,7 +540,11 @@
Guardian Relationship
-
+
@for (rel of guardianRelationshipOptions; track rel) {
{{ rel }}
}
@@ -541,6 +560,7 @@
@if (guardianName) {
@@ -667,7 +687,8 @@
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value"
[class.soa-cat-tile--male]="true"
- (click)="selectedSpecialCategory = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (selectedSpecialCategory = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
@@ -717,7 +738,8 @@
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedWeakerSection === opt.value"
[class.soa-cat-tile--female]="true"
- (click)="selectedWeakerSection = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (selectedWeakerSection = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
@@ -775,7 +797,7 @@
color="primary"
type="button"
class="ni-add-btn"
- [disabled]="totalSharePercentage >= 100"
+ [disabled]="totalSharePercentage >= 100 || readOnly"
(click)="openAddRelatedClientDialog()"
>
person_add Add Partner
@@ -826,7 +848,8 @@
class="ni-sign-toggle"
[class.ni-sign-toggle--mandatory]="c.isMandatory"
[class.ni-sign-toggle--optional]="!c.isMandatory"
- (click)="toggleMandatory(c)"
+ [disabled]="readOnly"
+ (click)="!readOnly && toggleMandatory(c)"
[title]="c.isMandatory ? 'Click to set Optional' : 'Click to set Mandatory'"
>
{{ c.isMandatory ? 'lock' : 'lock_open' }}
@@ -838,6 +861,7 @@
mat-icon-button
color="warn"
type="button"
+ [disabled]="readOnly"
(click)="removeRelatedClient(i)"
class="ni-del-btn"
>
@@ -915,7 +939,8 @@
type="button"
class="ni-sig-card"
[class.ni-sig-card--selected]="signatoryRights === opt.value"
- (click)="signatoryRights = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (signatoryRights = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts
index 51ab90f3f..8c5592d1a 100644
--- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts
+++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts
@@ -53,6 +53,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
@Input() currentAccountTemplate: any;
@Input() hideButtons = false;
+ @Input() readOnly = false;
minDate = new Date(2000, 0, 1);
maxDate = new Date();
@@ -236,6 +237,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
});
}
}
+ if (this.readOnly) {
+ this.currentAccountDetailsForm.disable();
+ }
}
createCurrentAccountDetailsForm() {
@@ -453,6 +457,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}
isTileDisabled(value: string): boolean {
+ if (this.readOnly) return true;
if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18;
if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60;
return false;
diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html
index 5b0970996..ae448a85a 100644
--- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html
+++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html
@@ -210,5 +210,29 @@
+
+
+
+
+ Internet Banking
+ {{ facilityConfig.internetBanking ? 'Enabled' : 'Disabled' }}
+
+
+
+
+
+
+
+
+
+
+ Mobile Banking
+ {{ facilityConfig.mobileBanking ? 'Enabled' : 'Disabled' }}
+
+
+
+
+
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts
index 49597f57a..6e5cc09d5 100644
--- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts
+++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts
@@ -26,7 +26,9 @@ const DEFAULT_FACILITY_CONFIG = {
interBankTotalCapFacility: false,
withdrawalFacility: false,
depositFacilityKyc: false,
- virtualDebitCard: false
+ virtualDebitCard: false,
+ internetBanking: false,
+ mobileBanking: false
};
@Component({
diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts
index d5d718959..dbddadaa1 100644
--- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts
+++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts
@@ -78,7 +78,9 @@ export class CurrentAccountPreviewStepComponent implements OnChanges {
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap' },
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
- { key: 'virtualDebitCard', label: 'Virtual Debit Card' }
+ { key: 'virtualDebitCard', label: 'Virtual Debit Card' },
+ { key: 'internetBanking', label: 'Internet Banking' },
+ { key: 'mobileBanking', label: 'Mobile Banking' }
];
private translateService = inject(TranslateService);
diff --git a/src/app/current-accounts/current-account-view/current-account-view.component.ts b/src/app/current-accounts/current-account-view/current-account-view.component.ts
index 3129db777..1412dd044 100644
--- a/src/app/current-accounts/current-account-view/current-account-view.component.ts
+++ b/src/app/current-accounts/current-account-view/current-account-view.component.ts
@@ -124,6 +124,7 @@ export class CurrentAccountViewComponent implements OnInit {
'Add Charge': 'admin',
'Apply Annual Fees': 'admin',
'Modify Application': 'admin',
+ 'Edit Nominee & Features': 'admin',
'Assign Staff': 'admin',
'Unassign Staff': 'admin',
Approve: 'admin',
@@ -156,6 +157,7 @@ export class CurrentAccountViewComponent implements OnInit {
'Add Charge': 'plus-circle',
'Apply Annual Fees': 'receipt',
'Modify Application': 'edit',
+ 'Edit Nominee & Features': 'edit',
'Assign Staff': 'user-plus',
'Unassign Staff': 'user-slash',
Approve: 'check-circle',
@@ -314,6 +316,9 @@ export class CurrentAccountViewComponent implements OnInit {
case 'Modify Application':
this.router.navigate(['edit'], { relativeTo: this.route });
break;
+ case 'Edit Nominee & Features':
+ this.router.navigate(['edit-nominee-features'], { relativeTo: this.route });
+ break;
case 'Delete':
this.deleteCurrentAccount();
break;
diff --git a/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts b/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts
index e5fb4df88..bbc0e3872 100644
--- a/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts
+++ b/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts
@@ -162,6 +162,10 @@ export class CurrentAccountButtonsConfiguration {
switch (status) {
case 'Active':
this.optionArray = [
+ {
+ name: 'Edit Nominee & Features',
+ taskPermissionName: 'SUBMITEDIT_CURRENTACCOUNT'
+ },
{
name: 'Post Interest',
taskPermissionName: 'POSTINTEREST_CURRENTACCOUNT'
diff --git a/src/app/current-accounts/current-accounts-routing.module.ts b/src/app/current-accounts/current-accounts-routing.module.ts
index 65539fe02..48be2cb27 100644
--- a/src/app/current-accounts/current-accounts-routing.module.ts
+++ b/src/app/current-accounts/current-accounts-routing.module.ts
@@ -20,6 +20,7 @@ import { StandingInstructionsTabComponent } from './current-account-view/standin
import { DatatableTabsComponent } from './current-account-view/datatable-tabs/datatable-tabs.component';
import { CreateCurrentAccountComponent } from './create-current-account/create-current-account.component';
import { EditCurrentAccountComponent } from './edit-current-account/edit-current-account.component';
+import { EditCurrentAccountFeaturesComponent } from './edit-current-account-features/edit-current-account-features.component';
import { ViewTransactionComponent } from './current-account-view/transactions/view-transaction/view-transaction.component';
import { ViewChargeComponent } from './current-account-view/view-charge/view-charge.component';
import { ViewRecieptComponent } from './current-account-view/transactions/view-reciept/view-reciept.component';
@@ -170,6 +171,18 @@ const routes: Routes = [
currentAccountAndTemplate: CurrentAccountAndTemplateResolver
}
},
+ {
+ path: ':currentAccountId/edit-nominee-features',
+ data: {
+ title: 'Edit Nominee & Features',
+ breadcrumb: 'Edit Nominee & Features',
+ routeParamBreadcrumb: false
+ },
+ component: EditCurrentAccountFeaturesComponent,
+ resolve: {
+ currentAccountAndTemplate: CurrentAccountAndTemplateResolver
+ }
+ },
{
path: ':currentAccountId/transactions/:id',
data: { title: 'Current Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false },
diff --git a/src/app/current-accounts/current-accounts.module.ts b/src/app/current-accounts/current-accounts.module.ts
index 5c7d9a284..bc321f7fb 100644
--- a/src/app/current-accounts/current-accounts.module.ts
+++ b/src/app/current-accounts/current-accounts.module.ts
@@ -61,6 +61,7 @@ import { CurrentAccountTransactionDatatableTabComponent } from './current-accoun
import { GeneralTabComponent } from './current-account-view/general-tab/general-tab.component';
import { CurrentAccountActiveClientMembersComponent } from './current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component';
import { PassbookTabComponent } from './current-account-view/passbook-tab/passbook-tab.component';
+import { EditCurrentAccountFeaturesComponent } from './edit-current-account-features/edit-current-account-features.component';
/**
* Current Accounts Module
@@ -117,7 +118,8 @@ import { PassbookTabComponent } from './current-account-view/passbook-tab/passbo
CurrentAccountTransactionDatatableTabComponent,
GeneralTabComponent,
CurrentAccountActiveClientMembersComponent,
- PassbookTabComponent
+ PassbookTabComponent,
+ EditCurrentAccountFeaturesComponent
],
providers: []
})
diff --git a/src/app/current-accounts/current-accounts.service.ts b/src/app/current-accounts/current-accounts.service.ts
index 18e19593f..f6152a7df 100644
--- a/src/app/current-accounts/current-accounts.service.ts
+++ b/src/app/current-accounts/current-accounts.service.ts
@@ -370,4 +370,49 @@ export class CurrentAccountsService {
loadCurrentAccountDocument(currentAccountId: any, data: any): Observable
{
return this.http.post(`/currentaccounts/${currentAccountId}/documents`, data);
}
+
+ /**
+ * Submits a maker-checker edit request (Nominee and/or Digital & Account Features change) for a
+ * current account. The edit is held for checker approval before it is actually applied.
+ * @param {string} accountId Current Account Id
+ * @param {any} data Edit payload — `accountFacilityConfig` and/or `nominee`, plus locale/dateFormat
+ * @returns {Observable}
+ */
+ submitCurrentAccountEdit(accountId: string, data: any): Observable {
+ return this.http.post(`/currentaccounts/${accountId}/edit`, data);
+ }
+
+ /**
+ * @param {string} accountId Current Account Id
+ * @returns {Observable} the pending edit for this account, if any.
+ */
+ getPendingCurrentAccountEdit(accountId: string): Observable {
+ return this.http.get(`/currentaccounts/${accountId}/edit/pending`);
+ }
+
+ /**
+ * @returns {Observable} all current account edits currently awaiting checker approval.
+ */
+ getAllPendingCurrentAccountEdits(): Observable {
+ return this.http.get(`/currentaccounts/edit/pending`);
+ }
+
+ /**
+ * Approves a pending current account edit, releasing any hold and applying the proposed changes.
+ * @param {string} accountId Current Account Id
+ * @returns {Observable}
+ */
+ approveCurrentAccountEdit(accountId: string): Observable {
+ return this.http.post(`/currentaccounts/${accountId}/edit/approve`, {});
+ }
+
+ /**
+ * Rejects a pending current account edit, releasing any hold without applying the changes.
+ * @param {string} accountId Current Account Id
+ * @param {string} rejectReason Optional reason for rejection
+ * @returns {Observable}
+ */
+ rejectCurrentAccountEdit(accountId: string, rejectReason?: string): Observable {
+ return this.http.post(`/currentaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' });
+ }
}
diff --git a/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.html b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.html
new file mode 100644
index 000000000..1d53a2459
--- /dev/null
+++ b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.html
@@ -0,0 +1,176 @@
+
+
+
+
+
+ @if (pendingEditLoading) {
+
+
+ Checking for pending edits...
+
+ }
+
+ @if (!pendingEditLoading && hasPendingEdit) {
+
+
hourglass_top
+
+ An edit is already pending approval for this account.
+
+ Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status:
+ {{ pendingEdit.status }}
+ @if (pendingEdit.holdAmount) {
+ · Hold placed: {{ pendingEdit.holdAmount }}
+ }
+
+ You cannot submit another edit until this one is approved or rejected.
+
+
+ }
+
+ @if (!pendingEditLoading) {
+
+
+
+
+
+
+ Product
+
+
+
+
+
+
+
+
+ {{ 'labels.buttons.Next' | translate }}
+
+
+
+
+
+
+
+
+
+
+ Currency
+
+
+ @if (currentAccountProductTemplate) {
+
+ }
+
+
+
+
+ {{ 'labels.buttons.Previous' | translate }}
+
+
+
+ {{ 'labels.buttons.Next' | translate }}
+
+
+
+
+
+
+
+
+
+
+ Digital
+
+
+ @if (currentAccountProductTemplate) {
+
+ }
+
+
+
+
+ {{ 'labels.buttons.Previous' | translate }}
+
+
+
+ {{ 'labels.buttons.Next' | translate }}
+
+
+
+
+
+
+
+
+
+
+ {{ 'labels.inputs.PREVIEW' | translate }}
+
+
+ @if (currentAccountProductTemplate) {
+
+
+ }
+
+
+
+
+ {{ 'labels.buttons.Previous' | translate }}
+
+
+ Cancel
+
+ @if (submitting) {
+
+ } @else {
+ send Submit for Approval
+ }
+
+
+
+
+
+ }
+
diff --git a/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.scss b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.scss
new file mode 100644
index 000000000..bc2c55495
--- /dev/null
+++ b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.scss
@@ -0,0 +1,206 @@
+/**
+ * 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/.
+ */
+
+$border: #e2e8f0;
+$muted: #64748b;
+$amber: #92400e;
+$amber-bg: #fffbeb;
+$amber-border: #fde68a;
+
+.esf-loading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 20px 0;
+ justify-content: center;
+ font-size: 13px;
+ color: $muted;
+
+ &--sm {
+ padding: 10px 0;
+ justify-content: flex-start;
+ }
+}
+
+.esf-pending-banner {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 12px 16px;
+ background: $amber-bg;
+ border: 1px solid $amber-border;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ color: $amber;
+
+ mat-icon {
+ flex: 0 0 auto;
+ }
+
+ &__text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 12.5px;
+
+ strong {
+ font-size: 13px;
+ }
+ }
+
+ &__status {
+ font-weight: 700;
+ text-transform: uppercase;
+ }
+}
+
+/* ─── CSA Stepper Styles ─── */
+$navy: #0d1b2a;
+$blue: #1e88e5;
+$blue-d: #1e40af;
+$indigo: #4f46e5;
+
+.csa-container {
+ max-width: 1180px;
+ margin: 0 auto;
+ padding: 20px 24px;
+}
+
+.csa-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+
+ &__icon {
+ width: 44px;
+ height: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+ background: linear-gradient(135deg, $blue 0%, $indigo 100%);
+ color: #fff;
+ font-size: 18px;
+ box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
+ flex-shrink: 0;
+ }
+
+ &__title {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 800;
+ color: $navy;
+ line-height: 1.1;
+ letter-spacing: 0.02em;
+ }
+
+ &__sub {
+ margin-top: 2px;
+ font-size: 12.5px;
+ color: #64748b;
+ }
+}
+
+.csa-tabs {
+ background: #fff;
+ border: 1px solid $border;
+ border-radius: 14px;
+ box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
+ overflow: hidden;
+
+ ::ng-deep {
+ .mat-mdc-tab-header {
+ background: #f8fafc;
+ border-bottom: 1px solid $border;
+ }
+
+ .mat-mdc-tab {
+ min-width: 140px;
+ height: 50px;
+ padding: 0 22px;
+ font-size: 13px !important;
+ font-weight: 600 !important;
+ letter-spacing: 0.01em;
+ color: #475569 !important;
+ opacity: 1 !important;
+ }
+
+ .mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
+ background: rgb(30 64 175 / 5%);
+ color: $navy !important;
+ }
+
+ .mat-mdc-tab.mat-mdc-tab-active {
+ .mdc-tab__text-label {
+ color: $blue-d !important;
+ font-weight: 700 !important;
+ }
+ }
+
+ .mdc-tab-indicator__content--underline {
+ border-color: $blue-d !important;
+ border-top-width: 3px !important;
+ }
+
+ .mat-mdc-tab.mat-mdc-tab-disabled {
+ opacity: 0.45 !important;
+ cursor: not-allowed;
+ }
+
+ .mat-mdc-tab-body-wrapper {
+ min-height: 320px;
+ }
+
+ .mat-mdc-tab-body,
+ .mat-mdc-tab-body-content {
+ overflow: visible !important;
+ }
+ }
+}
+
+.csa-tab__icon {
+ margin-right: 8px;
+ font-size: 13px;
+}
+
+.csa-tab-body {
+ padding: 24px;
+
+ @media (width <= 768px) {
+ padding: 16px;
+ }
+}
+
+.csa-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 24px;
+ padding-top: 16px;
+ border-top: 1px solid #f1f5f9;
+
+ &__spacer {
+ flex: 1;
+ }
+
+ button {
+ height: 40px !important;
+ border-radius: 10px !important;
+ font-weight: 600 !important;
+ padding: 0 18px !important;
+ }
+}
+
+.m-l-10 {
+ margin-left: 8px;
+}
+
+.m-r-10 {
+ margin-right: 8px;
+}
diff --git a/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.ts b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.ts
new file mode 100644
index 000000000..bca2ab003
--- /dev/null
+++ b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.ts
@@ -0,0 +1,417 @@
+/**
+ * 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/.
+ */
+
+/** Angular Imports */
+import { Component, inject, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FormsModule, FormGroup } from '@angular/forms';
+import { MatDialog } from '@angular/material/dialog';
+import { MatIcon } from '@angular/material/icon';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { MatTabsModule } from '@angular/material/tabs';
+import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component';
+import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component';
+import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component';
+import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component';
+
+/** Custom Services */
+import { CurrentAccountsService } from '../current-accounts.service';
+import { ClientsService } from 'app/clients/clients.service';
+import { SettingsService } from 'app/settings/settings.service';
+import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+
+/** Default state for every Digital & Account Feature toggle, mirrors
+ * current-account-digital-step's DEFAULT_FACILITY_CONFIG. */
+const DEFAULT_FACILITY_CONFIG: Record = {
+ atmFacility: false,
+ atmWithdrawalFacility: false,
+ upiFacility: false,
+ posTransactionsFacility: false,
+ chequeBook: false,
+ impsFacility: false,
+ smsAlerts: false,
+ bbpsFacility: false,
+ insuranceCover: false,
+ bepgFacility: false,
+ neftFacility: false,
+ rtgsFacility: false,
+ interBankTotalCapFacility: false,
+ withdrawalFacility: false,
+ depositFacilityKyc: false,
+ virtualDebitCard: false,
+ internetBanking: false,
+ mobileBanking: false
+};
+
+/** Toggle metadata used to render the feature grid — key must match a
+ * property of DEFAULT_FACILITY_CONFIG / the backend's accountFacilityConfig map. */
+const FACILITY_TOGGLES: { key: string; label: string; icon: string }[] = [
+ { key: 'atmFacility', label: 'ATM Facility', icon: 'fa-credit-card' },
+ { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal', icon: 'fa-wallet' },
+ { key: 'upiFacility', label: 'UPI Facility', icon: 'fa-mobile-alt' },
+ { key: 'posTransactionsFacility', label: 'POS Transactions', icon: 'fa-exchange-alt' },
+ { key: 'chequeBook', label: 'Cheque Book', icon: 'fa-book' },
+ { key: 'impsFacility', label: 'IMPS Facility', icon: 'fa-bolt' },
+ { key: 'smsAlerts', label: 'SMS Alerts', icon: 'fa-sms' },
+ { key: 'bbpsFacility', label: 'BBPS Facility', icon: 'fa-bell' },
+ { key: 'insuranceCover', label: 'Insurance Cover', icon: 'fa-shield-alt' },
+ { key: 'bepgFacility', label: 'BEPG Facility', icon: 'fa-plug' },
+ { key: 'neftFacility', label: 'NEFT Facility', icon: 'fa-money-check-alt' },
+ { key: 'rtgsFacility', label: 'RTGS Facility', icon: 'fa-money-check' },
+ { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap', icon: 'fa-balance-scale' },
+ { key: 'withdrawalFacility', label: 'Withdrawal Facility', icon: 'fa-arrow-right-arrow-left' },
+ { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)', icon: 'fa-id-card' },
+ { key: 'virtualDebitCard', label: 'Virtual Debit Card', icon: 'fa-credit-card' },
+ { key: 'internetBanking', label: 'Internet Banking', icon: 'fa-globe' },
+ { key: 'mobileBanking', label: 'Mobile Banking', icon: 'fa-mobile-screen-button' }
+];
+
+/**
+ * Edit Nominee & Features — maker-checker submission screen.
+ *
+ * A maker proposes changes to a current account's Digital & Account Features toggles and/or its
+ * nominee here; nothing is applied immediately. The request is staged and a different user (checker)
+ * must approve it from the "Pending Current Account Edits" screen before it takes effect.
+ */
+@Component({
+ selector: 'mifosx-edit-current-account-features',
+ templateUrl: './edit-current-account-features.component.html',
+ styleUrls: ['./edit-current-account-features.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ FormsModule,
+ FaIconComponent,
+ MatIcon,
+ MatProgressSpinner,
+ MatTabsModule,
+ CurrentAccountDetailsStepComponent,
+ CurrentAccountCurrencyStepComponent,
+ CurrentAccountDigitalStepComponent,
+ CurrentAccountPreviewStepComponent
+ ]
+})
+export class EditCurrentAccountFeaturesComponent {
+ private route = inject(ActivatedRoute);
+ private router = inject(Router);
+ private currentAccountsService = inject(CurrentAccountsService);
+ private clientsService = inject(ClientsService);
+ private settingsService = inject(SettingsService);
+ private dialog = inject(MatDialog);
+
+ @ViewChild(CurrentAccountDetailsStepComponent) currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
+ @ViewChild(CurrentAccountCurrencyStepComponent) currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent;
+ @ViewChild(CurrentAccountDigitalStepComponent) currentAccountDigitalStep: CurrentAccountDigitalStepComponent;
+
+ originalNominees: any[] = [];
+
+ get currentAccountTermForm(): FormGroup {
+ return new FormGroup({});
+ }
+
+ get currentAccountSettingsForm(): FormGroup {
+ return new FormGroup({});
+ }
+
+ get currentAccountTermsForm(): FormGroup {
+ return new FormGroup({});
+ }
+
+ get currentAccount() {
+ return this.currentAccountDetailsStep ? { ...this.currentAccountDetailsStep.currentAccountDetails } : null;
+ }
+
+ get currentAccountDigital() {
+ return this.currentAccountDigitalStep?.currentAccountDigital ?? null;
+ }
+
+ get currentAccountCurrencyFlags() {
+ return {
+ balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false,
+ msme: this.currentAccountCurrencyStep?.msme ?? false,
+ autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false
+ };
+ }
+
+ readonly toggles = FACILITY_TOGGLES;
+
+ accountId: string;
+ currentAccountAndTemplate: any;
+ clientId = '';
+ accountNo = '';
+
+ currentAccountProductTemplate: any = null;
+ selectedTabIndex = 0;
+ readonly totalTabs = 4;
+
+ originalFacilityConfig: Record = { ...DEFAULT_FACILITY_CONFIG };
+
+ /** Current nominees on the account, fetched read-only for reference. */
+ nominees: any[] = [];
+ nomineesLoading = false;
+ relationshipOptions: any[] = [];
+
+ /** Pending edit already submitted for this account, if any. */
+ pendingEdit: any = null;
+ pendingEditLoading = true;
+
+ submitting = false;
+
+ constructor() {
+ this.accountId = this.route.snapshot.params['currentAccountId'];
+ this.loadPendingEdit();
+ this.route.data.subscribe((data: { currentAccountAndTemplate: any }) => {
+ const account = data.currentAccountAndTemplate;
+ this.currentAccountAndTemplate = account;
+ this.clientId = account?.clientId != null ? String(account.clientId) : '';
+ this.accountNo = account?.accountNo || '';
+ this.originalFacilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...(account?.accountFacilityConfig || {}) };
+ if (this.clientId) {
+ this.loadNominees();
+ this.loadRelationshipTemplate();
+ }
+ });
+ }
+
+ private todayIso(): string {
+ const d = new Date();
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+ }
+
+ private loadPendingEdit(): void {
+ this.pendingEditLoading = true;
+ this.currentAccountsService.getPendingCurrentAccountEdit(this.accountId).subscribe({
+ next: (edit: any) => {
+ this.pendingEdit = edit && edit.id ? edit : null;
+ this.pendingEditLoading = false;
+ },
+ error: () => {
+ this.pendingEdit = null;
+ this.pendingEditLoading = false;
+ }
+ });
+ }
+
+ private loadNominees(): void {
+ this.nomineesLoading = true;
+ this.clientsService.getClientNominees(this.clientId, this.accountNo, 'CB').subscribe({
+ next: (data: any) => {
+ this.nominees = Array.isArray(data) ? data : [];
+ this.originalNominees = JSON.parse(JSON.stringify(this.nominees));
+ this.nomineesLoading = false;
+ },
+ error: () => {
+ this.nominees = [];
+ this.originalNominees = [];
+ this.nomineesLoading = false;
+ }
+ });
+ }
+
+ private loadRelationshipTemplate(): void {
+ this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({
+ next: (template: any) => {
+ this.relationshipOptions = template?.relationshipOptions || [];
+ },
+ error: () => {
+ this.relationshipOptions = [];
+ }
+ });
+ }
+
+ get hasPendingEdit(): boolean {
+ return !!this.pendingEdit && this.pendingEdit.status === 'PENDING';
+ }
+
+ get facilityConfig(): Record {
+ return this.currentAccountDigitalStep?.facilityConfig ?? this.originalFacilityConfig;
+ }
+
+ get facilityChanged(): boolean {
+ const config = this.facilityConfig;
+ return this.toggles.some((t) => config[t.key] !== this.originalFacilityConfig[t.key]);
+ }
+
+ get changedToggles(): any[] {
+ const list: any[] = [];
+ const config = this.facilityConfig;
+ this.toggles.forEach((t) => {
+ if (config[t.key] !== this.originalFacilityConfig[t.key]) {
+ list.push({
+ label: t.label,
+ from: this.originalFacilityConfig[t.key],
+ to: config[t.key]
+ });
+ }
+ });
+ return list;
+ }
+
+ get nomineeChanged(): boolean {
+ return this.buildNomineePayload() !== null;
+ }
+
+ get canSubmit(): boolean {
+ return (
+ !this.hasPendingEdit &&
+ !this.pendingEditLoading &&
+ !this.submitting &&
+ (this.facilityChanged || this.nomineeChanged)
+ );
+ }
+
+ setTemplate($event: any): void {
+ this.currentAccountProductTemplate = $event;
+ }
+
+ next(): void {
+ if (this.selectedTabIndex < this.totalTabs - 1) {
+ this.selectedTabIndex += 1;
+ }
+ }
+
+ prev(): void {
+ if (this.selectedTabIndex > 0) {
+ this.selectedTabIndex -= 1;
+ }
+ }
+
+ getRelationshipName(relationshipId: any): string {
+ const opt = this.relationshipOptions.find((r: any) => r.id === relationshipId);
+ return opt?.name || '';
+ }
+
+ private buildFacilityDiff(): Record | null {
+ const diff: Record = {};
+ let changed = false;
+ const config = this.facilityConfig;
+ this.toggles.forEach((t) => {
+ if (config[t.key] !== this.originalFacilityConfig[t.key]) {
+ diff[t.key] = config[t.key];
+ changed = true;
+ }
+ });
+ return changed ? diff : null;
+ }
+
+ private buildNomineePayload(): any | null {
+ const original = this.originalNominees || [];
+ const current = this.currentAccountCurrencyStep?.nominees ?? [];
+
+ // Find if a nominee has been added
+ const added = current.find(
+ (curr: any) => !original.some((orig: any) => orig.id === curr.id || orig.nomineeClientId === curr.nomineeClientId)
+ );
+ if (added) {
+ return {
+ operation: 'ADD',
+ nomineeId: null,
+ nomineeClientId: added.nomineeClientId,
+ relationshipId: added.relationshipId,
+ percentageShare: added.percentageShare,
+ isActive: added.isActive !== undefined ? !!added.isActive : true,
+ nomineeIsMinor: !!added.nomineeIsMinor,
+ guardianClientId: added.nomineeIsMinor ? added.guardianClientId : null,
+ guardianRelationshipId: added.nomineeIsMinor ? added.guardianRelationshipId : null,
+ effectiveDate: this.todayIso(),
+ remarks: added.remarks || null
+ };
+ }
+
+ // Find if a nominee has been deleted
+ const deleted = original.find(
+ (orig: any) => !current.some((curr: any) => curr.id === orig.id || curr.nomineeClientId === orig.nomineeClientId)
+ );
+ if (deleted) {
+ return {
+ operation: 'DELETE',
+ nomineeId: deleted.id
+ };
+ }
+
+ // Find if any nominee has been updated
+ for (const curr of current) {
+ const orig = original.find((o: any) => o.id === curr.id || o.nomineeClientId === curr.nomineeClientId);
+ if (orig) {
+ const relChanged = orig.relationshipId !== curr.relationshipId;
+ const shareChanged = +orig.percentageShare !== +curr.percentageShare;
+ const minorChanged = !!orig.nomineeIsMinor !== !!curr.nomineeIsMinor;
+ const guardianChanged =
+ orig.guardianClientId !== curr.guardianClientId ||
+ orig.guardianRelationshipId !== curr.guardianRelationshipId;
+ const activeChanged = !!orig.isActive !== (curr.isActive !== undefined ? !!curr.isActive : true);
+ const remarksChanged = (orig.remarks || '') !== (curr.remarks || '');
+
+ if (relChanged || shareChanged || minorChanged || guardianChanged || activeChanged || remarksChanged) {
+ return {
+ operation: 'UPDATE',
+ nomineeId: orig.id,
+ relationshipId: curr.relationshipId,
+ percentageShare: curr.percentageShare,
+ isActive: curr.isActive !== undefined ? !!curr.isActive : true,
+ nomineeIsMinor: !!curr.nomineeIsMinor,
+ guardianClientId: curr.nomineeIsMinor ? curr.guardianClientId : null,
+ guardianRelationshipId: curr.nomineeIsMinor ? curr.guardianRelationshipId : null,
+ effectiveDate: this.todayIso(),
+ remarks: curr.remarks || null
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ submit(): void {
+ if (!this.canSubmit) return;
+
+ const locale = this.settingsService.language.code;
+ const payload: any = { locale, dateFormat: 'yyyy-MM-dd' };
+
+ const facilityDiff = this.buildFacilityDiff();
+ if (facilityDiff) {
+ payload.accountFacilityConfig = facilityDiff;
+ }
+
+ const nomineePayload = this.buildNomineePayload();
+ if (nomineePayload) {
+ payload.nominee = nomineePayload;
+ }
+
+ this.submitting = true;
+ this.currentAccountsService.submitCurrentAccountEdit(this.accountId, payload).subscribe({
+ next: () => {
+ this.submitting = false;
+ this.showSuccess();
+ },
+ error: () => {
+ this.submitting = false;
+ }
+ });
+ }
+
+ private showSuccess(): void {
+ const ref = this.dialog.open(SuccessDialogComponent, {
+ width: '420px',
+ maxWidth: '92vw',
+ autoFocus: false,
+ data: {
+ title: 'Submitted for Approval',
+ message: 'Your changes have been submitted and are awaiting checker approval.',
+ buttonLabel: 'Done'
+ }
+ });
+ ref.afterClosed().subscribe(() => this.cancel());
+ }
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.html b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.html
new file mode 100644
index 000000000..a1cebfc3f
--- /dev/null
+++ b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ @if (pendingEditLoading) {
+
+
+ Checking for pending edits...
+
+ }
+
+ @if (!pendingEditLoading && hasPendingEdit) {
+
+
hourglass_top
+
+ An edit is already pending approval for this account.
+
+ Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status:
+ {{ pendingEdit.status }}
+ @if (pendingEdit.holdAmount) {
+ · Hold placed: {{ pendingEdit.holdAmount }}
+ }
+
+ You cannot submit another edit until this one is approved or rejected.
+
+
+ }
+
+ @if (!pendingEditLoading) {
+
+
+
+
+
+
+ Product
+
+
+
+
+
+
+
+
+ {{ 'labels.buttons.Next' | translate }}
+
+
+
+
+
+
+
+
+
+
+ Currency
+
+
+ @if (savingsAccountProductTemplate) {
+
+ }
+
+
+
+
+ {{ 'labels.buttons.Previous' | translate }}
+
+
+
+ {{ 'labels.buttons.Next' | translate }}
+
+
+
+
+
+
+
+
+
+
+ Digital
+
+
+ @if (savingsAccountProductTemplate) {
+
+ }
+
+
+
+
+ {{ 'labels.buttons.Previous' | translate }}
+
+
+
+ {{ 'labels.buttons.Next' | translate }}
+
+
+
+
+
+
+
+
+
+
+ {{ 'labels.inputs.PREVIEW' | translate }}
+
+
+ @if (savingsAccountProductTemplate) {
+
+
+ }
+
+
+
+
+ {{ 'labels.buttons.Previous' | translate }}
+
+
+ Cancel
+
+ @if (submitting) {
+
+ } @else {
+ send Submit for Approval
+ }
+
+
+
+
+
+ }
+
diff --git a/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.scss b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.scss
new file mode 100644
index 000000000..bc2c55495
--- /dev/null
+++ b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.scss
@@ -0,0 +1,206 @@
+/**
+ * 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/.
+ */
+
+$border: #e2e8f0;
+$muted: #64748b;
+$amber: #92400e;
+$amber-bg: #fffbeb;
+$amber-border: #fde68a;
+
+.esf-loading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 20px 0;
+ justify-content: center;
+ font-size: 13px;
+ color: $muted;
+
+ &--sm {
+ padding: 10px 0;
+ justify-content: flex-start;
+ }
+}
+
+.esf-pending-banner {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 12px 16px;
+ background: $amber-bg;
+ border: 1px solid $amber-border;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ color: $amber;
+
+ mat-icon {
+ flex: 0 0 auto;
+ }
+
+ &__text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 12.5px;
+
+ strong {
+ font-size: 13px;
+ }
+ }
+
+ &__status {
+ font-weight: 700;
+ text-transform: uppercase;
+ }
+}
+
+/* ─── CSA Stepper Styles ─── */
+$navy: #0d1b2a;
+$blue: #1e88e5;
+$blue-d: #1e40af;
+$indigo: #4f46e5;
+
+.csa-container {
+ max-width: 1180px;
+ margin: 0 auto;
+ padding: 20px 24px;
+}
+
+.csa-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+
+ &__icon {
+ width: 44px;
+ height: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+ background: linear-gradient(135deg, $blue 0%, $indigo 100%);
+ color: #fff;
+ font-size: 18px;
+ box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
+ flex-shrink: 0;
+ }
+
+ &__title {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 800;
+ color: $navy;
+ line-height: 1.1;
+ letter-spacing: 0.02em;
+ }
+
+ &__sub {
+ margin-top: 2px;
+ font-size: 12.5px;
+ color: #64748b;
+ }
+}
+
+.csa-tabs {
+ background: #fff;
+ border: 1px solid $border;
+ border-radius: 14px;
+ box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
+ overflow: hidden;
+
+ ::ng-deep {
+ .mat-mdc-tab-header {
+ background: #f8fafc;
+ border-bottom: 1px solid $border;
+ }
+
+ .mat-mdc-tab {
+ min-width: 140px;
+ height: 50px;
+ padding: 0 22px;
+ font-size: 13px !important;
+ font-weight: 600 !important;
+ letter-spacing: 0.01em;
+ color: #475569 !important;
+ opacity: 1 !important;
+ }
+
+ .mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) {
+ background: rgb(30 64 175 / 5%);
+ color: $navy !important;
+ }
+
+ .mat-mdc-tab.mat-mdc-tab-active {
+ .mdc-tab__text-label {
+ color: $blue-d !important;
+ font-weight: 700 !important;
+ }
+ }
+
+ .mdc-tab-indicator__content--underline {
+ border-color: $blue-d !important;
+ border-top-width: 3px !important;
+ }
+
+ .mat-mdc-tab.mat-mdc-tab-disabled {
+ opacity: 0.45 !important;
+ cursor: not-allowed;
+ }
+
+ .mat-mdc-tab-body-wrapper {
+ min-height: 320px;
+ }
+
+ .mat-mdc-tab-body,
+ .mat-mdc-tab-body-content {
+ overflow: visible !important;
+ }
+ }
+}
+
+.csa-tab__icon {
+ margin-right: 8px;
+ font-size: 13px;
+}
+
+.csa-tab-body {
+ padding: 24px;
+
+ @media (width <= 768px) {
+ padding: 16px;
+ }
+}
+
+.csa-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 24px;
+ padding-top: 16px;
+ border-top: 1px solid #f1f5f9;
+
+ &__spacer {
+ flex: 1;
+ }
+
+ button {
+ height: 40px !important;
+ border-radius: 10px !important;
+ font-weight: 600 !important;
+ padding: 0 18px !important;
+ }
+}
+
+.m-l-10 {
+ margin-left: 8px;
+}
+
+.m-r-10 {
+ margin-right: 8px;
+}
diff --git a/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.ts b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.ts
new file mode 100644
index 000000000..00e9e1c85
--- /dev/null
+++ b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.ts
@@ -0,0 +1,399 @@
+/**
+ * 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/.
+ */
+
+/** Angular Imports */
+import { Component, inject, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FormsModule, FormGroup } from '@angular/forms';
+import { MatDialog } from '@angular/material/dialog';
+import { MatIcon } from '@angular/material/icon';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { MatTabsModule } from '@angular/material/tabs';
+import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component';
+import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component';
+import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component';
+import { SavingsAccountPreviewStepComponent } from '../savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component';
+
+/** Custom Services */
+import { SavingsService } from '../savings.service';
+import { ClientsService } from 'app/clients/clients.service';
+import { SettingsService } from 'app/settings/settings.service';
+import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+
+/** Default state for every Digital & Account Feature toggle, mirrors
+ * savings-account-digital-step's DEFAULT_FACILITY_CONFIG. */
+const DEFAULT_FACILITY_CONFIG: Record = {
+ atmFacility: false,
+ atmWithdrawalFacility: false,
+ upiFacility: false,
+ posTransactionsFacility: false,
+ chequeBook: false,
+ impsFacility: false,
+ smsAlerts: false,
+ bbpsFacility: false,
+ insuranceCover: false,
+ bepgFacility: false,
+ neftFacility: false,
+ rtgsFacility: false,
+ interBankTotalCapFacility: false,
+ withdrawalFacility: false,
+ depositFacilityKyc: false,
+ virtualDebitCard: false
+};
+
+/** Toggle metadata used to render the feature grid — key must match a
+ * property of DEFAULT_FACILITY_CONFIG / the backend's accountFacilityConfig map. */
+const FACILITY_TOGGLES: { key: string; label: string; icon: string }[] = [
+ { key: 'atmFacility', label: 'ATM Facility', icon: 'fa-credit-card' },
+ { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal', icon: 'fa-wallet' },
+ { key: 'upiFacility', label: 'UPI Facility', icon: 'fa-mobile-alt' },
+ { key: 'posTransactionsFacility', label: 'POS Transactions', icon: 'fa-exchange-alt' },
+ { key: 'chequeBook', label: 'Cheque Book', icon: 'fa-book' },
+ { key: 'impsFacility', label: 'IMPS Facility', icon: 'fa-bolt' },
+ { key: 'smsAlerts', label: 'SMS Alerts', icon: 'fa-sms' },
+ { key: 'bbpsFacility', label: 'BBPS Facility', icon: 'fa-bell' },
+ { key: 'insuranceCover', label: 'Insurance Cover', icon: 'fa-shield-alt' },
+ { key: 'bepgFacility', label: 'BEPG Facility', icon: 'fa-plug' },
+ { key: 'neftFacility', label: 'NEFT Facility', icon: 'fa-money-check-alt' },
+ { key: 'rtgsFacility', label: 'RTGS Facility', icon: 'fa-money-check' },
+ { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap', icon: 'fa-balance-scale' },
+ { key: 'withdrawalFacility', label: 'Withdrawal Facility', icon: 'fa-arrow-right-arrow-left' },
+ { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)', icon: 'fa-id-card' },
+ { key: 'virtualDebitCard', label: 'Virtual Debit Card', icon: 'fa-credit-card' }
+];
+
+/**
+ * Edit Nominee & Features — maker-checker submission screen.
+ *
+ * A maker proposes changes to a savings account's Digital & Account Features toggles and/or its
+ * nominee here; nothing is applied immediately. The request is staged and a different user (checker)
+ * must approve it from the "Pending Savings Account Edits" screen before it takes effect.
+ */
+@Component({
+ selector: 'mifosx-edit-savings-account-features',
+ templateUrl: './edit-savings-account-features.component.html',
+ styleUrls: ['./edit-savings-account-features.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ FormsModule,
+ FaIconComponent,
+ MatIcon,
+ MatProgressSpinner,
+ MatTabsModule,
+ SavingsAccountDetailsStepComponent,
+ SavingsAccountCurrencyStepComponent,
+ SavingsAccountDigitalStepComponent,
+ SavingsAccountPreviewStepComponent
+ ]
+})
+export class EditSavingsAccountFeaturesComponent {
+ private route = inject(ActivatedRoute);
+ private router = inject(Router);
+ private savingsService = inject(SavingsService);
+ private clientsService = inject(ClientsService);
+ private settingsService = inject(SettingsService);
+ private dialog = inject(MatDialog);
+
+ @ViewChild(SavingsAccountDetailsStepComponent) savingsAccountDetailsStep: SavingsAccountDetailsStepComponent;
+ @ViewChild(SavingsAccountCurrencyStepComponent) savingsAccountCurrencyStep: SavingsAccountCurrencyStepComponent;
+ @ViewChild(SavingsAccountDigitalStepComponent) savingsAccountDigitalStep: SavingsAccountDigitalStepComponent;
+
+ originalNominees: any[] = [];
+
+ get savingsAccountTermsForm(): FormGroup {
+ return new FormGroup({});
+ }
+
+ get savingsAccount() {
+ return this.savingsAccountDetailsStep ? { ...this.savingsAccountDetailsStep.savingsAccountDetails } : null;
+ }
+
+ get savingsAccountDigital() {
+ return this.savingsAccountDigitalStep?.savingsAccountDigital ?? null;
+ }
+
+ readonly toggles = FACILITY_TOGGLES;
+
+ accountId: string;
+ savingsAccountAndTemplate: any;
+ clientId = '';
+ accountNo = '';
+
+ savingsAccountProductTemplate: any = null;
+ selectedTabIndex = 0;
+ readonly totalTabs = 4;
+
+ originalFacilityConfig: Record = { ...DEFAULT_FACILITY_CONFIG };
+
+ /** Current nominees on the account, fetched read-only for reference. */
+ nominees: any[] = [];
+ nomineesLoading = false;
+ relationshipOptions: any[] = [];
+
+ /** Pending edit already submitted for this account, if any. */
+ pendingEdit: any = null;
+ pendingEditLoading = true;
+
+ submitting = false;
+
+ constructor() {
+ this.accountId = this.route.snapshot.params['savingAccountId'];
+ this.loadPendingEdit();
+ this.route.data.subscribe((data: { savingsAccountAndTemplate: any }) => {
+ const account = data.savingsAccountAndTemplate;
+ this.savingsAccountAndTemplate = account;
+ this.clientId = account?.clientId != null ? String(account.clientId) : '';
+ this.accountNo = account?.accountNo || '';
+ this.originalFacilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...(account?.accountFacilityConfig || {}) };
+ if (this.clientId) {
+ this.loadNominees();
+ this.loadRelationshipTemplate();
+ }
+ });
+ }
+
+ private todayIso(): string {
+ const d = new Date();
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+ }
+
+ private loadPendingEdit(): void {
+ this.pendingEditLoading = true;
+ this.savingsService.getPendingSavingsAccountEdit(this.accountId).subscribe({
+ next: (edit: any) => {
+ this.pendingEdit = edit && edit.id ? edit : null;
+ this.pendingEditLoading = false;
+ },
+ error: () => {
+ this.pendingEdit = null;
+ this.pendingEditLoading = false;
+ }
+ });
+ }
+
+ private loadNominees(): void {
+ this.nomineesLoading = true;
+ this.clientsService.getClientNominees(this.clientId, this.accountNo, 'SB').subscribe({
+ next: (data: any) => {
+ this.nominees = Array.isArray(data) ? data : [];
+ this.originalNominees = JSON.parse(JSON.stringify(this.nominees));
+ this.nomineesLoading = false;
+ },
+ error: () => {
+ this.nominees = [];
+ this.originalNominees = [];
+ this.nomineesLoading = false;
+ }
+ });
+ }
+
+ private loadRelationshipTemplate(): void {
+ this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({
+ next: (template: any) => {
+ this.relationshipOptions = template?.relationshipOptions || [];
+ },
+ error: () => {
+ this.relationshipOptions = [];
+ }
+ });
+ }
+
+ /** Max share allowed for the mode currently being edited — excludes the nominee's own
+ * existing share from the running total when updating that same nominee. */
+ get hasPendingEdit(): boolean {
+ return !!this.pendingEdit && this.pendingEdit.status === 'PENDING';
+ }
+
+ get facilityConfig(): Record {
+ return this.savingsAccountDigitalStep?.facilityConfig ?? this.originalFacilityConfig;
+ }
+
+ get facilityChanged(): boolean {
+ const config = this.facilityConfig;
+ return this.toggles.some((t) => config[t.key] !== this.originalFacilityConfig[t.key]);
+ }
+
+ get changedToggles(): any[] {
+ const list: any[] = [];
+ const config = this.facilityConfig;
+ this.toggles.forEach((t) => {
+ if (config[t.key] !== this.originalFacilityConfig[t.key]) {
+ list.push({
+ label: t.label,
+ from: this.originalFacilityConfig[t.key],
+ to: config[t.key]
+ });
+ }
+ });
+ return list;
+ }
+
+ get nomineeChanged(): boolean {
+ return this.buildNomineePayload() !== null;
+ }
+
+ get canSubmit(): boolean {
+ return (
+ !this.hasPendingEdit &&
+ !this.pendingEditLoading &&
+ !this.submitting &&
+ (this.facilityChanged || this.nomineeChanged)
+ );
+ }
+
+ setTemplate($event: any): void {
+ this.savingsAccountProductTemplate = $event;
+ }
+
+ next(): void {
+ if (this.selectedTabIndex < this.totalTabs - 1) {
+ this.selectedTabIndex += 1;
+ }
+ }
+
+ prev(): void {
+ if (this.selectedTabIndex > 0) {
+ this.selectedTabIndex -= 1;
+ }
+ }
+
+ getRelationshipName(relationshipId: any): string {
+ const opt = this.relationshipOptions.find((r: any) => r.id === relationshipId);
+ return opt?.name || '';
+ }
+
+ private buildFacilityDiff(): Record | null {
+ const diff: Record = {};
+ let changed = false;
+ const config = this.facilityConfig;
+ this.toggles.forEach((t) => {
+ if (config[t.key] !== this.originalFacilityConfig[t.key]) {
+ diff[t.key] = config[t.key];
+ changed = true;
+ }
+ });
+ return changed ? diff : null;
+ }
+
+ private buildNomineePayload(): any | null {
+ const original = this.originalNominees || [];
+ const current = this.savingsAccountCurrencyStep?.nominees ?? [];
+
+ // Find if a nominee has been added
+ const added = current.find(
+ (curr: any) => !original.some((orig: any) => orig.id === curr.id || orig.nomineeClientId === curr.nomineeClientId)
+ );
+ if (added) {
+ return {
+ operation: 'ADD',
+ nomineeId: null,
+ nomineeClientId: added.nomineeClientId,
+ relationshipId: added.relationshipId,
+ percentageShare: added.percentageShare,
+ isActive: added.isActive !== undefined ? !!added.isActive : true,
+ nomineeIsMinor: !!added.nomineeIsMinor,
+ guardianClientId: added.nomineeIsMinor ? added.guardianClientId : null,
+ guardianRelationshipId: added.nomineeIsMinor ? added.guardianRelationshipId : null,
+ effectiveDate: this.todayIso(),
+ remarks: added.remarks || null
+ };
+ }
+
+ // Find if a nominee has been deleted
+ const deleted = original.find(
+ (orig: any) => !current.some((curr: any) => curr.id === orig.id || curr.nomineeClientId === orig.nomineeClientId)
+ );
+ if (deleted) {
+ return {
+ operation: 'DELETE',
+ nomineeId: deleted.id
+ };
+ }
+
+ // Find if any nominee has been updated
+ for (const curr of current) {
+ const orig = original.find((o: any) => o.id === curr.id || o.nomineeClientId === curr.nomineeClientId);
+ if (orig) {
+ const relChanged = orig.relationshipId !== curr.relationshipId;
+ const shareChanged = +orig.percentageShare !== +curr.percentageShare;
+ const minorChanged = !!orig.nomineeIsMinor !== !!curr.nomineeIsMinor;
+ const guardianChanged =
+ orig.guardianClientId !== curr.guardianClientId ||
+ orig.guardianRelationshipId !== curr.guardianRelationshipId;
+ const activeChanged = !!orig.isActive !== (curr.isActive !== undefined ? !!curr.isActive : true);
+ const remarksChanged = (orig.remarks || '') !== (curr.remarks || '');
+
+ if (relChanged || shareChanged || minorChanged || guardianChanged || activeChanged || remarksChanged) {
+ return {
+ operation: 'UPDATE',
+ nomineeId: orig.id,
+ relationshipId: curr.relationshipId,
+ percentageShare: curr.percentageShare,
+ isActive: curr.isActive !== undefined ? !!curr.isActive : true,
+ nomineeIsMinor: !!curr.nomineeIsMinor,
+ guardianClientId: curr.nomineeIsMinor ? curr.guardianClientId : null,
+ guardianRelationshipId: curr.nomineeIsMinor ? curr.guardianRelationshipId : null,
+ effectiveDate: this.todayIso(),
+ remarks: curr.remarks || null
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ submit(): void {
+ if (!this.canSubmit) return;
+
+ const locale = this.settingsService.language.code;
+ const payload: any = { locale, dateFormat: 'yyyy-MM-dd' };
+
+ const facilityDiff = this.buildFacilityDiff();
+ if (facilityDiff) {
+ payload.accountFacilityConfig = facilityDiff;
+ }
+
+ const nomineePayload = this.buildNomineePayload();
+ if (nomineePayload) {
+ payload.nominee = nomineePayload;
+ }
+
+ this.submitting = true;
+ this.savingsService.submitSavingsAccountEdit(this.accountId, payload).subscribe({
+ next: () => {
+ this.submitting = false;
+ this.showSuccess();
+ },
+ error: () => {
+ this.submitting = false;
+ }
+ });
+ }
+
+ private showSuccess(): void {
+ const ref = this.dialog.open(SuccessDialogComponent, {
+ width: '420px',
+ maxWidth: '92vw',
+ autoFocus: false,
+ data: {
+ title: 'Submitted for Approval',
+ message: 'Your changes have been submitted and are awaiting checker approval.',
+ buttonLabel: 'Done'
+ }
+ });
+ ref.afterClosed().subscribe(() => this.cancel());
+ }
+
+ cancel(): void {
+ this.router.navigate(['../'], { relativeTo: this.route });
+ }
+}
diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html
index ea68c5170..1f8ce8870 100644
--- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html
+++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html
@@ -71,7 +71,7 @@
Account Type
-
+
@for (opt of accountTypeOptions; track opt) {
{{ opt }}
}
@@ -80,7 +80,11 @@
Customer Segment
-
+
@for (opt of customerSegmentOptions; track opt) {
{{ opt }}
}
@@ -117,7 +121,7 @@
Share Linking Enabled
-
+
Yes
No
@@ -132,6 +136,7 @@
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
"
(introducerChange)="onIntroducerChange($event)"
+ [disabled]="readOnly"
>
}
}
@@ -223,7 +228,7 @@
color="primary"
type="button"
class="soa-add-btn"
- [disabled]="totalJointSharePercentage >= 100"
+ [disabled]="totalJointSharePercentage >= 100 || readOnly"
(click)="openAddJointHolderDialog()"
>
person_add Add Holder
@@ -269,6 +274,7 @@
max="100"
[(ngModel)]="primaryHolderShare"
[ngModelOptions]="{ standalone: true }"
+ [disabled]="readOnly"
placeholder="0"
(input)="onPrimaryShareInput($event)"
(blur)="onPrimaryShareBlur($event)"
@@ -320,7 +326,8 @@
class="soa-sign-toggle"
[class.soa-sign-toggle--mandatory]="h.isMandatory"
[class.soa-sign-toggle--optional]="!h.isMandatory"
- (click)="toggleJointHolderMandatory(h)"
+ [disabled]="readOnly"
+ (click)="!readOnly && toggleJointHolderMandatory(h)"
>
{{ h.isMandatory ? 'lock' : 'lock_open' }}
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
@@ -328,7 +335,13 @@
Co-Holder
-
+
delete_outline
@@ -410,7 +423,8 @@
type="button"
class="soa-mode-card"
[class.soa-mode-card--selected]="modeOfOperation === opt.value"
- (click)="modeOfOperation = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (modeOfOperation = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
@@ -465,8 +479,9 @@
[max]="maxDate"
[(ngModel)]="minorDob"
[ngModelOptions]="{ standalone: true }"
+ [disabled]="readOnly"
/>
-
+
@if (clientDob) {
Auto-filled from client profile
@@ -487,7 +502,11 @@
Guardian Relationship
-
+
@for (rel of guardianRelationshipOptions; track rel) {
{{ rel }}
}
@@ -503,6 +522,7 @@
@if (guardianName) {
@@ -630,7 +650,8 @@
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value"
[class.soa-cat-tile--male]="true"
- (click)="selectedSpecialCategory = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (selectedSpecialCategory = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
@@ -680,7 +701,8 @@
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedWeakerSection === opt.value"
[class.soa-cat-tile--female]="true"
- (click)="selectedWeakerSection = opt.value"
+ [disabled]="readOnly"
+ (click)="!readOnly && (selectedWeakerSection = opt.value)"
>
{{ opt.icon }}
{{ opt.label }}
diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts
index 1d6f93357..cb93fd19e 100644
--- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts
+++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts
@@ -53,6 +53,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
/** Savings Account Template */
@Input() savingsAccountTemplate: any;
+ @Input() readOnly = false;
minDate = new Date(2000, 0, 1);
maxDate = new Date();
@@ -219,6 +220,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
this.cdr.detectChanges();
});
}
+ if (this.readOnly) {
+ this.savingsAccountDetailsForm.disable();
+ }
}
/**
@@ -277,6 +281,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
}
isTileDisabled(value: string): boolean {
+ if (this.readOnly) return true;
if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18;
if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60;
return false;
diff --git a/src/app/savings/savings-account-view/savings-account-view.component.ts b/src/app/savings/savings-account-view/savings-account-view.component.ts
index ab5de3bf3..59501a2bd 100644
--- a/src/app/savings/savings-account-view/savings-account-view.component.ts
+++ b/src/app/savings/savings-account-view/savings-account-view.component.ts
@@ -122,6 +122,7 @@ export class SavingsAccountViewComponent implements OnInit {
'Add Charge': 'admin',
'Apply Annual Fees': 'admin',
'Modify Application': 'admin',
+ 'Edit Nominee & Features': 'admin',
'Assign Staff': 'admin',
'Unassign Staff': 'admin',
Approve: 'admin',
@@ -156,6 +157,7 @@ export class SavingsAccountViewComponent implements OnInit {
'Add Charge': 'plus-circle',
'Apply Annual Fees': 'receipt',
'Modify Application': 'edit',
+ 'Edit Nominee & Features': 'edit',
'Assign Staff': 'user-plus',
'Unassign Staff': 'user-slash',
Approve: 'check-circle',
@@ -311,6 +313,9 @@ export class SavingsAccountViewComponent implements OnInit {
case 'Modify Application':
this.router.navigate(['edit'], { relativeTo: this.route });
break;
+ case 'Edit Nominee & Features':
+ this.router.navigate(['edit-nominee-features'], { relativeTo: this.route });
+ break;
case 'Delete':
this.deleteSavingsAccount();
break;
diff --git a/src/app/savings/savings-account-view/savings-buttons.config.ts b/src/app/savings/savings-account-view/savings-buttons.config.ts
index 47198897f..930167fb5 100644
--- a/src/app/savings/savings-account-view/savings-buttons.config.ts
+++ b/src/app/savings/savings-account-view/savings-buttons.config.ts
@@ -161,6 +161,10 @@ export class SavingsButtonsConfiguration {
switch (status) {
case 'Active':
this.optionArray = [
+ {
+ name: 'Edit Nominee & Features',
+ taskPermissionName: 'SUBMITEDIT_SAVINGSACCOUNT'
+ },
{
name: 'Post Interest',
taskPermissionName: 'POSTINTEREST_SAVINGSACCOUNT'
diff --git a/src/app/savings/savings-routing.module.ts b/src/app/savings/savings-routing.module.ts
index a5e3a3856..289f14d1f 100644
--- a/src/app/savings/savings-routing.module.ts
+++ b/src/app/savings/savings-routing.module.ts
@@ -27,6 +27,7 @@ import { ExportTransactionsComponent } from './savings-account-view/transactions
import { EditTransactionComponent } from './savings-account-view/transactions/edit-transaction/edit-transaction.component';
import { CreateGsimAccountComponent } from './gsim-account/create-gsim-account/create-gsim-account.component';
import { GsimAccountComponent } from './gsim-account/gsim-account.component';
+import { EditSavingsAccountFeaturesComponent } from './edit-savings-account-features/edit-savings-account-features.component';
/** Custom Resolvers */
import { SavingsAccountViewResolver } from './common-resolvers/savings-account-view.resolver';
@@ -171,6 +172,18 @@ const routes: Routes = [
savingsAccountAndTemplate: SavingsAccountAndTemplateResolver
}
},
+ {
+ path: ':savingAccountId/edit-nominee-features',
+ data: {
+ title: 'Edit Nominee & Features',
+ breadcrumb: 'Edit Nominee & Features',
+ routeParamBreadcrumb: false
+ },
+ component: EditSavingsAccountFeaturesComponent,
+ resolve: {
+ savingsAccountAndTemplate: SavingsAccountAndTemplateResolver
+ }
+ },
{
path: ':savingAccountId/transactions/:id',
data: { title: 'Savings Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false },
diff --git a/src/app/savings/savings.module.ts b/src/app/savings/savings.module.ts
index fd9480340..fbd78b468 100644
--- a/src/app/savings/savings.module.ts
+++ b/src/app/savings/savings.module.ts
@@ -63,6 +63,7 @@ import { SavingsTransactionDatatableTabComponent } from './savings-account-view/
import { GeneralTabComponent } from './savings-account-view/general-tab/general-tab.component';
import { SavingsActiveClientMembersComponent } from './savings-account-stepper/savings-active-client-members/savings-active-client-members.component';
import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbook-tab.component';
+import { EditSavingsAccountFeaturesComponent } from './edit-savings-account-features/edit-savings-account-features.component';
/**
* Savings Module
@@ -121,7 +122,8 @@ import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbo
SavingsTransactionDatatableTabComponent,
GeneralTabComponent,
SavingsActiveClientMembersComponent,
- PassbookTabComponent
+ PassbookTabComponent,
+ EditSavingsAccountFeaturesComponent
],
providers: []
})
diff --git a/src/app/savings/savings.service.ts b/src/app/savings/savings.service.ts
index 52480fc4c..26388a148 100644
--- a/src/app/savings/savings.service.ts
+++ b/src/app/savings/savings.service.ts
@@ -381,4 +381,49 @@ export class SavingsService {
loadSavingsDocument(savingAccountId: any, data: any): Observable {
return this.http.post(`/savings/${savingAccountId}/documents`, data);
}
+
+ /**
+ * Submits a maker-checker edit request (Nominee and/or Digital & Account Features change) for a
+ * savings account. The edit is held for checker approval before it is actually applied.
+ * @param {string} accountId Savings Account Id
+ * @param {any} data Edit payload — `accountFacilityConfig` and/or `nominee`, plus locale/dateFormat
+ * @returns {Observable}
+ */
+ submitSavingsAccountEdit(accountId: string, data: any): Observable {
+ return this.http.post(`/savingsaccounts/${accountId}/edit`, data);
+ }
+
+ /**
+ * @param {string} accountId Savings Account Id
+ * @returns {Observable} the pending edit for this account, if any.
+ */
+ getPendingSavingsAccountEdit(accountId: string): Observable {
+ return this.http.get(`/savingsaccounts/${accountId}/edit/pending`);
+ }
+
+ /**
+ * @returns {Observable} all savings account edits currently awaiting checker approval.
+ */
+ getAllPendingSavingsAccountEdits(): Observable {
+ return this.http.get(`/savingsaccounts/edit/pending`);
+ }
+
+ /**
+ * Approves a pending savings account edit, releasing any hold and applying the proposed changes.
+ * @param {string} accountId Savings Account Id
+ * @returns {Observable}
+ */
+ approveSavingsAccountEdit(accountId: string): Observable {
+ return this.http.post(`/savingsaccounts/${accountId}/edit/approve`, {});
+ }
+
+ /**
+ * Rejects a pending savings account edit, releasing any hold without applying the changes.
+ * @param {string} accountId Savings Account Id
+ * @param {string} rejectReason Optional reason for rejection
+ * @returns {Observable}
+ */
+ rejectSavingsAccountEdit(accountId: string, rejectReason?: string): Observable {
+ return this.http.post(`/savingsaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' });
+ }
}
diff --git a/src/app/shared/account-nominees-tab/account-nominees-tab.component.html b/src/app/shared/account-nominees-tab/account-nominees-tab.component.html
index a36df6205..779a9c8e3 100644
--- a/src/app/shared/account-nominees-tab/account-nominees-tab.component.html
+++ b/src/app/shared/account-nominees-tab/account-nominees-tab.component.html
@@ -14,13 +14,37 @@
- @if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm) {
+ @if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm && !hasPendingEdit) {