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 }} {{ 'labels.inputs.DETAILS' | translate }}
</ng-template> </ng-template>
<div class="fdp-tab-content"> <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> </div>
</mat-tab> </mat-tab>
@@ -48,6 +50,7 @@
<div class="fdp-tab-content"> <div class="fdp-tab-content">
<mifosx-fixed-deposit-product-terms-step <mifosx-fixed-deposit-product-terms-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[fdType]="fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value"
></mifosx-fixed-deposit-product-terms-step> ></mifosx-fixed-deposit-product-terms-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -147,27 +150,40 @@
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[fixedDepositProductFormValid]="fixedDepositProductFormValid" [fixedDepositProductFormValid]="fixedDepositProductFormValid"
[isDormancyTrackingActive]="fixedDepositProductCurrencyForm.get('dormancyAlert')"
></mifosx-fixed-deposit-product-accounting-step> ></mifosx-fixed-deposit-product-accounting-step>
</div> </div>
</mat-tab> </mat-tab>
@if (fixedDepositProductFormValid) { <mat-tab>
<mat-tab> <ng-template mat-tab-label>
<ng-template mat-tab-label> <i class="fas fa-eye fdp-tab-icon"></i>
<i class="fas fa-eye fdp-tab-icon"></i> {{ 'labels.inputs.PREVIEW' | translate }}
{{ 'labels.inputs.PREVIEW' | translate }} </ng-template>
</ng-template> <div class="fdp-tab-content">
<div class="fdp-tab-content"> @if (fixedDepositProductFormValid) {
<mifosx-fixed-deposit-product-preview-step <mifosx-fixed-deposit-product-preview-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="fdSlabColumns" [chartSlabsDisplayedColumns]="[previewChartSlabColumns]"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct" [fixedDepositProduct]="fixedDepositProduct"
(submitEvent)="submit()" (submitEvent)="submit()"
></mifosx-fixed-deposit-product-preview-step> ></mifosx-fixed-deposit-product-preview-step>
</div> } @else {
</mat-tab> <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> </mat-tab-group>
<div class="fdp-action-bar"> <div class="fdp-action-bar">
@@ -33,6 +33,7 @@ import {
/** Custom Services */ /** Custom Services */
import { ProductsService } from 'app/products/products.service'; import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 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 { 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 productsService = inject(ProductsService);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private accounting = inject(Accounting); private accounting = inject(Accounting);
@ViewChild(FixedDepositProductDetailsStepComponent, { static: true }) @ViewChild(FixedDepositProductDetailsStepComponent, { static: true })
@@ -98,13 +100,20 @@ export class CreateFixedDepositProductComponent {
'periodTypeLabel', 'periodTypeLabel',
'annualInterestRate' 'annualInterestRate'
]; ];
readonly previewChartSlabColumns = [
'period',
'amountRange',
'annualInterestRate',
'description',
'actions'
];
constructor() { constructor() {
this.route.data.subscribe((data: { fixedDepositProductsTemplate: any }) => { this.route.data.subscribe((data: { fixedDepositProductsTemplate: any }) => {
this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate; this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate;
}); });
this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getRdBaseInterestRates().subscribe({ this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => { next: (rates: any[]) => {
this.fdInterestRates = rates || []; this.fdInterestRates = rates || [];
}, },
@@ -115,41 +124,41 @@ export class CreateFixedDepositProductComponent {
} }
get fixedDepositProductDetailsForm() { get fixedDepositProductDetailsForm() {
return this.fixedDepositProductDetailsStep.fixedDepositProductDetailsForm; return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm;
} }
get fixedDepositProductCurrencyForm() { get fixedDepositProductCurrencyForm() {
return this.fixedDepositProductCurrencyStep.fixedDepositProductCurrencyForm; return this.fixedDepositProductCurrencyStep?.fixedDepositProductCurrencyForm;
} }
get fixedDepositProductTermsForm() { get fixedDepositProductTermsForm() {
return this.fixedDepositProductTermsStep.fixedDepositProductTermsForm; return this.fixedDepositProductTermsStep?.fixedDepositProductTermsForm;
} }
get fixedDepositProductSettingsForm() { get fixedDepositProductSettingsForm() {
return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm; return this.fixedDepositProductSettingsStep?.fixedDepositProductSettingsForm;
} }
get fixedDepositProductAccountingForm() { get fixedDepositProductAccountingForm() {
return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm; return this.fixedDepositProductAccountingStep?.fixedDepositProductAccountingForm;
} }
get totalTabs(): number { get totalTabs(): number {
return this.fixedDepositProductFormValid ? 8 : 7; return 8;
} }
get fixedDepositProductFormValid() { get fixedDepositProductFormValid() {
return ( return !!this.fixedDepositProductDetailsForm?.valid;
this.fixedDepositProductDetailsForm.valid && }
this.fixedDepositProductCurrencyForm.valid &&
this.fixedDepositProductTermsForm.valid && get selectedFdType(): string | null {
this.fixedDepositProductSettingsForm.valid && return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value ?? null;
this.fixedDepositProductAccountingForm.valid
);
} }
get activeBaseVersion() { 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() { get activeBaseSlabs() {
@@ -164,16 +173,97 @@ export class CreateFixedDepositProductComponent {
...this.fixedDepositProductSettingsStep.fixedDepositProductSettings, ...this.fixedDepositProductSettingsStep.fixedDepositProductSettings,
...this.fixedDepositProductChargesStep.fixedDepositProductCharges, ...this.fixedDepositProductChargesStep.fixedDepositProductCharges,
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting, ...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() { submit() {
const fixedDepositProduct = { const fixedDepositProduct: any = this.pruneEmptyValues({
...this.fixedDepositProduct, ...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 locale: this.settingsService.language.code
}; });
delete fixedDepositProduct.advancedAccountingRules; delete fixedDepositProduct.advancedAccountingRules;
this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => { this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => {
this.router.navigate( this.router.navigate(
@@ -6,104 +6,197 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<div class="container"> <div class="fdp-container">
<mat-stepper <div class="fdp-header">
orientation="vertical" <i class="fas fa-piggy-bank fdp-header__icon"></i>
[linear]="false" <h2 class="fdp-header__title">{{ 'labels.heading.Edit Fixed Deposit Product' | translate }}</h2>
class="mat-elevation-z8" </div>
labelPosition="bottom"
#fixedDepositProductStepper <mat-tab-group
class="fdp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
> >
<ng-template matStepperIcon="number"> <mat-tab>
<fa-icon icon="pencil-alt" size="sm"></fa-icon> <ng-template mat-tab-label>
</ng-template> <i class="fas fa-info-circle fdp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }}
<ng-template matStepperIcon="edit"> </ng-template>
<fa-icon icon="pencil-alt" size="sm"></fa-icon> <div class="fdp-tab-content">
</ng-template> <mifosx-fixed-deposit-product-details-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
<ng-template matStepperIcon="done"> ></mifosx-fixed-deposit-product-details-step>
<fa-icon icon="check" size="sm"></fa-icon> </div>
</ng-template> </mat-tab>
<ng-template matStepperIcon="error"> <mat-tab>
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon> <ng-template mat-tab-label>
</ng-template> <i class="fas fa-coins fdp-tab-icon"></i>
{{ 'labels.inputs.CURRENCY' | translate }}
<ng-template matStepperIcon="preview"> </ng-template>
<fa-icon icon="eye" size="sm"></fa-icon> <div class="fdp-tab-content">
</ng-template> <mifosx-fixed-deposit-product-currency-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
<mat-step [stepControl]="fixedDepositProductDetailsForm"> ></mifosx-fixed-deposit-product-currency-step>
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template> </div>
</mat-tab>
<mifosx-fixed-deposit-product-details-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate">
</mifosx-fixed-deposit-product-details-step> <mat-tab>
</mat-step> <ng-template mat-tab-label>
<i class="fas fa-sliders-h fdp-tab-icon"></i>
<mat-step [stepControl]="fixedDepositProductCurrencyForm"> {{ 'labels.inputs.TERMS' | translate }}
<ng-template matStepLabel>{{ 'labels.inputs.CURRENCY' | translate }}</ng-template> </ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-currency-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"> <mifosx-fixed-deposit-product-terms-step
</mifosx-fixed-deposit-product-currency-step> [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
</mat-step> [fdType]="fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value"
></mifosx-fixed-deposit-product-terms-step>
<mat-step [stepControl]="fixedDepositProductTermsForm"> </div>
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template> </mat-tab>
<mifosx-fixed-deposit-product-terms-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"> <mat-tab>
</mifosx-fixed-deposit-product-terms-step> <ng-template mat-tab-label>
</mat-step> <i class="fas fa-cog fdp-tab-icon"></i>
{{ 'labels.inputs.SETTINGS' | translate }}
<mat-step [stepControl]="fixedDepositProductSettingsForm"> </ng-template>
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template> <div class="fdp-tab-content">
<mifosx-fixed-deposit-product-settings-step
<mifosx-fixed-deposit-product-settings-step [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"> [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
</mifosx-fixed-deposit-product-settings-step> ></mifosx-fixed-deposit-product-settings-step>
</mat-step> </div>
</mat-tab>
<mat-step [stepControl]="fixedDepositProductInterestRateChartForm">
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template> <mat-tab>
<ng-template mat-tab-label>
<mifosx-fixed-deposit-product-interest-rate-chart-step <i class="fas fa-receipt fdp-tab-icon"></i>
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" {{ 'labels.inputs.CHARGES' | translate }}
> </ng-template>
</mifosx-fixed-deposit-product-interest-rate-chart-step> <div class="fdp-tab-content">
</mat-step> <mifosx-fixed-deposit-product-charges-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
<mat-step> [currencyCode]="fixedDepositProductCurrencyForm?.get('currencyCode')"
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template> ></mifosx-fixed-deposit-product-charges-step>
</div>
<mifosx-fixed-deposit-product-charges-step </mat-tab>
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[currencyCode]="fixedDepositProductCurrencyForm.get('currencyCode')" <mat-tab>
> <ng-template mat-tab-label>
</mifosx-fixed-deposit-product-charges-step> <i class="fas fa-chart-area fdp-tab-icon"></i>
</mat-step> CBS Base Chart
</ng-template>
<mat-step [stepControl]="fixedDepositProductAccountingForm"> <div class="fdp-tab-content">
<ng-template matStepLabel>{{ 'labels.inputs.ACCOUNTING' | translate }}</ng-template> @if (activeBaseVersion) {
<div class="base-rate-card">
<mifosx-fixed-deposit-product-accounting-step <div class="base-rate-header">
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" <div>
[accountingRuleData]="accountingRuleData" <h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
[fixedDepositProductFormValid]="fixedDepositProductFormValidAndNotPrinstine" <p class="base-rate-meta">
> Active CBS base chart
</mifosx-fixed-deposit-product-accounting-step> <span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</mat-step> </p>
</div>
@if (fixedDepositProductFormValidAndNotPrinstine) { <span class="base-rate-badge">Active</span>
<mat-step state="preview" completed> </div>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-fixed-deposit-product-preview-step @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" [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="fixedDepositProductInterestRateChartStep.chartSlabsDisplayedColumns"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct" [fixedDepositProductFormValid]="fixedDepositProductFormValid"
(submitEvent)="submit()" [isDormancyTrackingActive]="fixedDepositProductCurrencyForm?.get('dormancyAlert')"
> ></mifosx-fixed-deposit-product-accounting-step>
</mifosx-fixed-deposit-product-preview-step> </div>
</mat-step> </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> </div>
@@ -5,3 +5,134 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * 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 { 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 { 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 { 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 { 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'; import { FixedDepositProductAccountingStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component';
/** Custom Services */ /** Custom Services */
import { ProductsService } from 'app/products/products.service'; import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { 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 { Accounting } from 'app/core/utils/accounting';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; 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 { 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'; 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'], styleUrls: ['./edit-fixed-deposit-product.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatStepper, MatTabsModule,
MatStepperIcon,
FaIconComponent, FaIconComponent,
MatStep, MatTable,
MatStepLabel, MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FixedDepositProductDetailsStepComponent, FixedDepositProductDetailsStepComponent,
FixedDepositProductCurrencyStepComponent, FixedDepositProductCurrencyStepComponent,
FixedDepositProductTermsStepComponent, FixedDepositProductTermsStepComponent,
FixedDepositProductSettingsStepComponent, FixedDepositProductSettingsStepComponent,
FixedDepositProductInterestRateChartStepComponent,
FixedDepositProductChargesStepComponent, FixedDepositProductChargesStepComponent,
FixedDepositProductAccountingStepComponent, FixedDepositProductAccountingStepComponent,
FixedDepositProductPreviewStepComponent FixedDepositProductPreviewStepComponent
@@ -55,6 +73,7 @@ export class EditFixedDepositProductComponent {
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private accounting = inject(Accounting); private accounting = inject(Accounting);
private dateUtils = inject(Dates);
@ViewChild(FixedDepositProductDetailsStepComponent, { static: true }) @ViewChild(FixedDepositProductDetailsStepComponent, { static: true })
fixedDepositProductDetailsStep: FixedDepositProductDetailsStepComponent; fixedDepositProductDetailsStep: FixedDepositProductDetailsStepComponent;
@@ -64,8 +83,6 @@ export class EditFixedDepositProductComponent {
fixedDepositProductTermsStep: FixedDepositProductTermsStepComponent; fixedDepositProductTermsStep: FixedDepositProductTermsStepComponent;
@ViewChild(FixedDepositProductSettingsStepComponent, { static: true }) @ViewChild(FixedDepositProductSettingsStepComponent, { static: true })
fixedDepositProductSettingsStep: FixedDepositProductSettingsStepComponent; fixedDepositProductSettingsStep: FixedDepositProductSettingsStepComponent;
@ViewChild(FixedDepositProductInterestRateChartStepComponent, { static: true })
fixedDepositProductInterestRateChartStep: FixedDepositProductInterestRateChartStepComponent;
@ViewChild(FixedDepositProductChargesStepComponent, { static: true }) @ViewChild(FixedDepositProductChargesStepComponent, { static: true })
fixedDepositProductChargesStep: FixedDepositProductChargesStepComponent; fixedDepositProductChargesStep: FixedDepositProductChargesStepComponent;
@ViewChild(FixedDepositProductAccountingStepComponent, { static: true }) @ViewChild(FixedDepositProductAccountingStepComponent, { static: true })
@@ -73,6 +90,23 @@ export class EditFixedDepositProductComponent {
fixedDepositProductsTemplate: any; fixedDepositProductsTemplate: any;
accountingRuleData: string[] = []; 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. * @param {ActivatedRoute} route Activated Route.
@@ -86,47 +120,42 @@ export class EditFixedDepositProductComponent {
this.fixedDepositProductsTemplate = data.fixedDepositProductAndTemplate; this.fixedDepositProductsTemplate = data.fixedDepositProductAndTemplate;
}); });
this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getFdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.fdInterestRates = rates || [];
},
error: () => {
this.fdInterestRates = [];
}
});
} }
get fixedDepositProductDetailsForm() { get fixedDepositProductDetailsForm() {
return this.fixedDepositProductDetailsStep.fixedDepositProductDetailsForm; return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm;
} }
get fixedDepositProductCurrencyForm() { get fixedDepositProductCurrencyForm() {
return this.fixedDepositProductCurrencyStep.fixedDepositProductCurrencyForm; return this.fixedDepositProductCurrencyStep?.fixedDepositProductCurrencyForm;
} }
get fixedDepositProductTermsForm() { get fixedDepositProductTermsForm() {
return this.fixedDepositProductTermsStep.fixedDepositProductTermsForm; return this.fixedDepositProductTermsStep?.fixedDepositProductTermsForm;
} }
get fixedDepositProductSettingsForm() { get fixedDepositProductSettingsForm() {
return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm; return this.fixedDepositProductSettingsStep?.fixedDepositProductSettingsForm;
}
get fixedDepositProductInterestRateChartForm() {
return this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChartForm;
} }
get fixedDepositProductAccountingForm() { get fixedDepositProductAccountingForm() {
return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm; return this.fixedDepositProductAccountingStep?.fixedDepositProductAccountingForm;
} }
get fixedDepositProductFormValidAndNotPrinstine() { get totalTabs(): number {
return ( return this.fixedDepositProductFormValid ? 8 : 7;
this.fixedDepositProductDetailsForm.valid && }
this.fixedDepositProductCurrencyForm.valid &&
this.fixedDepositProductTermsForm.valid && get fixedDepositProductFormValid() {
this.fixedDepositProductSettingsForm.valid && return !!this.fixedDepositProductDetailsForm?.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 fixedDepositProduct() { get fixedDepositProduct() {
@@ -135,33 +164,118 @@ export class EditFixedDepositProductComponent {
...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency, ...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency,
...this.fixedDepositProductTermsStep.fixedDepositProductTerms, ...this.fixedDepositProductTermsStep.fixedDepositProductTerms,
...this.fixedDepositProductSettingsStep.fixedDepositProductSettings, ...this.fixedDepositProductSettingsStep.fixedDepositProductSettings,
...this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChart,
...this.fixedDepositProductChargesStep.fixedDepositProductCharges, ...this.fixedDepositProductChargesStep.fixedDepositProductCharges,
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting ...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting,
charts: this.fixedDepositProductCharts,
fdSlabs: this.activeBaseVersion?.slabs || []
}; };
} }
submit() { get selectedFdType(): string | null {
const fixedDepositProduct = { return this.fixedDepositProductDetailsStep?.fixedDepositProductDetailsForm?.get('fdType')?.value ?? null;
...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 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; delete fixedDepositProduct.advancedAccountingRules;
this.productsService this.productsService
.updateFixedDepositProduct(this.fixedDepositProductsTemplate.id, fixedDepositProduct) .updateFixedDepositProduct(this.fixedDepositProductsTemplate.id, fixedDepositProduct)
@@ -169,4 +283,35 @@ export class EditFixedDepositProductComponent {
this.router.navigate(['../'], { relativeTo: this.route }); 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>
<div class="fdpa-list"> <div class="fdpa-list">
@for (row of section.rows; track row.key) { @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">
<div class="fdpa-row__meta"> <div class="fdpa-row__meta">
<span <span
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpa-section { .fdpa-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@@ -32,6 +32,7 @@ type AccountingRowConfig = {
options: GLAccount[]; options: GLAccount[];
required?: boolean; required?: boolean;
accrualOnly?: boolean; accrualOnly?: boolean;
dormancyOnly?: boolean;
}; };
type AccountingSectionConfig = { type AccountingSectionConfig = {
@@ -62,13 +63,14 @@ type OtherGlMappingValue = {
GlAccountSelectorComponent GlAccountSelectorComponent
] ]
}) })
export class FixedDepositProductAccountingStepComponent implements OnInit { export class FixedDepositProductAccountingStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private accounting = inject(Accounting); private accounting = inject(Accounting);
@Input() fixedDepositProductsTemplate: any; @Input() fixedDepositProductsTemplate: any;
@Input() accountingRuleData: any; @Input() accountingRuleData: any;
@Input() fixedDepositProductFormValid: boolean; @Input() fixedDepositProductFormValid: boolean;
@Input() isDormancyTrackingActive: any;
fixedDepositProductAccountingForm: UntypedFormGroup; fixedDepositProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = []; assetAccountOptions: GLAccount[] = [];
@@ -83,6 +85,16 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.initializeAccountingForm();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fixedDepositProductsTemplate?.currentValue) {
this.initializeAccountingForm();
}
}
private initializeAccountingForm() {
const accountingMappings = this.fixedDepositProductsTemplate?.accountingMappings || {}; const accountingMappings = this.fixedDepositProductsTemplate?.accountingMappings || {};
const mappings = { const mappings = {
...accountingMappings, ...accountingMappings,
@@ -98,14 +110,25 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
this.buildAccountingSectionConfigs(); this.buildAccountingSectionConfigs();
this.fixedDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }); this.fixedDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }, { emitEvent: false });
this.syncAccountingControls(accountingRuleId); this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings); this.patchAccountingValues(mappings);
this.setDormancyControl();
} }
createFixedDepositProductAccountingForm() { createFixedDepositProductAccountingForm() {
this.fixedDepositProductAccountingForm = this.formBuilder.group({ this.fixedDepositProductAccountingForm = this.formBuilder.group({
accountingRule: [2], accountingRule: [2],
savingsReferenceAccountId: [''],
savingsControlAccountId: [''],
transfersInSuspenseAccountId: [''],
interestOnSavingsAccountId: [''],
incomeFromFeeAccountId: [''],
incomeFromPenaltyAccountId: [''],
feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''],
escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]), assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]), liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: 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) { private patchAccountingValues(mappings: any) {
this.fixedDepositProductAccountingForm.patchValue({ this.fixedDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', savingsReferenceAccountId: this.extractAccountId(
savingsControlAccountId: mappings.savingsControlAccount?.id || '', mappings,
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', 'savingsReferenceAccount',
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', 'savingsReferenceAccountId'
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', ),
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', savingsControlAccountId: this.extractAccountId(mappings, 'savingsControlAccount', 'savingsControlAccountId'),
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', transfersInSuspenseAccountId: this.extractAccountId(
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', mappings,
interestPayableAccountId: mappings.interestPayableAccount?.id || '' '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()) { if (this.isCashOrAccrualAccounting()) {
@@ -215,53 +275,43 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; 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) { private syncAccountingControls(accountingRule: number) {
const addControl = (name: string, required = false) => { const setRequired = (name: string, required: boolean) => {
if (!this.fixedDepositProductAccountingForm.contains(name)) { const ctrl = this.fixedDepositProductAccountingForm.get(name);
this.fixedDepositProductAccountingForm.addControl( if (!ctrl) return;
name, if (required) {
new UntypedFormControl('', required ? Validators.required : undefined) ctrl.setValidators(Validators.required);
);
} 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);
} else { } else {
removeControl('feesReceivableAccountId'); ctrl.clearValidators();
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
} }
} else { ctrl.updateValueAndValidity({ emitEvent: false });
[ };
'savingsReferenceAccountId',
'savingsControlAccountId', const cashOrAccrual = this.accounting.isCashOrAccrualAccountingRuleId(accountingRule);
'transfersInSuspenseAccountId', const accrualOnly = this.accounting.isAccrualAccountingRuleId(accountingRule);
'interestOnSavingsAccountId',
'incomeFromFeeAccountId', setRequired('savingsReferenceAccountId', cashOrAccrual);
'incomeFromPenaltyAccountId', setRequired('savingsControlAccountId', cashOrAccrual);
'feesReceivableAccountId', setRequired('transfersInSuspenseAccountId', cashOrAccrual);
'penaltiesReceivableAccountId', setRequired('interestOnSavingsAccountId', cashOrAccrual);
'interestPayableAccountId' setRequired('incomeFromFeeAccountId', cashOrAccrual);
].forEach(removeControl); setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
setRequired('feesReceivableAccountId', accrualOnly);
setRequired('penaltiesReceivableAccountId', accrualOnly);
setRequired('interestPayableAccountId', accrualOnly);
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
if (!cashOrAccrual) {
this.clearOtherGlMappings(); this.clearOtherGlMappings();
} }
} }
@@ -331,6 +381,14 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
controlName: 'interestPayableAccountId', controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions, options: this.liabilityAccountOptions,
accrualOnly: true accrualOnly: true
},
{
key: 'escheatLiabilityId',
label: 'Escheat Liability Account',
inputLabel: 'Escheat liability account',
controlName: 'escheatLiabilityId',
options: this.liabilityAccountOptions,
dormancyOnly: true
} }
], ],
otherMappingsControlName: 'liabilitiesOtherGlMappings', otherMappingsControlName: 'liabilitiesOtherGlMappings',
@@ -392,7 +450,23 @@ export class FixedDepositProductAccountingStepComponent implements OnInit {
...accounting ...accounting
} = this.fixedDepositProductAccountingForm.value; } = this.fixedDepositProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings(); 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 { isCashOrAccrualAccounting(): boolean {
@@ -63,27 +63,30 @@
</mat-radio-group> </mat-radio-group>
</div> </div>
<div class="fdpc-alert"> @if (fixedDepositProductCurrencyForm.get('dormancyAlert')?.value === true) {
<div class="fdpc-alert__content"> <div
<h4>Balance Swap Option</h4> class="fdpc-grid"
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p> style="border-top: 1px solid #e2e8f0; background: #f8fafc; padding-top: 24px; padding-bottom: 24px"
</div> >
<mat-radio-group class="fdpc-alert__choices" formControlName="balanceSwapOption"> <mat-form-field appearance="outline" class="fdpc-field">
<mat-radio-button [value]="true">yes</mat-radio-button> <mat-label>{{ 'labels.inputs.Days to Inactive' | translate }}</mat-label>
<mat-radio-button [value]="false">no</mat-radio-button> <input matInput type="number" formControlName="daysToInactive" required />
</mat-radio-group> <mat-error> {{ 'labels.inputs.Days to Inactive' | translate }} is <strong>required</strong> </mat-error>
</div> </mat-form-field>
<div class="fdpc-alert"> <mat-form-field appearance="outline" class="fdpc-field">
<div class="fdpc-alert__content"> <mat-label>{{ 'labels.inputs.Days to Dormancy' | translate }}</mat-label>
<h4>MSME</h4> <input matInput type="number" formControlName="daysToDormancy" required />
<p>Micro, Small and Medium Enterprises</p> <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> </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">
<div class="fdpc-alert__content"> <div class="fdpc-alert__content">
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpc-section { .fdpc-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -51,8 +51,10 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
this.fixedDepositProductsTemplate.currency?.decimalPlaces ?? this.fixedDepositProductsTemplate.currency?.decimalPlaces ??
2, 2,
dormancyAlert: this.fixedDepositProductsTemplate.dormancyAlert ?? false, dormancyAlert: this.fixedDepositProductsTemplate.dormancyAlert ?? false,
balanceSwapOption: this.fixedDepositProductsTemplate.balanceSwapOption ?? false, isDormancyTrackingActive: this.fixedDepositProductsTemplate.isDormancyTrackingActive ?? false,
msme: this.fixedDepositProductsTemplate.msme ?? false, daysToInactive: this.fixedDepositProductsTemplate.daysToInactive ?? 30,
daysToDormancy: this.fixedDepositProductsTemplate.daysToDormancy ?? 60,
daysToEscheat: this.fixedDepositProductsTemplate.daysToEscheat ?? 90,
nominationFacility: this.fixedDepositProductsTemplate.nominationFacility ?? false, nominationFacility: this.fixedDepositProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.fixedDepositProductsTemplate.autoDebitFacility ?? false autoDebitFacility: this.fixedDepositProductsTemplate.autoDebitFacility ?? false
}); });
@@ -72,8 +74,10 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
] ]
], ],
dormancyAlert: [false], dormancyAlert: [false],
balanceSwapOption: [false], isDormancyTrackingActive: [false],
msme: [false], daysToInactive: [30],
daysToDormancy: [60],
daysToEscheat: [90],
nominationFacility: [false], nominationFacility: [false],
autoDebitFacility: [false] autoDebitFacility: [false]
}); });
@@ -81,14 +85,21 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
get fixedDepositProductCurrency() { get fixedDepositProductCurrency() {
const formValue = this.fixedDepositProductCurrencyForm.value; const formValue = this.fixedDepositProductCurrencyForm.value;
return { const result: any = {
currencyCode: formValue.currencyCode, currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal, digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert, dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility, nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility autoDebitFacility: formValue.autoDebitFacility
}; };
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>
<mat-form-field appearance="outline" class="fdpd-field"> <mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Customer Segment</mat-label> <mat-label>FD Type</mat-label>
<mat-select formControlName="customerSegment"> <mat-select formControlName="fdType" required>
@for (customerSegment of customerSegmentData; track customerSegment) { <mat-option value="SINGLE">FD Single (Days &lt; 1 Year)</mat-option>
<mat-option [value]="customerSegment">{{ customerSegment }}</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-select>
<mat-error>FD Type <strong>is required</strong></mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field"> <mat-form-field appearance="outline" class="fdpd-field">
@@ -110,15 +111,6 @@
<mat-datepicker #endDatePicker></mat-datepicker> <mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field> </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"> <div class="fdpd-field fdpd-field--full fdpd-checkbox-row">
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox> <mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
</div> </div>
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpd-section { .fdpd-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -34,12 +34,12 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
fixedDepositProductDetailsForm: UntypedFormGroup; fixedDepositProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = []; accountTypeData: string[] = [];
customerSegmentData: string[] = []; fdTypeOptions: string[] = [
liabilityAccountOptions: any[] = []; 'SINGLE',
yesNoOptions: Array<{ value: boolean; label: string }> = [ 'DOUBLE',
{ value: true, label: 'Yes' }, 'SPECIAL'
{ value: false, label: 'No' }
]; ];
liabilityAccountOptions: any[] = [];
constructor() { constructor() {
this.createFixedDepositProductDetailsForm(); this.createFixedDepositProductDetailsForm();
@@ -72,11 +72,13 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
null, null,
Validators.required Validators.required
], ],
customerSegment: [null], fdType: [
null,
Validators.required
],
glAccountId: [null], glAccountId: [null],
effectiveTo: [''], effectiveTo: [''],
endDate: [''], endDate: [''],
introducerRequired: [false],
shareLinkingEnabled: [false] shareLinkingEnabled: [false]
}); });
} }
@@ -99,7 +101,6 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
private applyTemplate(template: any) { private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions); this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions);
this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? [];
this.cdr.detectChanges(); this.cdr.detectChanges();
@@ -109,11 +110,10 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
shortName: isEditing ? template.shortName : template.shortName || '', shortName: isEditing ? template.shortName : template.shortName || '',
description: template.description || '', description: template.description || '',
accountType: template.accountType ?? this.accountTypeData[0] ?? null, accountType: template.accountType ?? this.accountTypeData[0] ?? null,
customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, fdType: template.fdType ?? null,
glAccountId: template.glAccountId ?? null, glAccountId: template.glAccountId ?? null,
effectiveTo: this.parseDate(template.effectiveTo), effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate), endDate: this.parseDate(template.endDate),
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
shareLinkingEnabled: !!template.shareLinkingEnabled shareLinkingEnabled: !!template.shareLinkingEnabled
}); });
} }
@@ -6,103 +6,39 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
.fdpp-container { @use 'assets/styles/colours' as *;
display: flex;
flex-direction: column; .mat-h3 {
gap: 16px; 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; 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; font-weight: 500;
color: #5f6368;
margin-bottom: 2px;
} }
.fdpp-value { table {
font-size: 14px; width: 100%;
font-weight: 500;
color: #202124;
} }
.fdpp-chart-block { span {
padding-bottom: 16px; margin: 0.5em 0;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 16px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
} }
mat-divider {
margin: 0 0 0.5em;
}
/* ── Incentives expand row ── */
.fdpp-table-wrapper { .fdpp-table-wrapper {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin-top: 8px;
table { table {
width: 100%; width: 100%;
} }
@@ -132,20 +68,95 @@
} }
} }
.fdpp-subsection-title { /* ── GL mapping summary (same as RD product preview) ── */
margin: 16px 0 8px; .spa-gl-summary,
font-size: 14px; .spa-other-gl-summary {
font-weight: 600; display: flex;
color: #202124; flex-direction: column;
gap: 0.9rem;
margin-top: 1.25rem;
}
&:first-child { .spa-gl-summary__header {
margin-top: 0; 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 { .spa-gl-row {
display: flex; display: grid;
align-items: center; grid-template-columns: minmax(220px, 40%) 1fr;
gap: 12px; gap: 1rem;
margin-top: 16px; 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 { trigger, state, transition, animate, style } from '@angular/animations';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { OptionData } from 'app/shared/models/option-data.model'; import { OptionData } from 'app/shared/models/option-data.model';
import { MatDivider } from '@angular/material/divider';
import { import {
MatTable, MatTable,
@@ -44,6 +45,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
], ],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatDivider,
MatTable, MatTable,
MatColumnDef, MatColumnDef,
MatHeaderCellDef, MatHeaderCellDef,
@@ -100,6 +102,23 @@ export class FixedDepositProductPreviewStepComponent implements OnInit, OnChange
accountingMappings: any = {}; accountingMappings: any = {};
accountingRule: OptionData; 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() { ngOnInit() {
this.setCurrentValues(); this.setCurrentValues();
} }
@@ -142,7 +161,9 @@ export class FixedDepositProductPreviewStepComponent implements OnInit, OnChange
incomeFromPenaltyAccount: this.accounting.glAccountLookUp( incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
this.fixedDepositProduct.incomeFromPenaltyAccountId, this.fixedDepositProduct.incomeFromPenaltyAccountId,
incomeAccountData incomeAccountData
) ),
escheatLiabilityAccount:
this.accounting.glAccountLookUp(this.fixedDepositProduct.escheatLiabilityId, liabilityAccountData) || null
}; };
if (this.isAccrualAccounting()) { if (this.isAccrualAccounting()) {
@@ -173,4 +194,30 @@ export class FixedDepositProductPreviewStepComponent implements OnInit, OnChange
isAccrualAccounting(): boolean { isAccrualAccounting(): boolean {
return this.accounting.isAccrualAccountingRuleId(this.fixedDepositProduct.accountingRule); 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 { .fdps-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -7,6 +7,33 @@
--> -->
<form [formGroup]="fixedDepositProductTermsForm"> <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 --> <!-- Interest Rate & Posting Terms -->
<div class="fdpt-section"> <div class="fdpt-section">
<div class="fdpt-section__header"> <div class="fdpt-section__header">
@@ -65,6 +92,28 @@
</mat-form-field> </mat-form-field>
</div> </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"> <div class="fdpt-grid fdpt-grid--three">
<mat-form-field appearance="outline" class="fdpt-field"> <mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Interest Calculate</mat-label> <mat-label>Interest Calculate</mat-label>
@@ -83,7 +132,7 @@
<mat-form-field appearance="outline" class="fdpt-field"> <mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Average Period</mat-label> <mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod"> <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-option [value]="period.value">{{ period.value }}</mat-option>
} }
</mat-select> </mat-select>
@@ -15,7 +15,6 @@ $gray-900: #202124;
.fdpt-section { .fdpt-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
@@ -22,12 +22,13 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip MatTooltip
] ]
}) })
export class FixedDepositProductTermsStepComponent implements OnInit { export class FixedDepositProductTermsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
@Input() fixedDepositProductsTemplate: any; @Input() fixedDepositProductsTemplate: any;
@Input() fdType: string | null = null;
fixedDepositProductTermsForm: UntypedFormGroup; fixedDepositProductTermsForm: UntypedFormGroup;
@@ -36,6 +37,7 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
interestCalculationTypeData: any; interestCalculationTypeData: any;
interestCalculationTypeOptionData: any; interestCalculationTypeOptionData: any;
interestCalculationDaysInYearTypeData: any; interestCalculationDaysInYearTypeData: any;
averagePeriodData: any[] = [];
periodFrequencyData: any[] = []; periodFrequencyData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [ yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' }, { value: true, label: 'Yes' },
@@ -47,16 +49,33 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
} }
ngOnInit() { 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.interestCompoundingPeriodTypeData = this.fixedDepositProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions;
this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions; this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationTypeOptionData = this.fixedDepositProductsTemplate.interestCalculationTypeOption || []; this.interestCalculationTypeOptionData = this.fixedDepositProductsTemplate.interestCalculationTypeOption || [];
this.interestCalculationDaysInYearTypeData = this.interestCalculationDaysInYearTypeData =
this.fixedDepositProductsTemplate.interestCalculationDaysInYearTypeOptions; this.fixedDepositProductsTemplate.interestCalculationDaysInYearTypeOptions;
this.periodFrequencyData = this.fixedDepositProductsTemplate.periodFrequencyOptions || []; this.averagePeriodData = this.fixedDepositProductsTemplate.periodFrequencyOptions || [];
this.periodFrequencyData = this.fixedDepositProductsTemplate.periodFrequencyTypeOptions || [];
const isEditing = !!this.fixedDepositProductsTemplate.id; 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 = const savedEnumTypeId =
this.fixedDepositProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1; this.fixedDepositProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1;
const savedSelectorId = isEditing const savedSelectorId = isEditing
@@ -65,15 +84,15 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
null) null)
: (this.interestCalculationTypeOptionData?.[0]?.id ?? 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({ 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, nominalAnnualInterestRate: this.fixedDepositProductsTemplate.nominalAnnualInterestRate ?? 0,
interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType?.id, interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType?.id, interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType?.id,
@@ -83,13 +102,34 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
? this.parseDate(this.fixedDepositProductsTemplate.effectiveDate) ? this.parseDate(this.fixedDepositProductsTemplate.effectiveDate)
: '', : '',
interestCalculateSelector: savedSelectorId, interestCalculateSelector: savedSelectorId,
averagePeriod: isEditing ? this.fixedDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, averagePeriod: isEditing
? this.fixedDepositProductsTemplate.averagePeriod || defaultAveragePeriod
: defaultAveragePeriod,
autoPost: isEditing ? (this.fixedDepositProductsTemplate.autoPost ?? true) : true, autoPost: isEditing ? (this.fixedDepositProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? (this.fixedDepositProductsTemplate.frequencyFixDay ?? 30) : 30, frequencyFixDay: isEditing ? (this.fixedDepositProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.fixedDepositProductsTemplate.lastInterestDate lastInterestDate: this.fixedDepositProductsTemplate.lastInterestDate
? this.parseDate(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 { private parseDate(dateValue: any): Date | null {
@@ -101,18 +141,21 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
createFixedDepositProductTermsForm() { createFixedDepositProductTermsForm() {
this.fixedDepositProductTermsForm = this.formBuilder.group({ this.fixedDepositProductTermsForm = this.formBuilder.group({
minDepositAmount: [ minDepositTerm: [
'', 1,
Validators.min(0)
],
depositAmount: [
'',
[ [
Validators.required,
Validators.min(0) Validators.min(0)
] ]
], ],
maxDepositAmount: [ minDepositTermTypeId: [''],
maxDepositTerm: [
'',
Validators.min(0)
],
maxDepositTermTypeId: [''],
inMultiplesOfDepositTerm: [''],
inMultiplesOfDepositTermTypeId: [''],
minDepositAmount: [
'', '',
Validators.min(0) Validators.min(0)
], ],
@@ -139,6 +182,10 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
'', '',
Validators.required Validators.required
], ],
maxDepositAmount: [
'',
Validators.min(0)
],
effectiveDate: [''], effectiveDate: [''],
interestCalculateSelector: [1], interestCalculateSelector: [1],
averagePeriod: ['Monthly'], averagePeriod: ['Monthly'],
@@ -152,7 +199,7 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
} }
get fixedDepositProductTerms() { get fixedDepositProductTerms() {
const formValue = { ...this.fixedDepositProductTermsForm.value }; const formValue = { ...this.fixedDepositProductTermsForm.getRawValue() };
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveDate instanceof Date) { if (formValue.effectiveDate instanceof Date) {
@@ -6,23 +6,29 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. 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"> <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'"> <button mat-raised-button color="primary" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_SAVINGSPRODUCT'">
<fa-icon icon="edit" class="m-r-10"></fa-icon> <fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Edit' | translate }} {{ 'labels.buttons.Edit' | translate }}
</button> </button>
</div> </div>
<div class="container"> <div class="container">
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<div class="responsive-column"> <div class="responsive-column">
<h2 class="mat-h2 flex-100">{{ fixedDepositProductData.name }}</h2> <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> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
@@ -36,9 +42,59 @@
</div> </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"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.heading.Currency' | translate }}:</span> <span class="flex-40">{{ 'labels.heading.Currency' | translate }}:</span>
@@ -57,83 +113,229 @@
<span class="flex-60">{{ fixedDepositProductData.currency.inMultiplesOf | formatNumber }}</span> <span class="flex-60">{{ fixedDepositProductData.currency.inMultiplesOf | formatNumber }}</span>
</div> </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"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Deposit Amount' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest Rate' | translate }} (%):</span>
<span class="flex-60">{{ <span class="flex-60">{{ fixedDepositProductData.nominalAnnualInterestRate | formatNumber }} %</span>
fixedDepositProductData.depositAmount +
' (Min: ' +
(fixedDepositProductData.minDepositAmount
? fixedDepositProductData.minDepositAmount
: fixedDepositProductData.depositAmount) +
', Max: ' +
(fixedDepositProductData.maxDepositAmount
? fixedDepositProductData.maxDepositAmount
: fixedDepositProductData.depositAmount) +
')'
}}</span>
</div> </div>
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span> <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>
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span> <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>
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated Using' | translate }}:</span> <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>
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span> <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> </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> <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"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60"> <span class="flex-60">
{{ fixedDepositProductData.lockinPeriodFrequency }} {{ fixedDepositProductData.lockinPeriodFrequency }}
{{ fixedDepositProductData.lockinPeriodFrequencyType.value }}</span {{ fixedDepositProductData.lockinPeriodFrequencyType?.value }}
> </span>
</div> </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"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span>
@if (fixedDepositProductData.minDepositTerm !== undefined) { <span class="flex-60"
<span class="flex-60" >{{ fixedDepositProductData.minDepositTerm }}
>{{ fixedDepositProductData.minDepositTerm }} {{ fixedDepositProductData.minDepositTermType?.value }}</span
&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
>
}
</div> </div>
} }
@@ -142,34 +344,35 @@
<span class="flex-40">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}:</span>
<span class="flex-60" <span class="flex-60"
>{{ fixedDepositProductData.inMultiplesOfDepositTerm }} >{{ fixedDepositProductData.inMultiplesOfDepositTerm }}
- {{ fixedDepositProductData.inMultiplesOfDepositTermType?.value }}</span
{{ fixedDepositProductData.inMultiplesOfDepositTermType.value }}</span
> >
</div> </div>
} }
<div class="flex-100 layout-row"> @if (fixedDepositProductData.maxDepositTerm) {
<span class="flex-40">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}:</span> <div class="flex-100 layout-row">
<span class="flex-60">{{ fixedDepositProductData.preClosurePenalApplicable | yesNo }}</span> <span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}:</span>
</div> <span class="flex-60"
>{{ fixedDepositProductData.maxDepositTerm }}
{{ fixedDepositProductData.maxDepositTermType?.value }}</span
>
</div>
}
@if (fixedDepositProductData.preClosurePenalApplicable) { @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"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Penal Interest' | translate }} (%):</span> <span class="flex-40">{{ 'labels.inputs.Penal Interest' | translate }} (%):</span>
<span class="flex-60" <span class="flex-60"
>{{ fixedDepositProductData.preClosurePenalInterest }} &nbsp; &nbsp; % &nbsp; &nbsp; On &nbsp;&nbsp; >{{ fixedDepositProductData.preClosurePenalInterest }} % On
{{ fixedDepositProductData.preClosurePenalInterestOnType.value }}</span {{ fixedDepositProductData.preClosurePenalInterestOnType?.value }}</span
> >
</div> </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"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.withHoldTax | yesNo }}</span> <span class="flex-60">{{ fixedDepositProductData.withHoldTax | yesNo }}</span>
@@ -178,249 +381,72 @@
@if (fixedDepositProductData.withHoldTax) { @if (fixedDepositProductData.withHoldTax) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax group' | translate }}:</span> <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> </div>
} }
@if (fixedDepositProductData.activeChart) { <!-- CBS Base Interest Rate Charts -->
<div class="responsive-column flex-100"> @if (fdSlabs.length) {
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Chart' | translate }}</h3> <mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider> <h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<div class="responsive-column flex-100"> <mat-divider class="flex-100"></mat-divider>
@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>
}
@if (fixedDepositProductData.charges.length) {
<div class="layout-row-wrap responsive-column flex-100"> <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> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fixedDepositProductData.charges"> <table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fixedDepositProductData.charges">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge"> <td mat-cell *matCellDef="let charge">{{ charge.name }}</td>
{{ charge.name }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge"> <td mat-cell *matCellDef="let charge">{{ charge.chargeCalculationType?.value }}</td>
{{ charge.chargeCalculationType.value }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="amount"> <ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge"> <td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
{{ charge.amount | formatNumber }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="collectedon"> <ng-container matColumnDef="collectedon">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge"> <td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType?.value }}</td>
{{ charge.chargeTimeType.value }}
</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
@@ -428,107 +454,16 @@
</div> </div>
} }
<!-- Accounting -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider> @if (!isNoneAccounting()) {
<mifosx-view-savings-accounting-details
<mifosx-view-savings-accounting-details [accountingRule]="fixedDepositProductData.accountingRule"
class="container" [accountingMappings]="fixedDepositProductData.accountingMappings"
[accountingRule]="fixedDepositProductData.accountingRule" ></mifosx-view-savings-accounting-details>
[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>
} }
</div> </div>
</mat-card-content> </mat-card-content>
@@ -39,7 +39,7 @@ span {
margin: 0.5em 0; margin: 0.5em 0;
} }
.mat-card .mat-divider-horizontal.mat-divider-inset { mat-divider {
margin: 0 0 0.5em; margin: 0 0 0.5em;
} }
@@ -6,12 +6,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { ActivatedRoute, RouterLink } from '@angular/router';
import { trigger, state, transition, animate, style } from '@angular/animations';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider'; import { MatDivider } from '@angular/material/divider';
import { import {
MatTable, MatTable,
MatColumnDef, MatColumnDef,
@@ -24,6 +22,9 @@ import {
MatRowDef, MatRowDef,
MatRow MatRow
} from '@angular/material/table'; } 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 { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
import { FindPipe } from '../../../../pipes/find.pipe'; import { FindPipe } from '../../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../../pipes/date-format.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', selector: 'mifosx-fixed-deposit-general-tab',
templateUrl: './fixed-deposit-general-tab.component.html', templateUrl: './fixed-deposit-general-tab.component.html',
styleUrls: ['./fixed-deposit-general-tab.component.scss'], 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: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
RouterLink,
FaIconComponent, FaIconComponent,
MatDivider, MatDivider,
MatTable, MatTable,
@@ -63,52 +58,90 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
YesnoPipe YesnoPipe
] ]
}) })
export class FixedDepositGeneralTabComponent { export class FixedDepositGeneralTabComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private accounting = inject(Accounting);
private productsService = inject(ProductsService);
/** Fixed Deposit Product data. */ /** Fixed Deposit Product data. */
fixedDepositProductData: any; fixedDepositProductData: any;
fixedDepositProductsTemplate: any; fixedDepositProductsTemplate: any;
chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; fdInterestRates: any[] = [];
chartSlabsDisplayedColumns: string[] = [
'period', accountingMappings: any = {};
'amountRange', accountingRule: OptionData;
'annualInterestRate',
readonly fdSlabColumns = [
'srNo',
'description', 'description',
'actions' 'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
]; ];
incentivesDisplayedColumns: string[] = [ readonly chargesDisplayedColumns = [
'entityType',
'attributeName',
'conditionType',
'attributeValue',
'incentiveType',
'amount'
];
chargesDisplayedColumns: string[] = [
'name', 'name',
'type', 'type',
'amount', 'amount',
'collectedon' 'collectedon'
]; ];
paymentFundSourceDisplayedColumns: string[] = [ readonly paymentFundSourceDisplayedColumns = [
'paymentTypeId', 'paymentTypeId',
'fundSourceAccountId' 'fundSourceAccountId'
]; ];
feesPenaltyIncomeDisplayedColumns: string[] = [ readonly feesPenaltyIncomeDisplayedColumns = [
'chargeId', 'chargeId',
'incomeAccountId' 'incomeAccountId'
]; ];
/**
* Retrieves the fixed deposit product data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
*/
constructor() { constructor() {
this.route.data.subscribe((data: { fixedDepositProduct: any; fixedDepositProductsTemplate: any }) => { this.route.data.subscribe((data: { fixedDepositProduct: any; fixedDepositProductsTemplate: any }) => {
this.fixedDepositProductData = data.fixedDepositProduct; this.fixedDepositProductData = data.fixedDepositProduct;
this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate; 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 */ /** Angular Imports */
import { Component, inject } from '@angular/core'; 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 { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; 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 { export class ViewFixedDepositProductComponent {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router);
fixedDepositDatatables: any = []; fixedDepositDatatables: any = [];
+52 -42
View File
@@ -685,44 +685,49 @@ const routes: Routes = [
breadcrumb: 'productId', breadcrumb: 'productId',
routeParamBreadcrumb: 'productId' routeParamBreadcrumb: 'productId'
}, },
component: ViewRecurringDepositProductComponent,
resolve: { resolve: {
recurringDepositDatatables: SavingProductDatatablesResolver recurringDepositDatatables: SavingProductDatatablesResolver
}, },
children: [ children: [
{ {
path: '', path: '',
redirectTo: 'general', component: ViewRecurringDepositProductComponent,
pathMatch: 'full'
},
{
path: 'general',
component: RecurringDepositGeneralTabComponent,
resolve: {
recurringDepositProduct: RecurringDepositProductResolver
}
},
{
path: 'datatables',
children: [ children: [
{ {
path: ':datatableName', path: '',
component: RecurringDepositDatatableTabComponent, redirectTo: 'general',
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' }, pathMatch: 'full'
},
{
path: 'general',
component: RecurringDepositGeneralTabComponent,
resolve: { 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', path: 'edit',
data: { title: 'Edit Recurring Deposit Product', breadcrumb: 'edit', routeParamBreadcrumb: false }, data: { title: 'Edit Recurring Deposit Product', breadcrumb: 'edit', routeParamBreadcrumb: false },
@@ -757,33 +762,38 @@ const routes: Routes = [
{ {
path: ':productId', path: ':productId',
data: { title: 'View Fixed Deposit Product', breadcrumb: 'productId', routeParamBreadcrumb: 'productId' }, data: { title: 'View Fixed Deposit Product', breadcrumb: 'productId', routeParamBreadcrumb: 'productId' },
component: ViewFixedDepositProductComponent,
resolve: { resolve: {
fixedDepositDatatables: SavingProductDatatablesResolver fixedDepositDatatables: SavingProductDatatablesResolver
}, },
children: [ children: [
{ {
path: '', path: '',
redirectTo: 'general', component: ViewFixedDepositProductComponent,
pathMatch: 'full'
},
{
path: 'general',
component: FixedDepositGeneralTabComponent,
resolve: {
fixedDepositProduct: FixedDepositProductResolver
}
},
{
path: 'datatables',
children: [ children: [
{ {
path: ':datatableName', path: '',
component: FixedDepositDatatableTabComponent, redirectTo: 'general',
data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' }, pathMatch: 'full'
},
{
path: 'general',
component: FixedDepositGeneralTabComponent,
resolve: { 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); 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. * @returns {Observable<any>} Collateral data.
*/ */
@@ -149,6 +149,7 @@
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate" [recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[recurringDepositProductFormValid]="recurringDepositProductFormValid" [recurringDepositProductFormValid]="recurringDepositProductFormValid"
[isDormancyTrackingActive]="recurringDepositProductCurrencyForm.get('dormancyAlert')"
></mifosx-recurring-deposit-product-accounting-step> ></mifosx-recurring-deposit-product-accounting-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -123,23 +123,23 @@ export class CreateRecurringDepositProductComponent {
} }
get recurringDepositProductDetailsForm() { get recurringDepositProductDetailsForm() {
return this.recurringDepositProductDetailsStep.recurringDepositProductDetailsForm; return this.recurringDepositProductDetailsStep?.recurringDepositProductDetailsForm;
} }
get recurringDepositProductCurrencyForm() { get recurringDepositProductCurrencyForm() {
return this.recurringDepositProductCurrencyStep.recurringDepositProductCurrencyForm; return this.recurringDepositProductCurrencyStep?.recurringDepositProductCurrencyForm;
} }
get recurringDepositProductTermsForm() { get recurringDepositProductTermsForm() {
return this.recurringDepositProductTermsStep.recurringDepositProductTermsForm; return this.recurringDepositProductTermsStep?.recurringDepositProductTermsForm;
} }
get recurringDepositProductSettingsForm() { get recurringDepositProductSettingsForm() {
return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm; return this.recurringDepositProductSettingsStep?.recurringDepositProductSettingsForm;
} }
get recurringDepositProductAccountingForm() { get recurringDepositProductAccountingForm() {
return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm; return this.recurringDepositProductAccountingStep?.recurringDepositProductAccountingForm;
} }
get totalTabs(): number { get totalTabs(): number {
@@ -147,13 +147,7 @@ export class CreateRecurringDepositProductComponent {
} }
get recurringDepositProductFormValid() { get recurringDepositProductFormValid() {
return ( return !!this.recurringDepositProductDetailsForm?.valid;
this.recurringDepositProductDetailsForm.valid &&
this.recurringDepositProductCurrencyForm.valid &&
this.recurringDepositProductTermsForm.valid &&
this.recurringDepositProductSettingsForm.valid &&
this.recurringDepositProductAccountingForm.valid
);
} }
get activeBaseVersion() { get activeBaseVersion() {
@@ -148,12 +148,14 @@
<mifosx-recurring-deposit-product-accounting-step <mifosx-recurring-deposit-product-accounting-step
[recurringDepositProductsTemplate]="recurringDepositProductsTemplate" [recurringDepositProductsTemplate]="recurringDepositProductsTemplate"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[recurringDepositProductFormValid]="recurringDepositProductFormValidAndNotPrinstine" [recurringDepositProductFormValid]="recurringDepositProductFormValid"
></mifosx-recurring-deposit-product-accounting-step> [isDormancyTrackingActive]="recurringDepositProductCurrencyForm.get('dormancyAlert')"
>
</mifosx-recurring-deposit-product-accounting-step>
</div> </div>
</mat-tab> </mat-tab>
@if (recurringDepositProductFormValidAndNotPrinstine) { @if (recurringDepositProductFormValid) {
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<i class="fas fa-eye rdp-tab-icon"></i> <i class="fas fa-eye rdp-tab-icon"></i>
@@ -189,7 +191,7 @@
<i class="fas fa-arrow-right" style="margin-left: 8px"></i> <i class="fas fa-arrow-right" style="margin-left: 8px"></i>
</button> </button>
} }
@if (recurringDepositProductFormValidAndNotPrinstine && selectedTabIndex === totalTabs - 1) { @if (recurringDepositProductFormValid && selectedTabIndex === totalTabs - 1) {
<button mat-raised-button color="primary" (click)="submit()"> <button mat-raised-button color="primary" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px"></i> <i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }} {{ 'labels.buttons.Submit' | translate }}
@@ -120,42 +120,31 @@ export class EditRecurringDepositProductComponent {
} }
get recurringDepositProductDetailsForm() { get recurringDepositProductDetailsForm() {
return this.recurringDepositProductDetailsStep.recurringDepositProductDetailsForm; return this.recurringDepositProductDetailsStep?.recurringDepositProductDetailsForm;
} }
get recurringDepositProductCurrencyForm() { get recurringDepositProductCurrencyForm() {
return this.recurringDepositProductCurrencyStep.recurringDepositProductCurrencyForm; return this.recurringDepositProductCurrencyStep?.recurringDepositProductCurrencyForm;
} }
get recurringDepositProductTermsForm() { get recurringDepositProductTermsForm() {
return this.recurringDepositProductTermsStep.recurringDepositProductTermsForm; return this.recurringDepositProductTermsStep?.recurringDepositProductTermsForm;
} }
get recurringDepositProductSettingsForm() { get recurringDepositProductSettingsForm() {
return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm; return this.recurringDepositProductSettingsStep?.recurringDepositProductSettingsForm;
} }
get recurringDepositProductAccountingForm() { get recurringDepositProductAccountingForm() {
return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm; return this.recurringDepositProductAccountingStep?.recurringDepositProductAccountingForm;
} }
get totalTabs(): number { get totalTabs(): number {
return this.recurringDepositProductFormValidAndNotPrinstine ? 7 : 6; return this.recurringDepositProductFormValid ? 8 : 7;
} }
get recurringDepositProductFormValidAndNotPrinstine() { get recurringDepositProductFormValid() {
return ( return !!this.recurringDepositProductDetailsForm?.valid;
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 activeBaseVersion() { get activeBaseVersion() {
@@ -173,7 +162,8 @@ export class EditRecurringDepositProductComponent {
...this.recurringDepositProductTermsStep.recurringDepositProductTerms, ...this.recurringDepositProductTermsStep.recurringDepositProductTerms,
...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings,
...this.recurringDepositProductChargesStep.recurringDepositProductCharges, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges,
...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting,
rdSlabs: this.activeBaseSlabs
}; };
} }
@@ -185,12 +175,11 @@ export class EditRecurringDepositProductComponent {
} }
submit() { submit() {
// TODO: Update once language and date settings are setup const recurringDepositProduct: any = this.pruneEmptyValues({
const recurringDepositProduct = {
...this.recurringDepositProduct, ...this.recurringDepositProduct,
charges: this.recurringDepositProduct.charges.map((charge: any) => ({ id: charge.id })), charges: (this.recurringDepositProduct.charges ?? []).map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code // locale required for depositAmount locale: this.settingsService.language.code
}; });
if (!recurringDepositProduct.description) { if (!recurringDepositProduct.description) {
recurringDepositProduct.description = ''; recurringDepositProduct.description = '';
} }
@@ -202,4 +191,35 @@ export class EditRecurringDepositProductComponent {
this.router.navigate(['../'], { relativeTo: this.route }); 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>
<div class="rdpa-list"> <div class="rdpa-list">
@for (row of section.rows; track row.key) { @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">
<div class="rdpa-row__meta"> <div class="rdpa-row__meta">
<span <span
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpa-section { .rdpa-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
} }
@@ -32,6 +32,7 @@ type AccountingRowConfig = {
options: GLAccount[]; options: GLAccount[];
required?: boolean; required?: boolean;
accrualOnly?: boolean; accrualOnly?: boolean;
dormancyOnly?: boolean;
}; };
type AccountingSectionConfig = { type AccountingSectionConfig = {
@@ -69,6 +70,7 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
@Input() recurringDepositProductsTemplate: any; @Input() recurringDepositProductsTemplate: any;
@Input() accountingRuleData: any; @Input() accountingRuleData: any;
@Input() recurringDepositProductFormValid: boolean; @Input() recurringDepositProductFormValid: boolean;
@Input() isDormancyTrackingActive: any;
recurringDepositProductAccountingForm: UntypedFormGroup; recurringDepositProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = []; assetAccountOptions: GLAccount[] = [];
@@ -101,11 +103,22 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
this.recurringDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }); this.recurringDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId });
this.syncAccountingControls(accountingRuleId); this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings); this.patchAccountingValues(mappings);
this.setDormancyControl();
} }
createRecurringDepositProductAccountingForm() { createRecurringDepositProductAccountingForm() {
this.recurringDepositProductAccountingForm = this.formBuilder.group({ this.recurringDepositProductAccountingForm = this.formBuilder.group({
accountingRule: [2], accountingRule: [2],
savingsReferenceAccountId: [''],
savingsControlAccountId: [''],
transfersInSuspenseAccountId: [''],
interestOnSavingsAccountId: [''],
incomeFromFeeAccountId: [''],
incomeFromPenaltyAccountId: [''],
feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''],
escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]), assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]), liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]), expensesOtherGlMappings: this.formBuilder.array([]),
@@ -129,7 +142,8 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
interestPayableAccountId: mappings.interestPayableAccount?.id || '' interestPayableAccountId: mappings.interestPayableAccount?.id || '',
escheatLiabilityId: mappings.escheatLiabilityAccount?.id || ''
}); });
if (this.isCashOrAccrualAccounting()) { if (this.isCashOrAccrualAccounting()) {
@@ -215,53 +229,43 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; 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) { private syncAccountingControls(accountingRule: number) {
const addControl = (name: string, required = false) => { const setRequired = (name: string, required: boolean) => {
if (!this.recurringDepositProductAccountingForm.contains(name)) { const ctrl = this.recurringDepositProductAccountingForm.get(name);
this.recurringDepositProductAccountingForm.addControl( if (!ctrl) return;
name, if (required) {
new UntypedFormControl('', required ? Validators.required : undefined) ctrl.setValidators(Validators.required);
);
} 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);
} else { } else {
removeControl('feesReceivableAccountId'); ctrl.clearValidators();
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
} }
} else { ctrl.updateValueAndValidity({ emitEvent: false });
[ };
'savingsReferenceAccountId',
'savingsControlAccountId', const cashOrAccrual = this.accounting.isCashOrAccrualAccountingRuleId(accountingRule);
'transfersInSuspenseAccountId', const accrualOnly = this.accounting.isAccrualAccountingRuleId(accountingRule);
'interestOnSavingsAccountId',
'incomeFromFeeAccountId', setRequired('savingsReferenceAccountId', cashOrAccrual);
'incomeFromPenaltyAccountId', setRequired('savingsControlAccountId', cashOrAccrual);
'feesReceivableAccountId', setRequired('transfersInSuspenseAccountId', cashOrAccrual);
'penaltiesReceivableAccountId', setRequired('interestOnSavingsAccountId', cashOrAccrual);
'interestPayableAccountId' setRequired('incomeFromFeeAccountId', cashOrAccrual);
].forEach(removeControl); setRequired('incomeFromPenaltyAccountId', cashOrAccrual);
setRequired('feesReceivableAccountId', accrualOnly);
setRequired('penaltiesReceivableAccountId', accrualOnly);
setRequired('interestPayableAccountId', accrualOnly);
setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled());
if (!cashOrAccrual) {
this.clearOtherGlMappings(); this.clearOtherGlMappings();
} }
} }
@@ -331,6 +335,14 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
controlName: 'interestPayableAccountId', controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions, options: this.liabilityAccountOptions,
accrualOnly: true accrualOnly: true
},
{
key: 'escheatLiabilityId',
label: 'Escheat Liability Account',
inputLabel: 'Escheat liability account',
controlName: 'escheatLiabilityId',
options: this.liabilityAccountOptions,
dormancyOnly: true
} }
], ],
otherMappingsControlName: 'liabilitiesOtherGlMappings', otherMappingsControlName: 'liabilitiesOtherGlMappings',
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpc-section { .rdpc-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpd-section { .rdpd-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdps-section { .rdps-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -15,7 +15,6 @@ $gray-900: #202124;
.rdpt-section { .rdpt-section {
border: 1px solid $border; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden;
background: #fff; background: #fff;
& + & { & + & {
@@ -7,7 +7,7 @@
*/ */
import { Component, inject } from '@angular/core'; 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 { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; 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 { export class ViewRecurringDepositProductComponent {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router);
recurringDepositDatatables: any = []; 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 * 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/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
/* tslint:disable */ /* tslint:disable */
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '260430', 'version': '260504',
'hash': '2d4fb423' 'hash': 'e9d2f4b5'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };