fd product releated update code

This commit is contained in:
Nitesh Yadav
2026-05-05 15:34:08 +05:30
parent e9d2f4b5e4
commit b0cffd3845
40 changed files with 2346 additions and 1539 deletions
@@ -24,7 +24,9 @@
{{ 'labels.inputs.DETAILS' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-details-step></mifosx-fixed-deposit-product-details-step>
<mifosx-fixed-deposit-product-details-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
></mifosx-fixed-deposit-product-details-step>
</div>
</mat-tab>
@@ -48,6 +50,7 @@
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-terms-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[fdType]="fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value"
></mifosx-fixed-deposit-product-terms-step>
</div>
</mat-tab>
@@ -147,27 +150,40 @@
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[accountingRuleData]="accountingRuleData"
[fixedDepositProductFormValid]="fixedDepositProductFormValid"
[isDormancyTrackingActive]="fixedDepositProductCurrencyForm.get('dormancyAlert')"
></mifosx-fixed-deposit-product-accounting-step>
</div>
</mat-tab>
@if (fixedDepositProductFormValid) {
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-eye fdp-tab-icon"></i>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-eye fdp-tab-icon"></i>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="fdp-tab-content">
@if (fixedDepositProductFormValid) {
<mifosx-fixed-deposit-product-preview-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="fdSlabColumns"
[chartSlabsDisplayedColumns]="[previewChartSlabColumns]"
[accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct"
(submitEvent)="submit()"
></mifosx-fixed-deposit-product-preview-step>
</div>
</mat-tab>
}
} @else {
<div style="padding: 32px; text-align: center; color: #666">
<i
class="fas fa-exclamation-triangle"
style="font-size: 32px; color: #f44336; margin-bottom: 12px; display: block"
></i>
<h3 style="margin-bottom: 8px">Please complete all required fields</h3>
<p>
Look for the <i class="fas fa-exclamation-circle" style="color: #f44336"></i> icon on each tab to find
which steps still need attention.
</p>
</div>
}
</div>
</mat-tab>
</mat-tab-group>
<div class="fdp-action-bar">
@@ -33,6 +33,7 @@ import {
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { Accounting } from 'app/core/utils/accounting';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FixedDepositProductPreviewStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component';
@@ -71,6 +72,7 @@ export class CreateFixedDepositProductComponent {
private productsService = inject(ProductsService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private accounting = inject(Accounting);
@ViewChild(FixedDepositProductDetailsStepComponent, { static: true })
@@ -98,13 +100,20 @@ export class CreateFixedDepositProductComponent {
'periodTypeLabel',
'annualInterestRate'
];
readonly previewChartSlabColumns = [
'period',
'amountRange',
'annualInterestRate',
'description',
'actions'
];
constructor() {
this.route.data.subscribe((data: { fixedDepositProductsTemplate: any }) => {
this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getRdBaseInterestRates().subscribe({
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.fdInterestRates = rates || [];
},
@@ -115,41 +124,41 @@ export class CreateFixedDepositProductComponent {
}
get fixedDepositProductDetailsForm() {
return this.fixedDepositProductDetailsStep.fixedDepositProductDetailsForm;
return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm;
}
get fixedDepositProductCurrencyForm() {
return this.fixedDepositProductCurrencyStep.fixedDepositProductCurrencyForm;
return this.fixedDepositProductCurrencyStep?.fixedDepositProductCurrencyForm;
}
get fixedDepositProductTermsForm() {
return this.fixedDepositProductTermsStep.fixedDepositProductTermsForm;
return this.fixedDepositProductTermsStep?.fixedDepositProductTermsForm;
}
get fixedDepositProductSettingsForm() {
return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm;
return this.fixedDepositProductSettingsStep?.fixedDepositProductSettingsForm;
}
get fixedDepositProductAccountingForm() {
return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm;
return this.fixedDepositProductAccountingStep?.fixedDepositProductAccountingForm;
}
get totalTabs(): number {
return this.fixedDepositProductFormValid ? 8 : 7;
return 8;
}
get fixedDepositProductFormValid() {
return (
this.fixedDepositProductDetailsForm.valid &&
this.fixedDepositProductCurrencyForm.valid &&
this.fixedDepositProductTermsForm.valid &&
this.fixedDepositProductSettingsForm.valid &&
this.fixedDepositProductAccountingForm.valid
);
return !!this.fixedDepositProductDetailsForm?.valid;
}
get selectedFdType(): string | null {
return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value ?? null;
}
get activeBaseVersion() {
return this.fdInterestRates.find((v: any) => v.isActive) ?? this.fdInterestRates[0] ?? null;
const fdType = this.selectedFdType;
const filtered = fdType ? this.fdInterestRates.filter((v: any) => v.fdType === fdType) : this.fdInterestRates;
return filtered.find((v: any) => v.isActive) ?? filtered[0] ?? null;
}
get activeBaseSlabs() {
@@ -164,16 +173,97 @@ export class CreateFixedDepositProductComponent {
...this.fixedDepositProductSettingsStep.fixedDepositProductSettings,
...this.fixedDepositProductChargesStep.fixedDepositProductCharges,
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting,
fdSlabs: this.activeBaseSlabs
charts: this.fixedDepositProductCharts,
fdSlabs: this.activeBaseVersion?.slabs || []
};
}
get fixedDepositProductCharts() {
const activeBaseVersion = this.activeBaseVersion;
if (!activeBaseVersion?.slabs?.length) {
return [];
}
const dateFormat = this.settingsService.dateFormat;
return [
{
name: activeBaseVersion.versionLabel || 'CBS Base Chart',
description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart',
fromDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) ||
activeBaseVersion.effectiveFromDate,
...(activeBaseVersion.effectiveToDate
? {
endDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) ||
activeBaseVersion.effectiveToDate
}
: {}),
isPrimaryGroupingByAmount: false,
locale: this.settingsService.language.code,
dateFormat,
chartSlabs: activeBaseVersion.slabs.map((slab: any) => {
const chartSlab: any = {
periodType: slab.periodTypeEnum,
fromPeriod: slab.fromPeriod,
toPeriod: slab.toPeriod,
annualInterestRate: slab.annualInterestRate,
description: slab.description || '',
incentives: [] as any[]
};
if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') {
chartSlab.amountRangeFrom = slab.amountRangeFrom;
}
if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') {
chartSlab.amountRangeTo = slab.amountRangeTo;
}
return chartSlab;
})
}
] as any[];
}
private pruneEmptyValues<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T;
}
if (value && typeof value === 'object') {
return Object.entries(value as Record<string, any>).reduce(
(acc, [
key,
currentValue
]) => {
const cleanedValue = this.pruneEmptyValues(currentValue);
if (cleanedValue === undefined) {
return acc;
}
return {
...acc,
[key]: cleanedValue
};
},
{} as Record<string, any>
) as T;
}
if (value === '' || value === null || value === undefined) {
return undefined as T;
}
return value;
}
submit() {
const fixedDepositProduct = {
const fixedDepositProduct: any = this.pruneEmptyValues({
...this.fixedDepositProduct,
charges: this.fixedDepositProduct.charges.map((charge: any) => ({ id: charge.id })),
charts: this.fixedDepositProductCharts,
charges: (this.fixedDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
};
});
delete fixedDepositProduct.advancedAccountingRules;
this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => {
this.router.navigate(
@@ -6,104 +6,197 @@
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"
#fixedDepositProductStepper
<div class="fdp-container">
<div class="fdp-header">
<i class="fas fa-piggy-bank fdp-header__icon"></i>
<h2 class="fdp-header__title">{{ 'labels.heading.Edit Fixed Deposit Product' | translate }}</h2>
</div>
<mat-tab-group
class="fdp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<ng-template matStepperIcon="number">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="done">
<fa-icon icon="check" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="error">
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
</ng-template>
<ng-template matStepperIcon="preview">
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="fixedDepositProductDetailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-fixed-deposit-product-details-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate">
</mifosx-fixed-deposit-product-details-step>
</mat-step>
<mat-step [stepControl]="fixedDepositProductCurrencyForm">
<ng-template matStepLabel>{{ 'labels.inputs.CURRENCY' | translate }}</ng-template>
<mifosx-fixed-deposit-product-currency-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate">
</mifosx-fixed-deposit-product-currency-step>
</mat-step>
<mat-step [stepControl]="fixedDepositProductTermsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-fixed-deposit-product-terms-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate">
</mifosx-fixed-deposit-product-terms-step>
</mat-step>
<mat-step [stepControl]="fixedDepositProductSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-fixed-deposit-product-settings-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate">
</mifosx-fixed-deposit-product-settings-step>
</mat-step>
<mat-step [stepControl]="fixedDepositProductInterestRateChartForm">
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mifosx-fixed-deposit-product-interest-rate-chart-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
>
</mifosx-fixed-deposit-product-interest-rate-chart-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-fixed-deposit-product-charges-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[currencyCode]="fixedDepositProductCurrencyForm.get('currencyCode')"
>
</mifosx-fixed-deposit-product-charges-step>
</mat-step>
<mat-step [stepControl]="fixedDepositProductAccountingForm">
<ng-template matStepLabel>{{ 'labels.inputs.ACCOUNTING' | translate }}</ng-template>
<mifosx-fixed-deposit-product-accounting-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[accountingRuleData]="accountingRuleData"
[fixedDepositProductFormValid]="fixedDepositProductFormValidAndNotPrinstine"
>
</mifosx-fixed-deposit-product-accounting-step>
</mat-step>
@if (fixedDepositProductFormValidAndNotPrinstine) {
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-fixed-deposit-product-preview-step
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-info-circle fdp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-details-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
></mifosx-fixed-deposit-product-details-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-coins fdp-tab-icon"></i>
{{ 'labels.inputs.CURRENCY' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-currency-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
></mifosx-fixed-deposit-product-currency-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-sliders-h fdp-tab-icon"></i>
{{ 'labels.inputs.TERMS' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-terms-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[fdType]="fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value"
></mifosx-fixed-deposit-product-terms-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-cog fdp-tab-icon"></i>
{{ 'labels.inputs.SETTINGS' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-settings-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
></mifosx-fixed-deposit-product-settings-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-receipt fdp-tab-icon"></i>
{{ 'labels.inputs.CHARGES' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-charges-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[currencyCode]="fixedDepositProductCurrencyForm?.get('currencyCode')"
></mifosx-fixed-deposit-product-charges-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-chart-area fdp-tab-icon"></i>
CBS Base Chart
</ng-template>
<div class="fdp-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>
@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="fdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: fdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-book fdp-tab-icon"></i>
{{ 'labels.inputs.ACCOUNTING' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-accounting-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="fixedDepositProductInterestRateChartStep.chartSlabsDisplayedColumns"
[accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct"
(submitEvent)="submit()"
>
</mifosx-fixed-deposit-product-preview-step>
</mat-step>
[fixedDepositProductFormValid]="fixedDepositProductFormValid"
[isDormancyTrackingActive]="fixedDepositProductCurrencyForm?.get('dormancyAlert')"
></mifosx-fixed-deposit-product-accounting-step>
</div>
</mat-tab>
@if (fixedDepositProductFormValid) {
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-eye fdp-tab-icon"></i>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-preview-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="[previewChartSlabColumns]"
[accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct"
(submitEvent)="submit()"
></mifosx-fixed-deposit-product-preview-step>
</div>
</mat-tab>
}
</mat-stepper>
</mat-tab-group>
<div class="fdp-action-bar">
<button mat-raised-button [routerLink]="['../../']" class="fdp-action-bar__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<div class="fdp-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 (fixedDepositProductFormValid && 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;
.fdp-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.fdp-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;
}
}
.fdp-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: 120px;
padding: 0 16px;
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;
}
}
}
.fdp-tab-icon {
margin-right: 8px;
font-size: 14px;
}
.fdp-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;
}
.fdp-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,15 +15,27 @@ import { FixedDepositProductDetailsStepComponent } from '../fixed-deposit-produc
import { FixedDepositProductCurrencyStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component';
import { FixedDepositProductTermsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component';
import { FixedDepositProductSettingsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component';
import { FixedDepositProductInterestRateChartStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component';
import { FixedDepositProductChargesStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component';
import { FixedDepositProductAccountingStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { MatTabsModule } from '@angular/material/tabs';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { Dates } from 'app/core/utils/dates';
import { Accounting } from 'app/core/utils/accounting';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FixedDepositProductPreviewStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -34,16 +46,22 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./edit-fixed-deposit-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
MatTabsModule,
FaIconComponent,
MatStep,
MatStepLabel,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FixedDepositProductDetailsStepComponent,
FixedDepositProductCurrencyStepComponent,
FixedDepositProductTermsStepComponent,
FixedDepositProductSettingsStepComponent,
FixedDepositProductInterestRateChartStepComponent,
FixedDepositProductChargesStepComponent,
FixedDepositProductAccountingStepComponent,
FixedDepositProductPreviewStepComponent
@@ -55,6 +73,7 @@ export class EditFixedDepositProductComponent {
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(FixedDepositProductDetailsStepComponent, { static: true })
fixedDepositProductDetailsStep: FixedDepositProductDetailsStepComponent;
@@ -64,8 +83,6 @@ export class EditFixedDepositProductComponent {
fixedDepositProductTermsStep: FixedDepositProductTermsStepComponent;
@ViewChild(FixedDepositProductSettingsStepComponent, { static: true })
fixedDepositProductSettingsStep: FixedDepositProductSettingsStepComponent;
@ViewChild(FixedDepositProductInterestRateChartStepComponent, { static: true })
fixedDepositProductInterestRateChartStep: FixedDepositProductInterestRateChartStepComponent;
@ViewChild(FixedDepositProductChargesStepComponent, { static: true })
fixedDepositProductChargesStep: FixedDepositProductChargesStepComponent;
@ViewChild(FixedDepositProductAccountingStepComponent, { static: true })
@@ -73,6 +90,23 @@ export class EditFixedDepositProductComponent {
fixedDepositProductsTemplate: any;
accountingRuleData: string[] = [];
selectedTabIndex = 0;
fdInterestRates: any[] = [];
readonly fdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
readonly previewChartSlabColumns = [
'period',
'amountRange',
'annualInterestRate',
'description',
'actions'
];
/**
* @param {ActivatedRoute} route Activated Route.
@@ -86,47 +120,42 @@ export class EditFixedDepositProductComponent {
this.fixedDepositProductsTemplate = data.fixedDepositProductAndTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.fdInterestRates = rates || [];
},
error: () => {
this.fdInterestRates = [];
}
});
}
get fixedDepositProductDetailsForm() {
return this.fixedDepositProductDetailsStep.fixedDepositProductDetailsForm;
return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm;
}
get fixedDepositProductCurrencyForm() {
return this.fixedDepositProductCurrencyStep.fixedDepositProductCurrencyForm;
return this.fixedDepositProductCurrencyStep?.fixedDepositProductCurrencyForm;
}
get fixedDepositProductTermsForm() {
return this.fixedDepositProductTermsStep.fixedDepositProductTermsForm;
return this.fixedDepositProductTermsStep?.fixedDepositProductTermsForm;
}
get fixedDepositProductSettingsForm() {
return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm;
}
get fixedDepositProductInterestRateChartForm() {
return this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChartForm;
return this.fixedDepositProductSettingsStep?.fixedDepositProductSettingsForm;
}
get fixedDepositProductAccountingForm() {
return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm;
return this.fixedDepositProductAccountingStep?.fixedDepositProductAccountingForm;
}
get fixedDepositProductFormValidAndNotPrinstine() {
return (
this.fixedDepositProductDetailsForm.valid &&
this.fixedDepositProductCurrencyForm.valid &&
this.fixedDepositProductTermsForm.valid &&
this.fixedDepositProductSettingsForm.valid &&
this.fixedDepositProductInterestRateChartForm.valid &&
this.fixedDepositProductAccountingForm.valid &&
(this.fixedDepositProductDetailsForm.pristine ||
this.fixedDepositProductCurrencyForm.pristine ||
this.fixedDepositProductTermsForm.pristine ||
this.fixedDepositProductSettingsForm.pristine ||
this.fixedDepositProductInterestRateChartForm.pristine ||
this.fixedDepositProductAccountingForm.pristine)
);
get totalTabs(): number {
return this.fixedDepositProductFormValid ? 8 : 7;
}
get fixedDepositProductFormValid() {
return !!this.fixedDepositProductDetailsForm?.valid;
}
get fixedDepositProduct() {
@@ -135,33 +164,118 @@ export class EditFixedDepositProductComponent {
...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency,
...this.fixedDepositProductTermsStep.fixedDepositProductTerms,
...this.fixedDepositProductSettingsStep.fixedDepositProductSettings,
...this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChart,
...this.fixedDepositProductChargesStep.fixedDepositProductCharges,
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting,
charts: this.fixedDepositProductCharts,
fdSlabs: this.activeBaseVersion?.slabs || []
};
}
submit() {
const fixedDepositProduct = {
...this.fixedDepositProduct,
charges: this.fixedDepositProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code // locale required for depositAmount
};
if (!fixedDepositProduct.description) {
fixedDepositProduct.description = '';
}
const charts: any[] = [];
fixedDepositProduct.charts.forEach((chart: any) => {
if (chart['amountRangeFrom'] === '') {
delete chart['amountRangeFrom'];
}
if (chart['amountRangeTo'] === '') {
delete chart['amountRangeTo'];
}
charts.push(chart);
});
fixedDepositProduct.charts = charts;
get selectedFdType(): string | null {
return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value ?? null;
}
get activeBaseVersion() {
const fdType = this.selectedFdType;
const filtered = fdType ? this.fdInterestRates.filter((v: any) => v.fdType === fdType) : this.fdInterestRates;
return filtered.find((v: any) => v.isActive) ?? filtered[0] ?? null;
}
get activeBaseSlabs() {
return this.activeBaseVersion?.slabs || [];
}
get fixedDepositProductCharts() {
const activeBaseVersion = this.activeBaseVersion;
if (!activeBaseVersion?.slabs?.length) {
return [];
}
const dateFormat = this.settingsService.dateFormat;
const formattedFromDate =
this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) || activeBaseVersion.effectiveFromDate;
// Find the existing chart from the template to UPDATE it instead of creating a duplicate
let existingChartId: number | undefined;
let existingChartSlabs: any[] = [];
const templateCharts: any[] = this.fixedDepositProductsTemplate?.charts ?? [];
if (templateCharts.length) {
const match = templateCharts.find((c: any) => {
const d = c.fromDate;
if (Array.isArray(d)) {
const formatted = this.dateUtils.formatDate(new Date(d[0], d[1] - 1, d[2]), dateFormat);
return formatted === formattedFromDate;
}
return d === formattedFromDate;
});
const matchedChart = match ?? templateCharts[0];
existingChartId = matchedChart?.id;
existingChartSlabs = matchedChart?.chartSlabs ?? [];
}
if (!existingChartId && this.fixedDepositProductsTemplate?.activeChart?.id) {
existingChartId = this.fixedDepositProductsTemplate.activeChart.id;
existingChartSlabs = this.fixedDepositProductsTemplate.activeChart.chartSlabs ?? [];
}
// Build a lookup: fromPeriod+toPeriod → existing slab id (to update existing slabs, not add new ones)
const existingSlabIdMap = new Map<string, number>();
for (const slab of existingChartSlabs) {
const key = `${slab.fromPeriod ?? ''}_${slab.toPeriod ?? ''}`;
if (slab.id) {
existingSlabIdMap.set(key, slab.id);
}
}
return [
{
...(existingChartId ? { id: existingChartId } : {}),
name: activeBaseVersion.versionLabel || 'CBS Base Chart',
description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart',
fromDate: formattedFromDate,
...(activeBaseVersion.effectiveToDate
? {
endDate:
this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) ||
activeBaseVersion.effectiveToDate
}
: {}),
isPrimaryGroupingByAmount: false,
locale: this.settingsService.language.code,
dateFormat,
chartSlabs: activeBaseVersion.slabs.map((slab: any) => {
const key = `${slab.fromPeriod ?? ''}_${slab.toPeriod ?? ''}`;
const existingSlabId = existingSlabIdMap.get(key);
const chartSlab: any = {
...(existingSlabId ? { id: existingSlabId } : {}),
periodType: slab.periodTypeEnum,
fromPeriod: slab.fromPeriod,
toPeriod: slab.toPeriod,
annualInterestRate: slab.annualInterestRate,
description: slab.description || '',
incentives: [] as any[]
};
if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') {
chartSlab.amountRangeFrom = slab.amountRangeFrom;
}
if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') {
chartSlab.amountRangeTo = slab.amountRangeTo;
}
return chartSlab;
})
}
] as any[];
}
submit() {
const fixedDepositProduct: any = this.pruneEmptyValues({
...this.fixedDepositProduct,
charts: this.fixedDepositProductCharts,
charges: (this.fixedDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
});
delete fixedDepositProduct.advancedAccountingRules;
this.productsService
.updateFixedDepositProduct(this.fixedDepositProductsTemplate.id, fixedDepositProduct)
@@ -169,4 +283,35 @@ export class EditFixedDepositProductComponent {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
private pruneEmptyValues<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T;
}
if (value && typeof value === 'object') {
return Object.entries(value as Record<string, any>).reduce(
(acc, [
key,
currentValue
]) => {
const cleanedValue = this.pruneEmptyValues(currentValue);
if (cleanedValue === undefined) {
return acc;
}
return {
...acc,
[key]: cleanedValue
};
},
{} as Record<string, any>
) as T;
}
if (value === '' || value === null || value === undefined) {
return undefined as T;
}
return value;
}
}
@@ -36,7 +36,9 @@
</div>
<div class="fdpa-list">
@for (row of section.rows; track row.key) {
@if (!row.accrualOnly || isAccrualAccounting()) {
@if (
(!row.accrualOnly || isAccrualAccounting()) && (!row.dormancyOnly || isDormancyTrackingEnabled())
) {
<div class="fdpa-row">
<div class="fdpa-row__meta">
<span
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpa-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input, OnInit, inject } from '@angular/core';
import { Component, Input, OnChanges, OnInit, SimpleChanges, inject } from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
@@ -32,6 +32,7 @@ type AccountingRowConfig = {
options: GLAccount[];
required?: boolean;
accrualOnly?: boolean;
dormancyOnly?: boolean;
};
type AccountingSectionConfig = {
@@ -62,13 +63,14 @@ type OtherGlMappingValue = {
GlAccountSelectorComponent
]
})
export class FixedDepositProductAccountingStepComponent implements OnInit {
export class FixedDepositProductAccountingStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private accounting = inject(Accounting);
@Input() fixedDepositProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() fixedDepositProductFormValid: boolean;
@Input() isDormancyTrackingActive: any;
fixedDepositProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = [];
@@ -83,6 +85,16 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
}
ngOnInit() {
this.initializeAccountingForm();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fixedDepositProductsTemplate?.currentValue) {
this.initializeAccountingForm();
}
}
private initializeAccountingForm() {
const accountingMappings = this.fixedDepositProductsTemplate?.accountingMappings || {};
const mappings = {
...accountingMappings,
@@ -98,14 +110,25 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
this.buildAccountingSectionConfigs();
this.fixedDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId });
this.fixedDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }, { emitEvent: false });
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
this.setDormancyControl();
}
createFixedDepositProductAccountingForm() {
this.fixedDepositProductAccountingForm = this.formBuilder.group({
accountingRule: [2],
savingsReferenceAccountId: [''],
savingsControlAccountId: [''],
transfersInSuspenseAccountId: [''],
interestOnSavingsAccountId: [''],
incomeFromFeeAccountId: [''],
incomeFromPenaltyAccountId: [''],
feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''],
escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
@@ -119,17 +142,54 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
});
}
private extractAccountId(mapping: any, ...keys: string[]) {
for (const key of keys) {
const value = mapping?.[key];
if (value?.id != null) {
return value.id;
}
if (value?.glAccountId != null) {
return value.glAccountId;
}
if (typeof value === 'number' || typeof value === 'string') {
return value;
}
}
return '';
}
private patchAccountingValues(mappings: any) {
this.fixedDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
interestPayableAccountId: mappings.interestPayableAccount?.id || ''
savingsReferenceAccountId: this.extractAccountId(
mappings,
'savingsReferenceAccount',
'savingsReferenceAccountId'
),
savingsControlAccountId: this.extractAccountId(mappings, 'savingsControlAccount', 'savingsControlAccountId'),
transfersInSuspenseAccountId: this.extractAccountId(
mappings,
'transfersInSuspenseAccount',
'transfersInSuspenseAccountId'
),
interestOnSavingsAccountId: this.extractAccountId(
mappings,
'interestOnSavingsAccount',
'interestOnSavingsAccountId'
),
incomeFromFeeAccountId: this.extractAccountId(mappings, 'incomeFromFeeAccount', 'incomeFromFeeAccountId'),
incomeFromPenaltyAccountId: this.extractAccountId(
mappings,
'incomeFromPenaltyAccount',
'incomeFromPenaltyAccountId'
),
feesReceivableAccountId: this.extractAccountId(mappings, 'feeReceivableAccount', 'feesReceivableAccountId'),
penaltiesReceivableAccountId: this.extractAccountId(
mappings,
'penaltyReceivableAccount',
'penaltiesReceivableAccountId'
),
interestPayableAccountId: this.extractAccountId(mappings, 'interestPayableAccount', 'interestPayableAccountId'),
escheatLiabilityId: this.extractAccountId(mappings, 'escheatLiabilityAccount', 'escheatLiabilityId')
});
if (this.isCashOrAccrualAccounting()) {
@@ -215,53 +275,43 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
}
private setDormancyControl() {
this.isDormancyTrackingActive?.valueChanges?.subscribe(() => {
this.syncAccountingControls(this.fixedDepositProductAccountingForm.value.accountingRule);
});
}
isDormancyTrackingEnabled(): boolean {
return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false;
}
private syncAccountingControls(accountingRule: number) {
const addControl = (name: string, required = false) => {
if (!this.fixedDepositProductAccountingForm.contains(name)) {
this.fixedDepositProductAccountingForm.addControl(
name,
new UntypedFormControl('', required ? Validators.required : undefined)
);
} else if (required) {
this.fixedDepositProductAccountingForm.get(name)?.setValidators(Validators.required);
this.fixedDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false });
}
};
const removeControl = (name: string) => {
if (this.fixedDepositProductAccountingForm.contains(name)) {
this.fixedDepositProductAccountingForm.removeControl(name);
}
};
if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) {
addControl('savingsReferenceAccountId', true);
addControl('savingsControlAccountId', true);
addControl('transfersInSuspenseAccountId', true);
addControl('interestOnSavingsAccountId', true);
addControl('incomeFromFeeAccountId', true);
addControl('incomeFromPenaltyAccountId', true);
if (this.accounting.isAccrualAccountingRuleId(accountingRule)) {
addControl('feesReceivableAccountId', true);
addControl('penaltiesReceivableAccountId', true);
addControl('interestPayableAccountId', true);
const setRequired = (name: string, required: boolean) => {
const ctrl = this.fixedDepositProductAccountingForm.get(name);
if (!ctrl) return;
if (required) {
ctrl.setValidators(Validators.required);
} else {
removeControl('feesReceivableAccountId');
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
ctrl.clearValidators();
}
} else {
[
'savingsReferenceAccountId',
'savingsControlAccountId',
'transfersInSuspenseAccountId',
'interestOnSavingsAccountId',
'incomeFromFeeAccountId',
'incomeFromPenaltyAccountId',
'feesReceivableAccountId',
'penaltiesReceivableAccountId',
'interestPayableAccountId'
].forEach(removeControl);
ctrl.updateValueAndValidity({ emitEvent: false });
};
const cashOrAccrual = this.accounting.isCashOrAccrualAccountingRuleId(accountingRule);
const accrualOnly = this.accounting.isAccrualAccountingRuleId(accountingRule);
setRequired('savingsReferenceAccountId', cashOrAccrual);
setRequired('savingsControlAccountId', cashOrAccrual);
setRequired('transfersInSuspenseAccountId', cashOrAccrual);
setRequired('interestOnSavingsAccountId', cashOrAccrual);
setRequired('incomeFromFeeAccountId', cashOrAccrual);
setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
setRequired('feesReceivableAccountId', accrualOnly);
setRequired('penaltiesReceivableAccountId', accrualOnly);
setRequired('interestPayableAccountId', accrualOnly);
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
if (!cashOrAccrual) {
this.clearOtherGlMappings();
}
}
@@ -331,6 +381,14 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions,
accrualOnly: true
},
{
key: 'escheatLiabilityId',
label: 'Escheat Liability Account',
inputLabel: 'Escheat liability account',
controlName: 'escheatLiabilityId',
options: this.liabilityAccountOptions,
dormancyOnly: true
}
],
otherMappingsControlName: 'liabilitiesOtherGlMappings',
@@ -392,7 +450,23 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
...accounting
} = this.fixedDepositProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings();
return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) };
const cleanedAccounting = Object.entries(accounting).reduce(
(acc, [
key,
value
]) => {
if (value === '' || value === null || value === undefined) {
return acc;
}
return {
...acc,
[key]: value
};
},
{} as Record<string, any>
);
return { ...cleanedAccounting, ...(otherGlMappings ? { otherGlMappings } : {}) };
}
isCashOrAccrualAccounting(): boolean {
@@ -63,27 +63,30 @@
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="balanceSwapOption">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
@if (fixedDepositProductCurrencyForm.get('dormancyAlert')?.value === true) {
<div
class="fdpc-grid"
style="border-top: 1px solid #e2e8f0; background: #f8fafc; padding-top: 24px; padding-bottom: 24px"
>
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Days to Inactive' | translate }}</mat-label>
<input matInput type="number" formControlName="daysToInactive" required />
<mat-error> {{ 'labels.inputs.Days to Inactive' | translate }} is <strong>required</strong> </mat-error>
</mat-form-field>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Days to Dormancy' | translate }}</mat-label>
<input matInput type="number" formControlName="daysToDormancy" required />
<mat-error> {{ 'labels.inputs.Days to Dormancy' | translate }} is <strong>required</strong> </mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Days to Escheat' | translate }}</mat-label>
<input matInput type="number" formControlName="daysToEscheat" required />
<mat-error> {{ 'labels.inputs.Days to Escheat' | translate }} is <strong>required</strong> </mat-error>
</mat-form-field>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="msme">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
}
<div class="fdpc-alert">
<div class="fdpc-alert__content">
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpc-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -51,8 +51,10 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
this.fixedDepositProductsTemplate.currency?.decimalPlaces ??
2,
dormancyAlert: this.fixedDepositProductsTemplate.dormancyAlert ?? false,
balanceSwapOption: this.fixedDepositProductsTemplate.balanceSwapOption ?? false,
msme: this.fixedDepositProductsTemplate.msme ?? false,
isDormancyTrackingActive: this.fixedDepositProductsTemplate.isDormancyTrackingActive ?? false,
daysToInactive: this.fixedDepositProductsTemplate.daysToInactive ?? 30,
daysToDormancy: this.fixedDepositProductsTemplate.daysToDormancy ?? 60,
daysToEscheat: this.fixedDepositProductsTemplate.daysToEscheat ?? 90,
nominationFacility: this.fixedDepositProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.fixedDepositProductsTemplate.autoDebitFacility ?? false
});
@@ -72,8 +74,10 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
]
],
dormancyAlert: [false],
balanceSwapOption: [false],
msme: [false],
isDormancyTrackingActive: [false],
daysToInactive: [30],
daysToDormancy: [60],
daysToEscheat: [90],
nominationFacility: [false],
autoDebitFacility: [false]
});
@@ -81,14 +85,21 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
get fixedDepositProductCurrency() {
const formValue = this.fixedDepositProductCurrencyForm.value;
return {
const result: any = {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
};
if (formValue.dormancyAlert) {
result.isDormancyTrackingActive = true;
result.daysToInactive = formValue.daysToInactive;
result.daysToDormancy = formValue.daysToDormancy;
result.daysToEscheat = formValue.daysToEscheat;
} else {
result.isDormancyTrackingActive = false;
}
return result;
}
}
@@ -79,12 +79,13 @@
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Customer Segment</mat-label>
<mat-select formControlName="customerSegment">
@for (customerSegment of customerSegmentData; track customerSegment) {
<mat-option [value]="customerSegment">{{ customerSegment }}</mat-option>
}
<mat-label>FD Type</mat-label>
<mat-select formControlName="fdType" required>
<mat-option value="SINGLE">FD Single (Days &lt; 1 Year)</mat-option>
<mat-option value="DOUBLE">FD Double (≥ 1 Year)</mat-option>
<mat-option value="SPECIAL">FD Special (Monthly Interest Payout)</mat-option>
</mat-select>
<mat-error>FD Type <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
@@ -110,15 +111,6 @@
<mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Introducer Required</mat-label>
<mat-select formControlName="introducerRequired">
@for (option of yesNoOptions; track option.value) {
<mat-option [value]="option.value">{{ option.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<div class="fdpd-field fdpd-field--full fdpd-checkbox-row">
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
</div>
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpd-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -34,12 +34,12 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
fixedDepositProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = [];
customerSegmentData: string[] = [];
liabilityAccountOptions: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
fdTypeOptions: string[] = [
'SINGLE',
'DOUBLE',
'SPECIAL'
];
liabilityAccountOptions: any[] = [];
constructor() {
this.createFixedDepositProductDetailsForm();
@@ -72,11 +72,13 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
null,
Validators.required
],
customerSegment: [null],
fdType: [
null,
Validators.required
],
glAccountId: [null],
effectiveTo: [''],
endDate: [''],
introducerRequired: [false],
shareLinkingEnabled: [false]
});
}
@@ -99,7 +101,6 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions);
this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? [];
this.cdr.detectChanges();
@@ -109,11 +110,10 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
shortName: isEditing ? template.shortName : template.shortName || '',
description: template.description || '',
accountType: template.accountType ?? this.accountTypeData[0] ?? null,
customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null,
fdType: template.fdType ?? null,
glAccountId: template.glAccountId ?? null,
effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate),
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
shareLinkingEnabled: !!template.shareLinkingEnabled
});
}
@@ -6,103 +6,39 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.fdpp-container {
display: flex;
flex-direction: column;
gap: 16px;
@use 'assets/styles/colours' as *;
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
}
.fdpp-title {
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
h2,
h3,
h4 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #202124;
}
.fdpp-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
i {
font-size: 14px;
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
}
}
.fdpp-section-body {
padding: 16px 24px;
}
.fdpp-preview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.fdpp-row {
display: flex;
flex-direction: column;
padding: 6px 0;
&--full {
grid-column: 1 / -1;
}
}
.fdpp-label {
font-size: 12px;
font-weight: 500;
color: #5f6368;
margin-bottom: 2px;
}
.fdpp-value {
font-size: 14px;
font-weight: 500;
color: #202124;
table {
width: 100%;
}
.fdpp-chart-block {
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 16px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
/* ── Incentives expand row ── */
.fdpp-table-wrapper {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin-top: 8px;
table {
width: 100%;
}
@@ -132,20 +68,95 @@
}
}
.fdpp-subsection-title {
margin: 16px 0 8px;
font-size: 14px;
font-weight: 600;
color: #202124;
/* ── GL mapping summary (same as RD product preview) ── */
.spa-gl-summary,
.spa-other-gl-summary {
display: flex;
flex-direction: column;
gap: 0.9rem;
margin-top: 1.25rem;
}
&:first-child {
margin-top: 0;
.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;
}
}
.fdpp-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
.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;
word-break: break-word;
}
.spa-other-gl-table {
margin-top: 0.5rem;
margin-bottom: 0.75rem;
table-layout: fixed;
width: 100%;
&:first-of-type {
margin-top: 0.25rem;
}
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;
}
}
.spa-other-gl-summary .mat-elevation-z1 {
border-radius: 10px;
overflow: hidden;
}
@media (width <= 768px) {
.spa-gl-row {
grid-template-columns: 1fr;
gap: 0.35rem;
}
}
@@ -10,6 +10,7 @@ import { Component, OnInit, Input, Output, EventEmitter, SimpleChanges, OnChange
import { trigger, state, transition, animate, style } from '@angular/animations';
import { Accounting } from 'app/core/utils/accounting';
import { OptionData } from 'app/shared/models/option-data.model';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
@@ -44,6 +45,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
@@ -100,6 +102,23 @@ export class FixedDepositProductPreviewStepComponent implements OnInit, OnChange
accountingMappings: any = {};
accountingRule: OptionData;
readonly fdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
get otherGlMappings() {
return this.fixedDepositProduct?.otherGlMappings || this.fixedDepositProduct?.otherGlMappingsMap || null;
}
get fdSlabs(): any[] {
return this.fixedDepositProduct?.fdSlabs ?? [];
}
ngOnInit() {
this.setCurrentValues();
}
@@ -142,7 +161,9 @@ export class FixedDepositProductPreviewStepComponent implements OnInit, OnChange
incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
this.fixedDepositProduct.incomeFromPenaltyAccountId,
incomeAccountData
)
),
escheatLiabilityAccount:
this.accounting.glAccountLookUp(this.fixedDepositProduct.escheatLiabilityId, liabilityAccountData) || null
};
if (this.isAccrualAccounting()) {
@@ -173,4 +194,30 @@ export class FixedDepositProductPreviewStepComponent implements OnInit, OnChange
isAccrualAccounting(): boolean {
return this.accounting.isAccrualAccountingRuleId(this.fixedDepositProduct.accountingRule);
}
getLockinPeriodFrequencyTypeLabel(value: any): string {
if (!value && value !== 0) return '';
const options = this.fixedDepositProductsTemplate?.lockinPeriodFrequencyTypeOptions || [];
const found = options.find((o: any) => o.id === value);
return found?.value || String(value);
}
getGlAccountName(id: any): string {
if (!id) return '';
const accounts = this.fixedDepositProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? [];
const found = accounts.find((a: any) => a.id === id || a.id === +id);
return found ? `${found.name} (${found.glCode})` : String(id);
}
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;
}
}
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdps-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -7,6 +7,33 @@
-->
<form [formGroup]="fixedDepositProductTermsForm">
<input type="hidden" formControlName="minDepositTerm" />
<input type="hidden" formControlName="minDepositTermTypeId" />
<input type="hidden" formControlName="maxDepositTerm" />
<input type="hidden" formControlName="maxDepositTermTypeId" />
<input type="hidden" formControlName="inMultiplesOfDepositTerm" />
<input type="hidden" formControlName="inMultiplesOfDepositTermTypeId" />
<div class="fdpt-section">
<div class="fdpt-section__header">
<span class="fdpt-section__badge"><i class="fas fa-coins"></i></span>
<span>Deposit Amount</span>
</div>
<div class="fdpt-body">
<div class="fdpt-grid fdpt-grid--three">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minDepositAmount" min="0" step="0.01" />
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxDepositAmount" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
<!-- Interest Rate & Posting Terms -->
<div class="fdpt-section">
<div class="fdpt-section__header">
@@ -65,6 +92,28 @@
</mat-form-field>
</div>
<div class="fdpt-grid fdpt-grid--three">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Interest Compounding Period</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (opt of interestCompoundingPeriodTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Interest Compounding Period <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Days in Year</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required>
@for (opt of interestCalculationDaysInYearTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Days in Year <strong>is required</strong></mat-error>
</mat-form-field>
</div>
<div class="fdpt-grid fdpt-grid--three">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Interest Calculate</mat-label>
@@ -83,7 +132,7 @@
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod">
@for (period of periodFrequencyData; track period.id) {
@for (period of averagePeriodData; track period.id) {
<mat-option [value]="period.value">{{ period.value }}</mat-option>
}
</mat-select>
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpt-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { Dates } from 'app/core/utils/dates';
@@ -22,12 +22,13 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip
]
})
export class FixedDepositProductTermsStepComponent implements OnInit {
export class FixedDepositProductTermsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() fixedDepositProductsTemplate: any;
@Input() fdType: string | null = null;
fixedDepositProductTermsForm: UntypedFormGroup;
@@ -36,6 +37,7 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
interestCalculationTypeData: any;
interestCalculationTypeOptionData: any;
interestCalculationDaysInYearTypeData: any;
averagePeriodData: any[] = [];
periodFrequencyData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
@@ -47,16 +49,33 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
}
ngOnInit() {
this.initializeTerms();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fdType) {
this.applyFdTypeConstraints();
}
if (changes.fixedDepositProductsTemplate?.currentValue) {
this.initializeTerms();
}
}
private initializeTerms() {
if (!this.fixedDepositProductsTemplate) return;
this.interestCompoundingPeriodTypeData = this.fixedDepositProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions;
this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationTypeOptionData = this.fixedDepositProductsTemplate.interestCalculationTypeOption || [];
this.interestCalculationDaysInYearTypeData =
this.fixedDepositProductsTemplate.interestCalculationDaysInYearTypeOptions;
this.periodFrequencyData = this.fixedDepositProductsTemplate.periodFrequencyOptions || [];
this.averagePeriodData = this.fixedDepositProductsTemplate.periodFrequencyOptions || [];
this.periodFrequencyData = this.fixedDepositProductsTemplate.periodFrequencyTypeOptions || [];
const isEditing = !!this.fixedDepositProductsTemplate.id;
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
const defaultPeriodTypeId = this.periodFrequencyData[0]?.id ?? null;
const defaultAveragePeriod = this.averagePeriodData[0]?.value || 'Monthly';
const savedEnumTypeId =
this.fixedDepositProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1;
const savedSelectorId = isEditing
@@ -65,15 +84,15 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
null)
: (this.interestCalculationTypeOptionData?.[0]?.id ?? null);
if (isEditing) {
this.fixedDepositProductTermsForm.patchValue({
minDepositAmount: this.fixedDepositProductsTemplate.minDepositAmount,
depositAmount: this.fixedDepositProductsTemplate.depositAmount,
maxDepositAmount: this.fixedDepositProductsTemplate.maxDepositAmount
});
}
this.fixedDepositProductTermsForm.patchValue({
minDepositTerm: this.fixedDepositProductsTemplate.minDepositTerm ?? 1,
minDepositTermTypeId: this.fixedDepositProductsTemplate.minDepositTermType?.id ?? defaultPeriodTypeId,
maxDepositTerm: this.fixedDepositProductsTemplate.maxDepositTerm ?? '',
maxDepositTermTypeId: this.fixedDepositProductsTemplate.maxDepositTermType?.id ?? '',
inMultiplesOfDepositTerm: this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm ?? '',
inMultiplesOfDepositTermTypeId: this.fixedDepositProductsTemplate.inMultiplesOfDepositTermType?.id ?? '',
minDepositAmount: this.fixedDepositProductsTemplate.minDepositAmount,
maxDepositAmount: this.fixedDepositProductsTemplate.maxDepositAmount,
nominalAnnualInterestRate: this.fixedDepositProductsTemplate.nominalAnnualInterestRate ?? 0,
interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType?.id,
@@ -83,13 +102,34 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
? this.parseDate(this.fixedDepositProductsTemplate.effectiveDate)
: '',
interestCalculateSelector: savedSelectorId,
averagePeriod: isEditing ? this.fixedDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod,
averagePeriod: isEditing
? this.fixedDepositProductsTemplate.averagePeriod || defaultAveragePeriod
: defaultAveragePeriod,
autoPost: isEditing ? (this.fixedDepositProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? (this.fixedDepositProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.fixedDepositProductsTemplate.lastInterestDate
? this.parseDate(this.fixedDepositProductsTemplate.lastInterestDate)
: ''
});
this.applyFdTypeConstraints();
}
private applyFdTypeConstraints() {
if (!this.fixedDepositProductTermsForm || !this.interestPostingPeriodTypeData) return;
const postingCtrl = this.fixedDepositProductTermsForm.get('interestPostingPeriodType');
if (!postingCtrl) return;
if (this.fdType === 'SPECIAL') {
// Monthly posting = id 4 (MONTHLY in Fineract enum)
const monthlyOption = this.interestPostingPeriodTypeData?.find(
(o: any) => o.value?.toLowerCase() === 'monthly' || o.id === 4
);
if (monthlyOption) {
postingCtrl.setValue(monthlyOption.id);
postingCtrl.disable();
}
} else {
postingCtrl.enable();
}
}
private parseDate(dateValue: any): Date | null {
@@ -101,18 +141,21 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
createFixedDepositProductTermsForm() {
this.fixedDepositProductTermsForm = this.formBuilder.group({
minDepositAmount: [
'',
Validators.min(0)
],
depositAmount: [
'',
minDepositTerm: [
1,
[
Validators.required,
Validators.min(0)
]
],
maxDepositAmount: [
minDepositTermTypeId: [''],
maxDepositTerm: [
'',
Validators.min(0)
],
maxDepositTermTypeId: [''],
inMultiplesOfDepositTerm: [''],
inMultiplesOfDepositTermTypeId: [''],
minDepositAmount: [
'',
Validators.min(0)
],
@@ -139,6 +182,10 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
'',
Validators.required
],
maxDepositAmount: [
'',
Validators.min(0)
],
effectiveDate: [''],
interestCalculateSelector: [1],
averagePeriod: ['Monthly'],
@@ -152,7 +199,7 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
}
get fixedDepositProductTerms() {
const formValue = { ...this.fixedDepositProductTermsForm.value };
const formValue = { ...this.fixedDepositProductTermsForm.getRawValue() };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveDate instanceof Date) {
@@ -6,23 +6,29 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 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']" *mifosxHasPermission="'UPDATE_SAVINGSPRODUCT'">
<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">{{ fixedDepositProductData.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>
@@ -36,9 +42,59 @@
</div>
}
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<!-- Product Classification -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">Product Classification</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider>
@if (fixedDepositProductData.accountType) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Account Type' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.accountType }}</span>
</div>
}
@if (fixedDepositProductData.fdType) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.FD Type' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.fdType }}</span>
</div>
}
@if (fixedDepositProductData.glAccountName || fixedDepositProductData.glAccountId) {
<div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span>
<span class="flex-60">
{{ fixedDepositProductData.glAccountName || fixedDepositProductData.glAccountId }}
</span>
</div>
}
@if (fixedDepositProductData.effectiveTo) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span>
<span class="flex-60">{{ fixedDepositProductData.effectiveTo | dateFormat }}</span>
</div>
}
@if (fixedDepositProductData.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.endDate | dateFormat }}</span>
</div>
}
@if (fixedDepositProductData.shareLinkingEnabled !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Share Linking Enabled' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.shareLinkingEnabled | yesNo }}</span>
</div>
}
<!-- Currency -->
<mat-divider class="flex-100"></mat-divider>
<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>
@@ -57,83 +113,229 @@
<span class="flex-60">{{ fixedDepositProductData.currency.inMultiplesOf | formatNumber }}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Terms' | translate }}</h3>
@if (fixedDepositProductData.dormancyAlert !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ fixedDepositProductData.dormancyAlert | yesNo }}</span>
</div>
}
<mat-divider [inset]="true"></mat-divider>
@if (fixedDepositProductData.dormancyAlert) {
<div class="flex-100 layout-row">
<span class="flex-40">Days to Inactive:</span>
<span class="flex-60">{{ fixedDepositProductData.daysToInactive }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Days to Dormancy:</span>
<span class="flex-60">{{ fixedDepositProductData.daysToDormancy }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Days to Escheat:</span>
<span class="flex-60">{{ fixedDepositProductData.daysToEscheat }}</span>
</div>
}
@if (fixedDepositProductData.nominationFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Nomination Facility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.nominationFacility | yesNo }}</span>
</div>
}
@if (fixedDepositProductData.autoDebitFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Auto Debit Facility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.autoDebitFacility | yesNo }}</span>
</div>
}
<!-- Deposit Amount -->
@if (
fixedDepositProductData.minDepositAmount !== undefined ||
fixedDepositProductData.maxDepositAmount !== undefined
) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">Deposit Amount</h3>
<mat-divider class="flex-100"></mat-divider>
@if (fixedDepositProductData.minDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Minimum:</span>
<span class="flex-60">{{ fixedDepositProductData.minDepositAmount }}</span>
</div>
}
@if (fixedDepositProductData.maxDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Maximum:</span>
<span class="flex-60">{{ fixedDepositProductData.maxDepositAmount }}</span>
</div>
}
}
<!-- Terms -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.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">{{
fixedDepositProductData.depositAmount +
' (Min: ' +
(fixedDepositProductData.minDepositAmount
? fixedDepositProductData.minDepositAmount
: fixedDepositProductData.depositAmount) +
', Max: ' +
(fixedDepositProductData.maxDepositAmount
? fixedDepositProductData.maxDepositAmount
: fixedDepositProductData.depositAmount) +
')'
}}</span>
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest Rate' | translate }} (%):</span>
<span class="flex-60">{{ fixedDepositProductData.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCompoundingPeriodType.value }}</span>
<span class="flex-60">{{ fixedDepositProductData.interestCompoundingPeriodType?.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestPostingPeriodType.value }}</span>
<span class="flex-60">{{ fixedDepositProductData.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">{{ fixedDepositProductData.interestCalculationType.value }}</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationType?.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationDaysInYearType.value }}</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationDaysInYearType?.value }}</span>
</div>
@if (fixedDepositProductData.interestCalculateSelector) {
<div class="flex-100 layout-row">
<span class="flex-40">Interest Calculation Selector:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculateSelector }}</span>
</div>
}
@if (fixedDepositProductData.averagePeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Average Period:</span>
<span class="flex-60">{{ fixedDepositProductData.averagePeriod }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Auto Interest Post:</span>
<span class="flex-60">{{ fixedDepositProductData.autoPost | yesNo }}</span>
</div>
@if (fixedDepositProductData.frequencyFixDay !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Frequency (Fix Day):</span>
<span class="flex-60">{{ fixedDepositProductData.frequencyFixDay }}</span>
</div>
}
@if (fixedDepositProductData.lastInterestDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ fixedDepositProductData.lastInterestDate | dateFormat }}</span>
</div>
}
<!-- Settings -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Loan Facility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.loanFacility | yesNo }}</span>
</div>
@if (fixedDepositProductData.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Premature Withdrawal Option' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.prematureWithdrawalOption | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Tenure Flexibility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.tenureFlexibility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lien Marked' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.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">{{ fixedDepositProductData.withdrawalFeeForTransfers | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Enable Lock-in Period:</span>
<span class="flex-60">{{ fixedDepositProductData.enableLockinPeriod | yesNo }}</span>
</div>
@if (fixedDepositProductData.lockinPeriodFrequency !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">
{{ fixedDepositProductData.lockinPeriodFrequency }}
{{ fixedDepositProductData.lockinPeriodFrequencyType.value }}</span
>
{{ fixedDepositProductData.lockinPeriodFrequencyType?.value }}
</span>
</div>
}
@if (fixedDepositProductData.minDepositTerm) {
@if (fixedDepositProductData.maturityInstructions !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Maturity Instructions:</span>
<span class="flex-60">{{ fixedDepositProductData.maturityInstructions | yesNo }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Credit Maturity Proceeds:</span>
<span class="flex-60">{{ fixedDepositProductData.creditMaturityProceeds | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Penalty Grace Period (Missed Installments):</span>
<span class="flex-60">{{ fixedDepositProductData.penaltyGracePeriodMissedInstallments | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Panel Request Applicable:</span>
<span class="flex-60">{{ fixedDepositProductData.panelRequestApplicable | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">No Penalty (Nominee / Heir Closure):</span>
<span class="flex-60">{{ fixedDepositProductData.noPenaltyNomineeHeirClosure | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Deceased Settlement:</span>
<span class="flex-60">{{ fixedDepositProductData.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">{{ fixedDepositProductData.allowDebitTxnAfterDueDate | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Account Closed with Zero Balance in Day Closing:</span>
<span class="flex-60">{{ fixedDepositProductData.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
@if (fixedDepositProductData.minBalanceForInterestCalculation !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Min Balance for Interest Calculation:</span>
<span class="flex-60">{{ fixedDepositProductData.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
@if (fixedDepositProductData.minDepositTerm !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span>
@if (fixedDepositProductData.minDepositTerm !== undefined) {
<span class="flex-60"
>{{ fixedDepositProductData.minDepositTerm }}
&nbsp;
{{ fixedDepositProductData.minDepositTermType.value }}</span
>
}
</div>
}
@if (fixedDepositProductData.maxDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}:</span>
@if (fixedDepositProductData.maxDepositTerm !== undefined) {
<span class="flex-60"
>{{ fixedDepositProductData.maxDepositTerm }}
&nbsp;
{{ fixedDepositProductData.maxDepositTermType.value }}</span
>
}
<span class="flex-60"
>{{ fixedDepositProductData.minDepositTerm }}
{{ fixedDepositProductData.minDepositTermType?.value }}</span
>
</div>
}
@@ -142,34 +344,35 @@
<span class="flex-40">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositProductData.inMultiplesOfDepositTerm }}
-
{{ fixedDepositProductData.inMultiplesOfDepositTermType.value }}</span
{{ fixedDepositProductData.inMultiplesOfDepositTermType?.value }}</span
>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.preClosurePenalApplicable | yesNo }}</span>
</div>
@if (fixedDepositProductData.maxDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositProductData.maxDepositTerm }}
{{ fixedDepositProductData.maxDepositTermType?.value }}</span
>
</div>
}
@if (fixedDepositProductData.preClosurePenalApplicable) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.preClosurePenalApplicable | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Penal Interest' | translate }} (%):</span>
<span class="flex-60"
>{{ fixedDepositProductData.preClosurePenalInterest }} &nbsp; &nbsp; % &nbsp; &nbsp; On &nbsp;&nbsp;
{{ fixedDepositProductData.preClosurePenalInterestOnType.value }}</span
>{{ fixedDepositProductData.preClosurePenalInterest }} % On
{{ fixedDepositProductData.preClosurePenalInterestOnType?.value }}</span
>
</div>
}
@if (fixedDepositProductData.minBalanceForInterestCalculation) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.withHoldTax | yesNo }}</span>
@@ -178,249 +381,72 @@
@if (fixedDepositProductData.withHoldTax) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax group' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.taxGroup.name }}</span>
<span class="flex-60">{{ fixedDepositProductData.taxGroup?.name }}</span>
</div>
}
@if (fixedDepositProductData.activeChart) {
<div class="responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Chart' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="responsive-column flex-100">
@if (fixedDepositProductData.activeChart.name) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Name' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.name }}</span>
</div>
}
@if (fixedDepositProductData.activeChart.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.description }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.From Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.fromDate | dateFormat }}</span>
</div>
@if (fixedDepositProductData.activeChart.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.endDate | dateFormat }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.isPrimaryGroupingByAmount | yesNo }}</span>
</div>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.activeChart.chartSlabs"
multiTemplateDataRows
>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
@if (chartSlab.fromPeriod !== undefined) {
<div>{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ chartSlab.periodType.value }}</div>
}
@if (chartSlab.fromPeriod === undefined) {
<div>&nbsp;</div>
}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
@if (chartSlab.amountRangeFrom !== undefined) {
<div>{{ chartSlab.amountRangeFrom }} - {{ chartSlab.amountRangeTo }}</div>
}
@if (chartSlab.amountRangeFrom === undefined) {
<div>&nbsp;</div>
}
</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>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.description }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
@if (expandChartSlabIndex !== chartSlabIndex) {
<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>
}
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
>
<div
class="incentives flex-100 layout-row-wrap"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<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: fixedDepositProductsTemplate.chartTemplate.entityTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.attributeName
| find
: fixedDepositProductsTemplate.chartTemplate.attributeNameOptions
: 'id'
: '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
: fixedDepositProductsTemplate.chartTemplate.conditionTypeOptions
: '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: fixedDepositProductsTemplate.chartTemplate.genderOptions : 'id' : 'name'
}}</span>
}
@case (3) {
<span>{{ incentive.attributeValue }}</span>
}
@case (4) {
<span>{{
incentive.attributeValue
| find
: fixedDepositProductsTemplate.chartTemplate.clientTypeOptions
: 'id'
: 'name'
}}</span>
}
@case (5) {
<span>{{
incentive.attributeValue
| find
: fixedDepositProductsTemplate.chartTemplate.clientClassificationOptions
: '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
: fixedDepositProductsTemplate.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 | formatNumber }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="incentives-row"
></tr>
</table>
</div>
</div>
}
<!-- CBS Base Interest Rate Charts -->
@if (fdSlabs.length) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (fixedDepositProductData.charges.length) {
<div class="layout-row-wrap responsive-column flex-100">
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fdSlabs">
<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</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">
<strong>{{ slab.annualInterestRate }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="fdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: fdSlabColumns"></tr>
</table>
</div>
}
<!-- Charges -->
@if (fixedDepositProductData.charges?.length) {
<div class="layout-row-wrap responsive-column flex-100">
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fixedDepositProductData.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>
@@ -428,107 +454,16 @@
</div>
}
<!-- Accounting -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider>
<mifosx-view-savings-accounting-details
class="container"
[accountingRule]="fixedDepositProductData.accountingRule"
[accountingMappings]="fixedDepositProductData.accountingMappings"
>
</mifosx-view-savings-accounting-details>
@if (
fixedDepositProductData.advancedAccountingRules &&
(fixedDepositProductData.paymentChannelToFundSourceMappings?.length ||
fixedDepositProductData.feeToIncomeAccountMappings?.length ||
fixedDepositProductData.penaltyToIncomeAccountMappings?.length)
) {
<div class="flex-100 layout-row-wrap layout-lt-md-column">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<hr />
@if (fixedDepositProductData.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>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.paymentChannelToFundSourceMappings"
>
<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>
</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>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
@if (fixedDepositProductData.feeToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.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>
</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>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
@if (fixedDepositProductData.penaltyToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<h4 class="mat-h4 flex-100">
{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}
</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.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>
</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>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
@if (!isNoneAccounting()) {
<mifosx-view-savings-accounting-details
[accountingRule]="fixedDepositProductData.accountingRule"
[accountingMappings]="fixedDepositProductData.accountingMappings"
></mifosx-view-savings-accounting-details>
}
</div>
</mat-card-content>
@@ -39,7 +39,7 @@ span {
margin: 0.5em 0;
}
.mat-card .mat-divider-horizontal.mat-divider-inset {
mat-divider {
margin: 0 0 0.5em;
}
@@ -6,12 +6,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { trigger, state, transition, animate, style } from '@angular/animations';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
MatColumnDef,
@@ -24,6 +22,9 @@ import {
MatRowDef,
MatRow
} from '@angular/material/table';
import { Accounting } from 'app/core/utils/accounting';
import { OptionData } from 'app/shared/models/option-data.model';
import { ProductsService } from 'app/products/products.service';
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';
@@ -35,15 +36,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
selector: 'mifosx-fixed-deposit-general-tab',
templateUrl: './fixed-deposit-general-tab.component.html',
styleUrls: ['./fixed-deposit-general-tab.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,
RouterLink,
FaIconComponent,
MatDivider,
MatTable,
@@ -63,52 +58,90 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
YesnoPipe
]
})
export class FixedDepositGeneralTabComponent {
export class FixedDepositGeneralTabComponent implements OnInit {
private route = inject(ActivatedRoute);
private accounting = inject(Accounting);
private productsService = inject(ProductsService);
/** Fixed Deposit Product data. */
fixedDepositProductData: any;
fixedDepositProductsTemplate: any;
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives'];
chartSlabsDisplayedColumns: string[] = [
'period',
'amountRange',
'annualInterestRate',
fdInterestRates: any[] = [];
accountingMappings: any = {};
accountingRule: OptionData;
readonly fdSlabColumns = [
'srNo',
'description',
'actions'
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
incentivesDisplayedColumns: string[] = [
'entityType',
'attributeName',
'conditionType',
'attributeValue',
'incentiveType',
'amount'
];
chargesDisplayedColumns: string[] = [
readonly chargesDisplayedColumns = [
'name',
'type',
'amount',
'collectedon'
];
paymentFundSourceDisplayedColumns: string[] = [
readonly paymentFundSourceDisplayedColumns = [
'paymentTypeId',
'fundSourceAccountId'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
readonly feesPenaltyIncomeDisplayedColumns = [
'chargeId',
'incomeAccountId'
];
/**
* Retrieves the fixed deposit product data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
*/
constructor() {
this.route.data.subscribe((data: { fixedDepositProduct: any; fixedDepositProductsTemplate: any }) => {
this.fixedDepositProductData = data.fixedDepositProduct;
this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate;
});
}
ngOnInit(): void {
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.fdInterestRates = rates || [];
this.buildAccountingMappings();
},
error: () => {
this.fdInterestRates = [];
this.buildAccountingMappings();
}
});
}
get activeBaseVersion(): any {
return this.fdInterestRates.find((v: any) => v.isActive) ?? this.fdInterestRates[0] ?? null;
}
get fdSlabs(): any[] {
return this.activeBaseVersion?.slabs || [];
}
get otherGlMappings(): any {
return this.fixedDepositProductData?.otherGlMappings || this.fixedDepositProductData?.otherGlMappingsMap || null;
}
private buildAccountingMappings(): void {
const data = this.fixedDepositProductData;
if (!data) return;
this.accountingRule = data.accountingRule;
// accountingMappings come directly from the API response with full account objects
this.accountingMappings = data.accountingMappings || {};
}
isNoneAccounting(): boolean {
return this.accounting.isNoneAccounting(this.fixedDepositProductData?.accountingRule);
}
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;
}
}
@@ -8,7 +8,7 @@
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet, Router } from '@angular/router';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -30,6 +30,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
})
export class ViewFixedDepositProductComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
fixedDepositDatatables: any = [];
+52 -42
View File
@@ -685,44 +685,49 @@ const routes: Routes = [
breadcrumb: 'productId',
routeParamBreadcrumb: 'productId'
},
component: ViewRecurringDepositProductComponent,
resolve: {
recurringDepositDatatables: SavingProductDatatablesResolver
},
children: [
{
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
component: RecurringDepositGeneralTabComponent,
resolve: {
recurringDepositProduct: RecurringDepositProductResolver
}
},
{
path: 'datatables',
component: ViewRecurringDepositProductComponent,
children: [
{
path: ':datatableName',
component: RecurringDepositDatatableTabComponent,
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' },
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
component: RecurringDepositGeneralTabComponent,
resolve: {
recurringDepositDatatable: SavingProductDatatableResolver
recurringDepositProduct: RecurringDepositProductResolver
}
},
{
path: 'datatables',
children: [
{
path: ':datatableName',
component: RecurringDepositDatatableTabComponent,
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' },
resolve: {
recurringDepositDatatable: SavingProductDatatableResolver
}
}
]
},
{
path: 'interest-rates',
component: RdInterestRatesTabComponent,
data: { title: 'CBS Base Chart', breadcrumb: 'CBS Base Chart', routeParamBreadcrumb: false },
resolve: {
rdInterestRates: rdInterestRatesResolver
}
}
]
},
{
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 },
@@ -757,33 +762,38 @@ const routes: Routes = [
{
path: ':productId',
data: { title: 'View Fixed Deposit Product', breadcrumb: 'productId', routeParamBreadcrumb: 'productId' },
component: ViewFixedDepositProductComponent,
resolve: {
fixedDepositDatatables: SavingProductDatatablesResolver
},
children: [
{
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
component: FixedDepositGeneralTabComponent,
resolve: {
fixedDepositProduct: FixedDepositProductResolver
}
},
{
path: 'datatables',
component: ViewFixedDepositProductComponent,
children: [
{
path: ':datatableName',
component: FixedDepositDatatableTabComponent,
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' },
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
component: FixedDepositGeneralTabComponent,
resolve: {
fixedDepositDatatable: SavingProductDatatableResolver
fixedDepositProduct: FixedDepositProductResolver
}
},
{
path: 'datatables',
children: [
{
path: ':datatableName',
component: FixedDepositDatatableTabComponent,
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' },
resolve: {
fixedDepositDatatable: SavingProductDatatableResolver
}
}
]
}
]
},
+18
View File
@@ -546,6 +546,24 @@ export class ProductsService {
return this.http.post('/recurringdepositproducts/baseinterestrates', payload);
}
/** Returns the active FD CBS base interest-rate version(s), optionally filtered by fdType. */
getFdBaseInterestRates(fdType?: string): Observable<any> {
const url = fdType
? `/fixeddepositproducts/baseinterestrates?fdType=${encodeURIComponent(fdType)}`
: '/fixeddepositproducts/baseinterestrates';
return this.http.get(url);
}
/** Returns the full FD CBS base chart history. */
getFdBaseInterestRateHistory(): Observable<any> {
return this.http.get('/fixeddepositproducts/baseinterestrates/history');
}
/** Saves a new FD CBS base interest-rate version. */
saveFdBaseInterestRates(payload: any): Observable<any> {
return this.http.post('/fixeddepositproducts/baseinterestrates', payload);
}
/**
* @returns {Observable<any>} Collateral data.
*/
@@ -149,6 +149,7 @@
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[accountingRuleData]="accountingRuleData"
[recurringDepositProductFormValid]="recurringDepositProductFormValid"
[isDormancyTrackingActive]="recurringDepositProductCurrencyForm.get('dormancyAlert')"
></mifosx-recurring-deposit-product-accounting-step>
</div>
</mat-tab>
@@ -123,23 +123,23 @@ export class CreateRecurringDepositProductComponent {
}
get recurringDepositProductDetailsForm() {
return this.recurringDepositProductDetailsStep.recurringDepositProductDetailsForm;
return this.recurringDepositProductDetailsStep?.recurringDepositProductDetailsForm;
}
get recurringDepositProductCurrencyForm() {
return this.recurringDepositProductCurrencyStep.recurringDepositProductCurrencyForm;
return this.recurringDepositProductCurrencyStep?.recurringDepositProductCurrencyForm;
}
get recurringDepositProductTermsForm() {
return this.recurringDepositProductTermsStep.recurringDepositProductTermsForm;
return this.recurringDepositProductTermsStep?.recurringDepositProductTermsForm;
}
get recurringDepositProductSettingsForm() {
return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm;
return this.recurringDepositProductSettingsStep?.recurringDepositProductSettingsForm;
}
get recurringDepositProductAccountingForm() {
return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm;
return this.recurringDepositProductAccountingStep?.recurringDepositProductAccountingForm;
}
get totalTabs(): number {
@@ -147,13 +147,7 @@ export class CreateRecurringDepositProductComponent {
}
get recurringDepositProductFormValid() {
return (
this.recurringDepositProductDetailsForm.valid &&
this.recurringDepositProductCurrencyForm.valid &&
this.recurringDepositProductTermsForm.valid &&
this.recurringDepositProductSettingsForm.valid &&
this.recurringDepositProductAccountingForm.valid
);
return !!this.recurringDepositProductDetailsForm?.valid;
}
get activeBaseVersion() {
@@ -148,12 +148,14 @@
<mifosx-recurring-deposit-product-accounting-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[accountingRuleData]="accountingRuleData"
[recurringDepositProductFormValid]="recurringDepositProductFormValidAndNotPrinstine"
></mifosx-recurring-deposit-product-accounting-step>
[recurringDepositProductFormValid]="recurringDepositProductFormValid"
[isDormancyTrackingActive]="recurringDepositProductCurrencyForm.get('dormancyAlert')"
>
</mifosx-recurring-deposit-product-accounting-step>
</div>
</mat-tab>
@if (recurringDepositProductFormValidAndNotPrinstine) {
@if (recurringDepositProductFormValid) {
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-eye rdp-tab-icon"></i>
@@ -189,7 +191,7 @@
<i class="fas fa-arrow-right" style="margin-left: 8px"></i>
</button>
}
@if (recurringDepositProductFormValidAndNotPrinstine && selectedTabIndex === totalTabs - 1) {
@if (recurringDepositProductFormValid && 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 }}
@@ -120,42 +120,31 @@ export class EditRecurringDepositProductComponent {
}
get recurringDepositProductDetailsForm() {
return this.recurringDepositProductDetailsStep.recurringDepositProductDetailsForm;
return this.recurringDepositProductDetailsStep?.recurringDepositProductDetailsForm;
}
get recurringDepositProductCurrencyForm() {
return this.recurringDepositProductCurrencyStep.recurringDepositProductCurrencyForm;
return this.recurringDepositProductCurrencyStep?.recurringDepositProductCurrencyForm;
}
get recurringDepositProductTermsForm() {
return this.recurringDepositProductTermsStep.recurringDepositProductTermsForm;
return this.recurringDepositProductTermsStep?.recurringDepositProductTermsForm;
}
get recurringDepositProductSettingsForm() {
return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm;
return this.recurringDepositProductSettingsStep?.recurringDepositProductSettingsForm;
}
get recurringDepositProductAccountingForm() {
return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm;
return this.recurringDepositProductAccountingStep?.recurringDepositProductAccountingForm;
}
get totalTabs(): number {
return this.recurringDepositProductFormValidAndNotPrinstine ? 7 : 6;
return this.recurringDepositProductFormValid ? 8 : 7;
}
get recurringDepositProductFormValidAndNotPrinstine() {
return (
this.recurringDepositProductDetailsForm.valid &&
this.recurringDepositProductCurrencyForm.valid &&
this.recurringDepositProductTermsForm.valid &&
this.recurringDepositProductSettingsForm.valid &&
this.recurringDepositProductAccountingForm.valid &&
(!this.recurringDepositProductDetailsForm.pristine ||
!this.recurringDepositProductCurrencyForm.pristine ||
!this.recurringDepositProductTermsForm.pristine ||
!this.recurringDepositProductSettingsForm.pristine ||
!this.recurringDepositProductAccountingForm.pristine)
);
get recurringDepositProductFormValid() {
return !!this.recurringDepositProductDetailsForm?.valid;
}
get activeBaseVersion() {
@@ -173,7 +162,8 @@ export class EditRecurringDepositProductComponent {
...this.recurringDepositProductTermsStep.recurringDepositProductTerms,
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
rdSlabs: this.activeBaseSlabs
};
}
@@ -185,12 +175,11 @@ export class EditRecurringDepositProductComponent {
}
submit() {
// TODO: Update once language and date settings are setup
const recurringDepositProduct = {
const recurringDepositProduct: any = this.pruneEmptyValues({
...this.recurringDepositProduct,
charges: this.recurringDepositProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code // locale required for depositAmount
};
charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
});
if (!recurringDepositProduct.description) {
recurringDepositProduct.description = '';
}
@@ -202,4 +191,35 @@ export class EditRecurringDepositProductComponent {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
private pruneEmptyValues<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T;
}
if (value && typeof value === 'object') {
return Object.entries(value as Record<string, any>).reduce(
(acc, [
key,
currentValue
]) => {
const cleanedValue = this.pruneEmptyValues(currentValue);
if (cleanedValue === undefined) {
return acc;
}
return {
...acc,
[key]: cleanedValue
};
},
{} as Record<string, any>
) as T;
}
if (value === '' || value === null || value === undefined) {
return undefined as T;
}
return value;
}
}
@@ -36,7 +36,9 @@
</div>
<div class="rdpa-list">
@for (row of section.rows; track row.key) {
@if (!row.accrualOnly || isAccrualAccounting()) {
@if (
(!row.accrualOnly || isAccrualAccounting()) && (!row.dormancyOnly || isDormancyTrackingEnabled())
) {
<div class="rdpa-row">
<div class="rdpa-row__meta">
<span
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpa-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
}
@@ -32,6 +32,7 @@ type AccountingRowConfig = {
options: GLAccount[];
required?: boolean;
accrualOnly?: boolean;
dormancyOnly?: boolean;
};
type AccountingSectionConfig = {
@@ -69,6 +70,7 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
@Input() recurringDepositProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() recurringDepositProductFormValid: boolean;
@Input() isDormancyTrackingActive: any;
recurringDepositProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = [];
@@ -101,11 +103,22 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
this.recurringDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId });
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
this.setDormancyControl();
}
createRecurringDepositProductAccountingForm() {
this.recurringDepositProductAccountingForm = this.formBuilder.group({
accountingRule: [2],
savingsReferenceAccountId: [''],
savingsControlAccountId: [''],
transfersInSuspenseAccountId: [''],
interestOnSavingsAccountId: [''],
incomeFromFeeAccountId: [''],
incomeFromPenaltyAccountId: [''],
feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''],
escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
@@ -129,7 +142,8 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
interestPayableAccountId: mappings.interestPayableAccount?.id || ''
interestPayableAccountId: mappings.interestPayableAccount?.id || '',
escheatLiabilityId: mappings.escheatLiabilityAccount?.id || ''
});
if (this.isCashOrAccrualAccounting()) {
@@ -215,53 +229,43 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
}
private setDormancyControl() {
this.isDormancyTrackingActive?.valueChanges?.subscribe(() => {
this.syncAccountingControls(this.recurringDepositProductAccountingForm.value.accountingRule);
});
}
isDormancyTrackingEnabled(): boolean {
return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false;
}
private syncAccountingControls(accountingRule: number) {
const addControl = (name: string, required = false) => {
if (!this.recurringDepositProductAccountingForm.contains(name)) {
this.recurringDepositProductAccountingForm.addControl(
name,
new UntypedFormControl('', required ? Validators.required : undefined)
);
} else if (required) {
this.recurringDepositProductAccountingForm.get(name)?.setValidators(Validators.required);
this.recurringDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false });
}
};
const removeControl = (name: string) => {
if (this.recurringDepositProductAccountingForm.contains(name)) {
this.recurringDepositProductAccountingForm.removeControl(name);
}
};
if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) {
addControl('savingsReferenceAccountId', true);
addControl('savingsControlAccountId', true);
addControl('transfersInSuspenseAccountId', true);
addControl('interestOnSavingsAccountId', true);
addControl('incomeFromFeeAccountId', true);
addControl('incomeFromPenaltyAccountId', true);
if (this.accounting.isAccrualAccountingRuleId(accountingRule)) {
addControl('feesReceivableAccountId', true);
addControl('penaltiesReceivableAccountId', true);
addControl('interestPayableAccountId', true);
const setRequired = (name: string, required: boolean) => {
const ctrl = this.recurringDepositProductAccountingForm.get(name);
if (!ctrl) return;
if (required) {
ctrl.setValidators(Validators.required);
} else {
removeControl('feesReceivableAccountId');
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
ctrl.clearValidators();
}
} else {
[
'savingsReferenceAccountId',
'savingsControlAccountId',
'transfersInSuspenseAccountId',
'interestOnSavingsAccountId',
'incomeFromFeeAccountId',
'incomeFromPenaltyAccountId',
'feesReceivableAccountId',
'penaltiesReceivableAccountId',
'interestPayableAccountId'
].forEach(removeControl);
ctrl.updateValueAndValidity({ emitEvent: false });
};
const cashOrAccrual = this.accounting.isCashOrAccrualAccountingRuleId(accountingRule);
const accrualOnly = this.accounting.isAccrualAccountingRuleId(accountingRule);
setRequired('savingsReferenceAccountId', cashOrAccrual);
setRequired('savingsControlAccountId', cashOrAccrual);
setRequired('transfersInSuspenseAccountId', cashOrAccrual);
setRequired('interestOnSavingsAccountId', cashOrAccrual);
setRequired('incomeFromFeeAccountId', cashOrAccrual);
setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
setRequired('feesReceivableAccountId', accrualOnly);
setRequired('penaltiesReceivableAccountId', accrualOnly);
setRequired('interestPayableAccountId', accrualOnly);
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
if (!cashOrAccrual) {
this.clearOtherGlMappings();
}
}
@@ -331,6 +335,14 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions,
accrualOnly: true
},
{
key: 'escheatLiabilityId',
label: 'Escheat Liability Account',
inputLabel: 'Escheat liability account',
controlName: 'escheatLiabilityId',
options: this.liabilityAccountOptions,
dormancyOnly: true
}
],
otherMappingsControlName: 'liabilitiesOtherGlMappings',
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpc-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpd-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdps-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpt-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
@@ -7,7 +7,7 @@
*/
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet, Router } from '@angular/router';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -27,6 +27,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
})
export class ViewRecurringDepositProductComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
recurringDepositDatatables: any = [];
+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': '260430',
'hash': '2d4fb423'
'version': '260504',
'hash': 'e9d2f4b5'
},
'allow_switching_backend_instance': true
};