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 { ngOnInit(): void {
this.parseAndStore(SAMPLE_CKYC_FILE); this.parseAndStore(SAMPLE_CKYC_FILE);
// eslint-disable-next-line no-console
console.log( console.log(
'[CKYC] Sample loaded —', '[CKYC] Sample loaded —',
this.customers.length, this.customers.length,
@@ -74,7 +74,8 @@
<mifosx-recurring-deposits-account-interest-rate-chart-step <mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate" [recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates" [submittedOnDate]="submittedOnDate"
(tenureSelected)="handleTenureSelected($event)"
> >
</mifosx-recurring-deposits-account-interest-rate-chart-step> </mifosx-recurring-deposits-account-interest-rate-chart-step>
@@ -97,11 +98,13 @@
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate" [recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData" [recurringDepositAccountData]="recurringDepositAccountData"
[rdInterestRates]="rdInterestRates"
[rdNominee]="rdNominee" [rdNominee]="rdNominee"
[rdCurrencyFlags]="rdCurrencyFlags" [rdCurrencyFlags]="rdCurrencyFlags"
[rdTenureDays]="rdTenureDays" [rdTenureDays]="rdTenureDays"
[rdApplicableSlab]="rdApplicableSlab" [rdApplicableSlab]="rdApplicableSlab"
[rdMaturityAmount]="rdMaturityAmount"
[rdInterestEarned]="rdInterestEarned"
[rdDaysInYear]="rdDaysInYear"
(submitEvent)="submit()" (submitEvent)="submit()"
> >
</mifosx-recurring-deposits-account-preview-step> </mifosx-recurring-deposits-account-preview-step>
@@ -75,8 +75,10 @@ export class CreateRecurringDepositsAccountComponent {
recurringDepositsAccountTemplate: any; recurringDepositsAccountTemplate: any;
/** Recurring Deposit Account Product Template */ /** Recurring Deposit Account Product Template */
recurringDepositsAccountProductTemplate: any; recurringDepositsAccountProductTemplate: any;
/** Active CBS base interest rate chart */ /** Selected tenure value and frequency from calculator */
rdInterestRates: any[] = []; selectedTenureValue: number | null = null;
selectedTenureFrequencyId: number | null = null;
selectedDepositAmount: number | null = null;
constructor() { constructor() {
this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => { this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => {
@@ -90,14 +92,13 @@ export class CreateRecurringDepositsAccountComponent {
*/ */
setTemplate($event: any) { setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event; this.recurringDepositsAccountProductTemplate = $event;
this.productsService.getRdBaseInterestRates().subscribe({ }
next: (rates: any[]) => {
this.rdInterestRates = rates || []; /** Captures tenure selection from calculator */
}, handleTenureSelected($event: any) {
error: () => { this.selectedTenureValue = $event.depositPeriod;
this.rdInterestRates = []; this.selectedTenureFrequencyId = $event.depositPeriodFrequencyId;
} this.selectedDepositAmount = $event.depositAmount;
});
} }
/** Get Recurring Deposit Account Details Form Data */ /** Get Recurring Deposit Account Details Form Data */
@@ -123,17 +124,31 @@ export class CreateRecurringDepositsAccountComponent {
} }
get rdTenureDays(): number | null { get rdTenureDays(): number | null {
return this.recurringDepositsAccountChartStep?.tenureDays ?? null; return this.recurringDepositsAccountChartStep?.matchResult?.tenureDays ?? null;
} }
get rdApplicableSlab(): any | 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() { get rdCurrencyFlags() {
return { return {
balanceSwapOption: this.recurringDepositsAccountCurrencyStep?.balanceSwapOption ?? false,
msme: this.recurringDepositsAccountCurrencyStep?.msme ?? false,
autoDebitFacility: this.recurringDepositsAccountCurrencyStep?.autoDebitFacility ?? false autoDebitFacility: this.recurringDepositsAccountCurrencyStep?.autoDebitFacility ?? false
}; };
} }
@@ -145,34 +160,93 @@ export class CreateRecurringDepositsAccountComponent {
const locale = this.settingsService.language.code; const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
const pt = this.recurringDepositsAccountProductTemplate;
const details = this.recurringDepositAccount; const details = this.recurringDepositAccount;
const currencyStep = this.recurringDepositsAccountCurrencyStep; 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 = { const recurringDepositAccount: any = {
...details, ...details,
...terms,
...settings,
...productDefaults,
clientId: this.recurringDepositsAccountTemplate.clientId, clientId: this.recurringDepositsAccountTemplate.clientId,
charges: [], mandatoryRecommendedDepositAmount: pt?.minDepositAmount ?? 0,
isCalendarInherited: false, recurringFrequency: pt?.recurringFrequency ?? 1,
mandatoryRecommendedDepositAmount: 0, recurringFrequencyType: pt?.recurringFrequencyType ?? 2, // Monthly
recurringFrequency: 1,
recurringFrequencyType: 2,
submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat), submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat),
expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat), expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat),
dateFormat, dateFormat,
monthDayFormat, monthDayFormat,
locale, locale,
// Currency step overrides charts: [
balanceSwapOption: currencyStep?.balanceSwapOption ?? details.balanceSwapOption ?? false, {
msme: currencyStep?.msme ?? details.msme ?? false, chartSlabs:
autoDebitFacility: currencyStep?.autoDebitFacility ?? details.autoDebitFacility ?? false 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) // Tenure override from interest rate chart calculator
const tenureDays = this.rdTenureDays; if (this.selectedTenureValue && this.selectedTenureFrequencyId !== null) {
if (tenureDays && tenureDays > 0) { recurringDepositAccount.depositPeriod = this.selectedTenureValue;
recurringDepositAccount.depositPeriod = tenureDays; recurringDepositAccount.depositPeriodFrequencyId = this.selectedTenureFrequencyId;
recurringDepositAccount.depositPeriodFrequencyId = 0; // 0 = Days } 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 // Nominee from currency step
@@ -71,7 +71,7 @@
<mifosx-recurring-deposits-account-interest-rate-chart-step <mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate" [recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates" [submittedOnDate]="submittedOnDate"
> >
</mifosx-recurring-deposits-account-interest-rate-chart-step> </mifosx-recurring-deposits-account-interest-rate-chart-step>
</mat-step> </mat-step>
@@ -94,7 +94,11 @@
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate" [recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData" [recurringDepositAccountData]="recurringDepositAccountData"
[rdInterestRates]="rdInterestRates" [rdTenureDays]="rdTenureDays"
[rdApplicableSlab]="rdApplicableSlab"
[rdMaturityAmount]="rdMaturityAmount"
[rdInterestEarned]="rdInterestEarned"
[rdDaysInYear]="rdDaysInYear"
(submitEvent)="submit()" (submitEvent)="submit()"
> >
</mifosx-recurring-deposits-account-preview-step> </mifosx-recurring-deposits-account-preview-step>
@@ -67,12 +67,37 @@ export class EditRecurringDepositAccountComponent {
@ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true }) @ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true })
recurringDepositAccountChargesStep: RecurringDepositsAccountChargesStepComponent; 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 */ /** Recurring Deposits Account And Template */
recurringDepositsAccountAndTemplate: any; recurringDepositsAccountAndTemplate: any;
/** Recurring Deposit Account Product Template */ /** Recurring Deposit Account Product Template */
recurringDepositsAccountProductTemplate: any; recurringDepositsAccountProductTemplate: any;
/** Active CBS base interest rate chart */
rdInterestRates: any[] = [];
constructor() { constructor() {
this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => { this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => {
@@ -86,14 +111,6 @@ export class EditRecurringDepositAccountComponent {
*/ */
setTemplate($event: any) { setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event; this.recurringDepositsAccountProductTemplate = $event;
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
} }
/** Get Recurring Deposit Account Details Form Data */ /** Get Recurring Deposit Account Details Form Data */
@@ -169,6 +186,15 @@ export class EditRecurringDepositAccountComponent {
this.recurringDepositAccount.expectedFirstDepositOnDate, this.recurringDepositAccount.expectedFirstDepositOnDate,
dateFormat dateFormat
), ),
charts: [
{
chartSlabs:
this.recurringDepositsAccountProductTemplate?.accountChart?.chartSlabs ??
this.recurringDepositsAccountProductTemplate?.activeChart?.chartSlabs ??
this.recurringDepositsAccountProductTemplate?.interestRateCharts?.[0]?.chartSlabs ??
[]
}
],
dateFormat, dateFormat,
monthDayFormat, monthDayFormat,
locale locale
@@ -27,22 +27,6 @@
</mat-form-field> </mat-form-field>
</div> </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 racc-alert--editable">
<div class="racc-alert__content"> <div class="racc-alert__content">
<h4>Auto Debit Facility</h4> <h4>Auto Debit Facility</h4>
@@ -31,15 +31,11 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges
nomineeData: any = null; nomineeData: any = null;
// Editable feature toggles (pre-filled from product, overridable by user) // Editable feature toggles (pre-filled from product, overridable by user)
balanceSwapOption = false;
msme = false;
autoDebitFacility = false; autoDebitFacility = false;
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes['recurringDepositsAccountProductTemplate']?.currentValue) { if (changes['recurringDepositsAccountProductTemplate']?.currentValue) {
const pt = changes['recurringDepositsAccountProductTemplate'].currentValue; const pt = changes['recurringDepositsAccountProductTemplate'].currentValue;
this.balanceSwapOption = !!pt.balanceSwapOption;
this.msme = !!pt.msme;
this.autoDebitFacility = !!pt.autoDebitFacility; this.autoDebitFacility = !!pt.autoDebitFacility;
} }
} }
@@ -51,8 +47,6 @@ export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges
/** Called by parent to collect currency step values */ /** Called by parent to collect currency step values */
get currencyStepData() { get currencyStepData() {
return { return {
balanceSwapOption: this.balanceSwapOption,
msme: this.msme,
autoDebitFacility: this.autoDebitFacility, autoDebitFacility: this.autoDebitFacility,
...(this.nomineeData ? { nominee: this.nomineeData } : {}) ...(this.nomineeData ? { nominee: this.nomineeData } : {})
}; };
@@ -338,8 +338,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
], ],
fieldOfficerId: [''], fieldOfficerId: [''],
externalId: [''], externalId: [''],
balanceSwapOption: [false],
msme: [false],
autoDebitFacility: [false] autoDebitFacility: [false]
}); });
} }
@@ -375,8 +373,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
this.loadClientAccounts(); this.loadClientAccounts();
} }
this.recurringDepositAccountDetailsForm.patchValue({ this.recurringDepositAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
autoDebitFacility: !!response.autoDebitFacility autoDebitFacility: !!response.autoDebitFacility
}); });
this.cdr.detectChanges(); this.cdr.detectChanges();
@@ -6,47 +6,359 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<div class="layout-column gap-2px"> <div class="fdrc-wrapper">
<!-- ── Tenure Calculator ───────────────────────────────────────── --> <!-- ══ Chart Header Card ════════════════════════════════════════════════ -->
@if (_activeRdVersion) { @if (interestRateChartData.length > 0) {
<div class="tenure-calc"> <div class="fdrc-header-card">
<div class="tenure-calc__header"> <div class="fdrc-header-card__left">
<i class="fas fa-calculator"></i> <div class="fdrc-header-card__icon">
<span>Tenure &amp; Interest Calculator</span> <i class="fas fa-chart-line"></i>
</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> </div>
<div>
@if (tenureDays && tenureDays > 0) { <div class="fdrc-header-card__title">{{ chartName || 'RD Interest Rate Chart' }}</div>
@if (applicableSlab) { @if (chartDescription) {
<div class="tenure-calc__result"> <div class="fdrc-header-card__desc">{{ chartDescription }}</div>
<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>
<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>
</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> </div>
@@ -6,125 +6,836 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
// ── Tenure Calculator ───────────────────────────────────────────────────────── $border: #e2e8f0;
.tenure-calc { $gray-50: #f8f9fa;
border: 1px solid #e2e8f0; $gray-100: #f1f5f9;
border-radius: 10px; $gray-200: #e8eaed;
overflow: hidden; $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; background: #fff;
box-shadow: 0 1px 4px rgb(0 0 0 / 5%); border: 1px solid $border;
margin-bottom: 1.25rem; 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 { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: space-between;
background: #f0f9ff; padding: 14px 20px;
border-bottom: 1px solid #bae6fd; background: $gray-50;
padding: 0.65rem 1rem; border-bottom: 1px solid $gray-200;
font-size: 0.85rem; }
&__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; font-weight: 700;
color: #0369a1;
text-transform: uppercase; 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 { 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 { &__body {
padding: 1rem 1.25rem; padding: 12px 14px;
}
&__input-row {
display: flex; display: flex;
align-items: flex-start; flex-direction: column;
gap: 1rem; gap: 8px;
}
}
.fdrc-maturity-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
&__label {
font-size: 12px;
color: $gray-600;
} }
&__field { &__value {
width: 260px; font-size: 13px;
} font-weight: 700;
color: $gray-900;
&__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;
}
&--highlight { &--highlight {
background: #f0f9ff; color: $green;
}
&--big {
font-size: 16px;
color: $green;
} }
} }
&__result-label { &--interest {
color: #64748b; padding-top: 4px;
font-weight: 500; margin-top: 4px;
border-top: 1px dashed $border;
} }
&__result-value { &--total {
color: #1e293b; padding-top: 8px;
font-weight: 600; margin-top: 4px;
border-top: 1.5px solid $gray-200;
} }
}
&__result-rate { .fdrc-payout-hint {
font-size: 1.25rem; display: flex;
font-weight: 800; align-items: center;
color: #0369a1; gap: 8px;
margin-top: 12px;
font-size: 12px;
color: $gray-600;
font-style: italic;
i {
color: $amber;
} }
}
&__no-match { // ── Rate Chart Table Card ─────────────────────────────────────────────────
margin-top: 0.75rem;
padding: 0.6rem 1rem; .fdrc-table-card {
border-radius: 8px; background: #fff;
background: #fef2f2; border: 1px solid $border;
color: #dc2626; border-radius: 12px;
font-size: 0.85rem; overflow: hidden;
font-weight: 600;
&__header {
display: flex; display: flex;
align-items: center; 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 { i {
font-size: 13px; color: $blue;
} }
} }
&__title {
font-size: 14px;
font-weight: 700;
color: $gray-900;
}
} }
.margin-t { .fdrc-slab-count {
margin-top: 2em; 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%; width: 100%;
min-width: 600px;
}
tr.incentives-row { .fdrc-th {
height: 0; 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 { .fdrc-td {
padding: 0; padding: 12px 16px !important;
} font-size: 13px;
color: $gray-700;
vertical-align: middle;
.incentives { &--period {
overflow: hidden; display: flex;
} flex-direction: column;
gap: 2px;
}
.mat-card { &--na {
border-radius: 0; 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/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
/** Angular Imports */ import { Component, OnInit, Input, OnChanges, ViewChild, Output, EventEmitter, inject } from '@angular/core';
import { Component, OnInit, Input, OnChanges, ViewChild, Output, EventEmitter } from '@angular/core'; import { RecurringDepositsService } from '../../recurring-deposits.service';
import { import {
MatTable, MatTable,
MatColumnDef, MatColumnDef,
@@ -30,6 +30,18 @@ import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FormsModule } from '@angular/forms'; 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 * Recurring Deposits Account Interest Rate Chart Step
*/ */
@@ -64,45 +76,44 @@ import { FormsModule } from '@angular/forms';
TitleCasePipe, TitleCasePipe,
DateFormatPipe, DateFormatPipe,
FormatNumberPipe, FormatNumberPipe,
YesnoPipe YesnoPipe,
DecimalPipe
] ]
}) })
export class RecurringDepositsAccountInterestRateChartStepComponent implements OnInit, OnChanges { export class RecurringDepositsAccountInterestRateChartStepComponent implements OnInit, OnChanges {
@Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountTemplate: any;
@Input() recurringDepositsAccountProductTemplate: any; @Input() recurringDepositsAccountProductTemplate: any;
@Input() submittedOnDate: Date | null = null;
/** @Output() tenureSelected = new EventEmitter<{
* CBS base rate chart loaded from the active base-interest-rate service. depositPeriod: number;
* When set, this table is shown instead of the legacy accountChart.chartSlabs. depositPeriodFrequencyId: number;
*/ depositAmount?: number;
@Input() set rdInterestRates(rates: any[]) { maturityAmount?: number;
this._rdInterestRates = rates || []; interestEarned?: number;
this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null; 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[] = []; chartSlabsDisplayedColumns: string[] = [
_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[] = [
'period', 'period',
'amountRange', 'amountRange',
'interest', 'interest',
'description', 'description',
'actions' 'actions'
]; ];
/** Columns to be displayed in incentives sub-table. */
incentivesDisplayedColumns: string[] = [ incentivesDisplayedColumns: string[] = [
'entityType', 'entityType',
'attributeName', 'attributeName',
@@ -111,38 +122,242 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
'incentiveType', 'incentiveType',
'amount' 'amount'
]; ];
/** Additional Column to display incentives table */
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
/** Expand Chart Slab Index used in the view */
expandChartSlabIndex: number;
/** Tenure entered by user (in days) */ expandChartSlabIndex: number | null = null;
tenureDays: number | null = null;
@ViewChild('chartsTable', { static: true }) chartsTableRef: MatTable<Element>; @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 */ matchResult: SlabMatchResult | null = null;
get applicableSlab(): any | null { validationWarning: string | null = null;
if (!this.tenureDays || !this._activeRdVersion?.slabs?.length) return null;
const days = this.tenureDays; /** Period unit options */
return ( readonly allowedPeriodUnits: RdPeriodUnit[] = [
this._activeRdVersion.slabs.find((s: any) => { 'Months',
const from = s.fromPeriod ?? 0; 'Years'
const to = s.toPeriod ?? Infinity; ];
return days >= from && days <= to;
}) ?? null private recurringDepositsService = inject(RecurringDepositsService);
);
}
ngOnChanges() { ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) { const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; if (template) {
if (this.chartsTableRef) {
this.chartsTableRef.renderRows();
}
this.resetCalculator();
this.tenurePeriodUnit = this.allowedPeriodUnits[1]; // Default to Months
} }
} }
ngOnInit() { // Convert entered tenure to Days for slab matching
this.interestRateChartData = []; 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> <span class="preview-value">{{ recurringDepositsAccountProductTemplate.taxGroup.name }}</span>
</div> </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"> <div class="preview-field">
<span class="preview-label">Auto Debit Facility</span> <span class="preview-label">Auto Debit Facility</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="rdCurrencyFlags?.autoDebitFacility"> <span class="preview-value preview-badge" [class.preview-badge--yes]="rdCurrencyFlags?.autoDebitFacility">
@@ -497,7 +485,7 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Inactive</span> <span class="preview-dorm-step__label">Inactive</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct != null) { @if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct !== null) {
After {{ recurringDepositsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction After {{ recurringDepositsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction
} @else { } @else {
Not configured Not configured
@@ -511,7 +499,7 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Dormant</span> <span class="preview-dorm-step__label">Dormant</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct != null) { @if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct !== null) {
{{ recurringDepositsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive {{ recurringDepositsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive
} @else { } @else {
Not configured Not configured
@@ -525,7 +513,7 @@
<div class="preview-dorm-step__body"> <div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Escheat</span> <span class="preview-dorm-step__label">Escheat</span>
<span class="preview-dorm-step__days"> <span class="preview-dorm-step__days">
@if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct != null) { @if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct !== null) {
{{ recurringDepositsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant {{ recurringDepositsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant
} @else { } @else {
Not configured Not configured
@@ -637,183 +625,225 @@
</div> </div>
} }
<!-- ══ 7. Interest Rate (Tenure-based result) ════════════════════ --> <!-- ══ 6. Maturity Summary ═══════════════════════════════════ -->
@if (_activeRdVersion || recurringDepositsAccountProductTemplate?.accountChart) { @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">
<div class="preview-section__header"> <div class="preview-section__header">
<i class="fas fa-chart-line preview-section__icon"></i> <i class="fas fa-chart-line preview-section__icon"></i>
<span class="preview-section__title">Interest Rate</span> <span class="preview-section__title">{{ 'labels.heading.Interest Rate Chart' | translate }}</span>
@if (_activeRdVersion) { @if (rdTenureDays) {
<span class="preview-section__badge">{{ _activeRdVersion.versionLabel }}</span> <span class="preview-section__badge preview-section__badge--tenure">
<i class="fas fa-calendar-check"></i> Selected Tenure: {{ selectedTenureLabel }}
</span>
} }
</div> </div>
<div class="preview-body"> <div class="preview-body">
@if (_activeRdVersion) { <!-- Selected Tenure + Rate Banner -->
@if (rdTenureDays && rdApplicableSlab) { @if (rdTenureDays && matchedSlabIndex >= 0) {
<div class="preview-tenure-result"> <div class="preview-tenure-banner">
<div class="preview-tenure-result__row"> <div class="preview-tenure-banner__left">
<span class="preview-label">Tenure</span> <i class="fas fa-calendar-check"></i>
<span class="preview-value">{{ rdTenureDays }} Days</span> <div>
</div> <span class="preview-tenure-banner__label">Your RD Tenure</span>
<div class="preview-tenure-result__row"> <span class="preview-tenure-banner__value">{{ selectedTenureLabel }}</span>
<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
>
</div> </div>
</div> </div>
} @else { <div class="preview-tenure-banner__divider"></div>
<span class="preview-muted">No tenure entered — interest rate not calculated</span> <div class="preview-tenure-banner__right">
} <span class="preview-tenure-banner__rate-label">Applicable Interest Rate</span>
} @else { <span class="preview-tenure-banner__rate"
<div class="preview-grid preview-grid--three"> >{{ interestRateChartData[matchedSlabIndex]?.annualInterestRate | formatNumber }}%</span
@if (recurringDepositsAccountProductTemplate?.accountChart?.name) { >
<div class="preview-field"> </div>
<span class="preview-label">{{ 'labels.inputs.name' | translate }}</span> <div class="preview-tenure-banner__slab">
<span class="preview-value">{{ recurringDepositsAccountProductTemplate.accountChart.name }}</span> <i class="fas fa-layer-group"></i>
</div> Slab: {{ interestRateChartData[matchedSlabIndex]?.fromPeriod }}–{{
} interestRateChartData[matchedSlabIndex]?.toPeriod ?? '∞'
@if (recurringDepositsAccountProductTemplate?.accountChart?.fromDate) { }}
<div class="preview-field"> {{ interestRateChartData[matchedSlabIndex]?.periodType?.value }}
<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> </div>
</div> </div>
}
@if (interestRateChartData?.length) { <div class="preview-grid preview-grid--three" style="margin-bottom: 1rem">
<table @if (chartName) {
class="mat-elevation-z1 irc-table preview-table" <div class="preview-field">
mat-table <span class="preview-label">{{ 'labels.inputs.name' | translate }}</span>
[dataSource]="interestRateChartData" <span class="preview-value">{{ chartName }}</span>
multiTemplateDataRows </div>
> }
<ng-container matColumnDef="period"> @if (chartFromDate) {
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th> <div class="preview-field">
<td mat-cell *matCellDef="let chartSlab"> <span class="preview-label">Valid From</span>
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }}&nbsp;{{ <span class="preview-value">{{ chartFromDate | dateFormat }}</span>
chartSlab.periodType?.value | translateKey: 'catalogs' </div>
}} }
</td> <div class="preview-field">
</ng-container> <span class="preview-label">Primary Grouping by Amount</span>
<ng-container matColumnDef="amountRange"> <span class="preview-value preview-badge" [class.preview-badge--yes]="isPrimaryGroupingByAmount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th> {{ isPrimaryGroupingByAmount | yesNo }}
<td mat-cell *matCellDef="let chartSlab"> </span>
{{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} </div>
</td> </div>
</ng-container>
<ng-container matColumnDef="interest"> @if (interestRateChartData?.length) {
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th> <table
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate | formatNumber }} %</td> class="mat-elevation-z1 irc-table preview-table"
</ng-container> mat-table
<ng-container matColumnDef="description"> [dataSource]="interestRateChartData"
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th> multiTemplateDataRows
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td> >
</ng-container> <ng-container matColumnDef="period">
<ng-container matColumnDef="actions"> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th> <td mat-cell *matCellDef="let chartSlab">
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex"> {{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }}&nbsp;{{
<button chartSlab.periodType?.value | translateKey: 'catalogs'
mat-button }}
color="primary" </td>
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex" </ng-container>
> <ng-container matColumnDef="amountRange">
<span *ngIf="expandChartSlabIndex !== chartSlabIndex"> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<fa-icon icon="eye" class="m-r-10"></fa-icon> <td mat-cell *matCellDef="let chartSlab">
{{ 'labels.buttons.View Incentives' | translate }} {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }}
</span> </td>
<span *ngIf="expandChartSlabIndex === chartSlabIndex"> </ng-container>
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon> <ng-container matColumnDef="interest">
{{ 'labels.buttons.Hide Incentives' | translate }} <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
</span> <td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate | formatNumber }} %</td>
</button> </ng-container>
</td> <ng-container matColumnDef="description">
</ng-container> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<ng-container matColumnDef="incentives"> <td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
<td </ng-container>
mat-cell <ng-container matColumnDef="actions">
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex" <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
[attr.colspan]="chartSlabsDisplayedColumns.length" <td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
> >
<div <fa-icon
class="layout-row-wrap flex-100 incentives" [icon]="expandChartSlabIndex === chartSlabIndex ? 'eye-slash' : 'eye'"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'" class="m-r-10"
> ></fa-icon>
<mat-card class="layout-row-wrap flex-100"> {{
<h4 class="m-b-10 flex-13">{{ 'labels.heading.Incentives' | translate }}</h4> (expandChartSlabIndex === chartSlabIndex
<table ? 'labels.buttons.Hide Incentives'
class="flex-100 mat-elevation-z1" : 'labels.buttons.View Incentives'
mat-table ) | translate
[dataSource]="chartSlab.incentives" }}
*ngIf="chartSlab.incentives?.length" </button>
> </td>
<ng-container matColumnDef="entityType"> </ng-container>
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th> <ng-container matColumnDef="incentives">
<td mat-cell *matCellDef="let incentive">{{ incentive.entityType?.value }}</td> <td
</ng-container> mat-cell
<ng-container matColumnDef="attributeName"> *matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th> [attr.colspan]="chartSlabsDisplayedColumns?.length"
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeName?.value }}</td> >
</ng-container> <div
<ng-container matColumnDef="conditionType"> class="layout-row-wrap flex-100 incentives"
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th> [@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
<td mat-cell *matCellDef="let incentive">{{ incentive.conditionType?.value | titlecase }}</td> >
</ng-container> <mat-card class="layout-row-wrap flex-100">
<ng-container matColumnDef="attributeValue"> <h4 class="m-b-10 flex-13">{{ 'labels.heading.Incentives' | translate }}</h4>
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th> <table
<td mat-cell *matCellDef="let incentive">{{ incentive.attributeValueDesc }}</td> class="flex-100 mat-elevation-z1"
</ng-container> mat-table
<ng-container matColumnDef="incentiveType"> [dataSource]="chartSlab.incentives"
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th> *ngIf="chartSlab.incentives?.length"
<td mat-cell *matCellDef="let incentive">{{ incentive.incentiveType?.value }}</td> >
</ng-container> <ng-container matColumnDef="entityType">
<ng-container matColumnDef="amount"> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th> <td mat-cell *matCellDef="let incentive">{{ incentive.entityType?.value }}</td>
<td mat-cell *matCellDef="let incentive">{{ incentive.amount }}</td> </ng-container>
</ng-container> <ng-container matColumnDef="attributeName">
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr> <td mat-cell *matCellDef="let incentive">{{ incentive.attributeName?.value }}</td>
</table> </ng-container>
</mat-card> <ng-container matColumnDef="conditionType">
</div> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
</td> <td mat-cell *matCellDef="let incentive">{{ incentive.conditionType?.value | titlecase }}</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr> <ng-container matColumnDef="attributeValue">
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<tr <td mat-cell *matCellDef="let incentive">{{ incentive.attributeValueDesc }}</td>
mat-row </ng-container>
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" <ng-container matColumnDef="incentiveType">
class="incentives-row" <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
></tr> <td mat-cell *matCellDef="let incentive">{{ incentive.incentiveType?.value }}</td>
</table> </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> </div>
@@ -181,32 +181,125 @@
margin-top: 1em; margin-top: 1em;
} }
// ── Preview Tenure Result Card ───────────────────────────────────────────────── // ── Section badge variant for tenure ─────────────────────────────────────────
.preview-tenure-result { .preview-section__badge--tenure {
border: 1px solid #e0f2fe; 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; border-radius: 10px;
overflow: hidden; margin-bottom: 16px;
flex-wrap: wrap;
&__row { &__left {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 0.65rem 1rem; gap: 10px;
border-bottom: 1px solid #f0f9ff;
&:last-child { > i {
border-bottom: none; font-size: 20px;
} color: #16a34a;
&--highlight {
background: #f0f9ff;
} }
} }
&__rate { &__label {
font-size: 1.5rem; 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; 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() rdCurrencyFlags: any = null;
@Input() rdTenureDays: number | null = null; @Input() rdTenureDays: number | null = null;
@Input() rdApplicableSlab: any = null; @Input() rdApplicableSlab: any = null;
@Input() set rdInterestRates(rates: any[]) { @Input() rdMaturityAmount: number | null = null;
this._rdInterestRates = rates || []; @Input() rdInterestEarned: number | null = null;
this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null; @Input() rdDaysInYear: number | null = null;
}
/** Output the submit action */ /** Output the submit action */
@Output() submitEvent = new EventEmitter(); @Output() submitEvent = new EventEmitter();
@@ -96,9 +96,6 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
return this.recurringDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—'; return this.recurringDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—';
} }
_rdInterestRates: any[] = [];
_activeRdVersion: any = null;
readonly rdSlabColumns = [ readonly rdSlabColumns = [
'srNo', 'srNo',
'description', 'description',
@@ -117,8 +114,57 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
'date', 'date',
'repaymentsEvery' 'repaymentsEvery'
]; ];
/** Interest Rate Chart Data */ get interestRateChartData(): any[] {
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. */ /** Columns to be displayed in interest rate chart table. */
chartSlabsDisplayedColumns: any[] = [ chartSlabsDisplayedColumns: any[] = [
'period', 'period',
@@ -144,8 +190,68 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
constructor() {} constructor() {}
ngOnChanges() { ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) { const template = this.recurringDepositsAccountProductTemplate || this.recurringDepositsAccountTemplate;
this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; 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> <span>Interest Rate &amp; Posting Terms</span>
</div> </div>
<div class="rdpt-body"> <div class="rdpt-body">
@if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate != null) { @if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate !== null) {
<div class="rdpt-banner"> <div class="rdpt-banner">
<span class="rdpt-banner__icon">ℹ️</span> <span class="rdpt-banner__icon">ℹ️</span>
<span> <span>
@@ -92,7 +92,12 @@ export class InterestRateChartTabComponent {
*/ */
constructor() { constructor() {
this.route.parent.data.subscribe((data: { recurringDepositsAccountData: any }) => { 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 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 { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-tax-group.component';
import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components'; 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 { 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 { 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 { ViewFixedDepositProductComponent } from './fixed-deposit-products/view-fixed-deposit-product/view-fixed-deposit-product.component';
import { ViewDividendComponent } from './share-products/view-dividend/view-dividend.component'; import { ViewDividendComponent } from './share-products/view-dividend/view-dividend.component';
@@ -655,14 +654,6 @@ const routes: Routes = [
path: 'recurring-deposit-products', path: 'recurring-deposit-products',
data: { title: 'Recurring Deposit Products', breadcrumb: 'Recurring Deposit Products' }, data: { title: 'Recurring Deposit Products', breadcrumb: 'Recurring Deposit Products' },
children: [ children: [
{
path: 'base-interest-rates',
component: BaseRecurringDepositInterestRateChartComponent,
data: { title: 'CBS Base Interest Rates', breadcrumb: 'Base Interest Rates' },
resolve: {
recurringDepositProductsTemplate: RecurringDepositProductsTemplateResolver
}
},
{ {
path: 'create', path: 'create',
component: CreateRecurringDepositProductComponent, 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 { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { Dates } from 'app/core/utils/dates';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 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 { RecurringDepositProductPreviewStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
@@ -72,6 +73,7 @@ export class CreateRecurringDepositProductComponent {
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private accounting = inject(Accounting); private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(RecurringDepositProductDetailsStepComponent, { static: true }) @ViewChild(RecurringDepositProductDetailsStepComponent, { static: true })
recurringDepositProductDetailsStep: RecurringDepositProductDetailsStepComponent; recurringDepositProductDetailsStep: RecurringDepositProductDetailsStepComponent;
@@ -112,7 +114,7 @@ export class CreateRecurringDepositProductComponent {
}); });
this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
// Load the CBS base chart as the reference for the editable table // Load the CBS base chart as the reference for the editable table
this.productsService.getRdBaseInterestRates().subscribe({ this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => { next: (rates: any[]) => {
this.rdInterestRates = rates || []; this.rdInterestRates = rates || [];
}, },
@@ -151,7 +153,18 @@ export class CreateRecurringDepositProductComponent {
} }
get activeBaseVersion() { 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() { get activeBaseSlabs() {
@@ -166,17 +179,66 @@ export class CreateRecurringDepositProductComponent {
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting, ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
charts: this.recurringDepositProductCharts,
rdSlabs: this.activeBaseSlabs 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() { submit() {
// TODO: Update once language and date settings are setup const recurringDepositProduct: any = this.pruneEmptyValues({
const recurringDepositProduct = {
...this.recurringDepositProduct, ...this.recurringDepositProduct,
charges: this.recurringDepositProduct.charges.map((charge: any) => ({ id: charge.id })), charts: this.recurringDepositProductCharts,
locale: this.settingsService.language.code // locale required for depositAmount charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
}; locale: this.settingsService.language.code
});
if (!recurringDepositProduct.description) { if (!recurringDepositProduct.description) {
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 { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { Dates } from 'app/core/utils/dates';
import { MatTabsModule } from '@angular/material/tabs'; 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 { 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'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -70,6 +71,7 @@ export class EditRecurringDepositProductComponent {
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private accounting = inject(Accounting); private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(RecurringDepositProductDetailsStepComponent, { static: true }) @ViewChild(RecurringDepositProductDetailsStepComponent, { static: true })
recurringDepositProductDetailsStep: RecurringDepositProductDetailsStepComponent; recurringDepositProductDetailsStep: RecurringDepositProductDetailsStepComponent;
@@ -107,7 +109,7 @@ export class EditRecurringDepositProductComponent {
constructor() { constructor() {
this.route.data.subscribe((data: { recurringDepositProductAndTemplate: any }) => { this.route.data.subscribe((data: { recurringDepositProductAndTemplate: any }) => {
this.recurringDepositProductsTemplate = data.recurringDepositProductAndTemplate; this.recurringDepositProductsTemplate = data.recurringDepositProductAndTemplate;
this.productsService.getRdBaseInterestRates().subscribe({ this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => { next: (rates: any[]) => {
this.rdInterestRates = rates || []; this.rdInterestRates = rates || [];
}, },
@@ -148,7 +150,18 @@ export class EditRecurringDepositProductComponent {
} }
get activeBaseVersion() { 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() { get activeBaseSlabs() {
@@ -163,6 +176,7 @@ export class EditRecurringDepositProductComponent {
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting, ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
charts: this.recurringDepositProductCharts,
rdSlabs: this.activeBaseSlabs rdSlabs: this.activeBaseSlabs
}; };
} }
@@ -170,13 +184,63 @@ export class EditRecurringDepositProductComponent {
get recurringDepositProductPreview() { get recurringDepositProductPreview() {
return { return {
...this.recurringDepositProduct, ...this.recurringDepositProduct,
charts: this.recurringDepositProductCharts,
rdSlabs: this.activeBaseSlabs 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() { submit() {
const recurringDepositProduct: any = this.pruneEmptyValues({ const recurringDepositProduct: any = this.pruneEmptyValues({
...this.recurringDepositProduct, ...this.recurringDepositProduct,
charts: this.recurringDepositProductCharts,
charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })), charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code locale: this.settingsService.language.code
}); });
@@ -63,28 +63,6 @@
</mat-radio-group> </mat-radio-group>
</div> </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">
<div class="rdpc-alert__content"> <div class="rdpc-alert__content">
<h4>Nomination Facility</h4> <h4>Nomination Facility</h4>
@@ -55,8 +55,6 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30, daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30,
daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60, daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60,
daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90, daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90,
balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false,
msme: this.recurringDepositProductsTemplate.msme ?? false,
nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false, nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false
}); });
@@ -80,8 +78,6 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
daysToInactive: [30], daysToInactive: [30],
daysToDormancy: [60], daysToDormancy: [60],
daysToEscheat: [90], daysToEscheat: [90],
balanceSwapOption: [false],
msme: [false],
nominationFacility: [false], nominationFacility: [false],
autoDebitFacility: [false] autoDebitFacility: [false]
}); });
@@ -93,8 +89,6 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
currencyCode: formValue.currencyCode, currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal, digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert, dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility, nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility autoDebitFacility: formValue.autoDebitFacility
}; };
@@ -78,15 +78,6 @@
<mat-error>Account Type <strong>is required</strong></mat-error> <mat-error>Account Type <strong>is required</strong></mat-error>
</mat-form-field> </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-form-field appearance="outline" class="rdpd-field">
<mat-label>GL Account (Liability)</mat-label> <mat-label>GL Account (Liability)</mat-label>
<mat-select formControlName="glAccountId" [compareWith]="compareById"> <mat-select formControlName="glAccountId" [compareWith]="compareById">
@@ -72,7 +72,6 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh
null, null,
Validators.required Validators.required
], ],
customerSegment: [null],
glAccountId: [null], glAccountId: [null],
effectiveTo: [''], effectiveTo: [''],
endDate: [''], endDate: [''],
@@ -107,7 +106,6 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh
private applyTemplate(template: any) { private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions); this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions);
this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? [];
this.cdr.detectChanges(); this.cdr.detectChanges();
@@ -117,7 +115,6 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh
shortName: isEditing ? template.shortName : template.shortName || '', shortName: isEditing ? template.shortName : template.shortName || '',
description: template.description || '', description: template.description || '',
accountType: template.accountType ?? this.accountTypeData[0] ?? null, accountType: template.accountType ?? this.accountTypeData[0] ?? null,
customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null,
glAccountId: template.glAccountId ?? null, glAccountId: template.glAccountId ?? null,
effectiveTo: this.parseDate(template.effectiveTo), effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate), endDate: this.parseDate(template.endDate),
@@ -1,4 +1,4 @@
<!-- <!--
Copyright since 2025 Mifos Initiative Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
@@ -46,13 +46,6 @@
</div> </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) { @if (recurringDepositProduct.glAccountId) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span> <span class="flex-40">GL Account (Liability):</span>
@@ -116,20 +109,6 @@
</div> </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) { @if (recurringDepositProduct.nominationFacility !== undefined) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span> <span class="flex-40">Nomination Facility:</span>
@@ -144,6 +123,28 @@
</div> </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 ── --> <!-- ── Terms ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -324,8 +325,9 @@
</div> </div>
} }
<!-- ── Interest Rate Slabs ── --> <!-- ── Interest Rate Slabs ── -->
@if (recurringDepositProduct.rdSlabs?.length) { @if (recurringDepositProduct.rdSlabs?.length) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -7,6 +7,25 @@
--> -->
<form [formGroup]="recurringDepositProductTermsForm"> <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">
<div class="rdpt-section__header"> <div class="rdpt-section__header">
<span class="rdpt-section__badge"><i class="fas fa-chart-line"></i></span> <span class="rdpt-section__badge"><i class="fas fa-chart-line"></i></span>
@@ -78,6 +97,26 @@
</mat-error> </mat-error>
</mat-form-field> </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) { @if (recurringDepositProductTermsForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="rdpt-field"> <mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Average Period</mat-label> <mat-label>Average Period</mat-label>
@@ -85,7 +85,9 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
frequencyFixDay: isEditing ? (this.recurringDepositProductsTemplate.frequencyFixDay ?? 30) : 30, frequencyFixDay: isEditing ? (this.recurringDepositProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate
? this.parseDate(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, 30,
[Validators.min(0)] [Validators.min(0)]
], ],
lastInterestDate: [''] lastInterestDate: [''],
minDepositAmount: [''],
maxDepositAmount: ['']
}); });
} }
@@ -156,14 +160,8 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
if (formValue.effectiveDate instanceof Date) { if (formValue.effectiveDate instanceof Date) {
formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || ''; formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || '';
} }
if (formValue.lastInterestDate instanceof Date) { formValue.minDepositAmount = formValue.minDepositAmount;
formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; formValue.maxDepositAmount = formValue.maxDepositAmount;
}
for (const key in formValue) {
if (formValue[key] === '') {
delete formValue[key];
}
}
return formValue; return formValue;
} }
} }
@@ -20,18 +20,6 @@
{{ 'labels.buttons.Create Recurring Deposit Product' | translate }} {{ 'labels.buttons.Create Recurring Deposit Product' | translate }}
</button> </button>
</div> </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>
<div class="container"> <div class="container">
@@ -48,12 +48,6 @@
<span class="flex-60">{{ recurringDepositProduct.accountType }}</span> <span class="flex-60">{{ recurringDepositProduct.accountType }}</span>
</div> </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) { @if (recurringDepositProduct.glAccountId) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span> <span class="flex-40">GL Account (Liability):</span>
@@ -99,14 +93,6 @@
<span class="flex-40">Dormancy Alert:</span> <span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ recurringDepositProduct.dormancyAlert | yesNo }}</span> <span class="flex-60">{{ recurringDepositProduct.dormancyAlert | yesNo }}</span>
</div> </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"> <div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span> <span class="flex-40">Nomination Facility:</span>
<span class="flex-60">{{ recurringDepositProduct.nominationFacility | yesNo }}</span> <span class="flex-60">{{ recurringDepositProduct.nominationFacility | yesNo }}</span>
@@ -18,15 +18,6 @@
> >
{{ 'labels.heading.General' | translate }} {{ 'labels.heading.General' | translate }}
</a> </a>
<a
mat-tab-link
[routerLink]="['./interest-rates']"
routerLinkActive
#interestRates="routerLinkActive"
[active]="interestRates.isActive"
>
CBS Base Chart
</a>
@for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) { @for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) {
<a <a
mat-tab-link mat-tab-link
+1 -4
View File
@@ -19,7 +19,6 @@ import { VersionService } from 'app/system/version.service';
/** Environment Configuration */ /** Environment Configuration */
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { NgClass, DatePipe } from '@angular/common';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; 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', templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'], styleUrls: ['./footer.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS
NgClass,
DatePipe
] ]
}) })
export class FooterComponent implements OnInit, OnDestroy { 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 { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { MatButton, MatIconButton } from '@angular/material/button'; import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 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 { MatCheckbox } from '@angular/material/checkbox';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -41,7 +41,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
FaIconComponent, FaIconComponent,
MatCardTitle,
MatCheckbox, MatCheckbox,
MatIconButton, MatIconButton,
MatTooltip MatTooltip
@@ -9,7 +9,6 @@
/** Angular Imports */ /** Angular Imports */
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { MatDivider } from '@angular/material/divider';
import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -26,8 +25,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTabLink, MatTabLink,
RouterLinkActive, RouterLinkActive,
MatTabNavPanel, MatTabNavPanel,
RouterOutlet, RouterOutlet
MatDivider
] ]
}) })
export class CheckerInboxAndTasksComponent { export class CheckerInboxAndTasksComponent {
@@ -12,7 +12,6 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
/** Custom Services */ /** Custom Services */
import { UsersService } from '../users.service'; import { UsersService } from '../users.service';
@@ -35,8 +34,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
FaIconComponent, FaIconComponent,
MatMenuModule, MatMenuModule,
MatIconButton, MatIconButton
MatTooltip
] ]
}) })
export class ViewUserComponent { export class ViewUserComponent {
+2 -2
View File
@@ -10,8 +10,8 @@
/* tslint:disable */ /* tslint:disable */
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '260507', 'version': '260508',
'hash': 'a12ba20d' 'hash': '19c0e98e'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };