diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html index 1a0f71f1c..1f088d4d0 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html @@ -26,29 +26,31 @@ -
-
-

Balance Swap Option

-

Allows balance to be swapped or transferred between linked accounts as per product configuration.

+ @if (!nominationOnly) { +
+
+

Balance Swap Option

+

Allows balance to be swapped or transferred between linked accounts as per product configuration.

+
+
- -
-
-
-

MSME

-

Account is categorized under the Micro, Small and Medium Enterprises segment.

+
+
+

MSME

+

Account is categorized under the Micro, Small and Medium Enterprises segment.

+
+
- -
-
-
-

Auto Debit Facility

-

Enable automatic debit instructions for recurring payments linked to this account.

+
+
+

Auto Debit Facility

+

Enable automatic debit instructions for recurring payments linked to this account.

+
+
- -
+ }
diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts index 9e8e1acd6..89bdb9e11 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts @@ -30,6 +30,7 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { @Input() primaryClientId: number | null = null; @Input() isNonIndividualClient = false; @Input() existingAccount: any = null; + @Input() nominationOnly = false; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nominees: any[] = []; diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index c040b6031..1cf4ac389 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -104,7 +104,7 @@
Account Type - + @for (opt of accountTypeOptions; track opt) { {{ opt }} } @@ -113,7 +113,11 @@ Customer Segment - + @for (opt of customerSegmentOptions; track opt) { {{ opt }} } @@ -157,7 +161,7 @@ Share Linking Enabled - + Yes No @@ -172,6 +176,7 @@ introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null " (introducerChange)="onIntroducerChange($event)" + [disabled]="readOnly" > } } @@ -263,7 +268,7 @@ color="primary" type="button" class="soa-add-btn" - [disabled]="totalJointSharePercentage >= 100" + [disabled]="totalJointSharePercentage >= 100 || readOnly" (click)="openAddJointHolderDialog()" > person_add Add Holder @@ -309,6 +314,7 @@ max="100" [(ngModel)]="primaryHolderShare" [ngModelOptions]="{ standalone: true }" + [disabled]="readOnly" placeholder="0" (input)="onPrimaryShareInput($event)" (blur)="onPrimaryShareBlur($event)" @@ -358,7 +364,8 @@ class="soa-sign-toggle" [class.soa-sign-toggle--mandatory]="h.isMandatory" [class.soa-sign-toggle--optional]="!h.isMandatory" - (click)="toggleJointHolderMandatory(h)" + [disabled]="readOnly" + (click)="!readOnly && toggleJointHolderMandatory(h)" > {{ h.isMandatory ? 'lock' : 'lock_open' }} {{ h.isMandatory ? 'Mandatory' : 'Optional' }} @@ -366,7 +373,13 @@ Co-Holder - @@ -448,7 +461,8 @@ type="button" class="soa-mode-card" [class.soa-mode-card--selected]="modeOfOperation === opt.value" - (click)="modeOfOperation = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (modeOfOperation = opt.value)" > {{ opt.icon }}
{{ opt.label }}
@@ -503,8 +517,9 @@ [max]="maxDate" [(ngModel)]="minorDob" [ngModelOptions]="{ standalone: true }" + [disabled]="readOnly" /> - + @if (clientDob) { Auto-filled from client profile @@ -525,7 +540,11 @@ Guardian Relationship - + @for (rel of guardianRelationshipOptions; track rel) { {{ rel }} } @@ -541,6 +560,7 @@ @if (guardianName) { @@ -667,7 +687,8 @@ class="soa-cat-tile" [class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value" [class.soa-cat-tile--male]="true" - (click)="selectedSpecialCategory = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (selectedSpecialCategory = opt.value)" > {{ opt.icon }} {{ opt.label }} @@ -717,7 +738,8 @@ class="soa-cat-tile" [class.soa-cat-tile--selected]="selectedWeakerSection === opt.value" [class.soa-cat-tile--female]="true" - (click)="selectedWeakerSection = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (selectedWeakerSection = opt.value)" > {{ opt.icon }} {{ opt.label }} @@ -775,7 +797,7 @@ color="primary" type="button" class="ni-add-btn" - [disabled]="totalSharePercentage >= 100" + [disabled]="totalSharePercentage >= 100 || readOnly" (click)="openAddRelatedClientDialog()" > person_add Add Partner @@ -826,7 +848,8 @@ class="ni-sign-toggle" [class.ni-sign-toggle--mandatory]="c.isMandatory" [class.ni-sign-toggle--optional]="!c.isMandatory" - (click)="toggleMandatory(c)" + [disabled]="readOnly" + (click)="!readOnly && toggleMandatory(c)" [title]="c.isMandatory ? 'Click to set Optional' : 'Click to set Mandatory'" > {{ c.isMandatory ? 'lock' : 'lock_open' }} @@ -838,6 +861,7 @@ mat-icon-button color="warn" type="button" + [disabled]="readOnly" (click)="removeRelatedClient(i)" class="ni-del-btn" > @@ -915,7 +939,8 @@ type="button" class="ni-sig-card" [class.ni-sig-card--selected]="signatoryRights === opt.value" - (click)="signatoryRights = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (signatoryRights = opt.value)" > {{ opt.icon }}
{{ opt.label }}
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index 51ab90f3f..8c5592d1a 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -53,6 +53,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { @Input() currentAccountTemplate: any; @Input() hideButtons = false; + @Input() readOnly = false; minDate = new Date(2000, 0, 1); maxDate = new Date(); @@ -236,6 +237,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit { }); } } + if (this.readOnly) { + this.currentAccountDetailsForm.disable(); + } } createCurrentAccountDetailsForm() { @@ -453,6 +457,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { } isTileDisabled(value: string): boolean { + if (this.readOnly) return true; if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; return false; diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html index 5b0970996..ae448a85a 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html @@ -210,5 +210,29 @@
+ +
+
+
+ Internet Banking + {{ facilityConfig.internetBanking ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Mobile Banking + {{ facilityConfig.mobileBanking ? 'Enabled' : 'Disabled' }} +
+ +
diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts index 49597f57a..6e5cc09d5 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts @@ -26,7 +26,9 @@ const DEFAULT_FACILITY_CONFIG = { interBankTotalCapFacility: false, withdrawalFacility: false, depositFacilityKyc: false, - virtualDebitCard: false + virtualDebitCard: false, + internetBanking: false, + mobileBanking: false }; @Component({ diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts index d5d718959..dbddadaa1 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.ts @@ -78,7 +78,9 @@ export class CurrentAccountPreviewStepComponent implements OnChanges { { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap' }, { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' }, - { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + { key: 'virtualDebitCard', label: 'Virtual Debit Card' }, + { key: 'internetBanking', label: 'Internet Banking' }, + { key: 'mobileBanking', label: 'Mobile Banking' } ]; private translateService = inject(TranslateService); diff --git a/src/app/current-accounts/current-account-view/current-account-view.component.ts b/src/app/current-accounts/current-account-view/current-account-view.component.ts index 3129db777..1412dd044 100644 --- a/src/app/current-accounts/current-account-view/current-account-view.component.ts +++ b/src/app/current-accounts/current-account-view/current-account-view.component.ts @@ -124,6 +124,7 @@ export class CurrentAccountViewComponent implements OnInit { 'Add Charge': 'admin', 'Apply Annual Fees': 'admin', 'Modify Application': 'admin', + 'Edit Nominee & Features': 'admin', 'Assign Staff': 'admin', 'Unassign Staff': 'admin', Approve: 'admin', @@ -156,6 +157,7 @@ export class CurrentAccountViewComponent implements OnInit { 'Add Charge': 'plus-circle', 'Apply Annual Fees': 'receipt', 'Modify Application': 'edit', + 'Edit Nominee & Features': 'edit', 'Assign Staff': 'user-plus', 'Unassign Staff': 'user-slash', Approve: 'check-circle', @@ -314,6 +316,9 @@ export class CurrentAccountViewComponent implements OnInit { case 'Modify Application': this.router.navigate(['edit'], { relativeTo: this.route }); break; + case 'Edit Nominee & Features': + this.router.navigate(['edit-nominee-features'], { relativeTo: this.route }); + break; case 'Delete': this.deleteCurrentAccount(); break; diff --git a/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts b/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts index e5fb4df88..bbc0e3872 100644 --- a/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts +++ b/src/app/current-accounts/current-account-view/current-accounts-buttons.config.ts @@ -162,6 +162,10 @@ export class CurrentAccountButtonsConfiguration { switch (status) { case 'Active': this.optionArray = [ + { + name: 'Edit Nominee & Features', + taskPermissionName: 'SUBMITEDIT_CURRENTACCOUNT' + }, { name: 'Post Interest', taskPermissionName: 'POSTINTEREST_CURRENTACCOUNT' diff --git a/src/app/current-accounts/current-accounts-routing.module.ts b/src/app/current-accounts/current-accounts-routing.module.ts index 65539fe02..48be2cb27 100644 --- a/src/app/current-accounts/current-accounts-routing.module.ts +++ b/src/app/current-accounts/current-accounts-routing.module.ts @@ -20,6 +20,7 @@ import { StandingInstructionsTabComponent } from './current-account-view/standin import { DatatableTabsComponent } from './current-account-view/datatable-tabs/datatable-tabs.component'; import { CreateCurrentAccountComponent } from './create-current-account/create-current-account.component'; import { EditCurrentAccountComponent } from './edit-current-account/edit-current-account.component'; +import { EditCurrentAccountFeaturesComponent } from './edit-current-account-features/edit-current-account-features.component'; import { ViewTransactionComponent } from './current-account-view/transactions/view-transaction/view-transaction.component'; import { ViewChargeComponent } from './current-account-view/view-charge/view-charge.component'; import { ViewRecieptComponent } from './current-account-view/transactions/view-reciept/view-reciept.component'; @@ -170,6 +171,18 @@ const routes: Routes = [ currentAccountAndTemplate: CurrentAccountAndTemplateResolver } }, + { + path: ':currentAccountId/edit-nominee-features', + data: { + title: 'Edit Nominee & Features', + breadcrumb: 'Edit Nominee & Features', + routeParamBreadcrumb: false + }, + component: EditCurrentAccountFeaturesComponent, + resolve: { + currentAccountAndTemplate: CurrentAccountAndTemplateResolver + } + }, { path: ':currentAccountId/transactions/:id', data: { title: 'Current Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false }, diff --git a/src/app/current-accounts/current-accounts.module.ts b/src/app/current-accounts/current-accounts.module.ts index 5c7d9a284..bc321f7fb 100644 --- a/src/app/current-accounts/current-accounts.module.ts +++ b/src/app/current-accounts/current-accounts.module.ts @@ -61,6 +61,7 @@ import { CurrentAccountTransactionDatatableTabComponent } from './current-accoun import { GeneralTabComponent } from './current-account-view/general-tab/general-tab.component'; import { CurrentAccountActiveClientMembersComponent } from './current-account-stepper/current-accounts-active-client-members/current-accounts-active-client-members.component'; import { PassbookTabComponent } from './current-account-view/passbook-tab/passbook-tab.component'; +import { EditCurrentAccountFeaturesComponent } from './edit-current-account-features/edit-current-account-features.component'; /** * Current Accounts Module @@ -117,7 +118,8 @@ import { PassbookTabComponent } from './current-account-view/passbook-tab/passbo CurrentAccountTransactionDatatableTabComponent, GeneralTabComponent, CurrentAccountActiveClientMembersComponent, - PassbookTabComponent + PassbookTabComponent, + EditCurrentAccountFeaturesComponent ], providers: [] }) diff --git a/src/app/current-accounts/current-accounts.service.ts b/src/app/current-accounts/current-accounts.service.ts index 18e19593f..f6152a7df 100644 --- a/src/app/current-accounts/current-accounts.service.ts +++ b/src/app/current-accounts/current-accounts.service.ts @@ -370,4 +370,49 @@ export class CurrentAccountsService { loadCurrentAccountDocument(currentAccountId: any, data: any): Observable { return this.http.post(`/currentaccounts/${currentAccountId}/documents`, data); } + + /** + * Submits a maker-checker edit request (Nominee and/or Digital & Account Features change) for a + * current account. The edit is held for checker approval before it is actually applied. + * @param {string} accountId Current Account Id + * @param {any} data Edit payload — `accountFacilityConfig` and/or `nominee`, plus locale/dateFormat + * @returns {Observable} + */ + submitCurrentAccountEdit(accountId: string, data: any): Observable { + return this.http.post(`/currentaccounts/${accountId}/edit`, data); + } + + /** + * @param {string} accountId Current Account Id + * @returns {Observable} the pending edit for this account, if any. + */ + getPendingCurrentAccountEdit(accountId: string): Observable { + return this.http.get(`/currentaccounts/${accountId}/edit/pending`); + } + + /** + * @returns {Observable} all current account edits currently awaiting checker approval. + */ + getAllPendingCurrentAccountEdits(): Observable { + return this.http.get(`/currentaccounts/edit/pending`); + } + + /** + * Approves a pending current account edit, releasing any hold and applying the proposed changes. + * @param {string} accountId Current Account Id + * @returns {Observable} + */ + approveCurrentAccountEdit(accountId: string): Observable { + return this.http.post(`/currentaccounts/${accountId}/edit/approve`, {}); + } + + /** + * Rejects a pending current account edit, releasing any hold without applying the changes. + * @param {string} accountId Current Account Id + * @param {string} rejectReason Optional reason for rejection + * @returns {Observable} + */ + rejectCurrentAccountEdit(accountId: string, rejectReason?: string): Observable { + return this.http.post(`/currentaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' }); + } } diff --git a/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.html b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.html new file mode 100644 index 000000000..1d53a2459 --- /dev/null +++ b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.html @@ -0,0 +1,176 @@ + +
+ +
+ +
+

Modify Current Account Features

+
Review account, manage nominees and digital features before submit.
+
+
+ + @if (pendingEditLoading) { +
+ + Checking for pending edits... +
+ } + + @if (!pendingEditLoading && hasPendingEdit) { +
+ hourglass_top +
+ An edit is already pending approval for this account. + + Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status: + {{ pendingEdit.status }} + @if (pendingEdit.holdAmount) { + · Hold placed: {{ pendingEdit.holdAmount }} + } + + You cannot submit another edit until this one is approved or rejected. +
+
+ } + + @if (!pendingEditLoading) { + + + + + + + Product + +
+ + + +
+ + +
+
+
+ + + + + + Currency + +
+ @if (currentAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + Digital + +
+ @if (currentAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ @if (currentAccountProductTemplate) { + + + } + +
+ + + + +
+
+
+
+ } +
diff --git a/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.scss b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.scss new file mode 100644 index 000000000..bc2c55495 --- /dev/null +++ b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.scss @@ -0,0 +1,206 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$border: #e2e8f0; +$muted: #64748b; +$amber: #92400e; +$amber-bg: #fffbeb; +$amber-border: #fde68a; + +.esf-loading { + display: flex; + align-items: center; + gap: 8px; + padding: 20px 0; + justify-content: center; + font-size: 13px; + color: $muted; + + &--sm { + padding: 10px 0; + justify-content: flex-start; + } +} + +.esf-pending-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 16px; + background: $amber-bg; + border: 1px solid $amber-border; + border-radius: 8px; + margin-bottom: 16px; + color: $amber; + + mat-icon { + flex: 0 0 auto; + } + + &__text { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 12.5px; + + strong { + font-size: 13px; + } + } + + &__status { + font-weight: 700; + text-transform: uppercase; + } +} + +/* ─── CSA Stepper Styles ─── */ +$navy: #0d1b2a; +$blue: #1e88e5; +$blue-d: #1e40af; +$indigo: #4f46e5; + +.csa-container { + max-width: 1180px; + margin: 0 auto; + padding: 20px 24px; +} + +.csa-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + + &__icon { + width: 44px; + height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: linear-gradient(135deg, $blue 0%, $indigo 100%); + color: #fff; + font-size: 18px; + box-shadow: 0 2px 8px rgb(29 78 216 / 25%); + flex-shrink: 0; + } + + &__title { + margin: 0; + font-size: 20px; + font-weight: 800; + color: $navy; + line-height: 1.1; + letter-spacing: 0.02em; + } + + &__sub { + margin-top: 2px; + font-size: 12.5px; + color: #64748b; + } +} + +.csa-tabs { + background: #fff; + border: 1px solid $border; + border-radius: 14px; + box-shadow: 0 1px 3px rgb(15 23 42 / 4%); + overflow: hidden; + + ::ng-deep { + .mat-mdc-tab-header { + background: #f8fafc; + border-bottom: 1px solid $border; + } + + .mat-mdc-tab { + min-width: 140px; + height: 50px; + padding: 0 22px; + font-size: 13px !important; + font-weight: 600 !important; + letter-spacing: 0.01em; + color: #475569 !important; + opacity: 1 !important; + } + + .mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) { + background: rgb(30 64 175 / 5%); + color: $navy !important; + } + + .mat-mdc-tab.mat-mdc-tab-active { + .mdc-tab__text-label { + color: $blue-d !important; + font-weight: 700 !important; + } + } + + .mdc-tab-indicator__content--underline { + border-color: $blue-d !important; + border-top-width: 3px !important; + } + + .mat-mdc-tab.mat-mdc-tab-disabled { + opacity: 0.45 !important; + cursor: not-allowed; + } + + .mat-mdc-tab-body-wrapper { + min-height: 320px; + } + + .mat-mdc-tab-body, + .mat-mdc-tab-body-content { + overflow: visible !important; + } + } +} + +.csa-tab__icon { + margin-right: 8px; + font-size: 13px; +} + +.csa-tab-body { + padding: 24px; + + @media (width <= 768px) { + padding: 16px; + } +} + +.csa-actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f1f5f9; + + &__spacer { + flex: 1; + } + + button { + height: 40px !important; + border-radius: 10px !important; + font-weight: 600 !important; + padding: 0 18px !important; + } +} + +.m-l-10 { + margin-left: 8px; +} + +.m-r-10 { + margin-right: 8px; +} diff --git a/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.ts b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.ts new file mode 100644 index 000000000..bca2ab003 --- /dev/null +++ b/src/app/current-accounts/edit-current-account-features/edit-current-account-features.component.ts @@ -0,0 +1,417 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, inject, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormsModule, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTabsModule } from '@angular/material/tabs'; +import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component'; +import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component'; +import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component'; +import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component'; + +/** Custom Services */ +import { CurrentAccountsService } from '../current-accounts.service'; +import { ClientsService } from 'app/clients/clients.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Default state for every Digital & Account Feature toggle, mirrors + * current-account-digital-step's DEFAULT_FACILITY_CONFIG. */ +const DEFAULT_FACILITY_CONFIG: Record = { + atmFacility: false, + atmWithdrawalFacility: false, + upiFacility: false, + posTransactionsFacility: false, + chequeBook: false, + impsFacility: false, + smsAlerts: false, + bbpsFacility: false, + insuranceCover: false, + bepgFacility: false, + neftFacility: false, + rtgsFacility: false, + interBankTotalCapFacility: false, + withdrawalFacility: false, + depositFacilityKyc: false, + virtualDebitCard: false, + internetBanking: false, + mobileBanking: false +}; + +/** Toggle metadata used to render the feature grid — key must match a + * property of DEFAULT_FACILITY_CONFIG / the backend's accountFacilityConfig map. */ +const FACILITY_TOGGLES: { key: string; label: string; icon: string }[] = [ + { key: 'atmFacility', label: 'ATM Facility', icon: 'fa-credit-card' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal', icon: 'fa-wallet' }, + { key: 'upiFacility', label: 'UPI Facility', icon: 'fa-mobile-alt' }, + { key: 'posTransactionsFacility', label: 'POS Transactions', icon: 'fa-exchange-alt' }, + { key: 'chequeBook', label: 'Cheque Book', icon: 'fa-book' }, + { key: 'impsFacility', label: 'IMPS Facility', icon: 'fa-bolt' }, + { key: 'smsAlerts', label: 'SMS Alerts', icon: 'fa-sms' }, + { key: 'bbpsFacility', label: 'BBPS Facility', icon: 'fa-bell' }, + { key: 'insuranceCover', label: 'Insurance Cover', icon: 'fa-shield-alt' }, + { key: 'bepgFacility', label: 'BEPG Facility', icon: 'fa-plug' }, + { key: 'neftFacility', label: 'NEFT Facility', icon: 'fa-money-check-alt' }, + { key: 'rtgsFacility', label: 'RTGS Facility', icon: 'fa-money-check' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap', icon: 'fa-balance-scale' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility', icon: 'fa-arrow-right-arrow-left' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)', icon: 'fa-id-card' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card', icon: 'fa-credit-card' }, + { key: 'internetBanking', label: 'Internet Banking', icon: 'fa-globe' }, + { key: 'mobileBanking', label: 'Mobile Banking', icon: 'fa-mobile-screen-button' } +]; + +/** + * Edit Nominee & Features — maker-checker submission screen. + * + * A maker proposes changes to a current account's Digital & Account Features toggles and/or its + * nominee here; nothing is applied immediately. The request is staged and a different user (checker) + * must approve it from the "Pending Current Account Edits" screen before it takes effect. + */ +@Component({ + selector: 'mifosx-edit-current-account-features', + templateUrl: './edit-current-account-features.component.html', + styleUrls: ['./edit-current-account-features.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + FaIconComponent, + MatIcon, + MatProgressSpinner, + MatTabsModule, + CurrentAccountDetailsStepComponent, + CurrentAccountCurrencyStepComponent, + CurrentAccountDigitalStepComponent, + CurrentAccountPreviewStepComponent + ] +}) +export class EditCurrentAccountFeaturesComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private currentAccountsService = inject(CurrentAccountsService); + private clientsService = inject(ClientsService); + private settingsService = inject(SettingsService); + private dialog = inject(MatDialog); + + @ViewChild(CurrentAccountDetailsStepComponent) currentAccountDetailsStep: CurrentAccountDetailsStepComponent; + @ViewChild(CurrentAccountCurrencyStepComponent) currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent; + @ViewChild(CurrentAccountDigitalStepComponent) currentAccountDigitalStep: CurrentAccountDigitalStepComponent; + + originalNominees: any[] = []; + + get currentAccountTermForm(): FormGroup { + return new FormGroup({}); + } + + get currentAccountSettingsForm(): FormGroup { + return new FormGroup({}); + } + + get currentAccountTermsForm(): FormGroup { + return new FormGroup({}); + } + + get currentAccount() { + return this.currentAccountDetailsStep ? { ...this.currentAccountDetailsStep.currentAccountDetails } : null; + } + + get currentAccountDigital() { + return this.currentAccountDigitalStep?.currentAccountDigital ?? null; + } + + get currentAccountCurrencyFlags() { + return { + balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false, + msme: this.currentAccountCurrencyStep?.msme ?? false, + autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false + }; + } + + readonly toggles = FACILITY_TOGGLES; + + accountId: string; + currentAccountAndTemplate: any; + clientId = ''; + accountNo = ''; + + currentAccountProductTemplate: any = null; + selectedTabIndex = 0; + readonly totalTabs = 4; + + originalFacilityConfig: Record = { ...DEFAULT_FACILITY_CONFIG }; + + /** Current nominees on the account, fetched read-only for reference. */ + nominees: any[] = []; + nomineesLoading = false; + relationshipOptions: any[] = []; + + /** Pending edit already submitted for this account, if any. */ + pendingEdit: any = null; + pendingEditLoading = true; + + submitting = false; + + constructor() { + this.accountId = this.route.snapshot.params['currentAccountId']; + this.loadPendingEdit(); + this.route.data.subscribe((data: { currentAccountAndTemplate: any }) => { + const account = data.currentAccountAndTemplate; + this.currentAccountAndTemplate = account; + this.clientId = account?.clientId != null ? String(account.clientId) : ''; + this.accountNo = account?.accountNo || ''; + this.originalFacilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...(account?.accountFacilityConfig || {}) }; + if (this.clientId) { + this.loadNominees(); + this.loadRelationshipTemplate(); + } + }); + } + + private todayIso(): string { + const d = new Date(); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + } + + private loadPendingEdit(): void { + this.pendingEditLoading = true; + this.currentAccountsService.getPendingCurrentAccountEdit(this.accountId).subscribe({ + next: (edit: any) => { + this.pendingEdit = edit && edit.id ? edit : null; + this.pendingEditLoading = false; + }, + error: () => { + this.pendingEdit = null; + this.pendingEditLoading = false; + } + }); + } + + private loadNominees(): void { + this.nomineesLoading = true; + this.clientsService.getClientNominees(this.clientId, this.accountNo, 'CB').subscribe({ + next: (data: any) => { + this.nominees = Array.isArray(data) ? data : []; + this.originalNominees = JSON.parse(JSON.stringify(this.nominees)); + this.nomineesLoading = false; + }, + error: () => { + this.nominees = []; + this.originalNominees = []; + this.nomineesLoading = false; + } + }); + } + + private loadRelationshipTemplate(): void { + this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({ + next: (template: any) => { + this.relationshipOptions = template?.relationshipOptions || []; + }, + error: () => { + this.relationshipOptions = []; + } + }); + } + + get hasPendingEdit(): boolean { + return !!this.pendingEdit && this.pendingEdit.status === 'PENDING'; + } + + get facilityConfig(): Record { + return this.currentAccountDigitalStep?.facilityConfig ?? this.originalFacilityConfig; + } + + get facilityChanged(): boolean { + const config = this.facilityConfig; + return this.toggles.some((t) => config[t.key] !== this.originalFacilityConfig[t.key]); + } + + get changedToggles(): any[] { + const list: any[] = []; + const config = this.facilityConfig; + this.toggles.forEach((t) => { + if (config[t.key] !== this.originalFacilityConfig[t.key]) { + list.push({ + label: t.label, + from: this.originalFacilityConfig[t.key], + to: config[t.key] + }); + } + }); + return list; + } + + get nomineeChanged(): boolean { + return this.buildNomineePayload() !== null; + } + + get canSubmit(): boolean { + return ( + !this.hasPendingEdit && + !this.pendingEditLoading && + !this.submitting && + (this.facilityChanged || this.nomineeChanged) + ); + } + + setTemplate($event: any): void { + this.currentAccountProductTemplate = $event; + } + + next(): void { + if (this.selectedTabIndex < this.totalTabs - 1) { + this.selectedTabIndex += 1; + } + } + + prev(): void { + if (this.selectedTabIndex > 0) { + this.selectedTabIndex -= 1; + } + } + + getRelationshipName(relationshipId: any): string { + const opt = this.relationshipOptions.find((r: any) => r.id === relationshipId); + return opt?.name || ''; + } + + private buildFacilityDiff(): Record | null { + const diff: Record = {}; + let changed = false; + const config = this.facilityConfig; + this.toggles.forEach((t) => { + if (config[t.key] !== this.originalFacilityConfig[t.key]) { + diff[t.key] = config[t.key]; + changed = true; + } + }); + return changed ? diff : null; + } + + private buildNomineePayload(): any | null { + const original = this.originalNominees || []; + const current = this.currentAccountCurrencyStep?.nominees ?? []; + + // Find if a nominee has been added + const added = current.find( + (curr: any) => !original.some((orig: any) => orig.id === curr.id || orig.nomineeClientId === curr.nomineeClientId) + ); + if (added) { + return { + operation: 'ADD', + nomineeId: null, + nomineeClientId: added.nomineeClientId, + relationshipId: added.relationshipId, + percentageShare: added.percentageShare, + isActive: added.isActive !== undefined ? !!added.isActive : true, + nomineeIsMinor: !!added.nomineeIsMinor, + guardianClientId: added.nomineeIsMinor ? added.guardianClientId : null, + guardianRelationshipId: added.nomineeIsMinor ? added.guardianRelationshipId : null, + effectiveDate: this.todayIso(), + remarks: added.remarks || null + }; + } + + // Find if a nominee has been deleted + const deleted = original.find( + (orig: any) => !current.some((curr: any) => curr.id === orig.id || curr.nomineeClientId === orig.nomineeClientId) + ); + if (deleted) { + return { + operation: 'DELETE', + nomineeId: deleted.id + }; + } + + // Find if any nominee has been updated + for (const curr of current) { + const orig = original.find((o: any) => o.id === curr.id || o.nomineeClientId === curr.nomineeClientId); + if (orig) { + const relChanged = orig.relationshipId !== curr.relationshipId; + const shareChanged = +orig.percentageShare !== +curr.percentageShare; + const minorChanged = !!orig.nomineeIsMinor !== !!curr.nomineeIsMinor; + const guardianChanged = + orig.guardianClientId !== curr.guardianClientId || + orig.guardianRelationshipId !== curr.guardianRelationshipId; + const activeChanged = !!orig.isActive !== (curr.isActive !== undefined ? !!curr.isActive : true); + const remarksChanged = (orig.remarks || '') !== (curr.remarks || ''); + + if (relChanged || shareChanged || minorChanged || guardianChanged || activeChanged || remarksChanged) { + return { + operation: 'UPDATE', + nomineeId: orig.id, + relationshipId: curr.relationshipId, + percentageShare: curr.percentageShare, + isActive: curr.isActive !== undefined ? !!curr.isActive : true, + nomineeIsMinor: !!curr.nomineeIsMinor, + guardianClientId: curr.nomineeIsMinor ? curr.guardianClientId : null, + guardianRelationshipId: curr.nomineeIsMinor ? curr.guardianRelationshipId : null, + effectiveDate: this.todayIso(), + remarks: curr.remarks || null + }; + } + } + } + + return null; + } + + submit(): void { + if (!this.canSubmit) return; + + const locale = this.settingsService.language.code; + const payload: any = { locale, dateFormat: 'yyyy-MM-dd' }; + + const facilityDiff = this.buildFacilityDiff(); + if (facilityDiff) { + payload.accountFacilityConfig = facilityDiff; + } + + const nomineePayload = this.buildNomineePayload(); + if (nomineePayload) { + payload.nominee = nomineePayload; + } + + this.submitting = true; + this.currentAccountsService.submitCurrentAccountEdit(this.accountId, payload).subscribe({ + next: () => { + this.submitting = false; + this.showSuccess(); + }, + error: () => { + this.submitting = false; + } + }); + } + + private showSuccess(): void { + const ref = this.dialog.open(SuccessDialogComponent, { + width: '420px', + maxWidth: '92vw', + autoFocus: false, + data: { + title: 'Submitted for Approval', + message: 'Your changes have been submitted and are awaiting checker approval.', + buttonLabel: 'Done' + } + }); + ref.afterClosed().subscribe(() => this.cancel()); + } + + cancel(): void { + this.router.navigate(['../'], { relativeTo: this.route }); + } +} diff --git a/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.html b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.html new file mode 100644 index 000000000..a1cebfc3f --- /dev/null +++ b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.html @@ -0,0 +1,171 @@ + +
+ +
+ +
+

Modify Savings Account Features

+
Review account, manage nominees and digital features before submit.
+
+
+ + @if (pendingEditLoading) { +
+ + Checking for pending edits... +
+ } + + @if (!pendingEditLoading && hasPendingEdit) { +
+ hourglass_top +
+ An edit is already pending approval for this account. + + Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status: + {{ pendingEdit.status }} + @if (pendingEdit.holdAmount) { + · Hold placed: {{ pendingEdit.holdAmount }} + } + + You cannot submit another edit until this one is approved or rejected. +
+
+ } + + @if (!pendingEditLoading) { + + + + + + + Product + +
+ + + +
+ + +
+
+
+ + + + + + Currency + +
+ @if (savingsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + Digital + +
+ @if (savingsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ @if (savingsAccountProductTemplate) { + + + } + +
+ + + + +
+
+
+
+ } +
diff --git a/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.scss b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.scss new file mode 100644 index 000000000..bc2c55495 --- /dev/null +++ b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.scss @@ -0,0 +1,206 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$border: #e2e8f0; +$muted: #64748b; +$amber: #92400e; +$amber-bg: #fffbeb; +$amber-border: #fde68a; + +.esf-loading { + display: flex; + align-items: center; + gap: 8px; + padding: 20px 0; + justify-content: center; + font-size: 13px; + color: $muted; + + &--sm { + padding: 10px 0; + justify-content: flex-start; + } +} + +.esf-pending-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 16px; + background: $amber-bg; + border: 1px solid $amber-border; + border-radius: 8px; + margin-bottom: 16px; + color: $amber; + + mat-icon { + flex: 0 0 auto; + } + + &__text { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 12.5px; + + strong { + font-size: 13px; + } + } + + &__status { + font-weight: 700; + text-transform: uppercase; + } +} + +/* ─── CSA Stepper Styles ─── */ +$navy: #0d1b2a; +$blue: #1e88e5; +$blue-d: #1e40af; +$indigo: #4f46e5; + +.csa-container { + max-width: 1180px; + margin: 0 auto; + padding: 20px 24px; +} + +.csa-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + + &__icon { + width: 44px; + height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: linear-gradient(135deg, $blue 0%, $indigo 100%); + color: #fff; + font-size: 18px; + box-shadow: 0 2px 8px rgb(29 78 216 / 25%); + flex-shrink: 0; + } + + &__title { + margin: 0; + font-size: 20px; + font-weight: 800; + color: $navy; + line-height: 1.1; + letter-spacing: 0.02em; + } + + &__sub { + margin-top: 2px; + font-size: 12.5px; + color: #64748b; + } +} + +.csa-tabs { + background: #fff; + border: 1px solid $border; + border-radius: 14px; + box-shadow: 0 1px 3px rgb(15 23 42 / 4%); + overflow: hidden; + + ::ng-deep { + .mat-mdc-tab-header { + background: #f8fafc; + border-bottom: 1px solid $border; + } + + .mat-mdc-tab { + min-width: 140px; + height: 50px; + padding: 0 22px; + font-size: 13px !important; + font-weight: 600 !important; + letter-spacing: 0.01em; + color: #475569 !important; + opacity: 1 !important; + } + + .mat-mdc-tab:hover:not(.mdc-tab--active, .mat-mdc-tab-disabled) { + background: rgb(30 64 175 / 5%); + color: $navy !important; + } + + .mat-mdc-tab.mat-mdc-tab-active { + .mdc-tab__text-label { + color: $blue-d !important; + font-weight: 700 !important; + } + } + + .mdc-tab-indicator__content--underline { + border-color: $blue-d !important; + border-top-width: 3px !important; + } + + .mat-mdc-tab.mat-mdc-tab-disabled { + opacity: 0.45 !important; + cursor: not-allowed; + } + + .mat-mdc-tab-body-wrapper { + min-height: 320px; + } + + .mat-mdc-tab-body, + .mat-mdc-tab-body-content { + overflow: visible !important; + } + } +} + +.csa-tab__icon { + margin-right: 8px; + font-size: 13px; +} + +.csa-tab-body { + padding: 24px; + + @media (width <= 768px) { + padding: 16px; + } +} + +.csa-actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f1f5f9; + + &__spacer { + flex: 1; + } + + button { + height: 40px !important; + border-radius: 10px !important; + font-weight: 600 !important; + padding: 0 18px !important; + } +} + +.m-l-10 { + margin-left: 8px; +} + +.m-r-10 { + margin-right: 8px; +} diff --git a/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.ts b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.ts new file mode 100644 index 000000000..00e9e1c85 --- /dev/null +++ b/src/app/savings/edit-savings-account-features/edit-savings-account-features.component.ts @@ -0,0 +1,399 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, inject, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormsModule, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTabsModule } from '@angular/material/tabs'; +import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component'; +import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component'; +import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component'; +import { SavingsAccountPreviewStepComponent } from '../savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component'; + +/** Custom Services */ +import { SavingsService } from '../savings.service'; +import { ClientsService } from 'app/clients/clients.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Default state for every Digital & Account Feature toggle, mirrors + * savings-account-digital-step's DEFAULT_FACILITY_CONFIG. */ +const DEFAULT_FACILITY_CONFIG: Record = { + atmFacility: false, + atmWithdrawalFacility: false, + upiFacility: false, + posTransactionsFacility: false, + chequeBook: false, + impsFacility: false, + smsAlerts: false, + bbpsFacility: false, + insuranceCover: false, + bepgFacility: false, + neftFacility: false, + rtgsFacility: false, + interBankTotalCapFacility: false, + withdrawalFacility: false, + depositFacilityKyc: false, + virtualDebitCard: false +}; + +/** Toggle metadata used to render the feature grid — key must match a + * property of DEFAULT_FACILITY_CONFIG / the backend's accountFacilityConfig map. */ +const FACILITY_TOGGLES: { key: string; label: string; icon: string }[] = [ + { key: 'atmFacility', label: 'ATM Facility', icon: 'fa-credit-card' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal', icon: 'fa-wallet' }, + { key: 'upiFacility', label: 'UPI Facility', icon: 'fa-mobile-alt' }, + { key: 'posTransactionsFacility', label: 'POS Transactions', icon: 'fa-exchange-alt' }, + { key: 'chequeBook', label: 'Cheque Book', icon: 'fa-book' }, + { key: 'impsFacility', label: 'IMPS Facility', icon: 'fa-bolt' }, + { key: 'smsAlerts', label: 'SMS Alerts', icon: 'fa-sms' }, + { key: 'bbpsFacility', label: 'BBPS Facility', icon: 'fa-bell' }, + { key: 'insuranceCover', label: 'Insurance Cover', icon: 'fa-shield-alt' }, + { key: 'bepgFacility', label: 'BEPG Facility', icon: 'fa-plug' }, + { key: 'neftFacility', label: 'NEFT Facility', icon: 'fa-money-check-alt' }, + { key: 'rtgsFacility', label: 'RTGS Facility', icon: 'fa-money-check' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap', icon: 'fa-balance-scale' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility', icon: 'fa-arrow-right-arrow-left' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)', icon: 'fa-id-card' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card', icon: 'fa-credit-card' } +]; + +/** + * Edit Nominee & Features — maker-checker submission screen. + * + * A maker proposes changes to a savings account's Digital & Account Features toggles and/or its + * nominee here; nothing is applied immediately. The request is staged and a different user (checker) + * must approve it from the "Pending Savings Account Edits" screen before it takes effect. + */ +@Component({ + selector: 'mifosx-edit-savings-account-features', + templateUrl: './edit-savings-account-features.component.html', + styleUrls: ['./edit-savings-account-features.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + FaIconComponent, + MatIcon, + MatProgressSpinner, + MatTabsModule, + SavingsAccountDetailsStepComponent, + SavingsAccountCurrencyStepComponent, + SavingsAccountDigitalStepComponent, + SavingsAccountPreviewStepComponent + ] +}) +export class EditSavingsAccountFeaturesComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private savingsService = inject(SavingsService); + private clientsService = inject(ClientsService); + private settingsService = inject(SettingsService); + private dialog = inject(MatDialog); + + @ViewChild(SavingsAccountDetailsStepComponent) savingsAccountDetailsStep: SavingsAccountDetailsStepComponent; + @ViewChild(SavingsAccountCurrencyStepComponent) savingsAccountCurrencyStep: SavingsAccountCurrencyStepComponent; + @ViewChild(SavingsAccountDigitalStepComponent) savingsAccountDigitalStep: SavingsAccountDigitalStepComponent; + + originalNominees: any[] = []; + + get savingsAccountTermsForm(): FormGroup { + return new FormGroup({}); + } + + get savingsAccount() { + return this.savingsAccountDetailsStep ? { ...this.savingsAccountDetailsStep.savingsAccountDetails } : null; + } + + get savingsAccountDigital() { + return this.savingsAccountDigitalStep?.savingsAccountDigital ?? null; + } + + readonly toggles = FACILITY_TOGGLES; + + accountId: string; + savingsAccountAndTemplate: any; + clientId = ''; + accountNo = ''; + + savingsAccountProductTemplate: any = null; + selectedTabIndex = 0; + readonly totalTabs = 4; + + originalFacilityConfig: Record = { ...DEFAULT_FACILITY_CONFIG }; + + /** Current nominees on the account, fetched read-only for reference. */ + nominees: any[] = []; + nomineesLoading = false; + relationshipOptions: any[] = []; + + /** Pending edit already submitted for this account, if any. */ + pendingEdit: any = null; + pendingEditLoading = true; + + submitting = false; + + constructor() { + this.accountId = this.route.snapshot.params['savingAccountId']; + this.loadPendingEdit(); + this.route.data.subscribe((data: { savingsAccountAndTemplate: any }) => { + const account = data.savingsAccountAndTemplate; + this.savingsAccountAndTemplate = account; + this.clientId = account?.clientId != null ? String(account.clientId) : ''; + this.accountNo = account?.accountNo || ''; + this.originalFacilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...(account?.accountFacilityConfig || {}) }; + if (this.clientId) { + this.loadNominees(); + this.loadRelationshipTemplate(); + } + }); + } + + private todayIso(): string { + const d = new Date(); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + } + + private loadPendingEdit(): void { + this.pendingEditLoading = true; + this.savingsService.getPendingSavingsAccountEdit(this.accountId).subscribe({ + next: (edit: any) => { + this.pendingEdit = edit && edit.id ? edit : null; + this.pendingEditLoading = false; + }, + error: () => { + this.pendingEdit = null; + this.pendingEditLoading = false; + } + }); + } + + private loadNominees(): void { + this.nomineesLoading = true; + this.clientsService.getClientNominees(this.clientId, this.accountNo, 'SB').subscribe({ + next: (data: any) => { + this.nominees = Array.isArray(data) ? data : []; + this.originalNominees = JSON.parse(JSON.stringify(this.nominees)); + this.nomineesLoading = false; + }, + error: () => { + this.nominees = []; + this.originalNominees = []; + this.nomineesLoading = false; + } + }); + } + + private loadRelationshipTemplate(): void { + this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({ + next: (template: any) => { + this.relationshipOptions = template?.relationshipOptions || []; + }, + error: () => { + this.relationshipOptions = []; + } + }); + } + + /** Max share allowed for the mode currently being edited — excludes the nominee's own + * existing share from the running total when updating that same nominee. */ + get hasPendingEdit(): boolean { + return !!this.pendingEdit && this.pendingEdit.status === 'PENDING'; + } + + get facilityConfig(): Record { + return this.savingsAccountDigitalStep?.facilityConfig ?? this.originalFacilityConfig; + } + + get facilityChanged(): boolean { + const config = this.facilityConfig; + return this.toggles.some((t) => config[t.key] !== this.originalFacilityConfig[t.key]); + } + + get changedToggles(): any[] { + const list: any[] = []; + const config = this.facilityConfig; + this.toggles.forEach((t) => { + if (config[t.key] !== this.originalFacilityConfig[t.key]) { + list.push({ + label: t.label, + from: this.originalFacilityConfig[t.key], + to: config[t.key] + }); + } + }); + return list; + } + + get nomineeChanged(): boolean { + return this.buildNomineePayload() !== null; + } + + get canSubmit(): boolean { + return ( + !this.hasPendingEdit && + !this.pendingEditLoading && + !this.submitting && + (this.facilityChanged || this.nomineeChanged) + ); + } + + setTemplate($event: any): void { + this.savingsAccountProductTemplate = $event; + } + + next(): void { + if (this.selectedTabIndex < this.totalTabs - 1) { + this.selectedTabIndex += 1; + } + } + + prev(): void { + if (this.selectedTabIndex > 0) { + this.selectedTabIndex -= 1; + } + } + + getRelationshipName(relationshipId: any): string { + const opt = this.relationshipOptions.find((r: any) => r.id === relationshipId); + return opt?.name || ''; + } + + private buildFacilityDiff(): Record | null { + const diff: Record = {}; + let changed = false; + const config = this.facilityConfig; + this.toggles.forEach((t) => { + if (config[t.key] !== this.originalFacilityConfig[t.key]) { + diff[t.key] = config[t.key]; + changed = true; + } + }); + return changed ? diff : null; + } + + private buildNomineePayload(): any | null { + const original = this.originalNominees || []; + const current = this.savingsAccountCurrencyStep?.nominees ?? []; + + // Find if a nominee has been added + const added = current.find( + (curr: any) => !original.some((orig: any) => orig.id === curr.id || orig.nomineeClientId === curr.nomineeClientId) + ); + if (added) { + return { + operation: 'ADD', + nomineeId: null, + nomineeClientId: added.nomineeClientId, + relationshipId: added.relationshipId, + percentageShare: added.percentageShare, + isActive: added.isActive !== undefined ? !!added.isActive : true, + nomineeIsMinor: !!added.nomineeIsMinor, + guardianClientId: added.nomineeIsMinor ? added.guardianClientId : null, + guardianRelationshipId: added.nomineeIsMinor ? added.guardianRelationshipId : null, + effectiveDate: this.todayIso(), + remarks: added.remarks || null + }; + } + + // Find if a nominee has been deleted + const deleted = original.find( + (orig: any) => !current.some((curr: any) => curr.id === orig.id || curr.nomineeClientId === orig.nomineeClientId) + ); + if (deleted) { + return { + operation: 'DELETE', + nomineeId: deleted.id + }; + } + + // Find if any nominee has been updated + for (const curr of current) { + const orig = original.find((o: any) => o.id === curr.id || o.nomineeClientId === curr.nomineeClientId); + if (orig) { + const relChanged = orig.relationshipId !== curr.relationshipId; + const shareChanged = +orig.percentageShare !== +curr.percentageShare; + const minorChanged = !!orig.nomineeIsMinor !== !!curr.nomineeIsMinor; + const guardianChanged = + orig.guardianClientId !== curr.guardianClientId || + orig.guardianRelationshipId !== curr.guardianRelationshipId; + const activeChanged = !!orig.isActive !== (curr.isActive !== undefined ? !!curr.isActive : true); + const remarksChanged = (orig.remarks || '') !== (curr.remarks || ''); + + if (relChanged || shareChanged || minorChanged || guardianChanged || activeChanged || remarksChanged) { + return { + operation: 'UPDATE', + nomineeId: orig.id, + relationshipId: curr.relationshipId, + percentageShare: curr.percentageShare, + isActive: curr.isActive !== undefined ? !!curr.isActive : true, + nomineeIsMinor: !!curr.nomineeIsMinor, + guardianClientId: curr.nomineeIsMinor ? curr.guardianClientId : null, + guardianRelationshipId: curr.nomineeIsMinor ? curr.guardianRelationshipId : null, + effectiveDate: this.todayIso(), + remarks: curr.remarks || null + }; + } + } + } + + return null; + } + + submit(): void { + if (!this.canSubmit) return; + + const locale = this.settingsService.language.code; + const payload: any = { locale, dateFormat: 'yyyy-MM-dd' }; + + const facilityDiff = this.buildFacilityDiff(); + if (facilityDiff) { + payload.accountFacilityConfig = facilityDiff; + } + + const nomineePayload = this.buildNomineePayload(); + if (nomineePayload) { + payload.nominee = nomineePayload; + } + + this.submitting = true; + this.savingsService.submitSavingsAccountEdit(this.accountId, payload).subscribe({ + next: () => { + this.submitting = false; + this.showSuccess(); + }, + error: () => { + this.submitting = false; + } + }); + } + + private showSuccess(): void { + const ref = this.dialog.open(SuccessDialogComponent, { + width: '420px', + maxWidth: '92vw', + autoFocus: false, + data: { + title: 'Submitted for Approval', + message: 'Your changes have been submitted and are awaiting checker approval.', + buttonLabel: 'Done' + } + }); + ref.afterClosed().subscribe(() => this.cancel()); + } + + cancel(): void { + this.router.navigate(['../'], { relativeTo: this.route }); + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index ea68c5170..1f8ce8870 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -71,7 +71,7 @@
Account Type - + @for (opt of accountTypeOptions; track opt) { {{ opt }} } @@ -80,7 +80,11 @@ Customer Segment - + @for (opt of customerSegmentOptions; track opt) { {{ opt }} } @@ -117,7 +121,7 @@ Share Linking Enabled - + Yes No @@ -132,6 +136,7 @@ introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null " (introducerChange)="onIntroducerChange($event)" + [disabled]="readOnly" > } } @@ -223,7 +228,7 @@ color="primary" type="button" class="soa-add-btn" - [disabled]="totalJointSharePercentage >= 100" + [disabled]="totalJointSharePercentage >= 100 || readOnly" (click)="openAddJointHolderDialog()" > person_add Add Holder @@ -269,6 +274,7 @@ max="100" [(ngModel)]="primaryHolderShare" [ngModelOptions]="{ standalone: true }" + [disabled]="readOnly" placeholder="0" (input)="onPrimaryShareInput($event)" (blur)="onPrimaryShareBlur($event)" @@ -320,7 +326,8 @@ class="soa-sign-toggle" [class.soa-sign-toggle--mandatory]="h.isMandatory" [class.soa-sign-toggle--optional]="!h.isMandatory" - (click)="toggleJointHolderMandatory(h)" + [disabled]="readOnly" + (click)="!readOnly && toggleJointHolderMandatory(h)" > {{ h.isMandatory ? 'lock' : 'lock_open' }} {{ h.isMandatory ? 'Mandatory' : 'Optional' }} @@ -328,7 +335,13 @@ Co-Holder - @@ -410,7 +423,8 @@ type="button" class="soa-mode-card" [class.soa-mode-card--selected]="modeOfOperation === opt.value" - (click)="modeOfOperation = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (modeOfOperation = opt.value)" > {{ opt.icon }}
{{ opt.label }}
@@ -465,8 +479,9 @@ [max]="maxDate" [(ngModel)]="minorDob" [ngModelOptions]="{ standalone: true }" + [disabled]="readOnly" /> - + @if (clientDob) { Auto-filled from client profile @@ -487,7 +502,11 @@ Guardian Relationship - + @for (rel of guardianRelationshipOptions; track rel) { {{ rel }} } @@ -503,6 +522,7 @@ @if (guardianName) { @@ -630,7 +650,8 @@ class="soa-cat-tile" [class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value" [class.soa-cat-tile--male]="true" - (click)="selectedSpecialCategory = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (selectedSpecialCategory = opt.value)" > {{ opt.icon }} {{ opt.label }} @@ -680,7 +701,8 @@ class="soa-cat-tile" [class.soa-cat-tile--selected]="selectedWeakerSection === opt.value" [class.soa-cat-tile--female]="true" - (click)="selectedWeakerSection = opt.value" + [disabled]="readOnly" + (click)="!readOnly && (selectedWeakerSection = opt.value)" > {{ opt.icon }} {{ opt.label }} diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index 1d6f93357..cb93fd19e 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -53,6 +53,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit { /** Savings Account Template */ @Input() savingsAccountTemplate: any; + @Input() readOnly = false; minDate = new Date(2000, 0, 1); maxDate = new Date(); @@ -219,6 +220,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit { this.cdr.detectChanges(); }); } + if (this.readOnly) { + this.savingsAccountDetailsForm.disable(); + } } /** @@ -277,6 +281,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit { } isTileDisabled(value: string): boolean { + if (this.readOnly) return true; if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; return false; diff --git a/src/app/savings/savings-account-view/savings-account-view.component.ts b/src/app/savings/savings-account-view/savings-account-view.component.ts index ab5de3bf3..59501a2bd 100644 --- a/src/app/savings/savings-account-view/savings-account-view.component.ts +++ b/src/app/savings/savings-account-view/savings-account-view.component.ts @@ -122,6 +122,7 @@ export class SavingsAccountViewComponent implements OnInit { 'Add Charge': 'admin', 'Apply Annual Fees': 'admin', 'Modify Application': 'admin', + 'Edit Nominee & Features': 'admin', 'Assign Staff': 'admin', 'Unassign Staff': 'admin', Approve: 'admin', @@ -156,6 +157,7 @@ export class SavingsAccountViewComponent implements OnInit { 'Add Charge': 'plus-circle', 'Apply Annual Fees': 'receipt', 'Modify Application': 'edit', + 'Edit Nominee & Features': 'edit', 'Assign Staff': 'user-plus', 'Unassign Staff': 'user-slash', Approve: 'check-circle', @@ -311,6 +313,9 @@ export class SavingsAccountViewComponent implements OnInit { case 'Modify Application': this.router.navigate(['edit'], { relativeTo: this.route }); break; + case 'Edit Nominee & Features': + this.router.navigate(['edit-nominee-features'], { relativeTo: this.route }); + break; case 'Delete': this.deleteSavingsAccount(); break; diff --git a/src/app/savings/savings-account-view/savings-buttons.config.ts b/src/app/savings/savings-account-view/savings-buttons.config.ts index 47198897f..930167fb5 100644 --- a/src/app/savings/savings-account-view/savings-buttons.config.ts +++ b/src/app/savings/savings-account-view/savings-buttons.config.ts @@ -161,6 +161,10 @@ export class SavingsButtonsConfiguration { switch (status) { case 'Active': this.optionArray = [ + { + name: 'Edit Nominee & Features', + taskPermissionName: 'SUBMITEDIT_SAVINGSACCOUNT' + }, { name: 'Post Interest', taskPermissionName: 'POSTINTEREST_SAVINGSACCOUNT' diff --git a/src/app/savings/savings-routing.module.ts b/src/app/savings/savings-routing.module.ts index a5e3a3856..289f14d1f 100644 --- a/src/app/savings/savings-routing.module.ts +++ b/src/app/savings/savings-routing.module.ts @@ -27,6 +27,7 @@ import { ExportTransactionsComponent } from './savings-account-view/transactions import { EditTransactionComponent } from './savings-account-view/transactions/edit-transaction/edit-transaction.component'; import { CreateGsimAccountComponent } from './gsim-account/create-gsim-account/create-gsim-account.component'; import { GsimAccountComponent } from './gsim-account/gsim-account.component'; +import { EditSavingsAccountFeaturesComponent } from './edit-savings-account-features/edit-savings-account-features.component'; /** Custom Resolvers */ import { SavingsAccountViewResolver } from './common-resolvers/savings-account-view.resolver'; @@ -171,6 +172,18 @@ const routes: Routes = [ savingsAccountAndTemplate: SavingsAccountAndTemplateResolver } }, + { + path: ':savingAccountId/edit-nominee-features', + data: { + title: 'Edit Nominee & Features', + breadcrumb: 'Edit Nominee & Features', + routeParamBreadcrumb: false + }, + component: EditSavingsAccountFeaturesComponent, + resolve: { + savingsAccountAndTemplate: SavingsAccountAndTemplateResolver + } + }, { path: ':savingAccountId/transactions/:id', data: { title: 'Savings Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false }, diff --git a/src/app/savings/savings.module.ts b/src/app/savings/savings.module.ts index fd9480340..fbd78b468 100644 --- a/src/app/savings/savings.module.ts +++ b/src/app/savings/savings.module.ts @@ -63,6 +63,7 @@ import { SavingsTransactionDatatableTabComponent } from './savings-account-view/ import { GeneralTabComponent } from './savings-account-view/general-tab/general-tab.component'; import { SavingsActiveClientMembersComponent } from './savings-account-stepper/savings-active-client-members/savings-active-client-members.component'; import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbook-tab.component'; +import { EditSavingsAccountFeaturesComponent } from './edit-savings-account-features/edit-savings-account-features.component'; /** * Savings Module @@ -121,7 +122,8 @@ import { PassbookTabComponent } from './savings-account-view/passbook-tab/passbo SavingsTransactionDatatableTabComponent, GeneralTabComponent, SavingsActiveClientMembersComponent, - PassbookTabComponent + PassbookTabComponent, + EditSavingsAccountFeaturesComponent ], providers: [] }) diff --git a/src/app/savings/savings.service.ts b/src/app/savings/savings.service.ts index 52480fc4c..26388a148 100644 --- a/src/app/savings/savings.service.ts +++ b/src/app/savings/savings.service.ts @@ -381,4 +381,49 @@ export class SavingsService { loadSavingsDocument(savingAccountId: any, data: any): Observable { return this.http.post(`/savings/${savingAccountId}/documents`, data); } + + /** + * Submits a maker-checker edit request (Nominee and/or Digital & Account Features change) for a + * savings account. The edit is held for checker approval before it is actually applied. + * @param {string} accountId Savings Account Id + * @param {any} data Edit payload — `accountFacilityConfig` and/or `nominee`, plus locale/dateFormat + * @returns {Observable} + */ + submitSavingsAccountEdit(accountId: string, data: any): Observable { + return this.http.post(`/savingsaccounts/${accountId}/edit`, data); + } + + /** + * @param {string} accountId Savings Account Id + * @returns {Observable} the pending edit for this account, if any. + */ + getPendingSavingsAccountEdit(accountId: string): Observable { + return this.http.get(`/savingsaccounts/${accountId}/edit/pending`); + } + + /** + * @returns {Observable} all savings account edits currently awaiting checker approval. + */ + getAllPendingSavingsAccountEdits(): Observable { + return this.http.get(`/savingsaccounts/edit/pending`); + } + + /** + * Approves a pending savings account edit, releasing any hold and applying the proposed changes. + * @param {string} accountId Savings Account Id + * @returns {Observable} + */ + approveSavingsAccountEdit(accountId: string): Observable { + return this.http.post(`/savingsaccounts/${accountId}/edit/approve`, {}); + } + + /** + * Rejects a pending savings account edit, releasing any hold without applying the changes. + * @param {string} accountId Savings Account Id + * @param {string} rejectReason Optional reason for rejection + * @returns {Observable} + */ + rejectSavingsAccountEdit(accountId: string, rejectReason?: string): Observable { + return this.http.post(`/savingsaccounts/${accountId}/edit/reject`, { rejectReason: rejectReason || '' }); + } } diff --git a/src/app/shared/account-nominees-tab/account-nominees-tab.component.html b/src/app/shared/account-nominees-tab/account-nominees-tab.component.html index a36df6205..779a9c8e3 100644 --- a/src/app/shared/account-nominees-tab/account-nominees-tab.component.html +++ b/src/app/shared/account-nominees-tab/account-nominees-tab.component.html @@ -14,13 +14,37 @@

Manage nominees for this account. Nominees are account-specific.

- @if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm) { + @if (!loading && !nomineesLoading && totalShare < 100 && !showAddForm && !hasPendingEdit) { }
+ @if (pendingEditLoading) { +
+ + Checking for pending edits... +
+ } + + @if (!pendingEditLoading && hasPendingEdit) { +
+ hourglass_top +
+ An edit is already pending approval for this account. + + Submitted by {{ pendingEdit.makerUsername || 'a maker' }} on {{ pendingEdit.createdOn | dateFormat }}. Status: + {{ pendingEdit.status }} + @if (pendingEdit.holdAmount) { + · Hold placed: {{ pendingEdit.holdAmount }} + } + + You cannot submit another edit until this one is approved or rejected. +
+
+ } + @if (loading) {
@@ -98,18 +122,20 @@ - - + @if (!hasPendingEdit) { + + + }
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) { diff --git a/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss b/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss index c25ab0e21..8f3f29a3d 100644 --- a/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss +++ b/src/app/shared/account-nominees-tab/account-nominees-tab.component.scss @@ -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; + } +} diff --git a/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts b/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts index afbfccc59..21e84b4b4 100644 --- a/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts +++ b/src/app/shared/account-nominees-tab/account-nominees-tab.component.ts @@ -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: () => {} }); } }); diff --git a/src/app/shared/introducer-search/introducer-search.component.html b/src/app/shared/introducer-search/introducer-search.component.html index e93a3a5ae..46a4e815a 100644 --- a/src/app/shared/introducer-search/introducer-search.component.html +++ b/src/app/shared/introducer-search/introducer-search.component.html @@ -19,7 +19,13 @@
Search by Name or Client ID - + search @@ -86,9 +92,11 @@ }
- + @if (!disabled) { + + } @if (fetchingLinkedData) { diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts index 467502a06..1d4287d16 100644 --- a/src/app/shared/introducer-search/introducer-search.component.ts +++ b/src/app/shared/introducer-search/introducer-search.component.ts @@ -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'; diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts index 54da1de31..8f7ec6d2c 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts @@ -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[] { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html new file mode 100644 index 000000000..d9aaafb86 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html @@ -0,0 +1,157 @@ + + +
+ @if (showData) { +
+ + search + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account No.{{ edit.accountNo }}Account Type + + {{ edit.accountType }} + + Submitted By{{ edit.makerUsername }}Submitted On{{ formatDate(edit.createdOn) }}Hold Amount + @if (edit.holdAmount) { + {{ edit.holdAmount }} + } @else { + — + } + Status + {{ edit.status }} + Actions + + + +
+
+ + @if (selectedEdit) { +
+
+

Proposed Changes — Account "{{ selectedEdit.accountNo }}"

+ +
+ + @if (selectedEdit.facilityConfigSubmitted) { +
+

Digital & Account Features

+ + + + + + + + + @for (row of entries(selectedEdit.proposedFacilityConfig); track row.key) { + + + + + } + +
FeatureProposed Value
{{ row.key }}{{ row.value }}
+
+ } + + @if (selectedEdit.nomineeSubmitted) { +
+

Nominee Change

+ + + + + + + + + @for (row of entries(selectedEdit.proposedNominee); track row.key) { + + + + + } + +
FieldProposed Value
{{ row.key }}{{ row.value }}
+
+ } + + @if (!selectedEdit.facilityConfigSubmitted && !selectedEdit.nomineeSubmitted) { +

No proposed changes recorded on this edit.

+ } + + @if (selectedEdit.rejectReason) { +

Reject Reason: {{ selectedEdit.rejectReason }}

+ } +
+ } + } + + @if (!showData) { +
+ +

No pending account edits

+

There are no Nominee / Digital-Feature edits awaiting approval at this time.

+
+ } +
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss new file mode 100644 index 000000000..81f4fff23 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss @@ -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; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts new file mode 100644 index 000000000..78bd3c1fa --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts @@ -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; + 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])); + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html index 1b6c575b1..59d9419f1 100644 --- a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html @@ -106,6 +106,18 @@ Pending Product Edits + + + Pending Account Edits + { + private currentAccountsService = inject(CurrentAccountsService); + + resolve(): Observable { + return this.currentAccountsService.getAllPendingCurrentAccountEdits(); + } +} diff --git a/src/app/tasks/common-resolvers/getPendingSavingsAccountEdits.resolver.ts b/src/app/tasks/common-resolvers/getPendingSavingsAccountEdits.resolver.ts new file mode 100644 index 000000000..3bdf0e6b6 --- /dev/null +++ b/src/app/tasks/common-resolvers/getPendingSavingsAccountEdits.resolver.ts @@ -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 { + private savingsService = inject(SavingsService); + + resolve(): Observable { + return this.savingsService.getAllPendingSavingsAccountEdits(); + } +} diff --git a/src/app/tasks/tasks-routing.module.ts b/src/app/tasks/tasks-routing.module.ts index 273019314..0a557006a 100644 --- a/src/app/tasks/tasks-routing.module.ts +++ b/src/app/tasks/tasks-routing.module.ts @@ -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 {} diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 194839b55..346448b2f 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260703', - 'hash': 'd00eac0a' + 'version': '260710', + 'hash': '96c4a28c' }, 'allow_switching_backend_instance': true };