update current product code and RD code

This commit is contained in:
Nitesh Yadav
2026-04-15 20:46:15 +05:30
parent 4679c6f729
commit fe61c925ae
115 changed files with 8057 additions and 1717 deletions
View File
+6 -2
View File
@@ -6,8 +6,12 @@
"Bash(git commit *)", "Bash(git commit *)",
"Bash(select-object *)", "Bash(select-object *)",
"Bash(git config *)", "Bash(git config *)",
"Bash(set *)" "Bash(set *)",
"Bash(git push)",
"Bash(xcopy *)",
"Bash(powershell *)",
"mcp__idea__build_project"
] ]
}, },
"$version": 3 "$version": 3
} }
@@ -98,10 +98,10 @@
gap: 12px; gap: 12px;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.cc-grid { .cc-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -98,10 +98,10 @@
gap: 12px; gap: 12px;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.ec-grid { .ec-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -109,10 +109,10 @@
gap: 12px; gap: 12px;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.vc-top-bar { .vc-top-bar {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -72,10 +72,10 @@
gap: 12px; gap: 12px;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.ccl-section__body { .ccl-section__body {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -72,10 +72,10 @@
gap: 12px; gap: 12px;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.ecl-section__body { .ecl-section__body {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -99,7 +99,7 @@
} }
} }
@media (max-width: 600px) { @media (width <= 600px) {
.vcl-top-bar { .vcl-top-bar {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -12,14 +12,21 @@
<h2 class="cp-header__title">{{ 'labels.heading.Create Current Product' | translate }}</h2> <h2 class="cp-header__title">{{ 'labels.heading.Create Current Product' | translate }}</h2>
</div> </div>
<mat-tab-group class="cp-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex"> <mat-tab-group
class="cp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<i class="fas fa-info-circle cp-tab-icon"></i> <i class="fas fa-info-circle cp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }} {{ 'labels.inputs.DETAILS' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-details-step></mifosx-saving-product-details-step> <mifosx-current-product-details-step
[CurrentProductsTemplate]="currentProductsTemplate"
></mifosx-current-product-details-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -29,9 +36,9 @@
{{ 'labels.inputs.CURRENCY' | translate }} {{ 'labels.inputs.CURRENCY' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-currency-step <mifosx-current-product-currency-step
[savingProductsTemplate]="currentProductsTemplate" [CurrentProductsTemplate]="currentProductsTemplate"
></mifosx-saving-product-currency-step> ></mifosx-current-product-currency-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -41,9 +48,9 @@
{{ 'labels.inputs.TERMS' | translate }} {{ 'labels.inputs.TERMS' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-terms-step <mifosx-current-product-terms-step
[savingProductsTemplate]="currentProductsTemplate" [CurrentProductsTemplate]="currentProductsTemplate"
></mifosx-saving-product-terms-step> ></mifosx-current-product-terms-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -53,9 +60,9 @@
{{ 'labels.inputs.SETTINGS' | translate }} {{ 'labels.inputs.SETTINGS' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-settings-step <mifosx-current-product-settings-step
[savingProductsTemplate]="currentProductsTemplate" [CurrentProductsTemplate]="currentProductsTemplate"
></mifosx-saving-product-settings-step> ></mifosx-current-product-settings-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -65,10 +72,22 @@
{{ 'labels.inputs.CHARGES' | translate }} {{ 'labels.inputs.CHARGES' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-charges-step <mifosx-current-product-charges-step
[savingProductsTemplate]="currentProductsTemplate" [CurrentProductsTemplate]="currentProductsTemplate"
[currencyCode]="currencyForm.get('currencyCode')" [currencyCode]="currencyForm.get('currencyCode')"
></mifosx-saving-product-charges-step> ></mifosx-current-product-charges-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-mobile-alt cp-tab-icon"></i>
Digital
</ng-template>
<div class="cp-tab-content">
<mifosx-current-product-limits-step
[CurrentProductsTemplate]="currentProductsTemplate"
></mifosx-current-product-limits-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -78,13 +97,13 @@
{{ 'labels.inputs.ACCOUNTING' | translate }} {{ 'labels.inputs.ACCOUNTING' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-accounting-step <mifosx-current-product-accounting-step
[savingProductsTemplate]="currentProductsTemplate" [CurrentProductsTemplate]="currentProductsTemplate"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[isDormancyTrackingActive]="settingsForm.get('isDormancyTrackingActive')" [isDormancyTrackingActive]="settingsForm.get('isDormancyTrackingActive')"
[savingProductFormValid]="formValid" [CurrentProductFormValid]="formValid"
[allowOverdraft]="settingsForm.get('allowOverdraft')" [allowOverdraft]="settingsForm.get('allowOverdraft')"
></mifosx-saving-product-accounting-step> ></mifosx-current-product-accounting-step>
</div> </div>
</mat-tab> </mat-tab>
@@ -95,13 +114,13 @@
{{ 'labels.inputs.PREVIEW' | translate }} {{ 'labels.inputs.PREVIEW' | translate }}
</ng-template> </ng-template>
<div class="cp-tab-content"> <div class="cp-tab-content">
<mifosx-saving-product-preview-step <mifosx-current-product-preview-step
[savingProductsTemplate]="currentProductsTemplate" [CurrentProductsTemplate]="currentProductsTemplate"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[savingProduct]="currentProduct" [CurrentProduct]="currentProduct"
[taskPermission]="'CREATE_CURRENTPRODUCT'" [taskPermission]="'CREATE_CURRENTPRODUCT'"
(submitEvent)="submit()" (submitEvent)="submit()"
></mifosx-saving-product-preview-step> ></mifosx-current-product-preview-step>
</div> </div>
</mat-tab> </mat-tab>
} }
@@ -114,19 +133,19 @@
<div class="cp-action-bar__right"> <div class="cp-action-bar__right">
@if (selectedTabIndex > 0) { @if (selectedTabIndex > 0) {
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1"> <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 }} {{ 'labels.buttons.Previous' | translate }}
</button> </button>
} }
@if (selectedTabIndex < totalTabs - 1) { @if (selectedTabIndex < totalTabs - 1) {
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1"> <button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
{{ 'labels.buttons.Next' | translate }} {{ '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> </button>
} }
@if (formValid && selectedTabIndex === totalTabs - 1) { @if (formValid && selectedTabIndex === totalTabs - 1) {
<button mat-raised-button color="primary" (click)="submit()"> <button mat-raised-button color="primary" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px;"></i> <i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }} {{ 'labels.buttons.Submit' | translate }}
</button> </button>
} }
@@ -35,11 +35,11 @@ $primary: #1353a0;
.cp-tabs { .cp-tabs {
background: var(--md-sys-color-surface, #fff); background: var(--md-sys-color-surface, #fff);
border-radius: 8px; 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 { ::ng-deep {
.mat-mdc-tab-header { .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 { .mat-mdc-tab {
@@ -77,10 +77,10 @@ $primary: #1353a0;
align-items: center; align-items: center;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel { &__cancel {
color: rgba(0, 0, 0, 0.6); color: rgb(0 0 0 / 60%);
} }
&__right { &__right {
@@ -8,13 +8,14 @@
import { Component, ViewChild, inject } from '@angular/core'; import { Component, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; import { CurrentProductDetailsStepComponent } from '../current-product-stepper/current-product-details-step/current-product-details-step.component';
import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; import { CurrentProductCurrencyStepComponent } from '../current-product-stepper/current-product-currency-step/current-product-currency-step.component';
import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; import { CurrentProductTermsStepComponent } from '../current-product-stepper/current-product-terms-step/current-product-terms-step.component';
import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; import { CurrentProductSettingsStepComponent } from '../current-product-stepper/current-product-settings-step/current-product-settings-step.component';
import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; import { CurrentProductChargesStepComponent } from '../current-product-stepper/current-product-charges-step/current-product-charges-step.component';
import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; import { CurrentProductLimitsStepComponent } from '../current-product-stepper/current-product-limits-step/current-product-limits-step.component';
import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; import { CurrentProductAccountingStepComponent } from '../current-product-stepper/current-product-accounting-step/current-product-accounting-step.component';
import { CurrentProductPreviewStepComponent } from '../current-product-stepper/current-product-preview-step/current-product-preview-step.component';
import { ProductsService } from 'app/products/products.service'; import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
@@ -28,13 +29,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatTabsModule, MatTabsModule,
SavingProductDetailsStepComponent, CurrentProductDetailsStepComponent,
SavingProductCurrencyStepComponent, CurrentProductCurrencyStepComponent,
SavingProductTermsStepComponent, CurrentProductTermsStepComponent,
SavingProductSettingsStepComponent, CurrentProductSettingsStepComponent,
SavingProductChargesStepComponent, CurrentProductChargesStepComponent,
SavingProductAccountingStepComponent, CurrentProductLimitsStepComponent,
SavingProductPreviewStepComponent CurrentProductAccountingStepComponent,
CurrentProductPreviewStepComponent
] ]
}) })
export class CreateCurrentProductComponent { export class CreateCurrentProductComponent {
@@ -44,13 +46,14 @@ export class CreateCurrentProductComponent {
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private accounting = inject(Accounting); private accounting = inject(Accounting);
@ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; @ViewChild(CurrentProductDetailsStepComponent, { static: true }) detailsStep: CurrentProductDetailsStepComponent;
@ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; @ViewChild(CurrentProductCurrencyStepComponent, { static: true }) currencyStep: CurrentProductCurrencyStepComponent;
@ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; @ViewChild(CurrentProductTermsStepComponent, { static: true }) termsStep: CurrentProductTermsStepComponent;
@ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; @ViewChild(CurrentProductSettingsStepComponent, { static: true }) settingsStep: CurrentProductSettingsStepComponent;
@ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; @ViewChild(CurrentProductChargesStepComponent, { static: true }) chargesStep: CurrentProductChargesStepComponent;
@ViewChild(SavingProductAccountingStepComponent, { static: true }) @ViewChild(CurrentProductLimitsStepComponent, { static: true }) limitsStep: CurrentProductLimitsStepComponent;
accountingStep: SavingProductAccountingStepComponent; @ViewChild(CurrentProductAccountingStepComponent, { static: true })
accountingStep: CurrentProductAccountingStepComponent;
currentProductsTemplate: any; currentProductsTemplate: any;
accountingRuleData: string[] = []; accountingRuleData: string[] = [];
@@ -64,23 +67,26 @@ export class CreateCurrentProductComponent {
} }
get detailsForm() { get detailsForm() {
return this.detailsStep.savingProductDetailsForm; return this.detailsStep.CurrentProductDetailsForm;
} }
get currencyForm() { get currencyForm() {
return this.currencyStep.savingProductCurrencyForm; return this.currencyStep.CurrentProductCurrencyForm;
} }
get termsForm() { get termsForm() {
return this.termsStep.savingProductTermsForm; return this.termsStep.CurrentProductTermsForm;
} }
get settingsForm() { get settingsForm() {
return this.settingsStep.savingProductSettingsForm; return this.settingsStep.CurrentProductSettingsForm;
}
get limitsForm(): void {
return; // Limits step doesn't have a form, it's data-driven
} }
get accountingForm() { get accountingForm() {
return this.accountingStep.savingProductAccountingForm; return this.accountingStep.CurrentProductAccountingForm;
} }
get totalTabs(): number { get totalTabs(): number {
return this.formValid ? 7 : 6; return this.formValid ? 8 : 7;
} }
get formValid() { get formValid() {
@@ -95,23 +101,50 @@ export class CreateCurrentProductComponent {
get currentProduct() { get currentProduct() {
return { return {
...this.detailsStep.savingProductDetails, ...this.detailsStep.CurrentProductDetails,
...this.currencyStep.savingProductCurrency, ...this.currencyStep.CurrentProductCurrency,
...this.termsStep.savingProductTerms, ...this.termsStep.CurrentProductTerms,
...this.settingsStep.savingProductSettings, ...this.settingsStep.CurrentProductSettings,
...this.chargesStep.savingProductCharges, ...this.chargesStep.CurrentProductCharges,
...this.accountingStep.savingProductAccounting ...this.limitsStep.CurrentProductLimits,
...this.accountingStep.CurrentProductAccounting
}; };
} }
submit() { private prepareCurrentProductPayload() {
const product = { const currentProduct = {
...this.currentProduct, ...this.currentProduct,
charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code locale: this.settingsService.language.code,
dateFormat: 'dd MMMM yyyy'
}; };
delete product.advancedAccountingRules; delete currentProduct.advancedAccountingRules;
this.productsService.createCurrentProduct(product).subscribe((response: any) => {
// Convert lockinPeriodFrequencyType from string to integer
// Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS
if (currentProduct.lockinPeriodFrequencyType) {
const frequencyTypeMapping: { [key: string]: number } = {
DAYS: 0,
WEEKS: 1,
MONTHS: 2,
YEARS: 3
};
currentProduct.lockinPeriodFrequencyType = frequencyTypeMapping[currentProduct.lockinPeriodFrequencyType];
}
Object.keys(currentProduct).forEach((key) => {
if (currentProduct[key] === '' || currentProduct[key] === null || currentProduct[key] === undefined) {
delete currentProduct[key];
}
});
return currentProduct;
}
submit() {
const currentProduct = this.prepareCurrentProductPayload();
console.log('Current product submit payload:', currentProduct);
this.productsService.createCurrentProduct(currentProduct).subscribe((response: any) => {
this.router.navigate( this.router.navigate(
[ [
'../', '../',
@@ -0,0 +1,121 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<form [formGroup]="CurrentProductAccountingForm">
<div class="spa-section">
<div class="spa-section__header">
<span class="spa-section__icon">
<i class="fas fa-book"></i>
</span>
<h3>Accounting</h3>
</div>
<div class="spa-section__body">
<div class="spa-group">
<div class="spa-group__header">
<h4>Accounting Rule</h4>
</div>
<div class="spa-radio-group">
<mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
</div>
</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>
<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'"
>
{{ section.pillLabel }}
</span>
<span class="spa-row__label">{{ row.label }}</span>
</div>
<mifosx-gl-account-selector
class="spa-row__input"
[inputFormControl]="CurrentProductAccountingForm.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>
}
}
</div>
</div>
</form>
@@ -0,0 +1,314 @@
/**
* 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: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #6b7280;
$gray-700: #334155;
$gray-900: #202124;
$primary: #1353a0;
.spa-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
}
.spa-section__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
.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: 16px;
margin: 0 0 12px;
}
h4 {
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #94a3b8;
}
}
.spa-add-btn {
border: 0;
background: transparent;
color: #2563eb;
font-size: 12px;
font-weight: 700;
padding: 0;
cursor: pointer;
}
.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;
background: #fff;
@media (width <= 900px) {
flex-direction: column;
align-items: stretch;
}
}
.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%;
}
}
@@ -0,0 +1,495 @@
/**
* 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, OnInit, inject } from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators
} from '@angular/forms';
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-current-product-accounting-step',
templateUrl: './current-product-accounting-step.component.html',
styleUrls: ['./current-product-accounting-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
ReactiveFormsModule,
MatRadioGroup,
MatRadioButton,
GlAccountSelectorComponent
]
})
export class CurrentProductAccountingStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private accounting = inject(Accounting);
@Input() CurrentProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() isDormancyTrackingActive: any;
@Input() CurrentProductFormValid: boolean;
@Input() allowOverdraft: any;
CurrentProductAccountingForm: UntypedFormGroup;
liabilityAccountOptions: GLAccount[] = [];
expenseAccountOptions: GLAccount[] = [];
incomeAccountOptions: GLAccount[] = [];
assetAccountOptions: GLAccount[] = [];
accountingSectionConfigs: AccountingSectionConfig[] = [];
constructor() {
this.createCurrentProductAccountingForm();
this.setConditionalControls();
}
ngOnInit() {
const accountingMappings = this.CurrentProductsTemplate?.accountingMappings || {};
// otherGlMappingsMap is at the root of the template, not inside accountingMappings
const mappings = {
...accountingMappings,
otherGlMappingsMap: this.CurrentProductsTemplate?.otherGlMappingsMap
};
const accountingMappingOptions = this.CurrentProductsTemplate?.accountingMappingOptions || {};
const accountingRuleId = 2; // Always Cash
this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
this.buildAccountingSectionConfigs();
this.CurrentProductAccountingForm.patchValue({
accountingRule: accountingRuleId
});
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
this.setOverdraftControl();
this.setDormancyControl();
}
createCurrentProductAccountingForm() {
this.CurrentProductAccountingForm = this.formBuilder.group({
accountingRule: [2],
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
incomeOtherGlMappings: this.formBuilder.array([])
});
}
private setConditionalControls() {
this.CurrentProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => {
this.syncAccountingControls(accountingRule);
});
}
private patchAccountingValues(mappings: any) {
this.CurrentProductAccountingForm.patchValue({
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
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 || ''
});
if (this.isCashOrAccrualAccounting()) {
// Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility
const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings;
this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
this.setOtherGlMappings(
'liabilitiesOtherGlMappings',
glMappings?.liabilities || [],
this.liabilityAccountOptions
);
this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
}
}
private createOtherGlMappingGroup(otherGlMapping: Partial<OtherGlMappingValue> = {}) {
return this.formBuilder.group({
accountName: [
otherGlMapping.accountName || '',
Validators.required
],
glAccountId: [
otherGlMapping.glAccountId || '',
Validators.required
]
});
}
getOtherGlMappingsArray(controlName: string): UntypedFormArray {
return this.CurrentProductAccountingForm.get(controlName) as UntypedFormArray;
}
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
})
);
});
}
addOtherGlMapping(controlName: string) {
this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
}
removeOtherGlMapping(controlName: string, index: number) {
this.getOtherGlMappingsArray(controlName).removeAt(index);
}
getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl {
return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl;
}
private clearOtherGlMappings() {
[
'assetsOtherGlMappings',
'liabilitiesOtherGlMappings',
'expensesOtherGlMappings',
'incomeOtherGlMappings'
].forEach((controlName) => this.getOtherGlMappingsArray(controlName).clear());
}
private setOverdraftControl() {
this.allowOverdraft?.valueChanges?.subscribe(() => {
this.syncAccountingControls(this.CurrentProductAccountingForm.value.accountingRule);
});
}
private setDormancyControl() {
this.isDormancyTrackingActive?.valueChanges?.subscribe(() => {
this.syncAccountingControls(this.CurrentProductAccountingForm.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.CurrentProductAccountingForm.contains(controlName)) {
this.CurrentProductAccountingForm.addControl(
controlName,
new UntypedFormControl('', required ? Validators.required : undefined)
);
} else if (required) {
this.CurrentProductAccountingForm.get(controlName)?.setValidators(Validators.required);
this.CurrentProductAccountingForm.get(controlName)?.updateValueAndValidity({ emitEvent: false });
}
};
const removeControl = (controlName: string) => {
if (this.CurrentProductAccountingForm.contains(controlName)) {
this.CurrentProductAccountingForm.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 {
removeControl('feesReceivableAccountId');
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
removeControl('interestReceivableAccountId');
}
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();
}
}
get CurrentProductAccounting() {
const {
assetsOtherGlMappings,
liabilitiesOtherGlMappings,
expensesOtherGlMappings,
incomeOtherGlMappings,
...CurrentProductAccounting
} = this.CurrentProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings();
return {
...CurrentProductAccounting,
...(otherGlMappings ? { otherGlMappings } : {})
};
}
isCashOrAccrualAccounting(): boolean {
return this.accounting.isCashOrAccrualAccountingRuleId(this.CurrentProductAccountingForm.value.accountingRule);
}
isAccrualAccounting(): boolean {
return this.accounting.isAccrualAccountingRuleId(this.CurrentProductAccountingForm.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
}
];
}
}
@@ -0,0 +1,93 @@
<!--
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="spch-section">
<div class="spch-section__header">
<i class="fas fa-receipt"></i>
<h3>{{ 'labels.inputs.Charges' | translate }}</h3>
</div>
<div class="spch-body">
<div class="spch-add-row">
<mat-form-field appearance="outline" class="spch-add-row__select">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select #charge>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
<mat-option [value]="charge">
{{ charge.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<button
type="button"
mat-flat-button
color="primary"
(click)="addCharge(charge)"
[disabled]="!charge.value"
class="spch-add-row__btn"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargesDataSource.length > 0) {
<div class="spch-table-wrap">
<table mat-table [dataSource]="chargesDataSource" class="spch-table">
<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>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
@if (chargesDataSource.length === 0) {
<div class="spch-empty">
<i class="fas fa-info-circle"></i>
<span>{{ 'labels.text.No charges added yet' | translate }}</span>
</div>
}
</div>
</div>
@@ -0,0 +1,103 @@
/**
* 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/.
*/
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-900: #202124;
.spch-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
&__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;
}
}
}
.spch-body {
padding: 16px 24px;
}
.spch-add-row {
display: flex;
align-items: flex-start;
gap: 12px;
&__select {
flex: 1;
max-width: 400px;
}
&__btn {
margin-top: 4px;
height: 36px !important;
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
}
@media (width <= 600px) {
flex-direction: column;
&__select {
max-width: 100%;
width: 100%;
}
}
}
.spch-table-wrap {
margin-top: 8px;
border: 1px solid $border;
border-radius: 8px;
overflow: hidden;
}
.spch-table {
width: 100%;
}
.spch-empty {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
margin-top: 8px;
background: $gray-50;
border-radius: 8px;
color: $gray-500;
font-size: 13px;
i {
font-size: 14px;
}
}
@@ -0,0 +1,108 @@
/**
* 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, OnInit, inject } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
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 { ChargesFilterPipe } from '../../../../pipes/charges-filter.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-product-charges-step',
templateUrl: './current-product-charges-step.component.html',
styleUrls: ['./current-product-charges-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
ChargesFilterPipe,
FormatNumberPipe
]
})
export class CurrentProductChargesStepComponent implements OnInit {
dialog = inject(MatDialog);
private translateService = inject(TranslateService);
@Input() CurrentProductsTemplate: any;
@Input() currencyCode: UntypedFormControl;
chargeData: any;
chargesDataSource: {}[];
displayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'action'
];
pristine = true;
ngOnInit() {
this.chargeData = this.CurrentProductsTemplate.chargeOptions;
this.chargesDataSource = this.CurrentProductsTemplate.charges || [];
this.pristine = true;
this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = []));
}
addCharge(charge: any) {
this.chargesDataSource = this.chargesDataSource.concat([charge.value]);
charge.value = '';
this.pristine = false;
}
deleteCharge(charge: any) {
const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: this.translateService.instant('labels.inputs.Charge') + ' ' + charge.name }
});
deleteChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
this.chargesDataSource = this.chargesDataSource.concat([]);
this.pristine = false;
}
});
}
get CurrentProductCharges() {
return {
charges: this.chargesDataSource
};
}
}
@@ -0,0 +1,111 @@
<!--
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/.
-->
<form [formGroup]="CurrentProductCurrencyForm">
<div class="spc-section">
<div class="spc-section__header">
<i class="fas fa-coins"></i>
<span>Currency &amp; Account Status Configuration</span>
</div>
<div class="spc-body">
<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-hint>Default: Indian Rupee - auto-set for domestic products</mat-hint>
<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>
<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>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-radio-group class="spc-alert__choices" formControlName="balanceSwapOption">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="spc-alert">
<div class="spc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<mat-radio-group class="spc-alert__choices" formControlName="msme">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="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 class="spc-alert">
<div class="spc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-radio-group class="spc-alert__choices" formControlName="autoDebitFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
</form>
@@ -0,0 +1,114 @@
/**
* 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/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.spc-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;
}
}
}
.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 (width <= 600px) {
grid-template-columns: 1fr;
}
}
.spc-body {
background: #fff;
}
.spc-field {
width: 100%;
}
.spc-alert {
display: flex;
align-items: center;
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;
}
}
@@ -0,0 +1,94 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-product-currency-step',
templateUrl: './current-product-currency-step.component.html',
styleUrls: ['./current-product-currency-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
MatRadioGroup,
MatRadioButton,
FaIconComponent
]
})
export class CurrentProductCurrencyStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
@Input() CurrentProductsTemplate: any;
CurrentProductCurrencyForm: UntypedFormGroup;
currencyData: any;
decimalPlacesData = [
2,
0,
3
];
constructor() {
this.createCurrentProductCurrencyForm();
}
ngOnInit() {
this.currencyData = this.CurrentProductsTemplate.currencyOptions;
this.CurrentProductCurrencyForm.patchValue({
currencyCode: this.CurrentProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
digitsAfterDecimal:
this.CurrentProductsTemplate.digitsAfterDecimal ?? this.CurrentProductsTemplate.currency?.decimalPlaces ?? 2,
dormancyAlert: this.CurrentProductsTemplate.dormancyAlert ?? false,
balanceSwapOption: this.CurrentProductsTemplate.balanceSwapOption ?? false,
msme: this.CurrentProductsTemplate.msme ?? false,
nominationFacility: this.CurrentProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.CurrentProductsTemplate.autoDebitFacility ?? false
});
}
createCurrentProductCurrencyForm() {
this.CurrentProductCurrencyForm = this.formBuilder.group({
currencyCode: [
'',
Validators.required
],
digitsAfterDecimal: [
2,
[
Validators.required,
Validators.min(0)
]
],
dormancyAlert: [false],
balanceSwapOption: [false],
msme: [false],
nominationFacility: [false],
autoDebitFacility: [false]
});
}
get CurrentProductCurrency() {
const formValue = this.CurrentProductCurrencyForm.value;
return {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
};
}
}
@@ -0,0 +1,117 @@
<!--
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/.
-->
<form [formGroup]="CurrentProductDetailsForm">
<div class="spd-section">
<div class="spd-section__header">
<i class="fas fa-info-circle"></i>
<h3>{{ 'labels.inputs.Product Name' | translate }} &amp; {{ 'labels.inputs.Description' | translate }}</h3>
</div>
<div class="spd-grid">
<mat-form-field appearance="outline" class="spd-field">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<input
matInput
formControlName="name"
matTooltip="{{ 'tooltips.A unique identifier for the current product' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="spd-field">
<mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label>
<input
matInput
formControlName="shortName"
matTooltip="{{ 'tooltips.A unique identifier for the current product' | translate }}"
maxlength="4"
required
/>
<mat-error>
{{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="spd-field spd-field--full">
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
<textarea
matInput
matTooltip="{{ 'tooltips.Provides additional information' | translate }}"
formControlName="description"
cdkTextareaAutosize
cdkAutosizeMinRows="2"
></textarea>
</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" readonly />
</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>
@@ -0,0 +1,70 @@
/**
* 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/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.spd-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
&__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;
}
}
}
.spd-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.spd-grid--classification {
grid-template-columns: repeat(3, 1fr);
}
.spd-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.spd-checkbox-row {
display: flex;
align-items: center;
min-height: 56px;
}
@@ -0,0 +1,190 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, 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({
selector: 'mifosx-current-product-details-step',
templateUrl: './current-product-details-step.component.html',
styleUrls: ['./current-product-details-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
CdkTextareaAutosize,
FaIconComponent
]
})
export class CurrentProductDetailsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() CurrentProductsTemplate: any;
CurrentProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = [];
customerSegmentData: string[] = [];
yesNoOptions: Array<{
value: boolean;
label: string;
}> = [
{
value: true,
label: 'Yes'
},
{
value: false,
label: 'No'
}
];
constructor() {
this.createCurrentProductDetailsForm();
}
ngOnInit() {
if (this.CurrentProductsTemplate) {
this.applyTemplate(this.CurrentProductsTemplate);
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.CurrentProductsTemplate?.currentValue) {
this.applyTemplate(changes.CurrentProductsTemplate.currentValue);
}
}
createCurrentProductDetailsForm() {
this.CurrentProductDetailsForm = this.formBuilder.group({
name: [
'',
Validators.required
],
shortName: [
'',
Validators.required
],
description: [''],
accountType: [
null,
Validators.required
],
customerSegment: [null],
productCode: [''],
effectiveTo: [''],
endDate: [''],
introducerRequired: [false],
shareLinkingEnabled: [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 parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
// Handle array format [year, month, day]
if (Array.isArray(dateValue)) {
return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
}
// Handle Date object
if (dateValue instanceof Date) {
return dateValue;
}
// Handle string format
return new Date(dateValue);
}
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.CurrentProductDetailsForm.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 ?? '',
effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate),
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
shareLinkingEnabled: !!template.shareLinkingEnabled
});
}
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 CurrentProductDetails() {
const formValue = { ...this.CurrentProductDetailsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveTo instanceof Date) {
formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || '';
}
if (formValue.endDate instanceof Date) {
formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || '';
}
return formValue;
}
}
@@ -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 &amp; Product Limits</div>
<div class="spl-section__badge">R&amp;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>
@@ -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;
}
}
@@ -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-current-product-limits-step',
templateUrl: './current-product-limits-step.component.html',
styleUrls: ['./current-product-limits-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule
]
})
export class CurrentProductLimitsStepComponent implements OnChanges {
@Input() CurrentProductsTemplate: 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.CurrentProductsTemplate?.currentValue) {
const template = changes.CurrentProductsTemplate.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 CurrentProductLimits() {
return {
accountFacilityConfig: { ...this.accountFacilityConfig },
accountFacilityLimits: this.accountFacilityLimits.map((row) => ({ ...row })),
depositLimitKyc: {
...this.depositLimitKyc
}
};
}
}
@@ -0,0 +1,799 @@
<!--
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="responsive-column">
<h2 class="mat-h2 flex-100">{{ CurrentProduct.name }}</h2>
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.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">{{ CurrentProduct.shortName }}</span>
</div>
@if (CurrentProduct.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.description }}</span>
</div>
}
@if (
CurrentProduct.accountType ||
CurrentProduct.customerSegment ||
CurrentProduct.productCode ||
CurrentProduct.effectiveTo ||
CurrentProduct.introducerRequired !== undefined ||
CurrentProduct.shareLinkingEnabled !== undefined
) {
<h3 class="mat-h3 flex-100">Product Classification</h3>
<mat-divider class="flex-100"></mat-divider>
@if (CurrentProduct.accountType) {
<div class="flex-100 layout-row">
<span class="flex-40">Account Type:</span>
<span class="flex-60">{{ CurrentProduct.accountType }}</span>
</div>
}
@if (CurrentProduct.customerSegment) {
<div class="flex-100 layout-row">
<span class="flex-40">Customer Segment:</span>
<span class="flex-60">{{ CurrentProduct.customerSegment }}</span>
</div>
}
@if (CurrentProduct.productCode) {
<div class="flex-100 layout-row">
<span class="flex-40">Product Code:</span>
<span class="flex-60">{{ CurrentProduct.productCode }}</span>
</div>
}
@if (CurrentProduct.effectiveTo) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span>
<span class="flex-60">{{ CurrentProduct.effectiveTo }}</span>
</div>
}
@if (CurrentProduct.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">End Date:</span>
<span class="flex-60">{{ CurrentProduct.endDate }}</span>
</div>
}
@if (CurrentProduct.introducerRequired !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Introducer Required:</span>
<span class="flex-60">{{
CurrentProduct.introducerRequired === true || CurrentProduct.introducerRequired === 'Yes' ? 'Yes' : 'No'
}}</span>
</div>
}
@if (CurrentProduct.shareLinkingEnabled !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Share Linking Enabled:</span>
<span class="flex-60">{{ CurrentProduct.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">{{
CurrentProduct.currency?.name ||
CurrentProduct.currency?.displayLabel ||
(CurrentProduct.currencyCode | find: CurrentProductsTemplate?.currencyOptions : 'code' : 'name') ||
CurrentProduct.currencyCode ||
'-'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.digitsAfterDecimal ?? CurrentProduct.currency?.decimalPlaces ?? 2 }}</span>
</div>
@if (CurrentProduct.currency?.inMultiplesOf !== undefined && CurrentProduct.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">{{ CurrentProduct.currency.inMultiplesOf }}</span>
</div>
}
@if (CurrentProduct.dormancyAlert !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ CurrentProduct.dormancyAlert | yesNo }}</span>
</div>
}
@if (CurrentProduct.balanceSwapOption !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Balance Swap Option:</span>
<span class="flex-60">{{ CurrentProduct.balanceSwapOption | yesNo }}</span>
</div>
}
@if (CurrentProduct.msme !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">MSME:</span>
<span class="flex-60">{{ CurrentProduct.msme | yesNo }}</span>
</div>
}
@if (CurrentProduct.nominationFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span>
<span class="flex-60">{{ CurrentProduct.nominationFacility | yesNo }}</span>
</div>
}
@if (CurrentProduct.autoDebitFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Auto Debit Facility:</span>
<span class="flex-60">{{ CurrentProduct.autoDebitFacility | yesNo }}</span>
</div>
}
<h3 class="mat-h3 flex-100">Interest On OD</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">Interest Posting Frequency:</span>
<span class="flex-60">{{
CurrentProduct.interestPostingPeriodType
| find: CurrentProductsTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Auto Interest Post:</span>
<span class="flex-60">{{ CurrentProduct.autoPost | yesNo }}</span>
</div>
@if (
CurrentProduct.frequencyFixDay !== undefined &&
CurrentProduct.frequencyFixDay !== null &&
CurrentProduct.frequencyFixDay !== ''
) {
<div class="flex-100 layout-row">
<span class="flex-40">Frequency (Fix Day):</span>
<span class="flex-60">{{ CurrentProduct.frequencyFixDay }} Days</span>
</div>
}
@if (CurrentProduct.lastInterestDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ CurrentProduct.lastInterestDate }}</span>
</div>
}
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (CurrentProduct.minRequiredOpeningBalance) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
@if (CurrentProduct.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">{{
CurrentProduct.lockinPeriodFrequency +
' ' +
(CurrentProduct.lockinPeriodFrequencyType
| find: CurrentProductsTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value')
| translateKey: 'catalogs'
}}</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">{{ CurrentProduct.withdrawalFeeForTransfers | yesNo }}</span>
</div>
@if (CurrentProduct.balanceFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">Balance Frequency:</span>
<span class="flex-60">{{ CurrentProduct.balanceFrequency }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Enable Lock-in Period:</span>
<span class="flex-60">{{ CurrentProduct.enableLockinPeriod | yesNo }}</span>
</div>
@if (CurrentProduct.enableLockinPeriod && CurrentProduct.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">Lock-in Period:</span>
<span class="flex-60">
{{ CurrentProduct.lockinPeriodFrequency }}
{{ getLockinPeriodFrequencyTypeLabel(CurrentProduct.lockinPeriodFrequencyType) }}
</span>
</div>
}
@if (CurrentProduct.minBalanceForInterestCalculation) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.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">{{ CurrentProduct.enforceMinRequiredBalance | yesNo }}</span>
</div>
@if (CurrentProduct.minRequiredBalance) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.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">{{ CurrentProduct.withHoldTax | yesNo }}</span>
</div>
@if (CurrentProduct.withHoldTax) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Tax Group' | translate }}:</span>
<span class="flex-60">{{
CurrentProduct.taxGroupId | find: CurrentProductsTemplate?.taxGroupOptions : 'id' : 'name'
}}</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">{{ CurrentProduct.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
<h3 class="mat-h3 flex-100">Overdraft (OD) Facility</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">Is Overdraft Allowed:</span>
<span class="flex-60">{{ CurrentProduct.allowOverdraft | yesNo }}</span>
</div>
@if (CurrentProduct.allowOverdraft) {
@if (CurrentProduct.avgBalanceCalcPeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Avg Balance Calculation Period:</span>
<span class="flex-60">{{ CurrentProduct.avgBalanceCalcPeriod }}</span>
</div>
}
@if (CurrentProduct.validForPeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Valid for Period:</span>
<span class="flex-60">{{ CurrentProduct.validForPeriod }}</span>
</div>
}
@if (CurrentProduct.overdraftLimit !== undefined && CurrentProduct.overdraftLimit !== '') {
<div class="flex-100 layout-row">
<span class="flex-40">OD Limit %:</span>
<span class="flex-60">{{ CurrentProduct.overdraftLimit }}</span>
</div>
}
@if (
CurrentProduct.nominalAnnualInterestRateOverdraft !== undefined &&
CurrentProduct.nominalAnnualInterestRateOverdraft !== ''
) {
<div class="flex-100 layout-row">
<span class="flex-40">Interest on OD %:</span>
<span class="flex-60">{{ CurrentProduct.nominalAnnualInterestRateOverdraft }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">OD Account Closed with Zero Balance in Day Closing:</span>
<span class="flex-60">{{ CurrentProduct.odAccountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Enable Dormancy Tracking' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.isDormancyTrackingActive | yesNo }}</span>
</div>
@if (CurrentProduct.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">{{ CurrentProduct.daysToInactive }}</span>
<span class="flex-40">{{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.daysToDormancy }}</span>
<span class="flex-40">{{ 'labels.inputs.Number of Days to Escheat' | translate }}:</span>
<span class="flex-60">{{ CurrentProduct.daysToEscheat }}</span>
</div>
}
<h3 class="mat-h3 flex-100">Account Facilities</h3>
<mat-divider class="flex-100"></mat-divider>
@if (CurrentProduct.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">{{ CurrentProduct.accountFacilityConfig[feature.key] | yesNo }}</span>
</div>
}
</div>
}
@if (CurrentProduct.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]="CurrentProduct.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 (CurrentProduct.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"
>{{ CurrentProduct.depositLimitKyc.general.kycType }} -
{{ CurrentProduct.depositLimitKyc.general.limit }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Minor:</span>
<span class="flex-60"
>{{ CurrentProduct.depositLimitKyc.minor.kycType }} - {{ CurrentProduct.depositLimitKyc.minor.limit }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Small Saving:</span>
<span class="flex-60">
{{ CurrentProduct.depositLimitKyc.smallSaving.kycType }} -
{{ CurrentProduct.depositLimitKyc.smallSaving.limit }}
</span>
</div>
</div>
}
@if (CurrentProduct.charges.length) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="CurrentProduct.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>
@if (isCashOrAccrualAccounting()) {
<div class="spa-gl-summary flex-100">
<div class="spa-gl-summary__header">
<h4 class="mat-h4">GL Mapping</h4>
</div>
<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(
CurrentProduct.savingsReferenceAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.overdraftPortfolioControlId,
CurrentProductsTemplate?.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(
CurrentProduct.interestReceivableAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.feesReceivableAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.penaltiesReceivableAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.savingsControlAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.transfersInSuspenseAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.interestPayableAccountId,
CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions
)
}}</span>
</div>
@if (CurrentProduct.isDormancyTrackingActive) {
<div class="spa-gl-row">
<span class="spa-gl-row__label">Escheat liability account</span>
<span class="spa-gl-row__value">{{
formatGlAccountById(
CurrentProduct.escheatLiabilityId,
CurrentProductsTemplate?.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(
CurrentProduct.interestOnSavingsAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.writeOffAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.incomeFromFeeAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.incomeFromPenaltyAccountId,
CurrentProductsTemplate?.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(
CurrentProduct.incomeFromInterestId,
CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions
)
}}</span>
</div>
</div>
</div>
}
@if (CurrentProduct.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 (CurrentProduct.otherGlMappings.assets?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="CurrentProduct.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, CurrentProductsTemplate?.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 (CurrentProduct.otherGlMappings.liabilities?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="CurrentProduct.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,
CurrentProductsTemplate?.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 (CurrentProduct.otherGlMappings.expenses?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="CurrentProduct.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,
CurrentProductsTemplate?.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 (CurrentProduct.otherGlMappings.income?.length) {
<table
class="spa-other-gl-table flex-100 mat-elevation-z1"
mat-table
[dataSource]="CurrentProduct.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,
CurrentProductsTemplate?.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 (
CurrentProduct.advancedAccountingRules &&
(CurrentProduct.paymentChannelToFundSourceMappings?.length ||
CurrentProduct.feeToIncomeAccountMappings?.length ||
CurrentProduct.penaltyToIncomeAccountMappings?.length)
) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (CurrentProduct.paymentChannelToFundSourceMappings?.length) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="CurrentProduct.paymentChannelToFundSourceMappings"
>
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find: CurrentProductsTemplate.paymentTypeOptions : '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: CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name'
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
@if (CurrentProduct.feeToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="CurrentProduct.feeToIncomeAccountMappings">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.chargeId | find: CurrentProductsTemplate.chargeOptions : '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: CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name'
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
@if (CurrentProduct.penaltyToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap responsive-column">
<h4 class="mat-h4 flex-100">{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}</h4>
<table
class="flex-100 mat-elevation-z1"
mat-table
[dataSource]="CurrentProduct.penaltyToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find: CurrentProductsTemplate.penaltyOptions : '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: CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name'
}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
}
</div>
@@ -0,0 +1,165 @@
/**
* 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/.
*/
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
.tab-container {
padding: 1%;
margin: 1%;
}
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
.interestCarts {
float: right;
}
h2,
h3,
h4 {
margin: 0;
font-weight: 500;
}
table {
width: 100%;
tr.incentives-row {
height: 0;
> td {
padding: 0;
}
.incentives {
overflow: hidden;
}
.mat-card {
border-radius: 0;
> h4 {
font-weight: 500;
margin: 0.5em 0 0;
}
}
}
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
.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;
}
}
@@ -0,0 +1,247 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core';
import { Accounting } from 'app/core/utils/accounting';
import { OptionData } from 'app/shared/models/option-data.model';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../../pipes/find.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-product-preview-step',
templateUrl: './current-product-preview-step.component.html',
styleUrls: ['./current-product-preview-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FaIconComponent,
FindPipe,
FormatNumberPipe,
YesnoPipe
]
})
export class CurrentProductPreviewStepComponent implements OnInit, OnChanges {
private accounting = inject(Accounting);
@Input() CurrentProductsTemplate: any;
@Input() accountingRuleData: any;
@Input() CurrentProduct: any;
@Input() taskPermission: string;
@Output() submitEvent = new EventEmitter();
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType'
];
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'incomeAccountId'
];
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();
}
ngOnChanges(changes: SimpleChanges): void {
this.setCurrentValues();
}
setCurrentValues(): void {
if (!this.CurrentProduct || !this.CurrentProductsTemplate) {
return;
}
this.interestCalculationTypeOptionData =
this.CurrentProductsTemplate.interestCalculationTypeOption ||
this.CurrentProductsTemplate.interestCalculationTypeOptions ||
[];
this.accountingRule = this.accounting.getAccountingRuleFrom(this.CurrentProduct.accountingRule);
if (!this.isCashOrAccrualAccounting()) {
return;
}
const assetAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.assetAccountOptions || [];
const incomeAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.incomeAccountOptions || [];
const expenseAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.expenseAccountOptions || [];
const liabilityAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions || [];
this.accountingMappings = {
savingsReferenceAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.savingsReferenceAccountId,
assetAccountData
),
overdraftPortfolioControl: this.accounting.glAccountLookUp(
this.CurrentProduct.overdraftPortfolioControlId,
assetAccountData
),
savingsControlAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.savingsControlAccountId,
liabilityAccountData
),
transfersInSuspenseAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.transfersInSuspenseAccountId,
liabilityAccountData
),
interestOnSavingsAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.interestOnSavingsAccountId,
expenseAccountData
),
writeOffAccount: this.accounting.glAccountLookUp(this.CurrentProduct.writeOffAccountId, expenseAccountData),
incomeFromFeeAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.incomeFromFeeAccountId,
incomeAccountData
),
incomeFromPenaltyAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.incomeFromPenaltyAccountId,
incomeAccountData
),
incomeFromInterest: this.accounting.glAccountLookUp(this.CurrentProduct.incomeFromInterestId, incomeAccountData)
};
if (this.isAccrualAccounting()) {
this.accountingMappings = {
...this.accountingMappings,
feeReceivableAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.feesReceivableAccountId,
assetAccountData
),
penaltyReceivableAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.penaltiesReceivableAccountId,
assetAccountData
),
interestPayableAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.interestPayableAccountId,
liabilityAccountData
),
interestReceivableAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.interestReceivableAccountId,
assetAccountData
)
};
}
if (this.CurrentProduct.isDormancyTrackingActive) {
this.accountingMappings = {
...this.accountingMappings,
escheatLiabilityAccount: this.accounting.glAccountLookUp(
this.CurrentProduct.escheatLiabilityId,
liabilityAccountData
)
};
}
}
isCashOrAccrualAccounting(): boolean {
return this.accounting.isCashOrAccrualAccounting(this.accountingRule);
}
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;
}
}
@@ -0,0 +1,189 @@
<!--
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/.
-->
<form [formGroup]="CurrentProductSettingsForm">
<!-- ── Balance & Minimum Balance ──────────────────────────────────────── -->
<div class="sps-section">
<div class="sps-section__header">
<span class="sps-section__badge"><i class="fas fa-balance-scale"></i></span>
<span>Balance &amp; Minimum Balance</span>
</div>
<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">
<span class="sps-section__badge"><i class="fas fa-lock"></i></span>
<span>Lock-in Period</span>
</div>
<div class="sps-body">
<div class="sps-lockin-row">
<span>Enable Lock-in Period</span>
<mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
</div>
@if (CurrentProductSettingsForm.value.enableLockinPeriod) {
<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 sps-field--compact">
<mat-label>Type *</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (type of lockinPeriodFrequencyTypeData; track type) {
<mat-option [value]="type.id">
{{ type.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div>
</div>
<!-- ── Additional Rules ───────────────────────────────────────────────── -->
<div class="sps-section">
<div class="sps-section__header">
<span class="sps-section__badge"><i class="fas fa-cog"></i></span>
<span>Additional Rules</span>
</div>
<div class="sps-body">
<div class="sps-toggle sps-toggle--inline">
<mat-checkbox formControlName="accountClosedZeroBalanceInDayClosing">
<span>Account Closed with Zero Balance in Day Closing</span>
</mat-checkbox>
</div>
<div class="sps-toggle sps-toggle--inline">
<mat-checkbox formControlName="withHoldTax">
<span>Withhold Tax is Applicable</span>
</mat-checkbox>
</div>
</div>
</div>
<!-- ── Overdraft (OD) Facility ────────────────────────────────────────── -->
<div class="sps-section">
<div class="sps-section__header">
<span class="sps-section__badge"><i class="fas fa-hand-holding-usd"></i></span>
<span>Overdraft (OD) Facility</span>
</div>
<div class="sps-body">
<div class="sps-od-layout">
<!-- Left: checkboxes -->
<div class="sps-od-checks">
<div class="sps-od-check-row">
<mat-checkbox formControlName="allowOverdraft">
<span>Is Overdraft Allowed</span>
</mat-checkbox>
</div>
@if (CurrentProductSettingsForm.value.allowOverdraft) {
<div class="sps-od-check-row">
<mat-checkbox formControlName="odAccountClosedZeroBalanceInDayClosing">
<span>OD Account Closed with Zero Balance in Day Closing</span>
</mat-checkbox>
<p class="sps-od-check-hint">
Automatically closes OD accounts when balance reaches zero during day-end closing.
</p>
</div>
}
</div>
<!-- Right: OD details panel — only visible when OD is allowed -->
@if (CurrentProductSettingsForm.value.allowOverdraft) {
<div class="sps-od-panel">
<div class="sps-od-radio-group">
<span class="sps-od-radio-label">Average Balance Calculation in Period</span>
<mat-radio-group formControlName="avgBalanceCalcPeriod" class="sps-od-radio-choices">
<mat-radio-button value="6M">6M</mat-radio-button>
<mat-radio-button value="1Y">1Y</mat-radio-button>
<mat-radio-button value="3Y">3Y</mat-radio-button>
</mat-radio-group>
</div>
<div class="sps-od-radio-group">
<span class="sps-od-radio-label">Valid for Period</span>
<mat-radio-group formControlName="validForPeriod" class="sps-od-radio-choices">
<mat-radio-button value="3M">3M</mat-radio-button>
<mat-radio-button value="1Y">1Y</mat-radio-button>
<mat-radio-button value="3Y">3Y</mat-radio-button>
</mat-radio-group>
</div>
<div class="sps-od-inputs">
<mat-form-field appearance="outline" class="sps-field">
<mat-label>OD Limit %</mat-label>
<input
type="number"
matInput
formControlName="overdraftLimit"
placeholder="e.g. 15"
min="0"
step="0.01"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="sps-field">
<mat-label>Interest on OD %</mat-label>
<input
type="number"
matInput
formControlName="nominalAnnualInterestRateOverdraft"
placeholder="e.g. 8.5"
min="0"
step="0.01"
/>
</mat-form-field>
</div>
</div>
}
</div>
</div>
</div>
</form>
@@ -0,0 +1,263 @@
/**
* 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/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.sps-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;
}
}
}
.sps-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.sps-body {
background: #fff;
padding: 18px 24px 24px;
}
.sps-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
&--compact {
width: 100%;
}
}
.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;
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;
}
}
// ── Overdraft (OD) Facility ───────────────────────────────────────────────
.sps-od-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: start;
@media (width <= 900px) {
grid-template-columns: 1fr;
}
}
.sps-od-checks {
display: flex;
flex-direction: column;
gap: 12px;
}
.sps-od-check-row {
padding: 14px 16px;
border: 1px solid $border;
border-radius: 8px;
background: $gray-50;
mat-checkbox {
font-size: 14px;
font-weight: 500;
color: $gray-900;
}
}
.sps-od-check-hint {
margin: 6px 0 0 28px;
font-size: 11.5px;
color: #64748b;
line-height: 1.5;
}
.sps-od-panel {
background: #f0f7ff;
border: 1px solid #bfdbfe;
border-radius: 10px;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.sps-od-radio-group {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
border-bottom: 1px solid #dbeafe;
&:last-of-type {
border-bottom: none;
padding-bottom: 0;
}
}
.sps-od-radio-label {
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.07em;
color: #3b82f6;
text-transform: uppercase;
}
.sps-od-radio-choices {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.sps-od-inputs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
@media (width <= 640px) {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,172 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-product-settings-step',
templateUrl: './current-product-settings-step.component.html',
styleUrls: ['./current-product-settings-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatRadioGroup,
MatRadioButton
]
})
export class CurrentProductSettingsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
@Input() CurrentProductsTemplate: any;
CurrentProductSettingsForm: UntypedFormGroup;
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.createCurrentProductSettingsForm();
this.setConditionalControls();
}
ngOnInit() {
this.periodFrequencyData = this.CurrentProductsTemplate.periodFrequencyOptions || [];
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
const isEditing = !!this.CurrentProductsTemplate.id;
this.CurrentProductSettingsForm.patchValue({
minRequiredOpeningBalance: this.CurrentProductsTemplate.minRequiredOpeningBalance,
withdrawalFeeForTransfers: this.CurrentProductsTemplate.withdrawalFeeForTransfers,
enforceMinRequiredBalance: this.CurrentProductsTemplate.enforceMinRequiredBalance,
minRequiredBalance: this.CurrentProductsTemplate.minRequiredBalance,
withHoldTax: this.CurrentProductsTemplate.withHoldTax ?? false,
balanceFrequency: isEditing ? this.CurrentProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod,
enableLockinPeriod: !!(
this.CurrentProductsTemplate.lockinPeriodFrequency && this.CurrentProductsTemplate.lockinPeriodFrequency > 0
),
accountClosedZeroBalanceInDayClosing: isEditing
? (this.CurrentProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false)
: false,
isDormancyTrackingActive: isEditing ? (this.CurrentProductsTemplate.isDormancyTrackingActive ?? true) : true,
daysToInactive: isEditing ? this.CurrentProductsTemplate.daysToInactive || 30 : 30,
daysToDormancy: isEditing ? this.CurrentProductsTemplate.daysToDormancy || 60 : 60,
daysToEscheat: isEditing ? this.CurrentProductsTemplate.daysToEscheat || 90 : 90,
// Overdraft fields
allowOverdraft: isEditing ? (this.CurrentProductsTemplate.allowOverdraft ?? false) : false,
overdraftLimit: isEditing ? this.CurrentProductsTemplate.overdraftLimit : '',
nominalAnnualInterestRateOverdraft: isEditing
? this.CurrentProductsTemplate.nominalAnnualInterestRateOverdraft
: '',
avgBalanceCalcPeriod: isEditing ? this.CurrentProductsTemplate.avgBalanceCalcPeriod || '1Y' : '1Y',
validForPeriod: isEditing ? this.CurrentProductsTemplate.validForPeriod || '1Y' : '1Y',
odAccountClosedZeroBalanceInDayClosing: isEditing
? (this.CurrentProductsTemplate.odAccountClosedZeroBalanceInDayClosing ?? false)
: false
});
if (
isEditing &&
this.CurrentProductsTemplate.lockinPeriodFrequency &&
this.CurrentProductsTemplate.lockinPeriodFrequency > 0
) {
const lockinTypeIdToKey: { [key: number]: string } = { 0: 'DAYS', 1: 'WEEKS', 2: 'MONTHS', 3: 'YEARS' };
const rawLockinType = this.CurrentProductsTemplate.lockinPeriodFrequencyType;
// Backend may return an EnumOptionData {id, code, value} or a plain integer
const lockinTypeId = typeof rawLockinType === 'object' ? rawLockinType?.id : rawLockinType;
this.CurrentProductSettingsForm.patchValue({
lockinPeriodFrequency: this.CurrentProductsTemplate.lockinPeriodFrequency,
lockinPeriodFrequencyType: lockinTypeIdToKey[lockinTypeId] || ''
});
}
}
createCurrentProductSettingsForm() {
this.CurrentProductSettingsForm = this.formBuilder.group({
minRequiredOpeningBalance: [
'',
[Validators.min(0)]
],
withdrawalFeeForTransfers: [false],
enforceMinRequiredBalance: [false],
minRequiredBalance: [
'',
[Validators.min(0)]
],
withHoldTax: [false],
balanceFrequency: ['Monthly'],
enableLockinPeriod: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
accountClosedZeroBalanceInDayClosing: [false],
// Dormancy (used internally / by accounting step)
isDormancyTrackingActive: [true],
daysToInactive: [30],
daysToDormancy: [60],
daysToEscheat: [90],
// Overdraft (OD) Facility
allowOverdraft: [false],
overdraftLimit: [
'',
[Validators.min(0)]
],
nominalAnnualInterestRateOverdraft: [
'',
[Validators.min(0)]
],
avgBalanceCalcPeriod: ['1Y'],
validForPeriod: ['1Y'],
odAccountClosedZeroBalanceInDayClosing: [false]
});
}
setConditionalControls() {
this.CurrentProductSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enabled: boolean) => {
const freq = this.CurrentProductSettingsForm.get('lockinPeriodFrequency');
const type = this.CurrentProductSettingsForm.get('lockinPeriodFrequencyType');
if (enabled) {
freq.setValidators([
Validators.required,
Validators.min(1),
Validators.pattern('^[1-9]\\d*$')
]);
type.setValidators([Validators.required]);
} else {
freq.setValue('');
type.setValue('');
freq.clearValidators();
type.clearValidators();
}
freq.updateValueAndValidity();
type.updateValueAndValidity();
});
}
get CurrentProductSettings() {
const formValue = { ...this.CurrentProductSettingsForm.value };
if (!formValue.allowOverdraft) {
delete formValue.overdraftLimit;
delete formValue.nominalAnnualInterestRateOverdraft;
delete formValue.avgBalanceCalcPeriod;
delete formValue.validForPeriod;
delete formValue.odAccountClosedZeroBalanceInDayClosing;
}
return formValue;
}
}
@@ -0,0 +1,68 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<form [formGroup]="CurrentProductTermsForm">
<div class="spt-section">
<div class="spt-section__header">
<span class="spt-section__badge"><i class="fas fa-chart-line"></i></span>
<span>Interest On OD</span>
</div>
<div class="spt-body">
<mat-form-field appearance="outline" class="spt-field" style="max-width: 320px">
<mat-label>Interest Posting Frequency *</mat-label>
<mat-select formControlName="interestPostingPeriodType" required>
@for (item of interestPostingPeriodTypeData; track item) {
<mat-option [value]="item.id">
{{ item.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
Interest Posting Frequency {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<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>
<div class="spt-note">
<span>⚠️</span>
<span>Daily product-based interest accrual enabled — interest calculated EOD on daily closing balance.</span>
</div>
</div>
</div>
</div>
</form>
@@ -0,0 +1,148 @@
/**
* 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/.
*/
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.spt-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;
}
}
}
.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;
padding: 16px 24px;
}
.spt-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.spt-grid--three {
grid-template-columns: repeat(3, 1fr);
}
.spt-field {
width: 100%;
&--full {
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;
}
@@ -0,0 +1,112 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
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({
selector: 'mifosx-current-product-terms-step',
templateUrl: './current-product-terms-step.component.html',
styleUrls: ['./current-product-terms-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent
]
})
export class CurrentProductTermsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() CurrentProductsTemplate: any;
CurrentProductTermsForm: UntypedFormGroup;
interestPostingPeriodTypeData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() {
this.createCurrentProductTermsForm();
}
ngOnInit() {
this.interestPostingPeriodTypeData = this.CurrentProductsTemplate.interestPostingPeriodTypeOptions || [];
this.setYesNoOptions(this.CurrentProductsTemplate?.yesNoOptions);
const isEditing = !!this.CurrentProductsTemplate.id;
this.CurrentProductTermsForm.patchValue({
interestPostingPeriodType: this.CurrentProductsTemplate.interestPostingPeriodType?.id || '',
autoPost: isEditing ? (this.CurrentProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? (this.CurrentProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.parseDate(this.CurrentProductsTemplate.lastInterestDate)
});
}
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) {
return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
}
if (dateValue instanceof Date) {
return dateValue;
}
return new Date(dateValue);
}
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((v: any) => !!v);
if (mappedOptions.length) {
this.yesNoOptions = mappedOptions;
}
}
createCurrentProductTermsForm() {
this.CurrentProductTermsForm = this.formBuilder.group({
interestPostingPeriodType: [
'',
Validators.required
],
autoPost: [true],
frequencyFixDay: [
30,
[Validators.min(0)]
],
lastInterestDate: ['']
});
}
get CurrentProductTerms() {
const formValue = { ...this.CurrentProductTermsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.lastInterestDate instanceof Date) {
formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || '';
}
return { ...formValue };
}
}
@@ -1,69 +1,154 @@
<div class="container"> <!--
<mat-stepper orientation="vertical" [linear]="false" class="mat-elevation-z8" labelPosition="bottom"> Copyright since 2025 Mifos Initiative
<ng-template matStepperIcon="number"><fa-icon icon="pencil-alt" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="edit"><fa-icon icon="pencil-alt" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="done"><fa-icon icon="check" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="error"><fa-icon icon="exclamation-triangle" size="lg"></fa-icon></ng-template>
<ng-template matStepperIcon="preview"><fa-icon icon="eye" size="sm"></fa-icon></ng-template>
<mat-step [stepControl]="detailsForm" completed> This Source Code Form is subject to the terms of the Mozilla Public
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template> License, v. 2.0. If a copy of the MPL was not distributed with this
<mifosx-saving-product-details-step file, You can obtain one at http://mozilla.org/MPL/2.0/.
[savingProductsTemplate]="currentProductAndTemplate" -->
></mifosx-saving-product-details-step>
</mat-step>
<mat-step [stepControl]="currencyForm" completed> <div class="cp-container">
<ng-template matStepLabel>{{ 'labels.inputs.CURRENCY' | translate }}</ng-template> <div class="cp-header">
<mifosx-saving-product-currency-step <i class="fas fa-credit-card cp-header__icon"></i>
[savingProductsTemplate]="currentProductAndTemplate" <h2 class="cp-header__title">{{ 'labels.heading.Edit Current Product' | translate }}</h2>
></mifosx-saving-product-currency-step> </div>
</mat-step>
<mat-step [stepControl]="termsForm" completed> <mat-tab-group
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template> class="cp-tabs"
<mifosx-saving-product-terms-step animationDuration="200ms"
[savingProductsTemplate]="currentProductAndTemplate" [preserveContent]="true"
></mifosx-saving-product-terms-step> [(selectedIndex)]="selectedTabIndex"
</mat-step> >
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-info-circle cp-tab-icon"></i>
{{ 'labels.inputs.DETAILS' | translate }}
</ng-template>
<div class="cp-tab-content">
<mifosx-current-product-details-step
[CurrentProductsTemplate]="currentProductAndTemplate"
></mifosx-current-product-details-step>
</div>
</mat-tab>
<mat-step [stepControl]="settingsForm" completed> <mat-tab>
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template> <ng-template mat-tab-label>
<mifosx-saving-product-settings-step <i class="fas fa-coins cp-tab-icon"></i>
[savingProductsTemplate]="currentProductAndTemplate" {{ 'labels.inputs.CURRENCY' | translate }}
></mifosx-saving-product-settings-step> </ng-template>
</mat-step> <div class="cp-tab-content">
<mifosx-current-product-currency-step
[CurrentProductsTemplate]="currentProductAndTemplate"
></mifosx-current-product-currency-step>
</div>
</mat-tab>
<mat-step completed> <mat-tab>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template> <ng-template mat-tab-label>
<mifosx-saving-product-charges-step <i class="fas fa-sliders-h cp-tab-icon"></i>
[savingProductsTemplate]="currentProductAndTemplate" {{ 'labels.inputs.TERMS' | translate }}
[currencyCode]="currencyForm.get('currencyCode')" </ng-template>
></mifosx-saving-product-charges-step> <div class="cp-tab-content">
</mat-step> <mifosx-current-product-terms-step
[CurrentProductsTemplate]="currentProductAndTemplate"
></mifosx-current-product-terms-step>
</div>
</mat-tab>
<mat-step [stepControl]="accountingForm" completed> <mat-tab>
<ng-template matStepLabel>{{ 'labels.inputs.ACCOUNTING' | translate }}</ng-template> <ng-template mat-tab-label>
<mifosx-saving-product-accounting-step <i class="fas fa-cog cp-tab-icon"></i>
[savingProductsTemplate]="currentProductAndTemplate" {{ 'labels.inputs.SETTINGS' | translate }}
[accountingRuleData]="accountingRuleData" </ng-template>
[isDormancyTrackingActive]="settingsForm.get('isDormancyTrackingActive')" <div class="cp-tab-content">
[savingProductFormValid]="formValidAndNotPristine" <mifosx-current-product-settings-step
[allowOverdraft]="settingsForm.get('allowOverdraft')" [CurrentProductsTemplate]="currentProductAndTemplate"
></mifosx-saving-product-accounting-step> ></mifosx-current-product-settings-step>
</mat-step> </div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-receipt cp-tab-icon"></i>
{{ 'labels.inputs.CHARGES' | translate }}
</ng-template>
<div class="cp-tab-content">
<mifosx-current-product-charges-step
[CurrentProductsTemplate]="currentProductAndTemplate"
[currencyCode]="currencyForm.get('currencyCode')"
></mifosx-current-product-charges-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-mobile-alt cp-tab-icon"></i>
Digital
</ng-template>
<div class="cp-tab-content">
<mifosx-current-product-limits-step
[CurrentProductsTemplate]="currentProductAndTemplate"
></mifosx-current-product-limits-step>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<i class="fas fa-book cp-tab-icon"></i>
{{ 'labels.inputs.ACCOUNTING' | translate }}
</ng-template>
<div class="cp-tab-content">
<mifosx-current-product-accounting-step
[CurrentProductsTemplate]="currentProductAndTemplate"
[accountingRuleData]="accountingRuleData"
[isDormancyTrackingActive]="settingsForm.get('isDormancyTrackingActive')"
[CurrentProductFormValid]="formValidAndNotPristine"
[allowOverdraft]="settingsForm.get('allowOverdraft')"
></mifosx-current-product-accounting-step>
</div>
</mat-tab>
@if (formValidAndNotPristine) { @if (formValidAndNotPristine) {
<mat-step state="preview" completed> <mat-tab>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template> <ng-template mat-tab-label>
<mifosx-saving-product-preview-step <i class="fas fa-eye cp-tab-icon"></i>
[savingProductsTemplate]="currentProductAndTemplate" {{ 'labels.inputs.PREVIEW' | translate }}
[accountingRuleData]="accountingRuleData" </ng-template>
[savingProduct]="currentProduct" <div class="cp-tab-content">
[taskPermission]="'UPDATE_CURRENTPRODUCT'" <mifosx-current-product-preview-step
(submitEvent)="submit()" [CurrentProductsTemplate]="currentProductAndTemplate"
></mifosx-saving-product-preview-step> [accountingRuleData]="accountingRuleData"
</mat-step> [CurrentProduct]="currentProduct"
[taskPermission]="'UPDATE_CURRENTPRODUCT'"
(submitEvent)="submit()"
></mifosx-current-product-preview-step>
</div>
</mat-tab>
} }
</mat-stepper> </mat-tab-group>
<div class="cp-action-bar">
<button mat-raised-button [routerLink]="['../../']" class="cp-action-bar__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<div class="cp-action-bar__right">
@if (selectedTabIndex > 0) {
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1">
<i class="fas fa-arrow-left" style="margin-right: 8px"></i>
{{ 'labels.buttons.Previous' | translate }}
</button>
}
@if (selectedTabIndex < totalTabs - 1) {
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
{{ 'labels.buttons.Next' | translate }}
<i class="fas fa-arrow-right" style="margin-left: 8px"></i>
</button>
}
@if (formValidAndNotPristine && selectedTabIndex === totalTabs - 1) {
<button mat-raised-button color="primary" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }}
</button>
}
</div>
</div>
</div> </div>
@@ -1,4 +1,90 @@
fa-icon { /**
position: relative; * Copyright since 2025 Mifos Initiative
left: 5%; *
* 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/.
*/
$primary: #1353a0;
.cp-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.cp-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
&__icon {
font-size: 20px;
color: $primary;
}
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.cp-tabs {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
::ng-deep {
.mat-mdc-tab-header {
border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.mat-mdc-tab {
min-width: 120px;
padding: 0 16px;
height: 52px;
}
.mat-mdc-tab-body-wrapper {
min-height: 200px;
}
.mat-mdc-tab-body {
overflow: visible !important;
}
.mat-mdc-tab-body-content {
overflow: visible !important;
}
}
}
.cp-tab-icon {
margin-right: 8px;
font-size: 14px;
}
.cp-tab-content {
padding: 24px 16px;
}
.cp-action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel {
color: rgb(0 0 0 / 60%);
}
&__right {
display: flex;
gap: 12px;
}
} }
@@ -1,17 +1,24 @@
/**
* 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, ViewChild, inject } from '@angular/core'; import { Component, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; import { CurrentProductDetailsStepComponent } from '../current-product-stepper/current-product-details-step/current-product-details-step.component';
import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; import { CurrentProductCurrencyStepComponent } from '../current-product-stepper/current-product-currency-step/current-product-currency-step.component';
import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; import { CurrentProductTermsStepComponent } from '../current-product-stepper/current-product-terms-step/current-product-terms-step.component';
import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; import { CurrentProductSettingsStepComponent } from '../current-product-stepper/current-product-settings-step/current-product-settings-step.component';
import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; import { CurrentProductChargesStepComponent } from '../current-product-stepper/current-product-charges-step/current-product-charges-step.component';
import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; import { CurrentProductLimitsStepComponent } from '../current-product-stepper/current-product-limits-step/current-product-limits-step.component';
import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; import { CurrentProductAccountingStepComponent } from '../current-product-stepper/current-product-accounting-step/current-product-accounting-step.component';
import { CurrentProductPreviewStepComponent } from '../current-product-stepper/current-product-preview-step/current-product-preview-step.component';
import { ProductsService } from 'app/products/products.service'; import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; import { MatTabsModule } from '@angular/material/tabs';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -20,18 +27,15 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./edit-current-product.component.scss'], styleUrls: ['./edit-current-product.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatStepper, MatTabsModule,
MatStepperIcon, CurrentProductDetailsStepComponent,
FaIconComponent, CurrentProductCurrencyStepComponent,
MatStep, CurrentProductTermsStepComponent,
MatStepLabel, CurrentProductSettingsStepComponent,
SavingProductDetailsStepComponent, CurrentProductChargesStepComponent,
SavingProductCurrencyStepComponent, CurrentProductLimitsStepComponent,
SavingProductTermsStepComponent, CurrentProductAccountingStepComponent,
SavingProductSettingsStepComponent, CurrentProductPreviewStepComponent
SavingProductChargesStepComponent,
SavingProductAccountingStepComponent,
SavingProductPreviewStepComponent
] ]
}) })
export class EditCurrentProductComponent { export class EditCurrentProductComponent {
@@ -41,16 +45,18 @@ export class EditCurrentProductComponent {
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private accounting = inject(Accounting); private accounting = inject(Accounting);
@ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; @ViewChild(CurrentProductDetailsStepComponent, { static: true }) detailsStep: CurrentProductDetailsStepComponent;
@ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; @ViewChild(CurrentProductCurrencyStepComponent, { static: true }) currencyStep: CurrentProductCurrencyStepComponent;
@ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; @ViewChild(CurrentProductTermsStepComponent, { static: true }) termsStep: CurrentProductTermsStepComponent;
@ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; @ViewChild(CurrentProductSettingsStepComponent, { static: true }) settingsStep: CurrentProductSettingsStepComponent;
@ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; @ViewChild(CurrentProductChargesStepComponent, { static: true }) chargesStep: CurrentProductChargesStepComponent;
@ViewChild(SavingProductAccountingStepComponent, { static: true }) @ViewChild(CurrentProductLimitsStepComponent, { static: true }) limitsStep: CurrentProductLimitsStepComponent;
accountingStep: SavingProductAccountingStepComponent; @ViewChild(CurrentProductAccountingStepComponent, { static: true })
accountingStep: CurrentProductAccountingStepComponent;
currentProductAndTemplate: any; currentProductAndTemplate: any;
accountingRuleData: string[] = []; accountingRuleData: string[] = [];
selectedTabIndex = 0;
constructor() { constructor() {
this.route.data.subscribe((data: { currentProductAndTemplate: any }) => { this.route.data.subscribe((data: { currentProductAndTemplate: any }) => {
@@ -60,19 +66,26 @@ export class EditCurrentProductComponent {
} }
get detailsForm() { get detailsForm() {
return this.detailsStep.savingProductDetailsForm; return this.detailsStep.CurrentProductDetailsForm;
} }
get currencyForm() { get currencyForm() {
return this.currencyStep.savingProductCurrencyForm; return this.currencyStep.CurrentProductCurrencyForm;
} }
get termsForm() { get termsForm() {
return this.termsStep.savingProductTermsForm; return this.termsStep.CurrentProductTermsForm;
} }
get settingsForm() { get settingsForm() {
return this.settingsStep.savingProductSettingsForm; return this.settingsStep.CurrentProductSettingsForm;
}
get limitsForm(): void {
return; // Limits step doesn't have a form, it's data-driven
} }
get accountingForm() { get accountingForm() {
return this.accountingStep.savingProductAccountingForm; return this.accountingStep.CurrentProductAccountingForm;
}
get totalTabs(): number {
return this.formValidAndNotPristine ? 8 : 7;
} }
get formValidAndNotPristine() { get formValidAndNotPristine() {
@@ -87,36 +100,64 @@ export class EditCurrentProductComponent {
!this.termsForm.pristine || !this.termsForm.pristine ||
!this.settingsForm.pristine || !this.settingsForm.pristine ||
!this.chargesStep.pristine || !this.chargesStep.pristine ||
!this.limitsStep.pristine ||
!this.accountingForm.pristine) !this.accountingForm.pristine)
); );
} }
get currentProduct() { get currentProduct() {
return { return {
...this.detailsStep.savingProductDetails, ...this.detailsStep.CurrentProductDetails,
...this.currencyStep.savingProductCurrency, ...this.currencyStep.CurrentProductCurrency,
...this.termsStep.savingProductTerms, ...this.termsStep.CurrentProductTerms,
...this.settingsStep.savingProductSettings, ...this.settingsStep.CurrentProductSettings,
...this.chargesStep.savingProductCharges, ...this.chargesStep.CurrentProductCharges,
...this.accountingStep.savingProductAccounting ...this.limitsStep.CurrentProductLimits,
...this.accountingStep.CurrentProductAccounting
}; };
} }
submit() { private prepareCurrentProductPayload() {
const product = { const currentProduct = {
...this.currentProduct, ...this.currentProduct,
charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code locale: this.settingsService.language.code,
dateFormat: 'dd MMMM yyyy'
}; };
delete product.advancedAccountingRules; delete currentProduct.advancedAccountingRules;
this.productsService.updateCurrentProduct(this.currentProductAndTemplate.id, product).subscribe((response: any) => {
this.router.navigate( if (currentProduct.lockinPeriodFrequencyType) {
[ const frequencyTypeMapping: { [key: string]: number } = {
'../../', DAYS: 0,
response.resourceId WEEKS: 1,
], MONTHS: 2,
{ relativeTo: this.route } YEARS: 3
); };
currentProduct.lockinPeriodFrequencyType = frequencyTypeMapping[currentProduct.lockinPeriodFrequencyType];
}
Object.keys(currentProduct).forEach((key) => {
if (currentProduct[key] === '' || currentProduct[key] === null || currentProduct[key] === undefined) {
delete currentProduct[key];
}
}); });
return currentProduct;
}
submit() {
const currentProduct = this.prepareCurrentProductPayload();
console.log('Current product update payload:', currentProduct);
this.productsService
.updateCurrentProduct(this.currentProductAndTemplate.id, currentProduct)
.subscribe((response: any) => {
this.router.navigate(
[
'../../',
response.resourceId
],
{ relativeTo: this.route }
);
});
} }
} }
@@ -1,3 +1,11 @@
<!--
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="layout-row align-end gap-2px responsive-column container m-b-20"> <div class="layout-row align-end gap-2px responsive-column container m-b-20">
<button <button
mat-raised-button mat-raised-button
@@ -10,93 +18,611 @@
</button> </button>
</div> </div>
<div class="tab-container"> <div class="responsive-column">
<div class="responsive-column"> <h2 class="mat-h2 flex-100">{{ currentProduct.name }}</h2>
<h2 class="mat-h2 flex-100">{{ currentProduct.name }}</h2>
<mat-divider [inset]="true"></mat-divider>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3> <mat-divider class="flex-100"></mat-divider>
<mat-divider [inset]="true"></mat-divider> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<div class="flex-fill layout-row"> <mat-divider class="flex-100"></mat-divider>
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span> <div class="flex-100 layout-row">
<span class="flex-60">{{ currentProduct.shortName }}</span> <span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
</div> <span class="flex-60">{{ currentProduct.shortName }}</span>
@if (currentProduct.description) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ currentProduct.description }}</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">{{ currentProduct.currency.name }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ currentProduct.currency.decimalPlaces }}</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">{{ currentProduct.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">{{ currentProduct.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">{{ currentProduct.interestPostingPeriodType?.value | translateKey: 'catalogs' }}</span>
</div>
@if (currentProduct.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]="currentProduct.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]="currentProduct.accountingRule"
[accountingMappings]="currentProduct.accountingMappings"
></mifosx-view-savings-accounting-details>
</div> </div>
@if (currentProduct.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ currentProduct.description }}</span>
</div>
}
@if (
currentProduct.accountType ||
currentProduct.customerSegment ||
currentProduct.productCode ||
currentProduct.effectiveDate ||
currentProduct.effectiveTo ||
currentProduct.endDate ||
currentProduct.introducerRequired !== undefined ||
currentProduct.shareLinkingEnabled !== undefined
) {
<h3 class="mat-h3 flex-100">Product Classification</h3>
<mat-divider class="flex-100"></mat-divider>
@if (currentProduct.accountType) {
<div class="flex-100 layout-row">
<span class="flex-40">Account Type:</span>
<span class="flex-60">{{ currentProduct.accountType }}</span>
</div>
}
@if (currentProduct.customerSegment) {
<div class="flex-100 layout-row">
<span class="flex-40">Customer Segment:</span>
<span class="flex-60">{{ currentProduct.customerSegment }}</span>
</div>
}
@if (currentProduct.productCode) {
<div class="flex-100 layout-row">
<span class="flex-40">Product Code:</span>
<span class="flex-60">{{ currentProduct.productCode }}</span>
</div>
}
@if (currentProduct.effectiveDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ currentProduct.effectiveDate }}</span>
</div>
}
@if (currentProduct.effectiveTo) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span>
<span class="flex-60">{{ currentProduct.effectiveTo }}</span>
</div>
}
@if (currentProduct.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">End Date:</span>
<span class="flex-60">{{ currentProduct.endDate }}</span>
</div>
}
@if (currentProduct.introducerRequired !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Introducer Required:</span>
<span class="flex-60">{{
currentProduct.introducerRequired === true || currentProduct.introducerRequired === 'Yes' ? 'Yes' : 'No'
}}</span>
</div>
}
@if (currentProduct.shareLinkingEnabled !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Share Linking Enabled:</span>
<span class="flex-60">{{ currentProduct.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">{{ currentProduct.currency.name }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ currentProduct.currency.decimalPlaces }}</span>
</div>
@if (currentProduct.currency.inMultiplesOf !== undefined && currentProduct.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">{{ currentProduct.currency.inMultiplesOf }}</span>
</div>
}
@if (currentProduct.dormancyAlert !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ currentProduct.dormancyAlert | yesNo }}</span>
</div>
}
@if (currentProduct.nominationFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Nomination Facility:</span>
<span class="flex-60">{{ currentProduct.nominationFacility | yesNo }}</span>
</div>
}
@if (currentProduct.balanceSwapOption !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Balance Swap Option:</span>
<span class="flex-60">{{ currentProduct.balanceSwapOption | yesNo }}</span>
</div>
}
@if (currentProduct.msme !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">MSME:</span>
<span class="flex-60">{{ currentProduct.msme | yesNo }}</span>
</div>
}
@if (currentProduct.autoDebitFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Auto Debit Facility:</span>
<span class="flex-60">{{ currentProduct.autoDebitFacility | yesNo }}</span>
</div>
}
<h3 class="mat-h3 flex-100">Interest On OD</h3>
<mat-divider class="flex-100"></mat-divider>
@if (currentProduct.interestPostingPeriodType?.value) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{ currentProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }}</span>
</div>
}
@if (currentProduct.interestCalculateSelector) {
<div class="flex-100 layout-row">
<span class="flex-40">Interest Calculate:</span>
<span class="flex-60">{{ currentProduct.interestCalculateSelector }}</span>
</div>
}
@if (currentProduct.averagePeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Average Period:</span>
<span class="flex-60">{{ currentProduct.averagePeriod }}</span>
</div>
}
@if (currentProduct.autoPost !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Auto Post:</span>
<span class="flex-60">{{ currentProduct.autoPost | yesNo }}</span>
</div>
}
@if (currentProduct.frequencyFixDay) {
<div class="flex-100 layout-row">
<span class="flex-40">Frequency Fix Day:</span>
<span class="flex-60">{{ currentProduct.frequencyFixDay }}</span>
</div>
}
@if (currentProduct.lastInterestDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ currentProduct.lastInterestDate }}</span>
</div>
}
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (currentProduct.minRequiredOpeningBalance) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
<span class="flex-60">{{ currentProduct.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
@if (currentProduct.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">{{
currentProduct.lockinPeriodFrequency +
' ' +
(currentProduct.lockinPeriodFrequencyType?.value || currentProduct.lockinPeriodFrequencyType?.code || '')
}}</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">{{ currentProduct.withdrawalFeeForTransfers | yesNo }}</span>
</div>
@if (currentProduct.balanceFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">Balance Frequency:</span>
<span class="flex-60">{{ currentProduct.balanceFrequency }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Enable Lock-in Period:</span>
<span class="flex-60">{{ currentProduct.enableLockinPeriod | yesNo }}</span>
</div>
@if (currentProduct.minBalanceForInterestCalculation) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
<span class="flex-60">{{ currentProduct.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">{{ currentProduct.enforceMinRequiredBalance | yesNo }}</span>
</div>
@if (currentProduct.minRequiredBalance) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ currentProduct.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">{{ currentProduct.withHoldTax | yesNo }}</span>
</div>
@if (currentProduct.withHoldTax) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Tax Group' | translate }}:</span>
<span class="flex-60">{{ currentProduct.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">{{ currentProduct.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">{{ currentProduct.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
@if (currentProduct.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">{{ currentProduct.minOverdraftForInterestCalculation | formatNumber }}</span>
</div>
}
@if (currentProduct.nominalAnnualInterestRateOverdraft) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span>
<span class="flex-60">{{ currentProduct.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
</div>
}
@if (currentProduct.overdraftLimit) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span>
<span class="flex-60">{{ currentProduct.overdraftLimit | formatNumber }}</span>
</div>
}
@if (currentProduct.allowOverdraft) {
@if (currentProduct.avgBalanceCalcPeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Average Balance Calculation Period:</span>
<span class="flex-60">{{ currentProduct.avgBalanceCalcPeriod }}</span>
</div>
}
@if (currentProduct.validForPeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Valid for Period:</span>
<span class="flex-60">{{ currentProduct.validForPeriod }}</span>
</div>
}
@if (currentProduct.odAccountClosedZeroBalanceInDayClosing !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">OD Account Closed with Zero Balance in Day Closing:</span>
<span class="flex-60">{{ currentProduct.odAccountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
}
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Enable Dormancy Tracking' | translate }}:</span>
<span class="flex-60">{{ currentProduct.isDormancyTrackingActive | yesNo }}</span>
</div>
@if (currentProduct.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">{{ currentProduct.daysToInactive }}</span>
<span class="flex-40">{{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}:</span>
<span class="flex-60">{{ currentProduct.daysToDormancy }}</span>
<span class="flex-40">{{ 'labels.inputs.Number of Days to Escheat' | translate }}:</span>
<span class="flex-60">{{ currentProduct.daysToEscheat }}</span>
</div>
}
<h3 class="mat-h3 flex-100">Account Facilities</h3>
<mat-divider class="flex-100"></mat-divider>
@if (currentProduct.accountFacilityConfig) {
<div class="layout-row-wrap responsive-column flex-100">
@for (feature of accountFacilityFeatureItems; track feature.key) {
@if (currentProduct.accountFacilityConfig[feature.key] !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ feature.label }}:</span>
<span class="flex-60">{{ currentProduct.accountFacilityConfig[feature.key] | yesNo }}</span>
</div>
}
}
</div>
}
@if (currentProduct.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]="currentProduct.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 (currentProduct.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"
>{{ currentProduct.depositLimitKyc.general.kycType }} -
{{ currentProduct.depositLimitKyc.general.limit }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Minor:</span>
<span class="flex-60"
>{{ currentProduct.depositLimitKyc.minor.kycType }} - {{ currentProduct.depositLimitKyc.minor.limit }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Small Saving:</span>
<span class="flex-60"
>{{ currentProduct.depositLimitKyc.smallSaving.kycType }} -
{{ currentProduct.depositLimitKyc.smallSaving.limit }}</span
>
</div>
</div>
}
@if (currentProduct.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]="currentProduct.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]="currentProduct.accountingRule"
[accountingMappings]="currentProduct.accountingMappings"
>
</mifosx-view-savings-accounting-details>
@if (currentProduct.otherGlMappingsMap) {
<div class="flex-100 layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-100">Other GL Mappings</h3>
<mat-divider class="flex-100"></mat-divider>
@if (currentProduct.otherGlMappingsMap.assets?.length) {
<h4 class="mat-h4 flex-100">Assets</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="currentProduct.otherGlMappingsMap.assets">
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Account Name</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glAccountId">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">{{ row.glCode || row.glAccountId }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glAccountId']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glAccountId']"></tr>
</table>
}
@if (currentProduct.otherGlMappingsMap.liabilities?.length) {
<h4 class="mat-h4 flex-100">Liabilities</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="currentProduct.otherGlMappingsMap.liabilities">
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Account Name</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glAccountId">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">{{ row.glCode || row.glAccountId }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glAccountId']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glAccountId']"></tr>
</table>
}
@if (currentProduct.otherGlMappingsMap.expenses?.length) {
<h4 class="mat-h4 flex-100">Expenses</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="currentProduct.otherGlMappingsMap.expenses">
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Account Name</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glAccountId">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">{{ row.glCode || row.glAccountId }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glAccountId']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glAccountId']"></tr>
</table>
}
@if (currentProduct.otherGlMappingsMap.income?.length) {
<h4 class="mat-h4 flex-100">Income</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="currentProduct.otherGlMappingsMap.income">
<ng-container matColumnDef="accountName">
<th mat-header-cell *matHeaderCellDef>Account Name</th>
<td mat-cell *matCellDef="let row">{{ row.accountName }}</td>
</ng-container>
<ng-container matColumnDef="glAccountId">
<th mat-header-cell *matHeaderCellDef>GL Account</th>
<td mat-cell *matCellDef="let row">{{ row.glCode || row.glAccountId }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['accountName', 'glAccountId']"></tr>
<tr mat-row *matRowDef="let row; columns: ['accountName', 'glAccountId']"></tr>
</table>
}
</div>
}
@if (
currentProduct.paymentChannelToFundSourceMappings?.length ||
currentProduct.feeToIncomeAccountMappings?.length ||
currentProduct.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 (currentProduct.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]="currentProduct.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 (currentProduct.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]="currentProduct.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 (currentProduct.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]="currentProduct.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> </div>
@@ -1,3 +1,10 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
@@ -56,6 +63,34 @@ export class CurrentProductGeneralTabComponent {
'chargeTimeType' 'chargeTimeType'
]; ];
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'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() { constructor() {
this.route.data.subscribe((data: { currentProduct: any }) => { this.route.data.subscribe((data: { currentProduct: any }) => {
this.currentProduct = data.currentProduct; this.currentProduct = data.currentProduct;
@@ -18,9 +18,10 @@ $primary: #1353a0;
.fdp-tabs { .fdp-tabs {
background: var(--md-sys-color-surface, #fff); border-radius: 8px; 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 { ::ng-deep {
.mat-mdc-tab-header { border-bottom: 1px solid rgba(0, 0, 0, 0.08); } .mat-mdc-tab-header { border-bottom: 1px solid rgb(0 0 0 / 8%); }
.mat-mdc-tab { min-width: 100px; padding: 0 12px; height: 52px; } .mat-mdc-tab { min-width: 100px; padding: 0 12px; height: 52px; }
.mat-mdc-tab-body-wrapper { min-height: 200px; } .mat-mdc-tab-body-wrapper { min-height: 200px; }
.mat-mdc-tab-body { overflow: visible !important; } .mat-mdc-tab-body { overflow: visible !important; }
@@ -33,7 +34,7 @@ $primary: #1353a0;
.fdp-action-bar { .fdp-action-bar {
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
margin-top: 20px; padding: 16px 0; border-top: 1px solid rgba(0, 0, 0, 0.08); margin-top: 20px; padding: 16px 0; border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel { color: rgba(0, 0, 0, 0.6); } &__cancel { color: rgb(0 0 0 / 60%); }
&__right { display: flex; gap: 12px; } &__right { display: flex; gap: 12px; }
} }
@@ -44,7 +44,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -62,7 +62,7 @@
gap: 24px; gap: 24px;
flex-wrap: wrap; flex-wrap: wrap;
@media (max-width: 600px) { @media (width <= 600px) {
flex-direction: column; flex-direction: column;
} }
} }
@@ -44,7 +44,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -44,7 +44,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -44,7 +44,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -51,7 +51,7 @@
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; gap: 4px 16px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -57,7 +57,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -44,7 +44,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -44,7 +44,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -152,7 +152,7 @@
padding-top: 8px; padding-top: 8px;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.cfr-container { .cfr-container {
padding: 16px 10px; padding: 16px 10px;
} }
@@ -152,7 +152,7 @@
padding-top: 8px; padding-top: 8px;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.efr-container { .efr-container {
padding: 16px 10px; padding: 16px 10px;
} }
@@ -154,7 +154,7 @@
margin-right: 10px; margin-right: 10px;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.vfr-container { .vfr-container {
padding: 16px 10px; padding: 16px 10px;
} }
@@ -35,11 +35,11 @@ $primary: #1353a0;
.lp-tabs { .lp-tabs {
background: var(--md-sys-color-surface, #fff); background: var(--md-sys-color-surface, #fff);
border-radius: 8px; 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 { ::ng-deep {
.mat-mdc-tab-header { .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 { .mat-mdc-tab {
@@ -77,10 +77,10 @@ $primary: #1353a0;
align-items: center; align-items: center;
margin-top: 20px; margin-top: 20px;
padding: 16px 0; padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08); border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel { &__cancel {
color: rgba(0, 0, 0, 0.6); color: rgb(0 0 0 / 60%);
} }
&__right { &__right {
@@ -112,7 +112,7 @@
} }
// Responsive // Responsive
@media (max-width: 600px) { @media (width <= 600px) {
.lpa-grid { .lpa-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -39,7 +39,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.lpdi-field { .lpdi-field {
@@ -57,7 +57,7 @@ $gray-900: #202124;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
} }
@media (max-width: 600px) { @media (width <= 600px) {
flex-direction: column; flex-direction: column;
&__select { max-width: 100%; width: 100%; } &__select { max-width: 100%; width: 100%; }
} }
@@ -39,7 +39,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.lpc-field { .lpc-field {
@@ -39,7 +39,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.lpd-field { .lpd-field {
@@ -37,7 +37,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.lpir-field { .lpir-field {
@@ -53,7 +53,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -80,7 +80,7 @@
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; gap: 4px 16px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -50,7 +50,7 @@
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -58,7 +58,7 @@
.lpt-grid-3 { .lpt-grid-3 {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -66,11 +66,11 @@
.lpt-grid-4 { .lpt-grid-4 {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@media (max-width: 900px) { @media (width <= 900px) {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
@media (max-width: 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -92,7 +92,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.db-field { .db-field {
@@ -92,7 +92,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.db-field { .db-field {
@@ -31,9 +31,9 @@
.cr-action-bar { .cr-action-bar {
display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding: 16px 0; display: flex; justify-content: flex-end; gap: 12px; 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%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.cr-grid { grid-template-columns: 1fr; } .cr-grid { grid-template-columns: 1fr; }
} }
@@ -31,9 +31,9 @@
.er-action-bar { .er-action-bar {
display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding: 16px 0; display: flex; justify-content: flex-end; gap: 12px; 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%);
} }
@media (max-width: 600px) { @media (width <= 600px) {
.er-grid { grid-template-columns: 1fr; } .er-grid { grid-template-columns: 1fr; }
} }
@@ -23,7 +23,7 @@
&:hover { &:hover {
border-color: #0ea5e9; border-color: #0ea5e9;
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.12); box-shadow: 0 2px 8px rgb(14 165 233 / 12%);
} }
&__icon-wrap { &__icon-wrap {
@@ -92,7 +92,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.ctc-field { .ctc-field {
@@ -92,7 +92,7 @@ $gray-900: #202124;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.etc-field { .etc-field {
@@ -81,14 +81,14 @@ $gray-900: #202124;
padding: 20px 28px 24px; padding: 20px 28px 24px;
background: #fff; background: #fff;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.vtc-cell { .vtc-cell {
padding: 14px 16px; padding: 14px 16px;
background: #f8fafc; background: #f8fafc;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid rgb(0 0 0 / 4%);
margin: 3px; margin: 3px;
transition: background 0.12s; transition: background 0.12s;
@@ -114,7 +114,7 @@ $gray-900: #202124;
} }
// Responsive // Responsive
@media (max-width: 600px) { @media (width <= 600px) {
.vtc-header { .vtc-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -62,7 +62,7 @@ $gray-900: #0f172a;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 12px; gap: 12px;
@media (max-width: 768px) { @media (width <= 768px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -82,7 +82,7 @@ $gray-900: #0f172a;
&:hover { &:hover {
border-color: #cbd5e1; border-color: #cbd5e1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 8px rgb(0 0 0 / 6%);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -102,7 +102,7 @@ $text-muted: #64748b;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.ctg-field { .ctg-field {
@@ -102,7 +102,7 @@ $text-muted: #64748b;
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.etg-field { .etg-field {
@@ -109,14 +109,14 @@ $text-muted: #64748b;
gap: 1px; gap: 1px;
padding: 20px 28px 24px; padding: 20px 28px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; } @media (width <= 600px) { grid-template-columns: 1fr; }
} }
.vtg-cell { .vtg-cell {
padding: 14px 16px; padding: 14px 16px;
background: #f8fafc; background: #f8fafc;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid rgb(0 0 0 / 4%);
margin: 3px; margin: 3px;
transition: background 0.12s; transition: background 0.12s;
@@ -195,7 +195,7 @@ $text-muted: #64748b;
} }
// Responsive // Responsive
@media (max-width: 600px) { @media (width <= 600px) {
.vtg-header { .vtg-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -81,7 +81,7 @@
padding-top: 8px; padding-top: 8px;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.cpm-container { .cpm-container {
padding: 16px 12px; padding: 16px 12px;
} }
@@ -81,7 +81,7 @@
padding-top: 8px; padding-top: 8px;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.epm-container { .epm-container {
padding: 16px 12px; padding: 16px 12px;
} }
@@ -156,7 +156,7 @@
background: transparent; background: transparent;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.vpm-container { .vpm-container {
padding: 16px 12px; padding: 16px 12px;
} }
+14 -14
View File
@@ -61,7 +61,7 @@ $gray-900: #0f172a;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 12px; gap: 12px;
@media (max-width: 768px) { @media (width <= 768px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -82,7 +82,7 @@ $gray-900: #0f172a;
&:hover { &:hover {
border-color: #cbd5e1; border-color: #cbd5e1;
background: $gray-50; background: $gray-50;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgb(0 0 0 / 4%);
.pd-card__arrow { .pd-card__arrow {
color: $gray-700; color: $gray-700;
@@ -101,62 +101,62 @@ $gray-900: #0f172a;
flex-shrink: 0; flex-shrink: 0;
&--blue { &--blue {
background: rgba(59, 130, 246, 0.1); background: rgb(59 130 246 / 10%);
color: #2563eb; color: #2563eb;
} }
&--green { &--green {
background: rgba(34, 197, 94, 0.1); background: rgb(34 197 94 / 10%);
color: #16a34a; color: #16a34a;
} }
&--cyan { &--cyan {
background: rgba(6, 182, 212, 0.1); background: rgb(6 182 212 / 10%);
color: #0891b2; color: #0891b2;
} }
&--purple { &--purple {
background: rgba(139, 92, 246, 0.1); background: rgb(139 92 246 / 10%);
color: #7c3aed; color: #7c3aed;
} }
&--amber { &--amber {
background: rgba(245, 158, 11, 0.1); background: rgb(245 158 11 / 10%);
color: #d97706; color: #d97706;
} }
&--teal { &--teal {
background: rgba(20, 184, 166, 0.1); background: rgb(20 184 166 / 10%);
color: #0d9488; color: #0d9488;
} }
&--indigo { &--indigo {
background: rgba(99, 102, 241, 0.1); background: rgb(99 102 241 / 10%);
color: #4f46e5; color: #4f46e5;
} }
&--orange { &--orange {
background: rgba(249, 115, 22, 0.1); background: rgb(249 115 22 / 10%);
color: #ea580c; color: #ea580c;
} }
&--slate { &--slate {
background: rgba(100, 116, 139, 0.1); background: rgb(100 116 139 / 10%);
color: #475569; color: #475569;
} }
&--rose { &--rose {
background: rgba(244, 63, 94, 0.1); background: rgb(244 63 94 / 10%);
color: #e11d48; color: #e11d48;
} }
&--emerald { &--emerald {
background: rgba(16, 185, 129, 0.1); background: rgb(16 185 129 / 10%);
color: #059669; color: #059669;
} }
&--red { &--red {
background: rgba(239, 68, 68, 0.1); background: rgb(239 68 68 / 10%);
color: #dc2626; color: #dc2626;
} }
} }
@@ -12,7 +12,12 @@
<h2 class="rdp-header__title">{{ 'labels.heading.Create Recurring Deposit Product' | translate }}</h2> <h2 class="rdp-header__title">{{ 'labels.heading.Create Recurring Deposit Product' | translate }}</h2>
</div> </div>
<mat-tab-group class="rdp-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex"> <mat-tab-group
class="rdp-tabs"
animationDuration="200ms"
[preserveContent]="true"
[(selectedIndex)]="selectedTabIndex"
>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<i class="fas fa-info-circle rdp-tab-icon"></i> <i class="fas fa-info-circle rdp-tab-icon"></i>
@@ -126,19 +131,19 @@
<div class="rdp-action-bar__right"> <div class="rdp-action-bar__right">
@if (selectedTabIndex > 0) { @if (selectedTabIndex > 0) {
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1"> <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 }} {{ 'labels.buttons.Previous' | translate }}
</button> </button>
} }
@if (selectedTabIndex < totalTabs - 1) { @if (selectedTabIndex < totalTabs - 1) {
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1"> <button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
{{ 'labels.buttons.Next' | translate }} {{ '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> </button>
} }
@if (recurringDepositProductFormValid && selectedTabIndex === totalTabs - 1) { @if (recurringDepositProductFormValid && selectedTabIndex === totalTabs - 1) {
<button mat-raised-button color="primary" (click)="submit()"> <button mat-raised-button color="primary" (click)="submit()">
<i class="fas fa-check" style="margin-right: 8px;"></i> <i class="fas fa-check" style="margin-right: 8px"></i>
{{ 'labels.buttons.Submit' | translate }} {{ 'labels.buttons.Submit' | translate }}
</button> </button>
} }
@@ -8,32 +8,83 @@
$primary: #1353a0; $primary: #1353a0;
.rdp-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } .rdp-container {
max-width: 960px;
.rdp-header { margin: 0 auto;
display: flex; align-items: center; gap: 10px; margin-bottom: 20px; padding: 24px 16px;
&__icon { font-size: 20px; color: $primary; }
&__title { margin: 0; font-size: 22px; font-weight: 500; }
} }
.rdp-tabs { .rdp-header {
background: var(--md-sys-color-surface, #fff); border-radius: 8px; display: flex;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); align-items: center;
::ng-deep { gap: 10px;
.mat-mdc-tab-header { border-bottom: 1px solid rgba(0, 0, 0, 0.08); } margin-bottom: 20px;
.mat-mdc-tab { min-width: 100px; padding: 0 12px; height: 52px; }
.mat-mdc-tab-body-wrapper { min-height: 200px; } &__icon {
.mat-mdc-tab-body { overflow: visible !important; } font-size: 20px;
.mat-mdc-tab-body-content { overflow: visible !important; } color: $primary;
}
&__title {
margin: 0;
font-size: 22px;
font-weight: 500;
} }
} }
.rdp-tab-icon { margin-right: 8px; font-size: 14px; } .rdp-tabs {
.rdp-tab-content { padding: 24px 16px; } background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
::ng-deep {
.mat-mdc-tab-header {
border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.mat-mdc-tab {
min-width: 100px;
padding: 0 12px;
height: 52px;
}
.mat-mdc-tab-body-wrapper {
min-height: 200px;
}
.mat-mdc-tab-body {
overflow: visible !important;
}
.mat-mdc-tab-body-content {
overflow: visible !important;
}
}
}
.rdp-tab-icon {
margin-right: 8px;
font-size: 14px;
}
.rdp-tab-content {
padding: 24px 16px;
}
.rdp-action-bar { .rdp-action-bar {
display: flex; justify-content: space-between; align-items: center; display: flex;
margin-top: 20px; padding: 16px 0; border-top: 1px solid rgba(0, 0, 0, 0.08); justify-content: space-between;
&__cancel { color: rgba(0, 0, 0, 0.6); } align-items: center;
&__right { display: flex; gap: 12px; } margin-top: 20px;
padding: 16px 0;
border-top: 1px solid rgb(0 0 0 / 8%);
&__cancel {
color: rgb(0 0 0 / 60%);
}
&__right {
display: flex;
gap: 12px;
}
} }
@@ -7,273 +7,113 @@
--> -->
<form [formGroup]="recurringDepositProductAccountingForm"> <form [formGroup]="recurringDepositProductAccountingForm">
<!-- Accounting Rule -->
<div class="rdpa-section"> <div class="rdpa-section">
<div class="rdpa-section__header"> <div class="rdpa-section__header">
<i class="fa fa-balance-scale"></i> <span class="rdpa-section__icon">
<h3>{{ 'labels.heading.Accounting Rule' | translate }}</h3> <i class="fas fa-book"></i>
</span>
<h3>Accounting</h3>
</div> </div>
<div class="rdpa-section__body"> <div class="rdpa-section__body">
<mat-radio-group class="rdpa-radio-group" formControlName="accountingRule"> <div class="rdpa-group">
@for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { <div class="rdpa-group__header">
<mat-radio-button [value]="i + 1"> <h4>Accounting Rule</h4>
{{ 'labels.accounting.' + accountingRule | translate }} </div>
</mat-radio-button> <div class="rdpa-radio-group">
} <mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
</mat-radio-group> </div>
</div>
</div>
@if (isCashOrAccrualAccounting()) {
<!-- Assets -->
<div class="rdpa-section">
<div class="rdpa-section__header">
<i class="fa fa-building"></i>
<h3>{{ 'labels.heading.Assets' | translate }}</h3>
</div>
<div class="rdpa-section__body rdpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.savingsReferenceAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Saving reference'"
></mifosx-gl-account-selector>
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.feesReceivableAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Fees Receivable'"
></mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.penaltiesReceivableAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Penalties Receivable'"
></mifosx-gl-account-selector>
}
</div>
</div>
<!-- Liabilities -->
<div class="rdpa-section">
<div class="rdpa-section__header">
<i class="fa fa-hand-holding-usd"></i>
<h3>{{ 'labels.heading.Liabilities' | translate }}</h3>
</div>
<div class="rdpa-section__body rdpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.savingsControlAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Saving control'"
></mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.transfersInSuspenseAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Savings transfers in suspense'"
></mifosx-gl-account-selector>
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.interestPayableAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Interest Payable'"
></mifosx-gl-account-selector>
}
</div>
</div>
<!-- Expenses -->
<div class="rdpa-section">
<div class="rdpa-section__header">
<i class="fa fa-money-bill-wave"></i>
<h3>{{ 'labels.heading.Expenses' | translate }}</h3>
</div>
<div class="rdpa-section__body rdpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.interestOnSavingsAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'Interest on savings'"
></mifosx-gl-account-selector>
</div>
</div>
<!-- Income -->
<div class="rdpa-section">
<div class="rdpa-section__header">
<i class="fa fa-chart-pie"></i>
<h3>{{ 'labels.heading.Income' | translate }}</h3>
</div>
<div class="rdpa-section__body rdpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.incomeFromFeeAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from fees'"
></mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="recurringDepositProductAccountingForm.controls.incomeFromPenaltyAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from penalties'"
></mifosx-gl-account-selector>
</div>
</div>
<!-- Advanced Accounting Rules -->
<div class="rdpa-section">
<div class="rdpa-section__header">
<i class="fa fa-cogs"></i>
<h3>{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
<mat-checkbox formControlName="advancedAccountingRules" class="rdpa-header-checkbox"></mat-checkbox>
</div> </div>
@if (recurringDepositProductAccountingForm.value.advancedAccountingRules) { @if (isCashOrAccrualAccounting()) {
<div class="rdpa-section__body"> @for (section of accountingSectionConfigs; track section.key) {
<div class="rdpa-group">
<!-- Payment Fund Sources --> <div class="rdpa-group__header">
<div class="rdpa-subsection"> <h4>{{ section.title }}</h4>
<div class="rdpa-subsection__header"> <button type="button" class="rdpa-add-btn" (click)="addOtherGlMapping(section.otherMappingsControlName)">
<h4>{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}</h4> + Add Other
<button type="button" mat-raised-button color="primary" class="rdpa-add-btn"
(click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button> </button>
</div> </div>
@if (paymentChannelToFundSourceMappings.value.length !== 0) { <div class="rdpa-list">
<div class="rdpa-table-wrapper"> @for (row of section.rows; track row.key) {
<table mat-table [dataSource]="paymentChannelToFundSourceMappings.value"> @if (!row.accrualOnly || isAccrualAccounting()) {
<ng-container matColumnDef="paymentTypeId"> <div class="rdpa-row">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th> <div class="rdpa-row__meta">
<td mat-cell *matCellDef="let paymentFundSource"> <span
{{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }} class="rdpa-pill"
</td> [class.rdpa-pill--green]="section.pillClass === 'green'"
</ng-container> [class.rdpa-pill--amber]="section.pillClass === 'amber'"
<ng-container matColumnDef="fundSourceAccountId"> [class.rdpa-pill--blue]="section.pillClass === 'blue'"
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th> >
<td mat-cell *matCellDef="let paymentFundSource"> {{ section.pillLabel }}
{{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }} </span>
</td> <span class="rdpa-row__label">{{ row.label }}</span>
</ng-container> </div>
<ng-container matColumnDef="actions"> <mifosx-gl-account-selector
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th> class="rdpa-row__input"
<td mat-cell *matCellDef="let paymentFundSource; let i = index"> [inputFormControl]="recurringDepositProductAccountingForm.get(row.controlName)"
<button mat-icon-button color="primary" [glAccountList]="row.options"
(click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)"> [required]="row.required"
<fa-icon icon="edit"></fa-icon> [inputLabel]="row.inputLabel"
</button> ></mifosx-gl-account-selector>
<button mat-icon-button color="warn" (click)="delete(paymentChannelToFundSourceMappings, i)"> </div>
<fa-icon icon="trash"></fa-icon> }
</button> }
</td> </div>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr> <div class="rdpa-list" [formArrayName]="section.otherMappingsControlName">
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr> @for (
</table> otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
</div> track $index;
} let i = $index
) {
<div class="rdpa-other-row-wrap">
<div class="rdpa-row rdpa-row--other" [formGroupName]="i">
<div class="rdpa-row__meta rdpa-row__meta--other">
<span
class="rdpa-pill"
[class.rdpa-pill--green]="section.pillClass === 'green'"
[class.rdpa-pill--amber]="section.pillClass === 'amber'"
[class.rdpa-pill--blue]="section.pillClass === 'blue'"
>
{{ section.pillLabel }}
</span>
<input
class="rdpa-row__name rdpa-row__name--other-account"
formControlName="accountName"
placeholder="Enter account name..."
type="text"
/>
</div>
<div class="rdpa-row__actions rdpa-row__actions--other">
<mat-form-field class="rdpa-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="rdpa-remove-btn rdpa-remove-btn--side"
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
>
<i class="fas fa-times"></i>
</button>
</div>
}
</div>
</div> </div>
}
<!-- Fees to Income -->
@if (existCharges()) {
<div class="rdpa-subsection">
<div class="rdpa-subsection__header">
<h4>{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}</h4>
<button type="button" mat-raised-button color="primary" class="rdpa-add-btn"
(click)="add('FeesIncome', feeToIncomeAccountMappings)">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (feeToIncomeAccountMappings.value.length !== 0) {
<div class="rdpa-table-wrapper">
<table mat-table [dataSource]="feeToIncomeAccountMappings.value">
<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 -->
@if (existCharges()) {
<div class="rdpa-subsection">
<div class="rdpa-subsection__header">
<h4>{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}</h4>
<button type="button" mat-raised-button color="primary" class="rdpa-add-btn"
(click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (penaltyToIncomeAccountMappings.value.length !== 0) {
<div class="rdpa-table-wrapper">
<table mat-table [dataSource]="penaltyToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.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>
} </div>
</form> </form>
@@ -6,33 +6,256 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-700: #334155;
$gray-900: #202124;
.rdpa-section { .rdpa-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
}
& + & { margin-top: 16px; } .rdpa-section__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
.rdpa-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;
}
.rdpa-section__body {
padding: 16px 24px 20px;
}
.rdpa-group {
& + & {
margin-top: 20px;
}
&__header { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; justify-content: space-between;
padding: 12px 24px; gap: 16px;
background: #f8f9fa; margin: 0 0 12px;
border-bottom: 1px solid #e8eaed;
i { font-size: 14px; color: #0ea5e9; }
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; }
} }
&__body { padding: 16px 24px; } h4 {
margin: 0 0 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #94a3b8;
}
} }
.rdpa-grid { .rdpa-add-btn {
border: 0;
background: transparent;
color: #2563eb;
font-size: 12px;
font-weight: 700;
padding: 0;
cursor: pointer;
}
.rdpa-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.rdpa-list + .rdpa-list {
margin-top: 12px;
}
.rdpa-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 12px;
border: 1px solid $border;
border-radius: 8px;
background: #fff;
@media (width <= 900px) {
flex-direction: column;
align-items: stretch;
}
}
.rdpa-row:has(mifosx-gl-account-selector) {
align-items: stretch;
}
.rdpa-row--other {
border-style: dashed;
border-color: #cbd5e1;
background: #f8fafc;
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
gap: 4px 16px; align-items: center;
padding: 12px 16px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.rdpa-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;
}
}
.rdpa-row__meta {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1 1 auto;
}
.rdpa-row__meta--other {
min-width: 0;
flex: 1 1 auto;
align-self: stretch;
}
.rdpa-row__label {
font-size: 14px;
font-weight: 600;
color: $gray-700;
}
.rdpa-row__input {
flex: 0 0 22rem;
max-width: 22rem;
min-width: 18rem;
@media (width <= 900px) {
width: 100%;
max-width: 100%;
min-width: 0;
}
}
.rdpa-row__input :is(mifosx-gl-account-selector) {
width: 100%;
display: block;
}
.rdpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) {
width: 100%;
margin-right: 0 !important;
}
.rdpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) {
width: 100%;
}
.rdpa-row__actions {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.rdpa-row__actions--other {
justify-content: flex-end;
align-self: stretch;
width: 100%;
@media (width <= 1100px) {
justify-content: flex-start;
}
}
.rdpa-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%;
}
}
.rdpa-row__name--other-account {
max-width: 100%;
flex: 1 1 auto;
height: 48px;
}
.rdpa-row__other-field {
width: 100%;
min-width: 0;
}
.rdpa-row__other-field :is(mat-form-field) {
width: 100%;
}
.rdpa-row__other-field :is(.mat-mdc-form-field-required-marker) {
display: none;
}
.rdpa-remove-btn {
border: 0;
background: transparent;
color: #ef4444;
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
.rdpa-remove-btn--side {
place-self: center end;
margin-right: 4px;
@media (width <= 1100px) {
justify-self: start;
margin-right: 0;
}
} }
.rdpa-radio-group { .rdpa-radio-group {
@@ -41,39 +264,41 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.rdpa-header-checkbox { margin-left: auto; } .rdpa-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;
.rdpa-subsection { &--green {
& + & { margin-top: 16px; } background: #dcfce7;
color: #16a34a;
}
&__header { &--amber {
display: flex; background: #fef3c7;
align-items: center; color: #d97706;
justify-content: space-between; }
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
h4 { margin: 0; font-size: 13px; font-weight: 600; color: #202124; } &--blue {
background: #dbeafe;
color: #2563eb;
} }
} }
.rdpa-add-btn { @media (width <= 900px) {
height: 36px; .rdpa-row__meta {
border-radius: 8px; margin-bottom: 4px;
font-size: 13px; }
font-weight: 600;
}
.rdpa-table-wrapper { .rdpa-row__input {
border: 1px solid #e2e8f0; flex: 1 1 auto;
border-radius: 8px; max-width: 100%;
overflow: hidden; }
table { width: 100%; }
}
@media (max-width: 600px) {
.rdpa-grid { grid-template-columns: 1fr; }
.rdpa-radio-group { flex-direction: column; }
} }
@@ -6,441 +6,393 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Component, OnInit, Input, inject } from '@angular/core'; import { Component, Input, OnInit, inject } from '@angular/core';
import { import {
UntypedFormGroup, FormsModule,
UntypedFormBuilder, ReactiveFormsModule,
UntypedFormArray, UntypedFormArray,
Validators, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
ReactiveFormsModule UntypedFormGroup,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component';
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 { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { TranslateService } from '@ngx-translate/core'; import { GLAccount } from 'app/shared/models/general.model';
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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income';
type AccountingRowConfig = {
key: string;
label: string;
inputLabel: string;
controlName: string;
options: GLAccount[];
required?: boolean;
accrualOnly?: boolean;
};
type AccountingSectionConfig = {
key: AccountingSectionKey;
title: string;
pillLabel: string;
pillClass: 'green' | 'amber' | 'blue';
rows: AccountingRowConfig[];
otherMappingsControlName: string;
otherAccountOptions: GLAccount[];
};
type OtherGlMappingValue = {
accountName: string;
glAccountId: number | null;
};
@Component({ @Component({
selector: 'mifosx-recurring-deposit-product-accounting-step', selector: 'mifosx-recurring-deposit-product-accounting-step',
templateUrl: './recurring-deposit-product-accounting-step.component.html', templateUrl: './recurring-deposit-product-accounting-step.component.html',
styleUrls: ['./recurring-deposit-product-accounting-step.component.scss'], styleUrls: ['./recurring-deposit-product-accounting-step.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
FormsModule,
ReactiveFormsModule,
MatRadioGroup, MatRadioGroup,
MatRadioButton, MatRadioButton,
GlAccountSelectorComponent, GlAccountSelectorComponent
MatCheckbox,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FindPipe
] ]
}) })
export class RecurringDepositProductAccountingStepComponent implements OnInit { export class RecurringDepositProductAccountingStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private dialog = inject(MatDialog);
private accounting = inject(Accounting); private accounting = inject(Accounting);
private translateService = inject(TranslateService);
@Input() recurringDepositProductsTemplate: any; @Input() recurringDepositProductsTemplate: any;
@Input() accountingRuleData: any; @Input() accountingRuleData: any;
@Input() recurringDepositProductFormValid: boolean; @Input() recurringDepositProductFormValid: boolean;
recurringDepositProductAccountingForm: UntypedFormGroup; recurringDepositProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = [];
chargeData: any; liabilityAccountOptions: GLAccount[] = [];
penaltyData: any; expenseAccountOptions: GLAccount[] = [];
paymentTypeData: any; incomeAccountOptions: GLAccount[] = [];
assetAccountData: any; accountingSectionConfigs: AccountingSectionConfig[] = [];
incomeAccountData: any;
expenseAccountData: any;
liabilityAccountData: any;
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId',
'actions'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'incomeAccountId',
'actions'
];
constructor() { constructor() {
this.createrecurringDepositProductAccountingForm(); this.createRecurringDepositProductAccountingForm();
this.setConditionalControls(); this.setConditionalControls();
} }
ngOnInit() { ngOnInit() {
this.chargeData = this.recurringDepositProductsTemplate.chargeOptions || []; const accountingMappings = this.recurringDepositProductsTemplate?.accountingMappings || {};
this.penaltyData = this.recurringDepositProductsTemplate.penaltyOptions || []; const mappings = {
this.paymentTypeData = this.recurringDepositProductsTemplate.paymentTypeOptions || []; ...accountingMappings,
this.assetAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; otherGlMappingsMap: this.recurringDepositProductsTemplate?.otherGlMappingsMap
this.incomeAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; };
this.expenseAccountData = const accountingMappingOptions = this.recurringDepositProductsTemplate?.accountingMappingOptions || {};
this.recurringDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; const accountingRuleId = 2; // Always Cash
this.liabilityAccountData =
this.recurringDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
this.assignAccountingStepData(); this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
} this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
this.buildAccountingSectionConfigs();
this.recurringDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId });
this.syncAccountingControls(accountingRuleId);
this.patchAccountingValues(mappings);
} }
assignAccountingStepData() { createRecurringDepositProductAccountingForm() {
this.recurringDepositProductAccountingForm.patchValue({
accountingRule: this.recurringDepositProductsTemplate.accountingRule.id
});
if (this.isCashOrAccrualAccounting()) {
this.recurringDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: this.recurringDepositProductsTemplate.accountingMappings.savingsReferenceAccount.id,
savingsControlAccountId: this.recurringDepositProductsTemplate.accountingMappings.savingsControlAccount.id,
transfersInSuspenseAccountId:
this.recurringDepositProductsTemplate.accountingMappings.transfersInSuspenseAccount.id,
incomeFromFeeAccountId: this.recurringDepositProductsTemplate.accountingMappings.incomeFromFeeAccount.id,
incomeFromPenaltyAccountId:
this.recurringDepositProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id,
interestOnSavingsAccountId: this.recurringDepositProductsTemplate.accountingMappings.interestOnSavingsAccount.id
});
if (this.isAccrualAccounting()) {
this.recurringDepositProductAccountingForm.patchValue({
feesReceivableAccountId: this.recurringDepositProductsTemplate.accountingMappings.feeReceivableAccount.id,
penaltiesReceivableAccountId:
this.recurringDepositProductsTemplate.accountingMappings.penaltyReceivableAccount.id,
interestPayableAccountId: this.recurringDepositProductsTemplate.accountingMappings.interestPayableAccount.id
});
}
if (
this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings ||
this.recurringDepositProductsTemplate.feeToIncomeAccountMappings ||
this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings
) {
this.recurringDepositProductAccountingForm.patchValue({
advancedAccountingRules: true
});
}
if (this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings) {
this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings.forEach(
(paymentChannelToFundSourceMapping: any) => {
const paymentChannelToFundSourceMappingData = this.formBuilder.group({
paymentTypeId: [
paymentChannelToFundSourceMapping.paymentType.id,
Validators.required
],
fundSourceAccountId: [
paymentChannelToFundSourceMapping.fundSourceAccount.id,
Validators.required
]
});
const formArray = this.recurringDepositProductAccountingForm.controls[
'paymentChannelToFundSourceMappings'
] as UntypedFormArray;
formArray.push(paymentChannelToFundSourceMappingData);
}
);
}
if (this.recurringDepositProductsTemplate.feeToIncomeAccountMappings) {
this.recurringDepositProductsTemplate.feeToIncomeAccountMappings.forEach((feeToIncomeAccountMapping: any) => {
const feeToIncomeAccountMappingData = this.formBuilder.group({
chargeId: [
feeToIncomeAccountMapping.charge.id,
Validators.required
],
incomeAccountId: [
feeToIncomeAccountMapping.incomeAccount.id,
Validators.required
]
});
const formArray = this.recurringDepositProductAccountingForm.controls[
'feeToIncomeAccountMappings'
] as UntypedFormArray;
formArray.push(feeToIncomeAccountMappingData);
});
}
if (this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings) {
this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings.forEach(
(penaltyToIncomeAccountMapping: any) => {
const penaltyToIncomeAccountMappingData = this.formBuilder.group({
chargeId: [
penaltyToIncomeAccountMapping.charge.id,
Validators.required
],
incomeAccountId: [
penaltyToIncomeAccountMapping.incomeAccount.id,
Validators.required
]
});
const formArray = this.recurringDepositProductAccountingForm.controls[
'penaltyToIncomeAccountMappings'
] as UntypedFormArray;
formArray.push(penaltyToIncomeAccountMappingData);
}
);
}
}
}
createrecurringDepositProductAccountingForm() {
this.recurringDepositProductAccountingForm = this.formBuilder.group({ this.recurringDepositProductAccountingForm = this.formBuilder.group({
accountingRule: [1] accountingRule: [2],
assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]),
expensesOtherGlMappings: this.formBuilder.array([]),
incomeOtherGlMappings: this.formBuilder.array([])
}); });
} }
existCharges(): boolean { private setConditionalControls() {
return this.chargeData.length > 0; this.recurringDepositProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => {
} this.syncAccountingControls(rule);
setConditionalControls() {
this.recurringDepositProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => {
if (accountingRule === 2 || accountingRule === 3) {
this.recurringDepositProductAccountingForm.addControl(
'savingsReferenceAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'savingsControlAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'transfersInSuspenseAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'interestOnSavingsAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'incomeFromFeeAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'incomeFromPenaltyAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false));
if (accountingRule === 3) {
this.recurringDepositProductAccountingForm.addControl(
'feesReceivableAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'penaltiesReceivableAccountId',
new UntypedFormControl('', Validators.required)
);
this.recurringDepositProductAccountingForm.addControl(
'interestPayableAccountId',
new UntypedFormControl('', Validators.required)
);
}
this.recurringDepositProductAccountingForm
.get('advancedAccountingRules')
.valueChanges.subscribe((advancedAccountingRules: boolean) => {
if (advancedAccountingRules) {
this.recurringDepositProductAccountingForm.addControl(
'paymentChannelToFundSourceMappings',
this.formBuilder.array([])
);
this.recurringDepositProductAccountingForm.addControl(
'feeToIncomeAccountMappings',
this.formBuilder.array([])
);
this.recurringDepositProductAccountingForm.addControl(
'penaltyToIncomeAccountMappings',
this.formBuilder.array([])
);
} else {
this.recurringDepositProductAccountingForm.removeControl('paymentChannelToFundSourceMappings');
this.recurringDepositProductAccountingForm.removeControl('feeToIncomeAccountMappings');
this.recurringDepositProductAccountingForm.removeControl('penaltyToIncomeAccountMappings');
}
});
} else {
this.recurringDepositProductAccountingForm.removeControl('savingsReferenceAccountId');
this.recurringDepositProductAccountingForm.removeControl('overdraftPortfolioControlId');
this.recurringDepositProductAccountingForm.removeControl('savingsControlAccountId');
this.recurringDepositProductAccountingForm.removeControl('transfersInSuspenseAccountId');
this.recurringDepositProductAccountingForm.removeControl('interestOnSavingsAccountId');
this.recurringDepositProductAccountingForm.removeControl('writeOffAccountId');
this.recurringDepositProductAccountingForm.removeControl('incomeFromFeeAccountId');
this.recurringDepositProductAccountingForm.removeControl('incomeFromPenaltyAccountId');
this.recurringDepositProductAccountingForm.removeControl('incomeFromInterestId');
this.recurringDepositProductAccountingForm.removeControl('advancedAccountingRules');
this.recurringDepositProductAccountingForm.removeControl('escheatLiabilityId');
this.recurringDepositProductAccountingForm.removeControl('feesReceivableAccountId');
this.recurringDepositProductAccountingForm.removeControl('penaltiesReceivableAccountId');
this.recurringDepositProductAccountingForm.removeControl('interestPayableAccountId');
}
}); });
} }
get paymentChannelToFundSourceMappings(): UntypedFormArray { private patchAccountingValues(mappings: any) {
return this.recurringDepositProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; this.recurringDepositProductAccountingForm.patchValue({
} savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
get feeToIncomeAccountMappings(): UntypedFormArray { transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
return this.recurringDepositProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
} incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
get penaltyToIncomeAccountMappings(): UntypedFormArray { feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
return this.recurringDepositProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
} interestPayableAccountId: mappings.interestPayableAccount?.id || ''
add(formType: string, formArray: UntypedFormArray) {
const data = { ...this.getData(formType), pristine: false };
const dialogRef = this.dialog.open(FormDialogComponent, { data });
dialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
formArray.push(response.data);
}
}); });
}
edit(formType: string, formArray: UntypedFormArray, index: number) { if (this.isCashOrAccrualAccounting()) {
const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings;
const dialogRef = this.dialog.open(FormDialogComponent, { data }); this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
dialogRef.afterClosed().subscribe((response: any) => { this.setOtherGlMappings(
if (response.data) { 'liabilitiesOtherGlMappings',
formArray.at(index).patchValue(response.data.value); glMappings?.liabilities || [],
} this.liabilityAccountOptions
}); );
} this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
delete(formArray: UntypedFormArray, index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
formArray.removeAt(index);
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case '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) { private createOtherGlMappingGroup(otherGlMapping: Partial<OtherGlMappingValue> = {}) {
const formfields: FormfieldBase[] = [ return this.formBuilder.group({
new SelectBase({ accountName: [
controlName: 'paymentTypeId', otherGlMapping.accountName || '',
label: this.translateService.instant('labels.inputs.Payment Type'), Validators.required
value: values ? values.paymentTypeId : this.paymentTypeData[0].id, ],
options: { label: 'name', value: 'id', data: this.paymentTypeData }, glAccountId: [
required: true, otherGlMapping.glAccountId || '',
order: 1 Validators.required
}), ]
new SelectBase({ });
controlName: 'fundSourceAccountId',
label: this.translateService.instant('labels.inputs.Fund Source'),
value: values ? values.fundSourceAccountId : this.assetAccountData[0].id,
options: { label: 'name', value: 'id', data: this.assetAccountData },
required: true,
order: 2
})
];
return formfields;
} }
getFeesIncomeFormfields(values?: any) { getOtherGlMappingsArray(controlName: string): UntypedFormArray {
const formfields: FormfieldBase[] = [ return this.recurringDepositProductAccountingForm.get(controlName) as UntypedFormArray;
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) { private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) {
const formfields: FormfieldBase[] = [ const arr = this.getOtherGlMappingsArray(controlName);
new SelectBase({ arr.clear();
controlName: 'chargeId', (otherGlMappings || []).forEach((m) => {
label: 'Penalty', arr.push(
value: values ? values.chargeId : this.penaltyData[0].id, this.createOtherGlMappingGroup({
options: { label: 'name', value: 'id', data: this.penaltyData }, accountName: m.accountName,
required: true, glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null
order: 1 })
}), );
new SelectBase({ });
controlName: 'incomeAccountId', }
label: 'Income Account',
value: values ? values.incomeAccountId : this.incomeAccountData[0].id, addOtherGlMapping(controlName: string) {
options: { label: 'name', value: 'id', data: this.incomeAccountData }, this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
required: true, }
order: 2
}) removeOtherGlMapping(controlName: string, index: number) {
this.getOtherGlMappingsArray(controlName).removeAt(index);
}
getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl {
return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl;
}
private clearOtherGlMappings() {
[
'assetsOtherGlMappings',
'liabilitiesOtherGlMappings',
'expensesOtherGlMappings',
'incomeOtherGlMappings'
].forEach((c) => this.getOtherGlMappingsArray(c).clear());
}
private getOtherGlMappings() {
const map = (controlName: string, options: GLAccount[]) =>
this.getOtherGlMappingsArray(controlName)
.value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId)
.map((item: OtherGlMappingValue) => ({
accountName: item.accountName,
glCode: options.find((o) => o.id === item.glAccountId)?.glCode || ''
}))
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
const otherGlMappings = {
assets: map('assetsOtherGlMappings', this.assetAccountOptions),
liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
expenses: map('expensesOtherGlMappings', this.expenseAccountOptions),
income: map('incomeOtherGlMappings', this.incomeAccountOptions)
};
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
}
private syncAccountingControls(accountingRule: number) {
const addControl = (name: string, required = false) => {
if (!this.recurringDepositProductAccountingForm.contains(name)) {
this.recurringDepositProductAccountingForm.addControl(
name,
new UntypedFormControl('', required ? Validators.required : undefined)
);
} else if (required) {
this.recurringDepositProductAccountingForm.get(name)?.setValidators(Validators.required);
this.recurringDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false });
}
};
const removeControl = (name: string) => {
if (this.recurringDepositProductAccountingForm.contains(name)) {
this.recurringDepositProductAccountingForm.removeControl(name);
}
};
if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) {
addControl('savingsReferenceAccountId', true);
addControl('savingsControlAccountId', true);
addControl('transfersInSuspenseAccountId', true);
addControl('interestOnSavingsAccountId', true);
addControl('incomeFromFeeAccountId', true);
addControl('incomeFromPenaltyAccountId', true);
if (this.accounting.isAccrualAccountingRuleId(accountingRule)) {
addControl('feesReceivableAccountId', true);
addControl('penaltiesReceivableAccountId', true);
addControl('interestPayableAccountId', true);
} else {
removeControl('feesReceivableAccountId');
removeControl('penaltiesReceivableAccountId');
removeControl('interestPayableAccountId');
}
} else {
[
'savingsReferenceAccountId',
'savingsControlAccountId',
'transfersInSuspenseAccountId',
'interestOnSavingsAccountId',
'incomeFromFeeAccountId',
'incomeFromPenaltyAccountId',
'feesReceivableAccountId',
'penaltiesReceivableAccountId',
'interestPayableAccountId'
].forEach(removeControl);
this.clearOtherGlMappings();
}
}
buildAccountingSectionConfigs() {
this.accountingSectionConfigs = [
{
key: 'assets',
title: 'Assets',
pillLabel: 'Asset',
pillClass: 'green',
rows: [
{
key: 'savingsReferenceAccountId',
label: 'Savings Reference Account',
inputLabel: 'Savings reference account',
controlName: 'savingsReferenceAccountId',
options: this.assetAccountOptions,
required: true
},
{
key: 'feesReceivableAccountId',
label: 'Fees Receivable Account',
inputLabel: 'Fees receivable account',
controlName: 'feesReceivableAccountId',
options: this.assetAccountOptions,
accrualOnly: true
},
{
key: 'penaltiesReceivableAccountId',
label: 'Penalties Receivable Account',
inputLabel: 'Penalties receivable account',
controlName: 'penaltiesReceivableAccountId',
options: this.assetAccountOptions,
accrualOnly: true
}
],
otherMappingsControlName: 'assetsOtherGlMappings',
otherAccountOptions: this.assetAccountOptions
},
{
key: 'liabilities',
title: 'Liabilities',
pillLabel: 'Liability',
pillClass: 'green',
rows: [
{
key: 'savingsControlAccountId',
label: 'Savings Control Account',
inputLabel: 'Savings control account',
controlName: 'savingsControlAccountId',
options: this.liabilityAccountOptions,
required: true
},
{
key: 'transfersInSuspenseAccountId',
label: 'Transfers In Suspense Account',
inputLabel: 'Transfers in suspense account',
controlName: 'transfersInSuspenseAccountId',
options: this.liabilityAccountOptions,
required: true
},
{
key: 'interestPayableAccountId',
label: 'Interest Payable Account',
inputLabel: 'Interest payable account',
controlName: 'interestPayableAccountId',
options: this.liabilityAccountOptions,
accrualOnly: true
}
],
otherMappingsControlName: 'liabilitiesOtherGlMappings',
otherAccountOptions: this.liabilityAccountOptions
},
{
key: 'expenses',
title: 'Expenses',
pillLabel: 'Expense',
pillClass: 'amber',
rows: [
{
key: 'interestOnSavingsAccountId',
label: 'Interest On Savings Account',
inputLabel: 'Interest on savings account',
controlName: 'interestOnSavingsAccountId',
options: this.expenseAccountOptions,
required: true
}
],
otherMappingsControlName: 'expensesOtherGlMappings',
otherAccountOptions: this.expenseAccountOptions
},
{
key: 'income',
title: 'Income',
pillLabel: 'Income',
pillClass: 'blue',
rows: [
{
key: 'incomeFromFeeAccountId',
label: 'Income From Fees',
inputLabel: 'Income from fees',
controlName: 'incomeFromFeeAccountId',
options: this.incomeAccountOptions,
required: true
},
{
key: 'incomeFromPenaltyAccountId',
label: 'Income From Penalties',
inputLabel: 'Income from penalties',
controlName: 'incomeFromPenaltyAccountId',
options: this.incomeAccountOptions,
required: true
}
],
otherMappingsControlName: 'incomeOtherGlMappings',
otherAccountOptions: this.incomeAccountOptions
}
]; ];
return formfields;
} }
get recurringDepositProductAccounting() { get recurringDepositProductAccounting() {
return this.recurringDepositProductAccountingForm.value; const {
assetsOtherGlMappings,
liabilitiesOtherGlMappings,
expensesOtherGlMappings,
incomeOtherGlMappings,
...accounting
} = this.recurringDepositProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings();
return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) };
} }
isCashOrAccrualAccounting(): boolean { isCashOrAccrualAccounting(): boolean {
@@ -23,7 +23,14 @@
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button type="button" mat-raised-button color="primary" class="rdpch-add-btn" (click)="addCharge(charge)" [disabled]="!charge.value"> <button
type="button"
mat-raised-button
color="primary"
class="rdpch-add-btn"
(click)="addCharge(charge)"
[disabled]="!charge.value"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon> <fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }} {{ 'labels.buttons.Add' | translate }}
</button> </button>
@@ -20,8 +20,17 @@
background: #f8f9fa; background: #f8f9fa;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid #e8eaed;
i { font-size: 14px; color: #0ea5e9; } i {
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } font-size: 14px;
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
} }
&__body { &__body {
@@ -35,7 +44,10 @@
gap: 16px; gap: 16px;
flex-wrap: wrap; flex-wrap: wrap;
&__select { flex: 1; min-width: 200px; } &__select {
flex: 1;
min-width: 200px;
}
} }
.rdpch-add-btn { .rdpch-add-btn {
@@ -51,5 +63,7 @@
overflow: hidden; overflow: hidden;
margin-top: 12px; margin-top: 12px;
table { width: 100%; } table {
width: 100%;
}
} }
@@ -9,71 +9,103 @@
<form [formGroup]="recurringDepositProductCurrencyForm"> <form [formGroup]="recurringDepositProductCurrencyForm">
<div class="rdpc-section"> <div class="rdpc-section">
<div class="rdpc-section__header"> <div class="rdpc-section__header">
<i class="fa fa-money-bill-wave"></i> <i class="fas fa-coins"></i>
<h3>{{ 'labels.inputs.Currency' | translate }}</h3> <span>Currency &amp; Account Status Configuration</span>
</div> </div>
<div class="rdpc-grid"> <div class="rdpc-body">
<mat-form-field appearance="outline" class="rdpc-field"> <div class="rdpc-grid">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<mat-select
formControlName="currencyCode"
matTooltip="{{ 'tooltips.The currency in which the deposit will be disbursed' | translate }}"
required
>
@for (currency of currencyData; track currency) {
<mat-option [value]="currency.code">
{{ currency.name }}
</mat-option>
}
</mat-select>
<mat-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="rdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="digitsAfterDecimal"
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
required
/>
<mat-error *ngIf="recurringDepositProductCurrencyForm.get('digitsAfterDecimal')?.hasError('required')">
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
<mat-error *ngIf="recurringDepositProductCurrencyForm.get('digitsAfterDecimal')?.hasError('min')">
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.Minimum Value must be' | translate }}
<strong>0</strong>
</mat-error>
</mat-form-field>
<div class="rdpc-field rdpc-field--full rdpc-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="setMultiples">
{{ 'labels.inputs.Set the recurring deposit installment in multiples of' | translate }}
</mat-checkbox>
</div>
@if (recurringDepositProductCurrencyForm.value.setMultiples) {
<mat-form-field appearance="outline" class="rdpc-field"> <mat-form-field appearance="outline" class="rdpc-field">
<mat-label>{{ 'labels.inputs.Currency in multiples of' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<input <mat-select
type="number" formControlName="currencyCode"
min="1" matTooltip="{{ 'tooltips.The currency in which the deposit will be disbursed' | translate }}"
matInput required
formControlName="inMultiplesOf" >
matTooltip="{{ 'tooltips.Enter multiples of currency value' | translate }}" @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> <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> <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
}
<mat-form-field appearance="outline" class="rdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<mat-select
formControlName="digitsAfterDecimal"
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
required
>
@for (places of decimalPlacesData; track places) {
<mat-option [value]="places">{{ places }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="rdpc-alert">
<div class="rdpc-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="rdpc-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="rdpc-alert">
<div class="rdpc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="balanceSwapOption">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="msme">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-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="rdpc-alert__choices" formControlName="nominationFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="rdpc-alert">
<div class="rdpc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-radio-group class="rdpc-alert__choices" formControlName="autoDebitFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdpc-section { .rdpc-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,46 +27,77 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { span {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
.rdpc-body {
background: #fff;
}
.rdpc-grid { .rdpc-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
} }
.rdpc-field { .rdpc-field {
width: 100%; width: 100%;
&--full {
grid-column: 1 / -1;
}
} }
.rdpc-checkbox-row { .rdpc-alert {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 0; justify-content: space-between;
} gap: 24px;
padding: 18px 24px 24px;
border-top: 1px solid $border;
background: #f8fafc;
@media (max-width: 600px) { @media (width <= 760px) {
.rdpc-grid { flex-direction: column;
grid-template-columns: 1fr; align-items: flex-start;
}
&__content {
h4 {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: #64748b;
max-width: 540px;
}
}
&__choices {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
} }
} }
@@ -6,12 +6,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core'; import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'mifosx-recurring-deposit-product-currency-step', selector: 'mifosx-recurring-deposit-product-currency-step',
@@ -20,18 +19,23 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatTooltip, MatTooltip,
MatCheckbox MatRadioGroup,
MatRadioButton
] ]
}) })
export class RecurringDepositProductCurrencyStepComponent implements OnInit { export class RecurringDepositProductCurrencyStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private destroyRef = inject(DestroyRef);
@Input() recurringDepositProductsTemplate: any; @Input() recurringDepositProductsTemplate: any;
recurringDepositProductCurrencyForm: UntypedFormGroup; recurringDepositProductCurrencyForm: UntypedFormGroup;
currencyData: any; currencyData: any;
decimalPlacesData = [
2,
0,
3
];
constructor() { constructor() {
this.createrecurringDepositProductCurrencyForm(); this.createrecurringDepositProductCurrencyForm();
@@ -41,24 +45,17 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
this.currencyData = this.recurringDepositProductsTemplate.currencyOptions; this.currencyData = this.recurringDepositProductsTemplate.currencyOptions;
this.recurringDepositProductCurrencyForm.patchValue({ this.recurringDepositProductCurrencyForm.patchValue({
currencyCode: this.recurringDepositProductsTemplate.currency?.code || this.currencyData[0].code, currencyCode: this.recurringDepositProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
digitsAfterDecimal: this.recurringDepositProductsTemplate.digitsAfterDecimal ?? '', digitsAfterDecimal:
setMultiples: !!this.recurringDepositProductsTemplate.inMultiplesOf, this.recurringDepositProductsTemplate.digitsAfterDecimal ??
inMultiplesOf: this.recurringDepositProductsTemplate.inMultiplesOf ?? '' this.recurringDepositProductsTemplate.currency?.decimalPlaces ??
2,
dormancyAlert: this.recurringDepositProductsTemplate.dormancyAlert ?? false,
balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false,
msme: this.recurringDepositProductsTemplate.msme ?? false,
nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false,
autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false
}); });
this.setupConditionalValidation();
// Apply initial validators based on the patched setMultiples value
const inMultiplesOfControl = this.recurringDepositProductCurrencyForm.get('inMultiplesOf');
const setMultiplesControl = this.recurringDepositProductCurrencyForm.get('setMultiples');
if (setMultiplesControl?.value) {
inMultiplesOfControl?.setValidators([
Validators.required,
Validators.min(1)
]);
inMultiplesOfControl?.updateValueAndValidity();
}
} }
createrecurringDepositProductCurrencyForm() { createrecurringDepositProductCurrencyForm() {
@@ -68,46 +65,30 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit {
Validators.required Validators.required
], ],
digitsAfterDecimal: [ digitsAfterDecimal: [
'', 2,
[ [
Validators.required, Validators.required,
Validators.min(0) Validators.min(0)
] ]
], ],
setMultiples: [false], dormancyAlert: [false],
inMultiplesOf: [''] balanceSwapOption: [false],
}); msme: [false],
} nominationFacility: [false],
autoDebitFacility: [false]
setupConditionalValidation() {
const inMultiplesOfControl = this.recurringDepositProductCurrencyForm.get('inMultiplesOf');
const setMultiplesControl = this.recurringDepositProductCurrencyForm.get('setMultiples');
setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => {
if (checked) {
inMultiplesOfControl?.setValidators([
Validators.required,
Validators.min(1)
]);
} else {
inMultiplesOfControl?.clearValidators();
inMultiplesOfControl?.setValue('');
}
inMultiplesOfControl?.updateValueAndValidity();
}); });
} }
get recurringDepositProductCurrency() { get recurringDepositProductCurrency() {
const formValue = this.recurringDepositProductCurrencyForm.value; const formValue = this.recurringDepositProductCurrencyForm.value;
const result: any = { return {
currencyCode: formValue.currencyCode, currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal digitsAfterDecimal: formValue.digitsAfterDecimal,
dormancyAlert: formValue.dormancyAlert,
balanceSwapOption: formValue.balanceSwapOption,
msme: formValue.msme,
nominationFacility: formValue.nominationFacility,
autoDebitFacility: formValue.autoDebitFacility
}; };
if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) {
result.inMultiplesOf = formValue.inMultiplesOf;
}
return result;
} }
} }
@@ -10,7 +10,7 @@
<div class="rdpd-section"> <div class="rdpd-section">
<div class="rdpd-section__header"> <div class="rdpd-section__header">
<i class="fa fa-info-circle"></i> <i class="fa fa-info-circle"></i>
<h3>{{ 'labels.inputs.Product Name' | translate }}</h3> <h3>{{ 'labels.inputs.Product Name' | translate }} &amp; {{ 'labels.inputs.Description' | translate }}</h3>
</div> </div>
<div class="rdpd-grid"> <div class="rdpd-grid">
<mat-form-field appearance="outline" class="rdpd-field"> <mat-form-field appearance="outline" class="rdpd-field">
@@ -57,13 +57,67 @@
cdkTextareaAutosize cdkTextareaAutosize
cdkAutosizeMinRows="2" cdkAutosizeMinRows="2"
></textarea> ></textarea>
@if (recurringDepositProductDetailsForm.controls.description.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="rdpd-section">
<div class="rdpd-section__header">
<i class="fa fa-tags"></i>
<h3>Product Classification</h3>
</div>
<div class="rdpd-grid rdpd-grid--classification">
<mat-form-field appearance="outline" class="rdpd-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="rdpd-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="rdpd-field">
<mat-label>Product Code</mat-label>
<input matInput formControlName="productCode" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="rdpd-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="rdpd-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="rdpd-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="rdpd-field rdpd-field--full rdpd-checkbox-row">
<mat-checkbox formControlName="shareLinkingEnabled">Share Linking Enabled</mat-checkbox>
</div>
</div>
</div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdpd-section { .rdpd-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,19 +27,19 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { h3 {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
@@ -43,6 +49,22 @@
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.rdpd-grid--classification {
grid-template-columns: repeat(3, 1fr);
@media (width <= 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
} }
.rdpd-field { .rdpd-field {
@@ -53,8 +75,8 @@
} }
} }
@media (max-width: 600px) { .rdpd-checkbox-row {
.rdpd-grid { display: flex;
grid-template-columns: 1fr; align-items: center;
} min-height: 56px;
} }
@@ -6,10 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Component, OnInit, Input, inject } from '@angular/core'; import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -22,24 +24,34 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
CdkTextareaAutosize CdkTextareaAutosize
] ]
}) })
export class RecurringDepositProductDetailsStepComponent implements OnInit { export class RecurringDepositProductDetailsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() recurringDepositProductsTemplate: any; @Input() recurringDepositProductsTemplate: any;
recurringDepositProductDetailsForm: UntypedFormGroup; recurringDepositProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = [];
customerSegmentData: string[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() { constructor() {
this.createrecurringDepositProductDetailsForm(); this.createrecurringDepositProductDetailsForm();
} }
ngOnInit() { ngOnInit() {
if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { if (this.recurringDepositProductsTemplate) {
this.recurringDepositProductDetailsForm.patchValue({ this.applyTemplate(this.recurringDepositProductsTemplate);
name: this.recurringDepositProductsTemplate.name, }
shortName: this.recurringDepositProductsTemplate.shortName, }
description: this.recurringDepositProductsTemplate.description
}); ngOnChanges(changes: SimpleChanges) {
if (changes.recurringDepositProductsTemplate?.currentValue) {
this.applyTemplate(changes.recurringDepositProductsTemplate.currentValue);
} }
} }
@@ -53,14 +65,70 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit {
'', '',
Validators.required Validators.required
], ],
description: [ description: [''],
'', accountType: [
null,
Validators.required Validators.required
] ],
customerSegment: [null],
productCode: [''],
effectiveTo: [''],
endDate: [''],
introducerRequired: [false],
shareLinkingEnabled: [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 parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (Array.isArray(dateValue)) {
return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]);
}
if (dateValue instanceof Date) {
return dateValue;
}
return new Date(dateValue);
}
private applyTemplate(template: any) {
this.accountTypeData = this.getOptionNames(template.accountTypeOptions);
this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions);
const isEditing = !!template.id;
this.recurringDepositProductDetailsForm.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 ?? '',
effectiveTo: this.parseDate(template.effectiveTo),
endDate: this.parseDate(template.endDate),
introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes',
shareLinkingEnabled: !!template.shareLinkingEnabled
}); });
} }
get recurringDepositProductDetails() { get recurringDepositProductDetails() {
return this.recurringDepositProductDetailsForm.value; const formValue = { ...this.recurringDepositProductDetailsForm.value };
const dateFormat = this.settingsService.dateFormat;
if (formValue.effectiveTo instanceof Date) {
formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || '';
}
if (formValue.endDate instanceof Date) {
formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || '';
}
return formValue;
} }
} }
@@ -78,7 +78,13 @@
<mat-form-field appearance="outline" class="rdpi-field" (click)="endDatePicker.open()"> <mat-form-field appearance="outline" class="rdpi-field" (click)="endDatePicker.open()">
<mat-label>{{ 'labels.inputs.End Date' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.End Date' | translate }}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="endDatePicker" formControlName="endDate" /> <input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="endDatePicker"
formControlName="endDate"
/>
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<mat-datepicker #endDatePicker></mat-datepicker> <mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field> </mat-form-field>
@@ -91,7 +97,13 @@
</div> </div>
<div class="rdpi-slab-actions"> <div class="rdpi-slab-actions">
<button type="button" mat-raised-button color="primary" class="rdpi-add-btn" (click)="addChartSlab(chart.controls.chartSlabs)"> <button
type="button"
mat-raised-button
color="primary"
class="rdpi-add-btn"
(click)="addChartSlab(chart.controls.chartSlabs)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon> <fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add Slab' | translate }} {{ 'labels.buttons.Add Slab' | translate }}
</button> </button>
@@ -105,11 +117,7 @@
@if (chart.value.chartSlabs.length !== 0) { @if (chart.value.chartSlabs.length !== 0) {
<div class="rdpi-table-wrapper"> <div class="rdpi-table-wrapper">
<table <table mat-table [dataSource]="chart.value.chartSlabs" multiTemplateDataRows>
mat-table
[dataSource]="chart.value.chartSlabs"
multiTemplateDataRows
>
<ng-container matColumnDef="period"> <ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab"> <td mat-cell *matCellDef="let chartSlab">
@@ -184,7 +192,9 @@
> >
<div <div
class="rdpi-incentives" class="rdpi-incentives"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex[chartIndex] ? 'expanded' : 'collapsed'" [@expandChartSlab]="
chartSlabIndex === expandChartSlabIndex[chartIndex] ? 'expanded' : 'collapsed'
"
> >
<mat-card class="rdpi-incentive-card"> <mat-card class="rdpi-incentive-card">
<h4 class="rdpi-incentive-title"> <h4 class="rdpi-incentive-title">
@@ -205,25 +215,33 @@
<div class="rdpi-table-wrapper"> <div class="rdpi-table-wrapper">
<table mat-table [dataSource]="chartSlab.incentives"> <table mat-table [dataSource]="chartSlab.incentives">
<ng-container matColumnDef="entityType"> <ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Entity Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ incentive.entityType | find: entityTypeData : 'id' : 'value' }} {{ incentive.entityType | find: entityTypeData : 'id' : 'value' }}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="attributeName"> <ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Attribute Name' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }} {{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="conditionType"> <ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Condition Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }} {{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="attributeValue"> <ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Attribute Value' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
@switch (incentive.attributeName) { @switch (incentive.attributeName) {
@case (2) { @case (2) {
@@ -233,7 +251,9 @@
<span>{{ incentive.attributeValue }}</span> <span>{{ incentive.attributeValue }}</span>
} }
@case (4) { @case (4) {
<span>{{ incentive.attributeValue | find: clientTypeData : 'id' : 'name' }}</span> <span>{{
incentive.attributeValue | find: clientTypeData : 'id' : 'name'
}}</span>
} }
@case (5) { @case (5) {
<span>{{ <span>{{
@@ -244,7 +264,9 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="incentiveType"> <ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Incentive Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }} {{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }}
</td> </td>
@@ -143,7 +143,7 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
@media (max-width: 600px) { @media (width <= 600px) {
.rdpi-grid { .rdpi-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -300,7 +300,9 @@
> >
<div <div
class="rdpp-incentives" class="rdpp-incentives"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex[chartIndex] ? 'expanded' : 'collapsed'" [@expandChartSlab]="
chartSlabIndex === expandChartSlabIndex[chartIndex] ? 'expanded' : 'collapsed'
"
> >
<mat-card class="rdpp-incentives__card"> <mat-card class="rdpp-incentives__card">
<h4>{{ 'labels.heading.Incentives' | translate }}</h4> <h4>{{ 'labels.heading.Incentives' | translate }}</h4>
@@ -319,7 +321,9 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="attributeName"> <ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Attribute Name' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ {{
incentive.attributeName incentive.attributeName
@@ -331,7 +335,9 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="conditionType"> <ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Condition Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ {{
incentive.conditionType incentive.conditionType
@@ -343,13 +349,18 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="attributeValue"> <ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Attribute Value' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
@switch (incentive.attributeName) { @switch (incentive.attributeName) {
@case (2) { @case (2) {
<span>{{ <span>{{
incentive.attributeValue incentive.attributeValue
| find: recurringDepositProductsTemplate.chartTemplate.genderOptions : 'id' : 'name' | find
: recurringDepositProductsTemplate.chartTemplate.genderOptions
: 'id'
: 'name'
}}</span> }}</span>
} }
@case (3) { @case (3) {
@@ -377,7 +388,9 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="incentiveType"> <ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th> <th mat-header-cell *matHeaderCellDef>
{{ 'labels.inputs.Incentive Type' | translate }}
</th>
<td mat-cell *matCellDef="let incentive"> <td mat-cell *matCellDef="let incentive">
{{ {{
incentive.incentiveType incentive.incentiveType
@@ -468,7 +481,10 @@
</div> </div>
<div class="rdpp-section__body"> <div class="rdpp-section__body">
@if (!isNoneAccounting()) { @if (!isNoneAccounting()) {
<mifosx-view-savings-accounting-details [accountingRule]="accountingRule" [accountingMappings]="accountingMappings"> <mifosx-view-savings-accounting-details
[accountingRule]="accountingRule"
[accountingMappings]="accountingMappings"
>
</mifosx-view-savings-accounting-details> </mifosx-view-savings-accounting-details>
} }
@@ -12,7 +12,9 @@
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
& + & { margin-top: 16px; } & + & {
margin-top: 16px;
}
&__header { &__header {
display: flex; display: flex;
@@ -22,11 +24,22 @@
background: #f8f9fa; background: #f8f9fa;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid #e8eaed;
i { font-size: 14px; color: #0ea5e9; } i {
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } font-size: 14px;
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
} }
&__body { padding: 16px 24px; } &__body {
padding: 16px 24px;
}
} }
.rdpp-product-name { .rdpp-product-name {
@@ -41,7 +54,9 @@
padding: 6px 0; padding: 6px 0;
border-bottom: 1px solid #f1f3f4; border-bottom: 1px solid #f1f3f4;
&:last-child { border-bottom: none; } &:last-child {
border-bottom: none;
}
} }
.rdpp-label { .rdpp-label {
@@ -63,9 +78,13 @@
overflow: hidden; overflow: hidden;
margin-top: 8px; margin-top: 8px;
table { width: 100%; } table {
width: 100%;
}
& + & { margin-top: 12px; } & + & {
margin-top: 12px;
}
} }
.rdpp-sub-heading { .rdpp-sub-heading {
@@ -108,6 +127,9 @@
table { table {
tr.incentives-row { tr.incentives-row {
height: 0; height: 0;
> td { padding: 0; }
> td {
padding: 0;
}
} }
} }
@@ -7,215 +7,117 @@
--> -->
<form [formGroup]="recurringDepositProductSettingsForm"> <form [formGroup]="recurringDepositProductSettingsForm">
<!-- Premature option -->
<div class="rdps-section"> <div class="rdps-section">
<div class="rdps-section__header"> <div class="rdps-section__header">
<i class="fa fa-toggle-on"></i> <span class="rdps-section__badge"><i class="fas fa-balance-scale"></i></span>
<h3>{{ 'labels.inputs.Deposit Options' | translate }}</h3> <span>Premature option</span>
</div> </div>
<div class="rdps-grid"> <div class="rdps-body">
<div class="rdps-checkbox-row"> <div class="rdps-premature-row">
<mat-checkbox labelPosition="before" formControlName="isMandatoryDeposit"> <mat-checkbox formControlName="loanFacility">Loan Facility</mat-checkbox>
{{ 'labels.inputs.Is Mandatory Deposit' | translate }} <mat-checkbox formControlName="prematureWithdrawalOption">Premature Withdrawal option</mat-checkbox>
</mat-checkbox> <mat-checkbox formControlName="tenureFlexibility">Tenure Flexibility</mat-checkbox>
</div> <mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
<mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
<div class="rdps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="adjustAdvanceTowardsFuturePayments">
{{ 'labels.inputs.Adjust advance payments toward future installments' | translate }}
</mat-checkbox>
</div>
<div class="rdps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="allowWithdrawal">
{{ 'labels.inputs.Allow Withdrawals' | translate }}
</mat-checkbox>
</div> </div>
</div> </div>
</div> </div>
<!-- Lock-in Period -->
<div class="rdps-section"> <div class="rdps-section">
<div class="rdps-section__header"> <div class="rdps-section__header">
<i class="fa fa-lock"></i> <span class="rdps-section__badge"><i class="fas fa-lock"></i></span>
<h3>{{ 'labels.inputs.Lock-in Period' | translate }}</h3> <span>{{ 'labels.inputs.Lock-in Period' | translate }}</span>
</div> </div>
<div class="rdps-grid"> <div class="rdps-body">
<mat-form-field appearance="outline" class="rdps-field"> <div class="rdps-lockin-row">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label> <span>Enable Lock-in Period</span>
<input <mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
type="number"
matInput
formControlName="lockinPeriodFrequency"
matTooltip="{{ 'tooltips.The number at which lock in period occurs' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="rdps-section">
<div class="rdps-section__header">
<i class="fa fa-calendar-check"></i>
<h3>{{ 'labels.inputs.Minimum Deposit Term' | translate }}</h3>
</div>
<div class="rdps-grid">
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
matInput
formControlName="minDepositTerm"
matTooltip="{{ 'tooltips.The number at which minimum deposit occurs' | translate }}"
required
/>
<mat-error>
{{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Frequency Type' | translate }}</mat-label>
<mat-select formControlName="minDepositTermTypeId" required>
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
</div>
<div class="rdps-section">
<div class="rdps-section__header">
<i class="fa fa-layer-group"></i>
<h3>{{ 'labels.heading.And thereafter, in Multiples of' | translate }}</h3>
</div>
<div class="rdps-grid">
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
matInput
formControlName="inMultiplesOfDepositTerm"
matTooltip="{{ 'tooltips.The number at which multiple deposits occurs' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="inMultiplesOfDepositTermTypeId">
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="rdps-section">
<div class="rdps-section__header">
<i class="fa fa-calendar-alt"></i>
<h3>{{ 'labels.inputs.Maximum Deposit Term' | translate }}</h3>
</div>
<div class="rdps-grid">
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
matInput
formControlName="maxDepositTerm"
matTooltip="{{ 'tooltips.The number at which minimum deposit occurs' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="maxDepositTermTypeId">
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="rdps-section">
<div class="rdps-section__header">
<i class="fa fa-exclamation-triangle"></i>
<h3>{{ 'labels.heading.For Pre-mature closure' | translate }}</h3>
</div>
<div class="rdps-grid">
<div class="rdps-checkbox-row rdps-field--full">
<mat-checkbox labelPosition="before" formControlName="preClosurePenalApplicable">
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
</div> </div>
<mat-form-field appearance="outline" class="rdps-field"> @if (recurringDepositProductSettingsForm.value.enableLockinPeriod) {
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label> <div class="rdps-lockin-fields">
<input type="number" matInput formControlName="preClosurePenalInterest" /> <mat-form-field appearance="outline" class="rdps-field">
</mat-form-field> <mat-label>Frequency*</mat-label>
<input type="number" matInput formControlName="lockinPeriodFrequency" min="1" step="1" />
</mat-form-field>
<mat-form-field appearance="outline" class="rdps-field"> <mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label> <mat-label>Type*</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId"> <mat-select formControlName="lockinPeriodFrequencyType">
@for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="preClosurePenalInterestOnType.id"> <mat-option [value]="lockinPeriodFrequencyType.id">
{{ preClosurePenalInterestOnType.value }} {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option> </mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
</div>
<div class="rdps-section">
<div class="rdps-section__header">
<i class="fa fa-file-invoice-dollar"></i>
<h3>{{ 'labels.inputs.Tax' | translate }}</h3>
</div>
<div class="rdps-grid">
<div class="rdps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="withHoldTax">
{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}
</mat-checkbox>
</div>
@if (recurringDepositProductSettingsForm.value.withHoldTax) {
<mat-form-field appearance="outline" class="rdps-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>
</div> </div>
<!-- Maturity Instructions -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-lock"></i></span>
<span>Maturity Instructions</span>
</div>
<div class="rdps-body rdps-body--list">
<div class="rdps-list-row">
<span>Maturity Instructions</span>
<mat-checkbox formControlName="maturityInstructions"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span
>Option to credit maturity proceeds directly to the linked account or reinvest in another product (like
FD).</span
>
<mat-checkbox formControlName="creditMaturityProceeds"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Penalty / Grace Period for Missed Installments</span>
<mat-checkbox formControlName="penaltyGracePeriodMissedInstallments"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Panel Request Applicable</span>
<mat-checkbox formControlName="panelRequestApplicable"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>No penalty for nominee / legal heir closure</span>
<mat-checkbox formControlName="noPenaltyNomineeHeirClosure"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Deceased Settlement</span>
<mat-checkbox formControlName="deceasedSettlement"></mat-checkbox>
</div>
<div class="rdps-list-row">
<span>Allow Debit Txn after due date</span>
<mat-checkbox formControlName="allowDebitTxnAfterDueDate"></mat-checkbox>
</div>
</div>
</div>
<!-- Additional Rules -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-cog"></i></span>
<span>Additional Rules</span>
</div>
<div class="rdps-body">
<div class="rdps-layout rdps-layout--additional">
<div class="rdps-toggle rdps-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="rdps-field">
<mat-label>Balance Required for Interest Calculation</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdps-section { .rdps-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,28 +27,37 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { span:not(.rdps-section__badge) {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
.rdps-grid { .rdps-section__badge {
display: grid; display: inline-flex;
grid-template-columns: repeat(2, 1fr); align-items: center;
gap: 4px 16px; justify-content: center;
padding: 16px 24px; width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.rdps-body {
background: #fff;
padding: 18px 24px 24px;
} }
.rdps-field { .rdps-field {
@@ -53,14 +68,98 @@
} }
} }
.rdps-checkbox-row { .rdps-premature-row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 0; gap: 24px;
flex-wrap: wrap;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
} }
@media (max-width: 600px) { .rdps-layout {
.rdps-grid { display: grid;
gap: 16px;
}
.rdps-layout--additional {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 1100px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.rdps-toggle {
display: flex;
align-items: center;
min-height: 40px;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
&--full {
min-height: auto;
margin-bottom: 16px;
}
}
.rdps-body--list {
padding: 0 24px;
}
.rdps-list-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 0;
border-bottom: 1px solid $gray-200;
font-size: 14px;
color: $gray-900;
&:last-child {
border-bottom: none;
}
span {
max-width: 85%;
line-height: 1.4;
}
}
.rdps-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;
}
.rdps-lockin-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
&--single {
grid-template-columns: minmax(0, 1fr);
max-width: 50%;
}
@media (width <= 640px) {
grid-template-columns: 1fr;
&--single {
max-width: 100%;
}
}
}
@@ -7,15 +7,8 @@
*/ */
import { Component, OnInit, Input, inject } from '@angular/core'; import { Component, OnInit, Input, inject } from '@angular/core';
import { import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms';
UntypedFormGroup,
UntypedFormBuilder,
UntypedFormControl,
Validators,
ReactiveFormsModule
} from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -24,8 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./recurring-deposit-product-settings-step.component.scss'], styleUrls: ['./recurring-deposit-product-settings-step.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatCheckbox, MatCheckbox
MatTooltip
] ]
}) })
export class RecurringDepositProductSettingsStepComponent implements OnInit { export class RecurringDepositProductSettingsStepComponent implements OnInit {
@@ -36,106 +28,68 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit {
recurringDepositProductSettingsForm: UntypedFormGroup; recurringDepositProductSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any; lockinPeriodFrequencyTypeData: any;
periodFrequencyTypeData: any;
preClosurePenalInterestOnTypeData: any;
taxGroupData: any;
constructor() { constructor() {
this.createrecurringDepositProductSettingsForm(); this.createrecurringDepositProductSettingsForm();
this.setConditionalControls();
} }
ngOnInit() { ngOnInit() {
this.lockinPeriodFrequencyTypeData = this.recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; this.lockinPeriodFrequencyTypeData = this.recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions;
this.periodFrequencyTypeData = this.recurringDepositProductsTemplate.periodFrequencyTypeOptions.slice(0, -1);
this.preClosurePenalInterestOnTypeData = this.recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions;
this.taxGroupData = this.recurringDepositProductsTemplate.taxGroupOptions;
if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { if (this.recurringDepositProductsTemplate?.id) {
this.recurringDepositProductSettingsForm.patchValue({ this.recurringDepositProductSettingsForm.patchValue({
isMandatoryDeposit: this.recurringDepositProductsTemplate.isMandatoryDeposit, loanFacility: this.recurringDepositProductsTemplate.loanFacility ?? false,
adjustAdvanceTowardsFuturePayments: this.recurringDepositProductsTemplate.adjustAdvanceTowardsFuturePayments, prematureWithdrawalOption: this.recurringDepositProductsTemplate.prematureWithdrawalOption ?? false,
allowWithdrawal: this.recurringDepositProductsTemplate.allowWithdrawal, tenureFlexibility: this.recurringDepositProductsTemplate.tenureFlexibility ?? false,
lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency, lienMarked: this.recurringDepositProductsTemplate.lienMarked ?? false,
lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType withdrawalFeeForTransfers: this.recurringDepositProductsTemplate.withdrawalFeeForTransfers ?? false,
? this.recurringDepositProductsTemplate.lockinPeriodFrequencyType.id enableLockinPeriod: this.recurringDepositProductsTemplate.enableLockinPeriod ?? false,
: '', lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency ?? '',
minDepositTerm: this.recurringDepositProductsTemplate.minDepositTerm, lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '',
minDepositTermTypeId: this.recurringDepositProductsTemplate.minDepositTermType maturityInstructions: this.recurringDepositProductsTemplate.maturityInstructions ?? false,
? this.recurringDepositProductsTemplate.minDepositTermType.id creditMaturityProceeds: this.recurringDepositProductsTemplate.creditMaturityProceeds ?? false,
: '', penaltyGracePeriodMissedInstallments:
inMultiplesOfDepositTerm: this.recurringDepositProductsTemplate.inMultiplesOfDepositTerm, this.recurringDepositProductsTemplate.penaltyGracePeriodMissedInstallments ?? false,
inMultiplesOfDepositTermTypeId: this.recurringDepositProductsTemplate.inMultiplesOfDepositTermType panelRequestApplicable: this.recurringDepositProductsTemplate.panelRequestApplicable ?? false,
? this.recurringDepositProductsTemplate.inMultiplesOfDepositTerm.id noPenaltyNomineeHeirClosure: this.recurringDepositProductsTemplate.noPenaltyNomineeHeirClosure ?? false,
: '', deceasedSettlement: this.recurringDepositProductsTemplate.deceasedSettlement ?? false,
maxDepositTerm: this.recurringDepositProductsTemplate.maxDepositTerm, allowDebitTxnAfterDueDate: this.recurringDepositProductsTemplate.allowDebitTxnAfterDueDate ?? false,
maxDepositTermTypeId: this.recurringDepositProductsTemplate.maxDepositTermType accountClosedZeroBalanceInDayClosing:
? this.recurringDepositProductsTemplate.minDepositTermType.id this.recurringDepositProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false,
: '', minBalanceForInterestCalculation: this.recurringDepositProductsTemplate.minBalanceForInterestCalculation ?? ''
preClosurePenalApplicable: this.recurringDepositProductsTemplate.preClosurePenalApplicable,
preClosurePenalInterest: this.recurringDepositProductsTemplate.preClosurePenalInterest,
preClosurePenalInterestOnTypeId: this.recurringDepositProductsTemplate.preClosurePenalInterestOnType
? this.recurringDepositProductsTemplate.preClosurePenalInterestOnType.id
: '',
withHoldTax: this.recurringDepositProductsTemplate.withHoldTax
});
}
if (this.recurringDepositProductsTemplate.withHoldTax) {
this.recurringDepositProductSettingsForm.patchValue({
taxGroupId: this.recurringDepositProductsTemplate.taxGroup
? this.recurringDepositProductsTemplate.taxGroup.id
: ''
}); });
} }
} }
createrecurringDepositProductSettingsForm() { createrecurringDepositProductSettingsForm() {
this.recurringDepositProductSettingsForm = this.formBuilder.group({ this.recurringDepositProductSettingsForm = this.formBuilder.group({
isMandatoryDeposit: [false], loanFacility: [false],
adjustAdvanceTowardsFuturePayments: [false], prematureWithdrawalOption: [false],
allowWithdrawal: [false], tenureFlexibility: [false],
lienMarked: [false],
withdrawalFeeForTransfers: [false],
enableLockinPeriod: [false],
lockinPeriodFrequency: [''], lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''], lockinPeriodFrequencyType: [''],
minDepositTerm: [ maturityInstructions: [false],
'', creditMaturityProceeds: [false],
Validators.required penaltyGracePeriodMissedInstallments: [false],
], panelRequestApplicable: [false],
minDepositTermTypeId: [ noPenaltyNomineeHeirClosure: [false],
'', deceasedSettlement: [false],
Validators.required allowDebitTxnAfterDueDate: [false],
], accountClosedZeroBalanceInDayClosing: [false],
inMultiplesOfDepositTerm: [''], minBalanceForInterestCalculation: ['']
inMultiplesOfDepositTermTypeId: [''],
maxDepositTerm: [''],
maxDepositTermTypeId: [''],
preClosurePenalApplicable: [false],
preClosurePenalInterest: [''],
preClosurePenalInterestOnTypeId: [''],
withHoldTax: [false]
});
}
setConditionalControls() {
this.recurringDepositProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => {
if (withHoldTax) {
this.recurringDepositProductSettingsForm.addControl(
'taxGroupId',
new UntypedFormControl('', Validators.required)
);
} else {
this.recurringDepositProductSettingsForm.removeControl('taxGroupId');
}
}); });
} }
get recurringDepositProductSettings() { get recurringDepositProductSettings() {
const recurringDepositProductSettings = this.recurringDepositProductSettingsForm.value; const settings = { ...this.recurringDepositProductSettingsForm.value };
for (const key in recurringDepositProductSettings) { for (const key in settings) {
if (recurringDepositProductSettings[key] === '') { if (settings[key] === '') {
delete recurringDepositProductSettings[key]; delete settings[key];
} }
} }
return recurringDepositProductSettings; return settings;
} }
} }
@@ -9,99 +9,148 @@
<form [formGroup]="recurringDepositProductTermsForm"> <form [formGroup]="recurringDepositProductTermsForm">
<div class="rdpt-section"> <div class="rdpt-section">
<div class="rdpt-section__header"> <div class="rdpt-section__header">
<i class="fa fa-coins"></i> <span class="rdpt-section__badge"><i class="fas fa-chart-line"></i></span>
<h3>{{ 'labels.inputs.Deposit Amount' | translate }}</h3> <span>Interest Rate &amp; Posting Terms</span>
</div> </div>
<div class="rdpt-grid rdpt-grid--three"> <div class="rdpt-body">
<mat-form-field appearance="outline" class="rdpt-field"> <div class="rdpt-banner">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label> <span class="rdpt-banner__icon">ℹ️</span>
<input type="number" min="0" matInput formControlName="minDepositAmount" /> <span>
</mat-form-field> Recurring Deposit base rate:
<b>{{ recurringDepositProductTermsForm.get('nominalAnnualInterestRate').value }}%</b>
</span>
</div>
<mat-form-field appearance="outline" class="rdpt-field"> <div class="rdpt-grid rdpt-grid--three">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label> <mat-form-field appearance="outline" class="rdpt-field">
<input type="number" min="0" matInput formControlName="depositAmount" required /> <mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
<mat-error> <input
{{ 'labels.inputs.Default' | translate }} {{ 'labels.inputs.Deposit Amount' | translate }} type="number"
{{ 'labels.commons.is' | translate }} <strong>{{ 'labels.commons.required' | translate }}</strong> matInput
</mat-error> matTooltip="{{ 'tooltips.The default interest rate set' | translate }}"
</mat-form-field> formControlName="nominalAnnualInterestRate"
required
<mat-form-field appearance="outline" class="rdpt-field"> min="0"
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label> step="0.01"
<input type="number" min="0" matInput formControlName="maxDepositAmount" /> />
</mat-form-field> @if (recurringDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) {
</div> <mat-error>
</div> {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
<div class="rdpt-section"> </mat-error>
<div class="rdpt-section__header">
<i class="fa fa-percentage"></i>
<h3>{{ 'labels.inputs.Interest Compounding Period' | translate }}</h3>
</div>
<div class="rdpt-grid">
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) {
<mat-option [value]="interestCompoundingPeriodType.id">
{{ interestCompoundingPeriodType.value }}
</mat-option>
} }
</mat-select> @if (recurringDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) {
<mat-error> <mat-error>{{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater</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="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select formControlName="interestPostingPeriodType" required>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value }}
</mat-option>
} }
</mat-select> </mat-form-field>
<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="rdpt-field"> <mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculated using' | translate }}</mat-label> <mat-label>Effective Date</mat-label>
<mat-select formControlName="interestCalculationType" required> <input matInput [matDatepicker]="effectiveDatePicker" formControlName="effectiveDate" />
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { <mat-datepicker-toggle matSuffix [for]="effectiveDatePicker"></mat-datepicker-toggle>
<mat-option [value]="interestCalculationType.id"> <mat-datepicker #effectiveDatePicker></mat-datepicker>
{{ interestCalculationType.value }} </mat-form-field>
</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="rdpt-field"> <mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required> <mat-select
@for ( formControlName="interestCalculationType"
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
track interestCalculationDaysInYearType required
) { >
<mat-option [value]="interestCalculationDaysInYearType.id"> @for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) {
{{ interestCalculationDaysInYearType.value }} <mat-option [value]="interestCalculationType.id">
</mat-option> {{ interestCalculationType.value | translateKey: 'catalogs' }}
} </mat-option>
</mat-select> }
<mat-error> </mat-select>
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} <mat-error>
<strong>{{ 'labels.commons.required' | translate }}</strong> {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }}
</mat-error> <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-form-field> </mat-error>
</mat-form-field>
</div>
<div class="rdpt-grid rdpt-grid--three">
<mat-form-field appearance="outline" class="rdpt-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 (recurringDepositProductTermsForm.value.interestCalculateSelector === 2) {
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod">
@for (period of periodFrequencyData; track period.id) {
<mat-option [value]="period.value">{{ period.value }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select
formControlName="interestPostingPeriodType"
matTooltip="{{ 'tooltips.Interest rate is posted or credited to a saving account' | translate }}"
required
>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="rdpt-footer">
<div class="rdpt-footer__grid">
<div class="rdpt-toggle-card">
<div>
<h4>Auto Interest Post</h4>
</div>
<div class="rdpt-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="rdpt-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="rdpt-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 (recurringDepositProductTermsForm.value.interestCalculateSelector === 1) {
<div class="rdpt-note">
<span>⚠️</span>
<span
>Daily product-based interest accrual enabled - interest calculated EOD on daily closing balance.</span
>
</div>
}
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdpt-section { .rdpt-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,23 +27,39 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { h3,
span:not(.rdpt-section__badge) {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
.rdpt-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.rdpt-body {
background: #fff;
}
.rdpt-grid { .rdpt-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@@ -47,6 +69,14 @@
&--three { &--three {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
@media (width <= 600px) {
grid-template-columns: 1fr;
&--three {
grid-template-columns: 1fr;
}
}
} }
.rdpt-field { .rdpt-field {
@@ -57,12 +87,66 @@
} }
} }
@media (max-width: 600px) { .rdpt-banner {
.rdpt-grid { display: flex;
grid-template-columns: 1fr; 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;
&--three { &__icon {
grid-template-columns: 1fr; font-size: 14px;
}
} }
} }
.rdpt-footer {
padding: 0 24px 24px;
border-top: 1px solid #f1f5f9;
}
.rdpt-footer__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding-top: 18px;
@media (width <= 900px) {
grid-template-columns: 1fr;
}
}
.rdpt-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;
}
}
.rdpt-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;
}

Some files were not shown because too many files have changed in this diff Show More