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 b15fb3d56..9e8e1acd6 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 @@ -29,6 +29,7 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { @Input() currentAccountProductTemplate: any; @Input() primaryClientId: number | null = null; @Input() isNonIndividualClient = false; + @Input() existingAccount: any = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nominees: any[] = []; @@ -41,8 +42,29 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { msme = false; autoDebitFacility = false; + private existingAccountApplied = false; + ngOnChanges(changes: SimpleChanges): void { - if (changes['currentAccountProductTemplate']?.currentValue) { + if (changes['existingAccount']?.currentValue) { + const ea = changes['existingAccount'].currentValue; + this.existingAccountApplied = true; + if (ea.balanceSwapOption !== undefined) { + this.balanceSwapOption = !!ea.balanceSwapOption; + } + if (ea.msme !== undefined) { + this.msme = !!ea.msme; + } + if (ea.autoDebitFacility !== undefined) { + this.autoDebitFacility = !!ea.autoDebitFacility; + } + // nominees: try array first, then parse nomineeData JSON string + const nominees = + Array.isArray(ea.nominees) && ea.nominees.length > 0 ? ea.nominees : this.parseNomineeData(ea.nomineeData); + if (nominees.length > 0) { + this.nominees = [...nominees]; + } + } + if (changes['currentAccountProductTemplate']?.currentValue && !this.existingAccountApplied) { const pt = changes['currentAccountProductTemplate'].currentValue; this.balanceSwapOption = !!pt.balanceSwapOption; this.msme = !!pt.msme; @@ -50,6 +72,18 @@ export class CurrentAccountCurrencyStepComponent implements OnChanges { } } + private parseNomineeData(nomineeData: any): any[] { + if (!nomineeData) { + return []; + } + try { + const parsed = typeof nomineeData === 'string' ? JSON.parse(nomineeData) : nomineeData; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + get totalShare(): number { return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0); } 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 798a94250..435884bcb 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 @@ -124,7 +124,7 @@ Account No @@ -167,6 +167,9 @@ @if (productTemplate?.introducerRequired) { } 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 63994c088..6d0bc2920 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 @@ -65,6 +65,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { introducerId: number | null = null; introducerName: string | null = null; introducerCif: string | null = null; + private isEditMode = false; // ── Non-Individual (existing) ────────────────────────────────────────── isNonIndividualClient = false; @@ -201,11 +202,29 @@ export class CurrentAccountDetailsStepComponent implements OnInit { if (this.currentAccountTemplate) { this.productData = this.currentAccountTemplate.productOptions; - if (this.currentAccountTemplate.currentAccountProductId) { + const existingProductId = + this.currentAccountTemplate.currentAccountProductId ?? this.currentAccountTemplate.productId; + // Edit mode: account response always has accountNo; pure template has previewAccountNo only + if (this.currentAccountTemplate.accountNo) { + this.isEditMode = true; + } + if ((!this.productData || this.productData.length === 0) && existingProductId) { + this.isEditMode = true; + this.productData = [ + { + id: existingProductId, + name: + this.currentAccountTemplate.productName ?? + this.currentAccountTemplate.currentAccountProductName ?? + 'Product' + } + ]; + } + if (existingProductId) { this.currentAccountDetailsForm.patchValue({ - productId: this.currentAccountTemplate.currentAccountProductId, + productId: existingProductId, submittedOnDate: - this.currentAccountTemplate.timeline.submittedOnDate && + this.currentAccountTemplate.timeline?.submittedOnDate && new Date(this.currentAccountTemplate.timeline.submittedOnDate), externalId: this.currentAccountTemplate.externalId }); @@ -273,10 +292,106 @@ export class CurrentAccountDetailsStepComponent implements OnInit { } else { this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(''); } + // === Restore existing account values when in edit/modify mode === + this.restoreExistingAccountValues(); }); }); } + private restoreExistingAccountValues(): void { + const ea = this.currentAccountTemplate; + // Edit mode: API returns `productId`, create template uses `currentAccountProductId` + if (!ea?.currentAccountProductId && !ea?.productId) { + return; + } + + const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? '')); + + const accountStatus = strVal(ea.accountStatus); + if (accountStatus) { + this.selectedAccountStatus = accountStatus; + } + + const customerSegment = strVal(ea.customerSegment); + if (customerSegment) { + this.selectedCustomerSegment = customerSegment; + } + + // cbsAccountType is the CBS descriptive type ("Standard Account", etc.) + // ea.accountType is Fineract's enum-based type ({id,value:"Individual"}) — do NOT use as fallback + const accountType = ea.cbsAccountType ? strVal(ea.cbsAccountType) : null; + if (accountType) { + this.selectedAccountType = accountType; + } + + if (accountStatus === 'JOINT') { + const modeOfOp = strVal(ea.modeOfOperation); + if (modeOfOp) { + this.modeOfOperation = modeOfOp; + } + // jointHolders may not be returned by backend — skip if absent + if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) { + // Support both camelCase and lowercase keys (MySQL queryForList returns lowercase) + const isPH = (h: any) => h.isPrimary ?? h.isprimary ?? false; + const primary = ea.jointHolders.find((h: any) => isPH(h)); + const ps = primary?.sharePercentage ?? primary?.sharepercentage; + if (ps) { + this.primaryHolderShare = +ps; + } + this.jointHolders = ea.jointHolders + .filter((h: any) => !isPH(h)) + .map((h: any) => ({ + clientId: h.clientId ?? h.clientid, + clientName: h.displayName ?? h.displayname ?? h.clientName ?? '', + clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '', + sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0), + isMandatory: !!(h.isMandatory ?? h.ismandatory) + })); + } + } + if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) { + this.selectedSpecialCategory = ea.specialCategory; + } + if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) { + this.selectedWeakerSection = ea.weakerSection; + } + if (accountStatus === 'MINOR') { + if (ea.guardianClientId) { + this.guardianClientId = ea.guardianClientId; + } + if (ea.guardianName) { + this.guardianName = ea.guardianName; + } + if (ea.guardianCifNumber) { + this.guardianCif = ea.guardianCifNumber; + } + if (ea.guardianRelationship) { + this.guardianRelationship = ea.guardianRelationship; + } + if (ea.minorDateOfBirth) { + const d = new Date(ea.minorDateOfBirth); + if (!isNaN(d.getTime())) { + this.minorDob = d; + } + } + } + // API returns introducerClientId, not introducerId + const introId = ea.introducerId ?? ea.introducerClientId; + if (introId) { + this.introducerId = introId; + this.introducerName = ea.introducerName ?? null; + this.introducerCif = ea.introducerCifNumber ?? null; + } + this.currentAccountDetailsForm.patchValue({ + balanceSwapOption: !!ea.balanceSwapOption, + msme: !!ea.msme, + autoDebitFacility: !!ea.autoDebitFacility, + ...(ea.establishmentDate ? { establishmentDate: new Date(ea.establishmentDate) } : {}), + ...(ea.startOperationDate ? { startOperationDate: new Date(ea.startOperationDate) } : {}) + }); + this.cdr.detectChanges(); + } + private extractOptions(options: any[], fallback?: string): string[] { if (Array.isArray(options) && options.length > 0) { return options.map((o: any) => o.value ?? o.name ?? o); @@ -317,6 +432,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit { } private autoDetectAccountStatus(): void { + if (this.isEditMode) { + return; + } if (this.clientAge === null) return; if (this.clientAge < 18) { this.selectedAccountStatus = 'MINOR'; 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 321d9984d..49597f57a 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 @@ -40,10 +40,19 @@ const DEFAULT_FACILITY_CONFIG = { }) export class CurrentAccountDigitalStepComponent { facilityConfig = { ...DEFAULT_FACILITY_CONFIG }; + private hasExistingAccount = false; + + @Input() + set existingAccount(value: any) { + if (value?.accountFacilityConfig) { + this.hasExistingAccount = true; + this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig }; + } + } @Input() set currentAccountProductTemplate(value: any) { - if (value?.accountFacilityConfig) { + if (!this.hasExistingAccount && value?.accountFacilityConfig) { this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig }; } } 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 e72cc8c0b..9c33daef1 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 @@ -143,6 +143,16 @@ export class CurrentAccountButtonsConfiguration { } ]; break; + case 'Rejected': + case 'rejected': + this.buttonsArray = [ + { + name: 'Modify Application', + icon: 'pen', + taskPermissionName: 'UPDATE_CURRENTACCOUNT' + } + ]; + break; default: this.buttonsArray = []; } diff --git a/src/app/current-accounts/edit-current-account/edit-current-account.component.html b/src/app/current-accounts/edit-current-account/edit-current-account.component.html index a23533b44..101e37353 100644 --- a/src/app/current-accounts/edit-current-account/edit-current-account.component.html +++ b/src/app/current-accounts/edit-current-account/edit-current-account.component.html @@ -6,76 +6,146 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- + +
+ +
+

Modify Current Account Application

+
+ Review and update the account details, currency settings and digital channels before resubmitting. +
+
+
+ + + - - - - - - - - - - - - - - - - - - - - - - {{ 'labels.inputs.DETAILS' | translate }} - - - - - - - {{ 'labels.inputs.TERMS' | translate }} - - - - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - @if (currentAccountFormValidAndNotPristine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - + + + + Product + +
+ - - - } - + + +
+ + +
+
+
+ + + + + + Currency + +
+ @if (currentAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + Digital + +
+ @if (currentAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ @if (currentAccountProductTemplate) { + + + } + +
+ + + + +
+
+
+
diff --git a/src/app/current-accounts/edit-current-account/edit-current-account.component.scss b/src/app/current-accounts/edit-current-account/edit-current-account.component.scss index af9f4c075..c22bb92c9 100644 --- a/src/app/current-accounts/edit-current-account/edit-current-account.component.scss +++ b/src/app/current-accounts/edit-current-account/edit-current-account.component.scss @@ -5,3 +5,164 @@ * 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/. */ + +$navy: #0d1b2a; +$blue: #1e88e5; +$blue-d: #1e40af; +$indigo: #4f46e5; +$border: #e2e8f0; + +.cca-container { + max-width: 1180px; + margin: 0 auto; + padding: 20px 24px; +} + +/* ─── Hero header (matches Nexora pages) ────────────────────────────── */ +.cca-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; + } +} + +/* ─── Wizard tab strip ──────────────────────────────────────────────── */ +.cca-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.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; + } + } +} + +.cca-tab__icon { + margin-right: 8px; + font-size: 13px; +} + +/* ─── Tab body + action bar ─────────────────────────────────────────── */ +.cca-tab-body { + padding: 24px; + + @media (width <= 768px) { + padding: 16px; + } +} + +.cca-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; +} + +button.preview-submit-row__primary, +.mat-mdc-raised-button.mat-primary.preview-submit-row__primary, +.preview-submit-row__primary { + background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important; + + --mdc-protected-button-container-color: transparent !important; + --mdc-filled-button-container-color: transparent !important; + + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; +} diff --git a/src/app/current-accounts/edit-current-account/edit-current-account.component.ts b/src/app/current-accounts/edit-current-account/edit-current-account.component.ts index 836824166..60a9561c6 100644 --- a/src/app/current-accounts/edit-current-account/edit-current-account.component.ts +++ b/src/app/current-accounts/edit-current-account/edit-current-account.component.ts @@ -9,23 +9,26 @@ /** Angular Imports */ import { Component, ViewChild, inject } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { ClientsService } from 'app/clients/clients.service'; /** Custom Components */ import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component'; -import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component'; -import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-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'; /** Custom Services */ import { CurrentAccountsService } from '../current-accounts.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { AlertService } from 'app/core/alert/alert.service'; +import { MatTabsModule } from '@angular/material/tabs'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** - * Edit Current Account Component + * Edit Current Account Component — same tab UI as Create, pre-populated with existing data. */ @Component({ selector: 'mifosx-edit-current-account-account', @@ -33,14 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-current-account.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, + MatTabsModule, FaIconComponent, - MatStep, - MatStepLabel, CurrentAccountDetailsStepComponent, - CurrentAccountTermsStepComponent, - CurrentAccountChargesStepComponent, + CurrentAccountCurrencyStepComponent, + CurrentAccountDigitalStepComponent, CurrentAccountPreviewStepComponent ] }) @@ -50,109 +50,172 @@ export class EditCurrentAccountComponent { private dateUtils = inject(Dates); private currentAccountsService = inject(CurrentAccountsService); private settingsService = inject(SettingsService); + private alertService = inject(AlertService); + private clientsService = inject(ClientsService); - /** Current Account Template */ + /** Full account data + template from resolver */ currentAccountAndTemplate: any; - /** Current Account Product Template */ + /** Product template emitted by details step */ currentAccountProductTemplate: any; + /** Currently visible wizard tab (0-indexed). */ + selectedTabIndex = 0; + private readonly totalTabs = 4; - /** Current Account Details Step */ @ViewChild(CurrentAccountDetailsStepComponent, { static: true }) currentAccountDetailsStep: CurrentAccountDetailsStepComponent; - /** Current Account Terms Step */ - @ViewChild(CurrentAccountTermsStepComponent, { static: true }) - currentAccountTermsStep: CurrentAccountTermsStepComponent; - /** Current Account Charges Step */ - @ViewChild(CurrentAccountChargesStepComponent, { static: true }) - currentAccountChargesStep: CurrentAccountChargesStepComponent; - /** - * Fetches current account template from `resolve` - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {Dates} dateUtils Date Utils - * @param {CurrentAccountsService} currentAccountsService Current Accounts Service - * @param {SettingsService} settingsService Settings Service - */ + @ViewChild(CurrentAccountCurrencyStepComponent) + currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent; + + @ViewChild(CurrentAccountDigitalStepComponent) + currentAccountDigitalStep: CurrentAccountDigitalStepComponent; + constructor() { this.route.data.subscribe((data: { currentAccountAndTemplate: any }) => { - this.currentAccountAndTemplate = data.currentAccountAndTemplate; + const account = data.currentAccountAndTemplate; + this.currentAccountAndTemplate = account; + if (account?.clientId && account?.accountNo) { + this.clientsService + .getClientNominees(String(account.clientId), account.accountNo, 'CB') + .subscribe((nominees: any[]) => { + if (Array.isArray(nominees) && nominees.length > 0) { + this.currentAccountAndTemplate = { ...this.currentAccountAndTemplate, nominees }; + } + }); + } }); } - /** - * Sets current account product template. - * @param {any} $event API response - */ setTemplate($event: any) { this.currentAccountProductTemplate = $event; } - /** - * Retrieves current account details form. - */ + next(): void { + if (this.selectedTabIndex < this.totalTabs - 1) { + this.selectedTabIndex += 1; + } + } + + prev(): void { + if (this.selectedTabIndex > 0) { + this.selectedTabIndex -= 1; + } + } + get currentAccountDetailsForm() { return this.currentAccountDetailsStep.currentAccountDetailsForm; } - /** - * Retrieves current account terms form. - */ - get currentAccountTermsForm() { - return this.currentAccountTermsStep.currentAccountTermsForm; + get currentAccountTermForm(): FormGroup { + return new FormGroup({}); } - /** - * Checks validity and pristinity of overall current account form . - */ - get currentAccountFormValidAndNotPristine() { - return ( - this.currentAccountDetailsForm.valid && - this.currentAccountTermsForm.valid && - (!this.currentAccountDetailsForm.pristine || - !this.currentAccountTermsForm.pristine || - !this.currentAccountChargesStep.pristine) - ); + get currentAccountSettingsForm(): FormGroup { + return new FormGroup({}); + } + + get currentAccountTermsForm(): FormGroup { + return new FormGroup({}); } - /** - * Retrieves current account object. - */ get currentAccount() { + return { ...this.currentAccountDetailsStep.currentAccountDetails }; + } + + get currentAccountDigital() { + return this.currentAccountDigitalStep?.currentAccountDigital ?? null; + } + + get currentAccountCurrencyFlags() { return { - ...this.currentAccountDetailsStep.currentAccountDetails, - ...this.currentAccountTermsStep.currentAccountTerms, - ...this.currentAccountChargesStep.currentAccountCharges + balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false, + msme: this.currentAccountCurrencyStep?.msme ?? false, + autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false }; } - /** - * Creates a new share account. - */ submit() { - // TODO: Update once language and date settings are setup + const detailsStep = this.currentAccountDetailsStep; + if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } + if ( + !this.currentAccountDetailsStep.isNonIndividualClient && + this.currentAccountProductTemplate?.nominationFacility && + (this.currentAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (this.currentAccountProductTemplate?.introducerRequired && !this.currentAccountDetailsStep.introducerId) { + this.alertService.alert({ + type: 'Error', + message: 'Introducer is required for this product. Please select an introducer before submitting.' + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const currentAccount = { - ...this.currentAccount, - charges: this.currentAccount.charges.map((charge: any) => ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: this.dateUtils.formatDate(charge.dueDate, dateFormat), - feeOnMonthDay: this.dateUtils.formatDate(charge.feeOnMonthDay, dateFormat), - feeInterval: charge.feeInterval - })), - submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat), + const details = this.currentAccount; + + const currentAccount: any = { + ...details, + ...this.currentAccountCurrencyStep.currencyStepData, + ...(this.currentAccountDigitalStep?.currentAccountDigital ?? {}), + charges: [], + submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat), dateFormat, monthDayFormat, locale }; + + if (details.establishmentDate) { + currentAccount.establishmentDate = this.dateUtils.formatDate(details.establishmentDate, dateFormat); + } + if (details.startOperationDate) { + currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat); + } + + if (this.currentAccountDetailsStep.isNonIndividualClient) { + currentAccount.isNonIndividualClient = true; + currentAccount.partners = this.currentAccountDetailsStep.relatedClients.map((c: any) => ({ + clientId: c.clientId, + clientName: c.clientName, + clientCif: c.clientCif, + sharePercentage: c.sharePercentage, + isMandatory: c.isMandatory + })); + } else { + delete currentAccount.isNonIndividualClient; + } + if (this.currentAccountAndTemplate.clientId) { currentAccount.clientId = this.currentAccountAndTemplate.clientId; } else { currentAccount.groupId = this.currentAccountAndTemplate.groupId; } + + const nominees = this.currentAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + currentAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); + } + this.currentAccountsService .updateCurrentAccount(this.currentAccountAndTemplate.id, currentAccount) .subscribe((response: any) => { diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html index 7d09064d1..a0b81c891 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html @@ -87,6 +87,7 @@ [fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate" [fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate" [cbsBaseRates]="cbsBaseRates" + [submittedOnDate]="submittedOnDate" (tenureSelected)="onTenureSelected($event)" > diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts index 1d47b3e8d..99e4fcfef 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.ts @@ -153,6 +153,11 @@ export class CreateFixedDepositAccountComponent { }; } + get submittedOnDate(): Date | null { + const val = this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails?.submittedOnDate; + return val ? new Date(val) : null; + } + /** * Submits the fixed deposit form and creates a new fixed deposit account */ diff --git a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html index b98a181e3..8cbf3f2df 100644 --- a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.html @@ -6,96 +6,145 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- + +
+ +
+

Modify Fixed Deposit Account Application

+
+ Review and update the account details, currency and interest terms before resubmitting. +
+
+
+ + + - - - - - - - - - - - - - - - - - - - - - - {{ 'labels.inputs.DETAILS' | translate }} - - - - - - - {{ 'labels.inputs.TERMS' | translate }} - - - - - - - {{ 'labels.inputs.SETTINGS' | translate }} - - - - - - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - - - - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - @if (fixedDepositAccountFormValidAndNotPristine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - + + + + Product + +
+ - - - } - + + +
+ + +
+
+
+ + + + + + Currency + +
+ @if (fixedDepositsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + Interest Rate Chart + +
+ @if (fixedDepositsAccountProductTemplate) { + + + } + +
+ + + +
+
+
+ + + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ @if (fixedDepositsAccountProductTemplate) { + + } + +
+ + + + +
+
+
+
diff --git a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.scss b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.scss index af9f4c075..03cb2b0c8 100644 --- a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.scss +++ b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.scss @@ -5,3 +5,164 @@ * 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/. */ + +$navy: #0d1b2a; +$blue: #1e88e5; +$blue-d: #1e40af; +$indigo: #4f46e5; +$border: #e2e8f0; + +.cfa-container { + max-width: 1180px; + margin: 0 auto; + padding: 20px 24px; +} + +/* ─── Hero header (matches Nexora pages) ────────────────────────────── */ +.cfa-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; + } +} + +/* ─── Wizard tab strip ──────────────────────────────────────────────── */ +.cfa-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.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; + } + } +} + +.cfa-tab__icon { + margin-right: 8px; + font-size: 13px; +} + +/* ─── Tab body + action bar ─────────────────────────────────────────── */ +.cfa-tab-body { + padding: 24px; + + @media (width <= 768px) { + padding: 16px; + } +} + +.cfa-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; +} + +button.preview-submit-row__primary, +.mat-mdc-raised-button.mat-primary.preview-submit-row__primary, +.preview-submit-row__primary { + background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important; + + --mdc-protected-button-container-color: transparent !important; + --mdc-filled-button-container-color: transparent !important; + + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; +} diff --git a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts index 8b7135817..413186748 100644 --- a/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts +++ b/src/app/deposits/fixed-deposits/edit-fixed-deposit-account/edit-fixed-deposit-account.component.ts @@ -12,23 +12,23 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { FixedDepositsService } from '../fixed-deposits.service'; +import { ClientsService } from 'app/clients/clients.service'; import { SettingsService } from 'app/settings/settings.service'; +import { AlertService } from 'app/core/alert/alert.service'; +import { ProductsService } from 'app/products/products.service'; /** Custom Components */ import { FixedDepositAccountDetailsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component'; -import { FixedDepositAccountTermsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-terms-step/fixed-deposit-account-terms-step.component'; -import { FixedDepositAccountSettingsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-settings-step/fixed-deposit-account-settings-step.component'; -import { FixedDepositAccountChargesStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-charges-step/fixed-deposit-account-charges-step.component'; +import { FixedDepositAccountCurrencyStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component'; import { Dates } from 'app/core/utils/dates'; -import { Currency } from 'app/shared/models/general.model'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { MatTabsModule } from '@angular/material/tabs'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component'; import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** - * Edit Fixed Deposit Account Component + * Edit Fixed Deposit Account Component — same tab UI as Create, pre-populated with existing data. */ @Component({ selector: 'mifosx-edit-fixed-deposit-account', @@ -36,16 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-fixed-deposit-account.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, + MatTabsModule, FaIconComponent, - MatStep, - MatStepLabel, FixedDepositAccountDetailsStepComponent, - FixedDepositAccountTermsStepComponent, - FixedDepositAccountSettingsStepComponent, FixedDepositAccountInterestRateChartStepComponent, - FixedDepositAccountChargesStepComponent, + FixedDepositAccountCurrencyStepComponent, FixedDepositAccountPreviewStepComponent ] }) @@ -55,110 +50,201 @@ export class EditFixedDepositAccountComponent { private dateUtils = inject(Dates); private fixedDepositsService = inject(FixedDepositsService); private settingsService = inject(SettingsService); + private alertService = inject(AlertService); + private productsService = inject(ProductsService); + private clientsService = inject(ClientsService); - /** Fixed Deposits Account Details Step */ @ViewChild(FixedDepositAccountDetailsStepComponent, { static: true }) fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent; - /** Fixed Deposits Account Terms Step */ - @ViewChild(FixedDepositAccountTermsStepComponent, { static: true }) - fixedDepositAccountTermsStep: FixedDepositAccountTermsStepComponent; - /** Fixed Deposits Account Settings Step */ - @ViewChild(FixedDepositAccountSettingsStepComponent, { static: true }) - fixedDepositAccountSettingsStep: FixedDepositAccountSettingsStepComponent; - /** Fixed Deposits Account Charges Step */ - @ViewChild(FixedDepositAccountChargesStepComponent, { static: true }) - fixedDepositAccountChargesStep: FixedDepositAccountChargesStepComponent; + @ViewChild(FixedDepositAccountCurrencyStepComponent) + fixedDepositAccountCurrencyStep: FixedDepositAccountCurrencyStepComponent; - /** Fixed Deposits Account Template */ + /** Full account data + template from resolver */ fixedDepositsAccountAndTemplate: any; - /** Fixed Deposit Account Product Template */ + /** Product template emitted by details step */ fixedDepositsAccountProductTemplate: any; + /** Currently visible wizard tab (0-indexed). */ + selectedTabIndex = 0; + private readonly totalTabs = 4; + /** Active CBS base interest rate versions for the selected product's FD type */ + cbsBaseRates: any[] = []; + /** Tenure selected from the interest rate chart step */ + selectedTenure: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + maturityAmount?: number; + interestEarned?: number; + appliedRate?: number; + isSeniorCitizen?: boolean; + } | null = null; - currency: Currency; - - /** - * Fetches FD account template from `resolve` - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {Dates} dateUtils Date Utils - * @param {FixedDepositsService} fixedDepositsService Fixed Deposits Service - * @param {SettingsService} settingsService Settings Service - */ constructor() { this.route.data.subscribe((data: { fixedDepositsAccountAndTemplate: any }) => { - this.fixedDepositsAccountAndTemplate = data.fixedDepositsAccountAndTemplate; + const account = data.fixedDepositsAccountAndTemplate; + this.fixedDepositsAccountAndTemplate = account; + // Pre-populate tenure from existing account + if (account?.depositPeriod) { + this.selectedTenure = { + depositPeriod: account.depositPeriod, + depositPeriodFrequencyId: account.depositPeriodFrequencyType?.id ?? 2, + depositAmount: account.depositAmount, + isSeniorCitizen: !!account.isSeniorCitizen, + appliedRate: account.seniorCitizenInterestRate ?? undefined + }; + } + if (account?.clientId && account?.accountNo) { + this.clientsService + .getClientNominees(String(account.clientId), account.accountNo, 'FD') + .subscribe((nominees: any[]) => { + if (Array.isArray(nominees) && nominees.length > 0) { + this.fixedDepositsAccountAndTemplate = { ...this.fixedDepositsAccountAndTemplate, nominees }; + } + }); + } }); } - /** - * Sets fixed deposits account product template. - * @param {any} $event API response - */ + next(): void { + if (this.selectedTabIndex < this.totalTabs - 1) { + this.selectedTabIndex += 1; + } + } + + prev(): void { + if (this.selectedTabIndex > 0) { + this.selectedTabIndex -= 1; + } + } + setTemplate($event: any) { this.fixedDepositsAccountProductTemplate = $event; - this.currency = this.fixedDepositsAccountAndTemplate.currency; + const fdType = $event?.fdType; + this.productsService.getFdBaseInterestRates(fdType).subscribe({ + next: (rates: any[]) => (this.cbsBaseRates = rates || []), + error: () => (this.cbsBaseRates = []) + }); + } + + onTenureSelected($event: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + maturityAmount?: number; + interestEarned?: number; + appliedRate?: number; + isSeniorCitizen?: boolean; + }) { + this.selectedTenure = $event; } - /** - * Retrieves Fixed Deposit Account Details Form Data - */ get fixedDepositAccountDetailsForm() { - return this.fixedDepositsAccountDetailsStep.fixedDepositAccountDetailsForm; + return this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetailsForm; } - /** - * Retrieves Fixed Deposit Account Terms Form Data - */ - get fixedDepositAccountTermsForm() { - return this.fixedDepositAccountTermsStep.fixedDepositAccountTermsForm; - } - - /** - * Retrieves Fixed Deposit Account Settings Form Data - */ - get fixedDepositAccountSettingsForm() { - return this.fixedDepositAccountSettingsStep.fixedDepositAccountSettingsForm; - } - - /** - * Checks if stepper is valid and not pristine. - */ - get fixedDepositAccountFormValidAndNotPristine() { - return ( - this.fixedDepositAccountDetailsForm.valid && - this.fixedDepositAccountTermsForm.valid && - this.fixedDepositAccountSettingsForm.valid && - (!this.fixedDepositAccountDetailsForm.pristine || - !this.fixedDepositAccountTermsForm.pristine || - !this.fixedDepositAccountSettingsForm.pristine || - !this.fixedDepositAccountChargesStep.pristine) - ); - } - - /** - * Creates the fixed deposit account object. - */ get fixedDepositAccount() { return { - ...this.fixedDepositsAccountDetailsStep.fixedDepositAccountDetails, - ...this.fixedDepositAccountTermsStep.fixedDepositAccountTerms, - ...this.fixedDepositAccountSettingsStep.fixedDepositAccountSettings, - ...this.fixedDepositAccountChargesStep?.fixedDepositAccountCharges + ...this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails, + ...this.fixedDepositAccountCurrencyStep?.currencyStepData }; } - /** - * Submits the fixed deposit form and creates a new fixed deposit account - */ + get submittedOnDate(): Date | null { + const val = + this.fixedDepositsAccountDetailsStep?.fixedDepositAccountDetails?.submittedOnDate ?? + this.fixedDepositsAccountAndTemplate?.timeline?.submittedOnDate; + if (!val) return null; + const d = Array.isArray(val) ? new Date(val[0], val[1] - 1, val[2]) : new Date(val); + return isNaN(d.getTime()) ? null : d; + } + submit() { + const detailsStep = this.fixedDepositsAccountDetailsStep; + if (detailsStep?.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } + if ( + this.fixedDepositsAccountProductTemplate?.nominationFacility && + (this.fixedDepositAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (!this.selectedTenure || !this.selectedTenure.depositPeriod) { + this.alertService.alert({ + type: 'Error', + message: 'Please enter the Deposit Tenure and click "Find Rate" before submitting.' + }); + return; + } + if (!detailsStep?.selectedCustomerSegment) { + this.alertService.alert({ type: 'Error', message: 'Please select a Customer Segment before submitting.' }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const fixedDepositAccount = { + const pt = this.fixedDepositsAccountProductTemplate; + + const terms: any = {}; + if (pt?.nominalAnnualInterestRate != null) terms.nominalAnnualInterestRate = pt.nominalAnnualInterestRate; + if (pt?.interestCompoundingPeriodType?.id != null) + terms.interestCompoundingPeriodType = pt.interestCompoundingPeriodType.id; + if (pt?.interestPostingPeriodType?.id != null) terms.interestPostingPeriodType = pt.interestPostingPeriodType.id; + if (pt?.interestCalculationType?.id != null) terms.interestCalculationType = pt.interestCalculationType.id; + if (pt?.interestCalculationDaysInYearType?.id != null) + terms.interestCalculationDaysInYearType = pt.interestCalculationDaysInYearType.id; + + if (this.selectedTenure?.depositAmount != null) { + terms.depositAmount = this.selectedTenure.depositAmount; + } else if (pt?.depositAmount != null) { + terms.depositAmount = pt.depositAmount; + } + + if (pt?.minDepositAmount != null) terms.minDepositAmount = pt.minDepositAmount; + if (pt?.maxDepositAmount != null) terms.maxDepositAmount = pt.maxDepositAmount; + + const settings: any = {}; + if (pt?.lockinPeriodFrequency != null) settings.lockinPeriodFrequency = pt.lockinPeriodFrequency; + if (pt?.lockinPeriodFrequencyType?.id != null) settings.lockinPeriodFrequencyType = pt.lockinPeriodFrequencyType.id; + if (pt?.minDepositTerm != null) settings.minDepositTerm = pt.minDepositTerm; + if (pt?.minDepositTermType?.id != null) settings.minDepositTermTypeId = pt.minDepositTermType.id; + if (pt?.inMultiplesOfDepositTerm != null) settings.inMultiplesOfDepositTerm = pt.inMultiplesOfDepositTerm; + if (pt?.inMultiplesOfDepositTermType?.id != null) + settings.inMultiplesOfDepositTermTypeId = pt.inMultiplesOfDepositTermType.id; + if (pt?.maxDepositTerm != null) settings.maxDepositTerm = pt.maxDepositTerm; + if (pt?.maxDepositTermType?.id != null) settings.maxDepositTermTypeId = pt.maxDepositTermType.id; + if (pt?.preClosurePenalApplicable != null) settings.preClosurePenalApplicable = pt.preClosurePenalApplicable; + if (pt?.preClosurePenalInterest != null) settings.preClosurePenalInterest = pt.preClosurePenalInterest; + if (pt?.preClosurePenalInterestOnType?.id != null) + settings.preClosurePenalInterestOnTypeId = pt.preClosurePenalInterestOnType.id; + if (pt?.maturityInstructionId != null) settings.maturityInstructionId = pt.maturityInstructionId; + if (pt?.transferInterestToSavings != null) settings.transferInterestToSavings = pt.transferInterestToSavings; + if (pt?.withHoldTax != null) settings.withHoldTax = pt.withHoldTax; + + const rawCharges: any[] = + (this.fixedDepositAccount.charges?.length ? this.fixedDepositAccount.charges : pt?.charges) ?? []; + + const fixedDepositAccount: any = { ...this.fixedDepositAccount, + ...terms, + ...settings, + ...(this.selectedTenure + ? { + depositPeriod: this.selectedTenure.depositPeriod, + depositPeriodFrequencyId: this.selectedTenure.depositPeriodFrequencyId + } + : {}), clientId: this.fixedDepositsAccountAndTemplate.clientId, - charges: this.fixedDepositAccount.charges.map((charge: any) => ({ - chargeId: charge.id, + charges: rawCharges.map((charge: any) => ({ + chargeId: charge.chargeId ?? charge.id, amount: charge.amount, dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat), feeOnMonthDay: @@ -166,11 +252,35 @@ export class EditFixedDepositAccountComponent { feeInterval: charge.feeInterval })), submittedOnDate: this.dateUtils.formatDate(this.fixedDepositAccount.submittedOnDate, dateFormat), - charts: [{ chartSlabs: this.fixedDepositsAccountProductTemplate.accountChart.chartSlabs }], + charts: [{ chartSlabs: pt?.accountChart?.chartSlabs ?? [] }], dateFormat, monthDayFormat, locale }; + + const nominees = this.fixedDepositAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + fixedDepositAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); + } + + if ( + this.selectedTenure?.isSeniorCitizen && + this.selectedTenure.appliedRate != null && + this.selectedTenure.appliedRate > 0 + ) { + fixedDepositAccount.isSeniorCitizen = true; + fixedDepositAccount.seniorCitizenInterestRate = this.selectedTenure.appliedRate; + fixedDepositAccount.nominalAnnualInterestRate = this.selectedTenure.appliedRate; + } + this.fixedDepositsService .updateFixedDepositAccount(this.fixedDepositsAccountAndTemplate.id, fixedDepositAccount) .subscribe((response: any) => { diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts index 7bd2ffca1..76e81186b 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-currency-step/fixed-deposit-account-currency-step.component.ts @@ -28,6 +28,7 @@ import { FormsModule } from '@angular/forms'; export class FixedDepositAccountCurrencyStepComponent implements OnChanges { @Input() fixedDepositsAccountProductTemplate: any; @Input() primaryClientId: number | null = null; + @Input() existingAccount: any = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nominees: any[] = []; @@ -38,8 +39,20 @@ export class FixedDepositAccountCurrencyStepComponent implements OnChanges { autoDebitFacility = false; nominationFacility = false; + private existingAccountApplied = false; + ngOnChanges(changes: SimpleChanges): void { - if (changes['fixedDepositsAccountProductTemplate']?.currentValue) { + if (changes['existingAccount']?.currentValue) { + const ea = changes['existingAccount'].currentValue; + this.existingAccountApplied = true; + if (ea.autoDebitFacility !== undefined) { + this.autoDebitFacility = !!ea.autoDebitFacility; + } + if (Array.isArray(ea.nominees) && ea.nominees.length > 0) { + this.nominees = [...ea.nominees]; + } + } + if (changes['fixedDepositsAccountProductTemplate']?.currentValue && !this.existingAccountApplied) { const pt = changes['fixedDepositsAccountProductTemplate'].currentValue; this.autoDebitFacility = !!pt.autoDebitFacility; this.nominationFacility = !!pt.nominationFacility; diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html index dc49e8bd7..b5f865cbd 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.html @@ -101,7 +101,7 @@ Account No diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts index 21b6b7435..c4a42c210 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-details-step/fixed-deposit-account-details-step.component.ts @@ -71,6 +71,7 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { fixedDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; + private isEditMode = false; /** Fixed Deposits Account Template with product data */ @Output() fixedDepositsAccountProductTemplate = new EventEmitter(); @@ -248,6 +249,20 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { currencyCode: a.currency?.code ?? '' })); this.dalLoading = false; + // Pre-select linked account in edit mode + const linkedId = + this.fixedDepositsAccountTemplate?.linkedAccount?.id ?? this.fixedDepositsAccountTemplate?.linkedAccountId; + if (linkedId) { + const savings = this.dalAllSavings.find((a: any) => a.id === linkedId); + const current = this.dalAllCurrent.find((a: any) => a.id === linkedId); + if (savings) { + this.dalSelectedType = 'SAVINGS'; + this.selectedLinkedAccount = savings; + } else if (current) { + this.dalSelectedType = 'CURRENT'; + this.selectedLinkedAccount = current; + } + } this.cdr.detectChanges(); }, error: () => { @@ -265,9 +280,27 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { this.buildDependencies(); if (this.fixedDepositsAccountTemplate) { this.productData = this.fixedDepositsAccountTemplate.productOptions; - if (this.fixedDepositsAccountTemplate.depositProductId) { + const existingProductId = + this.fixedDepositsAccountTemplate.depositProductId ?? this.fixedDepositsAccountTemplate.productId; + // Edit mode: account response has accountNo; pure template has previewAccountNo but no accountNo + if (this.fixedDepositsAccountTemplate.accountNo) { + this.isEditMode = true; + } + if ((!this.productData || this.productData.length === 0) && existingProductId) { + this.isEditMode = true; + this.productData = [ + { + id: existingProductId, + name: + this.fixedDepositsAccountTemplate.productName ?? + this.fixedDepositsAccountTemplate.depositProductName ?? + 'Product' + } + ]; + } + if (existingProductId) { this.fixedDepositAccountDetailsForm.patchValue({ - productId: this.fixedDepositsAccountTemplate.depositProductId, + productId: existingProductId, submittedOnDate: this.fixedDepositsAccountTemplate.timeline?.submittedOnDate && new Date(this.fixedDepositsAccountTemplate.timeline.submittedOnDate), @@ -361,6 +394,9 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { } private autoDetectAccountStatus(): void { + if (this.isEditMode) { + return; + } if (this.clientAge === null) return; if (this.clientAge < 18) { this.selectedAccountStatus = 'MINOR'; @@ -430,10 +466,90 @@ export class FixedDepositAccountDetailsStepComponent implements OnInit { } else { this.fixedDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue(''); } + // === Restore existing account values when in edit/modify mode === + this.restoreExistingAccountValues(); }); }); } + private restoreExistingAccountValues(): void { + const ea = this.fixedDepositsAccountTemplate; + if (!ea?.depositProductId && !ea?.productId) { + return; + } + + const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? '')); + + const accountStatus = strVal(ea.accountStatus); + if (accountStatus) { + this.selectedAccountStatus = accountStatus; + } + + const customerSegment = strVal(ea.customerSegment); + if (customerSegment) { + this.selectedCustomerSegment = customerSegment; + } + + // FD API returns cbs_account_type aliased as "accountType" (string); Current/Savings return it as "cbsAccountType" + const rawType = ea.cbsAccountType ?? (typeof ea.accountType === 'string' ? ea.accountType : null); + const accountType = rawType ? strVal(rawType) : null; + if (accountType) { + this.selectedAccountType = accountType; + } + + if (accountStatus === 'JOINT') { + const modeOfOp = strVal(ea.modeOfOperation); + if (modeOfOp) { + this.modeOfOperation = modeOfOp; + } + if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) { + // Support both camelCase (fixed) and lowercase (MySQL queryForList) key formats + const isPrimary = (h: any) => h.isPrimary ?? h.isprimary ?? false; + const primary = ea.jointHolders.find((h: any) => isPrimary(h)); + const primaryShare = primary?.sharePercentage ?? primary?.sharepercentage; + if (primaryShare) { + this.primaryHolderShare = +primaryShare; + } + this.jointHolders = ea.jointHolders + .filter((h: any) => !isPrimary(h)) + .map((h: any) => ({ + clientId: h.clientId ?? h.clientid, + clientName: h.displayName ?? h.displayname ?? h.clientName ?? '', + clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '', + sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0), + isMandatory: !!(h.isMandatory ?? h.ismandatory) + })); + } + } + if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) { + this.selectedSpecialCategory = ea.specialCategory; + } + if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) { + this.selectedWeakerSection = ea.weakerSection; + } + if (accountStatus === 'MINOR') { + if (ea.guardianClientId) { + this.guardianClientId = ea.guardianClientId; + } + if (ea.guardianName) { + this.guardianName = ea.guardianName; + } + if (ea.guardianCifNumber) { + this.guardianCif = ea.guardianCifNumber; + } + if (ea.guardianRelationship) { + this.guardianRelationship = ea.guardianRelationship; + } + if (ea.minorDateOfBirth) { + const d = new Date(ea.minorDateOfBirth); + if (!isNaN(d.getTime())) { + this.minorDob = d; + } + } + } + this.cdr.detectChanges(); + } + hasValue(val: any): boolean { return val !== null && val !== undefined && val !== ''; } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts index 3589f5d2d..a54cf9e1d 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, OnChanges, Output, EventEmitter, ViewChild } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, inject } from '@angular/core'; import { MatTableDataSource, MatTable, @@ -25,6 +25,7 @@ import { NgIf, NgSwitch, TitleCasePipe, DecimalPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FixedDepositsService } from '../../fixed-deposits.service'; export type FdPeriodUnit = 'Days' | 'Months' | 'Years'; @@ -69,10 +70,22 @@ export interface SlabMatchResult { ] }) export class FixedDepositAccountInterestRateChartStepComponent implements OnChanges { + private fixedDepositsService = inject(FixedDepositsService); + @Input() fixedDepositsAccountTemplate: any; @Input() fixedDepositsAccountProductTemplate: any; /** Active CBS base interest rate versions — when provided, overrides the product chart slabs */ @Input() cbsBaseRates: any[] = []; + /** Pre-populate tenure from existing account (edit mode) */ + @Input() initialTenure: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + isSeniorCitizen?: boolean; + appliedRate?: number; + } | null = null; + /** Submission date for exact calculation (passed from create/edit form) */ + @Input() submittedOnDate: Date | null = null; activeBase: any = null; @@ -157,8 +170,10 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan return this.fdType === 'SPECIAL'; } - ngOnChanges() { - if (this.fixedDepositsAccountProductTemplate) { + ngOnChanges(changes: SimpleChanges) { + // Only reset/re-apply when TEMPLATE or cbsBaseRates changes, NOT when initialTenure/submittedOnDate changes alone + const templateChanged = changes['fixedDepositsAccountProductTemplate'] || changes['cbsBaseRates']; + if (templateChanged && this.fixedDepositsAccountProductTemplate) { const fdType = this.fixedDepositsAccountProductTemplate?.fdType; this.activeBase = (this.cbsBaseRates || []).find((r) => r.fdType === fdType && r.isActive); this.interestRateChartData = this.activeBase?.slabs @@ -169,9 +184,36 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan } this.resetCalculator(); this.tenurePeriodUnit = this.allowedPeriodUnits[0]; + // Pre-fill tenure in edit mode + this.applyInitialTenure(); } } + private applyInitialTenure(): void { + if (!this.initialTenure?.depositPeriod) { + return; + } + // Map frequency ID to period unit: Days=0, Weeks=1, Months=2, Years=3 + const freqId = this.initialTenure.depositPeriodFrequencyId; + const unitMap: Record = { 0: 'Days', 1: 'Days', 2: 'Months', 3: 'Years' }; + const unit: FdPeriodUnit = unitMap[freqId] ?? 'Months'; + if (this.allowedPeriodUnits.includes(unit)) { + this.tenurePeriodUnit = unit; + } else { + this.tenurePeriodUnit = this.allowedPeriodUnits[0]; + } + this.tenureValue = this.initialTenure.depositPeriod; + if (this.initialTenure.depositAmount) { + this.principalAmount = this.initialTenure.depositAmount; + } + // Pre-check senior citizen if it was set during create + if (this.initialTenure.isSeniorCitizen) { + this.isSeniorCitizen = true; + } + // Auto-calculate when tenure is pre-filled (edit mode) + setTimeout(() => this.calculateRate(), 0); + } + private normalizeCbsSlabs(slabs: any[]): any[] { return (slabs || []).map((s) => ({ ...s, @@ -257,38 +299,116 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan tenureDays: enteredDays }; - if (this.principalAmount && this.principalAmount > 0) { - const p = this.principalAmount; - const r = rate / 100; - const years = enteredDays / 365; - const n = 4; // Quarterly compounding - - if (this.isSpecial) { - // Monthly payout: simple interest paid monthly, principal returned at maturity - result.estimatedMonthlyPayout = (p * rate) / 1200; - result.interestEarned = result.estimatedMonthlyPayout * (enteredDays / 30); - result.maturityAmount = p; // principal returned at maturity - } else { - // Compound interest (Standard Quarterly) - result.maturityAmount = p * Math.pow(1 + r / n, n * years); - result.interestEarned = result.maturityAmount - p; - } - } - - this.matchResult = result; - - // Emit selected tenure and calculated maturity to parent + this.matchResult = result; // Show slab match immediately, maturity updates async if principal entered const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit); - if (frequencyId !== null) { - this.tenureSelected.emit({ - depositPeriod: this.tenureValue, - depositPeriodFrequencyId: frequencyId, - depositAmount: this.principalAmount || undefined, - maturityAmount: result.maturityAmount, - interestEarned: result.interestEarned, - appliedRate: result.appliedRate, - isSeniorCitizen: this.isSeniorCitizen - }); + + if (this.principalAmount && this.principalAmount > 0) { + // Convert tenure to months for backend API + let tenureInMonths = 0; + if (this.tenurePeriodUnit === 'Years') { + tenureInMonths = this.tenureValue * 12; + } else if (this.tenurePeriodUnit === 'Months') { + tenureInMonths = this.tenureValue; + } else { + tenureInMonths = Math.floor(enteredDays / 30); + } + + // Map compounding period type id → months (0=Daily, 1=Daily, 4=Monthly, 5=Quarterly, 6=SemiAnnual, 7=Annual) + const compMap: Record = { 1: 0, 4: 1, 5: 3, 6: 6, 7: 12 }; + const compId = this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id ?? 4; + const compMonths = compMap[compId] ?? 1; + + // Map posting period type id → months + const postId = this.fixedDepositsAccountProductTemplate?.interestPostingPeriodType?.id ?? 4; + const postMonths = compMap[postId] ?? 1; + + const emitResult = () => { + if (frequencyId !== null) { + this.tenureSelected.emit({ + depositPeriod: this.tenureValue, + depositPeriodFrequencyId: frequencyId, + depositAmount: this.principalAmount || undefined, + maturityAmount: result.maturityAmount, + interestEarned: result.interestEarned, + appliedRate: result.appliedRate, + isSeniorCitizen: this.isSeniorCitizen + }); + } + }; + + // Get submittedOnDate — Input takes priority over template data + let submittedOnDateStr: string; + if (this.submittedOnDate) { + submittedOnDateStr = this.submittedOnDate.toISOString().split('T')[0]; + } else { + const submittedOnDateRaw = + this.fixedDepositsAccountTemplate?.timeline?.submittedOnDate ?? + this.fixedDepositsAccountProductTemplate?.timeline?.submittedOnDate; + if (submittedOnDateRaw) { + const d = Array.isArray(submittedOnDateRaw) + ? new Date(submittedOnDateRaw[0], submittedOnDateRaw[1] - 1, submittedOnDateRaw[2]) + : new Date(submittedOnDateRaw); + submittedOnDateStr = !isNaN(d.getTime()) + ? d.toISOString().split('T')[0] + : new Date().toISOString().split('T')[0]; + } else { + submittedOnDateStr = new Date().toISOString().split('T')[0]; + } + } + const daysInYearVal = + this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365; + + // Always use backend API for accurate calculation (including daily compounding with 0) + this.fixedDepositsService + .calculateFixedDepositInterest({ + principalAmount: this.principalAmount, + annualInterestRate: rate, + tenureInMonths, + interestCompoundingPeriodInMonths: compMonths, // 0 = daily, now supported by backend + interestPostingPeriodInMonths: postMonths, + submittedOnDate: submittedOnDateStr, + daysInYear: daysInYearVal + }) + .subscribe({ + next: (apiResult: any) => { + result.maturityAmount = apiResult.maturityAmount; + result.interestEarned = + apiResult.totalInterest ?? Math.round((apiResult.maturityAmount - this.principalAmount) * 100) / 100; + this.matchResult = { ...result }; + emitResult(); + }, + error: () => { + // Fallback if API fails: frontend formula + const r = rate / 100; + if (compMonths === 0) { + // Daily compounding fallback + result.maturityAmount = + Math.round(this.principalAmount * Math.pow(1 + r / daysInYearVal, enteredDays) * 100) / 100; + } else { + const n = 12 / compMonths; + const totalCompoundings = Math.floor(tenureInMonths / compMonths); + const rPerCompounding = rate / (n * 100); + result.maturityAmount = + Math.round(this.principalAmount * Math.pow(1 + rPerCompounding, totalCompoundings) * 100) / 100; + } + result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100; + this.matchResult = { ...result }; + emitResult(); + } + }); + } else { + this.matchResult = result; + if (frequencyId !== null) { + this.tenureSelected.emit({ + depositPeriod: this.tenureValue, + depositPeriodFrequencyId: frequencyId, + depositAmount: undefined, + maturityAmount: undefined, + interestEarned: undefined, + appliedRate: result.appliedRate, + isSeniorCitizen: this.isSeniorCitizen + }); + } } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts index ca1b56aee..b4a2ea5e0 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposits-buttons.config.ts @@ -92,6 +92,15 @@ export class FixedDepositsButtonsConfiguration { } ]; break; + case 'Rejected': + case 'rejected': + this.buttonsArray = [ + { + name: 'Modify Application', + icon: 'fa fa-pencil' + } + ]; + break; default: this.buttonsArray = []; } diff --git a/src/app/deposits/fixed-deposits/fixed-deposits.service.ts b/src/app/deposits/fixed-deposits/fixed-deposits.service.ts index 5e19f5786..6d20ed276 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits.service.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits.service.ts @@ -114,6 +114,34 @@ export class FixedDepositsService { return this.http.get(`/fixeddepositaccounts/${accountId}`, { params: httpParams }); } + /** + * Calculate FD maturity using backend formula. + * When submittedOnDate + daysInYear are provided, uses exact calendar-based calculation + * matching the actual account creation engine. + */ + calculateFixedDepositInterest(params: { + principalAmount: number; + annualInterestRate: number; + tenureInMonths: number; + interestCompoundingPeriodInMonths: number; + interestPostingPeriodInMonths: number; + submittedOnDate?: string; // yyyy-MM-dd — enables exact calendar calculation + daysInYear?: number; // 360 or 365 + }): Observable { + let httpParams = new HttpParams(); + Object.entries(params).forEach( + ([ + key, + val + ]) => { + if (val !== undefined && val !== null) { + httpParams = httpParams.set(key, String(val)); + } + } + ); + return this.http.get(`/fixeddepositaccounts/calculate-fd-interest`, { params: httpParams }); + } + /** * @param fixedDepositAccount Fixed Deposit Account * @returns {Observable} diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html index 055268a5f..f3d7d5225 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html @@ -6,103 +6,157 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- + +
+ +
+

Modify Recurring Deposit Account Application

+
+ Review and update the account details, currency and interest terms before resubmitting. +
+
+
+ + + - - - - - - - - - - - - - - - - - - - - - - {{ 'labels.inputs.DETAILS' | translate }} - - - - - - - {{ 'labels.inputs.TERMS' | translate }} - - - - - - - {{ 'labels.inputs.SETTINGS' | translate }} - - - - - - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - - - - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - @if (recurringDepositAccountFormValidAndNotPristine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - + + + + Product + +
+ - - - } - + + +
+ + +
+
+
+ + + + + + Currency + +
+ @if (recurringDepositsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + Interest Rate Chart + +
+ @if (recurringDepositsAccountProductTemplate) { + + + } + +
+ + + +
+
+
+ + + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ @if (recurringDepositsAccountProductTemplate) { + + + } + +
+ + + + +
+
+
+
diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.scss b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.scss index af9f4c075..657546bcc 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.scss +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.scss @@ -5,3 +5,164 @@ * 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/. */ + +$navy: #0d1b2a; +$blue: #1e88e5; +$blue-d: #1e40af; +$indigo: #4f46e5; +$border: #e2e8f0; + +.cra-container { + max-width: 1180px; + margin: 0 auto; + padding: 20px 24px; +} + +/* ─── Hero header (matches Nexora pages) ────────────────────────────── */ +.cra-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; + } +} + +/* ─── Wizard tab strip ──────────────────────────────────────────────── */ +.cra-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.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; + } + } +} + +.cra-tab__icon { + margin-right: 8px; + font-size: 13px; +} + +/* ─── Tab body + action bar ─────────────────────────────────────────── */ +.cra-tab-body { + padding: 24px; + + @media (width <= 768px) { + padding: 16px; + } +} + +.cra-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; +} + +button.preview-submit-row__primary, +.mat-mdc-raised-button.mat-primary.preview-submit-row__primary, +.preview-submit-row__primary { + background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important; + + --mdc-protected-button-container-color: transparent !important; + --mdc-filled-button-container-color: transparent !important; + + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; +} diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts index 8df7a977d..702c8228e 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts @@ -12,23 +12,23 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { RecurringDepositsService } from '../recurring-deposits.service'; +import { ClientsService } from 'app/clients/clients.service'; import { SettingsService } from 'app/settings/settings.service'; import { ProductsService } from 'app/products/products.service'; +import { AlertService } from 'app/core/alert/alert.service'; /** Step Components */ import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; -import { RecurringDepositsAccountTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-terms-step/recurring-deposits-account-terms-step.component'; -import { RecurringDepositsAccountSettingsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component'; -import { RecurringDepositsAccountChargesStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component'; import { Dates } from 'app/core/utils/dates'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { MatTabsModule } from '@angular/material/tabs'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component'; import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component'; import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** - * Edit new recurring deposit account + * Edit Recurring Deposit Account Component — same tab UI as Create, pre-populated with existing data. */ @Component({ selector: 'mifosx-edit-recurring-deposit-account', @@ -36,16 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-recurring-deposit-account.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, + MatTabsModule, FaIconComponent, - MatStep, - MatStepLabel, RecurringDepositsAccountDetailsStepComponent, - RecurringDepositsAccountTermsStepComponent, - RecurringDepositsAccountSettingsStepComponent, + RecurringDepositsAccountCurrencyStepComponent, RecurringDepositsAccountInterestRateChartStepComponent, - RecurringDepositsAccountChargesStepComponent, RecurringDepositsAccountPreviewStepComponent ] }) @@ -56,20 +51,117 @@ export class EditRecurringDepositAccountComponent { private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); private productsService = inject(ProductsService); + private alertService = inject(AlertService); + private clientsService = inject(ClientsService); - /** Imports all the step component */ @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent; - @ViewChild(RecurringDepositsAccountTermsStepComponent, { static: true }) - recurringDepositAccountTermsStep: RecurringDepositsAccountTermsStepComponent; - @ViewChild(RecurringDepositsAccountSettingsStepComponent, { static: true }) - recurringDepositAccountSettingsStep: RecurringDepositsAccountSettingsStepComponent; - @ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true }) - recurringDepositAccountChargesStep: RecurringDepositsAccountChargesStepComponent; + + @ViewChild(RecurringDepositsAccountCurrencyStepComponent) + recurringDepositsAccountCurrencyStep: RecurringDepositsAccountCurrencyStepComponent; @ViewChild(RecurringDepositsAccountInterestRateChartStepComponent) recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent; + /** Full account data + template from resolver */ + recurringDepositsAccountAndTemplate: any; + /** Product template emitted by details step */ + recurringDepositsAccountProductTemplate: any; + /** Currently visible wizard tab (0-indexed). */ + selectedTabIndex = 0; + private readonly totalTabs = 4; + /** Active CBS base interest rate versions */ + cbsBaseRates: any[] = []; + /** Selected tenure value and frequency from calculator */ + selectedTenureValue: number | null = null; + selectedTenureFrequencyId: number | null = null; + selectedDepositAmount: number | null = null; + rdAppliedRate: number | null = null; + rdIsSeniorCitizen = false; + + constructor() { + this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => { + const account = data.recurringDepositsAccountAndTemplate; + this.recurringDepositsAccountAndTemplate = account; + // Pre-populate tenure from existing account + if (account?.depositPeriod) { + this.selectedTenureValue = account.depositPeriod; + this.selectedTenureFrequencyId = account.depositPeriodFrequencyType?.id ?? 2; + } + if (account?.mandatoryRecommendedDepositAmount) { + this.selectedDepositAmount = account.mandatoryRecommendedDepositAmount; + } + if (account?.clientId && account?.accountNo) { + this.clientsService + .getClientNominees(String(account.clientId), account.accountNo, 'RD') + .subscribe((nominees: any[]) => { + if (Array.isArray(nominees) && nominees.length > 0) { + this.recurringDepositsAccountAndTemplate = { ...this.recurringDepositsAccountAndTemplate, nominees }; + } + }); + } + }); + } + + get initialTenure(): { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + isSeniorCitizen?: boolean; + appliedRate?: number; + } | null { + if (!this.selectedTenureValue) { + return null; + } + return { + depositPeriod: this.selectedTenureValue, + depositPeriodFrequencyId: this.selectedTenureFrequencyId ?? 2, + depositAmount: this.selectedDepositAmount ?? undefined, + isSeniorCitizen: !!this.recurringDepositsAccountAndTemplate?.isSeniorCitizen, + appliedRate: this.recurringDepositsAccountAndTemplate?.seniorCitizenInterestRate ?? undefined + }; + } + + next(): void { + if (this.selectedTabIndex < this.totalTabs - 1) { + this.selectedTabIndex += 1; + } + } + + prev(): void { + if (this.selectedTabIndex > 0) { + this.selectedTabIndex -= 1; + } + } + + setTemplate($event: any) { + this.recurringDepositsAccountProductTemplate = $event; + this.productsService.getFdBaseInterestRates().subscribe({ + next: (rates: any[]) => (this.cbsBaseRates = rates || []), + error: () => (this.cbsBaseRates = []) + }); + } + + handleTenureSelected($event: any) { + this.selectedTenureValue = $event.depositPeriod; + this.selectedTenureFrequencyId = $event.depositPeriodFrequencyId; + this.selectedDepositAmount = $event.depositAmount; + this.rdAppliedRate = $event.appliedRate ?? null; + this.rdIsSeniorCitizen = $event.isSeniorCitizen ?? false; + } + + get recurringDepositAccountDetailsForm() { + return this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetailsForm; + } + + get recurringDepositAccountData() { + return { ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails }; + } + + get recurringDepositAccount() { + return { ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails }; + } + get rdTenureDays(): number | null { return this.recurringDepositsAccountChartStep?.matchResult?.tenureDays ?? null; } @@ -94,112 +186,153 @@ export class EditRecurringDepositAccountComponent { return this.recurringDepositAccountDetailsForm?.value?.submittedOnDate ?? null; } - /** Recurring Deposits Account And Template */ - recurringDepositsAccountAndTemplate: any; - /** Recurring Deposit Account Product Template */ - recurringDepositsAccountProductTemplate: any; - - constructor() { - this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => { - this.recurringDepositsAccountAndTemplate = data.recurringDepositsAccountAndTemplate; - }); - } - - /** - * Sets recurring deposits account product template. - * @param {any} $event API response - */ - setTemplate($event: any) { - this.recurringDepositsAccountProductTemplate = $event; - } - - /** Get Recurring Deposit Account Details Form Data */ - get recurringDepositAccountDetailsForm() { - return this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetailsForm; - } - - /** Get Recurring Deposit Account Terms Form Data */ - get recurringDepositAccountTermsForm() { - return this.recurringDepositAccountTermsStep.recurringDepositAccountTermsForm; - } - - /** Get Recurring Deposit Account Settings Form Data */ - get recurringDepositAccountSettingsForm() { - return this.recurringDepositAccountSettingsStep.recurringDepositAccountSettingsForm; - } - - // Checks if stepper is valid and not pristine. - get recurringDepositAccountFormValidAndNotPristine() { - return ( - this.recurringDepositAccountDetailsForm.valid && - this.recurringDepositAccountTermsForm.valid && - this.recurringDepositAccountSettingsForm.valid && - (!this.recurringDepositAccountDetailsForm.pristine || - !this.recurringDepositAccountTermsForm.pristine || - !this.recurringDepositAccountSettingsForm.pristine || - !this.recurringDepositAccountChargesStep.pristine) - ); - } - - /** Retrieves Data of all the forms */ - get recurringDepositAccountData() { + get rdCurrencyFlags() { return { - ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails, - ...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms, - ...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings, - ...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges + autoDebitFacility: this.recurringDepositsAccountCurrencyStep?.autoDebitFacility ?? false }; } - /** Retrieves Data of all forms except Currency to submit the data */ - get recurringDepositAccount() { - return { - ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails, - ...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms, - ...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings, - ...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges - }; - } - - /** - * Submits the recurring deposit form to create a new recurring deposit account - */ submit() { + const detailsStep = this.recurringDepositsAccountDetailsStep; + if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } + if ( + this.recurringDepositsAccountProductTemplate?.nominationFacility && + (this.recurringDepositsAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (!this.selectedTenureValue || this.selectedTenureFrequencyId === null) { + this.alertService.alert({ + type: 'Error', + message: 'Please enter the Deposit Tenure and click "Find Rate" before submitting.' + }); + return; + } + if (!this.selectedDepositAmount || this.selectedDepositAmount <= 0) { + this.alertService.alert({ + type: 'Error', + message: 'Please enter the Monthly Installment amount and click "Find Rate" before submitting.' + }); + return; + } + if (!detailsStep?.selectedCustomerSegment) { + this.alertService.alert({ type: 'Error', message: 'Please select a Customer Segment before submitting.' }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const recurringDepositAccount = { - ...this.recurringDepositAccount, + const pt = this.recurringDepositsAccountProductTemplate; + const details = this.recurringDepositAccount; + const currencyStep = this.recurringDepositsAccountCurrencyStep; + + const terms: any = {}; + if (pt?.nominalAnnualInterestRate != null) terms.nominalAnnualInterestRate = pt.nominalAnnualInterestRate; + if (pt?.interestCompoundingPeriodType?.id != null) + terms.interestCompoundingPeriodType = pt.interestCompoundingPeriodType.id; + if (pt?.interestPostingPeriodType?.id != null) terms.interestPostingPeriodType = pt.interestPostingPeriodType.id; + if (pt?.interestCalculationType?.id != null) terms.interestCalculationType = pt.interestCalculationType.id; + if (pt?.interestCalculationDaysInYearType?.id != null) + terms.interestCalculationDaysInYearType = pt.interestCalculationDaysInYearType.id; + + const settings: any = {}; + if (pt?.lockinPeriodFrequency != null) settings.lockinPeriodFrequency = pt.lockinPeriodFrequency; + if (pt?.lockinPeriodFrequencyType?.id != null) settings.lockinPeriodFrequencyType = pt.lockinPeriodFrequencyType.id; + if (pt?.minDepositTerm != null) settings.minDepositTerm = pt.minDepositTerm; + if (pt?.minDepositTermType?.id != null) settings.minDepositTermTypeId = pt.minDepositTermType.id; + if (pt?.inMultiplesOfDepositTerm != null) settings.inMultiplesOfDepositTerm = pt.inMultiplesOfDepositTerm; + if (pt?.inMultiplesOfDepositTermType?.id != null) + settings.inMultiplesOfDepositTermTypeId = pt.inMultiplesOfDepositTermType.id; + if (pt?.maxDepositTerm != null) settings.maxDepositTerm = pt.maxDepositTerm; + if (pt?.maxDepositTermType?.id != null) settings.maxDepositTermTypeId = pt.maxDepositTermType.id; + if (pt?.preClosurePenalApplicable != null) settings.preClosurePenalApplicable = pt.preClosurePenalApplicable; + if (pt?.preClosurePenalInterest != null) settings.preClosurePenalInterest = pt.preClosurePenalInterest; + if (pt?.preClosurePenalInterestOnType?.id != null) + settings.preClosurePenalInterestOnTypeId = pt.preClosurePenalInterestOnType.id; + if (pt?.maturityInstructionId != null) settings.maturityInstructionId = pt.maturityInstructionId; + if (pt?.transferInterestToSavings != null) settings.transferInterestToSavings = pt.transferInterestToSavings; + if (pt?.withHoldTax != null) settings.withHoldTax = pt.withHoldTax; + + const productDefaults: any = { + isCalendarInherited: pt?.isCalendarInherited ?? false, + isMandatoryDeposit: pt?.isMandatoryDeposit ?? false, + allowWithdrawal: pt?.allowWithdrawal ?? false, + adjustAdvanceTowardsFuturePayments: pt?.adjustAdvanceTowardsFuturePayments ?? false, + autoDebitFacility: currencyStep?.autoDebitFacility ?? pt?.autoDebitFacility ?? false + }; + + const rawCharges: any[] = pt?.charges ?? []; + + const recurringDepositAccount: any = { + ...details, + ...terms, + ...settings, + ...productDefaults, clientId: this.recurringDepositsAccountAndTemplate.clientId, - charges: this.recurringDepositAccount.charges.map((charge: any) => ({ - chargeId: charge.id, + mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0, + recurringFrequency: pt?.recurringFrequency ?? 1, + recurringFrequencyType: pt?.recurringFrequencyType ?? 2, + submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat), + expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat), + dateFormat, + monthDayFormat, + locale, + charts: [ + { + chartSlabs: + pt?.accountChart?.chartSlabs ?? pt?.activeChart?.chartSlabs ?? pt?.interestRateCharts?.[0]?.chartSlabs ?? [] + } + ], + charges: rawCharges.map((charge: any) => ({ + chargeId: charge.chargeId ?? charge.id, amount: charge.amount, dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat), feeOnMonthDay: charge.feeOnMonthDay, feeInterval: charge.feeInterval - })), - isCalendarInherited: this.recurringDepositAccount.recurringDepositAccount - ? this.recurringDepositAccount.recurringDepositAccount - : false, - submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat), - expectedFirstDepositOnDate: this.dateUtils.formatDate( - this.recurringDepositAccount.expectedFirstDepositOnDate, - dateFormat - ), - charts: [ - { - chartSlabs: - this.recurringDepositsAccountProductTemplate?.accountChart?.chartSlabs ?? - this.recurringDepositsAccountProductTemplate?.activeChart?.chartSlabs ?? - this.recurringDepositsAccountProductTemplate?.interestRateCharts?.[0]?.chartSlabs ?? - [] - } - ], - dateFormat, - monthDayFormat, - locale + })) }; + if (this.selectedTenureValue && this.selectedTenureFrequencyId !== null) { + recurringDepositAccount.depositPeriod = this.selectedTenureValue; + recurringDepositAccount.depositPeriodFrequencyId = this.selectedTenureFrequencyId; + } else if (this.rdTenureDays) { + recurringDepositAccount.depositPeriod = this.rdTenureDays; + recurringDepositAccount.depositPeriodFrequencyId = 0; + } + + if (this.selectedDepositAmount) { + recurringDepositAccount.mandatoryRecommendedDepositAmount = this.selectedDepositAmount; + } + + if (this.rdIsSeniorCitizen && this.rdAppliedRate != null && this.rdAppliedRate > 0) { + recurringDepositAccount.isSeniorCitizen = true; + recurringDepositAccount.seniorCitizenInterestRate = this.rdAppliedRate; + recurringDepositAccount.nominalAnnualInterestRate = this.rdAppliedRate; + } + + const nominees = this.recurringDepositsAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + recurringDepositAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); + } + this.recurringDepositsService .updateRecurringDepositAccount(this.recurringDepositsAccountAndTemplate.id, recurringDepositAccount) .subscribe((response: any) => { diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts index 8ca90813f..cb46ab05c 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts @@ -28,6 +28,7 @@ import { FormsModule } from '@angular/forms'; export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges { @Input() recurringDepositsAccountProductTemplate: any; @Input() primaryClientId: number | null = null; + @Input() existingAccount: any = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nominees: any[] = []; @@ -37,8 +38,20 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges autoDebitFacility = false; + private existingAccountApplied = false; + ngOnChanges(changes: SimpleChanges): void { - if (changes['recurringDepositsAccountProductTemplate']?.currentValue) { + if (changes['existingAccount']?.currentValue) { + const ea = changes['existingAccount'].currentValue; + this.existingAccountApplied = true; + if (ea.autoDebitFacility !== undefined) { + this.autoDebitFacility = !!ea.autoDebitFacility; + } + if (Array.isArray(ea.nominees) && ea.nominees.length > 0) { + this.nominees = [...ea.nominees]; + } + } + if (changes['recurringDepositsAccountProductTemplate']?.currentValue && !this.existingAccountApplied) { const pt = changes['recurringDepositsAccountProductTemplate'].currentValue; this.autoDebitFacility = !!pt.autoDebitFacility; } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index ee854d2ac..702defd3a 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -95,7 +95,7 @@ diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 18d37b66a..6ba4bea37 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -70,6 +70,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { recurringDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; + private isEditMode = false; /** Recurring Deposits Account Template with product data */ @Output() recurringDepositsAccountProductTemplate = new EventEmitter(); @@ -224,6 +225,21 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { currencyCode: a.currency?.code ?? '' })); this.dalLoading = false; + // Pre-select linked account in edit mode + const linkedId = + this.recurringDepositsAccountTemplate?.linkedAccount?.id ?? + this.recurringDepositsAccountTemplate?.linkedAccountId; + if (linkedId) { + const savings = this.dalAllSavings.find((a: any) => a.id === linkedId); + const current = this.dalAllCurrent.find((a: any) => a.id === linkedId); + if (savings) { + this.dalSelectedType = 'SAVINGS'; + this.selectedLinkedAccount = savings; + } else if (current) { + this.dalSelectedType = 'CURRENT'; + this.selectedLinkedAccount = current; + } + } this.cdr.detectChanges(); }, error: () => { @@ -241,11 +257,29 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.buildDependencies(); if (this.recurringDepositsAccountTemplate) { this.productData = this.recurringDepositsAccountTemplate.productOptions; - if (this.recurringDepositsAccountTemplate.depositProductId) { + const existingProductId = + this.recurringDepositsAccountTemplate.depositProductId ?? this.recurringDepositsAccountTemplate.productId; + // Edit mode: account response has accountNo; pure template has previewAccountNo but no accountNo + if (this.recurringDepositsAccountTemplate.accountNo) { + this.isEditMode = true; + } + if ((!this.productData || this.productData.length === 0) && existingProductId) { + this.isEditMode = true; + this.productData = [ + { + id: existingProductId, + name: + this.recurringDepositsAccountTemplate.productName ?? + this.recurringDepositsAccountTemplate.depositProductName ?? + 'Product' + } + ]; + } + if (existingProductId) { this.recurringDepositAccountDetailsForm.patchValue({ - productId: this.recurringDepositsAccountTemplate.depositProductId, + productId: existingProductId, submittedOnDate: - this.recurringDepositsAccountTemplate.timeline.submittedOnDate && + this.recurringDepositsAccountTemplate.timeline?.submittedOnDate && new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate), externalId: this.recurringDepositsAccountTemplate.externalId }); @@ -317,6 +351,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { } private autoDetectAccountStatus(): void { + if (this.isEditMode) { + return; + } if (this.clientAge === null) return; if (this.clientAge < 18) { this.selectedAccountStatus = 'MINOR'; @@ -391,10 +428,93 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { } else { this.recurringDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue(''); } + // === Restore existing account values when in edit/modify mode === + this.restoreExistingAccountValues(); }); }); } + private restoreExistingAccountValues(): void { + const ea = this.recurringDepositsAccountTemplate; + if (!ea?.depositProductId && !ea?.productId) { + return; + } + + const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? '')); + + const accountStatus = strVal(ea.accountStatus); + if (accountStatus) { + this.selectedAccountStatus = accountStatus; + } + + const customerSegment = strVal(ea.customerSegment); + if (customerSegment) { + this.selectedCustomerSegment = customerSegment; + } + + // RD API returns cbs_account_type aliased as "accountType" (string); Current/Savings return it as "cbsAccountType" + const rawType = ea.cbsAccountType ?? (typeof ea.accountType === 'string' ? ea.accountType : null); + const accountType = rawType ? strVal(rawType) : null; + if (accountType) { + this.selectedAccountType = accountType; + } + + if (accountStatus === 'JOINT') { + const modeOfOp = strVal(ea.modeOfOperation); + if (modeOfOp) { + this.modeOfOperation = modeOfOp; + } + if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) { + const isPrimaryHolder = (h: any) => h.isPrimary ?? h.isprimary ?? false; + const primary = ea.jointHolders.find((h: any) => isPrimaryHolder(h)); + const primaryShare = primary?.sharePercentage ?? primary?.sharepercentage; + if (primaryShare) { + this.primaryHolderShare = +primaryShare; + } + this.jointHolders = ea.jointHolders + .filter((h: any) => !isPrimaryHolder(h)) + .map((h: any) => ({ + clientId: h.clientId ?? h.clientid, + clientName: h.displayName ?? h.displayname ?? h.clientName ?? '', + clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '', + sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0), + isMandatory: !!(h.isMandatory ?? h.ismandatory) + })); + } + } + if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) { + this.selectedSpecialCategory = ea.specialCategory; + } + if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) { + this.selectedWeakerSection = ea.weakerSection; + } + if (accountStatus === 'MINOR') { + if (ea.guardianClientId) { + this.guardianClientId = ea.guardianClientId; + } + if (ea.guardianName) { + this.guardianName = ea.guardianName; + } + if (ea.guardianCifNumber) { + this.guardianCif = ea.guardianCifNumber; + } + if (ea.guardianRelationship) { + this.guardianRelationship = ea.guardianRelationship; + } + if (ea.minorDateOfBirth) { + const d = new Date(ea.minorDateOfBirth); + if (!isNaN(d.getTime())) { + this.minorDob = d; + } + } + } + // autoDebitFacility from existing account (override product default) + if (ea.autoDebitFacility !== undefined) { + this.recurringDepositAccountDetailsForm.patchValue({ autoDebitFacility: !!ea.autoDebitFacility }); + } + this.cdr.detectChanges(); + } + hasValue(val: any): boolean { return val !== null && val !== undefined && val !== ''; } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts index 55678ab38..46cf19aa6 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, OnChanges, ViewChild, Output, EventEmitter, inject } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, ViewChild, Output, EventEmitter, inject } from '@angular/core'; import { RecurringDepositsService } from '../../recurring-deposits.service'; import { MatTable, @@ -86,6 +86,14 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O @Input() submittedOnDate: Date | null = null; /** Active CBS base interest rate versions — when provided, overrides the product chart slabs */ @Input() cbsBaseRates: any[] = []; + /** Pre-populate tenure from existing account (edit mode) */ + @Input() initialTenure: { + depositPeriod: number; + depositPeriodFrequencyId: number; + depositAmount?: number; + isSeniorCitizen?: boolean; + appliedRate?: number; + } | null = null; @Output() tenureSelected = new EventEmitter<{ depositPeriod: number; @@ -165,17 +173,42 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O private recurringDepositsService = inject(RecurringDepositsService); - ngOnChanges() { + ngOnChanges(changes: SimpleChanges) { + // Only reset/re-apply when the TEMPLATE or cbsBaseRates changes, NOT when initialTenure changes alone + const templateChanged = + changes['recurringDepositsAccountProductTemplate'] || + changes['recurringDepositsAccountTemplate'] || + changes['cbsBaseRates']; const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate; - if (template) { + if (templateChanged && template) { if (this.chartsTableRef) { this.chartsTableRef.renderRows(); } this.resetCalculator(); this.tenurePeriodUnit = this.allowedPeriodUnits[1]; // Default to Months + this.applyInitialTenure(); } } + private applyInitialTenure(): void { + if (!this.initialTenure?.depositPeriod) { + return; + } + const freqId = this.initialTenure.depositPeriodFrequencyId; + // RD: Months=2, Years=3 + this.tenurePeriodUnit = freqId === 3 ? 'Years' : 'Months'; + this.tenureValue = this.initialTenure.depositPeriod; + if (this.initialTenure.depositAmount) { + this.principalAmount = this.initialTenure.depositAmount; + } + // Pre-check senior citizen if it was set during create + if (this.initialTenure.isSeniorCitizen) { + this.isSeniorCitizen = true; + } + // Auto-calculate when tenure is pre-filled (edit mode) + setTimeout(() => this.calculateRate(), 0); + } + // Convert entered tenure to Days for slab matching private toDays(value: number, unit: RdPeriodUnit): number { if (unit === 'Days') return value; @@ -240,6 +273,8 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O tenureDays: enteredDays }; + this.matchResult = result; // Show slab match immediately, maturity updates async when API responds + // Maturity Estimation via Backend API if (this.principalAmount && this.principalAmount > 0) { const compoundingType = this.recurringDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id; diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-buttons.config.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-buttons.config.ts index 0055573fd..5c18484c6 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-buttons.config.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-buttons.config.ts @@ -111,6 +111,16 @@ export class RecurringDepositsButtonsConfiguration { } ]; break; + case 'Rejected': + case 'rejected': + this.buttonsArray = [ + { + name: 'Modify Application', + icon: 'fa fa-edit', + taskPermissionName: 'UPDATE_RECURRINGDEPOSITACCOUNT' + } + ]; + break; default: this.buttonsArray = []; } diff --git a/src/app/savings/edit-savings-account/edit-savings-account.component.html b/src/app/savings/edit-savings-account/edit-savings-account.component.html index 3e7293cbd..f262b50d0 100644 --- a/src/app/savings/edit-savings-account/edit-savings-account.component.html +++ b/src/app/savings/edit-savings-account/edit-savings-account.component.html @@ -6,76 +6,140 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- + +
+ +
+

Modify Savings Account Application

+
+ Review and update the account details, currency settings and digital channels before resubmitting. +
+
+
+ + + - - - - - - - - - - - - - - - - - - - - - - {{ 'labels.inputs.DETAILS' | translate }} - - - - - - - {{ 'labels.inputs.TERMS' | translate }} - - - - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - @if (savingsAccountFormValidAndNotPristine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - + + + + Product + +
+ - - - } - + + +
+ + +
+
+
+ + + + + + Currency + +
+ @if (savingsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + Digital + +
+ @if (savingsAccountProductTemplate) { + + } + +
+ + + +
+
+
+ + + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ @if (savingsAccountProductTemplate) { + + + } + +
+ + + + +
+
+
+
diff --git a/src/app/savings/edit-savings-account/edit-savings-account.component.scss b/src/app/savings/edit-savings-account/edit-savings-account.component.scss index af9f4c075..4115661a8 100644 --- a/src/app/savings/edit-savings-account/edit-savings-account.component.scss +++ b/src/app/savings/edit-savings-account/edit-savings-account.component.scss @@ -5,3 +5,164 @@ * 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/. */ + +$navy: #0d1b2a; +$blue: #1e88e5; +$blue-d: #1e40af; +$indigo: #4f46e5; +$border: #e2e8f0; + +.csa-container { + max-width: 1180px; + margin: 0 auto; + padding: 20px 24px; +} + +/* ─── Hero header (matches Nexora pages) ────────────────────────────── */ +.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; + } +} + +/* ─── Wizard tab strip ──────────────────────────────────────────────── */ +.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.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; +} + +/* ─── Tab body + action bar ─────────────────────────────────────────── */ +.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; +} + +button.preview-submit-row__primary, +.mat-mdc-raised-button.mat-primary.preview-submit-row__primary, +.preview-submit-row__primary { + background: linear-gradient(135deg, #10b981 0%, #047857 100%) !important; + + --mdc-protected-button-container-color: transparent !important; + --mdc-filled-button-container-color: transparent !important; + + color: #fff !important; + box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important; +} diff --git a/src/app/savings/edit-savings-account/edit-savings-account.component.ts b/src/app/savings/edit-savings-account/edit-savings-account.component.ts index 99c596600..fcc0e0cd6 100644 --- a/src/app/savings/edit-savings-account/edit-savings-account.component.ts +++ b/src/app/savings/edit-savings-account/edit-savings-account.component.ts @@ -9,23 +9,26 @@ /** Angular Imports */ import { Component, ViewChild, inject } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { ClientsService } from 'app/clients/clients.service'; /** Custom Components */ import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component'; -import { SavingsAccountTermsStepComponent } from '../savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component'; -import { SavingsAccountChargesStepComponent } from '../savings-account-stepper/savings-account-charges-step/savings-account-charges-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'; /** Custom Services */ import { SavingsService } from '../savings.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { AlertService } from 'app/core/alert/alert.service'; +import { MatTabsModule } from '@angular/material/tabs'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { SavingsAccountPreviewStepComponent } from '../savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** - * Edit Savings Account Component + * Edit Savings Account Component — same tab UI as Create, pre-populated with existing data. */ @Component({ selector: 'mifosx-edit-savings-account', @@ -33,14 +36,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-savings-account.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, + MatTabsModule, FaIconComponent, - MatStep, - MatStepLabel, SavingsAccountDetailsStepComponent, - SavingsAccountTermsStepComponent, - SavingsAccountChargesStepComponent, + SavingsAccountCurrencyStepComponent, + SavingsAccountDigitalStepComponent, SavingsAccountPreviewStepComponent ] }) @@ -50,109 +50,135 @@ export class EditSavingsAccountComponent { private dateUtils = inject(Dates); private savingsService = inject(SavingsService); private settingsService = inject(SettingsService); + private alertService = inject(AlertService); + private clientsService = inject(ClientsService); - /** Savings Account Template */ + /** Full account data + template from resolver */ savingsAccountAndTemplate: any; - /** Savings Account Product Template */ + /** Product template emitted by details step */ savingsAccountProductTemplate: any; + /** Currently visible wizard tab (0-indexed). */ + selectedTabIndex = 0; + private readonly totalTabs = 4; - /** Savings Account Details Step */ @ViewChild(SavingsAccountDetailsStepComponent, { static: true }) savingsAccountDetailsStep: SavingsAccountDetailsStepComponent; - /** Savings Account Terms Step */ - @ViewChild(SavingsAccountTermsStepComponent, { static: true }) - savingsAccountTermsStep: SavingsAccountTermsStepComponent; - /** Savings Account Charges Step */ - @ViewChild(SavingsAccountChargesStepComponent, { static: true }) - savingsAccountChargesStep: SavingsAccountChargesStepComponent; - /** - * Fetches savings account template from `resolve` - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {Dates} dateUtils Date Utils - * @param {SavingsService} savingsService Savings Service - * @param {SettingsService} settingsService Settings Service - */ + @ViewChild(SavingsAccountCurrencyStepComponent) + savingsAccountCurrencyStep: SavingsAccountCurrencyStepComponent; + + @ViewChild(SavingsAccountDigitalStepComponent) + savingsAccountDigitalStep: SavingsAccountDigitalStepComponent; + constructor() { this.route.data.subscribe((data: { savingsAccountAndTemplate: any }) => { - this.savingsAccountAndTemplate = data.savingsAccountAndTemplate; + const account = data.savingsAccountAndTemplate; + this.savingsAccountAndTemplate = account; + if (account?.clientId && account?.accountNo) { + this.clientsService + .getClientNominees(String(account.clientId), account.accountNo, 'SB') + .subscribe((nominees: any[]) => { + if (Array.isArray(nominees) && nominees.length > 0) { + this.savingsAccountAndTemplate = { ...this.savingsAccountAndTemplate, nominees }; + } + }); + } }); } - /** - * Sets savings account product template. - * @param {any} $event API response - */ setTemplate($event: any) { this.savingsAccountProductTemplate = $event; } - /** - * Retrieves savings account details form. - */ + next(): void { + if (this.selectedTabIndex < this.totalTabs - 1) { + this.selectedTabIndex += 1; + } + } + + prev(): void { + if (this.selectedTabIndex > 0) { + this.selectedTabIndex -= 1; + } + } + get savingsAccountDetailsForm() { return this.savingsAccountDetailsStep.savingsAccountDetailsForm; } - /** - * Retrieves savings account terms form. - */ - get savingsAccountTermsForm() { - return this.savingsAccountTermsStep.savingsAccountTermsForm; + get savingsAccountTermsForm(): FormGroup { + return new FormGroup({}); } - /** - * Checks validity and pristinity of overall savings account form . - */ - get savingsAccountFormValidAndNotPristine() { - return ( - this.savingsAccountDetailsForm.valid && - this.savingsAccountTermsForm.valid && - (!this.savingsAccountDetailsForm.pristine || - !this.savingsAccountTermsForm.pristine || - !this.savingsAccountChargesStep.pristine) - ); - } - - /** - * Retrieves savings account object. - */ get savingsAccount() { - return { - ...this.savingsAccountDetailsStep.savingsAccountDetails, - ...this.savingsAccountTermsStep.savingsAccountTerms, - ...this.savingsAccountChargesStep.savingsAccountCharges - }; + return { ...this.savingsAccountDetailsStep.savingsAccountDetails }; + } + + get savingsAccountDigital() { + return this.savingsAccountDigitalStep?.savingsAccountDigital ?? null; } - /** - * Creates a new share account. - */ submit() { - // TODO: Update once language and date settings are setup + const detailsStep = this.savingsAccountDetailsStep; + if (detailsStep.selectedAccountStatus === 'JOINT' && detailsStep.totalJointSharePercentage !== 100) { + this.alertService.alert({ + type: 'Error', + message: `Total ownership is ${detailsStep.totalJointSharePercentage}%. It must equal 100% before submitting.` + }); + return; + } + if ( + this.savingsAccountProductTemplate?.nominationFacility && + (this.savingsAccountCurrencyStep?.nominees ?? []).length === 0 + ) { + this.alertService.alert({ + type: 'Error', + message: 'Nomination Facility is enabled for this product. Please add at least one nominee before submitting.' + }); + return; + } + if (this.savingsAccountProductTemplate?.introducerRequired && !this.savingsAccountDetailsStep.introducerId) { + this.alertService.alert({ + type: 'Error', + message: 'Introducer is required for this product. Please select an introducer before submitting.' + }); + return; + } const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const savingsAccount = { + const savingsAccount: any = { ...this.savingsAccount, - charges: this.savingsAccount.charges.map((charge: any) => ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: this.dateUtils.formatDate(charge.dueDate, dateFormat), - feeOnMonthDay: this.dateUtils.formatDate(charge.feeOnMonthDay, dateFormat), - feeInterval: charge.feeInterval - })), + charges: [], submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat), dateFormat, monthDayFormat, locale }; + + if (this.savingsAccountDigital?.accountFacilityConfig) { + savingsAccount.accountFacilityConfig = this.savingsAccountDigital.accountFacilityConfig; + } + if (this.savingsAccountAndTemplate.clientId) { savingsAccount.clientId = this.savingsAccountAndTemplate.clientId; } else { savingsAccount.groupId = this.savingsAccountAndTemplate.groupId; } + + const nominees = this.savingsAccountCurrencyStep?.nominees ?? []; + if (nominees.length > 0) { + savingsAccount.nominees = nominees.map((n: any) => ({ + nomineeClientId: n.nomineeClientId, + nominationType: n.nominationType, + relationshipId: n.relationshipId, + percentageShare: n.percentageShare, + nomineeIsMinor: n.nomineeIsMinor, + ...(n.guardianClientId ? { guardianClientId: n.guardianClientId } : {}), + remarks: n.remarks + })); + } + this.savingsService .updateSavingsAccount(this.savingsAccountAndTemplate.id, savingsAccount) .subscribe((response: any) => { diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts index 8ba58f1a6..1a08f026d 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, ViewChild } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; @@ -21,9 +21,10 @@ import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dor DormancyAlertPanelComponent ] }) -export class SavingsAccountCurrencyStepComponent { +export class SavingsAccountCurrencyStepComponent implements OnChanges { @Input() savingsAccountProductTemplate: any; @Input() primaryClientId: number | null = null; + @Input() existingAccount: any = null; @ViewChild(NomineeFormComponent) nomineeFormComp: NomineeFormComponent; nominees: any[] = []; @@ -31,6 +32,32 @@ export class SavingsAccountCurrencyStepComponent { showNomineeForm = false; nomineeError = ''; + private existingAccountApplied = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['existingAccount']?.currentValue) { + const ea = changes['existingAccount'].currentValue; + this.existingAccountApplied = true; + const nominees = + Array.isArray(ea.nominees) && ea.nominees.length > 0 ? ea.nominees : this.parseNomineeData(ea.nomineeData); + if (nominees.length > 0) { + this.nominees = [...nominees]; + } + } + } + + private parseNomineeData(nomineeData: any): any[] { + if (!nomineeData) { + return []; + } + try { + const parsed = typeof nomineeData === 'string' ? JSON.parse(nomineeData) : nomineeData; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + get totalShare(): number { return this.nominees.reduce((sum, n) => sum + (+n.percentageShare || 0), 0); } 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 89fa66639..0e2834bc4 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 @@ -91,7 +91,7 @@ Account No @@ -127,6 +127,9 @@ @if (productTemplate?.introducerRequired) { } 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 55fb52cce..f9fb23339 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 @@ -65,6 +65,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit { introducerId: number | null = null; introducerName: string | null = null; introducerCif: string | null = null; + private isEditMode = false; @Output() savingsAccountProductTemplate = new EventEmitter(); @@ -166,11 +167,25 @@ export class SavingsAccountDetailsStepComponent implements OnInit { this.buildDependencies(); if (this.savingsAccountTemplate) { this.productData = this.savingsAccountTemplate.productOptions; - if (this.savingsAccountTemplate.savingsProductId) { + const existingProductId = this.savingsAccountTemplate.savingsProductId ?? this.savingsAccountTemplate.productId; + // Edit mode: account response always has accountNo + if (this.savingsAccountTemplate.accountNo) { + this.isEditMode = true; + } + if ((!this.productData || this.productData.length === 0) && existingProductId) { + this.isEditMode = true; + this.productData = [ + { + id: existingProductId, + name: this.savingsAccountTemplate.productName ?? this.savingsAccountTemplate.savingsProductName ?? 'Product' + } + ]; + } + if (existingProductId) { this.savingsAccountDetailsForm.patchValue({ - productId: this.savingsAccountTemplate.savingsProductId, + productId: existingProductId, submittedOnDate: - this.savingsAccountTemplate.timeline.submittedOnDate && + this.savingsAccountTemplate.timeline?.submittedOnDate && new Date(this.savingsAccountTemplate.timeline.submittedOnDate) }); } else { @@ -280,6 +295,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit { /** Auto-set status tile based on age; pre-fill minorDob if minor */ private autoDetectAccountStatus(): void { + if (this.isEditMode) { + return; + } if (this.clientAge === null) return; if (this.clientAge < 18) { @@ -319,8 +337,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit { this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment); this.selectedAccountType = response.accountType ?? null; this.selectedCustomerSegment = response.customerSegment ?? null; - // Override the product's default segment with the one inferred - // from the client's entity type when an option matches. this.applyInferredCustomerSegment(); this.savingsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; @@ -332,10 +348,93 @@ export class SavingsAccountDetailsStepComponent implements OnInit { } else { this.savingsAccountDetailsForm.get('fieldOfficerId').patchValue(''); } + // === Restore existing account values when in edit/modify mode === + this.restoreExistingAccountValues(); }); }); } + private restoreExistingAccountValues(): void { + const ea = this.savingsAccountTemplate; + if (!ea?.savingsProductId && !ea?.productId) { + return; + } + + const strVal = (v: any): string => (typeof v === 'object' && v !== null ? (v.value ?? '') : (v ?? '')); + + const accountStatus = strVal(ea.accountStatus); + if (accountStatus) { + this.selectedAccountStatus = accountStatus; + } + + const customerSegment = strVal(ea.customerSegment); + if (customerSegment) { + this.selectedCustomerSegment = customerSegment; + } + + const accountType = ea.cbsAccountType ? strVal(ea.cbsAccountType) : null; + if (accountType) { + this.selectedAccountType = accountType; + } + + if (accountStatus === 'JOINT') { + const modeOfOp = strVal(ea.modeOfOperation); + if (modeOfOp) { + this.modeOfOperation = modeOfOp; + } + if (Array.isArray(ea.jointHolders) && ea.jointHolders.length > 0) { + const isPH = (h: any) => h.isPrimary ?? h.isprimary ?? false; + const primary = ea.jointHolders.find((h: any) => isPH(h)); + const ps = primary?.sharePercentage ?? primary?.sharepercentage; + if (ps) { + this.primaryHolderShare = +ps; + } + this.jointHolders = ea.jointHolders + .filter((h: any) => !isPH(h)) + .map((h: any) => ({ + clientId: h.clientId ?? h.clientid, + clientName: h.displayName ?? h.displayname ?? h.clientName ?? '', + clientCif: h.cifNumber ?? h.cifnumber ?? h.clientCif ?? '', + sharePercentage: +(h.sharePercentage ?? h.sharepercentage ?? 0), + isMandatory: !!(h.isMandatory ?? h.ismandatory) + })); + } + } + if (accountStatus === 'INDIVIDUAL_MALE' && ea.specialCategory) { + this.selectedSpecialCategory = ea.specialCategory; + } + if (accountStatus === 'INDIVIDUAL_FEMALE' && ea.weakerSection) { + this.selectedWeakerSection = ea.weakerSection; + } + if (accountStatus === 'MINOR') { + if (ea.guardianClientId) { + this.guardianClientId = ea.guardianClientId; + } + if (ea.guardianName) { + this.guardianName = ea.guardianName; + } + if (ea.guardianCifNumber) { + this.guardianCif = ea.guardianCifNumber; + } + if (ea.guardianRelationship) { + this.guardianRelationship = ea.guardianRelationship; + } + if (ea.minorDateOfBirth) { + const d = new Date(ea.minorDateOfBirth); + if (!isNaN(d.getTime())) { + this.minorDob = d; + } + } + } + const introId = ea.introducerId ?? ea.introducerClientId; + if (introId) { + this.introducerId = introId; + this.introducerName = ea.introducerName ?? null; + this.introducerCif = ea.introducerCifNumber ?? null; + } + this.cdr.detectChanges(); + } + private extractOptions(options: any[], fallback?: string): string[] { if (Array.isArray(options) && options.length > 0) { return options.map((o: any) => o.value ?? o.name ?? o); diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts index 5c747b548..30da85e32 100644 --- a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts @@ -40,10 +40,19 @@ const DEFAULT_FACILITY_CONFIG = { }) export class SavingsAccountDigitalStepComponent { facilityConfig = { ...DEFAULT_FACILITY_CONFIG }; + private hasExistingAccount = false; + + @Input() + set existingAccount(value: any) { + if (value?.accountFacilityConfig) { + this.hasExistingAccount = true; + this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig }; + } + } @Input() set savingsAccountProductTemplate(value: any) { - if (value?.accountFacilityConfig) { + if (!this.hasExistingAccount && value?.accountFacilityConfig) { this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig }; } } 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 a9be208f1..ffee9dea1 100644 --- a/src/app/savings/savings-account-view/savings-buttons.config.ts +++ b/src/app/savings/savings-account-view/savings-buttons.config.ts @@ -142,6 +142,16 @@ export class SavingsButtonsConfiguration { } ]; break; + case 'Rejected': + case 'rejected': + this.buttonsArray = [ + { + name: 'Modify Application', + icon: 'pen', + taskPermissionName: 'UPDATE_SAVINGSACCOUNT' + } + ]; + break; default: this.buttonsArray = []; } diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts index 8f46fa2b0..467502a06 100644 --- a/src/app/shared/introducer-search/introducer-search.component.ts +++ b/src/app/shared/introducer-search/introducer-search.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input, Output, EventEmitter, inject } from '@angular/core'; +import { Component, Input, Output, EventEmitter, inject, OnChanges, SimpleChanges } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { ClientsService } from 'app/clients/clients.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -27,11 +27,12 @@ import { FormsModule } from '@angular/forms'; MatIcon ] }) -export class IntroducerSearchComponent { +export class IntroducerSearchComponent implements OnChanges { private clientsService = inject(ClientsService); private sanitizer = inject(DomSanitizer); @Input() excludedClientIds: number[] = []; + @Input() preselectedIntroducer: { clientId: number; displayName: string; cifNumber?: string } | null = null; @Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string; cifNumber?: string } | null>(); entryMode: 'linked' = 'linked'; @@ -50,6 +51,14 @@ export class IntroducerSearchComponent { private searchTimer: any; + ngOnChanges(changes: SimpleChanges): void { + if (changes['preselectedIntroducer']?.currentValue && !this.selectedClient) { + const val = changes['preselectedIntroducer'].currentValue; + this.selectedClient = { id: val.clientId, displayName: val.displayName, cifNumber: val.cifNumber ?? '' }; + this.fetchLinkedClientData(String(val.clientId)); + } + } + onSearch(text: string): void { this.searchText = text; clearTimeout(this.searchTimer); diff --git a/src/environments/.env.ts b/src/environments/.env.ts index b69dd8cf8..9d0b77b96 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260527', - 'hash': '33b01d66' + 'version': '260602', + 'hash': '3be2f962' }, 'allow_switching_backend_instance': true };