feat(accounts): add Modify Application for rejected Current/Savings/FD/RD accounts

- Add 'Modify Application' button in kebab menu for rejected accounts (all 4 types)
- Rewrite edit components with tab-based UI matching create form
- Pre-populate all create-time values in edit form: product, account status,
  customer segment, account type, joint holders, nominees, introducer,
  deposit account link, senior citizen rate, tenure, flags (msme/balanceSwap/autoDebit)
- Fix edit mode detection using accountNo presence
- Add preselectedIntroducer @Input to IntroducerSearchComponent for pre-fill
- Support both camelCase and lowercase keys for joint holders (MySQL queryForList)
- Fix account number display: actual accountNo before previewAccountNo
- Fix cbsAccountType restore: cbsAccountType only, avoid Fineract enum object
- Add nominees fetch via /clients/{id}/nominees API in all 4 edit components
- Fix RD/FD Interest Rate Chart: SimpleChanges to prevent reset race condition
- Pre-check Senior Citizen checkbox in edit form from isSeniorCitizen DB field
- Auto-calculate rate when tenure pre-filled in edit mode
- FD calculator: use backend calculate-fd-interest API for all compounding types
- Support interestCompoundingPeriodInMonths=0 (daily) in FD calculator API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-03 10:36:40 +05:30
co-authored by Claude Sonnet 4.6
parent 3be2f96210
commit 260fabaa48
37 changed files with 2744 additions and 730 deletions
@@ -6,103 +6,157 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-stepper
orientation="vertical"
[linear]="false"
class="mat-elevation-z8"
labelPosition="bottom"
#recurringDepositAccountStepper
<div class="cra-container">
<!-- Hero Header -->
<div class="cra-header">
<fa-icon icon="university" class="cra-header__icon"></fa-icon>
<div>
<h2 class="cra-header__title">Modify Recurring Deposit Account Application</h2>
<div class="cra-header__sub">
Review and update the account details, currency and interest terms before resubmitting.
</div>
</div>
</div>
<!-- Wizard tabs -->
<mat-tab-group
class="cra-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<ng-template matStepperIcon="number">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="done">
<fa-icon icon="check" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="error">
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
</ng-template>
<ng-template matStepperIcon="preview">
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="recurringDepositAccountDetailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-recurring-deposits-account-details-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
(recurringDepositsAccountProductTemplate)="setTemplate($event)"
>
</mifosx-recurring-deposits-account-details-step>
</mat-step>
<mat-step [stepControl]="recurringDepositAccountTermsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-recurring-deposits-account-terms-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
>
</mifosx-recurring-deposits-account-terms-step>
</mat-step>
<mat-step [stepControl]="recurringDepositAccountSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-recurring-deposits-account-settings-step
[isNew]="false"
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
>
</mifosx-recurring-deposits-account-settings-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[submittedOnDate]="submittedOnDate"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-recurring-deposits-account-charges-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountFormValid]="recurringDepositAccountFormValidAndNotPristine"
>
</mifosx-recurring-deposits-account-charges-step>
</mat-step>
@if (recurringDepositAccountFormValidAndNotPristine) {
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-recurring-deposits-account-preview-step
<!-- Tab 1: Product Classification + Details -->
<mat-tab>
<ng-template mat-tab-label>
<fa-icon icon="check-square" class="cra-tab__icon"></fa-icon>
Product
</ng-template>
<div class="cra-tab-body">
<mifosx-recurring-deposits-account-details-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData"
[rdTenureDays]="rdTenureDays"
[rdApplicableSlab]="rdApplicableSlab"
[rdMaturityAmount]="rdMaturityAmount"
[rdInterestEarned]="rdInterestEarned"
[rdDaysInYear]="rdDaysInYear"
(submitEvent)="submit()"
(recurringDepositsAccountProductTemplate)="setTemplate($event)"
>
</mifosx-recurring-deposits-account-preview-step>
</mat-step>
}
</mat-stepper>
</mifosx-recurring-deposits-account-details-step>
<div class="cra-actions">
<span class="cra-actions__spacer"></span>
<button
mat-raised-button
color="primary"
[disabled]="!recurringDepositsAccountProductTemplate"
(click)="next()"
>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 2: Currency -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="dollar-sign" class="cra-tab__icon"></fa-icon>
Currency
</ng-template>
<div class="cra-tab-body">
@if (recurringDepositsAccountProductTemplate) {
<mifosx-recurring-deposits-account-currency-step
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[primaryClientId]="recurringDepositsAccountAndTemplate?.clientId ?? null"
[existingAccount]="recurringDepositsAccountAndTemplate"
></mifosx-recurring-deposits-account-currency-step>
}
<div class="cra-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cra-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 3: Interest Rate Chart -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="university" class="cra-tab__icon"></fa-icon>
Interest Rate Chart
</ng-template>
<div class="cra-tab-body">
@if (recurringDepositsAccountProductTemplate) {
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[submittedOnDate]="submittedOnDate"
[cbsBaseRates]="cbsBaseRates"
[initialTenure]="initialTenure"
(tenureSelected)="handleTenureSelected($event)"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
}
<div class="cra-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cra-actions__spacer"></span>
<button mat-raised-button color="primary" (click)="next()">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
</mat-tab>
<!-- Tab 4: Preview -->
<mat-tab [disabled]="!recurringDepositsAccountProductTemplate">
<ng-template mat-tab-label>
<fa-icon icon="eye" class="cra-tab__icon"></fa-icon>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="cra-tab-body">
@if (recurringDepositsAccountProductTemplate) {
<mifosx-recurring-deposits-account-preview-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData"
[rdNominees]="recurringDepositsAccountCurrencyStep?.nominees ?? []"
[rdCurrencyFlags]="rdCurrencyFlags"
[rdTenureDays]="rdTenureDays"
[rdApplicableSlab]="rdApplicableSlab"
[rdMaturityAmount]="rdMaturityAmount"
[rdInterestEarned]="rdInterestEarned"
[rdDaysInYear]="rdDaysInYear"
[rdAppliedRate]="rdAppliedRate"
[rdIsSeniorCitizen]="rdIsSeniorCitizen"
[hideButtons]="true"
(submitEvent)="submit()"
>
</mifosx-recurring-deposits-account-preview-step>
}
<div class="cra-actions">
<button mat-stroked-button (click)="prev()">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<span class="cra-actions__spacer"></span>
<button mat-stroked-button [routerLink]="['../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" class="preview-submit-row__primary" (click)="submit()">
<fa-icon icon="check" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Submit' | translate }}
</button>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
@@ -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;
}
@@ -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) => {
@@ -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;
}
@@ -95,7 +95,7 @@
<input
matInput
[value]="
productTemplate?.previewAccountNo || recurringDepositsAccountTemplate?.accountNo || 'Auto-generated'
recurringDepositsAccountTemplate?.accountNo || productTemplate?.previewAccountNo || 'Auto-generated'
"
readonly
/>
@@ -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 !== '';
}
@@ -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;
@@ -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 = [];
}