update RD prodcut code

This commit is contained in:
Nitesh Yadav
2026-04-20 13:16:03 +05:30
parent fe61c925ae
commit e212bf7d79
35 changed files with 3000 additions and 1728 deletions
@@ -70,6 +70,7 @@
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
</mat-step>
@@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
/** Custom Services */
import { RecurringDepositsService } from '../recurring-deposits.service';
import { SettingsService } from 'app/settings/settings.service';
import { ProductsService } from 'app/products/products.service';
/** Step Components */
import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component';
@@ -54,6 +55,7 @@ export class CreateRecurringDepositsAccountComponent {
private dateUtils = inject(Dates);
private recurringDepositsService = inject(RecurringDepositsService);
private settingsService = inject(SettingsService);
private productsService = inject(ProductsService);
/** Imports all the step component */
@ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true })
@@ -69,6 +71,8 @@ export class CreateRecurringDepositsAccountComponent {
recurringDepositsAccountTemplate: any;
/** Recurring Deposit Account Product Template */
recurringDepositsAccountProductTemplate: any;
/** Active CBS base interest rate chart */
rdInterestRates: any[] = [];
constructor() {
this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => {
@@ -78,10 +82,19 @@ export class CreateRecurringDepositsAccountComponent {
/**
* Sets recurring deposits account product template.
* @param {any} $event API response
* Also fetches the active CBS base interest rate chart.
* @param {any} $event API response (includes productId)
*/
setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event;
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
}
/** Get Recurring Deposit Account Details Form Data */
@@ -71,6 +71,7 @@
<mifosx-recurring-deposits-account-interest-rate-chart-step
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[rdInterestRates]="rdInterestRates"
>
</mifosx-recurring-deposits-account-interest-rate-chart-step>
</mat-step>
@@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
/** Custom Services */
import { RecurringDepositsService } from '../recurring-deposits.service';
import { SettingsService } from 'app/settings/settings.service';
import { ProductsService } from 'app/products/products.service';
/** Step Components */
import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component';
@@ -54,6 +55,7 @@ export class EditRecurringDepositAccountComponent {
private dateUtils = inject(Dates);
private recurringDepositsService = inject(RecurringDepositsService);
private settingsService = inject(SettingsService);
private productsService = inject(ProductsService);
/** Imports all the step component */
@ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true })
@@ -69,6 +71,8 @@ export class EditRecurringDepositAccountComponent {
recurringDepositsAccountAndTemplate: any;
/** Recurring Deposit Account Product Template */
recurringDepositsAccountProductTemplate: any;
/** Active CBS base interest rate chart */
rdInterestRates: any[] = [];
constructor() {
this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => {
@@ -82,6 +86,14 @@ export class EditRecurringDepositAccountComponent {
*/
setTemplate($event: any) {
this.recurringDepositsAccountProductTemplate = $event;
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
}
/** Get Recurring Deposit Account Details Form Data */
@@ -7,6 +7,48 @@
-->
<div class="layout-column gap-2px">
<!-- ── Custom RD Interest Rate Chart (from m_rd_interest_rate_slab_history) ── -->
@if (_activeRdVersion) {
<div class="rd-rate-header">
<strong>{{ _activeRdVersion.versionLabel }}</strong>
<span class="rd-rate-active-badge">Active</span>
<span class="rd-rate-from">Effective from: {{ _activeRdVersion.effectiveFromDate }}</span>
</div>
<table class="mat-elevation-z1 rd-rate-table" mat-table [dataSource]="_activeRdVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="rdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: rdSlabColumns"></tr>
</table>
}
<!-- ── Legacy chart (shown only when no custom rate chart available) ── -->
@if (!_activeRdVersion) {
<div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart.name">
<span class="flex-40">{{ 'labels.inputs.name' | translate }}:</span>
<span class="flex-60">{{ recurringDepositsAccountProductTemplate.accountChart.name }}</span>
@@ -33,8 +75,16 @@
recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo
}}</span>
</div>
}
<table class="mat-elevation-z1" mat-table [dataSource]="interestRateChartData" #chartsTable multiTemplateDataRows>
<table
class="mat-elevation-z1"
[class.hidden]="_activeRdVersion"
mat-table
[dataSource]="interestRateChartData"
#chartsTable
multiTemplateDataRows
>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
@@ -21,7 +21,7 @@ import {
MatRow
} from '@angular/material/table';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { NgIf, NgSwitch, TitleCasePipe } from '@angular/common';
import { NgIf, NgSwitch, TitleCasePipe, DecimalPipe } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
@@ -69,7 +69,28 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
@Input() recurringDepositsAccountTemplate: any;
@Input() recurringDepositsAccountProductTemplate: any;
/** Interest Rate Chart Data */
/**
* CBS base rate chart loaded from the active base-interest-rate service.
* When set, this table is shown instead of the legacy accountChart.chartSlabs.
*/
@Input() set rdInterestRates(rates: any[]) {
this._rdInterestRates = rates || [];
this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null;
}
_rdInterestRates: any[] = [];
_activeRdVersion: any = null;
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
/** Interest Rate Chart Data (legacy – used when no custom rate chart is available) */
interestRateChartData: any = [];
/** Columns to be displayed in interest rate chart table. */
chartSlabsDisplayedColumns: any[] = [
@@ -88,7 +109,7 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
'incentiveType',
'amount'
];
/** Additional Column to disblac incentives table */
/** Additional Column to display incentives table */
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
/** Expand Chart Slab Index used in the view */
expandChartSlabIndex: number;
@@ -49,6 +49,7 @@ import { EditTaxComponentComponent } from './manage-tax-components/edit-tax-comp
import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-tax-group.component';
import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components';
import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component';
import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component';
import { CreateDividendComponent } from './share-products/create-dividend/create-dividend.component';
import { ViewFixedDepositProductComponent } from './fixed-deposit-products/view-fixed-deposit-product/view-fixed-deposit-product.component';
import { ViewDividendComponent } from './share-products/view-dividend/view-dividend.component';
@@ -129,6 +130,8 @@ import { FixedDepositGeneralTabComponent } from './fixed-deposit-products/view-f
import { FixedDepositDatatableTabComponent } from './fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-datatable-tab/fixed-deposit-datatable-tab.component';
import { RecurringDepositGeneralTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component';
import { RecurringDepositDatatableTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-datatable-tab/recurring-deposit-datatable-tab.component';
import { RdInterestRatesTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component';
import { rdInterestRatesResolver } from './recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver';
import { ShareProductGeneralTabComponent } from './share-products/view-share-product/share-product-general-tab/share-product-general-tab.component';
import { ShareProductDatatableTabComponent } from './share-products/view-share-product/share-product-datatable-tab/share-product-datatable-tab.component';
import { ShareProductDatatablesResolver } from './share-products/share-product-datatables.resolver';
@@ -652,6 +655,14 @@ const routes: Routes = [
path: 'recurring-deposit-products',
data: { title: 'Recurring Deposit Products', breadcrumb: 'Recurring Deposit Products' },
children: [
{
path: 'base-interest-rates',
component: BaseRecurringDepositInterestRateChartComponent,
data: { title: 'CBS Base Interest Rates', breadcrumb: 'Base Interest Rates' },
resolve: {
recurringDepositProductsTemplate: RecurringDepositProductsTemplateResolver
}
},
{
path: 'create',
component: CreateRecurringDepositProductComponent,
@@ -704,6 +715,14 @@ const routes: Routes = [
}
]
},
{
path: 'interest-rates',
component: RdInterestRatesTabComponent,
data: { title: 'CBS Base Chart', breadcrumb: 'CBS Base Chart', routeParamBreadcrumb: false },
resolve: {
rdInterestRates: rdInterestRatesResolver
}
},
{
path: 'edit',
data: { title: 'Edit Recurring Deposit Product', breadcrumb: 'edit', routeParamBreadcrumb: false },
+2
View File
@@ -84,6 +84,7 @@ import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-t
import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components';
import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-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 { RecurringDepositProductAccountingStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component';
import { RecurringDepositProductChargesStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component';
import { RecurringDepositProductCurrencyStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component';
@@ -213,6 +214,7 @@ import { AdvancedAccountingMappingRuleComponent } from './loan-products/loan-pro
DepositProductIncentiveFormDialogComponent,
ViewRecurringDepositProductComponent,
CreateRecurringDepositProductComponent,
BaseRecurringDepositInterestRateChartComponent,
RecurringDepositProductAccountingStepComponent,
RecurringDepositProductChargesStepComponent,
RecurringDepositProductCurrencyStepComponent,
+20
View File
@@ -526,6 +526,26 @@ export class ProductsService {
return this.http.put(`/recurringdepositproducts/${recurringDepositProductId}`, recurringDepositProduct);
}
/** Returns the active CBS base interest-rate version(s). */
getAllRdInterestRates(): Observable<any> {
return this.getRdBaseInterestRates();
}
/** Returns the active CBS base interest-rate version(s). */
getRdBaseInterestRates(): Observable<any> {
return this.http.get('/recurringdepositproducts/baseinterestrates');
}
/** Returns the full CBS base chart history. */
getRdBaseInterestRateHistory(): Observable<any> {
return this.http.get('/recurringdepositproducts/baseinterestrates/history');
}
/** Saves a new CBS base interest-rate version. */
saveRdBaseInterestRates(payload: any): Observable<any> {
return this.http.post('/recurringdepositproducts/baseinterestrates', payload);
}
/**
* @returns {Observable<any>} Collateral data.
*/
@@ -0,0 +1,119 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="page-layout">
<div class="page-header">
<div>
<h2>CBS Base Recurring Deposit Interest Chart</h2>
<p class="page-subtitle">This chart is the master template used when creating a recurring deposit product.</p>
</div>
</div>
@if (recurringDepositProductsTemplate?.chartTemplate) {
<mifosx-recurring-deposit-product-interest-rate-chart-step
#stepComponent
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[rdInterestRates]="rdInterestRates"
>
</mifosx-recurring-deposit-product-interest-rate-chart-step>
<div class="actions-row">
<button
type="button"
mat-raised-button
color="primary"
[disabled]="isSaving || !stepComponent"
(click)="save(stepComponent)"
>
{{ isSaving ? 'Saving...' : 'Save CBS Base Chart' }}
</button>
@if (saveMessage) {
<span class="save-message">{{ saveMessage }}</span>
}
</div>
} @else {
<p>Loading CBS base chart...</p>
}
<div class="history-section">
<div class="section-header">
<div>
<h3>Chart History</h3>
<p class="section-subtitle">Previous CBS base chart versions are listed here for comparison and audit.</p>
</div>
</div>
@if (isHistoryLoading) {
<p>Loading history...</p>
} @else if (rdInterestRateHistory.length === 0) {
<p>No history found for the CBS base chart.</p>
} @else {
<mat-accordion multi>
@for (version of rdInterestRateHistory; track version.id) {
<mat-expansion-panel [expanded]="version.isActive">
<mat-expansion-panel-header>
<mat-panel-title>
<span class="version-header">
<strong>{{ version.versionLabel }}</strong>
<span [class]="version.isActive ? 'active-badge' : 'inactive-badge'">
{{ version.isActive ? 'Active' : 'Inactive' }}
</span>
</span>
</mat-panel-title>
<mat-panel-description>
<span class="meta-text">From {{ version.effectiveFromDate }}</span>
<span class="meta-text">To {{ version.effectiveToDate || 'Current' }}</span>
</mat-panel-description>
</mat-expansion-panel-header>
@if (version.remarks) {
<p class="remarks-text">{{ version.remarks }}</p>
}
<table class="history-table" mat-table [dataSource]="version.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Description</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">
{{ slab.toPeriod !== null && slab.toPeriod !== undefined ? slab.toPeriod : 'N/A' }}
</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">{{ slab.annualInterestRate | number: '1.2-2' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="historySlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: historySlabColumns"></tr>
</table>
</mat-expansion-panel>
}
</mat-accordion>
}
</div>
</div>
@@ -0,0 +1,95 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.page-layout {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.page-header {
display: flex;
align-items: end;
justify-content: space-between;
}
.page-subtitle {
margin: 0.25rem 0 0;
color: #6b7280;
}
.actions-row {
display: flex;
align-items: center;
gap: 1rem;
}
.save-message {
color: #2563eb;
font-weight: 600;
}
.history-section {
margin-top: 1rem;
padding-top: 0.5rem;
border-top: 1px solid #e5e7eb;
}
.section-header {
display: flex;
align-items: end;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.section-subtitle {
margin: 0.25rem 0 0;
color: #6b7280;
}
.version-header {
display: inline-flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.meta-text {
margin-left: 0.75rem;
color: #6b7280;
font-size: 0.85rem;
}
.active-badge,
.inactive-badge {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
}
.active-badge {
background: #dcfce7;
color: #166534;
}
.inactive-badge {
background: #f3f4f6;
color: #4b5563;
}
.remarks-text {
margin: 0 0 0.75rem;
color: #374151;
}
.history-table {
width: 100%;
}
@@ -0,0 +1,121 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { ProductsService } from 'app/products/products.service';
import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component';
import { MatButton } from '@angular/material/button';
import { DecimalPipe } from '@angular/common';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatExpansionModule } from '@angular/material/expansion';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-base-recurring-deposit-interest-rate-chart',
templateUrl: './base-recurring-deposit-interest-rate-chart.component.html',
styleUrls: ['./base-recurring-deposit-interest-rate-chart.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatButton,
MatExpansionModule,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
DecimalPipe,
RecurringDepositProductInterestRateChartStepComponent
]
})
export class BaseRecurringDepositInterestRateChartComponent implements OnInit {
private productsService = inject(ProductsService);
recurringDepositProductsTemplate: any;
rdInterestRates: any[] = [];
rdInterestRateHistory: any[] = [];
readonly historySlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
isSaving = false;
isHistoryLoading = false;
saveMessage = '';
ngOnInit(): void {
this.productsService.getRecurringDepositProductsTemplate().subscribe({
next: (template: any) => (this.recurringDepositProductsTemplate = template),
error: () => (this.recurringDepositProductsTemplate = null)
});
this.loadBaseRates();
this.loadBaseRateHistory();
}
get chartStep(): RecurringDepositProductInterestRateChartStepComponent | null {
return null;
}
save(stepComponent: RecurringDepositProductInterestRateChartStepComponent): void {
this.isSaving = true;
this.saveMessage = '';
const payload = stepComponent.modifiedRdInterestRatePayload;
this.productsService.saveRdBaseInterestRates(payload).subscribe({
next: () => {
this.saveMessage = 'CBS base chart saved successfully.';
this.loadBaseRates();
this.loadBaseRateHistory();
this.isSaving = false;
},
error: () => {
this.saveMessage = 'Unable to save CBS base chart.';
this.isSaving = false;
}
});
}
private loadBaseRates(): void {
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => (this.rdInterestRates = rates || []),
error: () => (this.rdInterestRates = [])
});
}
private loadBaseRateHistory(): void {
this.isHistoryLoading = true;
this.productsService.getRdBaseInterestRateHistory().subscribe({
next: (history: any[]) => {
this.rdInterestRateHistory = history || [];
this.isHistoryLoading = false;
},
error: () => {
this.rdInterestRateHistory = [];
this.isHistoryLoading = false;
}
});
}
}
@@ -69,12 +69,60 @@
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-chart-area rdp-tab-icon"></i>
{{ 'labels.inputs.INTEREST RATE CHART' | translate }}
CBS Base Chart
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-interest-rate-chart-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
></mifosx-recurring-deposit-product-interest-rate-chart-step>
@if (activeBaseVersion) {
<div class="base-rate-card">
<div class="base-rate-header">
<div>
<h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
<p class="base-rate-meta">
Active CBS base chart
<span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</p>
</div>
<span class="base-rate-badge">Active</span>
</div>
@if (activeBaseVersion.remarks) {
<p class="base-rate-remarks">{{ activeBaseVersion.remarks }}</p>
}
<table class="base-rate-table" mat-table [dataSource]="activeBaseVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? 'N/A' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="rdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: rdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div>
</mat-tab>
@@ -114,7 +162,7 @@
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-preview-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[chartSlabsDisplayedColumns]="recurringDepositProductInterestRateChartStep.chartSlabsDisplayedColumns"
[chartSlabsDisplayedColumns]="rdSlabColumns"
[accountingRuleData]="accountingRuleData"
[recurringDepositProduct]="recurringDepositProduct"
(submitEvent)="submit()"
@@ -71,6 +71,54 @@ $primary: #1353a0;
padding: 24px 16px;
}
.base-rate-card {
padding: 16px;
border: 1px solid #dbe4f0;
border-radius: 14px;
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.base-rate-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.base-rate-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.base-rate-meta {
margin: 6px 0 0;
color: #5b6b7f;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.base-rate-badge {
padding: 4px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.base-rate-remarks {
margin: 0 0 12px;
color: #334155;
}
.base-rate-table {
width: 100%;
border-collapse: collapse;
}
.rdp-action-bar {
display: flex;
justify-content: space-between;
@@ -15,9 +15,20 @@ import { RecurringDepositProductDetailsStepComponent } from '../recurring-deposi
import { RecurringDepositProductCurrencyStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component';
import { RecurringDepositProductTermsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component';
import { RecurringDepositProductSettingsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component';
import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component';
import { RecurringDepositProductChargesStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component';
import { RecurringDepositProductAccountingStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
@@ -40,10 +51,19 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
RecurringDepositProductCurrencyStepComponent,
RecurringDepositProductTermsStepComponent,
RecurringDepositProductSettingsStepComponent,
RecurringDepositProductInterestRateChartStepComponent,
RecurringDepositProductChargesStepComponent,
RecurringDepositProductAccountingStepComponent,
RecurringDepositProductPreviewStepComponent
RecurringDepositProductPreviewStepComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
]
})
export class CreateRecurringDepositProductComponent {
@@ -61,8 +81,6 @@ export class CreateRecurringDepositProductComponent {
recurringDepositProductTermsStep: RecurringDepositProductTermsStepComponent;
@ViewChild(RecurringDepositProductSettingsStepComponent, { static: true })
recurringDepositProductSettingsStep: RecurringDepositProductSettingsStepComponent;
@ViewChild(RecurringDepositProductInterestRateChartStepComponent, { static: true })
recurringDepositProductInterestRateChartStep: RecurringDepositProductInterestRateChartStepComponent;
@ViewChild(RecurringDepositProductChargesStepComponent, { static: true })
recurringDepositProductChargesStep: RecurringDepositProductChargesStepComponent;
@ViewChild(RecurringDepositProductAccountingStepComponent, { static: true })
@@ -71,6 +89,15 @@ export class CreateRecurringDepositProductComponent {
recurringDepositProductsTemplate: any;
accountingRuleData: string[] = [];
selectedTabIndex = 0;
rdInterestRates: any[] = [];
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
/**
* @param {ActivatedRoute} route Activated Route.
@@ -84,6 +111,15 @@ export class CreateRecurringDepositProductComponent {
this.recurringDepositProductsTemplate = data.recurringDepositProductsTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
// Load the CBS base chart as the reference for the editable table
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
}
get recurringDepositProductDetailsForm() {
@@ -102,10 +138,6 @@ export class CreateRecurringDepositProductComponent {
return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm;
}
get recurringDepositProductInterestRateChartForm() {
return this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChartForm;
}
get recurringDepositProductAccountingForm() {
return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm;
}
@@ -120,20 +152,27 @@ export class CreateRecurringDepositProductComponent {
this.recurringDepositProductCurrencyForm.valid &&
this.recurringDepositProductTermsForm.valid &&
this.recurringDepositProductSettingsForm.valid &&
this.recurringDepositProductInterestRateChartForm.valid &&
this.recurringDepositProductAccountingForm.valid
);
}
get activeBaseVersion() {
return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null;
}
get activeBaseSlabs() {
return this.activeBaseVersion?.slabs || [];
}
get recurringDepositProduct() {
return {
...this.recurringDepositProductDetailsStep.recurringDepositProductDetails,
...this.recurringDepositProductCurrencyStep.recurringDepositProductCurrency,
...this.recurringDepositProductTermsStep.recurringDepositProductTerms,
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChart,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
rdSlabs: this.activeBaseSlabs
};
}
@@ -6,112 +6,195 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-stepper
orientation="vertical"
[linear]="false"
class="mat-elevation-z8"
labelPosition="bottom"
#recurringDepositProductStepper
<div class="rdp-container">
<div class="rdp-header">
<i class="fas fa-sync-alt rdp-header__icon"></i>
<h2 class="rdp-header__title">{{ 'labels.heading.Edit Recurring Deposit Product' | translate }}</h2>
</div>
<mat-tab-group
class="rdp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<ng-template matStepperIcon="number">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-info-circle rdp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }}
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="done">
<fa-icon icon="check" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="error">
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
</ng-template>
<ng-template matStepperIcon="preview">
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="recurringDepositProductDetailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-details-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
>
</mifosx-recurring-deposit-product-details-step>
</mat-step>
<mat-step [stepControl]="recurringDepositProductCurrencyForm">
<ng-template matStepLabel>{{ 'labels.inputs.CURRENCY' | translate }}</ng-template>
></mifosx-recurring-deposit-product-details-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-coins rdp-tab-icon"></i>
{{ 'labels.inputs.CURRENCY' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-currency-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
>
</mifosx-recurring-deposit-product-currency-step>
</mat-step>
<mat-step [stepControl]="recurringDepositProductTermsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
></mifosx-recurring-deposit-product-currency-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-sliders-h rdp-tab-icon"></i>
{{ 'labels.inputs.TERMS' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-terms-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
>
</mifosx-recurring-deposit-product-terms-step>
</mat-step>
<mat-step [stepControl]="recurringDepositProductSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
></mifosx-recurring-deposit-product-terms-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-cog rdp-tab-icon"></i>
{{ 'labels.inputs.SETTINGS' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-settings-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
>
</mifosx-recurring-deposit-product-settings-step>
</mat-step>
></mifosx-recurring-deposit-product-settings-step>
</div>
</mat-tab>
<mat-step [stepControl]="recurringDepositProductInterestRateChartForm">
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-chart-area rdp-tab-icon"></i>
CBS Base Chart
</ng-template>
<div class="rdp-tab-content">
@if (activeBaseVersion) {
<div class="base-rate-card">
<div class="base-rate-header">
<div>
<h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
<p class="base-rate-meta">
Active CBS base chart
<span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</p>
</div>
<span class="base-rate-badge">Active</span>
</div>
<mifosx-recurring-deposit-product-interest-rate-chart-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
>
</mifosx-recurring-deposit-product-interest-rate-chart-step>
</mat-step>
@if (activeBaseVersion.remarks) {
<p class="base-rate-remarks">{{ activeBaseVersion.remarks }}</p>
}
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<table class="base-rate-table" mat-table [dataSource]="activeBaseVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? 'N/A' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="rdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: rdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-receipt rdp-tab-icon"></i>
{{ 'labels.inputs.CHARGES' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-charges-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[currencyCode]="recurringDepositProductCurrencyForm.get('currencyCode')"
>
</mifosx-recurring-deposit-product-charges-step>
</mat-step>
<mat-step [stepControl]="recurringDepositProductAccountingForm">
<ng-template matStepLabel>{{ 'labels.inputs.ACCOUNTING' | translate }}</ng-template>
></mifosx-recurring-deposit-product-charges-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-book rdp-tab-icon"></i>
{{ 'labels.inputs.ACCOUNTING' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-accounting-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[accountingRuleData]="accountingRuleData"
[recurringDepositProductFormValid]="recurringDepositProductFormValidAndNotPrinstine"
>
</mifosx-recurring-deposit-product-accounting-step>
</mat-step>
></mifosx-recurring-deposit-product-accounting-step>
</div>
</mat-tab>
@if (recurringDepositProductFormValidAndNotPrinstine) {
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-eye rdp-tab-icon"></i>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="rdp-tab-content">
<mifosx-recurring-deposit-product-preview-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[chartSlabsDisplayedColumns]="recurringDepositProductInterestRateChartStep.chartSlabsDisplayedColumns"
[chartSlabsDisplayedColumns]="rdSlabColumns"
[accountingRuleData]="accountingRuleData"
[recurringDepositProduct]="recurringDepositProduct"
[recurringDepositProduct]="recurringDepositProductPreview"
(submitEvent)="submit()"
>
</mifosx-recurring-deposit-product-preview-step>
</mat-step>
></mifosx-recurring-deposit-product-preview-step>
</div>
</mat-tab>
}
</mat-stepper>
</mat-tab-group>
<div class="rdp-action-bar">
<button mat-raised-button [routerLink]="['../../']" class="rdp-action-bar__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<div class="rdp-action-bar__right">
@if (selectedTabIndex > 0) {
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1">
<i class="fas fa-arrow-left" style="margin-right: 8px"></i>
{{ 'labels.buttons.Previous' | translate }}
</button>
}
@if (selectedTabIndex < totalTabs - 1) {
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
{{ 'labels.buttons.Next' | translate }}
<i class="fas fa-arrow-right" style="margin-left: 8px"></i>
</button>
}
@if (recurringDepositProductFormValidAndNotPrinstine && selectedTabIndex === totalTabs - 1) {
<button mat-raised-button color="primary" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }}
</button>
}
</div>
</div>
</div>
@@ -5,3 +5,134 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$primary: #1353a0;
.rdp-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.rdp-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
&__icon {
font-size: 20px;
color: $primary;
}
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.rdp-tabs {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
::ng-deep {
.mat-mdc-tab-header {
border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.mat-mdc-tab {
min-width: 100px;
padding: 0 12px;
height: 52px;
}
.mat-mdc-tab-body-wrapper {
min-height: 200px;
}
.mat-mdc-tab-body {
overflow: visible !important;
}
.mat-mdc-tab-body-content {
overflow: visible !important;
}
}
}
.rdp-tab-icon {
margin-right: 8px;
font-size: 14px;
}
.rdp-tab-content {
padding: 24px 16px;
}
.base-rate-card {
padding: 16px;
border: 1px solid #dbe4f0;
border-radius: 14px;
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.base-rate-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.base-rate-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.base-rate-meta {
margin: 6px 0 0;
color: #5b6b7f;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.base-rate-badge {
padding: 4px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.base-rate-remarks {
margin: 0 0 12px;
color: #334155;
}
.base-rate-table {
width: 100%;
border-collapse: collapse;
}
.rdp-action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel {
color: rgb(0 0 0 / 60%);
}
&__right {
display: flex;
gap: 12px;
}
}
@@ -15,16 +15,26 @@ import { RecurringDepositProductDetailsStepComponent } from '../recurring-deposi
import { RecurringDepositProductCurrencyStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component';
import { RecurringDepositProductTermsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component';
import { RecurringDepositProductSettingsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component';
import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component';
import { RecurringDepositProductChargesStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component';
import { RecurringDepositProductAccountingStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTabsModule } from '@angular/material/tabs';
import { RecurringDepositProductPreviewStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -34,19 +44,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./edit-recurring-deposit-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
FaIconComponent,
MatStep,
MatStepLabel,
MatTabsModule,
RecurringDepositProductDetailsStepComponent,
RecurringDepositProductCurrencyStepComponent,
RecurringDepositProductTermsStepComponent,
RecurringDepositProductSettingsStepComponent,
RecurringDepositProductInterestRateChartStepComponent,
RecurringDepositProductChargesStepComponent,
RecurringDepositProductAccountingStepComponent,
RecurringDepositProductPreviewStepComponent
RecurringDepositProductPreviewStepComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
]
})
export class EditRecurringDepositProductComponent {
@@ -64,8 +79,6 @@ export class EditRecurringDepositProductComponent {
recurringDepositProductTermsStep: RecurringDepositProductTermsStepComponent;
@ViewChild(RecurringDepositProductSettingsStepComponent, { static: true })
recurringDepositProductSettingsStep: RecurringDepositProductSettingsStepComponent;
@ViewChild(RecurringDepositProductInterestRateChartStepComponent, { static: true })
recurringDepositProductInterestRateChartStep: RecurringDepositProductInterestRateChartStepComponent;
@ViewChild(RecurringDepositProductChargesStepComponent, { static: true })
recurringDepositProductChargesStep: RecurringDepositProductChargesStepComponent;
@ViewChild(RecurringDepositProductAccountingStepComponent, { static: true })
@@ -73,6 +86,16 @@ export class EditRecurringDepositProductComponent {
recurringDepositProductsTemplate: any;
accountingRuleData: string[] = [];
rdInterestRates: any[] = [];
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
selectedTabIndex = 0;
/**
* @param {ActivatedRoute} route Activated Route.
@@ -84,6 +107,14 @@ export class EditRecurringDepositProductComponent {
constructor() {
this.route.data.subscribe((data: { recurringDepositProductAndTemplate: any }) => {
this.recurringDepositProductsTemplate = data.recurringDepositProductAndTemplate;
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.rdInterestRates = rates || [];
},
error: () => {
this.rdInterestRates = [];
}
});
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
}
@@ -104,43 +135,55 @@ export class EditRecurringDepositProductComponent {
return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm;
}
get recurringDepositProductInterestRateChartForm() {
return this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChartForm;
}
get recurringDepositProductAccountingForm() {
return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm;
}
get totalTabs(): number {
return this.recurringDepositProductFormValidAndNotPrinstine ? 7 : 6;
}
get recurringDepositProductFormValidAndNotPrinstine() {
return (
this.recurringDepositProductDetailsForm.valid &&
this.recurringDepositProductCurrencyForm.valid &&
this.recurringDepositProductTermsForm.valid &&
this.recurringDepositProductSettingsForm.valid &&
this.recurringDepositProductInterestRateChartForm.valid &&
this.recurringDepositProductAccountingForm.valid &&
(this.recurringDepositProductDetailsForm.pristine ||
this.recurringDepositProductCurrencyForm.pristine ||
this.recurringDepositProductTermsForm.pristine ||
this.recurringDepositProductSettingsForm.pristine ||
this.recurringDepositProductInterestRateChartForm.pristine ||
this.recurringDepositProductAccountingForm.pristine)
);
}
get activeBaseVersion() {
return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null;
}
get activeBaseSlabs() {
return this.activeBaseVersion?.slabs || [];
}
get recurringDepositProduct() {
return {
...this.recurringDepositProductDetailsStep.recurringDepositProductDetails,
...this.recurringDepositProductCurrencyStep.recurringDepositProductCurrency,
...this.recurringDepositProductTermsStep.recurringDepositProductTerms,
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChart,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting
};
}
get recurringDepositProductPreview() {
return {
...this.recurringDepositProduct,
rdSlabs: this.activeBaseSlabs
};
}
submit() {
// TODO: Update once language and date settings are setup
const recurringDepositProduct = {
@@ -152,8 +195,9 @@ export class EditRecurringDepositProductComponent {
recurringDepositProduct.description = '';
}
delete recurringDepositProduct.advancedAccountingRules;
const productId = this.recurringDepositProductsTemplate.id;
this.productsService
.updateRecurringDepositProduct(this.recurringDepositProductsTemplate.id, recurringDepositProduct)
.updateRecurringDepositProduct(productId, recurringDepositProduct)
.subscribe((response: any) => {
this.router.navigate(['../'], { relativeTo: this.route });
});
@@ -6,325 +6,104 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<form [formGroup]="recurringDepositProductInterestRateChartForm">
<div class="rdpi-section">
<!-- ── Bank Interest Rate Chart (editable reference table from DB) ───────── -->
<div class="rdpi-section rdpi-ref-section">
<div class="rdpi-section__header">
<i class="fa fa-chart-line"></i>
<h3>{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<i class="fa fa-table"></i>
<h3>Bank Interest Rate Chart &nbsp;<span class="rdpi-ref-badge">Reference / Editable</span></h3>
<span class="rdpi-spacer"></span>
<button type="button" mat-raised-button color="primary" class="rdpi-add-btn" (click)="addChart()">
<button type="button" mat-raised-button color="primary" class="rdpi-add-btn" (click)="addSlab()">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
Add Slab
</button>
</div>
<div class="rdpi-content">
@for (chart of charts.controls; track chart; let chartIndex = $index) {
<div class="rdpi-chart-block" formArrayName="charts">
<div [formGroupName]="chartIndex">
<div class="rdpi-chart-actions">
<button
type="button"
mat-icon-button
color="warn"
(click)="delete(charts, chartIndex)"
matTooltip="{{ 'tooltips.Delete Interest Rate Chart' | translate }}"
matTooltipPosition="left"
>
<fa-icon icon="trash"></fa-icon>
</button>
</div>
<div class="rdpi-grid">
<mat-form-field appearance="outline" class="rdpi-field">
<mat-label>{{ 'labels.inputs.Name' | translate }}</mat-label>
<input matInput formControlName="name" required />
@if (chart.get('name')?.touched && chart.get('name')?.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
@if (_referenceVersionLabel) {
<div class="rdpi-ref-meta">
<span><strong>Version:</strong> {{ _referenceVersionLabel }}</span>
@if (_referenceRemarks) {
<span>&nbsp;| {{ _referenceRemarks }}</span>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpi-field rdpi-field--full">
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
<textarea matInput formControlName="description" required></textarea>
@if (chart.get('description')?.touched && chart.get('description')?.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</div>
}
</mat-form-field>
<mat-form-field appearance="outline" class="rdpi-field" (click)="validFromDatePicker.open()">
<mat-label>{{ 'labels.inputs.From Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="validFromDatePicker"
formControlName="fromDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="validFromDatePicker"></mat-datepicker-toggle>
<mat-datepicker #validFromDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.From Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpi-field" (click)="endDatePicker.open()">
<mat-label>{{ 'labels.inputs.End Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="endDatePicker"
formControlName="endDate"
/>
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field>
<div class="rdpi-field rdpi-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="isPrimaryGroupingByAmount">
{{ 'labels.inputs.Is primary grouping by amount?' | translate }}
</mat-checkbox>
</div>
</div>
<div class="rdpi-slab-actions">
<button
type="button"
mat-raised-button
color="primary"
class="rdpi-add-btn"
(click)="addChartSlab(chart.controls.chartSlabs)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add Slab' | translate }}
</button>
</div>
@if (chart.value.chartSlabs.length === 0) {
@if (_editableSlabs.length === 0) {
<div class="rdpi-empty-msg">
<h3>{{ 'labels.heading.It is required to add at least one Slab' | translate }}</h3>
<h3>No interest rate slabs defined. Click <strong>Add Slab</strong> to add one.</h3>
</div>
}
@if (chart.value.chartSlabs.length !== 0) {
@if (_editableSlabs.length > 0) {
<div class="rdpi-table-wrapper">
<table mat-table [dataSource]="chart.value.chartSlabs" multiTemplateDataRows>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{
chartSlab.fromPeriod +
' - ' +
chartSlab.toPeriod +
' ' +
(chartSlab.periodType | find: periodTypeData : 'id' : 'value')
}}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.amountRangeFrom + ' - ' + chartSlab.amountRangeTo }}
</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.annualInterestRate }}
</td>
<table mat-table [dataSource]="_editableSlabs" class="rdpi-ref-table">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.description }}
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input"
[value]="slab.description"
(change)="editSlab(i, 'description', $any($event.target).value)"
/>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-icon-button
color="primary"
(click)="editChartSlab(chart.controls.chartSlabs, chartSlabIndex)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(chart.controls.chartSlabs, chartSlabIndex)">
<fa-icon icon="trash"></fa-icon>
</button>
<button
mat-button
color="primary"
(click)="
expandChartSlabIndex[chartIndex] =
expandChartSlabIndex[chartIndex] === chartSlabIndex ? null : chartSlabIndex
"
>
@if (expandChartSlabIndex[chartIndex] !== chartSlabIndex) {
<span>
<fa-icon icon="eye" class="m-r-10"></fa-icon>
{{ 'labels.buttons.View Incentives' | translate }}
</span>
}
@if (expandChartSlabIndex[chartIndex] === chartSlabIndex) {
<span>
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Hide Incentives' | translate }}
</span>
}
</button>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input rdpi-inline-input--num"
type="number"
[value]="slab.fromPeriod"
(change)="editSlab(i, 'fromPeriod', +$any($event.target).value)"
/>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns[chartIndex].length"
>
<div
class="rdpi-incentives"
[@expandChartSlab]="
chartSlabIndex === expandChartSlabIndex[chartIndex] ? 'expanded' : 'collapsed'
"
>
<mat-card class="rdpi-incentive-card">
<h4 class="rdpi-incentive-title">
{{ 'labels.heading.Incentives' | translate }}
</h4>
<div class="rdpi-incentive-add">
<button
mat-raised-button
color="primary"
class="rdpi-add-btn"
(click)="addIncentive(getIncentives(chart.controls.chartSlabs, chartSlabIndex))"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chartSlab.incentives.length) {
<div class="rdpi-table-wrapper">
<table mat-table [dataSource]="chartSlab.incentives">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Entity Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.entityType | find: entityTypeData : 'id' : 'value' }}
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input rdpi-inline-input--num"
type="number"
[value]="slab.toPeriod ?? ''"
(change)="editSlab(i, 'toPeriod', $any($event.target).value ? +$any($event.target).value : null)"
/>
</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Attribute Name' | translate }}
</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }}
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
<td mat-cell *matCellDef="let slab; let i = index">
<input
class="rdpi-inline-input rdpi-inline-input--rate"
type="number"
step="0.01"
[value]="slab.annualInterestRate"
(change)="editSlab(i, 'annualInterestRate', +$any($event.target).value)"
/>
</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Condition Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }}
</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Attribute Value' | translate }}
</th>
<td mat-cell *matCellDef="let incentive">
@switch (incentive.attributeName) {
@case (2) {
<span>{{ incentive.attributeValue | find: genderData : 'id' : 'name' }}</span>
}
@case (3) {
<span>{{ incentive.attributeValue }}</span>
}
@case (4) {
<span>{{
incentive.attributeValue | find: clientTypeData : 'id' : 'name'
}}</span>
}
@case (5) {
<span>{{
incentive.attributeValue | find: clientClassificationData : 'id' : 'name'
}}</span>
}
}
</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Incentive Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.amount }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let incentive; let incentiveIndex = index">
<button
mat-icon-button
color="primary"
(click)="
editIncentive(
getIncentives(chart.controls.chartSlabs, chartSlabIndex),
incentiveIndex
)
"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="
delete(getIncentives(chart.controls.chartSlabs, chartSlabIndex), incentiveIndex)
"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
<tr mat-header-row *matHeaderRowDef="refSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: refSlabColumns"></tr>
</table>
</div>
}
</mat-card>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns[chartIndex]"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns[chartIndex]"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="incentives-row"
></tr>
</table>
<div class="rdpi-ref-footer">
<p class="rdpi-ref-hint">
Edit the rates above. When you <strong>Save</strong> the product, a new version will be inserted in the database
— previous rates are preserved as history.
</p>
</div>
}
</div>
</div>
}
</div>
</div>
</form>
@@ -148,3 +148,68 @@
grid-template-columns: 1fr;
}
}
/* ── Editable reference-rate table ───────────────────────────── */
.rdpi-ref-section {
margin-top: 24px;
border-color: #bfdbfe;
background: #f0f7ff;
.rdpi-section__header {
background: #dbeafe;
color: #1e3a5f;
}
}
.rdpi-ref-badge {
font-size: 11px;
font-weight: 500;
background: #3b82f6;
color: #fff;
border-radius: 10px;
padding: 2px 8px;
vertical-align: middle;
}
.rdpi-ref-meta {
padding: 8px 16px;
font-size: 13px;
color: #555;
border-bottom: 1px solid #bfdbfe;
}
.rdpi-ref-table {
width: 100%;
}
.rdpi-inline-input {
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 3px 6px;
font-size: 13px;
width: 100%;
background: #fff;
&:focus {
outline: none;
border-color: #3b82f6;
}
&--num {
width: 70px;
}
&--rate {
width: 80px;
}
}
.rdpi-ref-footer {
padding: 10px 16px;
}
.rdpi-ref-hint {
font-size: 12px;
color: #64748b;
margin: 0;
}
@@ -7,7 +7,7 @@
*/
/** Angular Imports */
import { Component, OnInit, Input, inject } from '@angular/core';
import { Component, OnInit, OnChanges, Input, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
@@ -21,7 +21,6 @@ import { animate, state, style, transition, trigger } from '@angular/animations'
/** Custom Components */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { DepositProductIncentiveFormDialogComponent } from 'app/products/deposit-product-incentive-form-dialog/deposit-product-incentive-form-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
/** Dialog Components */
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
@@ -31,11 +30,10 @@ import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { TranslateService } from '@ngx-translate/core';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MatButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import {
MatTable,
@@ -68,8 +66,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDivider,
MatIconButton,
MatTooltip,
MatButton,
MatCheckbox,
MatTable,
MatColumnDef,
@@ -81,8 +78,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
MatStepperNext,
FindPipe
]
})
@@ -94,6 +89,190 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On
private translateService = inject(TranslateService);
@Input() recurringDepositProductsTemplate: any;
/** Reference rate chart fetched from m_rd_interest_rate_effective / m_rd_interest_rate_slab_history */
@Input() set rdInterestRates(rates: any[]) {
if (rates && rates.length > 0) {
// Use the first active version's slabs as the editable working copy
const activeVersion = rates.find((v: any) => v.isActive) ?? rates[0];
this._referenceVersionLabel = activeVersion.versionLabel;
this._referenceRemarks = activeVersion.remarks;
// Deep-clone slabs so the original is not mutated
this._editableSlabs = (activeVersion.slabs ?? []).map((s: any) => ({ ...s }));
this.ensureSlabRangeRules(this._editableSlabs);
}
}
/** Slabs the user is editing in the reference-rate table */
_editableSlabs: any[] = [];
_referenceVersionLabel = '';
_referenceRemarks = '';
/** Columns for the editable reference-rate table */
readonly refSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
/** Returns the payload to save as a new rate version (called by parent after product save) */
get modifiedRdInterestRatePayload(): any {
return {
versionLabel: this._referenceVersionLabel || 'Rate Version ' + new Date().toISOString().slice(0, 10),
remarks: this._referenceRemarks,
slabs: this._editableSlabs.map((s: any, idx: number) => ({
srNo: s.srNo ?? idx + 1,
description: s.description,
periodTypeEnum: s.periodTypeEnum ?? 1,
fromPeriod: s.fromPeriod,
toPeriod: s.toPeriod ?? null,
annualInterestRate: s.annualInterestRate
}))
};
}
editSlab(index: number, field: string, value: any) {
const slabs = this._editableSlabs.map((slab: any) => ({ ...slab }));
const current = slabs[index];
if (!current) {
return;
}
const nextValue = this.normalizeSlabValue(value);
const oldFrom = this.normalizeSlabValue(current.fromPeriod);
const oldTo = this.normalizeSlabValue(current.toPeriod);
current[field] = nextValue;
// Keep the first slab anchored at 0 so the chart always starts from zero.
if (index === 0 && field === 'fromPeriod') {
current.fromPeriod = 0;
}
if (field === 'fromPeriod' && nextValue === null) {
current.fromPeriod = oldFrom;
}
if (field === 'fromPeriod') {
const effectiveFrom = this.normalizeSlabValue(current.fromPeriod);
const delta = effectiveFrom - oldFrom;
if (current.toPeriod !== null && current.toPeriod !== undefined) {
current.toPeriod = this.normalizeSlabValue(current.toPeriod) + delta;
}
this.shiftFollowingSlabs(slabs, index, delta);
}
if (field === 'toPeriod') {
if (index < slabs.length - 1 && nextValue === null) {
// Non-final slabs should always have a closing boundary so later slabs can stay contiguous.
current.toPeriod = oldTo;
} else {
const delta = (this.normalizeSlabValue(current.toPeriod) ?? 0) - (oldTo ?? 0);
this.shiftFollowingSlabs(slabs, index, delta);
}
}
this.ensureSlabRangeRules(slabs);
this._editableSlabs = slabs;
}
addSlab() {
const slabs = this._editableSlabs.map((slab: any) => ({ ...slab }));
const nextSr = slabs.length + 1;
const lastSlab = slabs[slabs.length - 1];
const lastFrom = this.normalizeSlabValue(lastSlab?.fromPeriod) ?? 0;
if (lastSlab && this.normalizeSlabValue(lastSlab.toPeriod) === null) {
// Split the open-ended tail so the new slab can become the new last row.
// This keeps the sequence contiguous while preserving the existing tail start.
lastSlab.toPeriod = lastFrom;
}
const previousSlab = slabs[slabs.length - 1];
this._editableSlabs = [
...slabs,
{
srNo: nextSr,
description: '',
periodTypeEnum: 1,
periodTypeLabel: 'Days',
fromPeriod: slabs.length === 0 ? 0 : this.nextFromPeriod(previousSlab),
toPeriod: null,
annualInterestRate: null
}
];
this.ensureSlabRangeRules(this._editableSlabs);
}
private normalizeSlabValue(value: any): number | null {
if (value === null || value === undefined || value === '') {
return null;
}
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
}
private nextFromPeriod(previousSlab: any): number {
const previousTo = this.normalizeSlabValue(previousSlab?.toPeriod);
const previousFrom = this.normalizeSlabValue(previousSlab?.fromPeriod) ?? 0;
return previousTo === null ? previousFrom + 1 : previousTo + 1;
}
private shiftFollowingSlabs(slabs: any[], index: number, delta: number): void {
if (!delta) {
return;
}
for (let i = index + 1; i < slabs.length; i++) {
const slab = slabs[i];
const currentFrom = this.normalizeSlabValue(slab.fromPeriod);
const currentTo = this.normalizeSlabValue(slab.toPeriod);
if (currentFrom !== null) {
slab.fromPeriod = currentFrom + delta;
}
if (currentTo !== null) {
slab.toPeriod = currentTo + delta;
}
}
}
private ensureSlabRangeRules(slabs: any[]): void {
if (slabs.length === 0) {
return;
}
slabs[0].fromPeriod = 0;
for (let i = 1; i < slabs.length; i++) {
const previous = slabs[i - 1];
const previousTo = this.normalizeSlabValue(previous.toPeriod);
if (previousTo === null) {
slabs[i].fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod);
continue;
}
slabs[i].fromPeriod = previousTo + 1;
}
// Keep the last slab open-ended so the chart ends with null.
slabs[slabs.length - 1].toPeriod = null;
// Prevent inverted ranges on intermediate rows.
for (let i = 0; i < slabs.length - 1; i++) {
const fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod);
const toPeriod = this.normalizeSlabValue(slabs[i].toPeriod);
if (fromPeriod !== null && toPeriod !== null && toPeriod < fromPeriod) {
slabs[i].toPeriod = fromPeriod;
}
}
}
recurringDepositProductInterestRateChartForm: UntypedFormGroup;
@@ -146,9 +325,9 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On
this.clientClassificationData = this.recurringDepositProductsTemplate.chartTemplate.clientClassificationOptions;
this.incentiveTypeData = this.recurringDepositProductsTemplate.chartTemplate.incentiveTypeOptions;
if (!(this.recurringDepositProductsTemplate === undefined)) {
this.assignFormData();
}
// assignFormData() intentionally not called — old chart form is removed from UI.
// The recurringDepositProductInterestRateChartForm is kept only for stepControl
// validity; it remains empty (valid) so the preview tab becomes available.
}
// Assign form the values for edit recurring Depoosit
@@ -429,17 +608,6 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On
});
}
delete(formArray: UntypedFormArray, index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
formArray.removeAt(index);
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case 'Slab':
@@ -541,6 +709,9 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On
delete chart.id;
}
}
return recurringDepositProductInterestRateChart;
return {
...recurringDepositProductInterestRateChart,
rdSlabs: this._editableSlabs
};
}
}
@@ -6,130 +6,126 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.rdpp-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
@use 'assets/styles/colours' as *;
& + & {
margin-top: 16px;
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
}
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
i {
font-size: 14px;
color: #0ea5e9;
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
h3 {
h2,
h3,
h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
}
&__body {
padding: 16px 24px;
}
}
.rdpp-product-name {
margin: 0 0 12px;
font-size: 18px;
font-weight: 600;
color: #202124;
}
.rdpp-row {
display: flex;
padding: 6px 0;
border-bottom: 1px solid #f1f3f4;
&:last-child {
border-bottom: none;
}
}
.rdpp-label {
flex: 0 0 40%;
font-size: 13px;
font-weight: 500;
color: #5f6368;
}
.rdpp-value {
flex: 1;
font-size: 13px;
color: #202124;
}
.rdpp-table-wrapper {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin-top: 8px;
table {
width: 100%;
}
& + & {
margin-top: 12px;
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
/* ── GL mapping summary (same as savings product preview) ── */
.spa-gl-summary,
.spa-other-gl-summary {
display: flex;
flex-direction: column;
gap: 0.9rem;
margin-top: 1.25rem;
}
.spa-gl-summary__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0 0.5rem;
}
.spa-gl-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.15rem;
border: 1px solid #e3e7f2;
border-radius: 12px;
background: #fff;
h5 {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #91a0bc;
}
}
.rdpp-sub-heading {
margin: 16px 0 8px;
font-size: 14px;
.spa-gl-row {
display: grid;
grid-template-columns: minmax(220px, 40%) 1fr;
gap: 1rem;
align-items: baseline;
padding: 0.2rem 0;
}
.spa-gl-row__label {
color: #1f2f4a;
font-weight: 500;
}
.spa-gl-row__value {
color: #101828;
font-weight: 600;
color: #0ea5e9;
word-break: break-word;
}
.rdpp-sub-heading-sm {
margin: 12px 0 4px;
font-size: 13px;
font-weight: 600;
color: #202124;
.spa-other-gl-table {
margin-top: 0.5rem;
margin-bottom: 0.75rem;
table-layout: fixed;
width: 100%;
&:first-of-type {
margin-top: 0.25rem;
}
.rdpp-chart-block {
& + & {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e2e8f0;
th.mat-mdc-header-cell,
td.mat-mdc-cell {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
th.mat-mdc-header-cell:first-child,
td.mat-mdc-cell:first-child {
width: 38%;
}
th.mat-mdc-header-cell:last-child,
td.mat-mdc-cell:last-child {
width: 62%;
word-break: break-word;
}
}
.rdpp-incentives {
.spa-other-gl-summary .mat-elevation-z1 {
border-radius: 10px;
overflow: hidden;
&__card {
border-radius: 0;
padding: 12px 16px;
h4 {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
}
}
}
table {
tr.incentives-row {
height: 0;
> td {
padding: 0;
}
@media (width <= 768px) {
.spa-gl-row {
grid-template-columns: 1fr;
gap: 0.35rem;
}
}
@@ -7,9 +7,9 @@
*/
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core';
import { trigger, state, transition, animate, style } from '@angular/animations';
import { OptionData } from 'app/shared/models/option-data.model';
import { Accounting } from 'app/core/utils/accounting';
import { OptionData } from 'app/shared/models/option-data.model';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
MatColumnDef,
@@ -23,7 +23,6 @@ import {
MatRow
} from '@angular/material/table';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
import { FindPipe } from '../../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
@@ -34,15 +33,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
selector: 'mifosx-recurring-deposit-product-preview-step',
templateUrl: './recurring-deposit-product-preview-step.component.html',
styleUrls: ['./recurring-deposit-product-preview-step.component.scss'],
animations: [
trigger('expandChartSlab', [
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
@@ -54,7 +47,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatHeaderRow,
MatRowDef,
MatRow,
ViewSavingsAccountingDetailsComponent,
FindPipe,
DateFormatPipe,
FormatNumberPipe,
@@ -65,20 +57,11 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh
private accounting = inject(Accounting);
@Input() recurringDepositProductsTemplate: any;
@Input() chartSlabsDisplayedColumns: any[];
@Input() chartSlabsDisplayedColumns: any[] = [];
@Input() accountingRuleData: any;
@Input() recurringDepositProduct: any;
@Output() submitEvent = new EventEmitter();
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
incentivesDisplayedColumns: string[] = [
'entityType',
'attributeName',
'conditionType',
'attributeValue',
'incentiveType',
'amount'
];
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
@@ -94,72 +77,28 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh
'incomeAccountId'
];
expandChartSlabIndex: number[] = [];
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
accountingMappings: any = {};
accountingRule: OptionData;
accountingRule: OptionData | undefined;
ngOnInit() {
this.setCurrentValues();
}
ngOnChanges(changes: SimpleChanges): void {
ngOnChanges(_changes: SimpleChanges): void {
this.setCurrentValues();
}
setCurrentValues(): void {
if (this.isCashOrAccrualAccounting()) {
if (this.recurringDepositProduct) {
this.accountingRule = this.accounting.getAccountingRuleFrom(this.recurringDepositProduct.accountingRule);
const assetAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
const incomeAccountData =
this.recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || [];
const expenseAccountData =
this.recurringDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || [];
const liabilityAccountData =
this.recurringDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
this.accountingMappings = {
savingsReferenceAccount: this.accounting.glAccountLookUp(
this.recurringDepositProduct.savingsReferenceAccountId,
assetAccountData
),
savingsControlAccount: this.accounting.glAccountLookUp(
this.recurringDepositProduct.savingsControlAccountId,
liabilityAccountData
),
transfersInSuspenseAccount: this.accounting.glAccountLookUp(
this.recurringDepositProduct.transfersInSuspenseAccountId,
liabilityAccountData
),
interestOnSavingsAccount: this.accounting.glAccountLookUp(
this.recurringDepositProduct.interestOnSavingsAccountId,
expenseAccountData
),
incomeFromFeeAccount: this.accounting.glAccountLookUp(
this.recurringDepositProduct.incomeFromFeeAccountId,
incomeAccountData
),
incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
this.recurringDepositProduct.incomeFromPenaltyAccountId,
incomeAccountData
)
};
if (this.isAccrualAccounting()) {
this.accountingMappings['feeReceivableAccount'] = this.accounting.glAccountLookUp(
this.recurringDepositProduct.feesReceivableAccountId,
assetAccountData
);
this.accountingMappings['penaltyReceivableAccount'] = this.accounting.glAccountLookUp(
this.recurringDepositProduct.penaltiesReceivableAccountId,
assetAccountData
);
this.accountingMappings['interestPayableAccount'] = this.accounting.glAccountLookUp(
this.recurringDepositProduct.interestPayableAccountId,
liabilityAccountData
);
}
}
}
@@ -174,4 +113,23 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh
isAccrualAccounting(): boolean {
return this.accounting.isAccrualAccountingRuleId(this.recurringDepositProduct.accountingRule);
}
getLockinPeriodFrequencyTypeLabel(value: any): string {
if (!value && value !== 0) return '';
const options = this.recurringDepositProductsTemplate?.lockinPeriodFrequencyTypeOptions || [];
const found = options.find((o: any) => o.id === value);
return found?.value || String(value);
}
formatGlAccountById(glAccountId: number, options: any[]): string {
if (!glAccountId || !Array.isArray(options)) return '-';
const account = this.accounting.glAccountLookUp(glAccountId, options as any);
return account ? `(${account.glCode}) ${account.name}` : '-';
}
formatGlAccountByCode(glCode: string, options: any[]): string {
if (!glCode || !Array.isArray(options)) return glCode || '-';
const account = options.find((o: any) => o?.glCode === glCode);
return account ? `(${account.glCode}) ${account.name}` : glCode;
}
}
@@ -72,7 +72,7 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
: this.interestCalculationTypeOptionData?.[0]?.id || 1,
interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType?.id,
effectiveDate: this.recurringDepositProductsTemplate.effectiveDate
? new Date(this.recurringDepositProductsTemplate.effectiveDate)
? this.parseDate(this.recurringDepositProductsTemplate.effectiveDate)
: '',
interestCalculateSelector: isEditing
? this.recurringDepositProductsTemplate.interestCalculateSelector || 1
@@ -83,7 +83,7 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
autoPost: isEditing ? (this.recurringDepositProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? this.recurringDepositProductsTemplate.frequencyFixDay || 30 : 30,
lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate
? new Date(this.recurringDepositProductsTemplate.lastInterestDate)
? this.parseDate(this.recurringDepositProductsTemplate.lastInterestDate)
: ''
});
@@ -93,6 +93,15 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
}
}
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) {
return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
}
if (dateValue instanceof Date) return dateValue;
return new Date(dateValue);
}
private setYesNoOptions(options: any[]) {
if (!Array.isArray(options)) return;
const mapped = options
@@ -20,6 +20,18 @@
{{ 'labels.buttons.Create Recurring Deposit Product' | translate }}
</button>
</div>
<div class="in-block">
<button
mat-stroked-button
color="primary"
[routerLink]="['/products/recurring-deposit-products/base-interest-rates']"
matTooltip="Open the CBS base interest chart"
matTooltipPosition="above"
>
<fa-icon icon="chart-line" class="m-r-10"></fa-icon>
CBS Base Chart
</button>
</div>
</div>
<div class="container">
@@ -0,0 +1,68 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="tab-container mat-typography">
@if (!rdInterestRates || rdInterestRates.length === 0) {
<p class="no-rates">No CBS base interest rate versions defined.</p>
}
<mat-accordion multi>
@for (version of rdInterestRates; track version.id) {
<mat-expansion-panel [expanded]="version.isActive">
<mat-expansion-panel-header>
<mat-panel-title>
<span class="rd-version-header">
<strong>{{ version.versionLabel }}</strong>
<span [class]="version.isActive ? 'rd-active-badge' : 'rd-inactive-badge'">
{{ version.isActive ? 'Active' : 'Inactive' }}
</span>
<span style="color: #757575; font-size: 13px">Effective from: {{ version.effectiveFromDate }}</span>
</span>
</mat-panel-title>
</mat-expansion-panel-header>
@if (version.remarks) {
<p>
<em>{{ version.remarks }}</em>
</p>
}
<table class="rd-slab-table mat-elevation-z1" mat-table [dataSource]="version.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="slabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: slabColumns"></tr>
</table>
</mat-expansion-panel>
}
</mat-accordion>
</div>
@@ -0,0 +1,40 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.rd-version-header {
display: flex;
align-items: center;
gap: 12px;
}
.rd-active-badge {
background: #4caf50;
color: #fff;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
}
.rd-inactive-badge {
background: #9e9e9e;
color: #fff;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
}
.rd-slab-table {
width: 100%;
margin-top: 8px;
}
.no-rates {
padding: 24px;
color: #757575;
text-align: center;
}
@@ -0,0 +1,66 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DecimalPipe } from '@angular/common';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatExpansionModule } from '@angular/material/expansion';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-rd-interest-rates-tab',
templateUrl: './rd-interest-rates-tab.component.html',
styleUrls: ['./rd-interest-rates-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatExpansionModule,
DecimalPipe
]
})
export class RdInterestRatesTabComponent {
private route = inject(ActivatedRoute);
rdInterestRates: any[] = [];
readonly slabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
constructor() {
this.route.data.subscribe((data: { rdInterestRates: any[] }) => {
this.rdInterestRates = data.rdInterestRates || [];
});
}
}
@@ -0,0 +1,16 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductsService } from '../../../products.service';
export const rdInterestRatesResolver: ResolveFn<any[]> = (route) => {
const productsService = inject(ProductsService);
return productsService.getRdBaseInterestRates();
};
@@ -6,24 +6,25 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row align-end gap-2px responsive-column container m-t-20 m-b-20">
<button mat-raised-button color="primary" [routerLink]="['../edit']">
<fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Edit' | translate }}
<div class="layout-row align-end gap-2px responsive-column container m-b-20">
<button
mat-raised-button
color="primary"
class="m-t-20"
[routerLink]="['../edit']"
*mifosxHasPermission="'UPDATE_RECURRINGDEPOSITPRODUCT'"
>
<fa-icon icon="edit" class="m-r-10"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
</button>
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div class="responsive-column">
<h2 class="mat-h2 flex-100">{{ recurringDepositProduct.name }}</h2>
<mat-divider [inset]="true"></mat-divider>
<!-- ===== DETAILS ===== -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
@@ -37,165 +38,301 @@
</div>
}
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<!-- Product Classification (inside Details tab) -->
<h3 class="mat-h3 flex-100">Product Classification</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider>
@if (recurringDepositProduct.accountType) {
<div class="flex-100 layout-row">
<span class="flex-40">Account Type:</span>
<span class="flex-60">{{ recurringDepositProduct.accountType }}</span>
</div>
}
@if (recurringDepositProduct.customerSegment) {
<div class="flex-100 layout-row">
<span class="flex-40">Customer Segment:</span>
<span class="flex-60">{{ recurringDepositProduct.customerSegment }}</span>
</div>
}
@if (recurringDepositProduct.productCode) {
<div class="flex-100 layout-row">
<span class="flex-40">Product Code:</span>
<span class="flex-60">{{ recurringDepositProduct.productCode }}</span>
</div>
}
@if (recurringDepositProduct.effectiveTo) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span>
<span class="flex-60">{{ recurringDepositProduct.effectiveTo | dateFormat }}</span>
</div>
}
@if (recurringDepositProduct.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">End Date:</span>
<span class="flex-60">{{ recurringDepositProduct.endDate | dateFormat }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Introducer Required:</span>
<span class="flex-60">{{ recurringDepositProduct.introducerRequired | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Share Linking Enabled:</span>
<span class="flex-60">{{ recurringDepositProduct.shareLinkingEnabled | yesNo }}</span>
</div>
<!-- ===== CURRENCY ===== -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.heading.Currency' | translate }}:</span>
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
<span class="flex-60"
>({{ recurringDepositProduct.currency.code }}) {{ recurringDepositProduct.currency.name }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.currency.decimalPlaces }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.currency.inMultiplesOf | formatNumber }}</span>
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ recurringDepositProduct.dormancyAlert | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Balance Swap Option:</span>
<span class="flex-60">{{ recurringDepositProduct.balanceSwapOption | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">MSME:</span>
<span class="flex-60">{{ recurringDepositProduct.msme | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span>
<span class="flex-60">{{ recurringDepositProduct.nominationFacility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Auto Debit Facility:</span>
<span class="flex-60">{{ recurringDepositProduct.autoDebitFacility | yesNo }}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<!-- ===== TERMS ===== -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Deposit Amount' | translate }}:</span>
<span class="flex-60">{{
recurringDepositProduct.depositAmount +
' (Min: ' +
(recurringDepositProduct.minDepositAmount
? recurringDepositProduct.minDepositAmount
: recurringDepositProduct.depositAmount) +
(recurringDepositProduct.minDepositAmount || recurringDepositProduct.depositAmount) +
', Max: ' +
(recurringDepositProduct.maxDepositAmount
? recurringDepositProduct.maxDepositAmount
: recurringDepositProduct.depositAmount) +
(recurringDepositProduct.maxDepositAmount || recurringDepositProduct.depositAmount) +
')'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCompoundingPeriodType.value }}</span>
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
@if (recurringDepositProduct.effectiveDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ recurringDepositProduct.effectiveDate | dateFormat }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculationType.value }}</span>
</div>
@if (recurringDepositProduct.interestCalculateSelector !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Interest Calculate:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculateSelector }}</span>
</div>
}
@if (recurringDepositProduct.averagePeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Average Period:</span>
<span class="flex-60">{{ recurringDepositProduct.averagePeriod }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestPostingPeriodType.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculationType.value }}</span>
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCompoundingPeriodType.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculationDaysInYearType.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Auto Post:</span>
<span class="flex-60">{{ recurringDepositProduct.autoPost | yesNo }}</span>
</div>
@if (recurringDepositProduct.frequencyFixDay) {
<div class="flex-100 layout-row">
<span class="flex-40">Frequency (Fix Day):</span>
<span class="flex-60">{{ recurringDepositProduct.frequencyFixDay }}</span>
</div>
}
@if (recurringDepositProduct.lastInterestDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ recurringDepositProduct.lastInterestDate | dateFormat }}</span>
</div>
}
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<!-- ===== SETTINGS ===== -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider>
<!-- Premature option -->
<h4 class="mat-h4 flex-100">Premature Option</h4>
<div class="flex-100 layout-row">
<span class="flex-40">Loan Facility:</span>
<span class="flex-60">{{ recurringDepositProduct.loanFacility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Premature Withdrawal Option:</span>
<span class="flex-60">{{ recurringDepositProduct.prematureWithdrawalOption | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Tenure Flexibility:</span>
<span class="flex-60">{{ recurringDepositProduct.tenureFlexibility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Lien Marked:</span>
<span class="flex-60">{{ recurringDepositProduct.lienMarked | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.withdrawalFeeForTransfers | yesNo }}</span>
</div>
<!-- Lock-in Period -->
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Lock-in Period' | translate }}</h4>
<div class="flex-100 layout-row">
<span class="flex-40">Enable Lock-in Period:</span>
<span class="flex-60">{{ recurringDepositProduct.enableLockinPeriod | yesNo }}</span>
</div>
@if (recurringDepositProduct.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
@if (recurringDepositProduct.lockinPeriodFrequency !== undefined) {
<span class="flex-60">
{{ recurringDepositProduct.lockinPeriodFrequency }}
{{ recurringDepositProduct.lockinPeriodFrequencyType.value }}</span
<span class="flex-60"
>{{ recurringDepositProduct.lockinPeriodFrequency }}
{{ recurringDepositProduct.lockinPeriodFrequencyType?.value }}</span
>
</div>
}
<!-- Maturity Instructions -->
<h4 class="mat-h4 flex-100">Maturity Instructions</h4>
<div class="flex-100 layout-row">
<span class="flex-40">Maturity Instructions:</span>
<span class="flex-60">{{ recurringDepositProduct.maturityInstructions | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Credit Maturity Proceeds:</span>
<span class="flex-60">{{ recurringDepositProduct.creditMaturityProceeds | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Penalty Grace Period (Missed Installments):</span>
<span class="flex-60">{{ recurringDepositProduct.penaltyGracePeriodMissedInstallments | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Panel Request Applicable:</span>
<span class="flex-60">{{ recurringDepositProduct.panelRequestApplicable | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">No Penalty (Nominee / Heir Closure):</span>
<span class="flex-60">{{ recurringDepositProduct.noPenaltyNomineeHeirClosure | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Deceased Settlement:</span>
<span class="flex-60">{{ recurringDepositProduct.deceasedSettlement | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Allow Debit Txn After Due Date:</span>
<span class="flex-60">{{ recurringDepositProduct.allowDebitTxnAfterDueDate | yesNo }}</span>
</div>
<!-- Additional Rules -->
<h4 class="mat-h4 flex-100">Additional Rules</h4>
<div class="flex-100 layout-row">
<span class="flex-40">Account Closed Zero Balance in Day Closing:</span>
<span class="flex-60">{{ recurringDepositProduct.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
@if (recurringDepositProduct.minBalanceForInterestCalculation) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
@if (recurringDepositProduct.minDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span>
@if (recurringDepositProduct.minDepositTerm !== undefined) {
<span class="flex-60"
>{{ recurringDepositProduct.minDepositTerm }}
&nbsp;
{{ recurringDepositProduct.minDepositTermType.value }}</span
>{{ recurringDepositProduct.minDepositTerm }} {{ recurringDepositProduct.minDepositTermType?.value }}</span
>
}
</div>
}
@if (recurringDepositProduct.maxDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}:</span>
@if (recurringDepositProduct.maxDepositTerm !== undefined) {
<span class="flex-60"
>{{ recurringDepositProduct.maxDepositTerm }}
&nbsp;
{{ recurringDepositProduct.maxDepositTermType.value }}</span
>{{ recurringDepositProduct.maxDepositTerm }} {{ recurringDepositProduct.maxDepositTermType?.value }}</span
>
}
</div>
}
@if (recurringDepositProduct.inMultipleOfDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}:</span>
@if (recurringDepositProduct.inMultipleOfDepositTerm !== undefined) {
<span class="flex-60"
>{{ recurringDepositProduct.inMultipleOfDepositTerm }}
-
{{ recurringDepositProduct.inMultipleOfDepositTermType.value }}</span
>{{ recurringDepositProduct.inMultipleOfDepositTerm }} -
{{ recurringDepositProduct.inMultipleOfDepositTermType?.value }}</span
>
}
</div>
}
@if (recurringDepositProduct.isMandatoryDeposit !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Recurring Deposit Type' | translate }}:</span>
@if (recurringDepositProduct.isMandatoryDeposit) {
<span class="flex-60">
{{ 'labels.text.Mandatory' | translate }}
</span>
}
@if (!recurringDepositProduct.isMandatoryDeposit) {
<span class="flex-60">
{{ 'labels.text.Voluntary' | translate }}
</span>
}
<span class="flex-60">{{
recurringDepositProduct.isMandatoryDeposit
? ('labels.text.Mandatory' | translate)
: ('labels.text.Voluntary' | translate)
}}</span>
</div>
}
@if (recurringDepositProduct.preClosurePenalInterest) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply penal interest (less)' | translate }}:</span>
<span class="flex-60"
><span class="m-r-5">{{ recurringDepositProduct.preClosurePenalInterest }}</span
>%<span class="m-r-5">On</span>
{{ recurringDepositProduct.preClosurePenalInterestOnType.value }}
</span>
>{{ recurringDepositProduct.preClosurePenalInterest }}% On
{{ recurringDepositProduct.preClosurePenalInterestOnType?.value }}</span
>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.withHoldTax | yesNo }}</span>
</div>
@if (recurringDepositProduct.withHoldTax) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax group' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.taxGroup.name }}</span>
<span class="flex-40">{{ 'labels.inputs.Tax Group' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.taxGroup?.name }}</span>
</div>
}
<!-- ===== INTEREST RATE CHART ===== -->
@if (recurringDepositProduct.activeChart) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row-wrap responsive-column flex-100">
@if (recurringDepositProduct.activeChart.name) {
<div class="flex-100 layout-row">
@@ -253,15 +390,11 @@
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.annualInterestRate }}
</td>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.description }}
</td>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.description }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
@@ -272,16 +405,16 @@
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
@if (expandChartSlabIndex !== chartSlabIndex) {
<span>
<fa-icon icon="eye" class="m-r-10"></fa-icon>
{{ 'labels.buttons.View Incentives' | translate }}
</span>
<span
><fa-icon icon="eye" class="m-r-10"></fa-icon
>{{ 'labels.buttons.View Incentives' | translate }}</span
>
}
@if (expandChartSlabIndex === chartSlabIndex) {
<span>
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Hide Incentives' | translate }}
</span>
<span
><fa-icon icon="eye-slash" class="m-r-10"></fa-icon
>{{ 'labels.buttons.Hide Incentives' | translate }}</span
>
}
</button>
</td>
@@ -297,19 +430,14 @@
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<h4 class="mat-h4 flex-23">
{{ 'labels.heading.Incentives' | translate }}
</h4>
<h4 class="mat-h4 flex-23">{{ 'labels.heading.Incentives' | translate }}</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="chartSlab.incentives">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.entityType
| find
: recurringDepositProductTemplate.chartTemplate.entityTypeOptions
: 'id'
: 'value'
| find: recurringDepositProductTemplate.chartTemplate.entityTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
@@ -318,10 +446,7 @@
<td mat-cell *matCellDef="let incentive">
{{
incentive.attributeName
| find
: recurringDepositProductTemplate.chartTemplate.attributeNameOptions
: 'id'
: 'value'
| find: recurringDepositProductTemplate.chartTemplate.attributeNameOptions : 'id' : 'value'
}}
</td>
</ng-container>
@@ -330,10 +455,7 @@
<td mat-cell *matCellDef="let incentive">
{{
incentive.conditionType
| find
: recurringDepositProductTemplate.chartTemplate.conditionTypeOptions
: 'id'
: 'value'
| find: recurringDepositProductTemplate.chartTemplate.conditionTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
@@ -344,10 +466,7 @@
@case (2) {
<span>{{
incentive.attributeValue
| find
: recurringDepositProductTemplate.chartTemplate.genderOptions
: 'id'
: 'name'
| find: recurringDepositProductTemplate.chartTemplate.genderOptions : 'id' : 'name'
}}</span>
}
@case (3) {
@@ -356,10 +475,7 @@
@case (4) {
<span>{{
incentive.attributeValue
| find
: recurringDepositProductTemplate.chartTemplate.clientTypeOptions
: 'id'
: 'name'
| find: recurringDepositProductTemplate.chartTemplate.clientTypeOptions : 'id' : 'name'
}}</span>
}
@case (5) {
@@ -379,18 +495,13 @@
<td mat-cell *matCellDef="let incentive">
{{
incentive.incentiveType
| find
: recurringDepositProductTemplate.chartTemplate.incentiveTypeOptions
: 'id'
: 'value'
| find: recurringDepositProductTemplate.chartTemplate.incentiveTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.amount }}
</td>
<td mat-cell *matCellDef="let incentive">{{ incentive.amount }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
@@ -401,45 +512,34 @@
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="incentives-row"
></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
<mat-divider [inset]="true"></mat-divider>
<mat-divider class="flex-100"></mat-divider>
</div>
</div>
}
<!-- ===== CHARGES ===== -->
@if (recurringDepositProduct.charges.length) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Charges' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="recurringDepositProduct.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name }}
</td>
<td mat-cell *matCellDef="let charge">{{ charge.name }}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
<td mat-cell *matCellDef="let charge">{{ charge.chargeCalculationType.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
</ng-container>
<ng-container matColumnDef="collectedon">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType.value }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
@@ -447,12 +547,12 @@
</div>
}
<!-- ===== ACCOUNTING ===== -->
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mat-divider class="flex-100"></mat-divider>
<mifosx-view-savings-accounting-details
class="container"
class="flex-100"
[accountingRule]="recurringDepositProduct.accountingRule"
[accountingMappings]="recurringDepositProduct.accountingMappings"
>
@@ -463,14 +563,12 @@
recurringDepositProduct.feeToIncomeAccountMappings?.length ||
recurringDepositProduct.penaltyToIncomeAccountMappings?.length
) {
<div class="flex-100 layout-row-wrap layout-column.lt-md">
<div class="flex-100 layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Advanced Accounting Rules' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mat-divider class="flex-100"></mat-divider>
@if (recurringDepositProduct.paymentChannelToFundSourceMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<h4 class="mat-h4 flex-100">
{{ 'labels.inputs.Configure Fund Sources for Payment Channels' | translate }}
</h4>
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
@@ -478,15 +576,11 @@
>
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentType.name }}
</td>
<td mat-cell *matCellDef="let paymentFundSource">{{ paymentFundSource.paymentType.name }}</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccount.name }}
</td>
<td mat-cell *matCellDef="let paymentFundSource">{{ paymentFundSource.fundSourceAccount.name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
@@ -494,24 +588,20 @@
</div>
}
@if (recurringDepositProduct.feeToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="recurringDepositProduct.feeToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.charge.name }}
</td>
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fee/Charge' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">{{ feesIncome.charge.name }}</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccount.name }}
</td>
<td mat-cell *matCellDef="let feesIncome">{{ feesIncome.incomeAccount.name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
@@ -519,24 +609,20 @@
</div>
}
@if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="recurringDepositProduct.penaltyToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.charge.name }}
</td>
<td mat-cell *matCellDef="let penaltyIncome">{{ penaltyIncome.charge.name }}</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccount.name }}
</td>
<td mat-cell *matCellDef="let penaltyIncome">{{ penaltyIncome.incomeAccount.name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
@@ -546,6 +632,3 @@
</div>
}
</div>
</mat-card-content>
</mat-card>
</div>
@@ -68,6 +68,7 @@ export class RecurringDepositGeneralTabComponent {
recurringDepositProduct: any;
recurringDepositProductTemplate: any;
expandChartSlabIndex: number | null = null;
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
chartSlabsDisplayedColumns: string[] = [
@@ -18,6 +18,15 @@
>
{{ 'labels.heading.General' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./interest-rates']"
routerLinkActive
#interestRates="routerLinkActive"
[active]="interestRates.isActive"
>
CBS Base Chart
</a>
@for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) {
<a
mat-tab-link
@@ -20,6 +20,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTabNav,
MatTabLink,
RouterLinkActive,
RouterLink,
MatTabNavPanel,
RouterOutlet
]
+3 -2
View File
@@ -5,12 +5,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
/* tslint:disable */
export default {
'mifos_x': {
'version': '260415',
'hash': '4679c6f7'
'version': '260420',
'hash': 'fe61c925'
},
'allow_switching_backend_instance': true
};