existing account saving and current add or remove nominee and digital & account features and also FE & RD nominee add or remove
This commit is contained in:
+20
-18
@@ -26,29 +26,31 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="sac-alert sac-alert--editable">
|
||||
<div class="sac-alert__content">
|
||||
<h4>Balance Swap Option</h4>
|
||||
<p>Allows balance to be swapped or transferred between linked accounts as per product configuration.</p>
|
||||
@if (!nominationOnly) {
|
||||
<div class="sac-alert sac-alert--editable">
|
||||
<div class="sac-alert__content">
|
||||
<h4>Balance Swap Option</h4>
|
||||
<p>Allows balance to be swapped or transferred between linked accounts as per product configuration.</p>
|
||||
</div>
|
||||
<mat-slide-toggle [(ngModel)]="balanceSwapOption" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
<mat-slide-toggle [(ngModel)]="balanceSwapOption" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<div class="sac-alert sac-alert--editable">
|
||||
<div class="sac-alert__content">
|
||||
<h4>MSME</h4>
|
||||
<p>Account is categorized under the Micro, Small and Medium Enterprises segment.</p>
|
||||
<div class="sac-alert sac-alert--editable">
|
||||
<div class="sac-alert__content">
|
||||
<h4>MSME</h4>
|
||||
<p>Account is categorized under the Micro, Small and Medium Enterprises segment.</p>
|
||||
</div>
|
||||
<mat-slide-toggle [(ngModel)]="msme" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
<mat-slide-toggle [(ngModel)]="msme" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<div class="sac-alert sac-alert--editable">
|
||||
<div class="sac-alert__content">
|
||||
<h4>Auto Debit Facility</h4>
|
||||
<p>Enable automatic debit instructions for recurring payments linked to this account.</p>
|
||||
<div class="sac-alert sac-alert--editable">
|
||||
<div class="sac-alert__content">
|
||||
<h4>Auto Debit Facility</h4>
|
||||
<p>Enable automatic debit instructions for recurring payments linked to this account.</p>
|
||||
</div>
|
||||
<mat-slide-toggle [(ngModel)]="autoDebitFacility" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
<mat-slide-toggle [(ngModel)]="autoDebitFacility" color="primary"></mat-slide-toggle>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="sac-alert">
|
||||
<div class="sac-alert__content">
|
||||
|
||||
+1
@@ -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[] = [];
|
||||
|
||||
+39
-14
@@ -104,7 +104,7 @@
|
||||
<div class="sad-grid sad-grid--classification">
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>Account Type</mat-label>
|
||||
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }" [disabled]="readOnly">
|
||||
@for (opt of accountTypeOptions; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
@@ -113,7 +113,11 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>Customer Segment</mat-label>
|
||||
<mat-select [(ngModel)]="selectedCustomerSegment" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select
|
||||
[(ngModel)]="selectedCustomerSegment"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
@for (opt of customerSegmentOptions; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
@@ -157,7 +161,7 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>Share Linking Enabled</mat-label>
|
||||
<mat-select [(ngModel)]="shareLinkingEnabled" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select [(ngModel)]="shareLinkingEnabled" [ngModelOptions]="{ standalone: true }" [disabled]="readOnly">
|
||||
<mat-option [value]="true">Yes</mat-option>
|
||||
<mat-option [value]="false">No</mat-option>
|
||||
</mat-select>
|
||||
@@ -172,6 +176,7 @@
|
||||
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
|
||||
"
|
||||
(introducerChange)="onIntroducerChange($event)"
|
||||
[disabled]="readOnly"
|
||||
></mifosx-introducer-search>
|
||||
}
|
||||
}
|
||||
@@ -263,7 +268,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="soa-add-btn"
|
||||
[disabled]="totalJointSharePercentage >= 100"
|
||||
[disabled]="totalJointSharePercentage >= 100 || readOnly"
|
||||
(click)="openAddJointHolderDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> 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)"
|
||||
>
|
||||
<mat-icon>{{ h.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
|
||||
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
|
||||
@@ -366,7 +373,13 @@
|
||||
</td>
|
||||
<td class="soa-td soa-td--center">Co-Holder</td>
|
||||
<td class="soa-td soa-td--center">
|
||||
<button mat-icon-button color="warn" type="button" (click)="removeJointHolder(i)">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
type="button"
|
||||
[disabled]="readOnly"
|
||||
(click)="removeJointHolder(i)"
|
||||
>
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
@@ -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)"
|
||||
>
|
||||
<mat-icon class="soa-mode-card__icon">{{ opt.icon }}</mat-icon>
|
||||
<div class="soa-mode-card__code">{{ opt.label }}</div>
|
||||
@@ -503,8 +517,9 @@
|
||||
[max]="maxDate"
|
||||
[(ngModel)]="minorDob"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="readOnly"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="minorDobPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker-toggle matSuffix [for]="minorDobPicker" [disabled]="readOnly"></mat-datepicker-toggle>
|
||||
<mat-datepicker #minorDobPicker></mat-datepicker>
|
||||
@if (clientDob) {
|
||||
<mat-hint>Auto-filled from client profile</mat-hint>
|
||||
@@ -525,7 +540,11 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="soa-field">
|
||||
<mat-label>Guardian Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="guardianRelationship" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select
|
||||
[(ngModel)]="guardianRelationship"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
@for (rel of guardianRelationshipOptions; track rel) {
|
||||
<mat-option [value]="rel">{{ rel }}</mat-option>
|
||||
}
|
||||
@@ -541,6 +560,7 @@
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onGuardianChange($event)"
|
||||
[disabled]="readOnly"
|
||||
></mifosx-introducer-search>
|
||||
|
||||
@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)"
|
||||
>
|
||||
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
|
||||
<span class="soa-cat-tile__label">{{ opt.label }}</span>
|
||||
@@ -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)"
|
||||
>
|
||||
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
|
||||
<span class="soa-cat-tile__label">{{ opt.label }}</span>
|
||||
@@ -775,7 +797,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="ni-add-btn"
|
||||
[disabled]="totalSharePercentage >= 100"
|
||||
[disabled]="totalSharePercentage >= 100 || readOnly"
|
||||
(click)="openAddRelatedClientDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> 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'"
|
||||
>
|
||||
<mat-icon>{{ c.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
|
||||
@@ -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)"
|
||||
>
|
||||
<mat-icon class="ni-sig-card__icon">{{ opt.icon }}</mat-icon>
|
||||
<div class="ni-sig-card__code">{{ opt.label }}</div>
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+24
@@ -210,5 +210,29 @@
|
||||
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="dig-card" [class.dig-card--on]="facilityConfig.internetBanking">
|
||||
<div class="dig-card__icon"><i class="fas fa-globe"></i></div>
|
||||
<div class="dig-card__body">
|
||||
<span class="dig-card__label">Internet Banking</span>
|
||||
<span class="dig-card__status">{{ facilityConfig.internetBanking ? 'Enabled' : 'Disabled' }}</span>
|
||||
</div>
|
||||
<label class="dig-toggle">
|
||||
<input type="checkbox" [(ngModel)]="facilityConfig.internetBanking" />
|
||||
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="dig-card" [class.dig-card--on]="facilityConfig.mobileBanking">
|
||||
<div class="dig-card__icon"><i class="fas fa-mobile-screen-button"></i></div>
|
||||
<div class="dig-card__body">
|
||||
<span class="dig-card__label">Mobile Banking</span>
|
||||
<span class="dig-card__status">{{ facilityConfig.mobileBanking ? 'Enabled' : 'Disabled' }}</span>
|
||||
</div>
|
||||
<label class="dig-toggle">
|
||||
<input type="checkbox" [(ngModel)]="facilityConfig.mobileBanking" />
|
||||
<span class="dig-toggle__track"><span class="dig-toggle__thumb"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-1
@@ -26,7 +26,9 @@ const DEFAULT_FACILITY_CONFIG = {
|
||||
interBankTotalCapFacility: false,
|
||||
withdrawalFacility: false,
|
||||
depositFacilityKyc: false,
|
||||
virtualDebitCard: false
|
||||
virtualDebitCard: false,
|
||||
internetBanking: false,
|
||||
mobileBanking: false
|
||||
};
|
||||
|
||||
@Component({
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: []
|
||||
})
|
||||
|
||||
@@ -370,4 +370,49 @@ export class CurrentAccountsService {
|
||||
loadCurrentAccountDocument(currentAccountId: any, data: any): Observable<any> {
|
||||
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<any>}
|
||||
*/
|
||||
submitCurrentAccountEdit(accountId: string, data: any): Observable<any> {
|
||||
return this.http.post(`/currentaccounts/${accountId}/edit`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountId Current Account Id
|
||||
* @returns {Observable<any>} the pending edit for this account, if any.
|
||||
*/
|
||||
getPendingCurrentAccountEdit(accountId: string): Observable<any> {
|
||||
return this.http.get(`/currentaccounts/${accountId}/edit/pending`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<any>} all current account edits currently awaiting checker approval.
|
||||
*/
|
||||
getAllPendingCurrentAccountEdits(): Observable<any> {
|
||||
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<any>}
|
||||
*/
|
||||
approveCurrentAccountEdit(accountId: string): Observable<any> {
|
||||
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<any>}
|
||||
*/
|
||||
rejectCurrentAccountEdit(accountId: string, rejectReason?: string): Observable<any> {
|
||||
return this.http.post(`/currentaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' });
|
||||
}
|
||||
}
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="csa-container mat-typography">
|
||||
<!-- Hero -->
|
||||
<div class="csa-header">
|
||||
<fa-icon icon="university" class="csa-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="csa-header__title">Modify Current Account Features</h2>
|
||||
<div class="csa-header__sub">Review account, manage nominees and digital features before submit.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (pendingEditLoading) {
|
||||
<div class="esf-loading">
|
||||
<mat-progress-spinner diameter="20" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Checking for pending edits...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!pendingEditLoading && hasPendingEdit) {
|
||||
<div class="esf-pending-banner">
|
||||
<mat-icon>hourglass_top</mat-icon>
|
||||
<div class="esf-pending-banner__text">
|
||||
<strong>An edit is already pending approval for this account.</strong>
|
||||
<span>
|
||||
Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status:
|
||||
<span class="esf-pending-banner__status">{{ pendingEdit.status }}</span>
|
||||
@if (pendingEdit.holdAmount) {
|
||||
· Hold placed: {{ pendingEdit.holdAmount }}
|
||||
}
|
||||
</span>
|
||||
<span>You cannot submit another edit until this one is approved or rejected.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!pendingEditLoading) {
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="csa-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="csa-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
<mifosx-current-account-details-step
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
(currentAccountProductTemplate)="setTemplate($event)"
|
||||
[readOnly]="true"
|
||||
>
|
||||
</mifosx-current-account-details-step>
|
||||
|
||||
<div class="csa-actions">
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" [disabled]="!currentAccountProductTemplate" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!currentAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="csa-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (currentAccountProductTemplate) {
|
||||
<mifosx-current-account-currency-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[primaryClientId]="currentAccountAndTemplate?.clientId ?? null"
|
||||
[isNonIndividualClient]="currentAccountDetailsStep?.isNonIndividualClient ?? false"
|
||||
[existingAccount]="currentAccountAndTemplate"
|
||||
[nominationOnly]="true"
|
||||
></mifosx-current-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Digital -->
|
||||
<mat-tab [disabled]="!currentAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="mobile-alt" class="csa-tab__icon"></fa-icon>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (currentAccountProductTemplate) {
|
||||
<mifosx-current-account-digital-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[existingAccount]="currentAccountAndTemplate"
|
||||
></mifosx-current-account-digital-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview / Submit -->
|
||||
<mat-tab [disabled]="!currentAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="csa-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (currentAccountProductTemplate) {
|
||||
<mifosx-current-account-preview-step
|
||||
[currentAccountProductTemplate]="currentAccountProductTemplate"
|
||||
[currentAccountTemplate]="currentAccountAndTemplate"
|
||||
[currentAccountTermForm]="currentAccountTermForm"
|
||||
[currentAccountSettingsForm]="currentAccountSettingsForm"
|
||||
[currentAccountTermsForm]="currentAccountTermsForm"
|
||||
[currentAccount]="currentAccount"
|
||||
[currentAccountDigital]="currentAccountDigital"
|
||||
[currentAccountNominees]="currentAccountCurrencyStep?.nominees ?? []"
|
||||
[currentAccountCurrencyFlags]="currentAccountCurrencyFlags"
|
||||
[hideButtons]="true"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-current-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="button" [disabled]="!canSubmit" (click)="submit()">
|
||||
@if (submitting) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<mat-icon>send</mat-icon> Submit for Approval
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
}
|
||||
</div>
|
||||
+206
@@ -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;
|
||||
}
|
||||
+417
@@ -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<string, boolean> = {
|
||||
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<string, boolean> = { ...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<string, boolean> {
|
||||
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<string, boolean> | null {
|
||||
const diff: Record<string, boolean> = {};
|
||||
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 });
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="csa-container mat-typography">
|
||||
<!-- Hero -->
|
||||
<div class="csa-header">
|
||||
<fa-icon icon="piggy-bank" class="csa-header__icon"></fa-icon>
|
||||
<div>
|
||||
<h2 class="csa-header__title">Modify Savings Account Features</h2>
|
||||
<div class="csa-header__sub">Review account, manage nominees and digital features before submit.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (pendingEditLoading) {
|
||||
<div class="esf-loading">
|
||||
<mat-progress-spinner diameter="20" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Checking for pending edits...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!pendingEditLoading && hasPendingEdit) {
|
||||
<div class="esf-pending-banner">
|
||||
<mat-icon>hourglass_top</mat-icon>
|
||||
<div class="esf-pending-banner__text">
|
||||
<strong>An edit is already pending approval for this account.</strong>
|
||||
<span>
|
||||
Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status:
|
||||
<span class="esf-pending-banner__status">{{ pendingEdit.status }}</span>
|
||||
@if (pendingEdit.holdAmount) {
|
||||
· Hold placed: {{ pendingEdit.holdAmount }}
|
||||
}
|
||||
</span>
|
||||
<span>You cannot submit another edit until this one is approved or rejected.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!pendingEditLoading) {
|
||||
<!-- Wizard tabs -->
|
||||
<mat-tab-group
|
||||
class="csa-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<!-- Tab 1: Product Classification + Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="check-square" class="csa-tab__icon"></fa-icon>
|
||||
Product
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
<mifosx-savings-account-details-step
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
(savingsAccountProductTemplate)="setTemplate($event)"
|
||||
[readOnly]="true"
|
||||
>
|
||||
</mifosx-savings-account-details-step>
|
||||
|
||||
<div class="csa-actions">
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" [disabled]="!savingsAccountProductTemplate" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 2: Currency -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="dollar-sign" class="csa-tab__icon"></fa-icon>
|
||||
Currency
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-currency-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[primaryClientId]="savingsAccountAndTemplate?.clientId ?? null"
|
||||
[existingAccount]="savingsAccountAndTemplate"
|
||||
></mifosx-savings-account-currency-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 3: Digital -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="mobile-alt" class="csa-tab__icon"></fa-icon>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-digital-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[existingAccount]="savingsAccountAndTemplate"
|
||||
></mifosx-savings-account-digital-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-raised-button color="primary" (click)="next()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Tab 4: Preview / Submit -->
|
||||
<mat-tab [disabled]="!savingsAccountProductTemplate">
|
||||
<ng-template mat-tab-label>
|
||||
<fa-icon icon="eye" class="csa-tab__icon"></fa-icon>
|
||||
{{ 'labels.inputs.PREVIEW' | translate }}
|
||||
</ng-template>
|
||||
<div class="csa-tab-body">
|
||||
@if (savingsAccountProductTemplate) {
|
||||
<mifosx-savings-account-preview-step
|
||||
[savingsAccountProductTemplate]="savingsAccountProductTemplate"
|
||||
[savingsAccountTemplate]="savingsAccountAndTemplate"
|
||||
[savingsAccountTermsForm]="savingsAccountTermsForm"
|
||||
[savingsAccount]="savingsAccount"
|
||||
[savingsAccountDigital]="savingsAccountDigital"
|
||||
[savingsAccountNominees]="savingsAccountCurrencyStep?.nominees ?? []"
|
||||
[hideButtons]="true"
|
||||
(submitEvent)="submit()"
|
||||
>
|
||||
</mifosx-savings-account-preview-step>
|
||||
}
|
||||
|
||||
<div class="csa-actions">
|
||||
<button mat-stroked-button (click)="prev()">
|
||||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
<span class="csa-actions__spacer"></span>
|
||||
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="button" [disabled]="!canSubmit" (click)="submit()">
|
||||
@if (submitting) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<mat-icon>send</mat-icon> Submit for Approval
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
}
|
||||
</div>
|
||||
+206
@@ -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;
|
||||
}
|
||||
+399
@@ -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<string, boolean> = {
|
||||
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<string, boolean> = { ...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<string, boolean> {
|
||||
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<string, boolean> | null {
|
||||
const diff: Record<string, boolean> = {};
|
||||
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 });
|
||||
}
|
||||
}
|
||||
+33
-11
@@ -71,7 +71,7 @@
|
||||
<div class="sad-grid sad-grid--classification">
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>Account Type</mat-label>
|
||||
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }" [disabled]="readOnly">
|
||||
@for (opt of accountTypeOptions; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
@@ -80,7 +80,11 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>Customer Segment</mat-label>
|
||||
<mat-select [(ngModel)]="selectedCustomerSegment" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select
|
||||
[(ngModel)]="selectedCustomerSegment"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
@for (opt of customerSegmentOptions; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
@@ -117,7 +121,7 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="sad-field">
|
||||
<mat-label>Share Linking Enabled</mat-label>
|
||||
<mat-select [(ngModel)]="shareLinkingEnabled" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select [(ngModel)]="shareLinkingEnabled" [ngModelOptions]="{ standalone: true }" [disabled]="readOnly">
|
||||
<mat-option [value]="true">Yes</mat-option>
|
||||
<mat-option [value]="false">No</mat-option>
|
||||
</mat-select>
|
||||
@@ -132,6 +136,7 @@
|
||||
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
|
||||
"
|
||||
(introducerChange)="onIntroducerChange($event)"
|
||||
[disabled]="readOnly"
|
||||
></mifosx-introducer-search>
|
||||
}
|
||||
}
|
||||
@@ -223,7 +228,7 @@
|
||||
color="primary"
|
||||
type="button"
|
||||
class="soa-add-btn"
|
||||
[disabled]="totalJointSharePercentage >= 100"
|
||||
[disabled]="totalJointSharePercentage >= 100 || readOnly"
|
||||
(click)="openAddJointHolderDialog()"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> 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)"
|
||||
>
|
||||
<mat-icon>{{ h.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
|
||||
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
|
||||
@@ -328,7 +335,13 @@
|
||||
</td>
|
||||
<td class="soa-td soa-td--center">Co-Holder</td>
|
||||
<td class="soa-td soa-td--center">
|
||||
<button mat-icon-button color="warn" type="button" (click)="removeJointHolder(i)">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
type="button"
|
||||
[disabled]="readOnly"
|
||||
(click)="removeJointHolder(i)"
|
||||
>
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
@@ -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)"
|
||||
>
|
||||
<mat-icon class="soa-mode-card__icon">{{ opt.icon }}</mat-icon>
|
||||
<div class="soa-mode-card__code">{{ opt.label }}</div>
|
||||
@@ -465,8 +479,9 @@
|
||||
[max]="maxDate"
|
||||
[(ngModel)]="minorDob"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="readOnly"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="minorDobPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker-toggle matSuffix [for]="minorDobPicker" [disabled]="readOnly"></mat-datepicker-toggle>
|
||||
<mat-datepicker #minorDobPicker></mat-datepicker>
|
||||
@if (clientDob) {
|
||||
<mat-hint>Auto-filled from client profile</mat-hint>
|
||||
@@ -487,7 +502,11 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="soa-field">
|
||||
<mat-label>Guardian Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="guardianRelationship" [ngModelOptions]="{ standalone: true }">
|
||||
<mat-select
|
||||
[(ngModel)]="guardianRelationship"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="readOnly"
|
||||
>
|
||||
@for (rel of guardianRelationshipOptions; track rel) {
|
||||
<mat-option [value]="rel">{{ rel }}</mat-option>
|
||||
}
|
||||
@@ -503,6 +522,7 @@
|
||||
<mifosx-introducer-search
|
||||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||||
(introducerChange)="onGuardianChange($event)"
|
||||
[disabled]="readOnly"
|
||||
></mifosx-introducer-search>
|
||||
|
||||
@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)"
|
||||
>
|
||||
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
|
||||
<span class="soa-cat-tile__label">{{ opt.label }}</span>
|
||||
@@ -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)"
|
||||
>
|
||||
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
|
||||
<span class="soa-cat-tile__label">{{ opt.label }}</span>
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: []
|
||||
})
|
||||
|
||||
@@ -381,4 +381,49 @@ export class SavingsService {
|
||||
loadSavingsDocument(savingAccountId: any, data: any): Observable<any> {
|
||||
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<any>}
|
||||
*/
|
||||
submitSavingsAccountEdit(accountId: string, data: any): Observable<any> {
|
||||
return this.http.post(`/savingsaccounts/${accountId}/edit`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountId Savings Account Id
|
||||
* @returns {Observable<any>} the pending edit for this account, if any.
|
||||
*/
|
||||
getPendingSavingsAccountEdit(accountId: string): Observable<any> {
|
||||
return this.http.get(`/savingsaccounts/${accountId}/edit/pending`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<any>} all savings account edits currently awaiting checker approval.
|
||||
*/
|
||||
getAllPendingSavingsAccountEdits(): Observable<any> {
|
||||
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<any>}
|
||||
*/
|
||||
approveSavingsAccountEdit(accountId: string): Observable<any> {
|
||||
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<any>}
|
||||
*/
|
||||
rejectSavingsAccountEdit(accountId: string, rejectReason?: string): Observable<any> {
|
||||
return this.http.post(`/savingsaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,37 @@
|
||||
<p class="nom-page-header__sub">Manage nominees for this account. Nominees are account-specific.</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm) {
|
||||
@if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm && !hasPendingEdit) {
|
||||
<button mat-flat-button color="primary" class="nom-add-btn" (click)="showAddForm = true">
|
||||
<mat-icon>add</mat-icon> Add Nominee
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (pendingEditLoading) {
|
||||
<div class="esf-loading">
|
||||
<mat-progress-spinner diameter="20" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Checking for pending edits...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!pendingEditLoading && hasPendingEdit) {
|
||||
<div class="esf-pending-banner">
|
||||
<mat-icon>hourglass_top</mat-icon>
|
||||
<div class="esf-pending-banner__text">
|
||||
<strong>An edit is already pending approval for this account.</strong>
|
||||
<span>
|
||||
Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status:
|
||||
<span class="esf-pending-banner__status">{{ pendingEdit.status }}</span>
|
||||
@if (pendingEdit.holdAmount) {
|
||||
· Hold placed: {{ pendingEdit.holdAmount }}
|
||||
}
|
||||
</span>
|
||||
<span>You cannot submit another edit until this one is approved or rejected.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
<div class="nom-loading">
|
||||
<mat-progress-spinner diameter="24" mode="indeterminate"></mat-progress-spinner>
|
||||
@@ -98,18 +122,20 @@
|
||||
<button mat-icon-button type="button" matTooltip="View" (click)="viewNominee(nominee)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
matTooltip="Delete"
|
||||
class="nom-card__delete"
|
||||
(click)="deleteNominee(nominee, i)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
@if (!hasPendingEdit) {
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
matTooltip="Delete"
|
||||
class="nom-card__delete"
|
||||
(click)="deleteNominee(nominee, i)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) {
|
||||
|
||||
@@ -645,3 +645,56 @@ $primary-light: #e8f0fe;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
import { DateFormatPipe } from '../../pipes/date-format.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SavingsService } from 'app/savings/savings.service';
|
||||
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
|
||||
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-account-nominees-tab',
|
||||
@@ -41,11 +44,14 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
private clientsService = inject(ClientsService);
|
||||
private dialog = inject(MatDialog);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
private savingsService = inject(SavingsService);
|
||||
private currentAccountsService = inject(CurrentAccountsService);
|
||||
|
||||
/** Set by route data */
|
||||
accountDataKey: string = '';
|
||||
accountType: string = '';
|
||||
|
||||
accountId: string = '';
|
||||
clientId: string = '';
|
||||
accountNo: string = '';
|
||||
loading = true;
|
||||
@@ -53,6 +59,9 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
nominees: any[] = [];
|
||||
nomineesLoading = false;
|
||||
|
||||
pendingEdit: any = null;
|
||||
pendingEditLoading = true;
|
||||
|
||||
showAddForm = false;
|
||||
searchText = '';
|
||||
searchResults: any[] = [];
|
||||
@@ -110,15 +119,45 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
this.route.parent.data.subscribe((data: any) => {
|
||||
const accountData = data[this.accountDataKey];
|
||||
if (accountData) {
|
||||
this.accountId = accountData.id?.toString() || this.route.parent.snapshot.params['savingAccountId'] || '';
|
||||
this.clientId = accountData.clientId?.toString() || '';
|
||||
this.accountNo = accountData.accountNo || '';
|
||||
this.loading = false;
|
||||
this.loadPendingEdit();
|
||||
this.loadNominees();
|
||||
this.loadTemplate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadPendingEdit(): void {
|
||||
this.pendingEditLoading = true;
|
||||
const fetch$ =
|
||||
this.accountType === 'CB'
|
||||
? this.currentAccountsService.getPendingCurrentAccountEdit(this.accountId)
|
||||
: this.savingsService.getPendingSavingsAccountEdit(this.accountId);
|
||||
fetch$.subscribe({
|
||||
next: (edit: any) => {
|
||||
this.pendingEdit = edit && edit.id ? edit : null;
|
||||
this.pendingEditLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.pendingEdit = null;
|
||||
this.pendingEditLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get hasPendingEdit(): boolean {
|
||||
return !!this.pendingEdit && this.pendingEdit.status === 'PENDING';
|
||||
}
|
||||
|
||||
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 loadTemplate(): void {
|
||||
this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({
|
||||
next: (template: any) => {
|
||||
@@ -356,27 +395,34 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
if (this.guardianSameAsNominee) return;
|
||||
|
||||
this.nomineeSaving = true;
|
||||
const payload: any = {
|
||||
const nomineePayload: any = {
|
||||
operation: 'ADD',
|
||||
nomineeId: null,
|
||||
nomineeClientId: this.selectedClient.id,
|
||||
accountNo: this.accountNo,
|
||||
accountType: this.accountType,
|
||||
nominationType: this.nomineeNomType,
|
||||
relationshipId: this.nomineeRelationshipId,
|
||||
percentageShare: this.nomineeShare,
|
||||
isActive: true,
|
||||
nomineeIsMinor: this.nomineeIsMinor,
|
||||
remarks: this.nomineeRemarks || null,
|
||||
locale: 'en'
|
||||
guardianClientId: this.nomineeIsMinor && this.selectedGuardian ? this.selectedGuardian.id : null,
|
||||
guardianRelationshipId: null,
|
||||
effectiveDate: this.todayIso(),
|
||||
remarks: this.nomineeRemarks || null
|
||||
};
|
||||
|
||||
if (this.nomineeIsMinor && this.selectedGuardian) {
|
||||
payload.guardianClientId = this.selectedGuardian.id;
|
||||
}
|
||||
const payload = {
|
||||
locale: 'en',
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
nominee: nomineePayload
|
||||
};
|
||||
|
||||
this.clientsService.addNominee(this.clientId, payload).subscribe({
|
||||
const submit$ =
|
||||
this.accountType === 'CB'
|
||||
? this.currentAccountsService.submitCurrentAccountEdit(this.accountId, payload)
|
||||
: this.savingsService.submitSavingsAccountEdit(this.accountId, payload);
|
||||
submit$.subscribe({
|
||||
next: () => {
|
||||
this.nomineeSaving = false;
|
||||
this.loadNominees();
|
||||
this.resetAddForm();
|
||||
this.showSuccess();
|
||||
},
|
||||
error: () => {
|
||||
this.nomineeSaving = false;
|
||||
@@ -384,6 +430,24 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
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.loadPendingEdit();
|
||||
this.loadNominees();
|
||||
this.resetAddForm();
|
||||
});
|
||||
}
|
||||
|
||||
viewNominee(nominee: any): void {
|
||||
const clientId = nominee.nomineeClientId || nominee.linkedClientId || nominee.clientId;
|
||||
const displayName = nominee.nomineeDisplayName || nominee.firstName || '';
|
||||
@@ -432,17 +496,33 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
if (!this.editRelId || !this.editShare || this.editShare < 1 || this.editShare > this.maxShareForEdit(index))
|
||||
return;
|
||||
|
||||
const payload: any = {
|
||||
const nomineePayload: any = {
|
||||
operation: 'UPDATE',
|
||||
nomineeId: nominee.id,
|
||||
relationshipId: this.editRelId,
|
||||
percentageShare: this.editShare,
|
||||
nominationType: this.editNomType,
|
||||
locale: 'en'
|
||||
isActive: nominee.isActive !== undefined ? !!nominee.isActive : true,
|
||||
nomineeIsMinor: !!nominee.nomineeIsMinor,
|
||||
guardianClientId: nominee.nomineeIsMinor ? nominee.guardianClientId : null,
|
||||
guardianRelationshipId: nominee.nomineeIsMinor ? nominee.guardianRelationshipId : null,
|
||||
effectiveDate: this.todayIso(),
|
||||
remarks: nominee.remarks || null
|
||||
};
|
||||
|
||||
this.clientsService.updateNominee(this.clientId, nominee.id.toString(), payload).subscribe({
|
||||
const payload = {
|
||||
locale: 'en',
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
nominee: nomineePayload
|
||||
};
|
||||
|
||||
const submit$ =
|
||||
this.accountType === 'CB'
|
||||
? this.currentAccountsService.submitCurrentAccountEdit(this.accountId, payload)
|
||||
: this.savingsService.submitSavingsAccountEdit(this.accountId, payload);
|
||||
submit$.subscribe({
|
||||
next: () => {
|
||||
this.loadNominees();
|
||||
this.editingIndex = null;
|
||||
this.showSuccess();
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
@@ -454,9 +534,26 @@ export class AccountNomineesTabComponent implements OnInit {
|
||||
});
|
||||
ref.afterClosed().subscribe((response: any) => {
|
||||
if (response?.delete) {
|
||||
this.clientsService.deleteNominee(this.clientId, nominee.id.toString()).subscribe({
|
||||
next: () => this.nominees.splice(index, 1),
|
||||
error: () => this.nominees.splice(index, 1)
|
||||
const nomineePayload: any = {
|
||||
operation: 'DELETE',
|
||||
nomineeId: nominee.id
|
||||
};
|
||||
|
||||
const payload = {
|
||||
locale: 'en',
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
nominee: nomineePayload
|
||||
};
|
||||
|
||||
const submit$ =
|
||||
this.accountType === 'CB'
|
||||
? this.currentAccountsService.submitCurrentAccountEdit(this.accountId, payload)
|
||||
: this.savingsService.submitSavingsAccountEdit(this.accountId, payload);
|
||||
submit$.subscribe({
|
||||
next: () => {
|
||||
this.showSuccess();
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
<div class="intro-search-wrap">
|
||||
<mat-form-field appearance="outline" class="intro-search-field">
|
||||
<mat-label>Search by Name or Client ID</mat-label>
|
||||
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
[value]="searchText"
|
||||
(input)="onSearch($any($event.target).value)"
|
||||
autocomplete="off"
|
||||
[disabled]="disabled"
|
||||
/>
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -86,9 +92,11 @@
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<button mat-icon-button type="button" (click)="removeClient()" class="intro-card__remove">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
@if (!disabled) {
|
||||
<button mat-icon-button type="button" (click)="removeClient()" class="intro-card__remove">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (fetchingLinkedData) {
|
||||
|
||||
@@ -33,6 +33,7 @@ export class IntroducerSearchComponent implements OnChanges {
|
||||
|
||||
@Input() excludedClientIds: number[] = [];
|
||||
@Input() preselectedIntroducer: { clientId: number; displayName: string; cifNumber?: string } | null = null;
|
||||
@Input() disabled = false;
|
||||
@Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string; cifNumber?: string } | null>();
|
||||
|
||||
entryMode: 'linked' = 'linked';
|
||||
|
||||
+10
-6
@@ -104,18 +104,22 @@ export class CheckerInboxComponent implements OnInit {
|
||||
];
|
||||
|
||||
/**
|
||||
* Entities whose pending edits are gated by our own m_savings_product_temp lock rather than the
|
||||
* native command-source pipeline. They're excluded from Checker Inbox entirely - approving/rejecting
|
||||
* them here would call the native /makercheckers/{id}?command=approve endpoint, which replays the
|
||||
* original command handler directly and never touches our temp table, leaving its lock stuck forever.
|
||||
* Action them from Approvals -> Pending Product Edits instead, where that's handled correctly.
|
||||
* Entities whose pending edits are gated by our own custom staging tables (m_savings_product_temp
|
||||
* for products, the savings account edit-request table for Nominee/Digital-Feature edits) rather
|
||||
* than the native command-source pipeline. They're excluded from Checker Inbox entirely -
|
||||
* approving/rejecting them here would call the native /makercheckers/{id}?command=approve endpoint,
|
||||
* which replays the original command handler directly and never touches our staging table, leaving
|
||||
* its lock/hold stuck forever. Action them from Approvals -> Pending Product Edits / Pending Account
|
||||
* Edits instead, where that's handled correctly.
|
||||
*/
|
||||
private readonly productEditEntities = [
|
||||
'SAVINGSPRODUCT',
|
||||
'CURRENTPRODUCT',
|
||||
'SHAREPRODUCT',
|
||||
'FIXEDDEPOSITPRODUCT',
|
||||
'RECURRINGDEPOSITPRODUCT'
|
||||
'RECURRINGDEPOSITPRODUCT',
|
||||
'SAVINGSACCOUNT',
|
||||
'CURRENTACCOUNT'
|
||||
];
|
||||
|
||||
private excludeProductEdits(items: any[]): any[] {
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="psae-tab-content">
|
||||
@if (showData) {
|
||||
<div class="psae-action-bar">
|
||||
<mat-form-field appearance="outline" class="psae-search-field">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input #filterInput matInput placeholder="Search by account number" (keyup)="applyFilter(filterInput.value)" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="modern-table">
|
||||
<ng-container matColumnDef="accountNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Account No.</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.accountNo }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="accountType">
|
||||
<th mat-header-cell *matHeaderCellDef>Account Type</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span
|
||||
class="status-badge"
|
||||
[class.status-badge--savings]="edit.accountType === 'Savings'"
|
||||
[class.status-badge--current]="edit.accountType === 'Current'"
|
||||
[class.status-badge--fd]="edit.accountType === 'Fixed Deposit'"
|
||||
[class.status-badge--rd]="edit.accountType === 'Recurring Deposit'"
|
||||
>
|
||||
{{ edit.accountType }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="makerUsername">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.makerUsername }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="createdOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ formatDate(edit.createdOn) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="holdAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>Hold Amount</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
@if (edit.holdAmount) {
|
||||
<span class="status-badge status-badge--hold">{{ edit.holdAmount }}</span>
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span class="status-badge status-badge--pending">{{ edit.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<button mat-stroked-button class="psae-btn psae-btn-view" (click)="viewCompare(edit)">
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>Compare
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-reject" (click)="reject(edit)">
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (selectedEdit) {
|
||||
<div class="psae-diff-panel">
|
||||
<div class="psae-diff-header">
|
||||
<h3>Proposed Changes — Account "{{ selectedEdit.accountNo }}"</h3>
|
||||
<button mat-icon-button (click)="closeCompare()"><fa-icon icon="times"></fa-icon></button>
|
||||
</div>
|
||||
|
||||
@if (selectedEdit.facilityConfigSubmitted) {
|
||||
<div class="psae-diff-block">
|
||||
<h4>Digital & Account Features</h4>
|
||||
<table class="psae-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Proposed Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of entries(selectedEdit.proposedFacilityConfig); track row.key) {
|
||||
<tr>
|
||||
<td class="psae-diff-label">{{ row.key }}</td>
|
||||
<td class="psae-diff-proposed">{{ row.value }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedEdit.nomineeSubmitted) {
|
||||
<div class="psae-diff-block">
|
||||
<h4>Nominee Change</h4>
|
||||
<table class="psae-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Proposed Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of entries(selectedEdit.proposedNominee); track row.key) {
|
||||
<tr>
|
||||
<td class="psae-diff-label">{{ row.key }}</td>
|
||||
<td class="psae-diff-proposed">{{ row.value }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!selectedEdit.facilityConfigSubmitted && !selectedEdit.nomineeSubmitted) {
|
||||
<p class="psae-diff-loading">No proposed changes recorded on this edit.</p>
|
||||
}
|
||||
|
||||
@if (selectedEdit.rejectReason) {
|
||||
<p class="psae-reject-reason"><strong>Reject Reason:</strong> {{ selectedEdit.rejectReason }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!showData) {
|
||||
<div class="empty-state">
|
||||
<fa-icon icon="hourglass-half" class="empty-icon"></fa-icon>
|
||||
<p class="empty-title">No pending account edits</p>
|
||||
<p class="empty-text">There are no Nominee / Digital-Feature edits awaiting approval at this time.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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;
|
||||
$text-muted: #64748b;
|
||||
|
||||
.psae-tab-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.psae-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.psae-search-field {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.psae-btn {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px !important;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
margin-right: 6px;
|
||||
|
||||
fa-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn-view {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn-approve {
|
||||
border-color: #16a34a;
|
||||
color: #16a34a;
|
||||
|
||||
&:hover {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn-reject {
|
||||
border-color: #dc2626;
|
||||
color: #dc2626;
|
||||
|
||||
&:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
||||
&--pending {
|
||||
background: #fef9c3;
|
||||
color: #854d0e;
|
||||
}
|
||||
|
||||
&--hold {
|
||||
background: #ffedd5;
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
&--savings {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
&--current {
|
||||
background: #f3e8ff;
|
||||
color: #6b21a8;
|
||||
}
|
||||
|
||||
&--fd {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
&--rd {
|
||||
background: #ecfdf5;
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-panel {
|
||||
margin-top: 20px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.psae-diff-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-block {
|
||||
margin-top: 16px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-loading {
|
||||
color: $text-muted;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.psae-diff-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 4px;
|
||||
font-size: 12.5px;
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $text-muted;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-label {
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.psae-diff-proposed {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.psae-reject-reason {
|
||||
margin-top: 14px;
|
||||
font-size: 12.5px;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 24px;
|
||||
background: #fff;
|
||||
border: 1px dashed $border;
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 42px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: $text-muted;
|
||||
max-width: 380px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
MatTableDataSource,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { SavingsService } from 'app/savings/savings.service';
|
||||
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
|
||||
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Checker-facing list of savings account Nominee / Digital-Feature edits awaiting approval.
|
||||
* Mirrors PendingSavingsProductsComponent's list + compare + approve/reject pattern.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-pending-savings-account-edits',
|
||||
templateUrl: './pending-savings-account-edits.component.html',
|
||||
styleUrls: ['./pending-savings-account-edits.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatIcon,
|
||||
MatIconButton
|
||||
]
|
||||
})
|
||||
export class PendingSavingsAccountEditsComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private savingsService = inject(SavingsService);
|
||||
private currentAccountsService = inject(CurrentAccountsService);
|
||||
|
||||
dataSource: MatTableDataSource<any>;
|
||||
showData = false;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'accountNo',
|
||||
'accountType',
|
||||
'makerUsername',
|
||||
'createdOn',
|
||||
'holdAmount',
|
||||
'status',
|
||||
'actions'
|
||||
];
|
||||
|
||||
selectedEdit: any = null;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { pendingSavingsAccountEditsData: any; pendingCurrentAccountEditsData: any }) => {
|
||||
const savingsItems = (data.pendingSavingsAccountEditsData || []).map((item: any) => {
|
||||
let type = 'Savings';
|
||||
if (item.depositAccountType === 'FIXED_DEPOSIT') {
|
||||
type = 'Fixed Deposit';
|
||||
} else if (item.depositAccountType === 'RECURRING_DEPOSIT') {
|
||||
type = 'Recurring Deposit';
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
accountType: type
|
||||
};
|
||||
});
|
||||
const currentItems = (data.pendingCurrentAccountEditsData || []).map((item: any) => ({
|
||||
...item,
|
||||
accountType: 'Current'
|
||||
}));
|
||||
const items = [
|
||||
...savingsItems,
|
||||
...currentItems
|
||||
];
|
||||
this.showData = items.length > 0;
|
||||
this.dataSource = new MatTableDataSource(items);
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string = '') {
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
}
|
||||
|
||||
formatDate(date: any): string {
|
||||
if (!date) return '';
|
||||
const d = Array.isArray(date)
|
||||
? new Date(date[0], date[1] - 1, date[2], date[3] || 0, date[4] || 0)
|
||||
: new Date(date);
|
||||
return isNaN(d.getTime()) ? String(date) : d.toLocaleString('en-IN');
|
||||
}
|
||||
|
||||
/** Readable list of "key: value" pairs for the proposed facility/nominee changes. */
|
||||
entries(obj: any): { key: string; value: any }[] {
|
||||
if (!obj) return [];
|
||||
return Object.keys(obj).map((key) => ({ key, value: obj[key] }));
|
||||
}
|
||||
|
||||
viewCompare(edit: any): void {
|
||||
this.selectedEdit = this.selectedEdit === edit ? null : edit;
|
||||
}
|
||||
|
||||
closeCompare(): void {
|
||||
this.selectedEdit = null;
|
||||
}
|
||||
|
||||
approve(edit: any): void {
|
||||
const isCurrent = edit.accountType === 'Current';
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
heading: isCurrent ? 'Approve Current Account Edit' : 'Approve Savings Account Edit',
|
||||
dialogContext: `Apply the pending Nominee/Digital-Feature edit for account "${edit.accountNo}"? This cannot be undone.`,
|
||||
type: 'basic'
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response?.confirm) {
|
||||
if (isCurrent) {
|
||||
this.currentAccountsService.approveCurrentAccountEdit(edit.currentAccountId).subscribe(() => this.reload());
|
||||
} else {
|
||||
this.savingsService.approveSavingsAccountEdit(edit.savingsAccountId).subscribe(() => this.reload());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reject(edit: any): void {
|
||||
const isCurrent = edit.accountType === 'Current';
|
||||
const formfields: FormfieldBase[] = [
|
||||
new InputBase({
|
||||
controlName: 'rejectReason',
|
||||
label: 'Reason for rejection',
|
||||
value: '',
|
||||
type: 'text',
|
||||
required: false
|
||||
})
|
||||
];
|
||||
const dialogRef = this.dialog.open(FormDialogComponent, {
|
||||
data: {
|
||||
title: `Reject edit for account "${edit.accountNo}"`,
|
||||
layout: { addButtonText: 'Reject' },
|
||||
formfields
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response?.data) {
|
||||
const rejectReason = response.data.value.rejectReason || '';
|
||||
if (isCurrent) {
|
||||
this.currentAccountsService
|
||||
.rejectCurrentAccountEdit(edit.currentAccountId, rejectReason)
|
||||
.subscribe(() => this.reload());
|
||||
} else {
|
||||
this.savingsService
|
||||
.rejectSavingsAccountEdit(edit.savingsAccountId, rejectReason)
|
||||
.subscribe(() => this.reload());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.closeCompare();
|
||||
const url = this.router.url;
|
||||
this.router
|
||||
.navigateByUrl('/checker-inbox-and-tasks', { skipLocationChange: true })
|
||||
.then(() => this.router.navigate([url]));
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,18 @@
|
||||
<fa-icon icon="piggy-bank" class="ac-tab__ic"></fa-icon>
|
||||
<span>Pending Product Edits</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
[routerLink]="['./pending-savings-account-edits']"
|
||||
routerLinkActive
|
||||
#pendingSavingsAccountEdits="routerLinkActive"
|
||||
[active]="pendingSavingsAccountEdits.isActive"
|
||||
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
|
||||
>
|
||||
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
|
||||
<span>Pending Account Edits</span>
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
class="ac-tab"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Resolve } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
|
||||
|
||||
/**
|
||||
* Resolves all current account Nominee/Digital-Feature edits currently awaiting checker approval,
|
||||
* for the Pending Current Account Edits screen.
|
||||
*/
|
||||
@Injectable()
|
||||
export class GetPendingCurrentAccountEdits implements Resolve<any> {
|
||||
private currentAccountsService = inject(CurrentAccountsService);
|
||||
|
||||
resolve(): Observable<any> {
|
||||
return this.currentAccountsService.getAllPendingCurrentAccountEdits();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Resolve } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SavingsService } from 'app/savings/savings.service';
|
||||
|
||||
/**
|
||||
* Resolves all savings account Nominee/Digital-Feature edits currently awaiting checker approval,
|
||||
* for the Pending Savings Account Edits screen.
|
||||
*/
|
||||
@Injectable()
|
||||
export class GetPendingSavingsAccountEdits implements Resolve<any> {
|
||||
private savingsService = inject(SavingsService);
|
||||
|
||||
resolve(): Observable<any> {
|
||||
return this.savingsService.getAllPendingSavingsAccountEdits();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { PendingAccountComponent } from './checker-inbox-and-tasks-tabs/pending-
|
||||
import { RejectedAccountComponent } from './checker-inbox-and-tasks-tabs/rejected-account/rejected-account.component';
|
||||
import { TerminatedAccountComponent } from './checker-inbox-and-tasks-tabs/terminated-account/terminated-account.component';
|
||||
import { PendingSavingsProductsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-products/pending-savings-products.component';
|
||||
import { PendingSavingsAccountEditsComponent } from './checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component';
|
||||
import { ViewCheckerInboxComponent } from './view-checker-inbox/view-checker-inbox.component';
|
||||
|
||||
/** Custom Resolvers */
|
||||
@@ -42,6 +43,8 @@ import { GetPendingAccounts } from './common-resolvers/getPendingAccounts.resolv
|
||||
import { GetRejectedAccounts } from './common-resolvers/getRejectedAccounts.resolver';
|
||||
import { GetTerminatedAccounts } from './common-resolvers/getTerminatedAccounts.resolver';
|
||||
import { GetPendingSavingsProducts } from './common-resolvers/getPendingSavingsProducts.resolver';
|
||||
import { GetPendingSavingsAccountEdits } from './common-resolvers/getPendingSavingsAccountEdits.resolver';
|
||||
import { GetPendingCurrentAccountEdits } from './common-resolvers/getPendingCurrentAccountEdits.resolver';
|
||||
|
||||
/** Tasks Routes */
|
||||
const routes: Routes = [
|
||||
@@ -125,6 +128,19 @@ const routes: Routes = [
|
||||
pendingSavingsProductsData: GetPendingSavingsProducts
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pending-savings-account-edits',
|
||||
component: PendingSavingsAccountEditsComponent,
|
||||
data: { title: 'Pending Account Edits' },
|
||||
resolve: {
|
||||
pendingSavingsAccountEditsData: GetPendingSavingsAccountEdits,
|
||||
pendingCurrentAccountEditsData: GetPendingCurrentAccountEdits
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pending-current-account-edits',
|
||||
redirectTo: 'pending-savings-account-edits'
|
||||
},
|
||||
{
|
||||
path: 'withdrawal-requests',
|
||||
component: WithdrawalRequestsComponent,
|
||||
@@ -197,7 +213,9 @@ const routes: Routes = [
|
||||
GetPendingAccounts,
|
||||
GetRejectedAccounts,
|
||||
GetTerminatedAccounts,
|
||||
GetPendingSavingsProducts
|
||||
GetPendingSavingsProducts,
|
||||
GetPendingSavingsAccountEdits,
|
||||
GetPendingCurrentAccountEdits
|
||||
]
|
||||
})
|
||||
export class TasksRoutingModule {}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260703',
|
||||
'hash': 'd00eac0a'
|
||||
'version': '260710',
|
||||
'hash': '96c4a28c'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user