rd account and rd product code update

This commit is contained in:
Nitesh Yadav
2026-05-08 13:51:58 +05:30
parent 19c0e98e96
commit 9e85872eac
35 changed files with 2247 additions and 591 deletions
@@ -108,7 +108,7 @@ export class CkycReportsComponent implements OnInit, AfterViewInit {
ngOnInit(): void {
this.parseAndStore(SAMPLE_CKYC_FILE);
// eslint-disable-next-line no-console
console.log(
'[CKYC] Sample loaded —',
this.customers.length,
@@ -74,7 +74,8 @@
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates"
[submittedOnDate]="submittedOnDate"
(tenureSelected)="handleTenureSelected($event)"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
@@ -97,11 +98,13 @@
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData"
[rdInterestRates]="rdInterestRates"
[rdNominee]="rdNominee"
[rdCurrencyFlags]="rdCurrencyFlags"
[rdTenureDays]="rdTenureDays"
[rdApplicableSlab]="rdApplicableSlab"
[rdMaturityAmount]="rdMaturityAmount"
[rdInterestEarned]="rdInterestEarned"
[rdDaysInYear]="rdDaysInYear"
(submitEvent)="submit()"
>
</mifosx-recurring-deposits-account-preview-step>
@@ -75,8 +75,10 @@ export class CreateRecurringDepositsAccountComponent {
recurringDepositsAccountTemplate: any;
/** Recurring Deposit Account Product Template */
recurringDepositsAccountProductTemplate: any;
/** Active CBS base interest rate chart */
rdInterestRates: any[] = [];
/** Selected tenure value and frequency from calculator */
selectedTenureValue: number | null = null;
selectedTenureFrequencyId: number | null = null;
selectedDepositAmount: number | null = null;
constructor() {
this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => {
@@ -90,14 +92,13 @@ export class CreateRecurringDepositsAccountComponent {
*/
setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event;
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
}
/** Captures tenure selection from calculator */
handleTenureSelected($event: any) {
this.selectedTenureValue = $event.depositPeriod;
this.selectedTenureFrequencyId = $event.depositPeriodFrequencyId;
this.selectedDepositAmount = $event.depositAmount;
}
/** Get Recurring Deposit Account Details Form Data */
@@ -123,17 +124,31 @@ export class CreateRecurringDepositsAccountComponent {
}
get rdTenureDays(): number | null {
return this.recurringDepositsAccountChartStep?.tenureDays ?? null;
return this.recurringDepositsAccountChartStep?.matchResult?.tenureDays ?? null;
}
get rdApplicableSlab(): any | null {
return this.recurringDepositsAccountChartStep?.applicableSlab ?? null;
return this.recurringDepositsAccountChartStep?.matchResult?.slab ?? null;
}
get rdMaturityAmount(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.maturityAmount ?? null;
}
get rdInterestEarned(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.interestEarned ?? null;
}
get rdDaysInYear(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.daysInYear ?? null;
}
get submittedOnDate(): Date | null {
return this.recurringDepositAccountDetailsForm?.value?.submittedOnDate ?? null;
}
get rdCurrencyFlags() {
return {
balanceSwapOption: this.recurringDepositsAccountCurrencyStep?.balanceSwapOption ?? false,
msme: this.recurringDepositsAccountCurrencyStep?.msme ?? false,
autoDebitFacility: this.recurringDepositsAccountCurrencyStep?.autoDebitFacility ?? false
};
}
@@ -145,34 +160,93 @@ export class CreateRecurringDepositsAccountComponent {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM';
const pt = this.recurringDepositsAccountProductTemplate;
const details = this.recurringDepositAccount;
const currencyStep = this.recurringDepositsAccountCurrencyStep;
// Terms — pulled from product template
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;
// Settings — pulled from product template
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;
// Flags and other product defaults
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
};
// Charges from product template
const rawCharges: any[] = pt?.charges ?? [];
const recurringDepositAccount: any = {
...details,
...terms,
...settings,
...productDefaults,
clientId: this.recurringDepositsAccountTemplate.clientId,
charges: [],
isCalendarInherited: false,
mandatoryRecommendedDepositAmount: 0,
recurringFrequency: 1,
recurringFrequencyType: 2,
mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0,
recurringFrequency: pt?.recurringFrequency ?? 1,
recurringFrequencyType: pt?.recurringFrequencyType ?? 2, // Monthly
submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat),
expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat),
dateFormat,
monthDayFormat,
locale,
// Currency step overrides
balanceSwapOption: currencyStep?.balanceSwapOption ?? details.balanceSwapOption ?? false,
msme: currencyStep?.msme ?? details.msme ?? false,
autoDebitFacility: currencyStep?.autoDebitFacility ?? details.autoDebitFacility ?? false
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
}))
};
// Tenure from interest rate chart step (depositPeriod in days)
const tenureDays = this.rdTenureDays;
if (tenureDays && tenureDays > 0) {
recurringDepositAccount.depositPeriod = tenureDays;
recurringDepositAccount.depositPeriodFrequencyId = 0; // 0 = Days
// Tenure override from interest rate chart calculator
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; // Days
}
// Amount override from calculator if provided
if (this.selectedDepositAmount) {
recurringDepositAccount.mandatoryRecommendedDepositAmount = this.selectedDepositAmount;
}
// Nominee from currency step
@@ -71,7 +71,7 @@
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates"
[submittedOnDate]="submittedOnDate"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
</mat-step>
@@ -94,7 +94,11 @@
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData"
[rdInterestRates]="rdInterestRates"
[rdTenureDays]="rdTenureDays"
[rdApplicableSlab]="rdApplicableSlab"
[rdMaturityAmount]="rdMaturityAmount"
[rdInterestEarned]="rdInterestEarned"
[rdDaysInYear]="rdDaysInYear"
(submitEvent)="submit()"
>
</mifosx-recurring-deposits-account-preview-step>
@@ -67,12 +67,37 @@ export class EditRecurringDepositAccountComponent {
@ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true })
recurringDepositAccountChargesStep: RecurringDepositsAccountChargesStepComponent;
@ViewChild(RecurringDepositsAccountInterestRateChartStepComponent)
recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent;
get rdTenureDays(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.tenureDays ?? null;
}
get rdApplicableSlab(): any | null {
return this.recurringDepositsAccountChartStep?.matchResult?.slab ?? null;
}
get rdMaturityAmount(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.maturityAmount ?? null;
}
get rdInterestEarned(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.interestEarned ?? null;
}
get rdDaysInYear(): number | null {
return this.recurringDepositsAccountChartStep?.matchResult?.daysInYear ?? null;
}
get submittedOnDate(): Date | null {
return this.recurringDepositAccountDetailsForm?.value?.submittedOnDate ?? null;
}
/** Recurring Deposits Account And Template */
recurringDepositsAccountAndTemplate: any;
/** Recurring Deposit Account Product Template */
recurringDepositsAccountProductTemplate: any;
/** Active CBS base interest rate chart */
rdInterestRates: any[] = [];
constructor() {
this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => {
@@ -86,14 +111,6 @@ export class EditRecurringDepositAccountComponent {
*/
setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event;
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
}
/** Get Recurring Deposit Account Details Form Data */
@@ -169,6 +186,15 @@ export class EditRecurringDepositAccountComponent {
this.recurringDepositAccount.expectedFirstDepositOnDate,
dateFormat
),
charts: [
{
chartSlabs:
this.recurringDepositsAccountProductTemplate?.accountChart?.chartSlabs ??
this.recurringDepositsAccountProductTemplate?.activeChart?.chartSlabs ??
this.recurringDepositsAccountProductTemplate?.interestRateCharts?.[0]?.chartSlabs ??
[]
}
],
dateFormat,
monthDayFormat,
locale
@@ -27,22 +27,6 @@
</mat-form-field>
</div>
<div class="racc-alert racc-alert--editable">
<div class="racc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-slide-toggle [(ngModel)]="balanceSwapOption" color="primary"></mat-slide-toggle>
</div>
<div class="racc-alert racc-alert--editable">
<div class="racc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises — mark this account under MSME category.</p>
</div>
<mat-slide-toggle [(ngModel)]="msme" color="primary"></mat-slide-toggle>
</div>
<div class="racc-alert racc-alert--editable">
<div class="racc-alert__content">
<h4>Auto Debit Facility</h4>
@@ -31,15 +31,11 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges
nomineeData: any = null;
// Editable feature toggles (pre-filled from product, overridable by user)
balanceSwapOption = false;
msme = false;
autoDebitFacility = false;
ngOnChanges(changes: SimpleChanges): void {
if (changes['recurringDepositsAccountProductTemplate']?.currentValue) {
const pt = changes['recurringDepositsAccountProductTemplate'].currentValue;
this.balanceSwapOption = !!pt.balanceSwapOption;
this.msme = !!pt.msme;
this.autoDebitFacility = !!pt.autoDebitFacility;
}
}
@@ -51,8 +47,6 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges
/** Called by parent to collect currency step values */
get currencyStepData() {
return {
balanceSwapOption: this.balanceSwapOption,
msme: this.msme,
autoDebitFacility: this.autoDebitFacility,
...(this.nomineeData ? { nominee: this.nomineeData } : {})
};
@@ -338,8 +338,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
],
fieldOfficerId: [''],
externalId: [''],
balanceSwapOption: [false],
msme: [false],
autoDebitFacility: [false]
});
}
@@ -375,8 +373,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.loadClientAccounts();
}
this.recurringDepositAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
autoDebitFacility: !!response.autoDebitFacility
});
this.cdr.detectChanges();
@@ -6,47 +6,359 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-column gap-2px">
<!-- ── Tenure Calculator ───────────────────────────────────────── -->
@if (_activeRdVersion) {
<div class="tenure-calc">
<div class="tenure-calc__header">
<i class="fas fa-calculator"></i>
<span>Tenure &amp; Interest Calculator</span>
</div>
<div class="tenure-calc__body">
<div class="tenure-calc__input-row">
<mat-form-field appearance="outline" class="tenure-calc__field">
<mat-label>Enter Tenure (Days)</mat-label>
<input matInput type="number" [(ngModel)]="tenureDays" min="1" placeholder="e.g. 365" />
<mat-hint>Enter the number of days for your RD</mat-hint>
</mat-form-field>
<div class="fdrc-wrapper">
<!-- ══ Chart Header Card ════════════════════════════════════════════════ -->
@if (interestRateChartData.length > 0) {
<div class="fdrc-header-card">
<div class="fdrc-header-card__left">
<div class="fdrc-header-card__icon">
<i class="fas fa-chart-line"></i>
</div>
@if (tenureDays && tenureDays > 0) {
@if (applicableSlab) {
<div class="tenure-calc__result">
<div class="tenure-calc__result-row">
<span class="tenure-calc__result-label">Tenure</span>
<span class="tenure-calc__result-value">{{ tenureDays }} Days</span>
</div>
<div class="tenure-calc__result-row">
<span class="tenure-calc__result-label">Applicable Slab</span>
<span class="tenure-calc__result-value">{{ applicableSlab.description }}</span>
</div>
<div class="tenure-calc__result-row tenure-calc__result-row--highlight">
<span class="tenure-calc__result-label">Annual Interest Rate</span>
<span class="tenure-calc__result-rate">{{ applicableSlab.annualInterestRate | number: '1.2-2' }}%</span>
</div>
</div>
} @else {
<div class="tenure-calc__no-match">
<i class="fas fa-exclamation-circle"></i>
No matching slab found for {{ tenureDays }} days
</div>
<div>
<div class="fdrc-header-card__title">{{ chartName || 'RD Interest Rate Chart' }}</div>
@if (chartDescription) {
<div class="fdrc-header-card__desc">{{ chartDescription }}</div>
}
</div>
</div>
<div class="fdrc-header-card__meta">
<div class="fdrc-header-card__dates">
<span class="fdrc-meta-item">
<i class="fas fa-calendar-check"></i>
From: <strong>{{ chartFromDate | dateFormat }}</strong>
</span>
@if (chartEndDate) {
<span class="fdrc-meta-item">
<i class="fas fa-calendar-times"></i>
To: <strong>{{ chartEndDate | dateFormat }}</strong>
</span>
}
</div>
@if (isPrimaryGroupingByAmount) {
<span class="fdrc-pill fdrc-pill--amount"> <i class="fas fa-layer-group"></i> Grouped by Amount </span>
}
</div>
</div>
}
<!-- ══ Tenure Calculator ════════════════════════════════════════════════ -->
<div class="fdrc-calc-card">
<div class="fdrc-calc-card__header">
<div class="fdrc-calc-card__header-left">
<div class="fdrc-calc-icon">
<i class="fas fa-calculator"></i>
</div>
<div>
<span class="fdrc-calc-card__title">RD Rate Calculator</span>
<span class="fdrc-calc-card__sub">Enter your RD tenure to find the applicable interest rate</span>
</div>
</div>
</div>
<div class="fdrc-calc-card__body">
<div class="fdrc-calc-inputs">
<!-- Tenure Value -->
<div class="fdrc-calc-field">
<label class="fdrc-calc-label">Deposit Tenure</label>
<div class="fdrc-tenure-row">
<input
type="number"
class="fdrc-tenure-input"
[(ngModel)]="tenureValue"
[min]="1"
placeholder="Enter value"
(keyup.enter)="calculateRate()"
/>
<!-- Period Unit Selector -->
<div class="fdrc-period-tabs">
@for (unit of allowedPeriodUnits; track unit) {
<button
type="button"
class="fdrc-period-tab"
[class.fdrc-period-tab--active]="tenurePeriodUnit === unit"
(click)="tenurePeriodUnit = unit; onPeriodUnitChange()"
>
{{ unit }}
</button>
}
</div>
</div>
</div>
<!-- Principal Amount (Monthly Installment) -->
<div class="fdrc-calc-field">
<label class="fdrc-calc-label">
Monthly Installment
<span class="fdrc-calc-optional">(optional — for maturity estimation)</span>
</label>
<div class="fdrc-amount-row">
<span class="fdrc-amount-prefix">₹</span>
<input
type="number"
class="fdrc-tenure-input fdrc-tenure-input--amount"
[(ngModel)]="principalAmount"
[min]="1"
placeholder="e.g. 5000"
/>
</div>
</div>
<div class="fdrc-calc-actions">
<button type="button" class="fdrc-calc-btn" (click)="calculateRate()">
<i class="fas fa-search"></i> Find Rate
</button>
@if (tenureValue || matchResult) {
<button type="button" class="fdrc-calc-reset" (click)="resetCalculator()">
<i class="fas fa-times"></i> Reset
</button>
}
</div>
</div>
<!-- Validation Warning -->
@if (validationWarning) {
<div class="fdrc-alert fdrc-alert--warn">
<i class="fas fa-exclamation-triangle"></i>
<span>{{ validationWarning }}</span>
</div>
}
<!-- Match Result -->
@if (matchResult) {
<div class="fdrc-result-card">
<div class="fdrc-result-card__check">
<i class="fas fa-check-circle"></i>
</div>
<div class="fdrc-result-card__body">
<!-- Selected Tenure Banner -->
<div class="fdrc-tenure-banner">
<i class="fas fa-calendar-check"></i>
<div class="fdrc-tenure-banner__content">
<span class="fdrc-tenure-banner__label">Your Selected RD Tenure</span>
<span class="fdrc-tenure-banner__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
</div>
<span class="fdrc-tenure-banner__saved"> <i class="fas fa-save"></i> Will be saved </span>
</div>
<div class="fdrc-result-rate">
<span class="fdrc-result-rate__value">{{ matchResult.appliedRate | number: '1.2-2' }}%</span>
<span class="fdrc-result-rate__label">Annual Interest Rate</span>
</div>
<div class="fdrc-result-meta">
<div class="fdrc-result-meta__item">
<i class="fas fa-layer-group"></i>
<span
>Applicable Slab:
<strong>
{{ matchResult.slab.fromPeriod }}–{{ matchResult.slab.toPeriod ?? '∞' }}
{{ matchResult.slab.periodType?.value }}
</strong>
</span>
</div>
@if (matchResult.slab.description) {
<div class="fdrc-result-meta__item">
<i class="fas fa-tag"></i>
<span>{{ matchResult.slab.description }}</span>
</div>
}
</div>
<!-- Maturity Summary -->
@if (matchResult.maturityAmount !== null) {
<div class="fdrc-maturity-card">
<div class="fdrc-maturity-card__header">
<i class="fas fa-coins"></i>
<span>RD Maturity Summary (Estimate)</span>
</div>
<div class="fdrc-maturity-card__body">
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Monthly Installment</span>
<span class="fdrc-maturity-row__value">₹ {{ principalAmount | number: '1.2-2' }}</span>
</div>
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Tenure</span>
<span class="fdrc-maturity-row__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
</div>
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Interest Rate</span>
<span class="fdrc-maturity-row__value">{{ matchResult.appliedRate | number: '1.2-2' }}% p.a.</span>
</div>
<div class="fdrc-maturity-row fdrc-maturity-row--interest">
<span class="fdrc-maturity-row__label">Estimated Interest Earned</span>
<span class="fdrc-maturity-row__value">₹ {{ matchResult.interestEarned | number: '1.2-2' }}</span>
</div>
<div class="fdrc-maturity-row fdrc-maturity-row--total">
<span class="fdrc-maturity-row__label">Estimated Maturity Amount</span>
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--big">
₹ {{ matchResult.maturityAmount | number: '1.2-2' }}
</span>
</div>
</div>
</div>
}
@if (!matchResult.maturityAmount) {
<div class="fdrc-payout-hint">
<i class="fas fa-lightbulb"></i>
Enter a monthly installment amount above to see the maturity calculation.
</div>
}
</div>
</div>
}
</div>
</div>
<!-- ══ Rate Chart Table ══════════════════════════════════════════════════ -->
<div class="fdrc-table-card">
<div class="fdrc-table-card__header">
<div class="fdrc-table-card__title-row">
<i class="fas fa-table"></i>
<span class="fdrc-table-card__title">Interest Rate Slabs</span>
@if (interestRateChartData.length) {
<span class="fdrc-slab-count"
>{{ interestRateChartData.length }} slab{{ interestRateChartData.length !== 1 ? 's' : '' }}</span
>
}
</div>
@if (matchResult) {
<span class="fdrc-highlight-note"> <i class="fas fa-highlighter"></i> Matched slab is highlighted </span>
}
</div>
<div class="fdrc-table-wrap">
<table mat-table [dataSource]="interestRateChartData" #chartsTable multiTemplateDataRows class="fdrc-table">
<!-- Period Column -->
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Period</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td fdrc-td--period">
<span class="fdrc-period-range">
{{ slab.fromPeriod }}
@if (slab.toPeriod !== null) {
– {{ slab.toPeriod }}
} @else {
+
}
</span>
<span class="fdrc-period-type">{{ slab.periodType?.value }}</span>
</td>
</ng-container>
<!-- Amount Range Column -->
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Amount Range</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
@if (slab.amountRangeFrom || slab.amountRangeTo) {
<span>{{ slab.amountRangeFrom | number }} – {{ slab.amountRangeTo | number }}</span>
} @else {
<span class="fdrc-td--na">—</span>
}
</td>
</ng-container>
<!-- Interest Column -->
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Interest Rate</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
<span class="fdrc-rate-badge">{{ slab.annualInterestRate | number: '1.2-2' }}%</span>
</td>
</ng-container>
<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Description</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
{{ slab.description || '—' }}
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Incentives</th>
<td mat-cell *matCellDef="let slab; let i = dataIndex" class="fdrc-td">
@if (slab.incentives?.length) {
<button
type="button"
class="fdrc-incentive-btn"
(click)="expandChartSlabIndex = expandChartSlabIndex === i ? null : i"
>
<fa-icon [icon]="expandChartSlabIndex !== i ? 'eye' : 'eye-slash'"></fa-icon>
{{ expandChartSlabIndex === i ? 'Hide' : 'View' }} ({{ slab.incentives.length }})
</button>
} @else {
<span class="fdrc-td--na">None</span>
}
</td>
</ng-container>
<!-- Incentives Expanded Row -->
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let slab; let i = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
class="fdrc-incentives-cell"
>
<div
class="fdrc-incentives-wrap"
[@expandChartSlab]="i === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
@if (slab.incentives?.length) {
<div class="fdrc-incentives-inner">
<div class="fdrc-incentives-title"><i class="fas fa-gift"></i> Incentives for this slab</div>
<table mat-table [dataSource]="slab.incentives" class="fdrc-incentives-table">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>Entity Type</th>
<td mat-cell *matCellDef="let inc">{{ inc.entityType?.value }}</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>Attribute</th>
<td mat-cell *matCellDef="let inc">{{ inc.attributeName?.value }}</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>Condition</th>
<td mat-cell *matCellDef="let inc">{{ inc.conditionType?.value | titlecase }}</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>Value</th>
<td mat-cell *matCellDef="let inc">{{ inc.attributeValueDesc }}</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let inc">{{ inc.incentiveType?.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>Amount</th>
<td mat-cell *matCellDef="let inc">{{ inc.amount }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</div>
}
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns" class="fdrc-header-row"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsDisplayedColumns; let i = dataIndex"
class="fdrc-data-row"
[class.fdrc-data-row--matched]="isMatchedSlab(i)"
></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="fdrc-incentives-row"
></tr>
</table>
@if (!interestRateChartData.length) {
<div class="fdrc-empty">
<i class="fas fa-chart-bar"></i>
<span>No interest rate slabs defined for this product.</span>
</div>
}
</div>
</div>
</div>
@@ -6,125 +6,836 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// ── Tenure Calculator ─────────────────────────────────────────────────────────
.tenure-calc {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-100: #f1f5f9;
$gray-200: #e8eaed;
$gray-600: #64748b;
$gray-700: #334155;
$gray-900: #0f172a;
$blue: #2563eb;
$blue-light: #dbeafe;
$green: #16a34a;
$green-light: #dcfce7;
$amber: #d97706;
$amber-light: #fef3c7;
$purple: #7c3aed;
$purple-light: #ede9fe;
$red: #dc2626;
$red-light: #fee2e2;
.fdrc-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
}
// ── Chart Header Card ──────────────────────────────────────────────────────
.fdrc-header-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
background: #fff;
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
margin-bottom: 1.25rem;
border: 1px solid $border;
border-radius: 12px;
padding: 16px 20px;
flex-wrap: wrap;
&__left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
background: $blue-light;
color: $blue;
font-size: 18px;
flex-shrink: 0;
}
&__title {
font-size: 15px;
font-weight: 700;
color: $gray-900;
}
&__desc {
font-size: 12px;
color: $gray-600;
margin-top: 2px;
}
&__meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
&__dates {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}
}
.fdrc-meta-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: $gray-700;
i {
color: $gray-600;
}
strong {
color: $gray-900;
}
}
// ── Pills ──────────────────────────────────────────────────────────────────
.fdrc-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
&--type {
background: #f1f5f9;
color: $gray-700;
}
&--single {
background: $amber-light;
color: $amber;
}
&--double {
background: $purple-light;
color: $purple;
}
&--special {
background: $green-light;
color: $green;
}
&--amount {
background: $blue-light;
color: $blue;
}
}
// ── Calculator Card ────────────────────────────────────────────────────────
.fdrc-calc-card {
background: #fff;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
&__header {
display: flex;
align-items: center;
gap: 8px;
background: #f0f9ff;
border-bottom: 1px solid #bae6fd;
padding: 0.65rem 1rem;
font-size: 0.85rem;
justify-content: space-between;
padding: 14px 20px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
}
&__header-left {
display: flex;
align-items: center;
gap: 12px;
}
&__title {
font-size: 14px;
font-weight: 700;
color: $gray-900;
display: block;
}
&__sub {
font-size: 12px;
color: $gray-600;
display: block;
margin-top: 1px;
}
&__body {
padding: 20px;
}
}
.fdrc-calc-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: $blue-light;
color: $blue;
font-size: 16px;
flex-shrink: 0;
}
.fdrc-calc-inputs {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.fdrc-calc-field {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1 1 220px;
min-width: 180px;
}
.fdrc-calc-label {
font-size: 12px;
font-weight: 600;
color: $gray-700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.fdrc-calc-optional {
font-weight: 400;
text-transform: none;
letter-spacing: 0;
color: $gray-600;
}
.fdrc-tenure-row {
display: flex;
align-items: center;
gap: 8px;
}
.fdrc-tenure-input {
height: 42px;
border: 1.5px solid $border;
border-radius: 8px;
padding: 0 12px;
font-size: 15px;
font-weight: 600;
color: $gray-900;
background: #fff;
width: 130px;
transition: border-color 0.15s;
&:focus {
outline: none;
border-color: $blue;
}
&--amount {
width: 180px;
}
}
.fdrc-period-badge {
display: inline-flex;
align-items: center;
padding: 0 14px;
height: 42px;
background: $gray-100;
border: 1.5px solid $border;
border-radius: 8px;
font-size: 13px;
font-weight: 700;
color: $gray-700;
}
.fdrc-period-tabs {
display: flex;
border: 1.5px solid $border;
border-radius: 8px;
overflow: hidden;
}
.fdrc-period-tab {
height: 42px;
padding: 0 14px;
font-size: 13px;
font-weight: 600;
border: none;
background: #fff;
color: $gray-600;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
& + & {
border-left: 1px solid $border;
}
&--active {
background: $blue;
color: #fff;
}
}
.fdrc-amount-row {
display: flex;
align-items: center;
}
.fdrc-amount-prefix {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 42px;
background: $gray-100;
border: 1.5px solid $border;
border-right: none;
border-radius: 8px 0 0 8px;
font-size: 15px;
font-weight: 700;
color: $gray-700;
}
.fdrc-amount-row .fdrc-tenure-input {
border-radius: 0 8px 8px 0;
}
.fdrc-calc-hint {
font-size: 11px;
color: $amber;
display: flex;
align-items: center;
gap: 4px;
}
.fdrc-calc-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
margin-top: 22px;
}
.fdrc-calc-btn {
display: inline-flex;
align-items: center;
gap: 7px;
height: 42px;
padding: 0 22px;
background: $blue;
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
&:hover {
background: #1d4ed8;
}
}
.fdrc-calc-reset {
display: inline-flex;
align-items: center;
gap: 6px;
height: 42px;
padding: 0 16px;
background: transparent;
color: $gray-600;
border: 1.5px solid $border;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
&:hover {
border-color: $red;
color: $red;
}
}
// ── Alert ─────────────────────────────────────────────────────────────────
.fdrc-alert {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
border-radius: 8px;
margin-top: 16px;
font-size: 13px;
i {
margin-top: 1px;
flex-shrink: 0;
}
&--warn {
background: $amber-light;
color: #b45309;
border: 1px solid rgba($amber, 0.4);
}
}
// ── Tenure Banner (inside result card) ───────────────────────────────────
.fdrc-tenure-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: #fff;
border: 1.5px solid rgba($green, 0.4);
border-radius: 8px;
margin-bottom: 14px;
> i {
font-size: 18px;
color: $green;
flex-shrink: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 1px;
flex: 1;
}
&__label {
font-size: 10px;
font-weight: 700;
color: #0369a1;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.06em;
color: $gray-600;
}
&__value {
font-size: 20px;
font-weight: 800;
color: $gray-900;
line-height: 1.2;
}
&__saved {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: $green-light;
color: $green;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
flex-shrink: 0;
}
}
// ── Result Card ───────────────────────────────────────────────────────────────
.fdrc-result-card {
display: flex;
align-items: flex-start;
gap: 16px;
margin-top: 16px;
padding: 18px 20px;
background: $green-light;
border: 1.5px solid rgba($green, 0.35);
border-radius: 12px;
&__check {
font-size: 26px;
color: $green;
flex-shrink: 0;
margin-top: 2px;
}
&__body {
flex: 1;
min-width: 0;
}
}
.fdrc-result-rate {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 10px;
&__value {
font-size: 28px;
font-weight: 800;
color: $green;
line-height: 1;
}
&__label {
font-size: 13px;
color: #166534;
font-weight: 600;
}
}
.fdrc-result-meta {
display: flex;
flex-direction: column;
gap: 5px;
&__item {
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: $gray-700;
i {
font-size: 13px;
color: $green;
font-size: 11px;
}
strong {
color: $gray-900;
}
}
}
// ── Maturity Card ────────────────────────────────────────────────────────────
.fdrc-maturity-card {
margin-top: 16px;
background: #fff;
border: 1.5px solid rgba($green, 0.2);
border-radius: 10px;
overflow: hidden;
&__header {
background: rgba($green, 0.05);
padding: 8px 14px;
border-bottom: 1px solid rgba($green, 0.1);
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 700;
color: #166534;
i {
font-size: 14px;
}
}
&__body {
padding: 1rem 1.25rem;
}
&__input-row {
padding: 12px 14px;
display: flex;
align-items: flex-start;
gap: 1rem;
flex-direction: column;
gap: 8px;
}
}
.fdrc-maturity-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
&__label {
font-size: 12px;
color: $gray-600;
}
&__field {
width: 260px;
}
&__result {
margin-top: 1rem;
border: 1px solid #e0f2fe;
border-radius: 8px;
overflow: hidden;
}
&__result-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.55rem 1rem;
border-bottom: 1px solid #f0f9ff;
font-size: 0.88rem;
&:last-child {
border-bottom: none;
}
&__value {
font-size: 13px;
font-weight: 700;
color: $gray-900;
&--highlight {
background: #f0f9ff;
color: $green;
}
&--big {
font-size: 16px;
color: $green;
}
}
&__result-label {
color: #64748b;
font-weight: 500;
&--interest {
padding-top: 4px;
margin-top: 4px;
border-top: 1px dashed $border;
}
&__result-value {
color: #1e293b;
font-weight: 600;
&--total {
padding-top: 8px;
margin-top: 4px;
border-top: 1.5px solid $gray-200;
}
}
&__result-rate {
font-size: 1.25rem;
font-weight: 800;
color: #0369a1;
.fdrc-payout-hint {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 12px;
color: $gray-600;
font-style: italic;
i {
color: $amber;
}
}
&__no-match {
margin-top: 0.75rem;
padding: 0.6rem 1rem;
border-radius: 8px;
background: #fef2f2;
color: #dc2626;
font-size: 0.85rem;
font-weight: 600;
// ── Rate Chart Table Card ─────────────────────────────────────────────────
.fdrc-table-card {
background: #fff;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
&__header {
display: flex;
align-items: center;
gap: 6px;
justify-content: space-between;
padding: 14px 20px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
flex-wrap: wrap;
gap: 8px;
}
&__title-row {
display: flex;
align-items: center;
gap: 10px;
i {
font-size: 13px;
color: $blue;
}
}
&__title {
font-size: 14px;
font-weight: 700;
color: $gray-900;
}
}
.margin-t {
margin-top: 2em;
.fdrc-slab-count {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: $blue-light;
color: $blue;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
table {
.fdrc-highlight-note {
font-size: 12px;
color: $green;
font-weight: 600;
display: flex;
align-items: center;
gap: 5px;
}
.fdrc-table-wrap {
overflow-x: auto;
}
.fdrc-table {
width: 100%;
min-width: 600px;
}
tr.incentives-row {
height: 0;
.fdrc-th {
font-size: 11px !important;
font-weight: 700 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $gray-700 !important;
background: $gray-50;
padding: 10px 16px !important;
}
> td {
padding: 0;
}
.fdrc-td {
padding: 12px 16px !important;
font-size: 13px;
color: $gray-700;
vertical-align: middle;
.incentives {
overflow: hidden;
}
&--period {
display: flex;
flex-direction: column;
gap: 2px;
}
.mat-card {
border-radius: 0;
}
&--na {
color: #94a3b8;
}
}
.fdrc-period-range {
font-weight: 700;
color: $gray-900;
font-size: 13px;
}
.fdrc-period-type {
font-size: 11px;
color: $gray-600;
}
.fdrc-rate-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
background: $blue-light;
color: $blue;
border-radius: 999px;
font-size: 13px;
font-weight: 700;
}
.fdrc-incentive-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
height: 32px;
background: transparent;
border: 1.5px solid $border;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: $gray-700;
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
&:hover {
border-color: $blue;
color: $blue;
}
}
.fdrc-header-row {
background: $gray-50;
}
.fdrc-data-row {
transition: background 0.15s;
&:hover td {
background: $gray-50;
}
&--matched td {
background: $green-light !important;
border-left: 3px solid $green;
}
}
.fdrc-incentives-row {
height: 0;
}
.fdrc-incentives-cell {
padding: 0 !important;
}
.fdrc-incentives-wrap {
overflow: hidden;
}
.fdrc-incentives-inner {
padding: 16px 20px;
background: $gray-50;
border-bottom: 1px solid $border;
}
.fdrc-incentives-title {
font-size: 12px;
font-weight: 700;
color: $gray-700;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
i {
color: $amber;
}
}
.fdrc-incentives-table {
width: 100%;
background: #fff;
border: 1px solid $border;
border-radius: 8px;
overflow: hidden;
th {
font-size: 10px !important;
padding: 8px 12px !important;
background: $gray-100;
}
td {
font-size: 12px;
padding: 8px 12px !important;
}
}
.fdrc-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: $gray-600;
gap: 12px;
i {
font-size: 32px;
opacity: 0.3;
}
span {
font-size: 14px;
}
}
@@ -6,8 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, Input, OnChanges, ViewChild, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, Input, OnChanges, ViewChild, Output, EventEmitter, inject } from '@angular/core';
import { RecurringDepositsService } from '../../recurring-deposits.service';
import {
MatTable,
MatColumnDef,
@@ -30,6 +30,18 @@ import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FormsModule } from '@angular/forms';
export type RdPeriodUnit = 'Days' | 'Months' | 'Years';
export interface SlabMatchResult {
slab: any;
slabIndex: number;
appliedRate: number;
maturityAmount?: number;
interestEarned?: number;
tenureDays?: number;
daysInYear?: number;
}
/**
* Recurring Deposits Account Interest Rate Chart Step
*/
@@ -64,45 +76,44 @@ import { FormsModule } from '@angular/forms';
TitleCasePipe,
DateFormatPipe,
FormatNumberPipe,
YesnoPipe
YesnoPipe,
DecimalPipe
]
})
export class RecurringDepositsAccountInterestRateChartStepComponent implements OnInit, OnChanges {
@Input() recurringDepositsAccountTemplate: any;
@Input() recurringDepositsAccountProductTemplate: any;
@Input() submittedOnDate: Date | null = null;
/**
* CBS base rate chart loaded from the active base-interest-rate service.
* When set, this table is shown instead of the legacy accountChart.chartSlabs.
*/
@Input() set rdInterestRates(rates: any[]) {
this._rdInterestRates = rates || [];
this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null;
@Output() tenureSelected = new EventEmitter<{
depositPeriod: number;
depositPeriodFrequencyId: number;
depositAmount?: number;
maturityAmount?: number;
interestEarned?: number;
daysInYear?: number;
}>();
get interestRateChartData(): any[] {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
if (!template) return [];
return (
template.accountChart?.chartSlabs ??
template.activeChart?.chartSlabs ??
template.interestRateCharts?.[0]?.chartSlabs ??
template.interestRateChart?.chartSlabs ??
template.chartSlabs ??
[]
);
}
_rdInterestRates: any[] = [];
_activeRdVersion: any = null;
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
/** Interest Rate Chart Data (legacy – used when no custom rate chart is available) */
interestRateChartData: any = [];
/** Columns to be displayed in interest rate chart table. */
chartSlabsDisplayedColumns: any[] = [
chartSlabsDisplayedColumns: string[] = [
'period',
'amountRange',
'interest',
'description',
'actions'
];
/** Columns to be displayed in incentives sub-table. */
incentivesDisplayedColumns: string[] = [
'entityType',
'attributeName',
@@ -111,38 +122,242 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
'incentiveType',
'amount'
];
/** Additional Column to display incentives table */
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
/** Expand Chart Slab Index used in the view */
expandChartSlabIndex: number;
/** Tenure entered by user (in days) */
tenureDays: number | null = null;
expandChartSlabIndex: number | null = null;
@ViewChild('chartsTable', { static: true }) chartsTableRef: MatTable<Element>;
constructor() {}
// ── Tenure Calculator ───────────────────────────────────────────────────
tenureValue: number | null = null;
tenurePeriodUnit: RdPeriodUnit = 'Months';
principalAmount: number | null = null; // Monthly installment for RD
/** Find the matching slab for the entered tenure */
get applicableSlab(): any | null {
if (!this.tenureDays || !this._activeRdVersion?.slabs?.length) return null;
const days = this.tenureDays;
return (
this._activeRdVersion.slabs.find((s: any) => {
const from = s.fromPeriod ?? 0;
const to = s.toPeriod ?? Infinity;
return days >= from && days <= to;
}) ?? null
);
}
matchResult: SlabMatchResult | null = null;
validationWarning: string | null = null;
/** Period unit options */
readonly allowedPeriodUnits: RdPeriodUnit[] = [
'Months',
'Years'
];
private recurringDepositsService = inject(RecurringDepositsService);
ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) {
this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
if (template) {
if (this.chartsTableRef) {
this.chartsTableRef.renderRows();
}
this.resetCalculator();
this.tenurePeriodUnit = this.allowedPeriodUnits[1]; // Default to Months
}
}
ngOnInit() {
this.interestRateChartData = [];
// Convert entered tenure to Days for slab matching
private toDays(value: number, unit: RdPeriodUnit): number {
if (unit === 'Days') return value;
if (unit === 'Months') return value * 30;
return value * 365;
}
// Convert slab period to days
private slabPeriodToDays(period: number, periodTypeValue: string): number {
const p = (periodTypeValue || '').toLowerCase();
if (p.includes('month')) return period * 30;
if (p.includes('year')) return period * 365;
return period; // days
}
calculateRate() {
this.validationWarning = null;
this.matchResult = null;
if (!this.tenureValue || this.tenureValue <= 0) {
this.validationWarning = 'Please enter a valid tenure value greater than 0.';
return;
}
const enteredDays = this.toDays(this.tenureValue, this.tenurePeriodUnit);
const slabs: any[] = this.interestRateChartData || [];
if (!slabs.length) {
this.validationWarning = 'No interest rate slabs are defined for this product.';
return;
}
let matchedSlab: any = null;
let matchedIndex = -1;
for (let i = 0; i < slabs.length; i++) {
const slab = slabs[i];
const periodTypeValue = slab.periodType?.value || 'Days';
const fromDays = this.slabPeriodToDays(slab.fromPeriod ?? 0, periodTypeValue);
const toDays = slab.toPeriod != null ? this.slabPeriodToDays(slab.toPeriod, periodTypeValue) : Infinity;
if (enteredDays >= fromDays && enteredDays <= toDays) {
matchedSlab = slab;
matchedIndex = i;
break;
}
}
if (!matchedSlab) {
this.validationWarning = 'No applicable interest rate slab found for the entered tenure.';
return;
}
const rate = matchedSlab.annualInterestRate ?? 0;
const result: SlabMatchResult = {
slab: matchedSlab,
slabIndex: matchedIndex,
appliedRate: rate,
tenureDays: enteredDays
};
// Maturity Estimation via Backend API
if (this.principalAmount && this.principalAmount > 0) {
const compoundingType = this.recurringDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id;
let compoundingMonths = 1; // Default Monthly
if (compoundingType === 5) compoundingMonths = 3;
else if (compoundingType === 6) compoundingMonths = 6;
else if (compoundingType === 7) compoundingMonths = 12;
else if (compoundingType === 1) compoundingMonths = 0; // Special case for Daily in my API logic
// Calculate exact months based on unit to avoid rounding errors (e.g. 10 years = 120 months, not 122)
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);
}
const calculationData: any = {
depositAmount: this.principalAmount,
annualInterestRate: rate,
tenureInMonths: tenureInMonths,
interestCompoundingPeriodInMonths: compoundingMonths
};
if (this.submittedOnDate) {
const date = new Date(this.submittedOnDate);
calculationData.submittedOnDate = date.toISOString().split('T')[0];
calculationData.dateFormat = 'yyyy-MM-dd'; // Standard ISO for API
calculationData.locale = 'en';
}
this.recurringDepositsService.calculateRecurringDepositInterest(calculationData).subscribe((apiResult: any) => {
result.maturityAmount = apiResult.maturityAmount;
result.interestEarned = apiResult.totalInterest;
result.daysInYear = apiResult.daysInYear;
this.matchResult = { ...result };
this.emitTenureSelection();
});
} else {
this.matchResult = result;
this.emitTenureSelection();
}
}
private emitTenureSelection() {
const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit);
if (frequencyId !== null && this.matchResult) {
this.tenureSelected.emit({
depositPeriod: this.tenureValue || 0,
depositPeriodFrequencyId: frequencyId,
depositAmount: this.principalAmount || undefined,
maturityAmount: this.matchResult.maturityAmount,
interestEarned: this.matchResult.interestEarned,
daysInYear: this.matchResult.daysInYear
});
}
}
private resolveFrequencyId(unit: RdPeriodUnit): number | null {
const options: any[] = this.recurringDepositsAccountProductTemplate?.periodFrequencyTypeOptions ?? [];
const unitLower = unit.toLowerCase();
const match = options.find((o: any) => (o.value || '').toLowerCase().includes(unitLower));
if (match) return match.id;
const fallback: Record<RdPeriodUnit, number> = { Days: 0, Months: 2, Years: 3 };
return fallback[unit] ?? null;
}
resetCalculator() {
this.tenureValue = null;
this.principalAmount = null;
this.matchResult = null;
this.validationWarning = null;
this.tenurePeriodUnit = 'Months';
}
onPeriodUnitChange() {
this.matchResult = null;
this.validationWarning = null;
}
isMatchedSlab(index: number): boolean {
return this.matchResult?.slabIndex === index;
}
// ── Header Getters ──────────────────────────────────────────────────────
get chartName(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.name ||
template?.activeChart?.name ||
template?.interestRateCharts?.[0]?.name ||
template?.interestRateChart?.name ||
template?.name ||
''
);
}
get chartFromDate(): any {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.fromDate ||
template?.activeChart?.fromDate ||
template?.interestRateCharts?.[0]?.fromDate ||
template?.interestRateChart?.fromDate ||
template?.fromDate
);
}
get chartEndDate(): any {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.endDate ||
template?.activeChart?.endDate ||
template?.interestRateCharts?.[0]?.endDate ||
template?.interestRateChart?.endDate ||
template?.endDate
);
}
get chartDescription(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.description ||
template?.activeChart?.description ||
template?.interestRateCharts?.[0]?.description ||
template?.interestRateChart?.description ||
template?.description ||
''
);
}
get isPrimaryGroupingByAmount(): boolean {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return !!(
template?.accountChart?.isPrimaryGroupingByAmount ||
template?.activeChart?.isPrimaryGroupingByAmount ||
template?.interestRateCharts?.[0]?.isPrimaryGroupingByAmount ||
template?.interestRateChart?.isPrimaryGroupingByAmount ||
template?.isPrimaryGroupingByAmount
);
}
}
@@ -422,18 +422,6 @@
<span class="preview-value">{{ recurringDepositsAccountProductTemplate.taxGroup.name }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Balance Swap Option</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="rdCurrencyFlags?.balanceSwapOption">
{{ rdCurrencyFlags?.balanceSwapOption | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">MSME</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="rdCurrencyFlags?.msme">
{{ rdCurrencyFlags?.msme | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Auto Debit Facility</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="rdCurrencyFlags?.autoDebitFacility">
@@ -497,7 +485,7 @@
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Inactive</span>
<span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct != null) {
@if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct !== null) {
After {{ recurringDepositsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction
} @else {
Not configured
@@ -511,7 +499,7 @@
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Dormant</span>
<span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct != null) {
@if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct !== null) {
{{ recurringDepositsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive
} @else {
Not configured
@@ -525,7 +513,7 @@
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Escheat</span>
<span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct != null) {
@if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct !== null) {
{{ recurringDepositsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant
} @else {
Not configured
@@ -637,183 +625,225 @@
</div>
}
<!-- ══ 7. Interest Rate (Tenure-based result) ════════════════════ -->
@if (_activeRdVersion || recurringDepositsAccountProductTemplate?.accountChart) {
<!-- ══ 6. Maturity Summary ═══════════════════════════════════ -->
@if (rdMaturityAmount) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-piggy-bank preview-section__icon"></i>
<span class="preview-section__title">Maturity Summary</span>
<span class="preview-section__badge preview-section__badge--active">Projected</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Total Deposit Amount</span>
<span class="preview-value"
>{{ rdMaturityAmount - rdInterestEarned | formatNumber }} {{ currencyName }}</span
>
</div>
<div class="preview-field">
<span class="preview-label">Applicable Interest Rate</span>
<span class="preview-value">{{ rdApplicableSlab?.annualInterestRate | formatNumber }}%</span>
</div>
<div class="preview-field">
<span class="preview-label">Tenure</span>
<span class="preview-value">{{ selectedTenureLabel }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Estimated Interest Earned</span>
<span class="preview-value" style="color: var(--theme-success, #16a34a); font-weight: 500">
+ {{ rdInterestEarned | formatNumber }} {{ currencyName }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Days in Year</span>
<span class="preview-value">{{ rdDaysInYear ?? 365 }} Days</span>
</div>
<div class="preview-field" style="grid-column: span 1">
<span class="preview-label">Projected Maturity Amount</span>
<span class="preview-value preview-mono" style="font-weight: 600; font-size: 1.25rem">
{{ rdMaturityAmount | formatNumber }} {{ currencyName }}
</span>
</div>
</div>
</div>
</div>
}
<!-- ══ 7. Interest Rate Chart ═══════════════════════════════════ -->
@if (interestRateChartData?.length > 0) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-chart-line preview-section__icon"></i>
<span class="preview-section__title">Interest Rate</span>
@if (_activeRdVersion) {
<span class="preview-section__badge">{{ _activeRdVersion.versionLabel }}</span>
<span class="preview-section__title">{{ 'labels.heading.Interest Rate Chart' | translate }}</span>
@if (rdTenureDays) {
<span class="preview-section__badge preview-section__badge--tenure">
<i class="fas fa-calendar-check"></i> Selected Tenure: {{ selectedTenureLabel }}
</span>
}
</div>
<div class="preview-body">
@if (_activeRdVersion) {
@if (rdTenureDays && rdApplicableSlab) {
<div class="preview-tenure-result">
<div class="preview-tenure-result__row">
<span class="preview-label">Tenure</span>
<span class="preview-value">{{ rdTenureDays }} Days</span>
</div>
<div class="preview-tenure-result__row">
<span class="preview-label">Applicable Slab</span>
<span class="preview-value">{{ rdApplicableSlab.description }}</span>
</div>
<div class="preview-tenure-result__row preview-tenure-result__row--highlight">
<span class="preview-label">Annual Interest Rate</span>
<span class="preview-tenure-result__rate"
>{{ rdApplicableSlab.annualInterestRate | formatNumber }}%</span
>
<!-- Selected Tenure + Rate Banner -->
@if (rdTenureDays && matchedSlabIndex >= 0) {
<div class="preview-tenure-banner">
<div class="preview-tenure-banner__left">
<i class="fas fa-calendar-check"></i>
<div>
<span class="preview-tenure-banner__label">Your RD Tenure</span>
<span class="preview-tenure-banner__value">{{ selectedTenureLabel }}</span>
</div>
</div>
} @else {
<span class="preview-muted">No tenure entered — interest rate not calculated</span>
}
} @else {
<div class="preview-grid preview-grid--three">
@if (recurringDepositsAccountProductTemplate?.accountChart?.name) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.name' | translate }}</span>
<span class="preview-value">{{ recurringDepositsAccountProductTemplate.accountChart.name }}</span>
</div>
}
@if (recurringDepositsAccountProductTemplate?.accountChart?.fromDate) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Valid from Date' | translate }}</span>
<span class="preview-value">{{
recurringDepositsAccountProductTemplate.accountChart.fromDate | dateFormat
}}</span>
</div>
}
@if (recurringDepositsAccountProductTemplate?.accountChart?.endDate) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.End Date' | translate }}</span>
<span class="preview-value">{{
recurringDepositsAccountProductTemplate.accountChart.endDate | dateFormat
}}</span>
</div>
}
@if (recurringDepositsAccountProductTemplate?.accountChart?.description) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Description' | translate }}</span>
<span class="preview-value">{{
recurringDepositsAccountProductTemplate.accountChart.description
}}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}</span>
<span class="preview-value">{{
recurringDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount | yesNo
}}</span>
<div class="preview-tenure-banner__divider"></div>
<div class="preview-tenure-banner__right">
<span class="preview-tenure-banner__rate-label">Applicable Interest Rate</span>
<span class="preview-tenure-banner__rate"
>{{ interestRateChartData[matchedSlabIndex]?.annualInterestRate | formatNumber }}%</span
>
</div>
<div class="preview-tenure-banner__slab">
<i class="fas fa-layer-group"></i>
Slab: {{ interestRateChartData[matchedSlabIndex]?.fromPeriod }}–{{
interestRateChartData[matchedSlabIndex]?.toPeriod ?? '∞'
}}
{{ interestRateChartData[matchedSlabIndex]?.periodType?.value }}
</div>
</div>
}
@if (interestRateChartData?.length) {
<table
class="mat-elevation-z1 irc-table preview-table"
mat-table
[dataSource]="interestRateChartData"
multiTemplateDataRows
>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }}&nbsp;{{
chartSlab.periodType?.value | translateKey: 'catalogs'
}}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate | formatNumber }} %</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
<span *ngIf="expandChartSlabIndex !== chartSlabIndex">
<fa-icon icon="eye" class="m-r-10"></fa-icon>
{{ 'labels.buttons.View Incentives' | translate }}
</span>
<span *ngIf="expandChartSlabIndex === chartSlabIndex">
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Hide Incentives' | translate }}
</span>
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
<div class="preview-grid preview-grid--three" style="margin-bottom: 1rem">
@if (chartName) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.name' | translate }}</span>
<span class="preview-value">{{ chartName }}</span>
</div>
}
@if (chartFromDate) {
<div class="preview-field">
<span class="preview-label">Valid From</span>
<span class="preview-value">{{ chartFromDate | dateFormat }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Primary Grouping by Amount</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="isPrimaryGroupingByAmount">
{{ isPrimaryGroupingByAmount | yesNo }}
</span>
</div>
</div>
@if (interestRateChartData?.length) {
<table
class="mat-elevation-z1 irc-table preview-table"
mat-table
[dataSource]="interestRateChartData"
multiTemplateDataRows
>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }}&nbsp;{{
chartSlab.periodType?.value | translateKey: 'catalogs'
}}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate | formatNumber }} %</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
<div
class="layout-row-wrap flex-100 incentives"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<h4 class="m-b-10 flex-13">{{ 'labels.heading.Incentives' | translate }}</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="chartSlab.incentives"
*ngIf="chartSlab.incentives?.length"
>
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.entityType?.value }}</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeName?.value }}</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.conditionType?.value | titlecase }}</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeValueDesc }}</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.incentiveType?.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.amount }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="incentives-row"
></tr>
</table>
<fa-icon
[icon]="expandChartSlabIndex === chartSlabIndex ? 'eye-slash' : 'eye'"
class="m-r-10"
></fa-icon>
{{
(expandChartSlabIndex === chartSlabIndex
? 'labels.buttons.Hide Incentives'
: 'labels.buttons.View Incentives'
) | translate
}}
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns?.length"
>
<div
class="layout-row-wrap flex-100 incentives"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<h4 class="m-b-10 flex-13">{{ 'labels.heading.Incentives' | translate }}</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="chartSlab.incentives"
*ngIf="chartSlab.incentives?.length"
>
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.entityType?.value }}</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeName?.value }}</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.conditionType?.value | titlecase }}</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeValueDesc }}</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.incentiveType?.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let incentive">{{ incentive.amount | formatNumber }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsDisplayedColumns; let i = dataIndex"
[class.preview-slab-row--matched]="i === matchedSlabIndex"
></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
@if (rdTenureDays && matchedSlabIndex >= 0) {
<div class="preview-slab-legend">
<span class="preview-slab-legend__dot"></span>
Highlighted row is the slab applicable for your selected tenure of
<strong>{{ selectedTenureLabel }}</strong>
</div>
}
}
</div>
@@ -181,32 +181,125 @@
margin-top: 1em;
}
// ── Preview Tenure Result Card ─────────────────────────────────────────────────
.preview-tenure-result {
border: 1px solid #e0f2fe;
// ── Section badge variant for tenure ─────────────────────────────────────────
.preview-section__badge--tenure {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #dcfce7;
color: #16a34a;
margin-left: auto;
}
// ── Tenure + Rate banner ──────────────────────────────────────────────────────
.preview-tenure-banner {
display: flex;
align-items: center;
gap: 20px;
padding: 14px 18px;
background: #f0fdf4;
border: 1.5px solid #86efac;
border-radius: 10px;
overflow: hidden;
margin-bottom: 16px;
flex-wrap: wrap;
&__row {
&__left {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.65rem 1rem;
border-bottom: 1px solid #f0f9ff;
gap: 10px;
&:last-child {
border-bottom: none;
}
&--highlight {
background: #f0f9ff;
> i {
font-size: 20px;
color: #16a34a;
}
}
&__rate {
font-size: 1.5rem;
&__label {
display: block;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #4b5563;
}
&__value {
display: block;
font-size: 22px;
font-weight: 800;
color: #0369a1;
color: #0f172a;
line-height: 1.2;
}
&__divider {
width: 1px;
height: 40px;
background: #86efac;
}
&__right {
display: flex;
flex-direction: column;
gap: 2px;
}
&__rate-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #4b5563;
}
&__rate {
font-size: 26px;
font-weight: 800;
color: #16a34a;
line-height: 1.2;
}
&__slab {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #374151;
margin-left: auto;
i {
color: #16a34a;
font-size: 11px;
}
}
}
// ── Matched slab row highlight ────────────────────────────────────────────────
.preview-slab-row--matched td {
background: #dcfce7 !important;
border-left: 3px solid #16a34a;
}
// ── Legend below table ────────────────────────────────────────────────────────
.preview-slab-legend {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 12px;
color: #4b5563;
&__dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 2px;
background: #dcfce7;
border: 2px solid #16a34a;
flex-shrink: 0;
}
}
@@ -77,10 +77,10 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
@Input() rdCurrencyFlags: any = null;
@Input() rdTenureDays: number | null = null;
@Input() rdApplicableSlab: any = null;
@Input() set rdInterestRates(rates: any[]) {
this._rdInterestRates = rates || [];
this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null;
}
@Input() rdMaturityAmount: number | null = null;
@Input() rdInterestEarned: number | null = null;
@Input() rdDaysInYear: number | null = null;
/** Output the submit action */
@Output() submitEvent = new EventEmitter();
@@ -96,9 +96,6 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
return this.recurringDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—';
}
_rdInterestRates: any[] = [];
_activeRdVersion: any = null;
readonly rdSlabColumns = [
'srNo',
'description',
@@ -117,8 +114,57 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
'date',
'repaymentsEvery'
];
/** Interest Rate Chart Data */
interestRateChartData: any = [];
get interestRateChartData(): any[] {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
if (!template) return [];
return (
template.accountChart?.chartSlabs ??
template.activeChart?.chartSlabs ??
template.interestRateCharts?.[0]?.chartSlabs ??
template.interestRateChart?.chartSlabs ??
template.chartSlabs ??
[]
);
}
/** Matched Slab Index for highlighting */
get matchedSlabIndex(): number {
if (!this.rdTenureDays || !this.interestRateChartData) return -1;
// Find index of the applicable slab in the chart data
return this.interestRateChartData.findIndex((slab) => {
const pTypeValue = slab.periodType?.value || 'Days';
const fromDays = this.slabPeriodToDays(slab.fromPeriod ?? 0, pTypeValue);
const toDays = slab.toPeriod != null ? this.slabPeriodToDays(slab.toPeriod, pTypeValue) : Infinity;
return this.rdTenureDays >= fromDays && this.rdTenureDays <= toDays;
});
}
/** Selected Tenure Label for banner */
get selectedTenureLabel(): string {
if (!this.rdTenureDays) return '';
if (this.rdTenureDays >= 365) {
const years = Math.floor(this.rdTenureDays / 365);
const rem = this.rdTenureDays % 365;
return years + (years === 1 ? ' Year' : ' Years') + (rem > 0 ? ` ${rem} Days` : '');
}
if (this.rdTenureDays >= 30) {
const months = Math.floor(this.rdTenureDays / 30);
const rem = this.rdTenureDays % 30;
return months + (months === 1 ? ' Month' : ' Months') + (rem > 0 ? ` ${rem} Days` : '');
}
return this.rdTenureDays + ' Days';
}
private slabPeriodToDays(period: number, periodTypeValue: string): number {
const p = (periodTypeValue || '').toLowerCase();
if (p.includes('day')) return period;
if (p.includes('week')) return period * 7;
if (p.includes('month')) return period * 30;
if (p.includes('year')) return period * 365;
return period;
}
/** Columns to be displayed in interest rate chart table. */
chartSlabsDisplayedColumns: any[] = [
'period',
@@ -144,8 +190,68 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
constructor() {}
ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) {
this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
if (template) {
// Logic for metadata getters is now handled by the dynamic template reference
}
}
// ── Header Getters ──────────────────────────────────────────────────────
get chartName(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.name ||
template?.activeChart?.name ||
template?.interestRateCharts?.[0]?.name ||
template?.interestRateChart?.name ||
template?.name ||
''
);
}
get chartFromDate(): any {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.fromDate ||
template?.activeChart?.fromDate ||
template?.interestRateCharts?.[0]?.fromDate ||
template?.interestRateChart?.fromDate ||
template?.fromDate
);
}
get chartEndDate(): any {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.endDate ||
template?.activeChart?.endDate ||
template?.interestRateCharts?.[0]?.endDate ||
template?.interestRateChart?.endDate ||
template?.endDate
);
}
get chartDescription(): string {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return (
template?.accountChart?.description ||
template?.activeChart?.description ||
template?.interestRateCharts?.[0]?.description ||
template?.interestRateChart?.description ||
template?.description ||
''
);
}
get isPrimaryGroupingByAmount(): boolean {
const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
return !!(
template?.accountChart?.isPrimaryGroupingByAmount ||
template?.activeChart?.isPrimaryGroupingByAmount ||
template?.interestRateCharts?.[0]?.isPrimaryGroupingByAmount ||
template?.interestRateChart?.isPrimaryGroupingByAmount ||
template?.isPrimaryGroupingByAmount
);
}
}
@@ -14,7 +14,7 @@
<span>Interest Rate &amp; Posting Terms</span>
</div>
<div class="rdpt-body">
@if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate != null) {
@if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate !== null) {
<div class="rdpt-banner">
<span class="rdpt-banner__icon">ℹ️</span>
<span>
@@ -92,7 +92,12 @@ export class InterestRateChartTabComponent {
*/
constructor() {
this.route.parent.data.subscribe((data: { recurringDepositsAccountData: any }) => {
this.interestRateChartData = data.recurringDepositsAccountData.accountChart.chartSlabs;
const accountData = data.recurringDepositsAccountData;
this.interestRateChartData =
accountData.accountChart?.chartSlabs ??
accountData.activeChart?.chartSlabs ??
accountData.interestRateCharts?.[0]?.chartSlabs ??
[];
});
}
}
@@ -173,4 +173,11 @@ export class RecurringDepositsService {
params: httpParams
});
}
/**
* Calculates the interest for a recurring deposit account
* @param data Calculation data
*/
calculateRecurringDepositInterest(data: any): Observable<any> {
return this.http.post(`/recurringdepositaccounts/calculate-rd-interest`, data);
}
}
@@ -49,7 +49,6 @@ import { EditTaxComponentComponent } from './manage-tax-components/edit-tax-comp
import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-tax-group.component';
import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components';
import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component';
import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component';
import { CreateDividendComponent } from './share-products/create-dividend/create-dividend.component';
import { ViewFixedDepositProductComponent } from './fixed-deposit-products/view-fixed-deposit-product/view-fixed-deposit-product.component';
import { ViewDividendComponent } from './share-products/view-dividend/view-dividend.component';
@@ -655,14 +654,6 @@ const routes: Routes = [
path: 'recurring-deposit-products',
data: { title: 'Recurring Deposit Products', breadcrumb: 'Recurring Deposit Products' },
children: [
{
path: 'base-interest-rates',
component: BaseRecurringDepositInterestRateChartComponent,
data: { title: 'CBS Base Interest Rates', breadcrumb: 'Base Interest Rates' },
resolve: {
recurringDepositProductsTemplate: RecurringDepositProductsTemplateResolver
}
},
{
path: 'create',
component: CreateRecurringDepositProductComponent,
@@ -717,14 +708,6 @@ const routes: Routes = [
}
}
]
},
{
path: 'interest-rates',
component: RdInterestRatesTabComponent,
data: { title: 'CBS Base Chart', breadcrumb: 'CBS Base Chart', routeParamBreadcrumb: false },
resolve: {
rdInterestRates: rdInterestRatesResolver
}
}
]
},
@@ -34,6 +34,7 @@ import {
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { Dates } from 'app/core/utils/dates';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RecurringDepositProductPreviewStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component';
import { MatTabsModule } from '@angular/material/tabs';
@@ -72,6 +73,7 @@ export class CreateRecurringDepositProductComponent {
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(RecurringDepositProductDetailsStepComponent, { static: true })
recurringDepositProductDetailsStep: RecurringDepositProductDetailsStepComponent;
@@ -112,7 +114,7 @@ export class CreateRecurringDepositProductComponent {
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
// Load the CBS base chart as the reference for the editable table
this.productsService.getRdBaseInterestRates().subscribe({
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
@@ -151,7 +153,18 @@ export class CreateRecurringDepositProductComponent {
}
get activeBaseVersion() {
return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null;
const chart =
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE' && version.isActive) ??
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE') ??
this.rdInterestRates[0];
if (chart) {
return {
...chart,
versionLabel: chart.versionLabel?.replace('FD', 'RD'),
remarks: chart.remarks?.replace('FD', 'RD')
};
}
return null;
}
get activeBaseSlabs() {
@@ -166,17 +179,66 @@ export class CreateRecurringDepositProductComponent {
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
charts: this.recurringDepositProductCharts,
rdSlabs: this.activeBaseSlabs
};
}
get recurringDepositProductCharts() {
const activeBaseVersion = this.activeBaseVersion;
if (!activeBaseVersion?.slabs?.length) {
return [];
}
const dateFormat = this.settingsService.dateFormat;
return [
{
name: activeBaseVersion.versionLabel || 'CBS Base Chart',
description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart',
fromDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) ||
activeBaseVersion.effectiveFromDate,
...(activeBaseVersion.effectiveToDate
? {
endDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) ||
activeBaseVersion.effectiveToDate
}
: {}),
isPrimaryGroupingByAmount: false,
locale: this.settingsService.language.code,
dateFormat,
chartSlabs: activeBaseVersion.slabs.map((slab: any) => {
const chartSlab: any = {
periodType: slab.periodTypeEnum,
fromPeriod: slab.fromPeriod,
toPeriod: slab.toPeriod,
annualInterestRate: slab.annualInterestRate,
description: slab.description || '',
incentives: [] as any[]
};
if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') {
chartSlab.amountRangeFrom = slab.amountRangeFrom;
}
if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') {
chartSlab.amountRangeTo = slab.amountRangeTo;
}
return chartSlab;
})
}
] as any[];
}
submit() {
// TODO: Update once language and date settings are setup
const recurringDepositProduct = {
const recurringDepositProduct: any = this.pruneEmptyValues({
...this.recurringDepositProduct,
charges: this.recurringDepositProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code // locale required for depositAmount
};
charts: this.recurringDepositProductCharts,
charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
});
if (!recurringDepositProduct.description) {
recurringDepositProduct.description = '';
}
@@ -191,4 +253,35 @@ export class CreateRecurringDepositProductComponent {
);
});
}
private pruneEmptyValues<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T;
}
if (value && typeof value === 'object') {
return Object.entries(value as Record<string, any>).reduce(
(acc, [
key,
currentValue
]) => {
const cleanedValue = this.pruneEmptyValues(currentValue);
if (cleanedValue === undefined) {
return acc;
}
return {
...acc,
[key]: cleanedValue
};
},
{} as Record<string, any>
) as T;
}
if (value === '' || value === null || value === undefined) {
return undefined as T;
}
return value;
}
}
@@ -34,6 +34,7 @@ import {
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { Dates } from 'app/core/utils/dates';
import { MatTabsModule } from '@angular/material/tabs';
import { RecurringDepositProductPreviewStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -70,6 +71,7 @@ export class EditRecurringDepositProductComponent {
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(RecurringDepositProductDetailsStepComponent, { static: true })
recurringDepositProductDetailsStep: RecurringDepositProductDetailsStepComponent;
@@ -107,7 +109,7 @@ export class EditRecurringDepositProductComponent {
constructor() {
this.route.data.subscribe((data: { recurringDepositProductAndTemplate: any }) => {
this.recurringDepositProductsTemplate = data.recurringDepositProductAndTemplate;
this.productsService.getRdBaseInterestRates().subscribe({
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
@@ -148,7 +150,18 @@ export class EditRecurringDepositProductComponent {
}
get activeBaseVersion() {
return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null;
const chart =
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE' && version.isActive) ??
this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE') ??
this.rdInterestRates[0];
if (chart) {
return {
...chart,
versionLabel: chart.versionLabel?.replace('FD', 'RD'),
remarks: chart.remarks?.replace('FD', 'RD')
};
}
return null;
}
get activeBaseSlabs() {
@@ -163,6 +176,7 @@ export class EditRecurringDepositProductComponent {
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
charts: this.recurringDepositProductCharts,
rdSlabs: this.activeBaseSlabs
};
}
@@ -170,13 +184,63 @@ export class EditRecurringDepositProductComponent {
get recurringDepositProductPreview() {
return {
...this.recurringDepositProduct,
charts: this.recurringDepositProductCharts,
rdSlabs: this.activeBaseSlabs
};
}
get recurringDepositProductCharts() {
const activeBaseVersion = this.activeBaseVersion;
if (!activeBaseVersion?.slabs?.length) {
return [];
}
const dateFormat = this.settingsService.dateFormat;
return [
{
name: activeBaseVersion.versionLabel || 'CBS Base Chart',
description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart',
fromDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) ||
activeBaseVersion.effectiveFromDate,
...(activeBaseVersion.effectiveToDate
? {
endDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) ||
activeBaseVersion.effectiveToDate
}
: {}),
isPrimaryGroupingByAmount: false,
locale: this.settingsService.language.code,
dateFormat,
chartSlabs: activeBaseVersion.slabs.map((slab: any) => {
const chartSlab: any = {
periodType: slab.periodTypeEnum,
fromPeriod: slab.fromPeriod,
toPeriod: slab.toPeriod,
annualInterestRate: slab.annualInterestRate,
description: slab.description || '',
incentives: [] as any[]
};
if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') {
chartSlab.amountRangeFrom = slab.amountRangeFrom;
}
if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') {
chartSlab.amountRangeTo = slab.amountRangeTo;
}
return chartSlab;
})
}
] as any[];
}
submit() {
const recurringDepositProduct: any = this.pruneEmptyValues({
...this.recurringDepositProduct,
charts: this.recurringDepositProductCharts,
charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
});
@@ -63,28 +63,6 @@
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="balanceSwapOption">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="msme">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>Nomination Facility</h4>
@@ -55,8 +55,6 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30,
daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60,
daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90,
balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false,
msme: this.recurringDepositProductsTemplate.msme ?? false,
nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false
});
@@ -80,8 +78,6 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
daysToInactive: [30],
daysToDormancy: [60],
daysToEscheat: [90],
balanceSwapOption: [false],
msme: [false],
nominationFacility: [false],
autoDebitFacility: [false]
});
@@ -93,8 +89,6 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
};
@@ -78,15 +78,6 @@
<mat-error>Account Type <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>Customer Segment</mat-label>
<mat-select formControlName="customerSegment">
@for (customerSegment of customerSegmentData; track customerSegment) {
<mat-option [value]="customerSegment">{{ customerSegment }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-field">
<mat-label>GL Account (Liability)</mat-label>
<mat-select formControlName="glAccountId" [compareWith]="compareById">
@@ -72,7 +72,6 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh
null,
Validators.required
],
customerSegment: [null],
glAccountId: [null],
effectiveTo: [''],
endDate: [''],
@@ -107,7 +106,6 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh
private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions);
this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? [];
this.cdr.detectChanges();
@@ -117,7 +115,6 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh
shortName: isEditing ? template.shortName : template.shortName || '',
description: template.description || '',
accountType: template.accountType ?? this.accountTypeData[0] ?? null,
customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null,
glAccountId: template.glAccountId ?? null,
effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate),
@@ -1,4 +1,4 @@
<!--
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
@@ -46,13 +46,6 @@
</div>
}
@if (recurringDepositProduct.customerSegment) {
<div class="flex-100 layout-row">
<span class="flex-40">Customer Segment:</span>
<span class="flex-60">{{ recurringDepositProduct.customerSegment }}</span>
</div>
}
@if (recurringDepositProduct.glAccountId) {
<div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span>
@@ -116,20 +109,6 @@
</div>
}
@if (recurringDepositProduct.balanceSwapOption !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Balance Swap Option:</span>
<span class="flex-60">{{ recurringDepositProduct.balanceSwapOption | yesNo }}</span>
</div>
}
@if (recurringDepositProduct.msme !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">MSME:</span>
<span class="flex-60">{{ recurringDepositProduct.msme | yesNo }}</span>
</div>
}
@if (recurringDepositProduct.nominationFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span>
@@ -144,6 +123,28 @@
</div>
}
@if (
recurringDepositProduct.minDepositAmount !== undefined || recurringDepositProduct.maxDepositAmount !== undefined
) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">Deposit Amount</h3>
<mat-divider class="flex-100"></mat-divider>
@if (recurringDepositProduct.minDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Minimum:</span>
<span class="flex-60">{{ recurringDepositProduct.minDepositAmount }}</span>
</div>
}
@if (recurringDepositProduct.maxDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Maximum:</span>
<span class="flex-60">{{ recurringDepositProduct.maxDepositAmount }}</span>
</div>
}
}
<!-- ── Terms ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@@ -324,8 +325,9 @@
</div>
}
<!-- ── Interest Rate Slabs ── -->
<!-- ── Interest Rate Slabs ── -->
@if (recurringDepositProduct.rdSlabs?.length) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@@ -7,6 +7,25 @@
-->
<form [formGroup]="recurringDepositProductTermsForm">
<div class="rdpt-section">
<div class="rdpt-section__header">
<span class="rdpt-section__badge"><i class="fas fa-coins"></i></span>
<span>Deposit Amount</span>
</div>
<div class="rdpt-body">
<div class="rdpt-grid rdpt-grid--three">
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minDepositAmount" min="0" step="0.01" />
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxDepositAmount" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
<div class="rdpt-section">
<div class="rdpt-section__header">
<span class="rdpt-section__badge"><i class="fas fa-chart-line"></i></span>
@@ -78,6 +97,26 @@
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Interest Compounding Period</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (opt of interestCompoundingPeriodTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Interest Compounding Period <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Days in Year</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required>
@for (opt of interestCalculationDaysInYearTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Days in Year <strong>is required</strong></mat-error>
</mat-form-field>
@if (recurringDepositProductTermsForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Average Period</mat-label>
@@ -85,7 +85,9 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
frequencyFixDay: isEditing ? (this.recurringDepositProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate
? this.parseDate(this.recurringDepositProductsTemplate.lastInterestDate)
: ''
: '',
minDepositAmount: this.recurringDepositProductsTemplate.minDepositAmount ?? '',
maxDepositAmount: this.recurringDepositProductsTemplate.maxDepositAmount ?? ''
});
}
@@ -145,7 +147,9 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
30,
[Validators.min(0)]
],
lastInterestDate: ['']
lastInterestDate: [''],
minDepositAmount: [''],
maxDepositAmount: ['']
});
}
@@ -156,14 +160,8 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
if (formValue.effectiveDate instanceof Date) {
formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || '';
}
if (formValue.lastInterestDate instanceof Date) {
formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || '';
}
for (const key in formValue) {
if (formValue[key] === '') {
delete formValue[key];
}
}
formValue.minDepositAmount = formValue.minDepositAmount;
formValue.maxDepositAmount = formValue.maxDepositAmount;
return formValue;
}
}
@@ -20,18 +20,6 @@
{{ 'labels.buttons.Create Recurring Deposit Product' | translate }}
</button>
</div>
<div class="in-block">
<button
mat-stroked-button
color="primary"
[routerLink]="['/products/recurring-deposit-products/base-interest-rates']"
matTooltip="Open the CBS base interest chart"
matTooltipPosition="above"
>
<fa-icon icon="chart-line" class="m-r-10"></fa-icon>
CBS Base Chart
</button>
</div>
</div>
<div class="container">
@@ -48,12 +48,6 @@
<span class="flex-60">{{ recurringDepositProduct.accountType }}</span>
</div>
}
@if (recurringDepositProduct.customerSegment) {
<div class="flex-100 layout-row">
<span class="flex-40">Customer Segment:</span>
<span class="flex-60">{{ recurringDepositProduct.customerSegment }}</span>
</div>
}
@if (recurringDepositProduct.glAccountId) {
<div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span>
@@ -99,14 +93,6 @@
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ recurringDepositProduct.dormancyAlert | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Balance Swap Option:</span>
<span class="flex-60">{{ recurringDepositProduct.balanceSwapOption | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">MSME:</span>
<span class="flex-60">{{ recurringDepositProduct.msme | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span>
<span class="flex-60">{{ recurringDepositProduct.nominationFacility | yesNo }}</span>
@@ -18,15 +18,6 @@
>
{{ 'labels.heading.General' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./interest-rates']"
routerLinkActive
#interestRates="routerLinkActive"
[active]="interestRates.isActive"
>
CBS Base Chart
</a>
@for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) {
<a
mat-tab-link
+1 -4
View File
@@ -19,7 +19,6 @@ import { VersionService } from 'app/system/version.service';
/** Environment Configuration */
import { environment } from '../../../environments/environment';
import { Subscription } from 'rxjs';
import { NgClass, DatePipe } from '@angular/common';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -30,9 +29,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
NgClass,
DatePipe
...STANDALONE_SHARED_IMPORTS
]
})
export class FooterComponent implements OnInit, OnDestroy {
@@ -26,7 +26,7 @@ import { TranslateService } from '@ngx-translate/core';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatCard, MatCardTitle, MatCardContent } from '@angular/material/card';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -41,7 +41,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatCardTitle,
MatCheckbox,
MatIconButton,
MatTooltip
@@ -9,7 +9,6 @@
/** Angular Imports */
import { Component } from '@angular/core';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { MatDivider } from '@angular/material/divider';
import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -26,8 +25,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTabLink,
RouterLinkActive,
MatTabNavPanel,
RouterOutlet,
MatDivider
RouterOutlet
]
})
export class CheckerInboxAndTasksComponent {
@@ -12,7 +12,6 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
/** Custom Services */
import { UsersService } from '../users.service';
@@ -35,8 +34,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatMenuModule,
MatIconButton,
MatTooltip
MatIconButton
]
})
export class ViewUserComponent {
+2 -2
View File
@@ -10,8 +10,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260507',
'hash': 'a12ba20d'
'version': '260508',
'hash': '19c0e98e'
},
'allow_switching_backend_instance': true
};