feat: update FD product and account creation UI to match RD layout

- Matched all FD product stepper steps (Details, Currency, Terms, Settings, Charges, Accounting) to RD product UI
- Updated FD account creation stepper structure with conditional steps and Previous/Next navigation
This commit is contained in:
Nitesh Yadav
2026-05-01 10:32:43 +05:30
parent 58cef1c20b
commit 05cf2ee33f
22 changed files with 1952 additions and 1424 deletions
@@ -34,7 +34,8 @@
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="fixedDepositAccountDetailsForm">
<!-- Step 1: Details -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-fixed-deposit-account-details-step
@@ -44,48 +45,97 @@
</mifosx-fixed-deposit-account-details-step>
</mat-step>
<mat-step [stepControl]="fixedDepositAccountTermsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
@if (fixedDepositsAccountProductTemplate) {
<!-- Step 2: Terms -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-fixed-deposit-account-terms-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-terms-step>
</mat-step>
<mifosx-fixed-deposit-account-terms-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-terms-step>
<mat-step [stepControl]="fixedDepositAccountSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<mifosx-fixed-deposit-account-settings-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-settings-step>
</mat-step>
<!-- Step 3: Settings -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mifosx-fixed-deposit-account-settings-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-settings-step>
<mifosx-fixed-deposit-account-interest-rate-chart-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-interest-rate-chart-step>
</mat-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<!-- Step 4: Interest Rate Chart -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mifosx-fixed-deposit-account-charges-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[fixedDepositAccountFormValid]="fixedDepositAccountFormValid"
>
</mifosx-fixed-deposit-account-charges-step>
</mat-step>
<mifosx-fixed-deposit-account-interest-rate-chart-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-interest-rate-chart-step>
@if (fixedDepositAccountFormValid) {
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 5: Charges -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-fixed-deposit-account-charges-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[fixedDepositAccountFormValid]="fixedDepositAccountFormValid"
>
</mifosx-fixed-deposit-account-charges-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 6: Preview -->
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-fixed-deposit-account-preview-step
@@ -20,7 +20,14 @@ import { FixedDepositAccountTermsStepComponent } from '../fixed-deposit-account-
import { FixedDepositAccountSettingsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-settings-step/fixed-deposit-account-settings-step.component';
import { FixedDepositAccountChargesStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-charges-step/fixed-deposit-account-charges-step.component';
import { Dates } from 'app/core/utils/dates';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import {
MatStepper,
MatStepperIcon,
MatStep,
MatStepLabel,
MatStepperNext,
MatStepperPrevious
} from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component';
import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component';
@@ -37,6 +44,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
MatStepperNext,
MatStepperPrevious,
FaIconComponent,
MatStep,
MatStepLabel,
@@ -55,17 +64,13 @@ export class CreateFixedDepositAccountComponent {
private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService);
/** Fixed Deposits Account Details Step */
@ViewChild(FixedDepositAccountDetailsStepComponent, { static: true })
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
/** Fixed Deposits Account Terms Step */
@ViewChild(FixedDepositAccountTermsStepComponent, { static: true })
@ViewChild(FixedDepositAccountTermsStepComponent)
fixedDepositAccountTermsStep: FixedDepositAccountTermsStepComponent;
/** Fixed Deposits Account Settings Step */
@ViewChild(FixedDepositAccountSettingsStepComponent, { static: true })
@ViewChild(FixedDepositAccountSettingsStepComponent)
fixedDepositAccountSettingsStep: FixedDepositAccountSettingsStepComponent;
/** Fixed Deposits Account Charges Step */
@ViewChild(FixedDepositAccountChargesStepComponent, { static: true })
@ViewChild(FixedDepositAccountChargesStepComponent)
fixedDepositAccountChargesStep: FixedDepositAccountChargesStepComponent;
/** Fixed Deposits Account Template */
@@ -67,12 +67,60 @@
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-chart-area fdp-tab-icon"></i>
{{ 'labels.inputs.INTEREST RATE CHART' | translate }}
CBS Base Chart
</ng-template>
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-interest-rate-chart-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
></mifosx-fixed-deposit-product-interest-rate-chart-step>
@if (activeBaseVersion) {
<div class="base-rate-card">
<div class="base-rate-header">
<div>
<h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
<p class="base-rate-meta">
Active CBS base chart
<span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</p>
</div>
<span class="base-rate-badge">Active</span>
</div>
@if (activeBaseVersion.remarks) {
<p class="base-rate-remarks">{{ activeBaseVersion.remarks }}</p>
}
<table class="base-rate-table" mat-table [dataSource]="activeBaseVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? 'N/A' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="fdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: fdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div>
</mat-tab>
@@ -112,7 +160,7 @@
<div class="fdp-tab-content">
<mifosx-fixed-deposit-product-preview-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="fixedDepositProductInterestRateChartStep.chartSlabsDisplayedColumns"
[chartSlabsDisplayedColumns]="fdSlabColumns"
[accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct"
(submitEvent)="submit()"
@@ -71,6 +71,54 @@ $primary: #1353a0;
padding: 24px 16px;
}
.base-rate-card {
padding: 16px;
border: 1px solid #dbe4f0;
border-radius: 14px;
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.base-rate-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.base-rate-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.base-rate-meta {
margin: 6px 0 0;
color: #5b6b7f;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.base-rate-badge {
padding: 4px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.base-rate-remarks {
margin: 0 0 12px;
color: #334155;
}
.base-rate-table {
width: 100%;
border-collapse: collapse;
}
.fdp-action-bar {
display: flex;
justify-content: space-between;
@@ -15,9 +15,20 @@ import { FixedDepositProductDetailsStepComponent } from '../fixed-deposit-produc
import { FixedDepositProductCurrencyStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component';
import { FixedDepositProductTermsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component';
import { FixedDepositProductSettingsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component';
import { FixedDepositProductInterestRateChartStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component';
import { FixedDepositProductChargesStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component';
import { FixedDepositProductAccountingStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
@@ -40,10 +51,19 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FixedDepositProductCurrencyStepComponent,
FixedDepositProductTermsStepComponent,
FixedDepositProductSettingsStepComponent,
FixedDepositProductInterestRateChartStepComponent,
FixedDepositProductChargesStepComponent,
FixedDepositProductAccountingStepComponent,
FixedDepositProductPreviewStepComponent
FixedDepositProductPreviewStepComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
]
})
export class CreateFixedDepositProductComponent {
@@ -61,8 +81,6 @@ export class CreateFixedDepositProductComponent {
fixedDepositProductTermsStep: FixedDepositProductTermsStepComponent;
@ViewChild(FixedDepositProductSettingsStepComponent, { static: true })
fixedDepositProductSettingsStep: FixedDepositProductSettingsStepComponent;
@ViewChild(FixedDepositProductInterestRateChartStepComponent, { static: true })
fixedDepositProductInterestRateChartStep: FixedDepositProductInterestRateChartStepComponent;
@ViewChild(FixedDepositProductChargesStepComponent, { static: true })
fixedDepositProductChargesStep: FixedDepositProductChargesStepComponent;
@ViewChild(FixedDepositProductAccountingStepComponent, { static: true })
@@ -71,19 +89,29 @@ export class CreateFixedDepositProductComponent {
fixedDepositProductsTemplate: any;
accountingRuleData: string[] = [];
selectedTabIndex = 0;
/**
* @param {ActivatedRoute} route Activated Route.
* @param {ProductsService} productsService Products Service.
* @param {Router} router Router for navigation.
* @param {SettingsService} settingsService Settings Service.
*/
fdInterestRates: any[] = [];
readonly fdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
constructor() {
this.route.data.subscribe((data: { fixedDepositProductsTemplate: any }) => {
this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.fdInterestRates = rates || [];
},
error: () => {
this.fdInterestRates = [];
}
});
}
get fixedDepositProductDetailsForm() {
@@ -102,10 +130,6 @@ export class CreateFixedDepositProductComponent {
return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm;
}
get fixedDepositProductInterestRateChartForm() {
return this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChartForm;
}
get fixedDepositProductAccountingForm() {
return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm;
}
@@ -120,29 +144,35 @@ export class CreateFixedDepositProductComponent {
this.fixedDepositProductCurrencyForm.valid &&
this.fixedDepositProductTermsForm.valid &&
this.fixedDepositProductSettingsForm.valid &&
this.fixedDepositProductInterestRateChartForm.valid &&
this.fixedDepositProductAccountingForm.valid
);
}
get activeBaseVersion() {
return this.fdInterestRates.find((v: any) => v.isActive) ?? this.fdInterestRates[0] ?? null;
}
get activeBaseSlabs() {
return this.activeBaseVersion?.slabs || [];
}
get fixedDepositProduct() {
return {
...this.fixedDepositProductDetailsStep.fixedDepositProductDetails,
...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency,
...this.fixedDepositProductTermsStep.fixedDepositProductTerms,
...this.fixedDepositProductSettingsStep.fixedDepositProductSettings,
...this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChart,
...this.fixedDepositProductChargesStep.fixedDepositProductCharges,
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting,
fdSlabs: this.activeBaseSlabs
};
}
submit() {
// TODO: Update once language and date settings are setup
const fixedDepositProduct = {
...this.fixedDepositProduct,
charges: this.fixedDepositProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code // locale required for depositAmount
locale: this.settingsService.language.code
};
delete fixedDepositProduct.advancedAccountingRules;
this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => {
@@ -9,310 +9,111 @@
<form [formGroup]="fixedDepositProductAccountingForm">
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-book"></i>
<h3>{{ 'labels.heading.Accounting' | translate }}</h3>
</div>
<div class="fdpa-grid">
<div class="fdpa-field fdpa-field--full">
<mat-radio-group class="fdpa-radio-group" formControlName="accountingRule">
@for (accountingRule of accountingRuleData; track accountingRule; let i = $index) {
<mat-radio-button [value]="i + 1">
{{ 'labels.accounting.' + accountingRule | translate }}
</mat-radio-button>
}
</mat-radio-group>
</div>
</div>
</div>
@if (isCashOrAccrualAccounting()) {
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-landmark"></i>
<h3>{{ 'labels.heading.Assets' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.savingsReferenceAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Saving reference'"
>
</mifosx-gl-account-selector>
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.feesReceivableAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Fees Receivable'"
>
</mifosx-gl-account-selector>
}
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.penaltiesReceivableAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Penalties Receivable'"
>
</mifosx-gl-account-selector>
}
</div>
<span class="fdpa-section__icon">
<i class="fas fa-book"></i>
</span>
<h3>Accounting</h3>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-balance-scale"></i>
<h3>{{ 'labels.heading.Liabilities' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.savingsControlAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Saving control'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.transfersInSuspenseAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Savings transfers in suspense'"
>
</mifosx-gl-account-selector>
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.interestPayableAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Interest Payable'"
>
</mifosx-gl-account-selector>
}
</div>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-arrow-circle-down"></i>
<h3>{{ 'labels.heading.Expenses' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.interestOnSavingsAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'Interest on savings'"
>
</mifosx-gl-account-selector>
</div>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-arrow-circle-up"></i>
<h3>{{ 'labels.heading.Income' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.incomeFromFeeAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from fees'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.incomeFromPenaltyAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from penalties'"
>
</mifosx-gl-account-selector>
</div>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-cogs"></i>
<h3>{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
</div>
<div class="fdpa-grid">
<div class="fdpa-field fdpa-field--full fdpa-checkbox-row">
<mat-checkbox formControlName="advancedAccountingRules"></mat-checkbox>
<div class="fdpa-section__body">
<div class="fdpa-group">
<div class="fdpa-group__header">
<h4>Accounting Rule</h4>
</div>
<div class="fdpa-radio-group">
<mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
</div>
</div>
@if (fixedDepositProductAccountingForm.value.advancedAccountingRules) {
<!-- Payment Fund Source -->
<div class="fdpa-subsection">
<div class="fdpa-subsection__header">
<h4>{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="fdpa-add-btn"
(click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (paymentChannelToFundSourceMappings.value.length !== 0) {
<div class="fdpa-table-wrapper">
<table mat-table [dataSource]="paymentChannelToFundSourceMappings.value">
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : '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.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Actions' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(paymentChannelToFundSourceMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
</div>
@if (existCharges()) {
<!-- Fees Income -->
<div class="fdpa-subsection">
<div class="fdpa-subsection__header">
<h4>{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="fdpa-add-btn"
(click)="add('FeesIncome', feeToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
@if (isCashOrAccrualAccounting()) {
@for (section of accountingSectionConfigs; track section.key) {
<div class="fdpa-group">
<div class="fdpa-group__header">
<h4>{{ section.title }}</h4>
<button type="button" class="fdpa-add-btn" (click)="addOtherGlMapping(section.otherMappingsControlName)">
+ Add Other
</button>
</div>
@if (feeToIncomeAccountMappings.value.length !== 0) {
<div class="fdpa-table-wrapper">
<table mat-table [dataSource]="feeToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.catalogs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.chargeId | find: chargeData : 'id' : '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.incomeAccountId | find: incomeAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Actions' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('FeesIncome', feeToIncomeAccountMappings, i)"
<div class="fdpa-list">
@for (row of section.rows; track row.key) {
@if (!row.accrualOnly || isAccrualAccounting()) {
<div class="fdpa-row">
<div class="fdpa-row__meta">
<span
class="fdpa-pill"
[class.fdpa-pill--green]="section.pillClass === 'green'"
[class.fdpa-pill--amber]="section.pillClass === 'amber'"
[class.fdpa-pill--blue]="section.pillClass === 'blue'"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(feeToIncomeAccountMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Penalty Income -->
<div class="fdpa-subsection">
<div class="fdpa-subsection__header">
<h4>
{{ 'labels.catalogs.Penalties' | translate }}
{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}
</h4>
<button
type="button"
mat-raised-button
color="primary"
class="fdpa-add-btn"
(click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
{{ section.pillLabel }}
</span>
<span class="fdpa-row__label">{{ row.label }}</span>
</div>
<mifosx-gl-account-selector
class="fdpa-row__input"
[inputFormControl]="fixedDepositProductAccountingForm.get(row.controlName)"
[glAccountList]="row.options"
[required]="row.required"
[inputLabel]="row.inputLabel"
></mifosx-gl-account-selector>
</div>
}
}
</div>
@if (penaltyToIncomeAccountMappings.value.length !== 0) {
<div class="fdpa-table-wrapper">
<table mat-table [dataSource]="penaltyToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find: penaltyData : 'id' : '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.incomeAccountId | find: incomeAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Actions' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
<div class="fdpa-list" [formArrayName]="section.otherMappingsControlName">
@for (
otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
track $index;
let i = $index
) {
<div class="fdpa-other-row-wrap">
<div class="fdpa-row fdpa-row--other" [formGroupName]="i">
<div class="fdpa-row__meta fdpa-row__meta--other">
<span
class="fdpa-pill"
[class.fdpa-pill--green]="section.pillClass === 'green'"
[class.fdpa-pill--amber]="section.pillClass === 'amber'"
[class.fdpa-pill--blue]="section.pillClass === 'blue'"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(penaltyToIncomeAccountMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
{{ section.pillLabel }}
</span>
<input
class="fdpa-row__name fdpa-row__name--other-account"
formControlName="accountName"
placeholder="Enter account name..."
type="text"
/>
</div>
<div class="fdpa-row__actions fdpa-row__actions--other">
<mat-form-field class="fdpa-row__other-field" appearance="outline" subscriptSizing="dynamic">
<mat-label>GL Code</mat-label>
<mat-select
[formControl]="getOtherGlMappingControl(section.otherMappingsControlName, i, 'glAccountId')"
>
@for (glAccount of section.otherAccountOptions; track glAccount.id) {
<mat-option [value]="glAccount.id">
{{ '(' + glAccount.glCode + ') ' + glAccount.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<button
type="button"
class="fdpa-remove-btn fdpa-remove-btn--side"
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
>
<i class="fas fa-times"></i>
</button>
</div>
}
</div>
</div>
}
}
</div>
}
</div>
</form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-700: #334155;
$gray-900: #202124;
.fdpa-section {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
@@ -15,45 +21,244 @@
& + & {
margin-top: 16px;
}
}
.fdpa-section__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
.fdpa-section__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
color: #2563eb;
font-size: 13px;
}
.fdpa-section__body {
padding: 16px 24px 20px;
}
.fdpa-group {
& + & {
margin-top: 20px;
}
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
justify-content: space-between;
gap: 16px;
margin: 0 0 12px;
}
i {
font-size: 14px;
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
h4 {
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #94a3b8;
}
}
.fdpa-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
.fdpa-add-btn {
border: 0;
background: transparent;
color: #2563eb;
font-size: 12px;
font-weight: 700;
padding: 0;
cursor: pointer;
}
@media (width <= 600px) {
.fdpa-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.fdpa-list + .fdpa-list {
margin-top: 12px;
}
.fdpa-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 12px;
border: 1px solid $border;
border-radius: 8px;
background: #fff;
@media (width <= 900px) {
flex-direction: column;
align-items: stretch;
}
}
.fdpa-row:has(mifosx-gl-account-selector) {
align-items: stretch;
}
.fdpa-row--other {
border-style: dashed;
border-color: #cbd5e1;
background: #f8fafc;
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
align-items: center;
padding: 12px 16px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.fdpa-field {
.fdpa-other-row-wrap {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
gap: 8px;
}
}
.fdpa-row__meta {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1 1 auto;
}
.fdpa-row__meta--other {
min-width: 0;
flex: 1 1 auto;
align-self: stretch;
}
.fdpa-row__label {
font-size: 14px;
font-weight: 600;
color: $gray-700;
}
.fdpa-row__input {
flex: 0 0 22rem;
max-width: 22rem;
min-width: 18rem;
@media (width <= 900px) {
width: 100%;
max-width: 100%;
min-width: 0;
}
}
.fdpa-row__input :is(mifosx-gl-account-selector) {
width: 100%;
display: block;
}
.fdpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) {
width: 100%;
margin-right: 0 !important;
}
.fdpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) {
width: 100%;
}
.fdpa-row__actions {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.fdpa-row__actions--other {
justify-content: flex-end;
align-self: stretch;
width: 100%;
&--full {
grid-column: 1 / -1;
@media (width <= 1100px) {
justify-content: flex-start;
}
}
.fdpa-row__name {
width: 100%;
min-width: 0;
max-width: 24rem;
height: 40px;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
background: #fff;
@media (width <= 900px) {
max-width: 100%;
}
}
.fdpa-row__name--other-account {
max-width: 100%;
flex: 1 1 auto;
height: 48px;
}
.fdpa-row__other-field {
width: 100%;
min-width: 0;
}
.fdpa-row__other-field :is(mat-form-field) {
width: 100%;
}
.fdpa-row__other-field :is(.mat-mdc-form-field-required-marker) {
display: none;
}
.fdpa-remove-btn {
border: 0;
background: transparent;
color: #ef4444;
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
.fdpa-remove-btn--side {
place-self: center end;
margin-right: 4px;
@media (width <= 1100px) {
justify-self: start;
margin-right: 0;
}
}
@@ -61,23 +266,34 @@
display: flex;
gap: 24px;
flex-wrap: wrap;
@media (width <= 600px) {
flex-direction: column;
}
}
.fdpa-checkbox-row {
display: flex;
.fdpa-pill {
display: inline-flex;
align-items: center;
padding: 4px 0;
}
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
.fdpa-add-btn {
height: 36px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
&--green {
background: #dcfce7;
color: #16a34a;
}
&--amber {
background: #fef3c7;
color: #d97706;
}
&--blue {
background: #dbeafe;
color: #2563eb;
}
}
.fdpa-subsection {
@@ -95,13 +311,13 @@
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
color: $gray-900;
}
}
}
.fdpa-table-wrapper {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 8px;
overflow: hidden;
@@ -109,3 +325,14 @@
width: 100%;
}
}
@media (width <= 900px) {
.fdpa-row__meta {
margin-bottom: 4px;
}
.fdpa-row__input {
flex: 1 1 auto;
max-width: 100%;
}
}
@@ -8,434 +8,391 @@
import { Component, Input, OnInit, inject } from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators,
ReactiveFormsModule
Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component';
import { Accounting } from 'app/core/utils/accounting';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { FindPipe } from '../../../../pipes/find.pipe';
import { GLAccount } from 'app/shared/models/general.model';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income';
type AccountingRowConfig = {
key: string;
label: string;
inputLabel: string;
controlName: string;
options: GLAccount[];
required?: boolean;
accrualOnly?: boolean;
};
type AccountingSectionConfig = {
key: AccountingSectionKey;
title: string;
pillLabel: string;
pillClass: 'green' | 'amber' | 'blue';
rows: AccountingRowConfig[];
otherMappingsControlName: string;
otherAccountOptions: GLAccount[];
};
type OtherGlMappingValue = {
accountName: string;
glAccountId: number | null;
};
@Component({
selector: 'mifosx-fixed-deposit-product-accounting-step',
templateUrl: './fixed-deposit-product-accounting-step.component.html',
styleUrls: ['./fixed-deposit-product-accounting-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
ReactiveFormsModule,
MatRadioGroup,
MatRadioButton,
GlAccountSelectorComponent,
MatCheckbox,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FindPipe
GlAccountSelectorComponent
]
})
export class FixedDepositProductAccountingStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
dialog = inject(MatDialog);
private accounting = inject(Accounting);
private translateService = inject(TranslateService);
@Input() fixedDepositProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() fixedDepositProductFormValid: boolean;
fixedDepositProductAccountingForm: UntypedFormGroup;
chargeData: any;
penaltyData: any;
paymentTypeData: any;
assetAccountData: any;
incomeAccountData: any;
expenseAccountData: any;
liabilityAccountData: any;
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId',
'actions'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'incomeAccountId',
'actions'
];
assetAccountOptions: GLAccount[] = [];
liabilityAccountOptions: GLAccount[] = [];
expenseAccountOptions: GLAccount[] = [];
incomeAccountOptions: GLAccount[] = [];
accountingSectionConfigs: AccountingSectionConfig[] = [];
constructor() {
this.createfixedDepositProductAccountingForm();
this.createFixedDepositProductAccountingForm();
this.setConditionalControls();
}
ngOnInit() {
this.chargeData = this.fixedDepositProductsTemplate.chargeOptions || [];
this.penaltyData = this.fixedDepositProductsTemplate.penaltyOptions || [];
this.paymentTypeData = this.fixedDepositProductsTemplate.paymentTypeOptions || [];
this.assetAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
this.incomeAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || [];
this.expenseAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || [];
this.liabilityAccountData =
this.fixedDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) {
this.assignAccountingStepData();
}
const accountingMappings = this.fixedDepositProductsTemplate?.accountingMappings || {};
const mappings = {
...accountingMappings,
otherGlMappingsMap: this.fixedDepositProductsTemplate?.otherGlMappingsMap
};
const accountingMappingOptions = this.fixedDepositProductsTemplate?.accountingMappingOptions || {};
const accountingRuleId = 2; // Always Cash
this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
this.buildAccountingSectionConfigs();
this.fixedDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId });
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
}
assignAccountingStepData() {
this.fixedDepositProductAccountingForm.patchValue({
accountingRule: this.fixedDepositProductsTemplate.accountingRule.id
});
if (this.isCashOrAccrualAccounting()) {
this.fixedDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: this.fixedDepositProductsTemplate.accountingMappings.savingsReferenceAccount.id,
savingsControlAccountId: this.fixedDepositProductsTemplate.accountingMappings.savingsControlAccount.id,
transfersInSuspenseAccountId:
this.fixedDepositProductsTemplate.accountingMappings.transfersInSuspenseAccount.id,
incomeFromFeeAccountId: this.fixedDepositProductsTemplate.accountingMappings.incomeFromFeeAccount.id,
incomeFromPenaltyAccountId: this.fixedDepositProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id,
interestOnSavingsAccountId: this.fixedDepositProductsTemplate.accountingMappings.interestOnSavingsAccount.id
});
if (this.isAccrualAccounting()) {
this.fixedDepositProductAccountingForm.patchValue({
feesReceivableAccountId: this.fixedDepositProductsTemplate.accountingMappings.feeReceivableAccount.id,
penaltiesReceivableAccountId:
this.fixedDepositProductsTemplate.accountingMappings.penaltyReceivableAccount.id,
interestPayableAccountId: this.fixedDepositProductsTemplate.accountingMappings.interestPayableAccount.id
});
}
if (
this.fixedDepositProductsTemplate.paymentChannelToFundSourceMappings ||
this.fixedDepositProductsTemplate.feeToIncomeAccountMappings ||
this.fixedDepositProductsTemplate.penaltyToIncomeAccountMappings
) {
this.fixedDepositProductAccountingForm.patchValue({
advancedAccountingRules: true
});
}
if (this.fixedDepositProductsTemplate.paymentChannelToFundSourceMappings) {
this.fixedDepositProductsTemplate.paymentChannelToFundSourceMappings.forEach(
(paymentChannelToFundSourceMapping: any) => {
const paymentChannelToFundSourceMappingData = this.formBuilder.group({
paymentTypeId: [
paymentChannelToFundSourceMapping.paymentType.id,
Validators.required
],
fundSourceAccountId: [
paymentChannelToFundSourceMapping.fundSourceAccount.id,
Validators.required
]
});
const formArray = this.fixedDepositProductAccountingForm.controls[
'paymentChannelToFundSourceMappings'
] as UntypedFormArray;
formArray.push(paymentChannelToFundSourceMappingData);
}
);
}
if (this.fixedDepositProductsTemplate.feeToIncomeAccountMappings) {
this.fixedDepositProductsTemplate.feeToIncomeAccountMappings.forEach((feeToIncomeAccountMapping: any) => {
const feeToIncomeAccountMappingData = this.formBuilder.group({
chargeId: [
feeToIncomeAccountMapping.charge.id,
Validators.required
],
incomeAccountId: [
feeToIncomeAccountMapping.incomeAccount.id,
Validators.required
]
});
const formArray = this.fixedDepositProductAccountingForm.controls[
'feeToIncomeAccountMappings'
] as UntypedFormArray;
formArray.push(feeToIncomeAccountMappingData);
});
}
if (this.fixedDepositProductsTemplate.penaltyToIncomeAccountMappings) {
this.fixedDepositProductsTemplate.penaltyToIncomeAccountMappings.forEach(
(penaltyToIncomeAccountMapping: any) => {
const penaltyToIncomeAccountMappingData = this.formBuilder.group({
chargeId: [
penaltyToIncomeAccountMapping.charge.id,
Validators.required
],
incomeAccountId: [
penaltyToIncomeAccountMapping.incomeAccount.id,
Validators.required
]
});
const formArray = this.fixedDepositProductAccountingForm.controls[
'penaltyToIncomeAccountMappings'
] as UntypedFormArray;
formArray.push(penaltyToIncomeAccountMappingData);
}
);
}
}
}
createfixedDepositProductAccountingForm() {
createFixedDepositProductAccountingForm() {
this.fixedDepositProductAccountingForm = this.formBuilder.group({
accountingRule: [1]
accountingRule: [2],
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
incomeOtherGlMappings: this.formBuilder.array([])
});
}
existCharges(): boolean {
return this.chargeData.length > 0;
}
setConditionalControls() {
this.fixedDepositProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => {
if (accountingRule === 2 || accountingRule === 3) {
this.fixedDepositProductAccountingForm.addControl(
'savingsReferenceAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'savingsControlAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'transfersInSuspenseAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'interestOnSavingsAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'incomeFromFeeAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'incomeFromPenaltyAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false));
if (accountingRule === 3) {
this.fixedDepositProductAccountingForm.addControl(
'feesReceivableAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'penaltiesReceivableAccountId',
new UntypedFormControl('', Validators.required)
);
this.fixedDepositProductAccountingForm.addControl(
'interestPayableAccountId',
new UntypedFormControl('', Validators.required)
);
}
this.fixedDepositProductAccountingForm
.get('advancedAccountingRules')
.valueChanges.subscribe((advancedAccountingRules: boolean) => {
if (advancedAccountingRules) {
this.fixedDepositProductAccountingForm.addControl(
'paymentChannelToFundSourceMappings',
this.formBuilder.array([])
);
this.fixedDepositProductAccountingForm.addControl(
'feeToIncomeAccountMappings',
this.formBuilder.array([])
);
this.fixedDepositProductAccountingForm.addControl(
'penaltyToIncomeAccountMappings',
this.formBuilder.array([])
);
} else {
this.fixedDepositProductAccountingForm.removeControl('paymentChannelToFundSourceMappings');
this.fixedDepositProductAccountingForm.removeControl('feeToIncomeAccountMappings');
this.fixedDepositProductAccountingForm.removeControl('penaltyToIncomeAccountMappings');
}
});
} else {
this.fixedDepositProductAccountingForm.removeControl('savingsReferenceAccountId');
this.fixedDepositProductAccountingForm.removeControl('overdraftPortfolioControlId');
this.fixedDepositProductAccountingForm.removeControl('savingsControlAccountId');
this.fixedDepositProductAccountingForm.removeControl('transfersInSuspenseAccountId');
this.fixedDepositProductAccountingForm.removeControl('interestOnSavingsAccountId');
this.fixedDepositProductAccountingForm.removeControl('writeOffAccountId');
this.fixedDepositProductAccountingForm.removeControl('incomeFromFeeAccountId');
this.fixedDepositProductAccountingForm.removeControl('incomeFromPenaltyAccountId');
this.fixedDepositProductAccountingForm.removeControl('incomeFromInterestId');
this.fixedDepositProductAccountingForm.removeControl('advancedAccountingRules');
this.fixedDepositProductAccountingForm.removeControl('escheatLiabilityId');
this.fixedDepositProductAccountingForm.removeControl('feesReceivableAccountId');
this.fixedDepositProductAccountingForm.removeControl('penaltiesReceivableAccountId');
this.fixedDepositProductAccountingForm.removeControl('interestPayableAccountId');
}
private setConditionalControls() {
this.fixedDepositProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => {
this.syncAccountingControls(rule);
});
}
get paymentChannelToFundSourceMappings(): UntypedFormArray {
return this.fixedDepositProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray;
}
get feeToIncomeAccountMappings(): UntypedFormArray {
return this.fixedDepositProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray;
}
get penaltyToIncomeAccountMappings(): UntypedFormArray {
return this.fixedDepositProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray;
}
add(formType: string, formArray: UntypedFormArray) {
const data = { ...this.getData(formType), pristine: false };
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
formArray.push(response.data);
}
private patchAccountingValues(mappings: any) {
this.fixedDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
interestPayableAccountId: mappings.interestPayableAccount?.id || ''
});
}
edit(formType: string, formArray: UntypedFormArray, index: number) {
const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } };
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
formArray.at(index).patchValue(response.data.value);
}
});
}
delete(formArray: UntypedFormArray, index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: this.translateService.instant('labels.text.this') }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
formArray.removeAt(index);
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case 'PaymentFundSource':
return {
title: 'Configure Fund Sources for Payment Channels',
formfields: this.getPaymentFundSourceFormfields(values)
};
case 'FeesIncome':
return { title: 'Map Fees to Income Accounts', formfields: this.getFeesIncomeFormfields(values) };
case 'PenaltyIncome':
return {
title: 'Map Penalties to Specific Income Accounts',
formfields: this.getPenaltyIncomeFormfields(values)
};
if (this.isCashOrAccrualAccounting()) {
const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings;
this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
this.setOtherGlMappings(
'liabilitiesOtherGlMappings',
glMappings?.liabilities || [],
this.liabilityAccountOptions
);
this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
}
}
getPaymentFundSourceFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'paymentTypeId',
label: 'Payment Type',
value: values ? values.paymentTypeId : this.paymentTypeData[0].id,
options: { label: 'name', value: 'id', data: this.paymentTypeData },
required: true,
order: 1
}),
new SelectBase({
controlName: 'fundSourceAccountId',
label: 'Fund Source',
value: values ? values.fundSourceAccountId : this.assetAccountData[0].id,
options: { label: 'name', value: 'id', data: this.assetAccountData },
required: true,
order: 2
})
];
return formfields;
private createOtherGlMappingGroup(otherGlMapping: Partial<OtherGlMappingValue> = {}) {
return this.formBuilder.group({
accountName: [
otherGlMapping.accountName || '',
Validators.required
],
glAccountId: [
otherGlMapping.glAccountId || '',
Validators.required
]
});
}
getFeesIncomeFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'chargeId',
label: 'Fees',
value: values ? values.chargeId : this.chargeData[0].id,
options: { label: 'name', value: 'id', data: this.chargeData },
required: true,
order: 1
}),
new SelectBase({
controlName: 'incomeAccountId',
label: 'Income Account',
value: values ? values.incomeAccountId : this.incomeAccountData[0].id,
options: { label: 'name', value: 'id', data: this.incomeAccountData },
required: true,
order: 2
})
];
return formfields;
getOtherGlMappingsArray(controlName: string): UntypedFormArray {
return this.fixedDepositProductAccountingForm.get(controlName) as UntypedFormArray;
}
getPenaltyIncomeFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'chargeId',
label: 'Penalty',
value: values ? values.chargeId : this.penaltyData[0].id,
options: { label: 'name', value: 'id', data: this.penaltyData },
required: true,
order: 1
}),
new SelectBase({
controlName: 'incomeAccountId',
label: 'Income Account',
value: values ? values.incomeAccountId : this.incomeAccountData[0].id,
options: { label: 'name', value: 'id', data: this.incomeAccountData },
required: true,
order: 2
})
private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) {
const arr = this.getOtherGlMappingsArray(controlName);
arr.clear();
(otherGlMappings || []).forEach((m) => {
arr.push(
this.createOtherGlMappingGroup({
accountName: m.accountName,
glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null
})
);
});
}
addOtherGlMapping(controlName: string) {
this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
}
removeOtherGlMapping(controlName: string, index: number) {
this.getOtherGlMappingsArray(controlName).removeAt(index);
}
getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl {
return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl;
}
private clearOtherGlMappings() {
[
'assetsOtherGlMappings',
'liabilitiesOtherGlMappings',
'expensesOtherGlMappings',
'incomeOtherGlMappings'
].forEach((c) => this.getOtherGlMappingsArray(c).clear());
}
private getOtherGlMappings() {
const map = (controlName: string, options: GLAccount[]) =>
this.getOtherGlMappingsArray(controlName)
.value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId)
.map((item: OtherGlMappingValue) => ({
accountName: item.accountName,
glCode: options.find((o) => o.id === item.glAccountId)?.glCode || ''
}))
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
const otherGlMappings = {
assets: map('assetsOtherGlMappings', this.assetAccountOptions),
liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
expenses: map('expensesOtherGlMappings', this.expenseAccountOptions),
income: map('incomeOtherGlMappings', this.incomeAccountOptions)
};
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
}
private syncAccountingControls(accountingRule: number) {
const addControl = (name: string, required = false) => {
if (!this.fixedDepositProductAccountingForm.contains(name)) {
this.fixedDepositProductAccountingForm.addControl(
name,
new UntypedFormControl('', required ? Validators.required : undefined)
);
} else if (required) {
this.fixedDepositProductAccountingForm.get(name)?.setValidators(Validators.required);
this.fixedDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false });
}
};
const removeControl = (name: string) => {
if (this.fixedDepositProductAccountingForm.contains(name)) {
this.fixedDepositProductAccountingForm.removeControl(name);
}
};
if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) {
addControl('savingsReferenceAccountId', true);
addControl('savingsControlAccountId', true);
addControl('transfersInSuspenseAccountId', true);
addControl('interestOnSavingsAccountId', true);
addControl('incomeFromFeeAccountId', true);
addControl('incomeFromPenaltyAccountId', true);
if (this.accounting.isAccrualAccountingRuleId(accountingRule)) {
addControl('feesReceivableAccountId', true);
addControl('penaltiesReceivableAccountId', true);
addControl('interestPayableAccountId', true);
} else {
removeControl('feesReceivableAccountId');
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
}
} else {
[
'savingsReferenceAccountId',
'savingsControlAccountId',
'transfersInSuspenseAccountId',
'interestOnSavingsAccountId',
'incomeFromFeeAccountId',
'incomeFromPenaltyAccountId',
'feesReceivableAccountId',
'penaltiesReceivableAccountId',
'interestPayableAccountId'
].forEach(removeControl);
this.clearOtherGlMappings();
}
}
buildAccountingSectionConfigs() {
this.accountingSectionConfigs = [
{
key: 'assets',
title: 'Assets',
pillLabel: 'Asset',
pillClass: 'green',
rows: [
{
key: 'savingsReferenceAccountId',
label: 'Savings Reference Account',
inputLabel: 'Savings reference account',
controlName: 'savingsReferenceAccountId',
options: this.assetAccountOptions,
required: true
},
{
key: 'feesReceivableAccountId',
label: 'Fees Receivable Account',
inputLabel: 'Fees receivable account',
controlName: 'feesReceivableAccountId',
options: this.assetAccountOptions,
accrualOnly: true
},
{
key: 'penaltiesReceivableAccountId',
label: 'Penalties Receivable Account',
inputLabel: 'Penalties receivable account',
controlName: 'penaltiesReceivableAccountId',
options: this.assetAccountOptions,
accrualOnly: true
}
],
otherMappingsControlName: 'assetsOtherGlMappings',
otherAccountOptions: this.assetAccountOptions
},
{
key: 'liabilities',
title: 'Liabilities',
pillLabel: 'Liability',
pillClass: 'green',
rows: [
{
key: 'savingsControlAccountId',
label: 'Savings Control Account',
inputLabel: 'Savings control account',
controlName: 'savingsControlAccountId',
options: this.liabilityAccountOptions,
required: true
},
{
key: 'transfersInSuspenseAccountId',
label: 'Transfers In Suspense Account',
inputLabel: 'Transfers in suspense account',
controlName: 'transfersInSuspenseAccountId',
options: this.liabilityAccountOptions,
required: true
},
{
key: 'interestPayableAccountId',
label: 'Interest Payable Account',
inputLabel: 'Interest payable account',
controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions,
accrualOnly: true
}
],
otherMappingsControlName: 'liabilitiesOtherGlMappings',
otherAccountOptions: this.liabilityAccountOptions
},
{
key: 'expenses',
title: 'Expenses',
pillLabel: 'Expense',
pillClass: 'amber',
rows: [
{
key: 'interestOnSavingsAccountId',
label: 'Interest On Savings Account',
inputLabel: 'Interest on savings account',
controlName: 'interestOnSavingsAccountId',
options: this.expenseAccountOptions,
required: true
}
],
otherMappingsControlName: 'expensesOtherGlMappings',
otherAccountOptions: this.expenseAccountOptions
},
{
key: 'income',
title: 'Income',
pillLabel: 'Income',
pillClass: 'blue',
rows: [
{
key: 'incomeFromFeeAccountId',
label: 'Income From Fees',
inputLabel: 'Income from fees',
controlName: 'incomeFromFeeAccountId',
options: this.incomeAccountOptions,
required: true
},
{
key: 'incomeFromPenaltyAccountId',
label: 'Income From Penalties',
inputLabel: 'Income from penalties',
controlName: 'incomeFromPenaltyAccountId',
options: this.incomeAccountOptions,
required: true
}
],
otherMappingsControlName: 'incomeOtherGlMappings',
otherAccountOptions: this.incomeAccountOptions
}
];
return formfields;
}
get fixedDepositProductAccounting() {
return this.fixedDepositProductAccountingForm.value;
const {
assetsOtherGlMappings,
liabilitiesOtherGlMappings,
expensesOtherGlMappings,
incomeOtherGlMappings,
...accounting
} = this.fixedDepositProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings();
return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) };
}
isCashOrAccrualAccounting(): boolean {
@@ -8,22 +8,21 @@
<div class="fdpch-section">
<div class="fdpch-section__header">
<i class="fas fa-money-check-alt"></i>
<h3>{{ 'labels.inputs.Charge' | translate }}</h3>
<i class="fa fa-file-invoice-dollar"></i>
<h3>{{ 'labels.heading.Charges' | translate }}</h3>
</div>
<div class="fdpch-grid">
<mat-form-field appearance="outline" class="fdpch-field">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select #charge>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
<mat-option [value]="charge">
{{ charge.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="fdpch-field fdpch-btn-wrap">
<div class="fdpch-section__body">
<div class="fdpch-add-row">
<mat-form-field appearance="outline" class="fdpch-add-row__select">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select #charge>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
<mat-option [value]="charge">
{{ charge.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<button
type="button"
mat-raised-button
@@ -36,51 +35,51 @@
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargesDataSource.length > 0) {
<div class="fdpch-table-wrapper">
<table mat-table [dataSource]="chargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div>
@if (chargesDataSource.length > 0) {
<div class="fdpch-table-wrapper">
<table mat-table [dataSource]="chargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div>
@@ -12,10 +12,6 @@
overflow: hidden;
background: #fff;
& + & {
margin-top: 16px;
}
&__header {
display: flex;
align-items: center;
@@ -36,30 +32,22 @@
color: #202124;
}
}
}
.fdpch-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
&__body {
padding: 16px 24px;
}
}
.fdpch-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.fdpch-btn-wrap {
.fdpch-add-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
&__select {
flex: 1;
min-width: 200px;
}
}
.fdpch-add-btn {
@@ -73,7 +61,7 @@
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin: 0 24px 16px;
margin-top: 12px;
table {
width: 100%;
@@ -10,60 +10,102 @@
<div class="fdpc-section">
<div class="fdpc-section__header">
<i class="fas fa-coins"></i>
<h3>{{ 'labels.inputs.Currency' | translate }}</h3>
<span>Currency &amp; Account Status Configuration</span>
</div>
<div class="fdpc-grid">
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<mat-select formControlName="currencyCode" required>
@for (currency of currencyData; track currency) {
<mat-option [value]="currency.code">
{{ currency.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
formControlName="digitsAfterDecimal"
required
/>
<mat-error *ngIf="fixedDepositProductCurrencyForm.get('digitsAfterDecimal')?.hasError('required')">
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
<mat-error *ngIf="fixedDepositProductCurrencyForm.get('digitsAfterDecimal')?.hasError('min')">
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.Minimum Value must be' | translate }}
<strong>0</strong>
</mat-error>
</mat-form-field>
<div class="fdpc-field fdpc-field--full fdpc-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="setMultiples">
{{ 'labels.inputs.Set the fixed deposit installment in multiples of' | translate }}
</mat-checkbox>
</div>
@if (fixedDepositProductCurrencyForm.value.setMultiples) {
<div class="fdpc-body">
<div class="fdpc-grid">
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Currency in multiples of' | translate }}</mat-label>
<input type="number" min="1" matInput formControlName="inMultiplesOf" />
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<mat-select
formControlName="currencyCode"
matTooltip="{{ 'tooltips.The currency in which the deposit will be disbursed' | translate }}"
required
>
@for (currency of currencyData; track currency) {
<mat-option [value]="currency.code">
{{ currency.name }}
</mat-option>
}
</mat-select>
<mat-hint>Default: Indian Rupee - auto-set for domestic products</mat-hint>
<mat-error>
{{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }}
{{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<mat-select
formControlName="digitsAfterDecimal"
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
required
>
@for (places of decimalPlacesData; track places) {
<mat-option [value]="places">{{ places }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="dormancyAlert">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="balanceSwapOption">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="msme">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Nomination Facility</h4>
<p>Allow account holders to designate individuals to receive funds in the event of account holder's death.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="nominationFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="autoDebitFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
</form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.fdpc-section {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
@@ -21,23 +27,27 @@
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: #0ea5e9;
color: $accent;
}
h3 {
span {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
color: $gray-900;
}
}
}
.fdpc-body {
background: #fff;
}
.fdpc-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -51,14 +61,43 @@
.fdpc-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.fdpc-checkbox-row {
.fdpc-alert {
display: flex;
align-items: center;
padding: 4px 0;
justify-content: space-between;
gap: 24px;
padding: 18px 24px 24px;
border-top: 1px solid $border;
background: #f8fafc;
@media (width <= 760px) {
flex-direction: column;
align-items: flex-start;
}
&__content {
h4 {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: #64748b;
max-width: 540px;
}
}
&__choices {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
}
}
@@ -6,12 +6,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { startWith } from 'rxjs/operators';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -21,18 +19,23 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
MatCheckbox
MatRadioGroup,
MatRadioButton
]
})
export class FixedDepositProductCurrencyStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private destroyRef = inject(DestroyRef);
@Input() fixedDepositProductsTemplate: any;
fixedDepositProductCurrencyForm: UntypedFormGroup;
currencyData: any;
decimalPlacesData = [
2,
0,
3
];
constructor() {
this.createFixedDepositProductCurrencyForm();
@@ -42,13 +45,17 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
this.currencyData = this.fixedDepositProductsTemplate.currencyOptions;
this.fixedDepositProductCurrencyForm.patchValue({
currencyCode: this.fixedDepositProductsTemplate.currency?.code || this.currencyData[0].code,
digitsAfterDecimal: this.fixedDepositProductsTemplate.digitsAfterDecimal ?? '',
setMultiples: !!this.fixedDepositProductsTemplate.inMultiplesOf,
inMultiplesOf: this.fixedDepositProductsTemplate.inMultiplesOf ?? ''
currencyCode: this.fixedDepositProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
digitsAfterDecimal:
this.fixedDepositProductsTemplate.digitsAfterDecimal ??
this.fixedDepositProductsTemplate.currency?.decimalPlaces ??
2,
dormancyAlert: this.fixedDepositProductsTemplate.dormancyAlert ?? false,
balanceSwapOption: this.fixedDepositProductsTemplate.balanceSwapOption ?? false,
msme: this.fixedDepositProductsTemplate.msme ?? false,
nominationFacility: this.fixedDepositProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.fixedDepositProductsTemplate.autoDebitFacility ?? false
});
this.setupConditionalValidation();
}
createFixedDepositProductCurrencyForm() {
@@ -58,48 +65,30 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
Validators.required
],
digitsAfterDecimal: [
'',
2,
[
Validators.required,
Validators.min(0)
]
],
setMultiples: [false],
inMultiplesOf: ['']
dormancyAlert: [false],
balanceSwapOption: [false],
msme: [false],
nominationFacility: [false],
autoDebitFacility: [false]
});
}
setupConditionalValidation() {
const inMultiplesOfControl = this.fixedDepositProductCurrencyForm.get('inMultiplesOf');
const setMultiplesControl = this.fixedDepositProductCurrencyForm.get('setMultiples');
setMultiplesControl?.valueChanges
.pipe(startWith(setMultiplesControl.value), takeUntilDestroyed(this.destroyRef))
.subscribe((checked) => {
if (checked) {
inMultiplesOfControl?.setValidators([
Validators.required,
Validators.min(1)
]);
} else {
inMultiplesOfControl?.clearValidators();
inMultiplesOfControl?.setValue('');
}
inMultiplesOfControl?.updateValueAndValidity();
});
}
get fixedDepositProductCurrency() {
const formValue = this.fixedDepositProductCurrencyForm.value;
const result: any = {
return {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal
digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
};
if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) {
result.inMultiplesOf = formValue.inMultiplesOf;
}
return result;
}
}
@@ -9,13 +9,18 @@
<form [formGroup]="fixedDepositProductDetailsForm">
<div class="fdpd-section">
<div class="fdpd-section__header">
<i class="fas fa-info-circle"></i>
<i class="fa fa-info-circle"></i>
<h3>{{ 'labels.inputs.Product Name' | translate }} &amp; {{ 'labels.inputs.Description' | translate }}</h3>
</div>
<div class="fdpd-grid">
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<input matInput formControlName="name" required />
<input
matInput
formControlName="name"
matTooltip="{{ 'tooltips.Product name is a unique identifier' | translate }}"
required
/>
@if (fixedDepositProductDetailsForm.controls.name.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
@@ -26,7 +31,13 @@
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label>
<input matInput formControlName="shortName" maxlength="4" required />
<input
matInput
formControlName="shortName"
maxlength="4"
matTooltip="{{ 'tooltips.Short name is a unique identifier' | translate }}"
required
/>
@if (fixedDepositProductDetailsForm.controls.shortName.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }}
@@ -37,14 +48,80 @@
<mat-form-field appearance="outline" class="fdpd-field fdpd-field--full">
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
<textarea matInput formControlName="description" required></textarea>
@if (fixedDepositProductDetailsForm.controls.description.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
<textarea
matInput
formControlName="description"
matTooltip="{{
'tooltips.Description is used to provide additional information regarding the purpose' | translate
}}"
cdkTextareaAutosize
cdkAutosizeMinRows="2"
></textarea>
</mat-form-field>
</div>
</div>
<div class="fdpd-section">
<div class="fdpd-section__header">
<i class="fa fa-tags"></i>
<h3>Product Classification</h3>
</div>
<div class="fdpd-grid fdpd-grid--classification">
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Account Type</mat-label>
<mat-select formControlName="accountType" required>
@for (accountType of accountTypeData; track accountType) {
<mat-option [value]="accountType">{{ accountType }}</mat-option>
}
</mat-select>
<mat-error>Account Type <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Customer Segment</mat-label>
<mat-select formControlName="customerSegment">
@for (customerSegment of customerSegmentData; track customerSegment) {
<mat-option [value]="customerSegment">{{ customerSegment }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>GL Account (Liability)</mat-label>
<mat-select formControlName="glAccountId" [compareWith]="compareById">
@for (account of liabilityAccountOptions; track account.id) {
<mat-option [value]="account.id">{{ account.name }} ({{ account.glCode }})</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field" (click)="effectiveToPicker.open()">
<mat-label>Effective To</mat-label>
<input matInput [matDatepicker]="effectiveToPicker" formControlName="effectiveTo" />
<mat-datepicker-toggle matSuffix [for]="effectiveToPicker"></mat-datepicker-toggle>
<mat-datepicker #effectiveToPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field" (click)="endDatePicker.open()">
<mat-label>End Date</mat-label>
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" />
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Introducer Required</mat-label>
<mat-select formControlName="introducerRequired">
@for (option of yesNoOptions; track option.value) {
<mat-option [value]="option.value">{{ option.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<div class="fdpd-field fdpd-field--full fdpd-checkbox-row">
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
</div>
</div>
</div>
</form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.fdpd-section {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
@@ -21,19 +27,19 @@
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: #0ea5e9;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
color: $gray-900;
}
}
}
@@ -49,6 +55,18 @@
}
}
.fdpd-grid--classification {
grid-template-columns: repeat(3, 1fr);
@media (width <= 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.fdpd-field {
width: 100%;
@@ -56,3 +74,9 @@
grid-column: 1 / -1;
}
}
.fdpd-checkbox-row {
display: flex;
align-items: center;
min-height: 56px;
}
@@ -6,8 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { MatTooltip } from '@angular/material/tooltip';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -15,27 +19,41 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
templateUrl: './fixed-deposit-product-details-step.component.html',
styleUrls: ['./fixed-deposit-product-details-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
...STANDALONE_SHARED_IMPORTS,
CdkTextareaAutosize,
MatTooltip
]
})
export class FixedDepositProductDetailsStepComponent implements OnInit {
export class FixedDepositProductDetailsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private cdr = inject(ChangeDetectorRef);
@Input() fixedDepositProductsTemplate: any;
fixedDepositProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = [];
customerSegmentData: string[] = [];
liabilityAccountOptions: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() {
this.createFixedDepositProductDetailsForm();
}
ngOnInit() {
if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) {
this.fixedDepositProductDetailsForm.patchValue({
name: this.fixedDepositProductsTemplate.name,
shortName: this.fixedDepositProductsTemplate.shortName,
description: this.fixedDepositProductsTemplate.description
});
if (this.fixedDepositProductsTemplate) {
this.applyTemplate(this.fixedDepositProductsTemplate);
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fixedDepositProductsTemplate?.currentValue) {
this.applyTemplate(changes.fixedDepositProductsTemplate.currentValue);
}
}
@@ -49,14 +67,66 @@ export class FixedDepositProductDetailsStepComponent implements OnInit {
'',
Validators.required
],
description: [
'',
description: [''],
accountType: [
null,
Validators.required
]
],
customerSegment: [null],
glAccountId: [null],
effectiveTo: [''],
endDate: [''],
introducerRequired: [false],
shareLinkingEnabled: [false]
});
}
private getOptionNames(options: any[]): string[] {
if (!Array.isArray(options)) return [];
return options.map((o) => o?.name ?? o?.value ?? o).filter((v) => typeof v === 'string' && v.trim().length > 0);
}
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
if (dateValue instanceof Date) return dateValue;
return new Date(dateValue);
}
compareById(o1: any, o2: any): boolean {
return o1 != null && o2 != null && Number(o1) === Number(o2);
}
private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions);
this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? [];
this.cdr.detectChanges();
const isEditing = !!template.id;
this.fixedDepositProductDetailsForm.patchValue({
name: isEditing ? template.name : template.name || '',
shortName: isEditing ? template.shortName : template.shortName || '',
description: template.description || '',
accountType: template.accountType ?? this.accountTypeData[0] ?? null,
customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null,
glAccountId: template.glAccountId ?? null,
effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate),
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
shareLinkingEnabled: !!template.shareLinkingEnabled
});
}
get fixedDepositProductDetails() {
return this.fixedDepositProductDetailsForm.value;
const formValue = { ...this.fixedDepositProductDetailsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveTo instanceof Date) {
formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || '';
}
if (formValue.endDate instanceof Date) {
formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || '';
}
return formValue;
}
}
@@ -7,204 +7,124 @@
-->
<form [formGroup]="fixedDepositProductSettingsForm">
<!-- Premature option -->
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-lock"></i>
<h3>{{ 'labels.inputs.Lock-in Period' | translate }}</h3>
<span class="fdps-section__badge"><i class="fas fa-balance-scale"></i></span>
<span>Premature option</span>
</div>
<div class="fdps-grid">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="lockinPeriodFrequency"
matTooltip="{{ 'tooltips.A period of time during which a fixed deposit account' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="fdps-body">
<div class="fdps-premature-row">
<mat-checkbox formControlName="loanFacility">Loan Facility</mat-checkbox>
<mat-checkbox formControlName="prematureWithdrawalOption">Premature Withdrawal option</mat-checkbox>
<mat-checkbox formControlName="tenureFlexibility">Tenure Flexibility</mat-checkbox>
<mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
<mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
</div>
</div>
</div>
<!-- Lock-in Period -->
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-calendar-check"></i>
<h3>{{ 'labels.inputs.Minimum Deposit Term' | translate }}</h3>
<span class="fdps-section__badge"><i class="fas fa-lock"></i></span>
<span>{{ 'labels.inputs.Lock-in Period' | translate }}</span>
</div>
<div class="fdps-grid">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="minDepositTerm"
required
matTooltip="{{ 'tooltips.The minimum length of time' | translate }}"
/>
<mat-error>
{{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency Type' | translate }}</mat-label>
<mat-select formControlName="minDepositTermTypeId" required>
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-layer-group"></i>
<h3>{{ 'labels.heading.And thereafter, in Multiples of' | translate }}</h3>
</div>
<div class="fdps-grid">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="inMultiplesOfDepositTerm"
matTooltip="{{ 'tooltips.After the minimum deposit term has passed' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="inMultiplesOfDepositTermTypeId">
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-calendar-alt"></i>
<h3>{{ 'labels.inputs.Maximum Deposit Term' | translate }}</h3>
</div>
<div class="fdps-grid">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="maxDepositTerm"
matTooltip="{{ 'tooltips.The maximum length of time funds' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="maxDepositTermTypeId">
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-exclamation-triangle"></i>
<h3>{{ 'labels.heading.For Pre-mature closure' | translate }}</h3>
</div>
<div class="fdps-grid">
<div class="fdps-field fdps-field--full fdps-checkbox-row">
<mat-checkbox
labelPosition="before"
formControlName="preClosurePenalApplicable"
matTooltip="{{ 'tooltips.This sections defines an interest penalty' | translate }}"
>
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
<div class="fdps-body">
<div class="fdps-lockin-row">
<span>Enable Lock-in Period</span>
<mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
</div>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label>
<input type="number" min="0" matInput formControlName="preClosurePenalInterest" />
</mat-form-field>
@if (fixedDepositProductSettingsForm.value.enableLockinPeriod) {
<div class="fdps-lockin-fields">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
matInput
formControlName="lockinPeriodFrequency"
min="1"
step="1"
matTooltip="{{ 'tooltips.A period of time during which a fixed deposit account' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId">
@for (
preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData;
track preClosurePenalInterestOnType
) {
<mat-option [value]="preClosurePenalInterestOnType.id">
{{ preClosurePenalInterestOnType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-file-invoice-dollar"></i>
<h3>{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}</h3>
</div>
<div class="fdps-grid">
<div class="fdps-field fdps-field--full fdps-checkbox-row">
<mat-checkbox
labelPosition="before"
formControlName="withHoldTax"
matTooltip="{{ 'tooltips.An boolean flag to attach' | translate }}"
>
{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}
</mat-checkbox>
</div>
@if (fixedDepositProductSettingsForm.value.withHoldTax) {
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Tax Group' | translate }}</mat-label>
<mat-select formControlName="taxGroupId" required>
@for (taxGroup of taxGroupData; track taxGroup) {
<mat-option [value]="taxGroup.id">
{{ taxGroup.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Tax Group' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div>
</div>
<!-- Maturity Instructions -->
<div class="fdps-section">
<div class="fdps-section__header">
<span class="fdps-section__badge"><i class="fas fa-lock"></i></span>
<span>Maturity Instructions</span>
</div>
<div class="fdps-body fdps-body--list">
<div class="fdps-list-row">
<span>Maturity Instructions</span>
<mat-checkbox formControlName="maturityInstructions"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span
>Option to credit maturity proceeds directly to the linked account or reinvest in another product (like
FD).</span
>
<mat-checkbox formControlName="creditMaturityProceeds"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Penalty / Grace Period for Missed Installments</span>
<mat-checkbox formControlName="penaltyGracePeriodMissedInstallments"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Panel Request Applicable</span>
<mat-checkbox formControlName="panelRequestApplicable"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>No penalty for nominee / legal heir closure</span>
<mat-checkbox formControlName="noPenaltyNomineeHeirClosure"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Deceased Settlement</span>
<mat-checkbox formControlName="deceasedSettlement"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Allow Debit Txn after due date</span>
<mat-checkbox formControlName="allowDebitTxnAfterDueDate"></mat-checkbox>
</div>
</div>
</div>
<!-- Additional Rules -->
<div class="fdps-section">
<div class="fdps-section__header">
<span class="fdps-section__badge"><i class="fas fa-cog"></i></span>
<span>Additional Rules</span>
</div>
<div class="fdps-body">
<div class="fdps-layout fdps-layout--additional">
<div class="fdps-toggle fdps-toggle--inline">
<mat-checkbox formControlName="accountClosedZeroBalanceInDayClosing">
<span>Account Closed with Zero balance in Day Closing</span>
</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>Balance Required for Interest Calculation</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
</form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.fdps-section {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
@@ -21,32 +27,41 @@
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: #0ea5e9;
color: $accent;
}
h3 {
span:not(.fdps-section__badge) {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
color: $gray-900;
}
}
}
.fdps-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
.fdps-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
.fdps-body {
background: #fff;
padding: 18px 24px 24px;
}
.fdps-body--list {
padding: 0 24px;
}
.fdps-field {
@@ -55,10 +70,91 @@
&--full {
grid-column: 1 / -1;
}
&--compact {
width: 200px;
min-width: 150px;
}
}
.fdps-checkbox-row {
.fdps-premature-row {
display: flex;
align-items: center;
padding: 4px 0;
gap: 24px;
flex-wrap: wrap;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
}
.fdps-list-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 0;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
&:last-child {
border-bottom: none;
}
span {
max-width: 85%;
line-height: 1.4;
}
}
.fdps-lockin-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
}
.fdps-lockin-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
@media (width <= 640px) {
grid-template-columns: 1fr;
}
}
.fdps-layout {
display: grid;
gap: 16px;
}
.fdps-layout--additional {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.fdps-toggle {
display: flex;
align-items: center;
min-height: 40px;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
&--inline {
min-height: auto;
}
}
@@ -7,13 +7,7 @@
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
UntypedFormControl,
Validators,
ReactiveFormsModule
} from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -36,113 +30,71 @@ export class FixedDepositProductSettingsStepComponent implements OnInit {
fixedDepositProductSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any;
periodFrequencyTypeData: any;
preClosurePenalInterestOnTypeData: any;
taxGroupData: any;
constructor() {
this.createFixedDepositProductSettingsForm();
this.setConditionalControls();
}
ngOnInit() {
this.lockinPeriodFrequencyTypeData = this.fixedDepositProductsTemplate.lockinPeriodFrequencyTypeOptions;
this.periodFrequencyTypeData = this.fixedDepositProductsTemplate.periodFrequencyTypeOptions.slice(0, -1);
this.preClosurePenalInterestOnTypeData = this.fixedDepositProductsTemplate.preClosurePenalInterestOnTypeOptions;
this.taxGroupData = this.fixedDepositProductsTemplate.taxGroupOptions;
if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) {
if (this.fixedDepositProductsTemplate?.id) {
this.fixedDepositProductSettingsForm.patchValue({
isMandatoryDeposit: this.fixedDepositProductsTemplate.isMandatoryDeposit,
adjustAdvanceTowardsFuturePayments: this.fixedDepositProductsTemplate.adjustAdvanceTowardsFuturePayments,
allowWithdrawal: this.fixedDepositProductsTemplate.allowWithdrawal,
lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency,
lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType
? this.fixedDepositProductsTemplate.lockinPeriodFrequencyType.id
: '',
minDepositTerm: this.fixedDepositProductsTemplate.minDepositTerm,
minDepositTermTypeId: this.fixedDepositProductsTemplate.minDepositTermType
? this.fixedDepositProductsTemplate.minDepositTermType.id
: '',
inMultiplesOfDepositTerm: this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm,
inMultiplesOfDepositTermTypeId: this.fixedDepositProductsTemplate.inMultiplesOfDepositTermType
? this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm.id
: '',
maxDepositTerm: this.fixedDepositProductsTemplate.maxDepositTerm,
maxDepositTermTypeId: this.fixedDepositProductsTemplate.maxDepositTermType
? this.fixedDepositProductsTemplate.minDepositTermType.id
: '',
preClosurePenalApplicable: this.fixedDepositProductsTemplate.preClosurePenalApplicable,
preClosurePenalInterest: this.fixedDepositProductsTemplate.preClosurePenalInterest,
preClosurePenalInterestOnTypeId: this.fixedDepositProductsTemplate.preClosurePenalInterestOnType
? this.fixedDepositProductsTemplate.preClosurePenalInterestOnType.id
: '',
withHoldTax: this.fixedDepositProductsTemplate.withHoldTax
});
}
if (this.fixedDepositProductsTemplate.withHoldTax) {
this.fixedDepositProductSettingsForm.patchValue({
taxGroupId: this.fixedDepositProductsTemplate.taxGroup ? this.fixedDepositProductsTemplate.taxGroup.id : ''
loanFacility: this.fixedDepositProductsTemplate.loanFacility ?? false,
prematureWithdrawalOption: this.fixedDepositProductsTemplate.prematureWithdrawalOption ?? false,
tenureFlexibility: this.fixedDepositProductsTemplate.tenureFlexibility ?? false,
lienMarked: this.fixedDepositProductsTemplate.lienMarked ?? false,
withdrawalFeeForTransfers: this.fixedDepositProductsTemplate.withdrawalFeeForTransfers ?? false,
enableLockinPeriod: this.fixedDepositProductsTemplate.enableLockinPeriod ?? false,
lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency ?? '',
lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '',
maturityInstructions: this.fixedDepositProductsTemplate.maturityInstructions ?? false,
creditMaturityProceeds: this.fixedDepositProductsTemplate.creditMaturityProceeds ?? false,
penaltyGracePeriodMissedInstallments:
this.fixedDepositProductsTemplate.penaltyGracePeriodMissedInstallments ?? false,
panelRequestApplicable: this.fixedDepositProductsTemplate.panelRequestApplicable ?? false,
noPenaltyNomineeHeirClosure: this.fixedDepositProductsTemplate.noPenaltyNomineeHeirClosure ?? false,
deceasedSettlement: this.fixedDepositProductsTemplate.deceasedSettlement ?? false,
allowDebitTxnAfterDueDate: this.fixedDepositProductsTemplate.allowDebitTxnAfterDueDate ?? false,
accountClosedZeroBalanceInDayClosing:
this.fixedDepositProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false,
minBalanceForInterestCalculation: this.fixedDepositProductsTemplate.minBalanceForInterestCalculation ?? ''
});
}
}
createFixedDepositProductSettingsForm() {
this.fixedDepositProductSettingsForm = this.formBuilder.group({
loanFacility: [false],
prematureWithdrawalOption: [false],
tenureFlexibility: [false],
lienMarked: [false],
withdrawalFeeForTransfers: [false],
enableLockinPeriod: [false],
lockinPeriodFrequency: [
'',
Validators.min(0)
],
lockinPeriodFrequencyType: [''],
minDepositTerm: [
'',
[
Validators.required,
Validators.min(0)
]
],
minDepositTermTypeId: [
'',
Validators.required
],
inMultiplesOfDepositTerm: [
'',
Validators.min(0)
],
inMultiplesOfDepositTermTypeId: [''],
maxDepositTerm: [
'',
Validators.min(0)
],
maxDepositTermTypeId: [''],
preClosurePenalApplicable: [false],
preClosurePenalInterest: [
'',
Validators.min(0)
],
preClosurePenalInterestOnTypeId: [''],
withHoldTax: [false]
});
}
setConditionalControls() {
this.fixedDepositProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => {
if (withHoldTax) {
this.fixedDepositProductSettingsForm.addControl('taxGroupId', new UntypedFormControl('', Validators.required));
} else {
this.fixedDepositProductSettingsForm.removeControl('taxGroupId');
}
maturityInstructions: [false],
creditMaturityProceeds: [false],
penaltyGracePeriodMissedInstallments: [false],
panelRequestApplicable: [false],
noPenaltyNomineeHeirClosure: [false],
deceasedSettlement: [false],
allowDebitTxnAfterDueDate: [false],
accountClosedZeroBalanceInDayClosing: [false],
minBalanceForInterestCalculation: ['']
});
}
get fixedDepositProductSettings() {
const fixedDepositProductSettings = this.fixedDepositProductSettingsForm.value;
for (const key in fixedDepositProductSettings) {
if (fixedDepositProductSettings[key] === '') {
delete fixedDepositProductSettings[key];
const settings = { ...this.fixedDepositProductSettingsForm.value };
for (const key in settings) {
if (settings[key] === '') {
delete settings[key];
}
}
return fixedDepositProductSettings;
return settings;
}
}
@@ -7,139 +7,148 @@
-->
<form [formGroup]="fixedDepositProductTermsForm">
<!-- Interest Rate & Posting Terms -->
<div class="fdpt-section">
<div class="fdpt-section__header">
<i class="fas fa-wallet"></i>
<h3>{{ 'labels.inputs.Deposit Amount' | translate }}</h3>
<span class="fdpt-section__badge"><i class="fas fa-chart-line"></i></span>
<span>Interest Rate &amp; Posting Terms</span>
</div>
<div class="fdpt-grid">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="minDepositAmount"
matTooltip="{{ 'tooltips.The minimum deposit amount required to open a fixed deposit' | translate }}"
/>
</mat-form-field>
<div class="fdpt-body">
<div class="fdpt-banner">
<span class="fdpt-banner__icon">ℹ️</span>
<span>
Fixed Deposit base rate:
<b>{{ fixedDepositProductTermsForm.get('nominalAnnualInterestRate').value }}%</b>
</span>
</div>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="depositAmount"
matTooltip="{{ 'tooltips.The default deposit amount expected' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Default' | translate }} {{ 'labels.inputs.Deposit Amount' | translate }}
{{ 'labels.commons.is' | translate }} <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="maxDepositAmount"
matTooltip="{{ 'tooltips.The maximum deposit amount allowed when a fixed deposit' | translate }}"
/>
</mat-form-field>
</div>
</div>
<div class="fdpt-section">
<div class="fdpt-section__header">
<i class="fas fa-percentage"></i>
<h3>{{ 'labels.inputs.Interest Compounding Period' | translate }}</h3>
</div>
<div class="fdpt-grid">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label>
<mat-select
formControlName="interestCompoundingPeriodType"
matTooltip="{{ 'tooltips.The period at which interest rate' | translate }}"
required
>
@for (
interestCompoundingPeriodType of interestCompoundingPeriodTypeData;
track interestCompoundingPeriodType
) {
<mat-option [value]="interestCompoundingPeriodType.id">
{{ interestCompoundingPeriodType.value }}
</mat-option>
<div class="fdpt-grid fdpt-grid--three">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
<input
type="number"
matInput
matTooltip="{{ 'tooltips.The default interest rate set' | translate }}"
formControlName="nominalAnnualInterestRate"
required
min="0"
step="0.01"
/>
@if (fixedDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) {
<mat-error>
{{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select
formControlName="interestPostingPeriodType"
matTooltip="{{ 'tooltips.The period at which interest rate is posted or credited' | translate }}"
required
>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value }}
</mat-option>
@if (fixedDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) {
<mat-error>{{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater</mat-error>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculated using' | translate }}</mat-label>
<mat-select
formControlName="interestCalculationType"
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
required
>
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) {
<mat-option [value]="interestCalculationType.id">
{{ interestCalculationType.value }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Effective Date</mat-label>
<input matInput [matDatepicker]="effectiveDatePicker" formControlName="effectiveDate" />
<mat-datepicker-toggle matSuffix [for]="effectiveDatePicker"></mat-datepicker-toggle>
<mat-datepicker #effectiveDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label>
<mat-select
formControlName="interestCalculationDaysInYearType"
matTooltip="{{ 'tooltips.The setting for number of days' | translate }}"
required
>
@for (
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData;
track interestCalculationDaysInYearType
) {
<mat-option [value]="interestCalculationDaysInYearType.id">
{{ interestCalculationDaysInYearType.value }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select
formControlName="interestCalculateSelector"
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
>
@for (opt of interestCalculationTypeOptionData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="fdpt-grid fdpt-grid--three">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Interest Calculate</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (mode of interestCalculationTypeData; track mode.id) {
<mat-option [value]="mode.id">{{ mode.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (fixedDepositProductTermsForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod">
@for (period of periodFrequencyData; track period.id) {
<mat-option [value]="period.value">{{ period.value }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select
formControlName="interestPostingPeriodType"
matTooltip="{{ 'tooltips.Interest rate is posted or credited to a saving account' | translate }}"
required
>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="fdpt-footer">
<div class="fdpt-footer__grid">
<div class="fdpt-toggle-card">
<div>
<h4>Auto Interest Post</h4>
</div>
<div class="fdpt-toggle-card__choices">
@for (option of yesNoOptions; track option.value) {
<label class="inline-flex items-center cursor-pointer">
<input type="radio" class="w-4 h-4" formControlName="autoPost" [value]="option.value" />
<span class="ml-2 text-sm">{{ option.label }}</span>
</label>
}
</div>
</div>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Frequency (Fix Day)</mat-label>
<input type="number" matInput formControlName="frequencyFixDay" />
<mat-hint>Days</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Last Interest Date</mat-label>
<input matInput [matDatepicker]="lastInterestDatePicker" formControlName="lastInterestDate" />
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
</div>
@if (fixedDepositProductTermsForm.value.interestCalculationType === 1) {
<div class="fdpt-note">
<span>⚠️</span>
<span
>Daily product-based interest accrual enabled - interest calculated EOD on daily closing balance.</span
>
</div>
}
</div>
</div>
</div>
</form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.fdpt-section {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
@@ -21,31 +27,54 @@
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: #0ea5e9;
color: $accent;
}
h3 {
span:not(.fdpt-section__badge) {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
color: $gray-900;
}
}
}
.fdpt-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.fdpt-body {
background: #fff;
}
.fdpt-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
&--three {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
&--three {
grid-template-columns: 1fr;
}
}
}
@@ -56,3 +85,67 @@
grid-column: 1 / -1;
}
}
.fdpt-footer {
padding: 0 24px 24px;
border-top: 1px solid #f1f5f9;
}
.fdpt-footer__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding-top: 18px;
@media (width <= 900px) {
grid-template-columns: 1fr;
}
}
.fdpt-banner {
display: flex;
align-items: center;
gap: 10px;
margin: 16px 24px 0;
padding: 12px 14px;
background: #eff6ff;
border: 1px solid #dbeafe;
border-radius: 8px;
color: #1d4ed8;
font-size: 13px;
&__icon {
font-size: 14px;
}
}
.fdpt-toggle-card {
padding: 12px 0;
h4 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
&__choices {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
}
.fdpt-note {
display: flex;
align-items: center;
gap: 10px;
margin-top: 18px;
padding: 12px 14px;
border: 1px solid #fde68a;
background: #fffbeb;
color: #b45309;
border-radius: 8px;
font-size: 12px;
}
@@ -7,8 +7,10 @@
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -22,6 +24,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
})
export class FixedDepositProductTermsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() fixedDepositProductsTemplate: any;
@@ -30,7 +34,13 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
interestCompoundingPeriodTypeData: any;
interestPostingPeriodTypeData: any;
interestCalculationTypeData: any;
interestCalculationTypeOptionData: any;
interestCalculationDaysInYearTypeData: any;
periodFrequencyData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() {
this.createFixedDepositProductTermsForm();
@@ -40,10 +50,22 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
this.interestCompoundingPeriodTypeData = this.fixedDepositProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions;
this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationTypeOptionData = this.fixedDepositProductsTemplate.interestCalculationTypeOption || [];
this.interestCalculationDaysInYearTypeData =
this.fixedDepositProductsTemplate.interestCalculationDaysInYearTypeOptions;
this.periodFrequencyData = this.fixedDepositProductsTemplate.periodFrequencyOptions || [];
if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) {
const isEditing = !!this.fixedDepositProductsTemplate.id;
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
const savedEnumTypeId =
this.fixedDepositProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1;
const savedSelectorId = isEditing
? (this.fixedDepositProductsTemplate.interestCalculateSelector ??
this.interestCalculationTypeOptionData?.[0]?.id ??
null)
: (this.interestCalculationTypeOptionData?.[0]?.id ?? null);
if (isEditing) {
this.fixedDepositProductTermsForm.patchValue({
minDepositAmount: this.fixedDepositProductsTemplate.minDepositAmount,
depositAmount: this.fixedDepositProductsTemplate.depositAmount,
@@ -52,13 +74,31 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
}
this.fixedDepositProductTermsForm.patchValue({
interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType.id,
interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType.id,
interestCalculationType: this.fixedDepositProductsTemplate.interestCalculationType.id,
interestCalculationDaysInYearType: this.fixedDepositProductsTemplate.interestCalculationDaysInYearType.id
nominalAnnualInterestRate: this.fixedDepositProductsTemplate.nominalAnnualInterestRate ?? 0,
interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType?.id,
interestCalculationType: savedEnumTypeId,
interestCalculationDaysInYearType: this.fixedDepositProductsTemplate.interestCalculationDaysInYearType?.id,
effectiveDate: this.fixedDepositProductsTemplate.effectiveDate
? this.parseDate(this.fixedDepositProductsTemplate.effectiveDate)
: '',
interestCalculateSelector: savedSelectorId,
averagePeriod: isEditing ? this.fixedDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod,
autoPost: isEditing ? (this.fixedDepositProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? (this.fixedDepositProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.fixedDepositProductsTemplate.lastInterestDate
? this.parseDate(this.fixedDepositProductsTemplate.lastInterestDate)
: ''
});
}
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
if (dateValue instanceof Date) return dateValue;
return new Date(dateValue);
}
createFixedDepositProductTermsForm() {
this.fixedDepositProductTermsForm = this.formBuilder.group({
minDepositAmount: [
@@ -76,6 +116,13 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
'',
Validators.min(0)
],
nominalAnnualInterestRate: [
'',
[
Validators.required,
Validators.min(0)
]
],
interestCompoundingPeriodType: [
'',
Validators.required
@@ -91,17 +138,34 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
interestCalculationDaysInYearType: [
'',
Validators.required
]
],
effectiveDate: [''],
interestCalculateSelector: [1],
averagePeriod: ['Monthly'],
autoPost: [true],
frequencyFixDay: [
30,
[Validators.min(0)]
],
lastInterestDate: ['']
});
}
get fixedDepositProductTerms() {
const fixedDepositProductTerms = this.fixedDepositProductTermsForm.value;
for (const key in fixedDepositProductTerms) {
if (fixedDepositProductTerms[key] === '') {
delete fixedDepositProductTerms[key];
const formValue = { ...this.fixedDepositProductTermsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveDate instanceof Date) {
formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || '';
}
if (formValue.lastInterestDate instanceof Date) {
formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || '';
}
for (const key in formValue) {
if (formValue[key] === '') {
delete formValue[key];
}
}
return fixedDepositProductTerms;
return formValue;
}
}