update saving product code
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npx *)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(select-object *)",
|
||||
"Bash(git config *)",
|
||||
"Bash(set *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npx *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -87,9 +87,9 @@ export class Accounting {
|
||||
} else if (accountingRuleId === 2) {
|
||||
return { code: 'CASH', id: accountingRuleId, value: 'Cash' };
|
||||
} else if (accountingRuleId === 3) {
|
||||
return { code: 'ACCRUAL_PERIODIC', id: accountingRuleId, value: 'Accural (periodic)' };
|
||||
return { code: 'ACCRUAL_PERIODIC', id: accountingRuleId, value: 'Accrual (periodic)' };
|
||||
} else if (accountingRuleId === 4) {
|
||||
return { code: 'ACCRUAL_UPFRONT', id: accountingRuleId, value: 'Accural (upfront)' };
|
||||
return { code: 'ACCRUAL_UPFRONT', id: accountingRuleId, value: 'Accrual (upfront)' };
|
||||
}
|
||||
return { code: 'INVALID', id: accountingRuleId, value: 'Invalid' };
|
||||
}
|
||||
|
||||
+24
-5
@@ -12,14 +12,21 @@
|
||||
<h2 class="sp-header__title">{{ 'labels.heading.Create Savings Product' | translate }}</h2>
|
||||
</div>
|
||||
|
||||
<mat-tab-group class="sp-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex">
|
||||
<mat-tab-group
|
||||
class="sp-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<i class="fas fa-info-circle sp-tab-icon"></i>
|
||||
{{ 'labels.inputs.DETAILS' | translate }}
|
||||
</ng-template>
|
||||
<div class="sp-tab-content">
|
||||
<mifosx-saving-product-details-step></mifosx-saving-product-details-step>
|
||||
<mifosx-saving-product-details-step
|
||||
[savingProductsTemplate]="savingProductsTemplate"
|
||||
></mifosx-saving-product-details-step>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
@@ -72,6 +79,18 @@
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<i class="fas fa-mobile-alt sp-tab-icon"></i>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="sp-tab-content">
|
||||
<mifosx-saving-product-limits-step
|
||||
[savingProductsTemplate]="savingProductsTemplate"
|
||||
></mifosx-saving-product-limits-step>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<i class="fas fa-book sp-tab-icon"></i>
|
||||
@@ -114,19 +133,19 @@
|
||||
<div class="sp-action-bar__right">
|
||||
@if (selectedTabIndex > 0) {
|
||||
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1">
|
||||
<i class="fas fa-arrow-left" style="margin-right: 8px;"></i>
|
||||
<i class="fas fa-arrow-left" style="margin-right: 8px"></i>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (selectedTabIndex < totalTabs - 1) {
|
||||
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<i class="fas fa-arrow-right" style="margin-left: 8px;"></i>
|
||||
<i class="fas fa-arrow-right" style="margin-left: 8px"></i>
|
||||
</button>
|
||||
}
|
||||
@if (savingProductFormValid && selectedTabIndex === totalTabs - 1) {
|
||||
<button mat-raised-button color="primary" (click)="submit()">
|
||||
<i class="fas fa-check" style="margin-right: 8px;"></i>
|
||||
<i class="fas fa-check" style="margin-right: 8px"></i>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
+4
-4
@@ -35,11 +35,11 @@ $primary: #1353a0;
|
||||
.sp-tabs {
|
||||
background: var(--md-sys-color-surface, #fff);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
@@ -77,10 +77,10 @@ $primary: #1353a0;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
|
||||
&__cancel {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
&__right {
|
||||
|
||||
+35
-5
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -16,6 +16,7 @@ import { SavingProductCurrencyStepComponent } from '../saving-product-stepper/sa
|
||||
import { SavingProductTermsStepComponent } from '../saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component';
|
||||
import { SavingProductSettingsStepComponent } from '../saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component';
|
||||
import { SavingProductChargesStepComponent } from '../saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component';
|
||||
import { SavingProductLimitsStepComponent } from '../saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component';
|
||||
import { SavingProductAccountingStepComponent } from '../saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component';
|
||||
|
||||
/** Custom Services */
|
||||
@@ -38,6 +39,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
SavingProductTermsStepComponent,
|
||||
SavingProductSettingsStepComponent,
|
||||
SavingProductChargesStepComponent,
|
||||
SavingProductLimitsStepComponent,
|
||||
SavingProductAccountingStepComponent,
|
||||
SavingProductPreviewStepComponent
|
||||
]
|
||||
@@ -58,6 +60,8 @@ export class CreateSavingProductComponent {
|
||||
savingProductSettingsStep: SavingProductSettingsStepComponent;
|
||||
@ViewChild(SavingProductChargesStepComponent, { static: true })
|
||||
savingProductChargesStep: SavingProductChargesStepComponent;
|
||||
@ViewChild(SavingProductLimitsStepComponent, { static: true })
|
||||
savingProductLimitsStep: SavingProductLimitsStepComponent;
|
||||
@ViewChild(SavingProductAccountingStepComponent, { static: true })
|
||||
savingProductAccountingStep: SavingProductAccountingStepComponent;
|
||||
|
||||
@@ -100,7 +104,7 @@ export class CreateSavingProductComponent {
|
||||
}
|
||||
|
||||
get totalTabs(): number {
|
||||
return this.savingProductFormValid ? 7 : 6;
|
||||
return this.savingProductFormValid ? 8 : 7;
|
||||
}
|
||||
|
||||
get savingProductFormValid() {
|
||||
@@ -120,18 +124,44 @@ export class CreateSavingProductComponent {
|
||||
...this.savingProductTermsStep.savingProductTerms,
|
||||
...this.savingProductSettingsStep.savingProductSettings,
|
||||
...this.savingProductChargesStep.savingProductCharges,
|
||||
...this.savingProductLimitsStep.savingProductLimits,
|
||||
...this.savingProductAccountingStep.savingProductAccounting
|
||||
};
|
||||
}
|
||||
|
||||
submit() {
|
||||
// TODO: Update once language and date settings are setup
|
||||
private prepareSavingProductPayload() {
|
||||
const savingProduct = {
|
||||
...this.savingProduct,
|
||||
charges: this.savingProduct.charges.map((charge: any) => ({ id: charge.id })),
|
||||
locale: this.settingsService.language.code // locale required for nominalAnnualInterestRate
|
||||
locale: this.settingsService.language.code,
|
||||
dateFormat: 'dd MMMM yyyy'
|
||||
};
|
||||
delete savingProduct.advancedAccountingRules;
|
||||
|
||||
// Convert lockinPeriodFrequencyType from string to integer
|
||||
// Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS
|
||||
if (savingProduct.lockinPeriodFrequencyType) {
|
||||
const frequencyTypeMapping: { [key: string]: number } = {
|
||||
DAYS: 0,
|
||||
WEEKS: 1,
|
||||
MONTHS: 2,
|
||||
YEARS: 3
|
||||
};
|
||||
savingProduct.lockinPeriodFrequencyType = frequencyTypeMapping[savingProduct.lockinPeriodFrequencyType];
|
||||
}
|
||||
|
||||
Object.keys(savingProduct).forEach((key) => {
|
||||
if (savingProduct[key] === '' || savingProduct[key] === null || savingProduct[key] === undefined) {
|
||||
delete savingProduct[key];
|
||||
}
|
||||
});
|
||||
|
||||
return savingProduct;
|
||||
}
|
||||
|
||||
submit() {
|
||||
const savingProduct = this.prepareSavingProductPayload();
|
||||
console.log('Saving product submit payload:', savingProduct);
|
||||
this.productsService.createSavingProduct(savingProduct).subscribe((response: any) => {
|
||||
this.router.navigate(
|
||||
[
|
||||
|
||||
+21
-4
@@ -12,7 +12,12 @@
|
||||
<h2 class="sp-header__title">{{ 'labels.heading.Edit Savings Product' | translate }}</h2>
|
||||
</div>
|
||||
|
||||
<mat-tab-group class="sp-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex">
|
||||
<mat-tab-group
|
||||
class="sp-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<i class="fas fa-info-circle sp-tab-icon"></i>
|
||||
@@ -74,6 +79,18 @@
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<i class="fas fa-mobile-alt sp-tab-icon"></i>
|
||||
Digital
|
||||
</ng-template>
|
||||
<div class="sp-tab-content">
|
||||
<mifosx-saving-product-limits-step
|
||||
[savingProductsTemplate]="savingProductAndTemplate"
|
||||
></mifosx-saving-product-limits-step>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<i class="fas fa-book sp-tab-icon"></i>
|
||||
@@ -116,19 +133,19 @@
|
||||
<div class="sp-action-bar__right">
|
||||
@if (selectedTabIndex > 0) {
|
||||
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1">
|
||||
<i class="fas fa-arrow-left" style="margin-right: 8px;"></i>
|
||||
<i class="fas fa-arrow-left" style="margin-right: 8px"></i>
|
||||
{{ 'labels.buttons.Previous' | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (selectedTabIndex < totalTabs - 1) {
|
||||
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
<i class="fas fa-arrow-right" style="margin-left: 8px;"></i>
|
||||
<i class="fas fa-arrow-right" style="margin-left: 8px"></i>
|
||||
</button>
|
||||
}
|
||||
@if (savingProductFormValidAndNotPristine && selectedTabIndex === totalTabs - 1) {
|
||||
<button mat-raised-button color="primary" (click)="submit()">
|
||||
<i class="fas fa-check" style="margin-right: 8px;"></i>
|
||||
<i class="fas fa-check" style="margin-right: 8px"></i>
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
+4
-4
@@ -35,11 +35,11 @@ $primary: #1353a0;
|
||||
.sp-tabs {
|
||||
background: var(--md-sys-color-surface, #fff);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
@@ -77,10 +77,10 @@ $primary: #1353a0;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
|
||||
&__cancel {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
&__right {
|
||||
|
||||
+36
-5
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -16,6 +16,7 @@ import { SavingProductCurrencyStepComponent } from '../saving-product-stepper/sa
|
||||
import { SavingProductTermsStepComponent } from '../saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component';
|
||||
import { SavingProductSettingsStepComponent } from '../saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component';
|
||||
import { SavingProductChargesStepComponent } from '../saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component';
|
||||
import { SavingProductLimitsStepComponent } from '../saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component';
|
||||
import { SavingProductAccountingStepComponent } from '../saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component';
|
||||
|
||||
/** Custom Services */
|
||||
@@ -38,6 +39,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
SavingProductTermsStepComponent,
|
||||
SavingProductSettingsStepComponent,
|
||||
SavingProductChargesStepComponent,
|
||||
SavingProductLimitsStepComponent,
|
||||
SavingProductAccountingStepComponent,
|
||||
SavingProductPreviewStepComponent
|
||||
]
|
||||
@@ -58,6 +60,8 @@ export class EditSavingProductComponent {
|
||||
savingProductSettingsStep: SavingProductSettingsStepComponent;
|
||||
@ViewChild(SavingProductChargesStepComponent, { static: true })
|
||||
savingProductChargesStep: SavingProductChargesStepComponent;
|
||||
@ViewChild(SavingProductLimitsStepComponent, { static: true })
|
||||
savingProductLimitsStep: SavingProductLimitsStepComponent;
|
||||
@ViewChild(SavingProductAccountingStepComponent, { static: true })
|
||||
savingProductAccountingStep: SavingProductAccountingStepComponent;
|
||||
|
||||
@@ -100,7 +104,7 @@ export class EditSavingProductComponent {
|
||||
}
|
||||
|
||||
get totalTabs(): number {
|
||||
return this.savingProductFormValidAndNotPristine ? 7 : 6;
|
||||
return this.savingProductFormValidAndNotPristine ? 8 : 7;
|
||||
}
|
||||
|
||||
get savingProductFormValidAndNotPristine() {
|
||||
@@ -115,6 +119,7 @@ export class EditSavingProductComponent {
|
||||
!this.savingProductTermsForm.pristine ||
|
||||
!this.savingProductSettingsForm.pristine ||
|
||||
!this.savingProductChargesStep.pristine ||
|
||||
!this.savingProductLimitsStep.pristine ||
|
||||
!this.savingProductAccountingForm.pristine)
|
||||
);
|
||||
}
|
||||
@@ -126,18 +131,44 @@ export class EditSavingProductComponent {
|
||||
...this.savingProductTermsStep.savingProductTerms,
|
||||
...this.savingProductSettingsStep.savingProductSettings,
|
||||
...this.savingProductChargesStep.savingProductCharges,
|
||||
...this.savingProductLimitsStep.savingProductLimits,
|
||||
...this.savingProductAccountingStep.savingProductAccounting
|
||||
};
|
||||
}
|
||||
|
||||
submit() {
|
||||
// TODO: Update once language and date settings are setup
|
||||
private prepareSavingProductPayload() {
|
||||
const savingProduct = {
|
||||
...this.savingProduct,
|
||||
charges: this.savingProduct.charges.map((charge: any) => ({ id: charge.id })),
|
||||
locale: this.settingsService.language.code // locale required for nominalAnnualInterestRate
|
||||
locale: this.settingsService.language.code,
|
||||
dateFormat: 'dd MMMM yyyy'
|
||||
};
|
||||
delete savingProduct.advancedAccountingRules;
|
||||
|
||||
// Convert lockinPeriodFrequencyType from string to integer
|
||||
// Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS
|
||||
if (savingProduct.lockinPeriodFrequencyType) {
|
||||
const frequencyTypeMapping: { [key: string]: number } = {
|
||||
DAYS: 0,
|
||||
WEEKS: 1,
|
||||
MONTHS: 2,
|
||||
YEARS: 3
|
||||
};
|
||||
savingProduct.lockinPeriodFrequencyType = frequencyTypeMapping[savingProduct.lockinPeriodFrequencyType];
|
||||
}
|
||||
|
||||
Object.keys(savingProduct).forEach((key) => {
|
||||
if (savingProduct[key] === '' || savingProduct[key] === null || savingProduct[key] === undefined) {
|
||||
delete savingProduct[key];
|
||||
}
|
||||
});
|
||||
|
||||
return savingProduct;
|
||||
}
|
||||
|
||||
submit() {
|
||||
const savingProduct = this.prepareSavingProductPayload();
|
||||
console.log('Saving product update payload:', savingProduct);
|
||||
this.productsService
|
||||
.updateSavingProduct(this.savingProductAndTemplate.id, savingProduct)
|
||||
.subscribe((response: any) => {
|
||||
|
||||
+111
-341
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -7,356 +7,126 @@
|
||||
-->
|
||||
|
||||
<form [formGroup]="savingProductAccountingForm">
|
||||
<!-- Accounting Rule -->
|
||||
<div class="spa-section">
|
||||
<div class="spa-section__header">
|
||||
<i class="fas fa-book"></i>
|
||||
<h3>{{ 'labels.inputs.Accounting Rule' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spa-body">
|
||||
<mat-radio-group class="spa-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>
|
||||
|
||||
@if (isCashOrAccrualAccounting()) {
|
||||
<!-- Assets -->
|
||||
<div class="spa-section">
|
||||
<div class="spa-section__header">
|
||||
<i class="fas fa-university"></i>
|
||||
<h3>{{ 'labels.heading.Assets' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spa-grid">
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.savingsReferenceAccountId"
|
||||
[glAccountList]="assetAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Saving reference'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.overdraftPortfolioControlId"
|
||||
[glAccountList]="assetAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Overdraft portfolio'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
@if (isAccrualAccounting()) {
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.feesReceivableAccountId"
|
||||
[glAccountList]="assetAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Fees Receivable'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.penaltiesReceivableAccountId"
|
||||
[glAccountList]="assetAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Penalties Receivable'"
|
||||
></mifosx-gl-account-selector>
|
||||
}
|
||||
|
||||
@if (isAccrualAccounting() && allowOverdraft.value) {
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.interestReceivableAccountId"
|
||||
[glAccountList]="assetAccountData"
|
||||
[required]="false"
|
||||
[inputLabel]="'Interest Receivable'"
|
||||
></mifosx-gl-account-selector>
|
||||
}
|
||||
</div>
|
||||
<span class="spa-section__icon">
|
||||
<i class="fas fa-book"></i>
|
||||
</span>
|
||||
<h3>Accounting</h3>
|
||||
</div>
|
||||
|
||||
<!-- Liabilities -->
|
||||
<div class="spa-section">
|
||||
<div class="spa-section__header">
|
||||
<i class="fas fa-file-invoice-dollar"></i>
|
||||
<h3>{{ 'labels.heading.Liabilities' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spa-grid">
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.savingsControlAccountId"
|
||||
[glAccountList]="liabilityAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Saving control'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.transfersInSuspenseAccountId"
|
||||
[glAccountList]="liabilityAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Savings transfers in suspense'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
@if (isAccrualAccounting()) {
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.interestPayableAccountId"
|
||||
[glAccountList]="liabilityAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Interest Payable'"
|
||||
></mifosx-gl-account-selector>
|
||||
}
|
||||
|
||||
@if (isDormancyTrackingActive.value) {
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.escheatLiabilityId"
|
||||
[glAccountList]="liabilityAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Escheat liability'"
|
||||
></mifosx-gl-account-selector>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expenses -->
|
||||
<div class="spa-section">
|
||||
<div class="spa-section__header">
|
||||
<i class="fas fa-money-bill-wave"></i>
|
||||
<h3>{{ 'labels.heading.Expenses' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spa-grid">
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.interestOnSavingsAccountId"
|
||||
[glAccountList]="expenseAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Interest on savings'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.writeOffAccountId"
|
||||
[glAccountList]="expenseAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Write-off'"
|
||||
></mifosx-gl-account-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Income -->
|
||||
<div class="spa-section">
|
||||
<div class="spa-section__header">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<h3>{{ 'labels.heading.Income' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spa-grid">
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.incomeFromFeeAccountId"
|
||||
[glAccountList]="incomeAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Income from fees'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.incomeFromPenaltyAccountId"
|
||||
[glAccountList]="incomeAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Income from penalties'"
|
||||
></mifosx-gl-account-selector>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-field"
|
||||
[inputFormControl]="savingProductAccountingForm.controls.incomeFromInterestId"
|
||||
[glAccountList]="incomeAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'Overdraft Interest Income'"
|
||||
></mifosx-gl-account-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Accounting Rules -->
|
||||
<div class="spa-section">
|
||||
<div class="spa-section__header">
|
||||
<i class="fas fa-cogs"></i>
|
||||
<h3>{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spa-body">
|
||||
<div class="spa-checkbox-row">
|
||||
<mat-checkbox formControlName="advancedAccountingRules">
|
||||
{{ 'labels.heading.Advanced Accounting Rules' | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="spa-section__body">
|
||||
<div class="spa-group">
|
||||
<div class="spa-group__header">
|
||||
<h4>Accounting Rule</h4>
|
||||
</div>
|
||||
<mat-radio-group class="spa-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>
|
||||
|
||||
@if (savingProductAccountingForm.value.advancedAccountingRules) {
|
||||
<!-- Payment Fund Sources -->
|
||||
<div class="spa-subsection">
|
||||
<div class="spa-subsection__header">
|
||||
<h4>{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}</h4>
|
||||
<button
|
||||
type="button"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)"
|
||||
class="spa-add-btn"
|
||||
>
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
@if (!isCashOrAccrualAccounting()) {
|
||||
<div class="spa-note">
|
||||
Select a cash or accrual accounting rule to configure GL mappings. The default "None" rule does not require
|
||||
any accounting mappings.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isCashOrAccrualAccounting()) {
|
||||
@for (section of accountingSectionConfigs; track section.key) {
|
||||
<div class="spa-group">
|
||||
<div class="spa-group__header">
|
||||
<h4>{{ section.title }}</h4>
|
||||
<button type="button" class="spa-add-btn" (click)="addOtherGlMapping(section.otherMappingsControlName)">
|
||||
+ Add Other
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (paymentChannelToFundSourceMappings.value.length !== 0) {
|
||||
<div class="spa-table-wrap">
|
||||
<table mat-table [dataSource]="paymentChannelToFundSourceMappings.value" class="spa-table">
|
||||
<ng-container matColumnDef="paymentTypeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.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: combinedAccountData : 'id' : 'name' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let paymentFundSource; let i = index">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
(click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
|
||||
<div class="spa-list">
|
||||
@for (row of section.rows; track row.key) {
|
||||
@if (
|
||||
(!row.accrualOnly || isAccrualAccounting()) && (!row.dormancyOnly || isDormancyTrackingEnabled())
|
||||
) {
|
||||
<div class="spa-row">
|
||||
<div class="spa-row__meta">
|
||||
<span
|
||||
class="spa-pill"
|
||||
[class.spa-pill--green]="section.pillClass === 'green'"
|
||||
[class.spa-pill--amber]="section.pillClass === 'amber'"
|
||||
[class.spa-pill--blue]="section.pillClass === 'blue'"
|
||||
>
|
||||
<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>
|
||||
}
|
||||
{{ section.pillLabel }}
|
||||
</span>
|
||||
<span class="spa-row__label">{{ row.label }}</span>
|
||||
</div>
|
||||
<mifosx-gl-account-selector
|
||||
class="spa-row__input"
|
||||
[inputFormControl]="savingProductAccountingForm.get(row.controlName)"
|
||||
[glAccountList]="row.options"
|
||||
[required]="row.required"
|
||||
[inputLabel]="row.inputLabel"
|
||||
></mifosx-gl-account-selector>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spa-list" [formArrayName]="section.otherMappingsControlName">
|
||||
@for (
|
||||
otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
|
||||
track $index;
|
||||
let i = $index
|
||||
) {
|
||||
<div class="spa-other-row-wrap">
|
||||
<div class="spa-row spa-row--other" [formGroupName]="i">
|
||||
<div class="spa-row__meta spa-row__meta--other">
|
||||
<span
|
||||
class="spa-pill"
|
||||
[class.spa-pill--green]="section.pillClass === 'green'"
|
||||
[class.spa-pill--amber]="section.pillClass === 'amber'"
|
||||
[class.spa-pill--blue]="section.pillClass === 'blue'"
|
||||
>
|
||||
{{ section.pillLabel }}
|
||||
</span>
|
||||
<input
|
||||
class="spa-row__name spa-row__name--other-account"
|
||||
formControlName="accountName"
|
||||
placeholder="Enter account name..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="spa-row__actions spa-row__actions--other">
|
||||
<mat-form-field class="spa-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="spa-remove-btn spa-remove-btn--side"
|
||||
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fees to Income -->
|
||||
@if (existCharges()) {
|
||||
<div class="spa-subsection">
|
||||
<div class="spa-subsection__header">
|
||||
<h4>{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}</h4>
|
||||
<button
|
||||
type="button"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="add('FeesIncome', feeToIncomeAccountMappings)"
|
||||
class="spa-add-btn"
|
||||
>
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (feeToIncomeAccountMappings.value.length !== 0) {
|
||||
<div class="spa-table-wrap">
|
||||
<table mat-table [dataSource]="feeToIncomeAccountMappings.value" class="spa-table">
|
||||
<ng-container matColumnDef="chargeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.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.inputs.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let feesIncome; let i = index">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
(click)="edit('FeesIncome', feeToIncomeAccountMappings, i)"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- Penalties to Income -->
|
||||
<div class="spa-subsection">
|
||||
<div class="spa-subsection__header">
|
||||
<h4>{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}</h4>
|
||||
<button
|
||||
type="button"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)"
|
||||
class="spa-add-btn"
|
||||
>
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (penaltyToIncomeAccountMappings.value.length !== 0) {
|
||||
<div class="spa-table-wrap">
|
||||
<table mat-table [dataSource]="penaltyToIncomeAccountMappings.value" class="spa-table">
|
||||
<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.inputs.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let penaltyIncome; let i = index">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
(click)="edit('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
|
||||
>
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+273
-88
@@ -6,124 +6,309 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$primary: #1353a0;
|
||||
$accent: #0ea5e9;
|
||||
$border: #e2e8f0;
|
||||
$gray-50: #f8f9fa;
|
||||
$gray-200: #e8eaed;
|
||||
$gray-500: #9aa0a6;
|
||||
$gray-700: #5f6368;
|
||||
$gray-500: #6b7280;
|
||||
$gray-700: #334155;
|
||||
$gray-900: #202124;
|
||||
$primary: #1353a0;
|
||||
|
||||
.spa-section {
|
||||
border: 1px solid $border;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
& + & {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 24px;
|
||||
background: $gray-50;
|
||||
border-bottom: 1px solid $gray-200;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spa-body {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.spa-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4px 16px;
|
||||
padding: 16px 24px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-field {
|
||||
width: 100%;
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-radio-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-checkbox-row {
|
||||
.spa-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
margin-bottom: 12px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-subsection {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid $gray-200;
|
||||
.spa-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;
|
||||
}
|
||||
|
||||
.spa-section__body {
|
||||
padding: 16px 24px 20px;
|
||||
}
|
||||
|
||||
.spa-group {
|
||||
& + & {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $gray-700;
|
||||
}
|
||||
h4 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-add-btn {
|
||||
height: 32px !important;
|
||||
border-radius: 8px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spa-table-wrap {
|
||||
border: 1px solid $border;
|
||||
.spa-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.spa-list + .spa-list {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.spa-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
@media (width <= 900px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-table {
|
||||
.spa-row:has(mifosx-gl-account-selector) {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.spa-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;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-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;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-row__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.spa-row__meta--other {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.spa-row__label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.spa-row__input {
|
||||
flex: 0 0 22rem;
|
||||
max-width: 22rem;
|
||||
min-width: 18rem;
|
||||
|
||||
@media (width <= 900px) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-row__input--other {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.spa-row__input :is(mifosx-gl-account-selector) {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) {
|
||||
width: 100%;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.spa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spa-row__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.spa-row__actions--other {
|
||||
justify-content: flex-end;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-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%;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-row__name--code {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.spa-row__name--other-account {
|
||||
max-width: 100%;
|
||||
flex: 1 1 auto;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.spa-row__other-field {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.spa-row__other-field :is(mat-form-field) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spa-row__other-field :is(.mat-mdc-form-field-required-marker) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spa-remove-btn {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.spa-remove-btn--side {
|
||||
place-self: center end;
|
||||
margin-right: 4px;
|
||||
|
||||
@media (width <= 1100px) {
|
||||
justify-self: start;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spa-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
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;
|
||||
|
||||
&--green {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&--amber {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
&--blue {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
datalist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.spa-row__meta {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.spa-row__input {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
+429
-423
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -6,489 +6,495 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, Input, inject } from '@angular/core';
|
||||
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||
import {
|
||||
UntypedFormGroup,
|
||||
UntypedFormBuilder,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
UntypedFormArray,
|
||||
Validators,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
ReactiveFormsModule
|
||||
UntypedFormGroup,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
||||
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
||||
|
||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
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 { 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 { 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;
|
||||
dormancyOnly?: 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-saving-product-accounting-step',
|
||||
templateUrl: './saving-product-accounting-step.component.html',
|
||||
styleUrls: ['./saving-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 SavingProductAccountingStepComponent implements OnInit {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private dialog = inject(MatDialog);
|
||||
private translateService = inject(TranslateService);
|
||||
private accounting = inject(Accounting);
|
||||
|
||||
@Input() savingProductsTemplate: any;
|
||||
@Input() accountingRuleData: any;
|
||||
@Input() isDormancyTrackingActive: UntypedFormControl;
|
||||
@Input() isDormancyTrackingActive: any;
|
||||
@Input() savingProductFormValid: boolean;
|
||||
@Input() allowOverdraft: UntypedFormControl;
|
||||
@Input() allowOverdraft: any;
|
||||
|
||||
savingProductAccountingForm: UntypedFormGroup;
|
||||
|
||||
chargeData: any;
|
||||
penaltyData: any;
|
||||
paymentTypeData: any;
|
||||
assetAccountData: any;
|
||||
incomeAccountData: any;
|
||||
expenseAccountData: any;
|
||||
liabilityAccountData: any;
|
||||
incomeAndLiabilityAccountData: any;
|
||||
combinedAccountData: any[];
|
||||
|
||||
paymentFundSourceDisplayedColumns: string[] = [
|
||||
'paymentTypeId',
|
||||
'fundSourceAccountId',
|
||||
'actions'
|
||||
];
|
||||
feesPenaltyIncomeDisplayedColumns: string[] = [
|
||||
'chargeId',
|
||||
'incomeAccountId',
|
||||
'actions'
|
||||
];
|
||||
liabilityAccountOptions: GLAccount[] = [];
|
||||
expenseAccountOptions: GLAccount[] = [];
|
||||
incomeAccountOptions: GLAccount[] = [];
|
||||
assetAccountOptions: GLAccount[] = [];
|
||||
accountingSectionConfigs: AccountingSectionConfig[] = [];
|
||||
|
||||
constructor() {
|
||||
this.createsavingProductAccountingForm();
|
||||
this.createSavingProductAccountingForm();
|
||||
this.setConditionalControls();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.chargeData = this.savingProductsTemplate.chargeOptions || [];
|
||||
this.penaltyData = this.savingProductsTemplate.penaltyOptions || [];
|
||||
this.paymentTypeData = this.savingProductsTemplate.paymentTypeOptions || [];
|
||||
this.assetAccountData = this.savingProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
|
||||
this.incomeAccountData = this.savingProductsTemplate.accountingMappingOptions.incomeAccountOptions || [];
|
||||
this.expenseAccountData = this.savingProductsTemplate.accountingMappingOptions.expenseAccountOptions || [];
|
||||
this.liabilityAccountData = this.savingProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
|
||||
this.combinedAccountData = [
|
||||
...this.assetAccountData,
|
||||
...this.incomeAccountData,
|
||||
...this.expenseAccountData,
|
||||
...this.liabilityAccountData
|
||||
];
|
||||
this.combinedAccountData.sort((a, b) => {
|
||||
const valueA = a.name.toLowerCase();
|
||||
const valueB = b.name.toLowerCase();
|
||||
if (valueA < valueB) {
|
||||
return -1;
|
||||
}
|
||||
if (valueA > valueB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
const mappings = this.savingProductsTemplate?.accountingMappings || {};
|
||||
const accountingMappingOptions = this.savingProductsTemplate?.accountingMappingOptions || {};
|
||||
const accountingRuleId = this.savingProductsTemplate?.accountingRule?.id || 1;
|
||||
this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
|
||||
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
|
||||
this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
|
||||
this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
|
||||
this.buildAccountingSectionConfigs();
|
||||
this.savingProductAccountingForm.patchValue({
|
||||
accountingRule: accountingRuleId
|
||||
});
|
||||
|
||||
this.syncAccountingControls(accountingRuleId);
|
||||
this.patchAccountingValues(mappings);
|
||||
this.setOverdraftControl();
|
||||
this.setDormancyControl();
|
||||
}
|
||||
|
||||
createSavingProductAccountingForm() {
|
||||
this.savingProductAccountingForm = this.formBuilder.group({
|
||||
accountingRule: [1],
|
||||
assetsOtherGlMappings: this.formBuilder.array([]),
|
||||
liabilitiesOtherGlMappings: this.formBuilder.array([]),
|
||||
expensesOtherGlMappings: this.formBuilder.array([]),
|
||||
incomeOtherGlMappings: this.formBuilder.array([])
|
||||
});
|
||||
}
|
||||
|
||||
private setConditionalControls() {
|
||||
this.savingProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => {
|
||||
this.syncAccountingControls(accountingRule);
|
||||
});
|
||||
}
|
||||
|
||||
private patchAccountingValues(mappings: any) {
|
||||
this.savingProductAccountingForm.patchValue({
|
||||
accountingRule: this.savingProductsTemplate.accountingRule.id
|
||||
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
|
||||
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
|
||||
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
|
||||
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
|
||||
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
|
||||
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
|
||||
incomeFromInterestId: mappings.incomeFromInterest?.id || '',
|
||||
overdraftPortfolioControlId: mappings.overdraftPortfolioControl?.id || '',
|
||||
writeOffAccountId: mappings.writeOffAccount?.id || '',
|
||||
feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
|
||||
penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
|
||||
interestPayableAccountId: mappings.interestPayableAccount?.id || '',
|
||||
interestReceivableAccountId: mappings.interestReceivableAccount?.id || '',
|
||||
escheatLiabilityId: mappings.escheatLiabilityAccount?.id || this.liabilityAccountOptions[0]?.id || ''
|
||||
});
|
||||
|
||||
if (this.isCashOrAccrualAccounting()) {
|
||||
this.savingProductAccountingForm.patchValue({
|
||||
savingsReferenceAccountId: this.savingProductsTemplate.accountingMappings.savingsReferenceAccount.id,
|
||||
overdraftPortfolioControlId: this.savingProductsTemplate.accountingMappings.overdraftPortfolioControl.id,
|
||||
savingsControlAccountId: this.savingProductsTemplate.accountingMappings.savingsControlAccount.id,
|
||||
transfersInSuspenseAccountId: this.savingProductsTemplate.accountingMappings.transfersInSuspenseAccount.id,
|
||||
interestOnSavingsAccountId: this.savingProductsTemplate.accountingMappings.interestOnSavingsAccount.id,
|
||||
writeOffAccountId: this.savingProductsTemplate.accountingMappings.writeOffAccount.id,
|
||||
incomeFromFeeAccountId: this.savingProductsTemplate.accountingMappings.incomeFromFeeAccount.id,
|
||||
incomeFromPenaltyAccountId: this.savingProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id,
|
||||
incomeFromInterestId: this.savingProductsTemplate.accountingMappings.incomeFromInterest.id,
|
||||
advancedAccountingRules:
|
||||
this.savingProductsTemplate.paymentChannelToFundSourceMappings ||
|
||||
this.savingProductsTemplate.feeToIncomeAccountMappings ||
|
||||
this.savingProductsTemplate.penaltyToIncomeAccountMappings ||
|
||||
this.savingProductsTemplate.accrualCharges
|
||||
? true
|
||||
: false
|
||||
});
|
||||
|
||||
if (this.isAccrualAccounting()) {
|
||||
this.savingProductAccountingForm.patchValue({
|
||||
feesReceivableAccountId: this.savingProductsTemplate.accountingMappings.feeReceivableAccount.id,
|
||||
penaltiesReceivableAccountId: this.savingProductsTemplate.accountingMappings.penaltyReceivableAccount.id,
|
||||
interestReceivableAccountId:
|
||||
this.savingProductsTemplate.accountingMappings.interestReceivableAccount == null || undefined
|
||||
? ''
|
||||
: this.savingProductsTemplate.accountingMappings.interestReceivableAccount.id,
|
||||
interestPayableAccountId: this.savingProductsTemplate.accountingMappings.interestPayableAccount.id
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isDormancyTrackingActive.value) {
|
||||
this.savingProductAccountingForm.patchValue({
|
||||
escheatLiabilityId: this.savingProductsTemplate.accountingMappings.escheatLiabilityAccount.id
|
||||
});
|
||||
}
|
||||
this.savingProductAccountingForm.setControl(
|
||||
'paymentChannelToFundSourceMappings',
|
||||
this.formBuilder.array(
|
||||
(this.savingProductsTemplate.paymentChannelToFundSourceMappings || []).map((paymentFundSource: any) => ({
|
||||
paymentTypeId: paymentFundSource.paymentType.id,
|
||||
fundSourceAccountId: paymentFundSource.fundSourceAccount.id
|
||||
}))
|
||||
)
|
||||
this.setOtherGlMappings(
|
||||
'assetsOtherGlMappings',
|
||||
mappings.otherGlMappings?.assets || [],
|
||||
this.assetAccountOptions
|
||||
);
|
||||
this.savingProductAccountingForm.setControl(
|
||||
'feeToIncomeAccountMappings',
|
||||
this.formBuilder.array(
|
||||
(this.savingProductsTemplate.feeToIncomeAccountMappings || []).map((feesIncome: any) => ({
|
||||
chargeId: feesIncome.charge.id,
|
||||
incomeAccountId: feesIncome.incomeAccount.id
|
||||
}))
|
||||
)
|
||||
this.setOtherGlMappings(
|
||||
'liabilitiesOtherGlMappings',
|
||||
mappings.otherGlMappings?.liabilities || [],
|
||||
this.liabilityAccountOptions
|
||||
);
|
||||
this.savingProductAccountingForm.setControl(
|
||||
'penaltyToIncomeAccountMappings',
|
||||
this.formBuilder.array(
|
||||
(this.savingProductsTemplate.penaltyToIncomeAccountMappings || []).map((penaltyIncome: any) => ({
|
||||
chargeId: penaltyIncome.charge.id,
|
||||
incomeAccountId: penaltyIncome.incomeAccount.id
|
||||
}))
|
||||
)
|
||||
this.setOtherGlMappings(
|
||||
'expensesOtherGlMappings',
|
||||
mappings.otherGlMappings?.expenses || [],
|
||||
this.expenseAccountOptions
|
||||
);
|
||||
this.setOtherGlMappings(
|
||||
'incomeOtherGlMappings',
|
||||
mappings.otherGlMappings?.income || [],
|
||||
this.incomeAccountOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
createsavingProductAccountingForm() {
|
||||
this.savingProductAccountingForm = this.formBuilder.group({
|
||||
accountingRule: [1]
|
||||
private createOtherGlMappingGroup(otherGlMapping: Partial<OtherGlMappingValue> = {}) {
|
||||
return this.formBuilder.group({
|
||||
accountName: [
|
||||
otherGlMapping.accountName || '',
|
||||
Validators.required
|
||||
],
|
||||
glAccountId: [
|
||||
otherGlMapping.glAccountId || '',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
setConditionalControls() {
|
||||
this.savingProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => {
|
||||
if (accountingRule === 2 || accountingRule === 3) {
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'savingsReferenceAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'overdraftPortfolioControlId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'savingsControlAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'transfersInSuspenseAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'interestOnSavingsAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'writeOffAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'incomeFromFeeAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'incomeFromPenaltyAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'incomeFromInterestId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false));
|
||||
getOtherGlMappingsArray(controlName: string): UntypedFormArray {
|
||||
return this.savingProductAccountingForm.get(controlName) as UntypedFormArray;
|
||||
}
|
||||
|
||||
if (accountingRule === 3) {
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'feesReceivableAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'penaltiesReceivableAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
if (this.allowOverdraft.value) {
|
||||
this.savingProductAccountingForm.addControl('interestReceivableAccountId', new UntypedFormControl(''));
|
||||
}
|
||||
this.allowOverdraft.valueChanges.subscribe((allowOverdraft: boolean) => {
|
||||
if (allowOverdraft) {
|
||||
this.savingProductAccountingForm.addControl('interestReceivableAccountId', new UntypedFormControl(''));
|
||||
} else {
|
||||
this.savingProductAccountingForm.removeControl('interestReceivableAccountId');
|
||||
}
|
||||
});
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'interestPayableAccountId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
}
|
||||
if (accountingRule === 2) {
|
||||
this.savingProductAccountingForm.removeControl('feesReceivableAccountId');
|
||||
this.savingProductAccountingForm.removeControl('penaltiesReceivableAccountId');
|
||||
this.savingProductAccountingForm.removeControl('interestPayableAccountId');
|
||||
}
|
||||
private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) {
|
||||
const otherGlMappingsArray = this.getOtherGlMappingsArray(controlName);
|
||||
otherGlMappingsArray.clear();
|
||||
(otherGlMappings || []).forEach((otherGlMapping) => {
|
||||
otherGlMappingsArray.push(
|
||||
this.createOtherGlMappingGroup({
|
||||
accountName: otherGlMapping.accountName,
|
||||
glAccountId:
|
||||
otherGlMapping.glAccountId || options.find((option) => option.glCode === otherGlMapping.glCode)?.id || null
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isDormancyTrackingActive.value) {
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'escheatLiabilityId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
}
|
||||
addOtherGlMapping(controlName: string) {
|
||||
this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
|
||||
}
|
||||
|
||||
this.isDormancyTrackingActive.valueChanges.subscribe((isDormancyTrackingActive: boolean) => {
|
||||
if (isDormancyTrackingActive) {
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'escheatLiabilityId',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
} else {
|
||||
this.savingProductAccountingForm.removeControl('escheatLiabilityId');
|
||||
}
|
||||
});
|
||||
removeOtherGlMapping(controlName: string, index: number) {
|
||||
this.getOtherGlMappingsArray(controlName).removeAt(index);
|
||||
}
|
||||
|
||||
this.savingProductAccountingForm
|
||||
.get('advancedAccountingRules')
|
||||
.valueChanges.subscribe((advancedAccountingRules: boolean) => {
|
||||
if (advancedAccountingRules) {
|
||||
this.savingProductAccountingForm.addControl(
|
||||
'paymentChannelToFundSourceMappings',
|
||||
this.formBuilder.array([])
|
||||
);
|
||||
this.savingProductAccountingForm.addControl('feeToIncomeAccountMappings', this.formBuilder.array([]));
|
||||
this.savingProductAccountingForm.addControl('penaltyToIncomeAccountMappings', this.formBuilder.array([]));
|
||||
} else {
|
||||
this.savingProductAccountingForm.removeControl('paymentChannelToFundSourceMappings');
|
||||
this.savingProductAccountingForm.removeControl('feeToIncomeAccountMappings');
|
||||
this.savingProductAccountingForm.removeControl('penaltyToIncomeAccountMappings');
|
||||
}
|
||||
});
|
||||
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((controlName) => this.getOtherGlMappingsArray(controlName).clear());
|
||||
}
|
||||
|
||||
private setOverdraftControl() {
|
||||
this.allowOverdraft?.valueChanges?.subscribe(() => {
|
||||
this.syncAccountingControls(this.savingProductAccountingForm.value.accountingRule);
|
||||
});
|
||||
}
|
||||
|
||||
private setDormancyControl() {
|
||||
this.isDormancyTrackingActive?.valueChanges?.subscribe(() => {
|
||||
this.syncAccountingControls(this.savingProductAccountingForm.value.accountingRule);
|
||||
});
|
||||
}
|
||||
|
||||
private getOtherGlMappings() {
|
||||
const mapOtherGlMappings = (controlName: string, options: GLAccount[]) =>
|
||||
this.getOtherGlMappingsArray(controlName)
|
||||
.value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId)
|
||||
.map((item: OtherGlMappingValue) => ({
|
||||
accountName: item.accountName,
|
||||
glCode: options.find((option) => option.id === item.glAccountId)?.glCode || ''
|
||||
}))
|
||||
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
|
||||
|
||||
const otherGlMappings = {
|
||||
assets: mapOtherGlMappings('assetsOtherGlMappings', this.assetAccountOptions),
|
||||
liabilities: mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
|
||||
expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions),
|
||||
income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions)
|
||||
};
|
||||
|
||||
return Object.values(otherGlMappings).some((value) => value.length) ? otherGlMappings : null;
|
||||
}
|
||||
|
||||
isDormancyTrackingEnabled(): boolean {
|
||||
return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false;
|
||||
}
|
||||
|
||||
private syncAccountingControls(accountingRule: number) {
|
||||
const isDormancyActive = this.isDormancyTrackingEnabled();
|
||||
|
||||
const addControl = (controlName: string, required = false) => {
|
||||
if (!this.savingProductAccountingForm.contains(controlName)) {
|
||||
this.savingProductAccountingForm.addControl(
|
||||
controlName,
|
||||
new UntypedFormControl('', required ? Validators.required : undefined)
|
||||
);
|
||||
} else if (required) {
|
||||
this.savingProductAccountingForm.get(controlName)?.setValidators(Validators.required);
|
||||
this.savingProductAccountingForm.get(controlName)?.updateValueAndValidity({ emitEvent: false });
|
||||
}
|
||||
};
|
||||
|
||||
const removeControl = (controlName: string) => {
|
||||
if (this.savingProductAccountingForm.contains(controlName)) {
|
||||
this.savingProductAccountingForm.removeControl(controlName);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) {
|
||||
addControl('savingsReferenceAccountId', true);
|
||||
addControl('savingsControlAccountId', true);
|
||||
addControl('transfersInSuspenseAccountId', true);
|
||||
addControl('interestOnSavingsAccountId', true);
|
||||
addControl('incomeFromFeeAccountId', true);
|
||||
addControl('incomeFromPenaltyAccountId', true);
|
||||
addControl('incomeFromInterestId', true);
|
||||
addControl('writeOffAccountId', true);
|
||||
addControl('overdraftPortfolioControlId', false);
|
||||
|
||||
if (this.accounting.isAccrualAccountingRuleId(accountingRule)) {
|
||||
addControl('feesReceivableAccountId', true);
|
||||
addControl('penaltiesReceivableAccountId', true);
|
||||
addControl('interestPayableAccountId', true);
|
||||
addControl('interestReceivableAccountId', false);
|
||||
} else {
|
||||
this.savingProductAccountingForm.removeControl('savingsReferenceAccountId');
|
||||
this.savingProductAccountingForm.removeControl('overdraftPortfolioControlId');
|
||||
this.savingProductAccountingForm.removeControl('savingsControlAccountId');
|
||||
this.savingProductAccountingForm.removeControl('transfersInSuspenseAccountId');
|
||||
this.savingProductAccountingForm.removeControl('interestOnSavingsAccountId');
|
||||
this.savingProductAccountingForm.removeControl('writeOffAccountId');
|
||||
this.savingProductAccountingForm.removeControl('incomeFromFeeAccountId');
|
||||
this.savingProductAccountingForm.removeControl('incomeFromPenaltyAccountId');
|
||||
this.savingProductAccountingForm.removeControl('incomeFromInterestId');
|
||||
this.savingProductAccountingForm.removeControl('advancedAccountingRules');
|
||||
this.savingProductAccountingForm.removeControl('escheatLiabilityId');
|
||||
this.savingProductAccountingForm.removeControl('feesReceivableAccountId');
|
||||
this.savingProductAccountingForm.removeControl('penaltiesReceivableAccountId');
|
||||
this.savingProductAccountingForm.removeControl('interestReceivableAccountId');
|
||||
this.savingProductAccountingForm.removeControl('interestPayableAccountId');
|
||||
removeControl('feesReceivableAccountId');
|
||||
removeControl('penaltiesReceivableAccountId');
|
||||
removeControl('interestPayableAccountId');
|
||||
removeControl('interestReceivableAccountId');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get paymentChannelToFundSourceMappings(): UntypedFormArray {
|
||||
return this.savingProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray;
|
||||
}
|
||||
|
||||
get feeToIncomeAccountMappings(): UntypedFormArray {
|
||||
return this.savingProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray;
|
||||
}
|
||||
|
||||
get penaltyToIncomeAccountMappings(): UntypedFormArray {
|
||||
return this.savingProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray;
|
||||
}
|
||||
|
||||
setSavingProductAccountingFormDirty() {
|
||||
if (this.savingProductAccountingForm.pristine) {
|
||||
this.savingProductAccountingForm.markAsDirty();
|
||||
if (isDormancyActive) {
|
||||
addControl('escheatLiabilityId', false);
|
||||
} else {
|
||||
removeControl('escheatLiabilityId');
|
||||
}
|
||||
} else {
|
||||
removeControl('savingsReferenceAccountId');
|
||||
removeControl('savingsControlAccountId');
|
||||
removeControl('transfersInSuspenseAccountId');
|
||||
removeControl('interestOnSavingsAccountId');
|
||||
removeControl('incomeFromFeeAccountId');
|
||||
removeControl('incomeFromPenaltyAccountId');
|
||||
removeControl('incomeFromInterestId');
|
||||
removeControl('writeOffAccountId');
|
||||
removeControl('overdraftPortfolioControlId');
|
||||
removeControl('feesReceivableAccountId');
|
||||
removeControl('penaltiesReceivableAccountId');
|
||||
removeControl('interestPayableAccountId');
|
||||
removeControl('interestReceivableAccountId');
|
||||
removeControl('escheatLiabilityId');
|
||||
this.clearOtherGlMappings();
|
||||
}
|
||||
}
|
||||
|
||||
existCharges(): boolean {
|
||||
return this.chargeData.length > 0;
|
||||
}
|
||||
|
||||
add(formType: string, formArray: UntypedFormArray) {
|
||||
const data = { ...this.getData(formType), pristine: false };
|
||||
const dialogRef = this.dialog.open(FormDialogComponent, { data, width: '20rem' });
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.data) {
|
||||
formArray.push(response.data);
|
||||
this.setSavingProductAccountingFormDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
this.setSavingProductAccountingFormDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete(formArray: UntypedFormArray, index: number) {
|
||||
const dialogRef = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `this` }
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.delete) {
|
||||
formArray.removeAt(index);
|
||||
this.setSavingProductAccountingFormDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getData(formType: string, values?: any) {
|
||||
switch (formType) {
|
||||
case 'PaymentFundSource':
|
||||
return {
|
||||
title: this.translateService.instant('labels.heading.Configure Fund Sources for Payment Channels'),
|
||||
formfields: this.getPaymentFundSourceFormfields(values)
|
||||
};
|
||||
case 'FeesIncome':
|
||||
return {
|
||||
title: this.translateService.instant('labels.heading.Map Fees to Specific Income Accounts'),
|
||||
formfields: this.getFeesIncomeFormfields(values)
|
||||
};
|
||||
case 'PenaltyIncome':
|
||||
return {
|
||||
title: this.translateService.instant('labels.heading.Map Penalties to Specific Income Accounts'),
|
||||
formfields: this.getPenaltyIncomeFormfields(values)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getPaymentFundSourceFormfields(values?: any) {
|
||||
const formfields: FormfieldBase[] = [
|
||||
new SelectBase({
|
||||
controlName: 'paymentTypeId',
|
||||
label: this.translateService.instant('labels.inputs.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: this.translateService.instant('labels.inputs.Fund Source'),
|
||||
value: values ? values.fundSourceAccountId : this.combinedAccountData[0].id,
|
||||
options: { label: 'name', value: 'id', data: this.combinedAccountData },
|
||||
required: true,
|
||||
order: 2
|
||||
})
|
||||
];
|
||||
return formfields;
|
||||
}
|
||||
|
||||
getFeesIncomeFormfields(values?: any) {
|
||||
const formfields: FormfieldBase[] = [
|
||||
new SelectBase({
|
||||
controlName: 'chargeId',
|
||||
label: this.translateService.instant('labels.inputs.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: this.translateService.instant('labels.inputs.Income Account'),
|
||||
value: values ? values.incomeAccountId : this.incomeAccountData[0].id,
|
||||
options: { label: 'name', value: 'id', data: this.incomeAccountData },
|
||||
required: true,
|
||||
order: 2
|
||||
})
|
||||
];
|
||||
return formfields;
|
||||
}
|
||||
|
||||
getPenaltyIncomeFormfields(values?: any) {
|
||||
const formfields: FormfieldBase[] = [
|
||||
new SelectBase({
|
||||
controlName: 'chargeId',
|
||||
label: this.translateService.instant('labels.inputs.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: this.translateService.instant('labels.inputs.Income Account'),
|
||||
value: values ? values.incomeAccountId : this.incomeAccountData[0].id,
|
||||
options: { label: 'name', value: 'id', data: this.incomeAccountData },
|
||||
required: true,
|
||||
order: 2
|
||||
})
|
||||
];
|
||||
return formfields;
|
||||
}
|
||||
|
||||
get savingProductAccounting() {
|
||||
return this.savingProductAccountingForm.value;
|
||||
const {
|
||||
assetsOtherGlMappings,
|
||||
liabilitiesOtherGlMappings,
|
||||
expensesOtherGlMappings,
|
||||
incomeOtherGlMappings,
|
||||
...savingProductAccounting
|
||||
} = this.savingProductAccountingForm.value;
|
||||
const otherGlMappings = this.getOtherGlMappings();
|
||||
|
||||
return {
|
||||
...savingProductAccounting,
|
||||
...(otherGlMappings ? { otherGlMappings } : {})
|
||||
};
|
||||
}
|
||||
|
||||
isCashOrAccrualAccounting(): boolean {
|
||||
return (
|
||||
this.savingProductAccountingForm.value.accountingRule === 2 ||
|
||||
this.savingProductAccountingForm.value.accountingRule === 3
|
||||
);
|
||||
return this.accounting.isCashOrAccrualAccountingRuleId(this.savingProductAccountingForm.value.accountingRule);
|
||||
}
|
||||
|
||||
isAccrualAccounting(): boolean {
|
||||
return this.savingProductAccountingForm.value.accountingRule === 3;
|
||||
return this.accounting.isAccrualAccountingRuleId(this.savingProductAccountingForm.value.accountingRule);
|
||||
}
|
||||
|
||||
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: 'overdraftPortfolioControlId',
|
||||
label: 'Overdraft Portfolio Control',
|
||||
inputLabel: 'Overdraft portfolio control',
|
||||
controlName: 'overdraftPortfolioControlId',
|
||||
options: this.assetAccountOptions
|
||||
},
|
||||
{
|
||||
key: 'interestReceivableAccountId',
|
||||
label: 'Interest Receivable Account',
|
||||
inputLabel: 'Interest receivable account',
|
||||
controlName: 'interestReceivableAccountId',
|
||||
options: this.assetAccountOptions,
|
||||
accrualOnly: 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
|
||||
},
|
||||
{
|
||||
key: 'escheatLiabilityId',
|
||||
label: 'Escheat Liability Account',
|
||||
inputLabel: 'Escheat liability account',
|
||||
controlName: 'escheatLiabilityId',
|
||||
options: this.liabilityAccountOptions,
|
||||
dormancyOnly: 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
|
||||
},
|
||||
{
|
||||
key: 'writeOffAccountId',
|
||||
label: 'Write Off Account',
|
||||
inputLabel: 'Write off account',
|
||||
controlName: 'writeOffAccountId',
|
||||
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
|
||||
},
|
||||
{
|
||||
key: 'incomeFromInterestId',
|
||||
label: 'Income From Interest',
|
||||
inputLabel: 'Income from interest',
|
||||
controlName: 'incomeFromInterestId',
|
||||
options: this.incomeAccountOptions,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
otherMappingsControlName: 'incomeOtherGlMappings',
|
||||
otherAccountOptions: this.incomeAccountOptions
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ $gray-900: #202124;
|
||||
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
flex-direction: column;
|
||||
|
||||
&__select {
|
||||
|
||||
+57
-62
@@ -10,74 +10,69 @@
|
||||
<div class="spc-section">
|
||||
<div class="spc-section__header">
|
||||
<i class="fas fa-coins"></i>
|
||||
<h3>{{ 'labels.inputs.Currency' | translate }}</h3>
|
||||
<span>Currency & Account Status Configuration</span>
|
||||
</div>
|
||||
<div class="spc-grid">
|
||||
<mat-form-field appearance="outline" class="spc-field">
|
||||
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
|
||||
<mat-select
|
||||
formControlName="currencyCode"
|
||||
matTooltip="{{ 'tooltips.The currency to be used' | translate }}"
|
||||
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="spc-field">
|
||||
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Track and report saving account' | translate }}"
|
||||
formControlName="digitsAfterDecimal"
|
||||
required
|
||||
/>
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spc-section">
|
||||
<div class="spc-section__header">
|
||||
<i class="fas fa-calculator"></i>
|
||||
<h3>{{ 'labels.inputs.Multiples' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spc-grid">
|
||||
<div class="spc-field--full spc-checkbox-row">
|
||||
<mat-checkbox labelPosition="before" formControlName="setMultiples">
|
||||
{{ 'labels.inputs.Set the saving installment in multiples of' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@if (savingProductCurrencyForm.value.setMultiples) {
|
||||
<div class="spc-body">
|
||||
<div class="spc-grid">
|
||||
<mat-form-field appearance="outline" class="spc-field">
|
||||
<mat-label>{{ 'labels.inputs.Currency in multiples of' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Amount to be rounded off' | translate }}"
|
||||
formControlName="inMultiplesOf"
|
||||
/>
|
||||
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
|
||||
<mat-select
|
||||
formControlName="currencyCode"
|
||||
matTooltip="{{ 'tooltips.The currency to be used' | 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="spc-field">
|
||||
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
|
||||
<mat-select
|
||||
formControlName="digitsAfterDecimal"
|
||||
matTooltip="{{ 'tooltips.Track and report saving account' | 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="spc-alert">
|
||||
<div class="spc-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="spc-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="spc-alert">
|
||||
<div class="spc-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="spc-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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+51
-7
@@ -44,27 +44,71 @@ $gray-900: #202124;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-section__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: #eff6ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.spc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4px 16px;
|
||||
padding: 16px 24px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spc-field {
|
||||
width: 100%;
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.spc-checkbox-row {
|
||||
.spc-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;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-45
@@ -6,11 +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 { Component, OnInit, Input, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@@ -21,19 +20,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatTooltip,
|
||||
FaIconComponent,
|
||||
MatCheckbox
|
||||
MatRadioGroup,
|
||||
MatRadioButton,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
@Input() savingProductsTemplate: any;
|
||||
|
||||
savingProductCurrencyForm: UntypedFormGroup;
|
||||
|
||||
currencyData: any;
|
||||
decimalPlacesData = [
|
||||
2,
|
||||
0,
|
||||
3
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.createSavingProductCurrencyForm();
|
||||
@@ -43,13 +47,11 @@ export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
this.currencyData = this.savingProductsTemplate.currencyOptions;
|
||||
|
||||
this.savingProductCurrencyForm.patchValue({
|
||||
currencyCode: this.savingProductsTemplate.currency.code || this.currencyData[0].code,
|
||||
digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? '',
|
||||
setMultiples: !!this.savingProductsTemplate.inMultiplesOf,
|
||||
inMultiplesOf: this.savingProductsTemplate.inMultiplesOf ?? ''
|
||||
currencyCode: this.savingProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
|
||||
digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? 2,
|
||||
dormancyAlert: this.savingProductsTemplate.isDormancyTrackingActive ?? false,
|
||||
nominationFacility: this.savingProductsTemplate.nominationFacility ?? false
|
||||
});
|
||||
|
||||
this.setupConditionalValidation();
|
||||
}
|
||||
|
||||
createSavingProductCurrencyForm() {
|
||||
@@ -59,39 +61,14 @@ export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
Validators.required
|
||||
],
|
||||
digitsAfterDecimal: [
|
||||
'',
|
||||
2,
|
||||
[
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
]
|
||||
],
|
||||
setMultiples: [false],
|
||||
inMultiplesOf: ['']
|
||||
});
|
||||
}
|
||||
|
||||
setupConditionalValidation() {
|
||||
const inMultiplesOfControl = this.savingProductCurrencyForm.get('inMultiplesOf');
|
||||
const setMultiplesControl = this.savingProductCurrencyForm.get('setMultiples');
|
||||
if (setMultiplesControl?.value) {
|
||||
inMultiplesOfControl?.setValidators([
|
||||
Validators.required,
|
||||
Validators.min(1)
|
||||
]);
|
||||
inMultiplesOfControl?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => {
|
||||
if (checked) {
|
||||
inMultiplesOfControl?.setValidators([
|
||||
Validators.required,
|
||||
Validators.min(1)
|
||||
]);
|
||||
} else {
|
||||
inMultiplesOfControl?.clearValidators();
|
||||
inMultiplesOfControl?.setValue('');
|
||||
}
|
||||
inMultiplesOfControl?.updateValueAndValidity();
|
||||
dormancyAlert: [false],
|
||||
nominationFacility: [false]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,13 +76,12 @@ export class SavingProductCurrencyStepComponent implements OnInit {
|
||||
const formValue = this.savingProductCurrencyForm.value;
|
||||
const result: any = {
|
||||
currencyCode: formValue.currencyCode,
|
||||
digitsAfterDecimal: formValue.digitsAfterDecimal
|
||||
digitsAfterDecimal: formValue.digitsAfterDecimal,
|
||||
dormancyAlert: formValue.dormancyAlert,
|
||||
isDormancyTrackingActive: formValue.dormancyAlert,
|
||||
nominationFacility: formValue.nominationFacility
|
||||
};
|
||||
|
||||
if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) {
|
||||
result.inMultiplesOf = formValue.inMultiplesOf;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -54,4 +54,64 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spd-section">
|
||||
<div class="spd-section__header">
|
||||
<i class="fas fa-tags"></i>
|
||||
<h3>Product Classification</h3>
|
||||
</div>
|
||||
|
||||
<div class="spd-grid spd-grid--classification">
|
||||
<mat-form-field appearance="outline" class="spd-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="spd-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="spd-field">
|
||||
<mat-label>Product Code</mat-label>
|
||||
<input matInput formControlName="productCode" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="spd-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="spd-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="spd-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="spd-field spd-field--full spd-checkbox-row">
|
||||
<mat-checkbox formControlName="shareLinkingEnabled">Share Linking Enabled</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+11
-1
@@ -46,11 +46,15 @@ $gray-900: #202124;
|
||||
gap: 4px 16px;
|
||||
padding: 16px 24px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.spd-grid--classification {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.spd-field {
|
||||
width: 100%;
|
||||
|
||||
@@ -58,3 +62,9 @@ $gray-900: #202124;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.spd-checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
+130
-9
@@ -6,11 +6,13 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, Input, inject } from '@angular/core';
|
||||
import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
@@ -24,12 +26,29 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class SavingProductDetailsStepComponent implements OnInit {
|
||||
export class SavingProductDetailsStepComponent implements OnInit, OnChanges {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private dateUtils = inject(Dates);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
@Input() savingProductsTemplate: any;
|
||||
|
||||
savingProductDetailsForm: UntypedFormGroup;
|
||||
accountTypeData: string[] = [];
|
||||
customerSegmentData: string[] = [];
|
||||
yesNoOptions: Array<{
|
||||
value: boolean;
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
value: true,
|
||||
label: 'Yes'
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: 'No'
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.createSavingProductDetailsForm();
|
||||
@@ -37,11 +56,13 @@ export class SavingProductDetailsStepComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
if (this.savingProductsTemplate) {
|
||||
this.savingProductDetailsForm.patchValue({
|
||||
name: this.savingProductsTemplate.name,
|
||||
shortName: this.savingProductsTemplate.shortName,
|
||||
description: this.savingProductsTemplate.description
|
||||
});
|
||||
this.applyTemplate(this.savingProductsTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.savingProductsTemplate?.currentValue) {
|
||||
this.applyTemplate(changes.savingProductsTemplate.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +76,111 @@ export class SavingProductDetailsStepComponent implements OnInit {
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
description: ['']
|
||||
description: [''],
|
||||
accountType: [
|
||||
null,
|
||||
Validators.required
|
||||
],
|
||||
customerSegment: [null],
|
||||
productCode: ['SB-IND-001'],
|
||||
effectiveDate: [''],
|
||||
effectiveTo: [''],
|
||||
endDate: [''],
|
||||
introducerRequired: [false],
|
||||
shareLinkingEnabled: [false],
|
||||
nominationFacility: [false],
|
||||
dormancyAlert: [false]
|
||||
});
|
||||
}
|
||||
|
||||
private getOptionNames(options: any[]): string[] {
|
||||
if (!Array.isArray(options)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return options
|
||||
.map((option) => option?.name ?? option?.value ?? option)
|
||||
.filter((value) => typeof value === 'string' && value.trim().length > 0);
|
||||
}
|
||||
|
||||
private applyTemplate(template: any) {
|
||||
const accountTypeOptions = this.getOptionNames(template.accountTypeOptions);
|
||||
const customerSegmentOptions = this.getOptionNames(template.customerSegmentOptions);
|
||||
const yesNoOptions = this.getYesNoOptions(template.yesNoOptions);
|
||||
|
||||
this.accountTypeData = accountTypeOptions;
|
||||
this.customerSegmentData = customerSegmentOptions;
|
||||
if (yesNoOptions.length) {
|
||||
this.yesNoOptions = yesNoOptions;
|
||||
}
|
||||
|
||||
// Use existing product data if available (when editing), otherwise use template defaults
|
||||
const isEditing = !!template.id;
|
||||
this.savingProductDetailsForm.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,
|
||||
productCode: template.productCode ?? 'SB-IND-001',
|
||||
effectiveDate: template.effectiveDate ? new Date(template.effectiveDate) : '',
|
||||
effectiveTo: template.effectiveTo ? new Date(template.effectiveTo) : '',
|
||||
endDate: template.endDate ? new Date(template.endDate) : '',
|
||||
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
|
||||
shareLinkingEnabled: !!template.shareLinkingEnabled,
|
||||
nominationFacility: !!template.nominationFacility,
|
||||
dormancyAlert: !!template.dormancyAlert
|
||||
});
|
||||
}
|
||||
|
||||
private getYesNoOptions(options: any[]): Array<{ value: boolean; label: string }> {
|
||||
if (!Array.isArray(options)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return options
|
||||
.map((option) => {
|
||||
const rawValue = option?.name ?? option?.value ?? option;
|
||||
const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue;
|
||||
|
||||
if (normalized === 'YES') {
|
||||
return {
|
||||
value: true,
|
||||
label: 'Yes'
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized === 'NO') {
|
||||
return {
|
||||
value: false,
|
||||
label: 'No'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((value) => !!value);
|
||||
}
|
||||
|
||||
get savingProductDetails() {
|
||||
return this.savingProductDetailsForm.value;
|
||||
const formValue = { ...this.savingProductDetailsForm.value };
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const prevEffectiveDate: Date = this.savingProductDetailsForm.value.effectiveDate;
|
||||
const prevEffectiveTo: Date = this.savingProductDetailsForm.value.effectiveTo;
|
||||
const prevEndDate: Date = this.savingProductDetailsForm.value.endDate;
|
||||
|
||||
if (formValue.effectiveDate instanceof Date) {
|
||||
formValue.effectiveDate = this.dateUtils.formatDate(prevEffectiveDate, dateFormat) || '';
|
||||
}
|
||||
|
||||
if (formValue.effectiveTo instanceof Date) {
|
||||
formValue.effectiveTo = this.dateUtils.formatDate(prevEffectiveTo, dateFormat) || '';
|
||||
}
|
||||
|
||||
if (formValue.endDate instanceof Date) {
|
||||
formValue.endDate = this.dateUtils.formatDate(prevEndDate, dateFormat) || '';
|
||||
}
|
||||
|
||||
return formValue;
|
||||
}
|
||||
}
|
||||
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<div class="spl-section">
|
||||
<div class="spl-section__header">
|
||||
<span class="spl-section__icon">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
</span>
|
||||
<div class="spl-section__title">Transaction & Product Limits</div>
|
||||
<div class="spl-section__badge">R&D BASED DEFAULTS</div>
|
||||
</div>
|
||||
|
||||
<div class="spl-section__body">
|
||||
<div class="spl-features">
|
||||
<div class="spl-features__header">Account Features (as per RBI guidelines)</div>
|
||||
|
||||
<div class="spl-features__grid">
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-automobile"></i>
|
||||
ATM Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.atmFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-wallet"></i>
|
||||
ATM Withdrawal
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.atmWithdrawalFacility"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
UPI Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.upiFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
POS Transactions
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.posTransactionsFacility"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-book"></i>
|
||||
Cheque Book
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.chequeBook" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-bolt"></i>
|
||||
IMPS Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.impsFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-sms"></i>
|
||||
SMS Alerts
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.smsAlerts" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-bell"></i>
|
||||
BBPS Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.bbpsFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
Insurance Cover
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.insuranceCover" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-bolt"></i>
|
||||
BEPG Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.bepgFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-money-check-alt"></i>
|
||||
NEFT Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.neftFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-money-check"></i>
|
||||
RTGS Facility
|
||||
</span>
|
||||
<select class="spl-select" [(ngModel)]="accountFacilityConfig.rtgsFacility" (ngModelChange)="markAsDirty()">
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-balance-scale"></i>
|
||||
Inter-Bank Total Cap
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.interBankTotalCapFacility"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-arrow-right-arrow-left"></i>
|
||||
Withdrawal Facility
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.withdrawalFacility"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Deposit Facility (KYC)
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.depositFacilityKyc"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="spl-feature">
|
||||
<span class="spl-feature__label">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
Virtual Debit Card
|
||||
</span>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="accountFacilityConfig.virtualDebitCard"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option [ngValue]="false">No</option>
|
||||
<option [ngValue]="true">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spl-table-wrap">
|
||||
<table class="spl-table">
|
||||
<colgroup>
|
||||
<col class="spl-table__col--label" />
|
||||
<col class="spl-table__col--value" />
|
||||
<col class="spl-table__col--value" />
|
||||
<col class="spl-table__col--value" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Limit Type</th>
|
||||
<th>General Limit (Daily)</th>
|
||||
<th>Minor Account Limit</th>
|
||||
<th>Small Saving Limit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of accountFacilityLimits; track row.limitType) {
|
||||
<tr>
|
||||
<td>{{ row.limitType }}</td>
|
||||
<td>
|
||||
<input type="text" class="spl-input" [(ngModel)]="row.generalLimit" (ngModelChange)="markAsDirty()" />
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="spl-input"
|
||||
[(ngModel)]="row.minorAccountLimit"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
[disabled]="row.limitType === 'RTGS Limit (Min 2L)'"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="spl-input"
|
||||
[(ngModel)]="row.smallSavingLimit"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
[disabled]="row.limitType === 'RTGS Limit (Min 2L)'"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="spl-table-wrap spl-table-wrap--kyc">
|
||||
<h4 class="mat-h4">Deposit Limit (KYC)</h4>
|
||||
<table class="spl-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account Type</th>
|
||||
<th>KYC Type</th>
|
||||
<th>Limit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>General</td>
|
||||
<td>
|
||||
<select class="spl-select" [(ngModel)]="depositLimitKyc.general.kycType" (ngModelChange)="markAsDirty()">
|
||||
<option>Full KYC</option>
|
||||
<option>Min KYC</option>
|
||||
<option>No KYC</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="spl-input"
|
||||
[(ngModel)]="depositLimitKyc.general.limit"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Minor</td>
|
||||
<td>
|
||||
<select class="spl-select" [(ngModel)]="depositLimitKyc.minor.kycType" (ngModelChange)="markAsDirty()">
|
||||
<option>Guardian KYC</option>
|
||||
<option>Full KYC</option>
|
||||
<option>Min KYC</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
class="spl-input"
|
||||
[(ngModel)]="depositLimitKyc.minor.limit"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Small Saving</td>
|
||||
<td>
|
||||
<select
|
||||
class="spl-select"
|
||||
[(ngModel)]="depositLimitKyc.smallSaving.kycType"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
>
|
||||
<option>Min KYC</option>
|
||||
<option>Full KYC</option>
|
||||
<option>Guardian KYC</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
class="spl-input"
|
||||
[(ngModel)]="depositLimitKyc.smallSaving.limit"
|
||||
(ngModelChange)="markAsDirty()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="spl-note">
|
||||
<strong>Note:</strong>
|
||||
Small Saving Account limits are strictly capped at Rs.1,00,000 maximum balance and Rs.10,000 monthly withdrawal
|
||||
per RBI Master Circular. Minor accounts are restricted from RTGS and high-risk digital products (BEPG/BBPS) by
|
||||
default.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$border: #d9e2ec;
|
||||
$border-strong: #c7d2de;
|
||||
$text: #24364b;
|
||||
$muted: #64748b;
|
||||
$header-bg: #f8fafc;
|
||||
$highlight-bg: #eff6ff;
|
||||
|
||||
.spl-section {
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spl-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: $header-bg;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.spl-section__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-size: 14px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.spl-section__title {
|
||||
flex: 1 1 auto;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.spl-section__badge {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spl-section__body {
|
||||
padding: 18px 20px 20px;
|
||||
}
|
||||
|
||||
.spl-subsection {
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
& + & {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.spl-subsection__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: $header-bg;
|
||||
border-bottom: 1px solid $border;
|
||||
color: $text;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spl-subsection__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-size: 13px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.spl-subsection__body {
|
||||
padding: 16px 16px 18px;
|
||||
}
|
||||
|
||||
.spl-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.spl-table {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid $border;
|
||||
|
||||
col.spl-table__col--label {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
col.spl-table__col--value {
|
||||
width: 26%;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid $border;
|
||||
padding: 6px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.spl-table__highlight td:first-child {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.spl-input,
|
||||
.spl-select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.spl-input {
|
||||
height: 30px;
|
||||
border: 1px solid $border-strong;
|
||||
border-radius: 2px;
|
||||
padding: 0 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spl-input:disabled,
|
||||
.spl-select:disabled {
|
||||
background: #f8fafc;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
.spl-select {
|
||||
height: 30px;
|
||||
border: 1px solid $border-strong;
|
||||
border-radius: 2px;
|
||||
padding: 0 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spl-input--stacked {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.spl-input--disabled {
|
||||
background: #f8fafc;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
.spl-table__muted {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.spl-note {
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: $muted;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.spl-features {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.spl-features__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
|
||||
@media (width <= 900px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.spl-feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.spl-feature__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 18px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
|
||||
i {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
type BooleanFeatureKey =
|
||||
| 'atmFacility'
|
||||
| 'atmWithdrawalFacility'
|
||||
| 'upiFacility'
|
||||
| 'posTransactionsFacility'
|
||||
| 'chequeBook'
|
||||
| 'impsFacility'
|
||||
| 'smsAlerts'
|
||||
| 'bbpsFacility'
|
||||
| 'insuranceCover'
|
||||
| 'bepgFacility'
|
||||
| 'neftFacility'
|
||||
| 'rtgsFacility'
|
||||
| 'interBankTotalCapFacility'
|
||||
| 'withdrawalFacility'
|
||||
| 'depositFacilityKyc'
|
||||
| 'virtualDebitCard';
|
||||
|
||||
interface FacilityLimitRow {
|
||||
limitType: string;
|
||||
generalLimit: string | number | null;
|
||||
minorAccountLimit: string | number | null;
|
||||
smallSavingLimit: string | number | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-saving-product-limits-step',
|
||||
templateUrl: './saving-product-limits-step.component.html',
|
||||
styleUrls: ['./saving-product-limits-step.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class SavingProductLimitsStepComponent implements OnChanges {
|
||||
@Input() savingProductsTemplate: any;
|
||||
pristine = true;
|
||||
|
||||
yesNoOptions: Array<{
|
||||
value: boolean;
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
value: true,
|
||||
label: 'Yes'
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: 'No'
|
||||
}
|
||||
];
|
||||
|
||||
accountFacilityConfig: Record<BooleanFeatureKey, boolean> = {
|
||||
atmFacility: true,
|
||||
atmWithdrawalFacility: true,
|
||||
upiFacility: true,
|
||||
posTransactionsFacility: true,
|
||||
chequeBook: true,
|
||||
impsFacility: true,
|
||||
smsAlerts: true,
|
||||
bbpsFacility: true,
|
||||
insuranceCover: false,
|
||||
bepgFacility: true,
|
||||
neftFacility: true,
|
||||
rtgsFacility: true,
|
||||
interBankTotalCapFacility: true,
|
||||
withdrawalFacility: true,
|
||||
depositFacilityKyc: true,
|
||||
virtualDebitCard: false
|
||||
};
|
||||
|
||||
accountFacilityLimits: FacilityLimitRow[] = [
|
||||
{
|
||||
limitType: 'ATM Withdrawal',
|
||||
generalLimit: 40000,
|
||||
minorAccountLimit: 5000,
|
||||
smallSavingLimit: 10000
|
||||
},
|
||||
{
|
||||
limitType: 'POS Transactions',
|
||||
generalLimit: 100000,
|
||||
minorAccountLimit: 2000,
|
||||
smallSavingLimit: 10000
|
||||
},
|
||||
{
|
||||
limitType: 'UPI Limit',
|
||||
generalLimit: 50000,
|
||||
minorAccountLimit: 1000,
|
||||
smallSavingLimit: 5000
|
||||
},
|
||||
{
|
||||
limitType: 'IMPS Limit',
|
||||
generalLimit: 50000,
|
||||
minorAccountLimit: 100,
|
||||
smallSavingLimit: 5000
|
||||
},
|
||||
{
|
||||
limitType: 'BBPS',
|
||||
generalLimit: 25000,
|
||||
minorAccountLimit: 0,
|
||||
smallSavingLimit: 2000
|
||||
},
|
||||
{
|
||||
limitType: 'BEPG',
|
||||
generalLimit: 25000,
|
||||
minorAccountLimit: 0,
|
||||
smallSavingLimit: 2000
|
||||
},
|
||||
{
|
||||
limitType: 'NEFT Limit',
|
||||
generalLimit: 1000000,
|
||||
minorAccountLimit: 25000,
|
||||
smallSavingLimit: 10000
|
||||
},
|
||||
{
|
||||
limitType: 'RTGS Limit (Min 2L)',
|
||||
generalLimit: 5000000,
|
||||
minorAccountLimit: 'N/A',
|
||||
smallSavingLimit: 'N/A'
|
||||
},
|
||||
{
|
||||
limitType: 'Inter-Bank Total Cap',
|
||||
generalLimit: 5000000,
|
||||
minorAccountLimit: 25000,
|
||||
smallSavingLimit: 10000
|
||||
},
|
||||
{
|
||||
limitType: 'Withdrawal Limit',
|
||||
generalLimit: 'UNLIMITED',
|
||||
minorAccountLimit: 10000,
|
||||
smallSavingLimit: 10000
|
||||
}
|
||||
];
|
||||
|
||||
depositLimitKyc = {
|
||||
general: {
|
||||
kycType: 'Full KYC',
|
||||
limit: 'No Limit'
|
||||
},
|
||||
minor: {
|
||||
kycType: 'Guardian KYC',
|
||||
limit: 100000
|
||||
},
|
||||
smallSaving: {
|
||||
kycType: 'Min KYC',
|
||||
limit: 50000
|
||||
}
|
||||
};
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.savingProductsTemplate?.currentValue) {
|
||||
const template = changes.savingProductsTemplate.currentValue;
|
||||
this.setYesNoOptions(template);
|
||||
this.populateFromTemplate(template);
|
||||
}
|
||||
}
|
||||
|
||||
markAsDirty() {
|
||||
this.pristine = false;
|
||||
}
|
||||
|
||||
private populateFromTemplate(template: any) {
|
||||
const isEditing = !!template.id;
|
||||
|
||||
// Populate accountFacilityConfig from existing product
|
||||
if (isEditing && template.accountFacilityConfig) {
|
||||
this.accountFacilityConfig = { ...this.accountFacilityConfig, ...template.accountFacilityConfig };
|
||||
}
|
||||
|
||||
// Populate accountFacilityLimits from existing product
|
||||
if (isEditing && template.accountFacilityLimits && template.accountFacilityLimits.length > 0) {
|
||||
this.accountFacilityLimits = template.accountFacilityLimits.map((limit: any) => ({
|
||||
limitType: limit.limitType || '',
|
||||
generalLimit: limit.generalLimit ?? null,
|
||||
minorAccountLimit: limit.minorAccountLimit ?? null,
|
||||
smallSavingLimit: limit.smallSavingLimit ?? null
|
||||
}));
|
||||
}
|
||||
|
||||
// Populate depositLimitKyc from existing product
|
||||
if (isEditing && template.depositLimitKyc) {
|
||||
this.depositLimitKyc = {
|
||||
general: {
|
||||
kycType: template.depositLimitKyc.general?.kycType || 'Full KYC',
|
||||
limit: template.depositLimitKyc.general?.limit || 'No Limit'
|
||||
},
|
||||
minor: {
|
||||
kycType: template.depositLimitKyc.minor?.kycType || 'Guardian KYC',
|
||||
limit: template.depositLimitKyc.minor?.limit ?? 100000
|
||||
},
|
||||
smallSaving: {
|
||||
kycType: template.depositLimitKyc.smallSaving?.kycType || 'Min KYC',
|
||||
limit: template.depositLimitKyc.smallSaving?.limit ?? 50000
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private setYesNoOptions(template: any) {
|
||||
const options = Array.isArray(template?.yesNoOptions) ? template.yesNoOptions : [];
|
||||
const mappedOptions = options
|
||||
.map((option: any) => {
|
||||
const rawValue = option?.name ?? option?.value ?? option;
|
||||
const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue;
|
||||
|
||||
if (normalized === 'YES') {
|
||||
return {
|
||||
value: true,
|
||||
label: 'Yes'
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized === 'NO') {
|
||||
return {
|
||||
value: false,
|
||||
label: 'No'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((value: any) => !!value);
|
||||
|
||||
if (mappedOptions.length) {
|
||||
this.yesNoOptions = mappedOptions;
|
||||
}
|
||||
}
|
||||
|
||||
get savingProductLimits() {
|
||||
return {
|
||||
accountFacilityConfig: { ...this.accountFacilityConfig },
|
||||
accountFacilityLimits: this.accountFacilityLimits.map((row) => ({ ...row })),
|
||||
depositLimitKyc: {
|
||||
...this.depositLimitKyc
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+495
-17
@@ -25,25 +25,107 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (
|
||||
savingProduct.accountType ||
|
||||
savingProduct.customerSegment ||
|
||||
savingProduct.productCode ||
|
||||
savingProduct.effectiveTo ||
|
||||
savingProduct.introducerRequired !== undefined ||
|
||||
savingProduct.shareLinkingEnabled !== undefined
|
||||
) {
|
||||
<h3 class="mat-h3 flex-100">Product Classification</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
|
||||
@if (savingProduct.accountType) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Account Type:</span>
|
||||
<span class="flex-60">{{ savingProduct.accountType }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.customerSegment) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Customer Segment:</span>
|
||||
<span class="flex-60">{{ savingProduct.customerSegment }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.productCode) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Product Code:</span>
|
||||
<span class="flex-60">{{ savingProduct.productCode }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.effectiveTo) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Effective To:</span>
|
||||
<span class="flex-60">{{ savingProduct.effectiveTo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.endDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">End Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.endDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.introducerRequired !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Introducer Required:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.introducerRequired === true || savingProduct.introducerRequired === 'Yes' ? 'Yes' : 'No'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.shareLinkingEnabled !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Share Linking Enabled:</span>
|
||||
<span class="flex-60">{{ savingProduct.shareLinkingEnabled | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.currencyCode | find: savingProductsTemplate.currencyOptions : 'code' : 'name'
|
||||
savingProduct.currency?.name ||
|
||||
savingProduct.currency?.displayLabel ||
|
||||
(savingProduct.currencyCode | find: savingProductsTemplate?.currencyOptions : 'code' : 'name') ||
|
||||
savingProduct.currencyCode ||
|
||||
'-'
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.digitsAfterDecimal ?? savingProduct.currency?.decimalPlaces }}</span>
|
||||
<span class="flex-60">{{ savingProduct.digitsAfterDecimal ?? savingProduct.currency?.decimalPlaces ?? 2 }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.inMultiplesOf) {
|
||||
@if (savingProduct.currency?.inMultiplesOf !== undefined && savingProduct.currency?.inMultiplesOf !== null) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.inMultiplesOf }}</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.inMultiplesOf }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.dormancyAlert !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Dormancy Alert:</span>
|
||||
<span class="flex-60">{{ savingProduct.dormancyAlert | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.nominationFacility !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Nomination Facility:</span>
|
||||
<span class="flex-60">{{ savingProduct.nominationFacility | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -59,7 +141,7 @@
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.interestCompoundingPeriodType
|
||||
| find: savingProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value'
|
||||
| find: savingProductsTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value'
|
||||
| translateKey: 'catalogs'
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -68,16 +150,16 @@
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.interestPostingPeriodType
|
||||
| find: savingProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value'
|
||||
| find: savingProductsTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
|
||||
| translateKey: 'catalogs'
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.interestCalculationType
|
||||
| find: savingProductsTemplate.interestCalculationTypeOptions : 'id' : 'value'
|
||||
| find: interestCalculationTypeOptionData : 'id' : 'value'
|
||||
| translateKey: 'catalogs'
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -86,11 +168,56 @@
|
||||
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.interestCalculationDaysInYearType
|
||||
| find: savingProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
|
||||
| find: savingProductsTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
|
||||
| translateKey: 'catalogs'
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.effectiveDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Effective Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.effectiveDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Interest Calculate:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.interestCalculateSelector
|
||||
| find: savingProductsTemplate?.interestCalculationTypeOptions : 'id' : 'value'
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.averagePeriod) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Average Period:</span>
|
||||
<span class="flex-60">{{ savingProduct.averagePeriod }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Auto Interest Post:</span>
|
||||
<span class="flex-60">{{ savingProduct.autoPost | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (
|
||||
savingProduct.frequencyFixDay !== undefined &&
|
||||
savingProduct.frequencyFixDay !== null &&
|
||||
savingProduct.frequencyFixDay !== ''
|
||||
) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Frequency (Fix Day):</span>
|
||||
<span class="flex-60">{{ savingProduct.frequencyFixDay }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.lastInterestDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Last Interest Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.lastInterestDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Settings' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
@@ -108,7 +235,8 @@
|
||||
savingProduct.lockinPeriodFrequency +
|
||||
' ' +
|
||||
(savingProduct.lockinPeriodFrequencyType
|
||||
| find: savingProductsTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value') | translateKey: 'catalogs'
|
||||
| find: savingProductsTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value')
|
||||
| translateKey: 'catalogs'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@@ -118,6 +246,28 @@
|
||||
<span class="flex-60">{{ savingProduct.withdrawalFeeForTransfers | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.balanceFrequency) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Balance Frequency:</span>
|
||||
<span class="flex-60">{{ savingProduct.balanceFrequency }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Enable Lock-in Period:</span>
|
||||
<span class="flex-60">{{ savingProduct.enableLockinPeriod | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.enableLockinPeriod && savingProduct.lockinPeriodFrequency) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Lock-in Period:</span>
|
||||
<span class="flex-60">
|
||||
{{ savingProduct.lockinPeriodFrequency }}
|
||||
{{ getLockinPeriodFrequencyTypeLabel(savingProduct.lockinPeriodFrequencyType) }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.minBalanceForInterestCalculation) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
|
||||
@@ -146,7 +296,7 @@
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Tax Group' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.taxGroupId | find: savingProductsTemplate.taxGroupOptions : 'id' : 'name'
|
||||
savingProduct.taxGroupId | find: savingProductsTemplate?.taxGroupOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@@ -156,6 +306,11 @@
|
||||
<span class="flex-60">{{ savingProduct.allowOverdraft | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Account Closed with Zero Balance in Day Closing:</span>
|
||||
<span class="flex-60">{{ savingProduct.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.minOverdraftForInterestCalculation) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40"
|
||||
@@ -195,6 +350,77 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">Account Facilities</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
@if (savingProduct.accountFacilityConfig) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
@for (feature of accountFacilityFeatureItems; track feature.key) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ feature.label }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.accountFacilityConfig[feature.key] | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.accountFacilityLimits?.length) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<h4 class="mat-h4 flex-100">Account Facility Limits</h4>
|
||||
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="savingProduct.accountFacilityLimits">
|
||||
<ng-container matColumnDef="limitType">
|
||||
<th mat-header-cell *matHeaderCellDef>Limit Type</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.limitType }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="generalLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>General Limit</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.generalLimit }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="minorAccountLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>Minor Account Limit</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.minorAccountLimit }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="smallSavingLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>Small Saving Limit</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.smallSavingLimit }}</td>
|
||||
</ng-container>
|
||||
<tr
|
||||
mat-header-row
|
||||
*matHeaderRowDef="['limitType', 'generalLimit', 'minorAccountLimit', 'smallSavingLimit']"
|
||||
></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: ['limitType', 'generalLimit', 'minorAccountLimit', 'smallSavingLimit']"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.depositLimitKyc) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<h4 class="mat-h4 flex-100">Deposit Limit (KYC)</h4>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">General:</span>
|
||||
<span class="flex-60"
|
||||
>{{ savingProduct.depositLimitKyc.general.kycType }} - {{ savingProduct.depositLimitKyc.general.limit }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Minor:</span>
|
||||
<span class="flex-60"
|
||||
>{{ savingProduct.depositLimitKyc.minor.kycType }} - {{ savingProduct.depositLimitKyc.minor.limit }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Small Saving:</span>
|
||||
<span class="flex-60">
|
||||
{{ savingProduct.depositLimitKyc.smallSaving.kycType }} -
|
||||
{{ savingProduct.depositLimitKyc.smallSaving.limit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.charges.length) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
|
||||
@@ -233,9 +459,262 @@
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
@if (isCashOrAccrualAccounting()) {
|
||||
<div class="spa-gl-summary flex-100">
|
||||
<div class="spa-gl-summary__header">
|
||||
<h4 class="mat-h4">GL Mapping</h4>
|
||||
</div>
|
||||
|
||||
<mifosx-view-savings-accounting-details [accountingRule]="accountingRule" [accountingMappings]="accountingMappings">
|
||||
</mifosx-view-savings-accounting-details>
|
||||
<div class="spa-gl-section">
|
||||
<h5 class="mat-h5">Assets</h5>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Savings reference account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.savingsReferenceAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Overdraft portfolio control</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.overdraftPortfolioControlId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Fees receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.feesReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Penalties receivable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.penaltiesReceivableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.assetAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spa-gl-section">
|
||||
<h5 class="mat-h5">Liabilities</h5>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Savings control account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.savingsControlAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Transfers in suspense account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.transfersInSuspenseAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest payable account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestPayableAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@if (savingProduct.isDormancyTrackingActive) {
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Escheat liability account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.escheatLiabilityId,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spa-gl-section">
|
||||
<h5 class="mat-h5">Expenses</h5>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Interest on savings account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.interestOnSavingsAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Write off account</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.writeOffAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spa-gl-section">
|
||||
<h5 class="mat-h5">Income</h5>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Income from fees</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.incomeFromFeeAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Income from penalties</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.incomeFromPenaltyAccountId,
|
||||
savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="spa-gl-row">
|
||||
<span class="spa-gl-row__label">Income from interest</span>
|
||||
<span class="spa-gl-row__value">{{
|
||||
formatGlAccountById(
|
||||
savingProduct.incomeFromInterestId,
|
||||
savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (savingProduct.otherGlMappings) {
|
||||
<div class="spa-other-gl-summary flex-100">
|
||||
<div class="spa-gl-summary__header">
|
||||
<h4 class="mat-h4">Other GL Mappings</h4>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.otherGlMappings.assets?.length) {
|
||||
<table
|
||||
class="spa-other-gl-table flex-100 mat-elevation-z1"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.otherGlMappings.assets"
|
||||
>
|
||||
<ng-container matColumnDef="accountName">
|
||||
<th mat-header-cell *matHeaderCellDef>Asset Account</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glCode">
|
||||
<th mat-header-cell *matHeaderCellDef>GL Account</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
{{
|
||||
formatGlAccountByCode(row.glCode, savingProductsTemplate?.accountingMappingOptions.assetAccountOptions)
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (savingProduct.otherGlMappings.liabilities?.length) {
|
||||
<table
|
||||
class="spa-other-gl-table flex-100 mat-elevation-z1"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.otherGlMappings.liabilities"
|
||||
>
|
||||
<ng-container matColumnDef="accountName">
|
||||
<th mat-header-cell *matHeaderCellDef>Liability Account</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glCode">
|
||||
<th mat-header-cell *matHeaderCellDef>GL Account</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
{{
|
||||
formatGlAccountByCode(
|
||||
row.glCode,
|
||||
savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (savingProduct.otherGlMappings.expenses?.length) {
|
||||
<table
|
||||
class="spa-other-gl-table flex-100 mat-elevation-z1"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.otherGlMappings.expenses"
|
||||
>
|
||||
<ng-container matColumnDef="accountName">
|
||||
<th mat-header-cell *matHeaderCellDef>Expense Account</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glCode">
|
||||
<th mat-header-cell *matHeaderCellDef>GL Account</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
{{
|
||||
formatGlAccountByCode(
|
||||
row.glCode,
|
||||
savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (savingProduct.otherGlMappings.income?.length) {
|
||||
<table
|
||||
class="spa-other-gl-table flex-100 mat-elevation-z1"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.otherGlMappings.income"
|
||||
>
|
||||
<ng-container matColumnDef="accountName">
|
||||
<th mat-header-cell *matHeaderCellDef>Income Account</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glCode">
|
||||
<th mat-header-cell *matHeaderCellDef>GL Account</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
{{
|
||||
formatGlAccountByCode(row.glCode, savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions)
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="['accountName', 'glCode']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glCode']"></tr>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (
|
||||
savingProduct.advancedAccountingRules &&
|
||||
@@ -265,7 +744,7 @@
|
||||
<td mat-cell *matCellDef="let paymentFundSource">
|
||||
{{
|
||||
paymentFundSource.fundSourceAccountId
|
||||
| find: savingProductsTemplate.accountingMappingOptions.assetAccountOptions : 'id' : 'name'
|
||||
| find: savingProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name'
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -289,7 +768,7 @@
|
||||
<td mat-cell *matCellDef="let feesIncome">
|
||||
{{
|
||||
feesIncome.incomeAccountId
|
||||
| find: savingProductsTemplate.accountingMappingOptions.incomeAccountOptions : 'id' : 'name'
|
||||
| find: savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name'
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -317,7 +796,7 @@
|
||||
<td mat-cell *matCellDef="let penaltyIncome">
|
||||
{{
|
||||
penaltyIncome.incomeAccountId
|
||||
| find: savingProductsTemplate.accountingMappingOptions.incomeAccountOptions : 'id' : 'name'
|
||||
| find: savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name'
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -329,4 +808,3 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
+92
@@ -71,3 +71,95 @@ mat-divider {
|
||||
.margin-t {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.spa-gl-summary,
|
||||
.spa-other-gl-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.spa-gl-summary__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.spa-gl-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border: 1px solid #e3e7f2;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spa-gl-section h5 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #91a0bc;
|
||||
}
|
||||
|
||||
.spa-gl-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 40%) 1fr;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.spa-gl-row__label {
|
||||
color: #1f2f4a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spa-gl-row__value {
|
||||
color: #101828;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.spa-other-gl-table {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spa-other-gl-table:first-of-type {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.spa-other-gl-summary .mat-elevation-z1 {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spa-other-gl-table th.mat-mdc-header-cell,
|
||||
.spa-other-gl-table td.mat-mdc-cell {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.spa-other-gl-table th.mat-mdc-header-cell:first-child,
|
||||
.spa-other-gl-table td.mat-mdc-cell:first-child {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.spa-other-gl-table th.mat-mdc-header-cell:last-child,
|
||||
.spa-other-gl-table td.mat-mdc-cell:last-child {
|
||||
width: 62%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.spa-gl-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
+137
-48
@@ -22,7 +22,6 @@ import {
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { FindPipe } from '../../../../pipes/find.pipe';
|
||||
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
|
||||
@@ -46,7 +45,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
ViewSavingsAccountingDetailsComponent,
|
||||
FaIconComponent,
|
||||
FindPipe,
|
||||
FormatNumberPipe,
|
||||
@@ -79,6 +77,25 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
|
||||
accountingMappings: any = {};
|
||||
accountingRule: OptionData;
|
||||
interestCalculationTypeOptionData: any[] = [];
|
||||
accountFacilityFeatureItems = [
|
||||
{ key: 'atmFacility', label: 'ATM Facility' },
|
||||
{ key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' },
|
||||
{ key: 'upiFacility', label: 'UPI Facility' },
|
||||
{ key: 'posTransactionsFacility', label: 'POS Transactions Facility' },
|
||||
{ key: 'chequeBook', label: 'Cheque Book' },
|
||||
{ key: 'impsFacility', label: 'IMPS Facility' },
|
||||
{ key: 'smsAlerts', label: 'SMS Alerts' },
|
||||
{ key: 'bbpsFacility', label: 'BBPS Facility' },
|
||||
{ key: 'insuranceCover', label: 'Insurance Cover' },
|
||||
{ key: 'bepgFacility', label: 'BEPG Facility' },
|
||||
{ key: 'neftFacility', label: 'NEFT Facility' },
|
||||
{ key: 'rtgsFacility', label: 'RTGS Facility' },
|
||||
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' },
|
||||
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
|
||||
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
|
||||
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
|
||||
];
|
||||
|
||||
ngOnInit() {
|
||||
this.setCurrentValues();
|
||||
@@ -89,62 +106,88 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
setCurrentValues(): void {
|
||||
if (!this.savingProduct || !this.savingProductsTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.interestCalculationTypeOptionData =
|
||||
this.savingProductsTemplate.interestCalculationTypeOption ||
|
||||
this.savingProductsTemplate.interestCalculationTypeOptions ||
|
||||
[];
|
||||
this.accountingRule = this.accounting.getAccountingRuleFrom(this.savingProduct.accountingRule);
|
||||
|
||||
if (this.isCashOrAccrualAccounting()) {
|
||||
const assetAccountData = this.savingProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
|
||||
const incomeAccountData = this.savingProductsTemplate.accountingMappingOptions.incomeAccountOptions || [];
|
||||
const expenseAccountData = this.savingProductsTemplate.accountingMappingOptions.expenseAccountOptions || [];
|
||||
const liabilityAccountData = this.savingProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
|
||||
if (!this.isCashOrAccrualAccounting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetAccountData = this.savingProductsTemplate?.accountingMappingOptions?.assetAccountOptions || [];
|
||||
const incomeAccountData = this.savingProductsTemplate?.accountingMappingOptions?.incomeAccountOptions || [];
|
||||
const expenseAccountData = this.savingProductsTemplate?.accountingMappingOptions?.expenseAccountOptions || [];
|
||||
const liabilityAccountData = this.savingProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions || [];
|
||||
|
||||
this.accountingMappings = {
|
||||
savingsReferenceAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.savingsReferenceAccountId,
|
||||
assetAccountData
|
||||
),
|
||||
overdraftPortfolioControl: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.overdraftPortfolioControlId,
|
||||
assetAccountData
|
||||
),
|
||||
savingsControlAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.savingsControlAccountId,
|
||||
liabilityAccountData
|
||||
),
|
||||
transfersInSuspenseAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.transfersInSuspenseAccountId,
|
||||
liabilityAccountData
|
||||
),
|
||||
interestOnSavingsAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.interestOnSavingsAccountId,
|
||||
expenseAccountData
|
||||
),
|
||||
writeOffAccount: this.accounting.glAccountLookUp(this.savingProduct.writeOffAccountId, expenseAccountData),
|
||||
incomeFromFeeAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.incomeFromFeeAccountId,
|
||||
incomeAccountData
|
||||
),
|
||||
incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.incomeFromPenaltyAccountId,
|
||||
incomeAccountData
|
||||
),
|
||||
incomeFromInterest: this.accounting.glAccountLookUp(this.savingProduct.incomeFromInterestId, incomeAccountData)
|
||||
};
|
||||
|
||||
if (this.isAccrualAccounting()) {
|
||||
this.accountingMappings = {
|
||||
savingsReferenceAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.savingsReferenceAccountId,
|
||||
assetAccountData
|
||||
),
|
||||
overdraftPortfolioControl: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.overdraftPortfolioControlId,
|
||||
assetAccountData
|
||||
),
|
||||
savingsControlAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.savingsControlAccountId,
|
||||
liabilityAccountData
|
||||
),
|
||||
transfersInSuspenseAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.transfersInSuspenseAccountId,
|
||||
liabilityAccountData
|
||||
),
|
||||
escheatLiability: this.accounting.glAccountLookUp(this.savingProduct.escheatLiabilityId, liabilityAccountData),
|
||||
interestOnSavingsAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.interestOnSavingsAccountId,
|
||||
expenseAccountData
|
||||
),
|
||||
writeOffAccount: this.accounting.glAccountLookUp(this.savingProduct.writeOffAccountId, expenseAccountData),
|
||||
incomeFromFeeAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.incomeFromFeeAccountId,
|
||||
incomeAccountData
|
||||
),
|
||||
incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.incomeFromPenaltyAccountId,
|
||||
incomeAccountData
|
||||
),
|
||||
incomeFromInterest: this.accounting.glAccountLookUp(this.savingProduct.incomeFromInterestId, incomeAccountData)
|
||||
};
|
||||
|
||||
if (this.isAccrualAccounting()) {
|
||||
this.accountingMappings['feesReceivableAccount'] = this.accounting.glAccountLookUp(
|
||||
...this.accountingMappings,
|
||||
feeReceivableAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.feesReceivableAccountId,
|
||||
assetAccountData
|
||||
);
|
||||
this.accountingMappings['penaltiesReceivableAccount'] = this.accounting.glAccountLookUp(
|
||||
),
|
||||
penaltyReceivableAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.penaltiesReceivableAccountId,
|
||||
assetAccountData
|
||||
);
|
||||
this.accountingMappings['interestPayableAccount'] = this.accounting.glAccountLookUp(
|
||||
),
|
||||
interestPayableAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.interestPayableAccountId,
|
||||
liabilityAccountData
|
||||
);
|
||||
}
|
||||
),
|
||||
interestReceivableAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.interestReceivableAccountId,
|
||||
assetAccountData
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if (this.savingProduct.isDormancyTrackingActive) {
|
||||
this.accountingMappings = {
|
||||
...this.accountingMappings,
|
||||
escheatLiabilityAccount: this.accounting.glAccountLookUp(
|
||||
this.savingProduct.escheatLiabilityId,
|
||||
liabilityAccountData
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,4 +198,50 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges {
|
||||
isAccrualAccounting(): boolean {
|
||||
return this.accounting.isAccrualAccounting(this.accountingRule);
|
||||
}
|
||||
|
||||
getBooleanLabel(value: any): string {
|
||||
return value === true || value === 'Yes' ? 'Yes' : 'No';
|
||||
}
|
||||
|
||||
getLockinPeriodFrequencyTypeLabel(value: any): string {
|
||||
switch (value) {
|
||||
case 'DAYS':
|
||||
return 'Days';
|
||||
case 'WEEKS':
|
||||
return 'Weeks';
|
||||
case 'MONTHS':
|
||||
return 'Months';
|
||||
case 'YEARS':
|
||||
return 'Years';
|
||||
default:
|
||||
return value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
getOptionLabel(value: any, options: any[], key: string, property: string): string {
|
||||
if (value === null || value === undefined || !Array.isArray(options)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const match = options.find((option) => option?.[key] === value);
|
||||
return match?.[property] || '-';
|
||||
}
|
||||
|
||||
formatGlAccountById(glAccountId: number, options: any[]): string {
|
||||
if (!glAccountId || !Array.isArray(options)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const account = this.accounting.glAccountLookUp(glAccountId, options as any);
|
||||
return account ? `(${account.glCode}) ${account.name}` : '-';
|
||||
}
|
||||
|
||||
formatGlAccountByCode(glCode: string, options: any[]): string {
|
||||
if (!glCode || !Array.isArray(options)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const account = options.find((option) => option?.glCode === glCode);
|
||||
return account ? `(${account.glCode}) ${account.name}` : glCode;
|
||||
}
|
||||
}
|
||||
|
||||
+77
-300
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -7,326 +7,103 @@
|
||||
-->
|
||||
|
||||
<form [formGroup]="savingProductSettingsForm">
|
||||
<!-- Opening Balance -->
|
||||
<div class="sps-section">
|
||||
<div class="sps-section__header">
|
||||
<i class="fas fa-wallet"></i>
|
||||
<h3>{{ 'labels.inputs.Opening Balance' | translate }}</h3>
|
||||
<span class="sps-section__badge"><i class="fas fa-balance-scale"></i></span>
|
||||
<span>Balance & Minimum Balance</span>
|
||||
</div>
|
||||
<div class="sps-grid">
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Minimum Opening Balance' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Sets the minimum deposit amount' | translate }}"
|
||||
formControlName="minRequiredOpeningBalance"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
@if (savingProductSettingsForm.get('minRequiredOpeningBalance').hasError('min')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Minimum Opening Balance' | translate }} {{ 'labels.commons.must be' | translate }}
|
||||
{{ 'labels.commons.a positive number' | translate }}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="sps-body">
|
||||
<div class="sps-layout sps-layout--balance">
|
||||
<div class="sps-row sps-row--three">
|
||||
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
|
||||
<mat-label>Minimum Opening Balance</mat-label>
|
||||
<input type="number" matInput formControlName="minRequiredOpeningBalance" min="0" step="0.01" />
|
||||
</mat-form-field>
|
||||
|
||||
<div class="sps-toggle sps-toggle--inline">
|
||||
<mat-checkbox labelPosition="before" formControlName="enforceMinRequiredBalance">
|
||||
<span>Enforce Minimum Balance</span>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
|
||||
<mat-label>Minimum Balance (₹)</mat-label>
|
||||
<input type="number" matInput formControlName="minRequiredBalance" min="0" step="0.01" />
|
||||
<mat-hint>Penal charge applies on breach</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="sps-row sps-row--two">
|
||||
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
|
||||
<mat-label>Balance Frequency</mat-label>
|
||||
<mat-select formControlName="balanceFrequency">
|
||||
@for (balanceFrequency of periodFrequencyData; track balanceFrequency.id) {
|
||||
<mat-option [value]="balanceFrequency.value">{{ balanceFrequency.value }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="sps-toggle sps-toggle--inline sps-toggle--right">
|
||||
<mat-checkbox labelPosition="before" formControlName="withdrawalFeeForTransfers">
|
||||
<span>Apply Withdrawal Fee for Transfers</span>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lock-in Period -->
|
||||
<div class="sps-section">
|
||||
<div class="sps-section__header">
|
||||
<i class="fas fa-lock"></i>
|
||||
<h3>{{ 'labels.inputs.Lock-in Period' | translate }}</h3>
|
||||
<span class="sps-section__badge"><i class="fas fa-lock"></i></span>
|
||||
<span>Lock-in Period</span>
|
||||
</div>
|
||||
<div class="sps-grid">
|
||||
<div class="sps-field--full sps-checkbox-row">
|
||||
<mat-checkbox
|
||||
labelPosition="before"
|
||||
matTooltip="{{ 'tooltips.Used to indicate the length of time' | translate }}"
|
||||
formControlName="enableLockinPeriod"
|
||||
>
|
||||
{{ 'labels.inputs.Enable Lock-in Period' | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="sps-body">
|
||||
<div class="sps-lockin-row">
|
||||
<span>Enable Lock-in Period</span>
|
||||
<mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
|
||||
</div>
|
||||
|
||||
@if (savingProductSettingsForm.value.enableLockinPeriod) {
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.The number at which lock in period occurs' | translate }}"
|
||||
formControlName="lockinPeriodFrequency"
|
||||
min="1"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
@if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Frequency' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
@if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('min')) {
|
||||
<mat-error>{{ 'tooltips.Frequency must be greater than zero' | translate }}</mat-error>
|
||||
}
|
||||
@if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('pattern')) {
|
||||
<mat-error>{{ 'tooltips.Frequency must be a positive integer' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="sps-lockin-fields">
|
||||
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
|
||||
<mat-label>Frequency*</mat-label>
|
||||
<input type="number" matInput formControlName="lockinPeriodFrequency" min="1" step="1" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
|
||||
<mat-select formControlName="lockinPeriodFrequencyType" required>
|
||||
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
|
||||
<mat-option [value]="lockinPeriodFrequencyType.id">
|
||||
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (savingProductSettingsForm.get('lockinPeriodFrequencyType')?.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Type' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
|
||||
<mat-label>Type*</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>
|
||||
|
||||
<!-- Balance & Fees -->
|
||||
<div class="sps-section">
|
||||
<div class="sps-section__header">
|
||||
<i class="fas fa-balance-scale"></i>
|
||||
<h3>{{ 'labels.inputs.Balance' | translate }} & {{ 'labels.inputs.Fees' | translate }}</h3>
|
||||
<span class="sps-section__badge"><i class="fas fa-cog"></i></span>
|
||||
<span>Additional Rules</span>
|
||||
</div>
|
||||
<div class="sps-grid">
|
||||
<div class="sps-field sps-checkbox-row">
|
||||
<mat-checkbox
|
||||
labelPosition="before"
|
||||
matTooltip="{{ 'tooltips.Indicates whether the withdrawal fee' | translate }}"
|
||||
formControlName="withdrawalFeeForTransfers"
|
||||
>
|
||||
{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="sps-body">
|
||||
<div class="sps-layout sps-layout--additional">
|
||||
<div class="sps-toggle sps-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="sps-field sps-field--compact">
|
||||
<mat-label>Balance Required for Interest Calculation</mat-label>
|
||||
<input type="number" matInput formControlName="minBalanceForInterestCalculation" min="0" step="0.01" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Sets the balance required for interest calculation' | translate }}"
|
||||
formControlName="minBalanceForInterestCalculation"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
@if (savingProductSettingsForm.get('minBalanceForInterestCalculation').hasError('min')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}
|
||||
{{ 'labels.commons.must be' | translate }}
|
||||
{{ 'labels.commons.a positive number' | translate }}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="sps-field sps-checkbox-row">
|
||||
<mat-checkbox
|
||||
labelPosition="before"
|
||||
matTooltip="{{ 'tooltips.Indicates whether to enforce a minimum balance' | translate }}"
|
||||
formControlName="enforceMinRequiredBalance"
|
||||
>
|
||||
{{ 'labels.inputs.Enforce Minimum Balance' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Minimum Balance' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Sets the minimum balance allowed for a saving account' | translate }}"
|
||||
formControlName="minRequiredBalance"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
@if (savingProductSettingsForm.get('minRequiredBalance').hasError('min')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Minimum Balance' | translate }} {{ 'labels.commons.must be' | translate }}
|
||||
{{ 'labels.commons.a positive number' | translate }}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax -->
|
||||
<div class="sps-section">
|
||||
<div class="sps-section__header">
|
||||
<i class="fas fa-file-invoice-dollar"></i>
|
||||
<h3>{{ 'labels.inputs.Tax' | translate }}</h3>
|
||||
</div>
|
||||
<div class="sps-grid">
|
||||
<div class="sps-field--full sps-checkbox-row">
|
||||
<mat-checkbox
|
||||
labelPosition="before"
|
||||
matTooltip="{{ 'tooltips.An boolean flag to attach' | translate }}"
|
||||
formControlName="withHoldTax"
|
||||
>
|
||||
{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@if (savingProductSettingsForm.value.withHoldTax) {
|
||||
<mat-form-field appearance="outline" class="sps-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>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overdraft -->
|
||||
<div class="sps-section">
|
||||
<div class="sps-section__header">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<h3>{{ 'labels.inputs.Overdraft' | translate }}</h3>
|
||||
</div>
|
||||
<div class="sps-grid">
|
||||
<div class="sps-field--full sps-checkbox-row">
|
||||
<mat-checkbox
|
||||
labelPosition="before"
|
||||
matTooltip="{{ 'tooltips.Indicates whether saving accounts based on this saving product' | translate }}"
|
||||
formControlName="allowOverdraft"
|
||||
>
|
||||
{{ 'labels.inputs.Is Overdraft Allowed' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@if (savingProductSettingsForm.value.allowOverdraft) {
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Sets the overdraft required for interest calculation' | translate }}"
|
||||
formControlName="minOverdraftForInterestCalculation"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Default interest rate on overdraft' | translate }}"
|
||||
formControlName="nominalAnnualInterestRateOverdraft"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Sets the maximum allowed overdraft amount' | translate }}"
|
||||
formControlName="overdraftLimit"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dormancy Tracking -->
|
||||
<div class="sps-section">
|
||||
<div class="sps-section__header">
|
||||
<i class="fas fa-bed"></i>
|
||||
<h3>{{ 'labels.inputs.Dormancy Tracking' | translate }}</h3>
|
||||
</div>
|
||||
<div class="sps-grid">
|
||||
<div class="sps-field--full sps-checkbox-row">
|
||||
<mat-checkbox
|
||||
labelPosition="before"
|
||||
matTooltip="{{ 'tooltips.Enables definition and tracking inactive Savings Accounts' | translate }}"
|
||||
formControlName="isDormancyTrackingActive"
|
||||
>
|
||||
{{ 'labels.inputs.Enable Dormancy Tracking' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@if (savingProductSettingsForm.value.isDormancyTrackingActive) {
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Inactive period to mark an account as Inactive' | translate }}"
|
||||
formControlName="daysToInactive"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}
|
||||
{{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Inactive period to mark an account as Dormant' | translate }}"
|
||||
formControlName="daysToDormancy"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}
|
||||
{{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="sps-field">
|
||||
<mat-label>{{ 'labels.inputs.Number of Days to Escheat' | translate }}</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
matTooltip="{{ 'tooltips.Inactive period to mark an account as Escheat' | translate }}"
|
||||
formControlName="daysToEscheat"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Number of Days to Escheat' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+117
-10
@@ -44,15 +44,20 @@ $gray-900: #202124;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4px 16px;
|
||||
padding: 16px 24px;
|
||||
.sps-section__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: #eff6ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sps-body {
|
||||
background: #fff;
|
||||
padding: 18px 24px 24px;
|
||||
}
|
||||
|
||||
.sps-field {
|
||||
@@ -61,10 +66,112 @@ $gray-900: #202124;
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&--compact {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-checkbox-row {
|
||||
.sps-layout {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sps-layout--balance {
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.sps-layout--lockin {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@media (width <= 640px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-layout--additional {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@media (width <= 1100px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-row {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.sps-row--three {
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
|
||||
.sps-field--compact:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sps-toggle--inline {
|
||||
align-self: center;
|
||||
min-height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-row--two {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
|
||||
.sps-field--compact {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
min-height: 56px;
|
||||
|
||||
mat-checkbox {
|
||||
font-size: 14px;
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
&--full {
|
||||
min-height: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&--inline {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&--right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.sps-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;
|
||||
}
|
||||
|
||||
.sps-lockin-row mat-checkbox {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.sps-lockin-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
@media (width <= 640px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
+67
-111
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -7,16 +7,8 @@
|
||||
*/
|
||||
|
||||
import { Component, OnInit, Input, inject } from '@angular/core';
|
||||
import {
|
||||
UntypedFormGroup,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
Validators,
|
||||
ReactiveFormsModule
|
||||
} from '@angular/forms';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
@@ -25,9 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./saving-product-settings-step.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatTooltip,
|
||||
MatCheckbox,
|
||||
FaIconComponent
|
||||
MatCheckbox
|
||||
]
|
||||
})
|
||||
export class SavingProductSettingsStepComponent implements OnInit {
|
||||
@@ -37,8 +27,26 @@ export class SavingProductSettingsStepComponent implements OnInit {
|
||||
|
||||
savingProductSettingsForm: UntypedFormGroup;
|
||||
|
||||
lockinPeriodFrequencyTypeData: any;
|
||||
taxGroupData: any;
|
||||
periodFrequencyData: any[] = [];
|
||||
|
||||
lockinPeriodFrequencyTypeData = [
|
||||
{
|
||||
id: 'DAYS',
|
||||
value: 'value.days'
|
||||
},
|
||||
{
|
||||
id: 'WEEKS',
|
||||
value: 'value.weeks'
|
||||
},
|
||||
{
|
||||
id: 'MONTHS',
|
||||
value: 'value.months'
|
||||
},
|
||||
{
|
||||
id: 'YEARS',
|
||||
value: 'value.years'
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.createSavingProductSettingsForm();
|
||||
@@ -46,32 +54,34 @@ export class SavingProductSettingsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.lockinPeriodFrequencyTypeData = this.savingProductsTemplate.lockinPeriodFrequencyTypeOptions;
|
||||
this.taxGroupData = this.savingProductsTemplate.taxGroupOptions;
|
||||
|
||||
const hasLockinPeriod =
|
||||
this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0;
|
||||
this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || [];
|
||||
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
|
||||
const isEditing = !!this.savingProductsTemplate.id;
|
||||
|
||||
this.savingProductSettingsForm.patchValue({
|
||||
minRequiredOpeningBalance: this.savingProductsTemplate.minRequiredOpeningBalance,
|
||||
enableLockinPeriod: hasLockinPeriod,
|
||||
withdrawalFeeForTransfers: this.savingProductsTemplate.withdrawalFeeForTransfers,
|
||||
minBalanceForInterestCalculation: this.savingProductsTemplate.minBalanceForInterestCalculation,
|
||||
enforceMinRequiredBalance: this.savingProductsTemplate.enforceMinRequiredBalance,
|
||||
minRequiredBalance: this.savingProductsTemplate.minRequiredBalance,
|
||||
allowOverdraft: this.savingProductsTemplate.allowOverdraft,
|
||||
minOverdraftForInterestCalculation: this.savingProductsTemplate.minOverdraftForInterestCalculation,
|
||||
nominalAnnualInterestRateOverdraft: this.savingProductsTemplate.nominalAnnualInterestRateOverdraft,
|
||||
overdraftLimit: this.savingProductsTemplate.overdraftLimit,
|
||||
withHoldTax: this.savingProductsTemplate.withHoldTax,
|
||||
taxGroupId: this.savingProductsTemplate.taxGroup && this.savingProductsTemplate.taxGroup.id,
|
||||
isDormancyTrackingActive: this.savingProductsTemplate.isDormancyTrackingActive,
|
||||
daysToInactive: this.savingProductsTemplate.daysToInactive,
|
||||
daysToDormancy: this.savingProductsTemplate.daysToDormancy,
|
||||
daysToEscheat: this.savingProductsTemplate.daysToEscheat
|
||||
balanceFrequency: isEditing ? this.savingProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod,
|
||||
enableLockinPeriod: isEditing
|
||||
? this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0
|
||||
: this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0,
|
||||
accountClosedZeroBalanceInDayClosing: isEditing
|
||||
? (this.savingProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false)
|
||||
: false,
|
||||
isDormancyTrackingActive: isEditing ? (this.savingProductsTemplate.isDormancyTrackingActive ?? true) : true,
|
||||
daysToInactive: isEditing ? this.savingProductsTemplate.daysToInactive || 30 : 30,
|
||||
daysToDormancy: isEditing ? this.savingProductsTemplate.daysToDormancy || 60 : 60,
|
||||
daysToEscheat: isEditing ? this.savingProductsTemplate.daysToEscheat || 90 : 90
|
||||
});
|
||||
|
||||
if (hasLockinPeriod) {
|
||||
if (
|
||||
isEditing &&
|
||||
this.savingProductsTemplate.lockinPeriodFrequency &&
|
||||
this.savingProductsTemplate.lockinPeriodFrequency > 0
|
||||
) {
|
||||
this.savingProductSettingsForm.patchValue({
|
||||
lockinPeriodFrequency: this.savingProductsTemplate.lockinPeriodFrequency,
|
||||
lockinPeriodFrequencyType:
|
||||
@@ -87,7 +97,6 @@ export class SavingProductSettingsStepComponent implements OnInit {
|
||||
'',
|
||||
[Validators.min(0)]
|
||||
],
|
||||
enableLockinPeriod: [false],
|
||||
withdrawalFeeForTransfers: [false],
|
||||
minBalanceForInterestCalculation: [
|
||||
'',
|
||||
@@ -98,95 +107,42 @@ export class SavingProductSettingsStepComponent implements OnInit {
|
||||
'',
|
||||
[Validators.min(0)]
|
||||
],
|
||||
allowOverdraft: [false],
|
||||
withHoldTax: [false],
|
||||
isDormancyTrackingActive: [false]
|
||||
balanceFrequency: ['Monthly'],
|
||||
enableLockinPeriod: [false],
|
||||
lockinPeriodFrequency: [''],
|
||||
lockinPeriodFrequencyType: [''],
|
||||
accountClosedZeroBalanceInDayClosing: [false],
|
||||
// Dormancy tracking fields (required when isDormancyTrackingActive is true)
|
||||
isDormancyTrackingActive: [true],
|
||||
daysToInactive: [30],
|
||||
daysToDormancy: [60],
|
||||
daysToEscheat: [90]
|
||||
});
|
||||
}
|
||||
|
||||
setConditionalControls() {
|
||||
this.savingProductSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enableLockinPeriod: any) => {
|
||||
if (enableLockinPeriod) {
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'lockinPeriodFrequency',
|
||||
new UntypedFormControl('', [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.pattern('^[1-9]\\d*$')
|
||||
])
|
||||
);
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'lockinPeriodFrequencyType',
|
||||
new UntypedFormControl('', Validators.required)
|
||||
);
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequency').setValidators([
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.pattern('^[1-9]\\d*$')
|
||||
]);
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequencyType').setValidators([
|
||||
Validators.required
|
||||
]);
|
||||
} else {
|
||||
this.savingProductSettingsForm.removeControl('lockinPeriodFrequency');
|
||||
this.savingProductSettingsForm.removeControl('lockinPeriodFrequencyType');
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequency').setValue('');
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequencyType').setValue('');
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequency').clearValidators();
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequencyType').clearValidators();
|
||||
}
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequency').updateValueAndValidity();
|
||||
this.savingProductSettingsForm.get('lockinPeriodFrequencyType').updateValueAndValidity();
|
||||
});
|
||||
|
||||
this.savingProductSettingsForm.get('allowOverdraft').valueChanges.subscribe((allowOverdraft: any) => {
|
||||
if (allowOverdraft) {
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'minOverdraftForInterestCalculation',
|
||||
new UntypedFormControl('', [Validators.min(0)])
|
||||
);
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'nominalAnnualInterestRateOverdraft',
|
||||
new UntypedFormControl('', [Validators.min(0)])
|
||||
);
|
||||
this.savingProductSettingsForm.addControl('overdraftLimit', new UntypedFormControl('', [Validators.min(0)]));
|
||||
} else {
|
||||
this.savingProductSettingsForm.removeControl('minOverdraftForInterestCalculation');
|
||||
this.savingProductSettingsForm.removeControl('nominalAnnualInterestRateOverdraft');
|
||||
this.savingProductSettingsForm.removeControl('overdraftLimit');
|
||||
}
|
||||
});
|
||||
|
||||
this.savingProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => {
|
||||
if (withHoldTax) {
|
||||
this.savingProductSettingsForm.addControl('taxGroupId', new UntypedFormControl('', Validators.required));
|
||||
} else {
|
||||
this.savingProductSettingsForm.removeControl('taxGroupId');
|
||||
}
|
||||
});
|
||||
|
||||
this.savingProductSettingsForm
|
||||
.get('isDormancyTrackingActive')
|
||||
.valueChanges.subscribe((isDormancyTrackingActive: any) => {
|
||||
if (isDormancyTrackingActive) {
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'daysToInactive',
|
||||
new UntypedFormControl('', [
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
])
|
||||
);
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'daysToDormancy',
|
||||
new UntypedFormControl('', [
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
])
|
||||
);
|
||||
this.savingProductSettingsForm.addControl(
|
||||
'daysToEscheat',
|
||||
new UntypedFormControl('', [
|
||||
Validators.required,
|
||||
Validators.min(0)
|
||||
])
|
||||
);
|
||||
} else {
|
||||
this.savingProductSettingsForm.removeControl('daysToInactive');
|
||||
this.savingProductSettingsForm.removeControl('daysToDormancy');
|
||||
this.savingProductSettingsForm.removeControl('daysToEscheat');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get savingProductSettings() {
|
||||
const formValue = { ...this.savingProductSettingsForm.value };
|
||||
delete formValue.enableLockinPeriod;
|
||||
return formValue;
|
||||
return { ...this.savingProductSettingsForm.value };
|
||||
}
|
||||
}
|
||||
|
||||
+134
-106
@@ -9,120 +9,148 @@
|
||||
<form [formGroup]="savingProductTermsForm">
|
||||
<div class="spt-section">
|
||||
<div class="spt-section__header">
|
||||
<i class="fas fa-percentage"></i>
|
||||
<h3>{{ 'labels.inputs.Interest Rate' | translate }}</h3>
|
||||
<span class="spt-section__badge"><i class="fas fa-chart-line"></i></span>
|
||||
<span>Interest Rate & Posting Terms</span>
|
||||
</div>
|
||||
<div class="spt-grid">
|
||||
<mat-form-field appearance="outline" class="spt-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 (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) {
|
||||
<div class="spt-body">
|
||||
<div class="spt-banner">
|
||||
<span class="spt-banner__icon">ℹ️</span>
|
||||
<span>
|
||||
Savings Account base rate:
|
||||
<b>{{ savingProductTermsForm.get('nominalAnnualInterestRate').value }}%</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="spt-grid spt-grid--three">
|
||||
<mat-form-field appearance="outline" class="spt-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 (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
@if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) {
|
||||
<mat-error> {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater </mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="spt-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="spt-field">
|
||||
<mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
|
||||
<mat-select
|
||||
formControlName="interestCalculationType"
|
||||
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
|
||||
required
|
||||
>
|
||||
@for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) {
|
||||
<mat-option [value]="interestCalculationType.id">
|
||||
{{ interestCalculationType.value | translateKey: 'catalogs' }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
{{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="spt-grid spt-grid--three">
|
||||
<mat-form-field appearance="outline" class="spt-field">
|
||||
<mat-label>Interest Calculate</mat-label>
|
||||
<mat-select formControlName="interestCalculateSelector" required>
|
||||
@for (mode of interestCalculationTypeData; track mode.id) {
|
||||
<mat-option [value]="mode.id">{{ mode.value }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@if (savingProductTermsForm.value.interestCalculateSelector === 2) {
|
||||
<mat-form-field appearance="outline" class="spt-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>
|
||||
}
|
||||
@if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) {
|
||||
|
||||
<mat-form-field appearance="outline" class="spt-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.Nominal Annual Interest' | translate }} must be zero or greater
|
||||
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="spt-footer">
|
||||
<div class="spt-footer__grid">
|
||||
<div class="spt-toggle-card">
|
||||
<div>
|
||||
<h4>Auto Interest Post</h4>
|
||||
</div>
|
||||
<div class="spt-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="spt-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="spt-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 (savingProductTermsForm.value.interestCalculateSelector === 1) {
|
||||
<div class="spt-note">
|
||||
<span>⚠️</span>
|
||||
<span
|
||||
>Daily product-based interest accrual enabled - interest calculated EOD on daily closing balance.</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spt-section">
|
||||
<div class="spt-section__header">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
<h3>{{ 'labels.inputs.Interest Compounding' | translate }}</h3>
|
||||
</div>
|
||||
<div class="spt-grid">
|
||||
<mat-form-field appearance="outline" class="spt-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 | translateKey: 'catalogs' }}
|
||||
</mat-option>
|
||||
}
|
||||
</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="spt-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>
|
||||
|
||||
<mat-form-field appearance="outline" class="spt-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 | translateKey: 'catalogs' }}
|
||||
</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="spt-field">
|
||||
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label>
|
||||
<mat-select
|
||||
formControlName="interestCalculationDaysInYearType"
|
||||
matTooltip="{{ 'tooltips.To calculate interest' | translate }}"
|
||||
required
|
||||
>
|
||||
@for (
|
||||
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData;
|
||||
track interestCalculationDaysInYearType
|
||||
) {
|
||||
<mat-option [value]="interestCalculationDaysInYearType.id">
|
||||
{{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+84
-1
@@ -44,17 +44,36 @@ $gray-900: #202124;
|
||||
}
|
||||
}
|
||||
|
||||
.spt-section__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: #eff6ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.spt-body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4px 16px;
|
||||
padding: 16px 24px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.spt-grid--three {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.spt-field {
|
||||
width: 100%;
|
||||
|
||||
@@ -62,3 +81,67 @@ $gray-900: #202124;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.spt-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;
|
||||
}
|
||||
}
|
||||
|
||||
.spt-footer {
|
||||
padding: 0 24px 24px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.spt-footer__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
padding-top: 18px;
|
||||
|
||||
@media (width <= 900px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.spt-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;
|
||||
}
|
||||
}
|
||||
|
||||
.spt-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;
|
||||
}
|
||||
|
||||
+132
-8
@@ -10,6 +10,8 @@ import { Component, OnInit, Input, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
@@ -24,6 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
})
|
||||
export class SavingProductTermsStepComponent implements OnInit {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private dateUtils = inject(Dates);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
@Input() savingProductsTemplate: any;
|
||||
|
||||
@@ -32,8 +36,22 @@ export class SavingProductTermsStepComponent 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.createSavingProductTermsForm();
|
||||
}
|
||||
@@ -42,15 +60,91 @@ export class SavingProductTermsStepComponent implements OnInit {
|
||||
this.interestCompoundingPeriodTypeData = this.savingProductsTemplate.interestCompoundingPeriodTypeOptions;
|
||||
this.interestPostingPeriodTypeData = this.savingProductsTemplate.interestPostingPeriodTypeOptions;
|
||||
this.interestCalculationTypeData = this.savingProductsTemplate.interestCalculationTypeOptions;
|
||||
this.interestCalculationTypeOptionData =
|
||||
this.savingProductsTemplate.interestCalculationTypeOption ||
|
||||
this.savingProductsTemplate.interestCalculationTypeOptions;
|
||||
this.interestCalculationDaysInYearTypeData = this.savingProductsTemplate.interestCalculationDaysInYearTypeOptions;
|
||||
this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || [];
|
||||
this.setYesNoOptions(this.savingProductsTemplate?.yesNoOptions);
|
||||
|
||||
const isEditing = !!this.savingProductsTemplate.id;
|
||||
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
|
||||
|
||||
this.savingProductTermsForm.patchValue({
|
||||
nominalAnnualInterestRate: this.savingProductsTemplate.nominalAnnualInterestRate,
|
||||
interestCompoundingPeriodType: this.savingProductsTemplate.interestCompoundingPeriodType.id,
|
||||
interestPostingPeriodType: this.savingProductsTemplate.interestPostingPeriodType.id,
|
||||
interestCalculationType: this.savingProductsTemplate.interestCalculationType.id,
|
||||
interestCalculationDaysInYearType: this.savingProductsTemplate.interestCalculationDaysInYearType.id
|
||||
nominalAnnualInterestRate: isEditing
|
||||
? this.savingProductsTemplate.nominalAnnualInterestRate
|
||||
: this.savingProductsTemplate.nominalAnnualInterestRate,
|
||||
interestCompoundingPeriodType: isEditing
|
||||
? this.savingProductsTemplate.interestCompoundingPeriodType?.id
|
||||
: this.savingProductsTemplate.interestCompoundingPeriodType?.id,
|
||||
interestPostingPeriodType: isEditing
|
||||
? this.savingProductsTemplate.interestPostingPeriodType?.id
|
||||
: this.savingProductsTemplate.interestPostingPeriodType?.id,
|
||||
interestCalculationType: isEditing
|
||||
? this.savingProductsTemplate.interestCalculationTypeOption?.id ||
|
||||
this.interestCalculationTypeOptionData?.[0]?.id ||
|
||||
1
|
||||
: this.interestCalculationTypeOptionData?.[0]?.id || 1,
|
||||
interestCalculationDaysInYearType: isEditing
|
||||
? this.savingProductsTemplate.interestCalculationDaysInYearType?.id
|
||||
: this.savingProductsTemplate.interestCalculationDaysInYearType?.id,
|
||||
effectiveDate: this.savingProductsTemplate.effectiveDate
|
||||
? new Date(this.savingProductsTemplate.effectiveDate)
|
||||
: '',
|
||||
interestCalculateSelector: isEditing
|
||||
? this.savingProductsTemplate.interestCalculateSelector || 1
|
||||
: this.savingProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1,
|
||||
averagePeriod: isEditing ? this.savingProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod,
|
||||
autoPost: isEditing ? (this.savingProductsTemplate.autoPost ?? true) : true,
|
||||
frequencyFixDay: isEditing ? this.savingProductsTemplate.frequencyFixDay || 30 : 30,
|
||||
lastInterestDate: this.savingProductsTemplate.lastInterestDate
|
||||
? new Date(this.savingProductsTemplate.lastInterestDate)
|
||||
: ''
|
||||
});
|
||||
|
||||
// Map code value IDs to backend enum values for interestCalculationType
|
||||
// Backend expects: 1=Daily Balance, 2=Average Daily Balance
|
||||
const currentInterestCalcType = this.savingProductTermsForm.get('interestCalculationType').value;
|
||||
if (currentInterestCalcType && currentInterestCalcType > 2) {
|
||||
// If it's a code value ID (like 340, 341), map it to backend enum
|
||||
// Default to 1 (Daily Balance) for custom code values
|
||||
this.savingProductTermsForm.patchValue({
|
||||
interestCalculationType: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setYesNoOptions(options: any[]) {
|
||||
if (!Array.isArray(options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedOptions = options
|
||||
.map((option: any) => {
|
||||
const rawValue = option?.name ?? option?.value ?? option;
|
||||
const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue;
|
||||
|
||||
if (normalized === 'YES') {
|
||||
return {
|
||||
value: true,
|
||||
label: 'Yes'
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized === 'NO') {
|
||||
return {
|
||||
value: false,
|
||||
label: 'No'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((value: any) => !!value);
|
||||
|
||||
if (mappedOptions.length) {
|
||||
this.yesNoOptions = mappedOptions;
|
||||
}
|
||||
}
|
||||
|
||||
createSavingProductTermsForm() {
|
||||
@@ -77,11 +171,41 @@ export class SavingProductTermsStepComponent implements OnInit {
|
||||
interestCalculationDaysInYearType: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
],
|
||||
effectiveDate: [''],
|
||||
interestCalculateSelector: [1],
|
||||
averagePeriod: ['Monthly'],
|
||||
autoPost: [true],
|
||||
frequencyFixDay: [
|
||||
30,
|
||||
[Validators.min(0)]
|
||||
],
|
||||
lastInterestDate: ['']
|
||||
});
|
||||
}
|
||||
|
||||
get savingProductTerms() {
|
||||
return this.savingProductTermsForm.value;
|
||||
const formValue = { ...this.savingProductTermsForm.value };
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const prevEffectiveDate: Date = this.savingProductTermsForm.value.effectiveDate;
|
||||
const prevLastInterestDate: Date = this.savingProductTermsForm.value.lastInterestDate;
|
||||
|
||||
if (formValue.effectiveDate instanceof Date) {
|
||||
formValue.effectiveDate = this.dateUtils.formatDate(prevEffectiveDate, dateFormat) || '';
|
||||
}
|
||||
|
||||
if (formValue.lastInterestDate instanceof Date) {
|
||||
formValue.lastInterestDate = this.dateUtils.formatDate(prevLastInterestDate, dateFormat) || '';
|
||||
}
|
||||
|
||||
// Convert interestCalculationType from code value ID to backend enum value
|
||||
// Backend expects: 1=Daily Balance, 2=Average Daily Balance
|
||||
if (formValue.interestCalculationType && formValue.interestCalculationType > 2) {
|
||||
formValue.interestCalculationType = 1; // Default to Daily Balance
|
||||
}
|
||||
|
||||
return {
|
||||
...formValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SavingProductResolver {
|
||||
private productsService = inject(ProductsService);
|
||||
|
||||
/**
|
||||
* Returns the saving product data.
|
||||
* Returns the saving product data with template options for dropdown lookups.
|
||||
* @returns {Observable<any>}
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot): Observable<any> {
|
||||
|
||||
+488
-278
@@ -18,304 +18,514 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-container">
|
||||
<div class="responsive-column">
|
||||
<h2 class="mat-h2 flex-100">{{ savingProduct.name }}</h2>
|
||||
<div class="responsive-column">
|
||||
<h2 class="mat-h2 flex-100">{{ savingProduct.name }}</h2>
|
||||
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
|
||||
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3>
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.shortName }}</span>
|
||||
</div>
|
||||
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.shortName }}</span>
|
||||
@if (savingProduct.description) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.description }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.description) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.description }}</span>
|
||||
@if (
|
||||
savingProduct.accountType ||
|
||||
savingProduct.customerSegment ||
|
||||
savingProduct.productCode ||
|
||||
savingProduct.effectiveDate ||
|
||||
savingProduct.effectiveTo ||
|
||||
savingProduct.endDate ||
|
||||
savingProduct.introducerRequired !== undefined ||
|
||||
savingProduct.shareLinkingEnabled !== undefined
|
||||
) {
|
||||
<h3 class="mat-h3 flex-100">Product Classification</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
|
||||
@if (savingProduct.accountType) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Account Type:</span>
|
||||
<span class="flex-60">{{ savingProduct.accountType }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Currency' | translate }}</h3>
|
||||
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.decimalPlaces }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.inMultiplesOf }}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
|
||||
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.nominalAnnualInterestRate | formatNumber }} %</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestCompoundingPeriodType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestCalculationType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.interestCalculationDaysInYearType.value | translateKey: 'catalogs'
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Settings' | translate }}</h3>
|
||||
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
|
||||
@if (savingProduct.minRequiredOpeningBalance) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.minRequiredOpeningBalance | formatNumber }}</span>
|
||||
@if (savingProduct.customerSegment) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Customer Segment:</span>
|
||||
<span class="flex-60">{{ savingProduct.customerSegment }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.lockinPeriodFrequency) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
|
||||
@if (savingProduct.productCode) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Product Code:</span>
|
||||
<span class="flex-60">{{ savingProduct.productCode }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.effectiveDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Effective Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.effectiveDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.effectiveTo) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Effective To:</span>
|
||||
<span class="flex-60">{{ savingProduct.effectiveTo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.endDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">End Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.endDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.introducerRequired !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Introducer Required:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.lockinPeriodFrequency + ' ' + savingProduct.lockinPeriodFrequencyType.value
|
||||
savingProduct.introducerRequired === true || savingProduct.introducerRequired === 'Yes' ? 'Yes' : 'No'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.withdrawalFeeForTransfers | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.minBalanceForInterestCalculation) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.minBalanceForInterestCalculation | formatNumber }}</span>
|
||||
@if (savingProduct.shareLinkingEnabled !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Share Linking Enabled:</span>
|
||||
<span class="flex-60">{{ savingProduct.shareLinkingEnabled | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.enforceMinRequiredBalance | yesNo }}</span>
|
||||
</div>
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
|
||||
|
||||
@if (savingProduct.minRequiredBalance) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.minRequiredBalance | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.withHoldTax | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.withHoldTax) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Tax Group' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.taxGroup.name }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.allowOverdraft | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.minOverdraftForInterestCalculation) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40"
|
||||
>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}:</span
|
||||
>
|
||||
<span class="flex-60">{{ savingProduct.minOverdraftForInterestCalculation }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.nominalAnnualInterestRateOverdraft) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.nominalAnnualInterestRateOverdraft }} %</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.overdraftLimit) {
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.overdraftLimit | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-fill layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Enable Dormancy Tracking' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.isDormancyTrackingActive | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.isDormancyTrackingActive) {
|
||||
<div class="layout-row-wrap responsive-column flex-fill">
|
||||
<span class="flex-40">{{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.daysToInactive }}</span>
|
||||
<span class="flex-40">{{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.daysToDormancy }}</span>
|
||||
<span class="flex-40">{{ 'labels.inputs.Number of Days to Escheat' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.daysToEscheat }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.charges.length) {
|
||||
<div class="layout-row-wrap responsive-column flex-fill">
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Charges' | translate }}</h3>
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="savingProduct.charges">
|
||||
<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 | translateKey: 'catalogs' }}
|
||||
</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 | translateKey: 'catalogs' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Accounting' | translate }}</h3>
|
||||
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
|
||||
<mifosx-view-savings-accounting-details
|
||||
class="flex-100"
|
||||
[accountingRule]="savingProduct.accountingRule"
|
||||
[accountingMappings]="savingProduct.accountingMappings"
|
||||
>
|
||||
</mifosx-view-savings-accounting-details>
|
||||
|
||||
@if (
|
||||
savingProduct.paymentChannelToFundSourceMappings?.length ||
|
||||
savingProduct.feeToIncomeAccountMappings?.length ||
|
||||
savingProduct.penaltyToIncomeAccountMappings?.length
|
||||
) {
|
||||
<div class="flex-fill layout-row-wrap responsive-column">
|
||||
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Advanced Accounting Rules' | translate }}</h3>
|
||||
<mat-divider [inset]="true"></mat-divider>
|
||||
@if (savingProduct.paymentChannelToFundSourceMappings?.length) {
|
||||
<div class="flex-fill layout-row-wrap responsive-column">
|
||||
<h4 class="mat-h4 flex-fill">{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}</h4>
|
||||
<table
|
||||
class="mat-elevation-z1 flex-fill"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.paymentChannelToFundSourceMappings"
|
||||
>
|
||||
<ng-container matColumnDef="paymentTypeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let paymentFundSource">
|
||||
{{ paymentFundSource.paymentType.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fundSourceAccountId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let paymentFundSource">
|
||||
({{ paymentFundSource.fundSourceAccount.glCode }}) {{ paymentFundSource.fundSourceAccount.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (savingProduct.feeToIncomeAccountMappings?.length) {
|
||||
<div class="flex-fill layout-row-wrap responsive-column">
|
||||
<h4 class="mat-h4 flex-fill">{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}</h4>
|
||||
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="savingProduct.feeToIncomeAccountMappings">
|
||||
<ng-container matColumnDef="chargeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let feesIncome">
|
||||
{{ feesIncome.charge.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="incomeAccountId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let feesIncome">
|
||||
({{ feesIncome.incomeAccount.glCode }}) {{ feesIncome.incomeAccount.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (savingProduct.penaltyToIncomeAccountMappings?.length) {
|
||||
<div class="flex-fill layout-row-wrap responsive-column">
|
||||
<h4 class="mat-h4 flex-fill">{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}</h4>
|
||||
<table
|
||||
class="mat-elevation-z1 flex-fill"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.penaltyToIncomeAccountMappings"
|
||||
>
|
||||
<ng-container matColumnDef="chargeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let penaltyIncome">
|
||||
{{ penaltyIncome.charge.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="incomeAccountId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let penaltyIncome">
|
||||
({{ penaltyIncome.incomeAccount.glCode }}) {{ penaltyIncome.incomeAccount.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.decimalPlaces }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.currency.inMultiplesOf !== undefined && savingProduct.currency.inMultiplesOf !== null) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.currency.inMultiplesOf }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.dormancyAlert !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Dormancy Alert:</span>
|
||||
<span class="flex-60">{{ savingProduct.dormancyAlert | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.nominationFacility !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Nomination Facility:</span>
|
||||
<span class="flex-60">{{ savingProduct.nominationFacility | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.nominalAnnualInterestRate | formatNumber }} %</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestCompoundingPeriodType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestCalculationType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.effectiveDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Effective Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.effectiveDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Interest Calculate:</span>
|
||||
<span class="flex-60">{{ savingProduct.interestCalculateSelector }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.averagePeriod) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Average Period:</span>
|
||||
<span class="flex-60">{{ savingProduct.averagePeriod }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Auto Post:</span>
|
||||
<span class="flex-60">{{ savingProduct.autoPost | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.frequencyFixDay) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Frequency Fix Day:</span>
|
||||
<span class="flex-60">{{ savingProduct.frequencyFixDay }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.lastInterestDate) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Last Interest Date:</span>
|
||||
<span class="flex-60">{{ savingProduct.lastInterestDate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Settings' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
|
||||
@if (savingProduct.minRequiredOpeningBalance) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.minRequiredOpeningBalance | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.lockinPeriodFrequency) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
|
||||
<span class="flex-60">{{
|
||||
savingProduct.lockinPeriodFrequency + ' ' + savingProduct.lockinPeriodFrequencyType.value
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.withdrawalFeeForTransfers | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.balanceFrequency) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Balance Frequency:</span>
|
||||
<span class="flex-60">{{ savingProduct.balanceFrequency }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Enable Lock-in Period:</span>
|
||||
<span class="flex-60">{{ savingProduct.enableLockinPeriod | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.minBalanceForInterestCalculation) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.minBalanceForInterestCalculation | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.enforceMinRequiredBalance | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.minRequiredBalance) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.minRequiredBalance | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.withHoldTax | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.withHoldTax) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Tax Group' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.taxGroup.name }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.allowOverdraft | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Account Closed Zero Balance in Day Closing:</span>
|
||||
<span class="flex-60">{{ savingProduct.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.minOverdraftForInterestCalculation) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40"
|
||||
>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}:</span
|
||||
>
|
||||
<span class="flex-60">{{ savingProduct.minOverdraftForInterestCalculation | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.nominalAnnualInterestRateOverdraft) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.overdraftLimit) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.overdraftLimit | formatNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ 'labels.inputs.Enable Dormancy Tracking' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.isDormancyTrackingActive | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
@if (savingProduct.isDormancyTrackingActive) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<span class="flex-40">{{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.daysToInactive }}</span>
|
||||
<span class="flex-40">{{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.daysToDormancy }}</span>
|
||||
<span class="flex-40">{{ 'labels.inputs.Number of Days to Escheat' | translate }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.daysToEscheat }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">Account Facilities</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
@if (savingProduct.accountFacilityConfig) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
@for (feature of accountFacilityFeatureItems; track feature.key) {
|
||||
@if (savingProduct.accountFacilityConfig[feature.key] !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">{{ feature.label }}:</span>
|
||||
<span class="flex-60">{{ savingProduct.accountFacilityConfig[feature.key] | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.accountFacilityLimits?.length) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<h4 class="mat-h4 flex-100">Account Facility Limits</h4>
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="savingProduct.accountFacilityLimits">
|
||||
<ng-container matColumnDef="limitType">
|
||||
<th mat-header-cell *matHeaderCellDef>Limit Type</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.limitType }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="generalLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>General Limit</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.generalLimit }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="minorAccountLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>Minor Account Limit</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.minorAccountLimit }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="smallSavingLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>Small Saving Limit</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.smallSavingLimit }}</td>
|
||||
</ng-container>
|
||||
<tr
|
||||
mat-header-row
|
||||
*matHeaderRowDef="['limitType', 'generalLimit', 'minorAccountLimit', 'smallSavingLimit']"
|
||||
></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: ['limitType', 'generalLimit', 'minorAccountLimit', 'smallSavingLimit']"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.depositLimitKyc) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<h4 class="mat-h4 flex-100">Deposit Limit (KYC)</h4>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">General:</span>
|
||||
<span class="flex-60"
|
||||
>{{ savingProduct.depositLimitKyc.general.kycType }} - {{ savingProduct.depositLimitKyc.general.limit }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Minor:</span>
|
||||
<span class="flex-60"
|
||||
>{{ savingProduct.depositLimitKyc.minor.kycType }} - {{ savingProduct.depositLimitKyc.minor.limit }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Small Saving:</span>
|
||||
<span class="flex-60"
|
||||
>{{ savingProduct.depositLimitKyc.smallSaving.kycType }} -
|
||||
{{ savingProduct.depositLimitKyc.smallSaving.limit }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (savingProduct.charges.length) {
|
||||
<div class="layout-row-wrap responsive-column flex-100">
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Charges' | translate }}</h3>
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="savingProduct.charges">
|
||||
<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 | translateKey: 'catalogs' }}
|
||||
</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 | translateKey: 'catalogs' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
|
||||
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
|
||||
<mifosx-view-savings-accounting-details
|
||||
class="flex-100"
|
||||
[accountingRule]="savingProduct.accountingRule"
|
||||
[accountingMappings]="savingProduct.accountingMappings"
|
||||
>
|
||||
</mifosx-view-savings-accounting-details>
|
||||
|
||||
@if (
|
||||
savingProduct.paymentChannelToFundSourceMappings?.length ||
|
||||
savingProduct.feeToIncomeAccountMappings?.length ||
|
||||
savingProduct.penaltyToIncomeAccountMappings?.length
|
||||
) {
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Advanced Accounting Rules' | translate }}</h3>
|
||||
<mat-divider class="flex-100"></mat-divider>
|
||||
@if (savingProduct.paymentChannelToFundSourceMappings?.length) {
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}</h4>
|
||||
<table
|
||||
class="mat-elevation-z1 flex-100"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.paymentChannelToFundSourceMappings"
|
||||
>
|
||||
<ng-container matColumnDef="paymentTypeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let paymentFundSource">
|
||||
{{ paymentFundSource.paymentType.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fundSourceAccountId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let paymentFundSource">
|
||||
({{ paymentFundSource.fundSourceAccount.glCode }}) {{ paymentFundSource.fundSourceAccount.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (savingProduct.feeToIncomeAccountMappings?.length) {
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}</h4>
|
||||
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="savingProduct.feeToIncomeAccountMappings">
|
||||
<ng-container matColumnDef="chargeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fee/Charge' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let fee">
|
||||
{{ fee.charge.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="incomeAccountId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let fee">({{ fee.incomeAccount.glCode }}) {{ fee.incomeAccount.name }}</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (savingProduct.penaltyToIncomeAccountMappings?.length) {
|
||||
<div class="flex-100 layout-row-wrap responsive-column">
|
||||
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}</h4>
|
||||
<table
|
||||
class="flex-100 mat-elevation-z1"
|
||||
mat-table
|
||||
[dataSource]="savingProduct.penaltyToIncomeAccountMappings"
|
||||
>
|
||||
<ng-container matColumnDef="chargeId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let penalty">
|
||||
{{ penalty.charge.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="incomeAccountId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let penalty">
|
||||
({{ penalty.incomeAccount.glCode }}) {{ penalty.incomeAccount.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
+92
@@ -71,3 +71,95 @@ mat-divider {
|
||||
.margin-t {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.spa-gl-summary,
|
||||
.spa-other-gl-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.spa-gl-summary__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.spa-gl-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border: 1px solid #e3e7f2;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.spa-gl-section h5 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #91a0bc;
|
||||
}
|
||||
|
||||
.spa-gl-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 40%) 1fr;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.spa-gl-row__label {
|
||||
color: #1f2f4a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spa-gl-row__value {
|
||||
color: #101828;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.spa-other-gl-table {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spa-other-gl-table:first-of-type {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.spa-other-gl-summary .mat-elevation-z1 {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spa-other-gl-table th.mat-mdc-header-cell,
|
||||
.spa-other-gl-table td.mat-mdc-cell {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.spa-other-gl-table th.mat-mdc-header-cell:first-child,
|
||||
.spa-other-gl-table td.mat-mdc-cell:first-child {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.spa-other-gl-table th.mat-mdc-header-cell:last-child,
|
||||
.spa-other-gl-table td.mat-mdc-cell:last-child {
|
||||
width: 62%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.spa-gl-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -72,6 +72,25 @@ export class SavingProductGeneralTabComponent {
|
||||
'incomeAccountId'
|
||||
];
|
||||
|
||||
accountFacilityFeatureItems = [
|
||||
{ key: 'atmFacility', label: 'ATM Facility' },
|
||||
{ key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' },
|
||||
{ key: 'upiFacility', label: 'UPI Facility' },
|
||||
{ key: 'posTransactionsFacility', label: 'POS Transactions Facility' },
|
||||
{ key: 'chequeBook', label: 'Cheque Book' },
|
||||
{ key: 'impsFacility', label: 'IMPS Facility' },
|
||||
{ key: 'smsAlerts', label: 'SMS Alerts' },
|
||||
{ key: 'bbpsFacility', label: 'BBPS Facility' },
|
||||
{ key: 'insuranceCover', label: 'Insurance Cover' },
|
||||
{ key: 'bepgFacility', label: 'BEPG Facility' },
|
||||
{ key: 'neftFacility', label: 'NEFT Facility' },
|
||||
{ key: 'rtgsFacility', label: 'RTGS Facility' },
|
||||
{ key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' },
|
||||
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
|
||||
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
|
||||
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { savingProduct: any }) => {
|
||||
this.savingProduct = data.savingProduct;
|
||||
@@ -85,4 +104,22 @@ export class SavingProductGeneralTabComponent {
|
||||
isAccrualAccounting(): boolean {
|
||||
return this.accounting.isAccrualAccounting(this.savingProduct.accountingRule);
|
||||
}
|
||||
|
||||
formatGlAccountById(glAccountId: number, options: any[]): string {
|
||||
if (!glAccountId || !Array.isArray(options)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const account = this.accounting.glAccountLookUp(glAccountId, options as any);
|
||||
return account ? `(${account.glCode}) ${account.name}` : '-';
|
||||
}
|
||||
|
||||
formatGlAccountByCode(glCode: string, options: any[]): string {
|
||||
if (!glCode || !Array.isArray(options)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const account = options.find((option) => option?.glCode === glCode);
|
||||
return account ? `(${account.glCode}) ${account.name}` : glCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260407',
|
||||
'hash': 'gb890d24bb'
|
||||
'version': '260413',
|
||||
'hash': '79e455c4'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user