saving , current and RD account code update

This commit is contained in:
Nitesh Yadav
2026-04-22 18:45:16 +05:30
79 changed files with 7637 additions and 1745 deletions
@@ -34,6 +34,7 @@
<fa-icon icon="eye" size="sm"></fa-icon> <fa-icon icon="eye" size="sm"></fa-icon>
</ng-template> </ng-template>
<!-- Step 1: Details -->
<mat-step [stepControl]="currentAccountDetailsForm"> <mat-step [stepControl]="currentAccountDetailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
@@ -44,14 +45,43 @@
</mifosx-current-account-details-step> </mifosx-current-account-details-step>
</mat-step> </mat-step>
<mat-step [stepControl]="currentAccountTermsForm"> <!-- Step 2: Currency -->
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template> <mat-step>
<ng-template matStepLabel>CURRENCY</ng-template>
<mifosx-current-account-terms-step <mifosx-current-account-currency-step
[currentAccountProductTemplate]="currentAccountProductTemplate" [currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-terms-step> ></mifosx-current-account-currency-step>
</mat-step> </mat-step>
<!-- Step 3: Term (Interest Rate) -->
<mat-step [stepControl]="currentAccountTermForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-current-account-term-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-term-step>
</mat-step>
<!-- Step 4: Settings -->
<mat-step>
<ng-template matStepLabel>SETTINGS</ng-template>
<mifosx-current-account-settings-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-settings-step>
</mat-step>
<!-- Step 5: Digital -->
<mat-step>
<ng-template matStepLabel>DIGITAL</ng-template>
<mifosx-current-account-digital-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-digital-step>
</mat-step>
<!-- Step 6: Charges -->
<mat-step> <mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
@@ -61,14 +91,25 @@
[currencyCode]="currentAccountTermsForm.get('currencyCode')" [currencyCode]="currentAccountTermsForm.get('currencyCode')"
> >
</mifosx-current-account-charges-step> </mifosx-current-account-charges-step>
<!-- Hidden terms step to keep currencyCode form populated -->
<div style="display: none">
<mifosx-current-account-terms-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
[currentAccountTemplate]="currentAccountTemplate"
></mifosx-current-account-terms-step>
</div>
</mat-step> </mat-step>
<!-- Step 7: Preview -->
@if (currentAccountFormValid) { @if (currentAccountFormValid) {
<mat-step completed> <mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-current-account-preview-step <mifosx-current-account-preview-step
[currentAccountProductTemplate]="currentAccountProductTemplate" [currentAccountProductTemplate]="currentAccountProductTemplate"
[currentAccountTemplate]="currentAccountTemplate" [currentAccountTemplate]="currentAccountTemplate"
[currentAccountTermForm]="currentAccountTermForm"
[currentAccountSettingsForm]="currentAccountSettingsForm"
[currentAccountTermsForm]="currentAccountTermsForm" [currentAccountTermsForm]="currentAccountTermsForm"
[currentAccount]="currentAccount" [currentAccount]="currentAccount"
(submitEvent)="submit()" (submitEvent)="submit()"
@@ -13,7 +13,11 @@ import { Router, ActivatedRoute } from '@angular/router';
/** Custom Components */ /** Custom Components */
import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component'; import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component';
import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component'; import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component';
import { CurrentAccountTermStepComponent } from '../current-account-stepper/current-account-term-step/current-account-term-step.component';
import { CurrentAccountSettingsStepComponent } from '../current-account-stepper/current-account-settings-step/current-account-settings-step.component';
import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component'; import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component';
import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component';
import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component';
/** Custom Services */ /** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service'; import { CurrentAccountsService } from '../current-accounts.service';
@@ -39,6 +43,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatStep, MatStep,
MatStepLabel, MatStepLabel,
CurrentAccountDetailsStepComponent, CurrentAccountDetailsStepComponent,
CurrentAccountCurrencyStepComponent,
CurrentAccountTermStepComponent,
CurrentAccountSettingsStepComponent,
CurrentAccountDigitalStepComponent,
CurrentAccountTermsStepComponent, CurrentAccountTermsStepComponent,
CurrentAccountChargesStepComponent, CurrentAccountChargesStepComponent,
CurrentAccountPreviewStepComponent CurrentAccountPreviewStepComponent
@@ -59,21 +67,19 @@ export class CreateCurrentAccountComponent {
/** Current Account Details Step */ /** Current Account Details Step */
@ViewChild(CurrentAccountDetailsStepComponent, { static: true }) @ViewChild(CurrentAccountDetailsStepComponent, { static: true })
currentAccountDetailsStep: CurrentAccountDetailsStepComponent; currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
/** Current Account Terms Step */ /** Current Account Term Step (new) */
@ViewChild(CurrentAccountTermStepComponent, { static: true })
currentAccountTermStep: CurrentAccountTermStepComponent;
/** Current Account Settings Step */
@ViewChild(CurrentAccountSettingsStepComponent, { static: true })
currentAccountSettingsStep: CurrentAccountSettingsStepComponent;
/** Current Account Terms Step (old — kept for currencyCode access) */
@ViewChild(CurrentAccountTermsStepComponent, { static: true }) @ViewChild(CurrentAccountTermsStepComponent, { static: true })
currentAccountTermsStep: CurrentAccountTermsStepComponent; currentAccountTermsStep: CurrentAccountTermsStepComponent;
/** Current Account Charges Step */ /** Current Account Charges Step */
@ViewChild(CurrentAccountChargesStepComponent, { static: true }) @ViewChild(CurrentAccountChargesStepComponent, { static: true })
currentAccountChargesStep: CurrentAccountChargesStepComponent; currentAccountChargesStep: CurrentAccountChargesStepComponent;
/**
* Fetches current account template from `resolve`
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {Dates} dateUtils Date Utils
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {SettingsService} settingsService Settings Service
*/
constructor() { constructor() {
this.route.data.subscribe((data: { currentAccountTemplate: any }) => { this.route.data.subscribe((data: { currentAccountTemplate: any }) => {
this.currentAccountTemplate = data.currentAccountTemplate; this.currentAccountTemplate = data.currentAccountTemplate;
@@ -82,7 +88,6 @@ export class CreateCurrentAccountComponent {
/** /**
* Sets current account product template. * Sets current account product template.
* @param {any} $event API response
*/ */
setTemplate($event: any) { setTemplate($event: any) {
this.currentAccountProductTemplate = $event; this.currentAccountProductTemplate = $event;
@@ -96,7 +101,21 @@ export class CreateCurrentAccountComponent {
} }
/** /**
* Retrieves current account terms form. * Retrieves new term form (interest rate).
*/
get currentAccountTermForm() {
return this.currentAccountTermStep.currentAccountTermForm;
}
/**
* Retrieves settings form.
*/
get currentAccountSettingsForm() {
return this.currentAccountSettingsStep.currentAccountSettingsForm;
}
/**
* Retrieves old terms form — still used for currencyCode by charges step.
*/ */
get currentAccountTermsForm() { get currentAccountTermsForm() {
return this.currentAccountTermsStep.currentAccountTermsForm; return this.currentAccountTermsStep.currentAccountTermsForm;
@@ -106,7 +125,7 @@ export class CreateCurrentAccountComponent {
* Checks validity of overall current account form. * Checks validity of overall current account form.
*/ */
get currentAccountFormValid() { get currentAccountFormValid() {
return this.currentAccountDetailsForm.valid && this.currentAccountTermsForm.valid; return this.currentAccountDetailsForm.valid && this.currentAccountTermForm.valid;
} }
/** /**
@@ -115,16 +134,16 @@ export class CreateCurrentAccountComponent {
get currentAccount() { get currentAccount() {
return { return {
...this.currentAccountDetailsStep.currentAccountDetails, ...this.currentAccountDetailsStep.currentAccountDetails,
...this.currentAccountTermsStep.currentAccountTerms, ...this.currentAccountTermStep.currentAccountTerms,
...this.currentAccountSettingsStep.currentAccountSettings,
...this.currentAccountChargesStep.currentAccountCharges ...this.currentAccountChargesStep.currentAccountCharges
}; };
} }
/** /**
* Creates a new share account. * Creates a new current account.
*/ */
submit() { submit() {
// TODO: Update once language and date settings are setup
const locale = this.settingsService.language.code; const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
@@ -7,7 +7,7 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; import { Component, OnInit, Input, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { UntypedFormControl } from '@angular/forms'; import { UntypedFormControl } from '@angular/forms';
@@ -69,13 +69,27 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FormatNumberPipe FormatNumberPipe
] ]
}) })
export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { export class CurrentAccountChargesStepComponent implements OnInit {
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private translateService = inject(TranslateService); private translateService = inject(TranslateService);
/** Current Account Product Template */ /** Current Account Product Template */
@Input() currentAccountProductTemplate: any; private _currentAccountProductTemplate: any;
@Input()
set currentAccountProductTemplate(value: any) {
this._currentAccountProductTemplate = value;
if (value) {
this.chargeData = value.chargeOptions || [];
// Only pre-populate from product template for new accounts
if (!this.currentAccountTemplate?.id) {
this.chargesDataSource = (value.charges || []).map((charge: any) => ({ ...charge, id: charge.chargeId }));
}
}
}
get currentAccountProductTemplate(): any {
return this._currentAccountProductTemplate;
}
/** Current Account Template */ /** Current Account Template */
@Input() currentAccountTemplate: any; @Input() currentAccountTemplate: any;
/** Currency Code */ /** Currency Code */
@@ -88,8 +102,6 @@ export class CurrentAccountChargesStepComponent implements OnInit, OnChanges {
chargesDataSource: {}[] = []; chargesDataSource: {}[] = [];
/** Component is pristine if there has been no changes by user interaction */ /** Component is pristine if there has been no changes by user interaction */
pristine = true; pristine = true;
/** For Edit Current Account Form */
isChargesPatched = false;
/** Display columns for charges table */ /** Display columns for charges table */
displayedColumns: string[] = [ displayedColumns: string[] = [
'name', 'name',
@@ -114,22 +126,10 @@ export class CurrentAccountChargesStepComponent implements OnInit, OnChanges {
]; ];
ngOnInit() { ngOnInit() {
if (this.currentAccountTemplate) { if (this.currentAccountTemplate?.id && this.currentAccountTemplate?.charges) {
if (!this.isChargesPatched && this.currentAccountTemplate.charges) { // Edit mode: load saved charges from existing account
this.chargesDataSource =
this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
this.isChargesPatched = true;
} else {
this.chargesDataSource = [];
}
}
}
ngOnChanges() {
if (this.currentAccountProductTemplate) {
this.chargeData = this.currentAccountProductTemplate.chargeOptions;
this.chargesDataSource = this.chargesDataSource =
this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; this.currentAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
} }
} }
@@ -0,0 +1,79 @@
<!--
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="sac-section">
<div class="sac-section__header">
<i class="fas fa-coins"></i>
<span>Currency &amp; Account Status Configuration</span>
</div>
<div class="sac-body">
<div class="sac-grid">
<mat-form-field appearance="outline" class="sac-field">
<mat-label>Currency</mat-label>
<input matInput [value]="currentAccountProductTemplate?.currency?.name || ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="sac-field">
<mat-label>Decimal Places</mat-label>
<input matInput [value]="currentAccountProductTemplate?.currency?.decimalPlaces || ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.dormancyAlert">
{{ currentAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }}
</span>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Balance Swap Option</h4>
<p>Allows balance to be swapped or transferred between linked accounts as per product configuration.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.balanceSwapOption">
{{ currentAccountProductTemplate?.balanceSwapOption ? 'Yes' : 'No' }}
</span>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>MSME</h4>
<p>Account is categorized under the Micro, Small and Medium Enterprises segment.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.msme">
{{ currentAccountProductTemplate?.msme ? 'Yes' : 'No' }}
</span>
</div>
<div class="sac-alert">
<div class="sac-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>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.nominationFacility">
{{ currentAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
</span>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic debit instructions for recurring payments linked to this account.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="currentAccountProductTemplate?.autoDebitFacility">
{{ currentAccountProductTemplate?.autoDebitFacility ? 'Yes' : 'No' }}
</span>
</div>
</div>
</div>
@@ -0,0 +1,116 @@
/**
* 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;
.sac-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;
}
span {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.sac-body {
background: #fff;
}
.sac-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sac-field {
width: 100%;
}
.sac-alert {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 18px 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;
}
}
}
.sac-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
padding: 4px 14px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #fee2e2;
color: #b91c1c;
border: 1px solid #fecaca;
&--yes {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
@@ -0,0 +1,20 @@
/**
* 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 } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-currency-step',
templateUrl: './current-account-currency-step.component.html',
styleUrls: ['./current-account-currency-step.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class CurrentAccountCurrencyStepComponent {
@Input() currentAccountProductTemplate: any;
}
@@ -7,63 +7,126 @@
--> -->
<form [formGroup]="currentAccountDetailsForm"> <form [formGroup]="currentAccountDetailsForm">
<div class="layout-row-wrap gap-2px responsive-column"> <!-- Account Details -->
<mat-form-field class="flex-48"> <div class="sad-section">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label> <div class="sad-section__header">
<mat-select formControlName="productId" required> <i class="fas fa-info-circle"></i>
@for (product of productData; track product) { <h3>Account Details</h3>
<mat-option [value]="product.id"> </div>
{{ product.name }} <div class="sad-grid">
</mat-option> <mat-form-field appearance="outline" class="sad-field">
} <mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
</mat-select> <mat-select formControlName="productId" required>
<mat-error> @for (product of productData; track product) {
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} <mat-option [value]="product.id">{{ product.name }}</mat-option>
<strong>{{ 'labels.commons.required' | translate }}</strong> }
</mat-error> </mat-select>
</mat-form-field>
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error> <mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.inputs.Product Name' | 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>
}
@if (currentAccountProductSelected) { @if (currentAccountProductSelected) {
<mat-form-field class="flex-48"> <mat-form-field appearance="outline" class="sad-field" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId"> <input
@for (officer of fieldOfficerData; track officer) { matInput
<mat-option [value]="officer.id"> [min]="minDate"
{{ officer.displayName }} [max]="maxDate"
</mat-option> [matDatepicker]="submittedOnDatePicker"
} formControlName="submittedOnDate"
</mat-select> required
</mat-form-field> />
} <mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
@if (currentAccountProductSelected) { @if (currentAccountProductSelected) {
<mat-form-field class="flex-48"> <mat-form-field appearance="outline" class="sad-field">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<input matInput formControlName="externalId" /> <mat-select formControlName="fieldOfficerId">
</mat-form-field> @for (officer of fieldOfficerData; track officer) {
} <mat-option [value]="officer.id">{{ officer.displayName }}</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
}
</div>
</div> </div>
<!-- Product Classification (read-only, shown after product is selected) -->
@if (currentAccountProductSelected && productTemplate) {
<div class="sad-section sad-section--mt">
<div class="sad-section__header">
<i class="fas fa-tags"></i>
<h3>Product Classification</h3>
<span class="sad-badge">From Product</span>
</div>
<div class="sad-grid sad-grid--classification">
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Account Type</mat-label>
<input matInput [value]="productTemplate.accountType || ''" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Customer Segment</mat-label>
<input matInput [value]="productTemplate.customerSegment || ''" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Product Code</mat-label>
<input matInput [value]="productTemplate.productCode || ''" readonly />
</mat-form-field>
@if (productTemplate.effectiveDate) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Effective Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.effectiveDate)" readonly />
</mat-form-field>
}
@if (productTemplate.effectiveTo) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Effective To</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.effectiveTo)" readonly />
</mat-form-field>
}
@if (productTemplate.endDate) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>End Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.endDate)" readonly />
</mat-form-field>
}
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Introducer Required</mat-label>
<input matInput [value]="productTemplate.introducerRequired ? 'Yes' : 'No'" readonly />
</mat-form-field>
<div class="sad-field sad-info-row">
<span class="sad-info-label">Share Linking Enabled</span>
<span class="sad-badge" [class.sad-badge--yes]="productTemplate.shareLinkingEnabled">
{{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }}
</span>
</div>
</div>
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled> <button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon> <fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
@@ -5,3 +5,101 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.sad-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
&--mt {
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;
flex: 1;
}
}
}
.sad-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sad-grid--classification {
grid-template-columns: repeat(3, 1fr);
@media (width <= 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sad-field {
width: 100%;
}
.sad-info-row {
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
}
.sad-info-label {
font-size: 14px;
color: $gray-900;
font-weight: 500;
}
.sad-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
&--yes {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
@@ -7,8 +7,9 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */ /** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service'; import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
@@ -35,6 +36,8 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService); private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
/** Current Account Template */ /** Current Account Template */
@Input() currentAccountTemplate: any; @Input() currentAccountTemplate: any;
@@ -57,12 +60,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
/** Current Account Template with product data */ /** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter(); @Output() currentAccountProductTemplate = new EventEmitter();
/** /** Local copy of product template for displaying classification info */
* Sets share account details form. productTemplate: any = null;
* @param {FormBuilder} formBuilder Form Builder.
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service.
* @param {SettingsService} settingsService Setting service
*/
constructor() { constructor() {
this.createCurrentAccountDetailsForm(); this.createCurrentAccountDetailsForm();
} }
@@ -82,7 +82,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}); });
} else { } else {
this.currentAccountDetailsForm.patchValue({ this.currentAccountDetailsForm.patchValue({
submittedOnDate: new Date() submittedOnDate: this.settingsService.businessDate
}); });
} }
} }
@@ -115,9 +115,23 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
this.currentAccountsService this.currentAccountsService
.getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false) .getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false)
.subscribe((response: any) => { .subscribe((response: any) => {
// Ensure currency is included in the product template
if (!response.currency) {
// Try to get currency from the selected product
const selectedProduct = this.currentAccountTemplate.productOptions?.find(
(product: any) => product.id == productId
);
if (selectedProduct?.currency) {
response.currency = selectedProduct.currency;
} else if (this.currentAccountTemplate.currency) {
response.currency = this.currentAccountTemplate.currency;
}
}
this.productTemplate = response;
this.currentAccountProductTemplate.emit(response); this.currentAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions; this.fieldOfficerData = response.fieldOfficerOptions;
this.currentAccountProductSelected = true; this.currentAccountProductSelected = true;
this.cdr.detectChanges();
if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) { if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) {
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId); this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId);
this.isFieldOfficerPatched = true; this.isFieldOfficerPatched = true;
@@ -128,6 +142,27 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}); });
} }
/**
* Formats template dates into the current CBS date format.
*/
formatTemplateDate(value: any): string {
if (!value) {
return '';
}
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
} else if (Array.isArray(value) && value.length >= 3) {
dateValue = new Date(value[0], value[1] - 1, value[2]);
} else {
dateValue = new Date(value);
}
if (!dateValue || Number.isNaN(dateValue.getTime())) {
return Array.isArray(value) ? value.join('/') : String(value);
}
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
/** /**
* Returns current account form value. * Returns current account form value.
*/ */
@@ -0,0 +1,234 @@
<!--
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">FROM PRODUCT</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-credit-card"></i> ATM Facility</span>
<select class="spl-select" [ngModel]="accountFacilityConfig.atmFacility" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
</div>
</div>
@if (accountFacilityLimits.length > 0) {
<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" [value]="row.generalLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.minorAccountLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.smallSavingLimit" disabled /></td>
</tr>
}
</tbody>
</table>
</div>
}
@if (depositLimitKyc?.general) {
<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" disabled>
<option>{{ depositLimitKyc.general.kycType }}</option>
</select>
</td>
<td><input type="text" class="spl-input" [value]="depositLimitKyc.general.limit" disabled /></td>
</tr>
<tr>
<td>Minor</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.minor.kycType" disabled>
<option>{{ depositLimitKyc.minor.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.minor.limit" disabled /></td>
</tr>
<tr>
<td>Small Saving</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.smallSaving.kycType" disabled>
<option>{{ depositLimitKyc.smallSaving.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.smallSaving.limit" disabled /></td>
</tr>
</tbody>
</table>
</div>
}
<div class="spl-note">
<strong>Note:</strong>
These facilities are defined at product level and applied to this account. Small Saving Account limits are
strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular.
</div>
</div>
</div>
@@ -0,0 +1,199 @@
/**
* 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;
.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-table-wrap {
overflow-x: auto;
margin-top: 16px;
}
.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;
}
td:first-child {
background: #f8fafc;
font-weight: 600;
color: $text;
white-space: nowrap;
}
td {
font-size: 13px;
overflow: hidden;
}
}
.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: #f8fafc;
color: $muted;
}
.spl-select {
height: 30px;
border: 1px solid $border-strong;
border-radius: 2px;
padding: 0 6px;
background: #f8fafc;
color: $muted;
}
.spl-features {
margin-bottom: 16px;
}
.spl-features__header {
font-size: 13px;
font-weight: 600;
color: $muted;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.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;
font-weight: 600;
color: #334155;
i {
color: #2563eb;
font-size: 12px;
width: 14px;
text-align: center;
}
}
.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;
}
@@ -0,0 +1,69 @@
/**
* 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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
const DEFAULT_FACILITY_CONFIG = {
atmFacility: false,
atmWithdrawalFacility: false,
upiFacility: false,
posTransactionsFacility: false,
chequeBook: false,
impsFacility: false,
smsAlerts: false,
bbpsFacility: false,
insuranceCover: false,
bepgFacility: false,
neftFacility: false,
rtgsFacility: false,
interBankTotalCapFacility: false,
withdrawalFacility: false,
depositFacilityKyc: false,
virtualDebitCard: false
};
@Component({
selector: 'mifosx-current-account-digital-step',
templateUrl: './current-account-digital-step.component.html',
styleUrls: ['./current-account-digital-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule
]
})
export class CurrentAccountDigitalStepComponent {
private _currentAccountProductTemplate: any;
@Input()
set currentAccountProductTemplate(value: any) {
this._currentAccountProductTemplate = value;
}
get currentAccountProductTemplate(): any {
return this._currentAccountProductTemplate;
}
get accountFacilityConfig() {
return this._currentAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG;
}
get accountFacilityLimits(): any[] {
return this._currentAccountProductTemplate?.accountFacilityLimits || [];
}
get depositLimitKyc() {
return (
this._currentAccountProductTemplate?.depositLimitKyc || {
general: { kycType: '', limit: '' },
minor: { kycType: '', limit: '' },
smallSaving: { kycType: '', limit: '' }
}
);
}
}
@@ -6,251 +6,511 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<div class="responsive-column"> <div class="preview-wrapper">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3> <!-- Details -->
<div class="preview-section">
<mat-divider class="flex-fill"></mat-divider> <div class="preview-section__header">
<div class="flex-fill layout-row"> <span class="preview-section__title">{{ 'labels.heading.Details' | translate }}</span>
<span class="flex-40">{{ 'labels.inputs.Product' | translate }}</span>
<span class="flex-60">{{
currentAccount.productId | find: currentAccountTemplate.productOptions : 'id' : 'name'
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Submitted On' | translate }}</span>
<span class="flex-60">{{ currentAccount.submittedOnDate | dateFormat }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span>
@if (currentAccount.externalId) {
<span class="flex-60">
<mifosx-external-identifier externalId="{{ currentAccount.externalId }}"></mifosx-external-identifier>
</span>
}
@if (!currentAccount.externalId) {
<span class="flex-60">
{{ 'labels.inputs.Unassigned' | translate }}
</span>
}
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Field Officer' | translate }}</span>
<span class="flex-60">{{
currentAccount.fieldOfficerId | find: currentAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName'
}}</span>
</div>
@if (activeClientMembers) {
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Client Members' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-60">
<table mat-table #membersTable [dataSource]="dataSource">
<!-- ID Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.id }}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.displayName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="membersDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: membersDisplayedColumns"></tr>
</table>
</div> </div>
} <div class="preview-body">
<div class="preview-grid preview-grid--three">
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Terms' | translate }}</h3> <div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Product' | translate }}</span>
<mat-divider class="flex-fill"></mat-divider> <span class="preview-value">{{
<div class="flex-fill layout-row"> currentAccount?.productId | find: currentAccountTemplate?.productOptions : 'id' : 'name'
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}</span> }}</span>
<span class="flex-60">{{ currentAccountTermsForm.get('currencyCode').value }}</span> </div>
</div> <div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Submitted On' | translate }}</span>
<div class="flex-fill layout-row"> <span class="preview-value">{{ currentAccount?.submittedOnDate | dateFormat }}</span>
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span> </div>
<span class="flex-60">{{ currentAccountTermsForm.get('decimal').value }}</span> <div class="preview-field">
</div> <span class="preview-label">{{ 'labels.inputs.Field Officer' | translate }}</span>
<span class="preview-value">{{
<div class="flex-fill layout-row"> currentAccount?.fieldOfficerId
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span> | find: currentAccountProductTemplate?.fieldOfficerOptions : 'id' : 'displayName'
<span class="flex-60">{{ currentAccount.nominalAnnualInterestRate | formatNumber }} %</span> }}</span>
</div> </div>
<div class="preview-field">
<div class="flex-fill layout-row"> <span class="preview-label">{{ 'labels.inputs.External Id' | translate }}</span>
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span> @if (currentAccount?.externalId) {
<span class="flex-60">{{ <span class="preview-value">
getCatalogTranslation( <mifosx-external-identifier externalId="{{ currentAccount.externalId }}"></mifosx-external-identifier>
currentAccount.interestCompoundingPeriodType </span>
| find: currentAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' } @else {
) <span class="preview-value">{{ 'labels.inputs.Unassigned' | translate }}</span>
}}</span> }
</div> </div>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestPostingPeriodType
| find: currentAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCalculationType
| find: currentAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCalculationDaysInYearType
| find: currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
)
}}</span>
</div>
@if (currentAccount.minRequiredOpeningBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.minRequiredOpeningBalance | formatNumber }}</span>
</div> </div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ currentAccount.withdrawalFeeForTransfers | yesNo }}</span>
</div> </div>
@if (currentAccount.lockinPeriodFrequency) { <!-- Product Classification -->
<div class="flex-fill layout-row"> @if (
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span> currentAccountProductTemplate?.accountType ||
<span class="flex-60">{{ currentAccountProductTemplate?.customerSegment ||
getCatalogTranslation( currentAccountProductTemplate?.productCode ||
currentAccount.lockinPeriodFrequency + currentAccountProductTemplate?.effectiveDate ||
' ' + currentAccountProductTemplate?.effectiveTo ||
(currentAccount.lockinPeriodFrequencyType currentAccountProductTemplate?.endDate ||
| find: currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value') currentAccountProductTemplate?.introducerRequired !== undefined ||
) currentAccountProductTemplate?.shareLinkingEnabled !== undefined
}}</span> ) {
</div> <div class="preview-section">
} <div class="preview-section__header">
<span class="preview-section__title">Product Classification</span>
<div class="flex-fill layout-row"> </div>
<span class="flex-40">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}:</span> <div class="preview-body">
<span class="flex-60">{{ currentAccount.allowOverdraft | yesNo }}</span> <div class="preview-grid preview-grid--three">
</div> @if (currentAccountProductTemplate?.accountType) {
<div class="preview-field">
@if (currentAccount.minOverdraftForInterestCalculation) { <span class="preview-label">Account Type</span>
<div class="flex-fill layout-row"> <span class="preview-value">{{ currentAccountProductTemplate?.accountType }}</span>
<span class="flex-40" </div>
>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}:</span }
> @if (currentAccountProductTemplate?.customerSegment) {
<span class="flex-60">{{ currentAccount.minOverdraftForInterestCalculation | formatNumber }}</span> <div class="preview-field">
</div> <span class="preview-label">Customer Segment</span>
} <span class="preview-value">{{ currentAccountProductTemplate?.customerSegment }}</span>
</div>
@if (currentAccount.nominalAnnualInterestRateOverdraft) { }
<div class="flex-fill layout-row"> @if (currentAccountProductTemplate?.productCode) {
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span> <div class="preview-field">
<span class="flex-60">{{ currentAccount.nominalAnnualInterestRateOverdraft | formatNumber }} %</span> <span class="preview-label">Product Code</span>
</div> <span class="preview-value">{{ currentAccountProductTemplate?.productCode }}</span>
} </div>
}
@if (currentAccount.overdraftLimit) { @if (currentAccountProductTemplate?.effectiveDate) {
<div class="flex-fill layout-row"> <div class="preview-field">
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span> <span class="preview-label">Effective Date</span>
<span class="flex-60">{{ currentAccount.overdraftLimit | formatNumber }}</span> <span class="preview-value">{{ currentAccountProductTemplate?.effectiveDate | dateFormat }}</span>
</div> </div>
} }
@if (currentAccountProductTemplate?.effectiveTo) {
<div class="flex-fill layout-row"> <div class="preview-field">
<span class="flex-40">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}:</span> <span class="preview-label">Effective To</span>
<span class="flex-60">{{ currentAccount.enforceMinRequiredBalance | yesNo }}</span> <span class="preview-value">{{ currentAccountProductTemplate?.effectiveTo | dateFormat }}</span>
</div> </div>
}
@if (currentAccount.minRequiredBalance) { @if (currentAccountProductTemplate?.endDate) {
<div class="flex-fill layout-row"> <div class="preview-field">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span> <span class="preview-label">End Date</span>
<span class="flex-60">{{ currentAccount.minRequiredBalance }}</span> <span class="preview-value">{{ currentAccountProductTemplate?.endDate | dateFormat }}</span>
</div> </div>
} }
@if (
@if (currentAccount.charges.length) { currentAccountProductTemplate?.introducerRequired !== undefined &&
<div class="layout-row-wrap responsive-column flex-fill"> currentAccountProductTemplate?.introducerRequired !== null
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Charges' | translate }}</h3> ) {
<mat-divider class="flex-fill"></mat-divider> <div class="preview-field">
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="currentAccount.charges"> <span class="preview-label">Introducer Required</span>
<ng-container matColumnDef="name"> <span
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th> class="preview-value preview-badge"
<td mat-cell *matCellDef="let charge"> [class.preview-badge--yes]="currentAccountProductTemplate?.introducerRequired"
{{ charge.name + ', ' + charge.currency.displaySymbol }} >
</td> {{ currentAccountProductTemplate?.introducerRequired | yesNo }}
</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="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') {
<span>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span> </span>
} </div>
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') { }
<span> @if (
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} currentAccountProductTemplate?.shareLinkingEnabled !== undefined &&
currentAccountProductTemplate?.shareLinkingEnabled !== null
) {
<div class="preview-field">
<span class="preview-label">Share Linking Enabled</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.shareLinkingEnabled"
>
{{ currentAccountProductTemplate?.shareLinkingEnabled | yesNo }}
</span> </span>
} </div>
@if ( }
!( </div>
charge.chargeTimeType.value === 'Monthly Fee' || </div>
charge.chargeTimeType.value === 'Annual Fee' || </div>
charge.chargeTimeType.value === 'Specified due date' || }
charge.chargeTimeType.value === 'Weekly Fee'
) <!-- Currency -->
) { <div class="preview-section">
<span> N/A </span> <div class="preview-section__header">
} <span class="preview-section__title">{{ 'labels.heading.Currency' | translate }}</span>
</td> </div>
</ng-container> <div class="preview-body">
<ng-container matColumnDef="repaymentsEvery"> <div class="preview-grid preview-grid--three">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th> <div class="preview-field">
<td mat-cell *matCellDef="let charge"> <span class="preview-label">{{ 'labels.inputs.Currency' | translate }}</span>
{{ charge.feeInterval || 'Not Provided' }} <span class="preview-value">{{ currentAccountTermsForm?.get('currencyCode')?.value }}</span>
</td> </div>
</ng-container> <div class="preview-field">
<ng-container matColumnDef="chargeTimeType"> <span class="preview-label">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th> <span class="preview-value">{{ currentAccountProductTemplate?.currency?.decimalPlaces }}</span>
<td mat-cell *matCellDef="let charge"> </div>
{{ charge.chargeTimeType.value }} <div class="preview-field">
</td> <span class="preview-label">{{ 'labels.inputs.Currency In Multiples Of' | translate }}</span>
</ng-container> <span class="preview-value">{{ currentAccountProductTemplate?.currency?.inMultiplesOf }}</span>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr> </div>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr> @if (
</table> currentAccountProductTemplate?.dormancyAlert !== undefined &&
currentAccountProductTemplate?.dormancyAlert !== null
) {
<div class="preview-field">
<span class="preview-label">Dormancy Alert</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.dormancyAlert"
>
{{ currentAccountProductTemplate?.dormancyAlert | yesNo }}
</span>
</div>
}
@if (
currentAccountProductTemplate?.balanceSwapOption !== undefined &&
currentAccountProductTemplate?.balanceSwapOption !== null
) {
<div class="preview-field">
<span class="preview-label">Balance Swap Option</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.balanceSwapOption"
>
{{ currentAccountProductTemplate?.balanceSwapOption | yesNo }}
</span>
</div>
}
@if (currentAccountProductTemplate?.msme !== undefined && currentAccountProductTemplate?.msme !== null) {
<div class="preview-field">
<span class="preview-label">MSME</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="currentAccountProductTemplate?.msme">
{{ currentAccountProductTemplate?.msme | yesNo }}
</span>
</div>
}
@if (
currentAccountProductTemplate?.nominationFacility !== undefined &&
currentAccountProductTemplate?.nominationFacility !== null
) {
<div class="preview-field">
<span class="preview-label">Nomination Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.nominationFacility"
>
{{ currentAccountProductTemplate?.nominationFacility | yesNo }}
</span>
</div>
}
@if (
currentAccountProductTemplate?.autoDebitFacility !== undefined &&
currentAccountProductTemplate?.autoDebitFacility !== null
) {
<div class="preview-field">
<span class="preview-label">Auto Debit Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.autoDebitFacility"
>
{{ currentAccountProductTemplate?.autoDebitFacility | yesNo }}
</span>
</div>
}
</div>
</div>
</div>
<!-- Terms -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Terms' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Interest Posting Frequency</span>
<span class="preview-value">{{
termPreview?.interestPostingPeriodType
| find: currentAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">Auto Interest Post</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="termPreview?.autoPost">
{{ termPreview?.autoPost | yesNo }}
</span>
</div>
@if (termPreview?.frequencyFixDay !== undefined && termPreview?.frequencyFixDay !== null) {
<div class="preview-field">
<span class="preview-label">Frequency Fix Day</span>
<span class="preview-value">{{ termPreview?.frequencyFixDay }} Days</span>
</div>
}
@if (termPreview?.lastInterestDate) {
<div class="preview-field">
<span class="preview-label">Last Interest Date</span>
<span class="preview-value">{{ termPreview?.lastInterestDate | dateFormat }}</span>
</div>
}
</div>
</div>
</div>
<!-- Settings -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Settings' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (
settingsPreview?.minRequiredOpeningBalance !== undefined &&
settingsPreview?.minRequiredOpeningBalance !== null &&
settingsPreview?.minRequiredOpeningBalance !== ''
) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Opening Balance' | translate }}</span>
<span class="preview-value">{{ settingsPreview?.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.enforceMinRequiredBalance"
>
{{ settingsPreview?.enforceMinRequiredBalance | yesNo }}
</span>
</div>
@if (
settingsPreview?.minRequiredBalance !== undefined &&
settingsPreview?.minRequiredBalance !== null &&
settingsPreview?.minRequiredBalance !== ''
) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Balance' | translate }}</span>
<span class="preview-value">{{ settingsPreview?.minRequiredBalance | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Balance Frequency</span>
<span class="preview-value">{{ settingsPreview?.balanceFrequency || 'Unassigned' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.withdrawalFeeForTransfers"
>
{{ settingsPreview?.withdrawalFeeForTransfers | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Enable Lock-in Period</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.enableLockinPeriod || !!settingsPreview?.lockinPeriodFrequency"
>
{{ settingsPreview?.enableLockinPeriod || !!settingsPreview?.lockinPeriodFrequency | yesNo }}
</span>
</div>
@if (settingsPreview?.lockinPeriodFrequency) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Lock-in Period' | translate }}</span>
<span class="preview-value">
{{ settingsPreview?.lockinPeriodFrequency }}
{{
settingsPreview?.lockinPeriodFrequencyType
| find: currentAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}
</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Account Closed Zero Balance In Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.accountClosedZeroBalanceInDayClosing"
>
{{ settingsPreview?.accountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="settingsPreview?.allowOverdraft">
{{ settingsPreview?.allowOverdraft | yesNo }}
</span>
</div>
@if (settingsPreview?.allowOverdraft) {
<div class="preview-field">
<span class="preview-label">Avg Balance Calc Period</span>
<span class="preview-value">{{ settingsPreview?.avgBalanceCalcPeriod || 'Unassigned' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Valid For Period</span>
<span class="preview-value">{{ settingsPreview?.validForPeriod || 'Unassigned' }}</span>
</div>
@if (
settingsPreview?.overdraftLimit !== undefined &&
settingsPreview?.overdraftLimit !== null &&
settingsPreview?.overdraftLimit !== ''
) {
<div class="preview-field">
<span class="preview-label">OD Limit %</span>
<span class="preview-value">{{ settingsPreview?.overdraftLimit | formatNumber }}</span>
</div>
}
@if (
settingsPreview?.nominalAnnualInterestRateOverdraft !== undefined &&
settingsPreview?.nominalAnnualInterestRateOverdraft !== null &&
settingsPreview?.nominalAnnualInterestRateOverdraft !== ''
) {
<div class="preview-field">
<span class="preview-label">Interest on OD %</span>
<span class="preview-value">{{
settingsPreview?.nominalAnnualInterestRateOverdraft | formatNumber
}}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">OD Account Closed Zero Balance In Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="settingsPreview?.odAccountClosedZeroBalanceInDayClosing"
>
{{ settingsPreview?.odAccountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
}
</div>
</div>
</div>
@if (hasDigitalSection) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">Digital</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@for (facility of digitalFacilityLabels; track facility.key) {
<div class="preview-field">
<span class="preview-label">{{ facility.label }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="digitalPreview?.accountFacilityConfig?.[facility.key]"
>
{{ digitalPreview?.accountFacilityConfig?.[facility.key] | yesNo }}
</span>
</div>
}
</div>
@if (digitalPreview?.accountFacilityLimits?.length) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Limit Type</th>
<th>General Limit</th>
<th>Minor Account Limit</th>
<th>Small Saving Limit</th>
</tr>
</thead>
<tbody>
@for (row of digitalPreview.accountFacilityLimits; track row.limitType) {
<tr>
<td>{{ row.limitType }}</td>
<td>{{ row.generalLimit || 'Unassigned' }}</td>
<td>{{ row.minorAccountLimit || 'Unassigned' }}</td>
<td>{{ row.smallSavingLimit || 'Unassigned' }}</td>
</tr>
}
</tbody>
</table>
}
@if (digitalPreview?.depositLimitKyc?.general) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Account Type</th>
<th>KYC Type</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>{{ digitalPreview.depositLimitKyc.general.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.general.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Minor</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Small Saving</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.limit || 'Unassigned' }}</td>
</tr>
</tbody>
</table>
}
</div>
</div>
}
<!-- Charges -->
@if (currentAccount?.charges?.length) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.inputs.Charges' | translate }}</span>
</div>
<div class="preview-body">
<table class="mat-elevation-z1 preview-table" mat-table [dataSource]="currentAccount.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 }}</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (
charge.chargeTimeType?.value === 'Specified due date' || charge.chargeTimeType?.value === 'Weekly Fee'
) {
<span>{{ (charge.dueDate | dateFormat) || 'Unassigned' }}</span>
} @else if (
charge.chargeTimeType?.value === 'Monthly Fee' || charge.chargeTimeType?.value === 'Annual Fee'
) {
<span>{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}</span>
} @else {
<span>N/A</span>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.feeInterval || 'Not Provided' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
</div> </div>
} }
</div> </div>
@@ -6,27 +6,95 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
table { .preview-wrapper {
width: 100%; display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 0.5rem 0;
} }
.mat-elevation-z1 { .preview-section {
margin: 1em 0 1.5em; border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
&__header {
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
padding: 0.6rem 1rem;
}
&__title {
font-size: 0.95rem;
font-weight: 600;
color: #333;
text-transform: uppercase;
letter-spacing: 0.04em;
}
} }
h2, .preview-body {
h3, padding: 1rem 1.25rem;
h4 { }
margin: 0;
.preview-grid {
display: grid;
gap: 0.75rem 1.5rem;
&--three {
grid-template-columns: repeat(3, 1fr);
@media (width <= 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 480px) {
grid-template-columns: 1fr;
}
}
}
.preview-field {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.preview-label {
font-size: 0.72rem;
color: #888;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.preview-value {
font-size: 0.92rem;
color: #222;
font-weight: 500; font-weight: 500;
} }
span { .preview-badge {
margin: 0.5em 0; display: inline-block;
padding: 0.1rem 0.55rem;
border-radius: 12px;
font-size: 0.78rem;
font-weight: 600;
background: #ffebee;
color: #c62828;
width: fit-content;
&--yes {
background: #e8f5e9;
color: #2e7d32;
}
} }
mat-divider { .preview-table {
margin: 0 0 0.5em; width: 100%;
margin-top: 1rem;
} }
.margin-t { .margin-t {
@@ -36,7 +36,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
* Current account preview step * Current account preview step
*/ */
@Component({ @Component({
selector: 'mifosx-current-account-account-preview-step', selector: 'mifosx-current-account-preview-step',
templateUrl: './current-account-preview-step.component.html', templateUrl: './current-account-preview-step.component.html',
styleUrls: ['./current-account-preview-step.component.scss'], styleUrls: ['./current-account-preview-step.component.scss'],
imports: [ imports: [
@@ -62,6 +62,25 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
] ]
}) })
export class CurrentAccountPreviewStepComponent implements OnChanges { export class CurrentAccountPreviewStepComponent implements OnChanges {
readonly digitalFacilityLabels = [
{ key: 'atmFacility', label: 'ATM Facility' },
{ key: 'atmWithdrawalFacility', label: 'ATM Withdrawal' },
{ key: 'upiFacility', label: 'UPI Facility' },
{ key: 'posTransactionsFacility', label: 'POS Transactions' },
{ 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' },
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
];
private translateService = inject(TranslateService); private translateService = inject(TranslateService);
/** Current Account Product Template */ /** Current Account Product Template */
@@ -70,6 +89,10 @@ export class CurrentAccountPreviewStepComponent implements OnChanges {
@Input() currentAccountTemplate: any; @Input() currentAccountTemplate: any;
/** Current Account Terms Form */ /** Current Account Terms Form */
@Input() currentAccountTermsForm: any; @Input() currentAccountTermsForm: any;
/** Current Account Term Form */
@Input() currentAccountTermForm: any;
/** Current Account Settings Form */
@Input() currentAccountSettingsForm: any;
/** Current Account */ /** Current Account */
@Input() currentAccount: any; @Input() currentAccount: any;
@Input() activeClientMembers?: any; @Input() activeClientMembers?: any;
@@ -104,4 +127,24 @@ export class CurrentAccountPreviewStepComponent implements OnChanges {
getCatalogTranslation(text: string): string { getCatalogTranslation(text: string): string {
return this.translateService.instant('labels.catalogs.' + text); return this.translateService.instant('labels.catalogs.' + text);
} }
get termPreview(): any {
return this.currentAccountTermForm?.getRawValue?.() || this.currentAccount || {};
}
get settingsPreview(): any {
return this.currentAccountSettingsForm?.getRawValue?.() || this.currentAccount || {};
}
get digitalPreview(): any {
return this.currentAccountProductTemplate || {};
}
get hasDigitalSection(): boolean {
return !!(
this.digitalPreview?.accountFacilityConfig ||
this.digitalPreview?.accountFacilityLimits?.length ||
this.digitalPreview?.depositLimitKyc
);
}
} }
@@ -0,0 +1,174 @@
<!--
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]="currentAccountSettingsForm">
<!-- Section 1: 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>{{ 'labels.inputs.Minimum Opening Balance' | translate }}</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 (&#8377;) *</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 (freq of periodFrequencyData; track freq.value) {
<mat-option [value]="freq.value">{{ freq.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>{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
</mat-checkbox>
</div>
</div>
</div>
</div>
</div>
<!-- Section 2: 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 (currentAccountSettingsForm.value.enableLockinPeriod) {
<div class="sps-lockin-fields">
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
<mat-label>{{ 'labels.inputs.Frequency' | translate }} *</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>{{ 'labels.inputs.Type' | translate }} *</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>
<!-- Section 3: 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>
</div>
<!-- Section 4: 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 (currentAccountSettingsForm.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 (currentAccountSettingsForm.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" 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"
min="0"
step="0.01"
/>
</mat-form-field>
</div>
</div>
}
</div>
</div>
</div>
</form>
@@ -0,0 +1,255 @@
/**
* 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;
}
span {
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--additional {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
@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,163 @@
/**
* 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 { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-settings-step',
templateUrl: './current-account-settings-step.component.html',
styleUrls: ['./current-account-settings-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatRadioGroup,
MatRadioButton,
FormatNumberPipe
]
})
export class CurrentAccountSettingsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private _currentAccountProductTemplate: any;
@Input()
set currentAccountProductTemplate(value: any) {
this._currentAccountProductTemplate = value;
if (value && this.currentAccountSettingsForm) {
this.applyTemplate(value);
}
}
get currentAccountProductTemplate(): any {
return this._currentAccountProductTemplate;
}
currentAccountSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any[] = [];
periodFrequencyData: any[] = [
{ value: 'Daily' },
{ value: 'Weekly' },
{ value: 'Monthly' },
{ value: 'Quarterly' },
{ value: 'Half Yearly' },
{ value: 'Yearly' }];
constructor() {
this.createForm();
this.buildDependencies();
}
createForm() {
this.currentAccountSettingsForm = this.formBuilder.group({
minRequiredOpeningBalance: [
'',
[Validators.min(0)]
],
withdrawalFeeForTransfers: [false],
enforceMinRequiredBalance: [false],
minRequiredBalance: [
'',
[Validators.min(0)]
],
balanceFrequency: ['Monthly'],
enableLockinPeriod: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
accountClosedZeroBalanceInDayClosing: [false],
allowOverdraft: [false],
odAccountClosedZeroBalanceInDayClosing: [false],
avgBalanceCalcPeriod: ['6M'],
validForPeriod: ['3M'],
overdraftLimit: [
'',
[Validators.min(0)]
],
nominalAnnualInterestRateOverdraft: [
'',
[Validators.min(0)]
]
});
}
ngOnInit() {
if (this._currentAccountProductTemplate) {
this.applyTemplate(this._currentAccountProductTemplate);
}
}
private applyTemplate(t: any) {
this.lockinPeriodFrequencyTypeData = t.lockinPeriodFrequencyTypeOptions || [];
const hasLockin = !!(t.lockinPeriodFrequency && t.lockinPeriodFrequency > 0);
this.currentAccountSettingsForm.patchValue({
minRequiredOpeningBalance: t.minRequiredOpeningBalance,
withdrawalFeeForTransfers: t.withdrawalFeeForTransfers || false,
enforceMinRequiredBalance: t.enforceMinRequiredBalance || false,
minRequiredBalance: t.minRequiredBalance,
balanceFrequency: t.balanceFrequency || 'Monthly',
enableLockinPeriod: hasLockin,
lockinPeriodFrequency: hasLockin ? t.lockinPeriodFrequency : '',
lockinPeriodFrequencyType: hasLockin ? t.lockinPeriodFrequencyType?.id || '' : '',
accountClosedZeroBalanceInDayClosing: t.accountClosedZeroBalanceInDayClosing || false,
allowOverdraft: t.allowOverdraft || false,
odAccountClosedZeroBalanceInDayClosing: t.odAccountClosedZeroBalanceInDayClosing || false,
avgBalanceCalcPeriod: t.avgBalanceCalcPeriod || '6M',
validForPeriod: t.validForPeriod || '3M',
overdraftLimit: t.overdraftLimit || '',
nominalAnnualInterestRateOverdraft: t.nominalAnnualInterestRateOverdraft || ''
});
}
buildDependencies() {
this.currentAccountSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enabled: boolean) => {
const freq = this.currentAccountSettingsForm.get('lockinPeriodFrequency');
const type = this.currentAccountSettingsForm.get('lockinPeriodFrequencyType');
if (enabled) {
freq.setValidators([
Validators.required,
Validators.min(1)
]);
type.setValidators([Validators.required]);
} else {
freq.setValue('');
type.setValue('');
freq.clearValidators();
type.clearValidators();
}
freq.updateValueAndValidity();
type.updateValueAndValidity();
});
}
get currentAccountSettings() {
const value = { ...this.currentAccountSettingsForm.getRawValue() };
if (!value.enableLockinPeriod) {
value.lockinPeriodFrequency = null;
value.lockinPeriodFrequencyType = null;
}
delete value.enableLockinPeriod;
delete value.balanceFrequency;
if (!value.allowOverdraft) {
delete value.overdraftLimit;
delete value.nominalAnnualInterestRateOverdraft;
delete value.avgBalanceCalcPeriod;
delete value.validForPeriod;
delete value.odAccountClosedZeroBalanceInDayClosing;
}
return value;
}
}
@@ -0,0 +1,71 @@
<!--
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]="currentAccountTermForm">
<div class="sat-section">
<div class="sat-section__header">
<span class="sat-section__badge"><i class="fas fa-chart-line"></i></span>
<span>Interest On OD</span>
</div>
<div class="sat-body">
<!-- Interest Posting Frequency -->
<mat-form-field appearance="outline" class="sat-field">
<mat-label>Interest Posting Frequency</mat-label>
<mat-select formControlName="interestPostingPeriodType" required>
@for (opt of interestPostingPeriodTypeData; track opt) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>Interest Posting Frequency is <strong>required</strong></mat-error>
</mat-form-field>
<!-- Footer: Auto Post + Frequency Fix Day + Last Interest Date -->
<div class="sat-footer">
<div class="sat-footer__grid">
<div class="sat-toggle-card">
<div>
<h4>Auto Interest Post</h4>
</div>
<div class="sat-toggle-card__choices">
@for (option of yesNoOptions; track option.value) {
<label class="sat-radio-label">
<input type="radio" formControlName="autoPost" [value]="option.value" />
<span>{{ option.label }}</span>
</label>
}
</div>
</div>
<mat-form-field appearance="outline" class="sat-field">
<mat-label>Frequency (Fix Day)</mat-label>
<input type="number" matInput formControlName="frequencyFixDay" min="0" />
<mat-hint>Days</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="sat-field" (click)="lastInterestDatePicker.open()">
<mat-label>Last Interest Date</mat-label>
<input
matInput
[matDatepicker]="lastInterestDatePicker"
formControlName="lastInterestDate"
[min]="minDate"
[max]="maxDate"
/>
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
</div>
<div class="sat-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,166 @@
/**
* 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;
.sat-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;
span {
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.sat-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.sat-body {
background: #fff;
padding: 18px 24px 24px;
}
.sat-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
margin-bottom: 18px;
border-radius: 8px;
background: #eff6ff;
border: 1px solid #bfdbfe;
font-size: 13px;
color: $gray-900;
&__icon {
font-size: 16px;
}
}
.sat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px 16px;
@media (width <= 900px) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sat-field {
width: 100%;
}
.sat-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid $border;
&__grid {
display: grid;
grid-template-columns: auto 1fr 1fr;
gap: 4px 16px;
align-items: start;
@media (width <= 900px) {
grid-template-columns: 1fr 1fr;
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
}
.sat-toggle-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 14px;
border: 1px solid $border;
border-radius: 8px;
background: $gray-50;
min-height: 56px;
h4 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.04em;
}
&__choices {
display: flex;
gap: 16px;
align-items: center;
}
}
.sat-radio-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: $gray-900;
cursor: pointer;
[type='radio'] {
accent-color: $accent;
width: 16px;
height: 16px;
cursor: pointer;
}
}
.sat-note {
display: flex;
align-items: flex-start;
gap: 8px;
margin-top: 12px;
padding: 10px 14px;
border-radius: 8px;
background: #fffbeb;
border: 1px solid #fde68a;
font-size: 13px;
color: #92400e;
}
@@ -0,0 +1,102 @@
/**
* 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 { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
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-account-term-step',
templateUrl: './current-account-term-step.component.html',
styleUrls: ['./current-account-term-step.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class CurrentAccountTermStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private _currentAccountProductTemplate: any;
@Input()
set currentAccountProductTemplate(value: any) {
this._currentAccountProductTemplate = value;
if (value && this.currentAccountTermForm) {
this.applyTemplate(value);
}
}
get currentAccountProductTemplate(): any {
return this._currentAccountProductTemplate;
}
currentAccountTermForm: UntypedFormGroup;
interestPostingPeriodTypeData: any[] = [];
yesNoOptions = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
minDate = new Date(2000, 0, 1);
maxDate = new Date();
constructor() {
this.createForm();
}
createForm() {
this.currentAccountTermForm = this.formBuilder.group({
currencyCode: [{ value: '', disabled: true }],
interestPostingPeriodType: [
'',
Validators.required
],
autoPost: [true],
frequencyFixDay: [
30,
[Validators.min(0)]
],
lastInterestDate: ['']
});
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
if (this._currentAccountProductTemplate) {
this.applyTemplate(this._currentAccountProductTemplate);
}
}
private applyTemplate(t: any) {
this.maxDate = this.settingsService.businessDate;
this.interestPostingPeriodTypeData = t.interestPostingPeriodTypeOptions || [];
this.currentAccountTermForm.patchValue({
currencyCode: t.currency?.code,
interestPostingPeriodType: t.interestPostingPeriodType?.id,
autoPost: t.autoPost ?? true,
frequencyFixDay: t.frequencyFixDay ?? 30,
lastInterestDate: t.lastInterestDate ? new Date(t.lastInterestDate) : ''
});
}
get currentAccountTerms() {
const value = { ...this.currentAccountTermForm.getRawValue() };
const dateFormat = this.settingsService.dateFormat;
if (value.lastInterestDate instanceof Date) {
value.lastInterestDate = this.dateUtils.formatDate(value.lastInterestDate, dateFormat) || '';
}
delete value.currencyCode;
return value;
}
}
@@ -93,6 +93,7 @@
[recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate" [recurringDepositsAccountTemplate]="recurringDepositsAccountTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData" [recurringDepositAccountData]="recurringDepositAccountData"
[rdInterestRates]="rdInterestRates"
(submitEvent)="submit()" (submitEvent)="submit()"
> >
</mifosx-recurring-deposits-account-preview-step> </mifosx-recurring-deposits-account-preview-step>
@@ -94,6 +94,7 @@
[recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate" [recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate"
[recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate"
[recurringDepositAccountData]="recurringDepositAccountData" [recurringDepositAccountData]="recurringDepositAccountData"
[rdInterestRates]="rdInterestRates"
(submitEvent)="submit()" (submitEvent)="submit()"
> >
</mifosx-recurring-deposits-account-preview-step> </mifosx-recurring-deposits-account-preview-step>
@@ -79,7 +79,7 @@ export class RecurringDepositsAccountChargesStepComponent implements OnInit, OnC
@Input() recurringDepositAccountFormValid: boolean; @Input() recurringDepositAccountFormValid: boolean;
/** Charges Data */ /** Charges Data */
chargeData: any; chargeData: any[] = [];
/** Charges Data Source */ /** Charges Data Source */
chargesDataSource: {}[] = []; chargesDataSource: {}[] = [];
/** Charges table columns */ /** Charges table columns */
@@ -102,8 +102,10 @@ export class RecurringDepositsAccountChargesStepComponent implements OnInit, OnC
ngOnInit() { ngOnInit() {
this.chargesDataSource = []; this.chargesDataSource = [];
if (this.recurringDepositsAccountTemplate.id && this.recurringDepositsAccountTemplate.charges) { if (this.recurringDepositsAccountTemplate.id && this.recurringDepositsAccountTemplate.charges) {
// Edit mode: load saved charges from existing account
this.chargesDataSource = this.chargesDataSource =
this.recurringDepositsAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; this.recurringDepositsAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
this.isChargesPatched = true;
} }
} }
@@ -120,8 +122,17 @@ export class RecurringDepositsAccountChargesStepComponent implements OnInit, OnC
} }
if (this.recurringDepositsAccountProductTemplate) { if (this.recurringDepositsAccountProductTemplate) {
this.chargeData = this.recurringDepositsAccountProductTemplate.chargeOptions.filter( this.chargeData = this.recurringDepositsAccountProductTemplate.chargeOptions.filter(
(c: Charge) => c.currency.code === this.currency.code (c: Charge) => c.currency.code === this.currency?.code
); );
// New account: pre-populate charges from product template
if (!this.isChargesPatched && !this.recurringDepositsAccountTemplate?.id) {
const productCharges = this.recurringDepositsAccountProductTemplate.charges;
if (Array.isArray(productCharges) && productCharges.length > 0) {
this.chargesDataSource = productCharges.map((charge: any) => ({ ...charge, id: charge.chargeId }));
}
this.isChargesPatched = true;
}
} }
} }
@@ -7,80 +7,207 @@
--> -->
<form [formGroup]="recurringDepositAccountDetailsForm"> <form [formGroup]="recurringDepositAccountDetailsForm">
<div class="layout-row-wrap gap-2px responsive-column"> <!-- ===================== Account Details ===================== -->
<mat-form-field class="flex-48"> <div class="rad-section">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label> <div class="rad-section__header">
<mat-select formControlName="productId" required> <i class="fas fa-info-circle"></i>
@for (product of productData; track product) { <h3>Account Details</h3>
<mat-option [value]="product.id"> </div>
{{ product.name }} <div class="rad-grid">
</mat-option> <mat-form-field appearance="outline" class="rad-field">
} <mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
</mat-select> <mat-select formControlName="productId" required>
<mat-error> @for (product of productData; track product) {
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} <mat-option [value]="product.id">{{ product.name }}</mat-option>
<strong>{{ 'labels.commons.required' | translate }}</strong> }
</mat-error> </mat-select>
</mat-form-field>
@if (isProductSelected) {
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error> <mat-error>
{{ 'labels.inputs.Submission Date' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.inputs.Product Name' | 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>
}
@if (isProductSelected) { @if (isProductSelected) {
<mat-form-field class="flex-48"> <mat-form-field appearance="outline" class="rad-field" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId"> <input
@for (officer of fieldOfficerData; track officer) { matInput
<mat-option [value]="officer.id"> [min]="minDate"
{{ officer.displayName }} [max]="maxDate"
</mat-option> [matDatepicker]="submittedOnDatePicker"
} formControlName="submittedOnDate"
</mat-select> required
</mat-form-field> />
} <mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (isProductSelected) { <mat-form-field appearance="outline" class="rad-field">
<mat-form-field class="flex-48"> <mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<mat-label>{{ 'labels.inputs.External id' | translate }}</mat-label> <mat-select formControlName="fieldOfficerId">
<input @for (officer of fieldOfficerData; track officer) {
matInput <mat-option [value]="officer.id">{{ officer.displayName }}</mat-option>
formControlName="externalId" }
matTooltip="{{ 'tooltips.Provides an external id' | translate }}" </mat-select>
/> </mat-form-field>
</mat-form-field>
} <mat-form-field appearance="outline" class="rad-field">
<mat-label>{{ 'labels.inputs.External id' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
}
</div>
</div> </div>
@if (isProductSelected && productTemplate) {
<!-- ===================== Product Classification ===================== -->
<div class="rad-section rad-section--mt">
<div class="rad-section__header">
<i class="fas fa-tags"></i>
<h3>Product Classification</h3>
<span class="rad-badge">From Product</span>
</div>
<div class="rad-grid rad-grid--classification">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Account Type</mat-label>
<input matInput [value]="productTemplate.accountType || ''" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Customer Segment</mat-label>
<input matInput [value]="productTemplate.customerSegment || ''" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Product Code</mat-label>
<input matInput [value]="productTemplate.productCode || ''" readonly />
</mat-form-field>
@if (productTemplate.effectiveDate) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Effective Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.effectiveDate)" readonly />
</mat-form-field>
}
@if (productTemplate.endDate) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>End Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.endDate)" readonly />
</mat-form-field>
}
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Introducer Required</mat-label>
<input matInput [value]="productTemplate.introducerRequired ? 'Yes' : 'No'" readonly />
</mat-form-field>
<div class="rad-field rad-info-row">
<span class="rad-info-label">Share Linking Enabled</span>
<span class="rad-badge" [class.rad-badge--yes]="productTemplate.shareLinkingEnabled">
{{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }}
</span>
</div>
</div>
</div>
<!-- ===================== Currency & Account Status Configuration ===================== -->
<div class="rad-section rad-section--mt">
<div class="rad-section__header">
<i class="fas fa-coins"></i>
<h3>Currency &amp; Account Status Configuration</h3>
<span class="rad-badge">From Product</span>
</div>
<div class="rad-body">
@if (productTemplate.currency) {
<div class="rad-grid">
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Currency</mat-label>
<input matInput [value]="productTemplate.currency.name" readonly />
<mat-hint>Default: Indian Rupee - auto-set for domestic products</mat-hint>
</mat-form-field>
@if (
productTemplate.currency.decimalPlaces !== undefined && productTemplate.currency.decimalPlaces !== null
) {
<mat-form-field appearance="outline" class="rad-field">
<mat-label>Decimal Places</mat-label>
<input matInput [value]="productTemplate.currency.decimalPlaces" readonly />
</mat-form-field>
}
</div>
}
<div class="rad-alert">
<div class="rad-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.dormancyAlert === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.dormancyAlert === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-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>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.balanceSwapOption === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.balanceSwapOption === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.msme === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.msme === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-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>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.nominationFacility === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.nominationFacility === false">no</span>
</div>
</div>
<div class="rad-alert">
<div class="rad-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<div class="rad-alert__choices">
<span class="rad-yn" [class.rad-yn--active]="productTemplate.autoDebitFacility === true">yes</span>
<span class="rad-yn" [class.rad-yn--active]="productTemplate.autoDebitFacility === false">no</span>
</div>
</div>
</div>
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled> <button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon> <fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }} {{ 'labels.buttons.Previous' | translate }}
</button> </button>
<button <button mat-raised-button matStepperNext [disabled]="recurringDepositAccountDetailsForm.invalid">
mat-raised-button
matStepperNext
[disabled]="
!recurringDepositAccountDetailsForm.get('productId')?.value ||
!recurringDepositAccountDetailsForm.get('submittedOnDate')?.value
"
>
{{ 'labels.buttons.Next' | translate }} {{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon> <fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button> </button>
@@ -6,6 +6,180 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
.margin-t { $accent: #0ea5e9;
margin-top: 1em; $border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
// ── Section card ───────────────────────────────────────────────
.rad-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
&--mt {
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;
flex: 1;
}
}
}
// ── 2-col grid (Account Details inputs) ───────────────────────
.rad-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
// ── 3-col grid (Product Classification) ───────────────────────
.rad-grid--classification {
grid-template-columns: repeat(3, 1fr);
@media (width <= 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.rad-field {
width: 100%;
}
// ── Share Linking badge row ────────────────────────────────────
.rad-info-row {
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
}
.rad-info-label {
font-size: 14px;
color: $gray-900;
font-weight: 500;
}
.rad-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
&--yes {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
// ── Currency section body ──────────────────────────────────────
.rad-body {
background: #fff;
}
// ── Yes / No alert rows (same as rdpc-alert) ──────────────────
.rad-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;
}
}
// ── yes / no indicator pills ───────────────────────────────────
.rad-yn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #94a3b8;
cursor: default;
&::before {
content: '';
display: inline-block;
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #cbd5e1;
background: #fff;
flex-shrink: 0;
}
&--active {
color: $gray-900;
font-weight: 500;
&::before {
border-color: #22c55e;
background: radial-gradient(circle, #22c55e 45%, #fff 46%);
}
}
} }
@@ -7,9 +7,10 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */ /** Custom Services */
import { RecurringDepositsService } from '../../recurring-deposits.service'; import { RecurringDepositsService } from '../../recurring-deposits.service';
@@ -37,6 +38,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private recurringDepositsService = inject(RecurringDepositsService); private recurringDepositsService = inject(RecurringDepositsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
/** Recurring Deposits Account Template */ /** Recurring Deposits Account Template */
@Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountTemplate: any;
@@ -52,19 +55,16 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
/** For edit recurring deposits form */ /** For edit recurring deposits form */
isFieldOfficerPatched = false; isFieldOfficerPatched = false;
/** Recurring Deposits Account Details Form */ /** Recurring Deposits Account Details Form */
recurringDepositAccountDetailsForm: UntypedFormGroup; recurringDepositAccountDetailsForm!: UntypedFormGroup;
isProductSelected = false; isProductSelected = false;
/** Recurring Deposits Account Template with product data */ /** Recurring Deposits Account Template with product data */
@Output() recurringDepositsAccountProductTemplate = new EventEmitter(); @Output() recurringDepositsAccountProductTemplate = new EventEmitter();
/** /** Local copy of product template for displaying classification info */
* Sets recurring deposits account details form. productTemplate: any = null;
* @param {FormBuilder} formBuilder Form Builder.
* @param {RecurringDepositsService} recurringDepositsService Recurring Deposits Service.
* @param {SettingsService} settingsService Settings Service
*/
constructor() { constructor() {
this.createRecurringDepositsAccountDetailsForm(); this.createRecurringDepositsAccountDetailsForm();
} }
@@ -75,6 +75,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
if (this.recurringDepositsAccountTemplate) { if (this.recurringDepositsAccountTemplate) {
this.productData = this.recurringDepositsAccountTemplate.productOptions; this.productData = this.recurringDepositsAccountTemplate.productOptions;
if (this.recurringDepositsAccountTemplate.depositProductId) { if (this.recurringDepositsAccountTemplate.depositProductId) {
// Edit mode: patch from saved data
this.recurringDepositAccountDetailsForm.patchValue({ this.recurringDepositAccountDetailsForm.patchValue({
productId: this.recurringDepositsAccountTemplate.depositProductId, productId: this.recurringDepositsAccountTemplate.depositProductId,
submittedOnDate: submittedOnDate:
@@ -82,6 +83,11 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate), new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate),
externalId: this.recurringDepositsAccountTemplate.externalId externalId: this.recurringDepositsAccountTemplate.externalId
}); });
} else {
// New account: default submittedOnDate to today's business date
this.recurringDepositAccountDetailsForm.patchValue({
submittedOnDate: this.settingsService.businessDate
});
} }
} }
} }
@@ -109,25 +115,72 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
*/ */
buildDependencies() { buildDependencies() {
const clientId = this.recurringDepositsAccountTemplate.clientId; const clientId = this.recurringDepositsAccountTemplate.clientId;
this.recurringDepositAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => { this.recurringDepositAccountDetailsForm.get('productId')!.valueChanges.subscribe((productId: string) => {
this.recurringDepositsService this.recurringDepositsService
.getRecurringDepositsAccountTemplate(clientId, productId) .getRecurringDepositsAccountTemplate(clientId, productId)
.subscribe((response: any) => { .subscribe((response: any) => {
// Ensure currency is included in the product template
if (!response.currency) {
// Try to get currency from the selected product
const selectedProduct = this.recurringDepositsAccountTemplate.productOptions?.find(
(product: any) => product.id == productId
);
if (selectedProduct?.currency) {
response.currency = selectedProduct.currency;
} else if (this.recurringDepositsAccountTemplate.currency) {
response.currency = this.recurringDepositsAccountTemplate.currency;
}
}
this.productTemplate = response;
this.recurringDepositsAccountProductTemplate.emit(response); this.recurringDepositsAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions; this.fieldOfficerData = response.fieldOfficerOptions;
this.isProductSelected = true; this.isProductSelected = true;
this.cdr.detectChanges();
if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) { if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) {
this.recurringDepositAccountDetailsForm this.recurringDepositAccountDetailsForm
.get('fieldOfficerId') .get('fieldOfficerId')!
.patchValue(this.recurringDepositsAccountTemplate.fieldOfficerId); .patchValue(this.recurringDepositsAccountTemplate.fieldOfficerId);
this.isFieldOfficerPatched = true; this.isFieldOfficerPatched = true;
} else { } else {
this.recurringDepositAccountDetailsForm.get('fieldOfficerId').patchValue(''); this.recurringDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue('');
} }
}); });
}); });
} }
/**
* Returns true if a value is not null, undefined, or empty string.
*/
hasValue(val: any): boolean {
return val !== null && val !== undefined && val !== '';
}
/**
* Formats template dates into the current CBS date format.
* Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects.
*/
formatTemplateDate(value: any): string {
if (!value) {
return '';
}
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
} else if (Array.isArray(value) && value.length >= 3) {
dateValue = new Date(value[0], value[1] - 1, value[2]);
} else {
dateValue = new Date(value);
}
if (!dateValue || Number.isNaN(dateValue.getTime())) {
return Array.isArray(value) ? value.join('/') : String(value);
}
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
/** /**
* Returns recurring deposits account details form value. * Returns recurring deposits account details form value.
*/ */
@@ -49,30 +49,30 @@
<!-- ── Legacy chart (shown only when no custom rate chart available) ── --> <!-- ── Legacy chart (shown only when no custom rate chart available) ── -->
@if (!_activeRdVersion) { @if (!_activeRdVersion) {
<div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart.name"> <div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart?.name">
<span class="flex-40">{{ 'labels.inputs.name' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.name' | translate }}:</span>
<span class="flex-60">{{ recurringDepositsAccountProductTemplate.accountChart.name }}</span> <span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart?.name }}</span>
</div> </div>
<div class="flex-fill"> <div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Valid from Date' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Valid from Date' | translate }}:</span>
<span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }}</span> <span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart?.fromDate | dateFormat }}</span>
</div> </div>
<div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart.endDate"> <div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart?.endDate">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }}</span> <span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart?.endDate | dateFormat }}</span>
</div> </div>
<div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart.description"> <div class="flex-fill" *ngIf="recurringDepositsAccountProductTemplate?.accountChart?.description">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart.description }}</span> <span class="flex-60">{{ recurringDepositsAccountProductTemplate?.accountChart?.description }}</span>
</div> </div>
<div class="flex-fill"> <div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}:</span>
<span class="flex-60">{{ <span class="flex-60">{{
recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo recurringDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount | yesNo
}}</span> }}</span>
</div> </div>
} }
@@ -120,7 +120,7 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O
ngOnChanges() { ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) { if (this.recurringDepositsAccountProductTemplate) {
this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart.chartSlabs; this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
} }
} }
@@ -6,35 +6,120 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
table { // ── Wrapper ──────────────────────────────────────────────────────────────────
width: 100%; .preview-wrapper {
display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 0.5rem 0;
} }
.mat-elevation-z1 { // ── Section card ─────────────────────────────────────────────────────────────
margin: 1em 0 1.5em; .preview-section {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
&__header {
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
padding: 0.6rem 1rem;
}
&__title {
font-size: 0.95rem;
font-weight: 600;
color: #333;
text-transform: uppercase;
letter-spacing: 0.04em;
}
} }
h2, // ── Body ─────────────────────────────────────────────────────────────────────
h3, .preview-body {
h4 { padding: 1rem 1.25rem;
margin: 0; }
// ── Subsection (inside Settings) ─────────────────────────────────────────────
.preview-subsection {
margin-bottom: 1rem;
&__title {
font-size: 0.82rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #666;
margin-bottom: 0.6rem;
}
}
.preview-divider {
margin: 0.75rem 0 1rem;
}
// ── Grid ─────────────────────────────────────────────────────────────────────
.preview-grid {
display: grid;
gap: 0.75rem 1.5rem;
&--three {
grid-template-columns: repeat(3, 1fr);
@media (width <= 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 480px) {
grid-template-columns: 1fr;
}
}
}
// ── Field ─────────────────────────────────────────────────────────────────────
.preview-field {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.preview-label {
font-size: 0.72rem;
color: #888;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.preview-value {
font-size: 0.92rem;
color: #222;
font-weight: 500; font-weight: 500;
} }
span { // ── Yes/No badge ─────────────────────────────────────────────────────────────
margin: 0.5em 0; .preview-badge {
display: inline-block;
padding: 0.1rem 0.55rem;
border-radius: 12px;
font-size: 0.78rem;
font-weight: 600;
background: #ffebee;
color: #c62828;
width: fit-content;
&--yes {
background: #e8f5e9;
color: #2e7d32;
}
} }
mat-divider { // ── Tables ───────────────────────────────────────────────────────────────────
margin: 0 0 0.5em; .preview-table {
} width: 100%;
margin-top: 1rem;
.margin-t {
margin-top: 1em;
}
.margin-b {
margin-bottom: 0.25em;
} }
.irc-table { .irc-table {
@@ -56,3 +141,78 @@ mat-divider {
} }
} }
} }
.margin-t {
margin-top: 1em;
}
// ── Product feature flag rows (same style as details step) ────────────────────
.preview-feature-list {
margin-top: 1rem;
border-top: 1px solid #e0e0e0;
}
.preview-feature-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #f0f0f0;
gap: 1rem;
}
.preview-feature-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
strong {
font-size: 0.88rem;
font-weight: 600;
color: #222;
}
span {
font-size: 0.78rem;
color: #26a69a;
margin: 0;
}
}
.preview-feature-choices {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.rad-yn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2rem 0.75rem;
border-radius: 20px;
border: 2px solid #ccc;
font-size: 0.82rem;
color: #999;
cursor: default;
&::before {
content: '';
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid #ccc;
display: inline-block;
}
&--active {
border-color: #4caf50;
color: #2e7d32;
font-weight: 600;
&::before {
border-color: #4caf50;
background: #4caf50;
}
}
}
@@ -73,9 +73,25 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
@Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountTemplate: any;
@Input() recurringDepositsAccountProductTemplate: any; @Input() recurringDepositsAccountProductTemplate: any;
@Input() recurringDepositAccountData: any; @Input() recurringDepositAccountData: any;
@Input() set rdInterestRates(rates: any[]) {
this._rdInterestRates = rates || [];
this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null;
}
/** Output the submit action */ /** Output the submit action */
@Output() submitEvent = new EventEmitter(); @Output() submitEvent = new EventEmitter();
_rdInterestRates: any[] = [];
_activeRdVersion: any = null;
readonly rdSlabColumns = [
'srNo',
'description',
'fromPeriod',
'toPeriod',
'periodTypeLabel',
'annualInterestRate'
];
/** Charges Displayed Columns */ /** Charges Displayed Columns */
chargesDisplayedColumns: string[] = [ chargesDisplayedColumns: string[] = [
'name', 'name',
@@ -113,7 +129,7 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges {
ngOnChanges() { ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) { if (this.recurringDepositsAccountProductTemplate) {
this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart.chartSlabs; this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? [];
} }
} }
} }
@@ -7,219 +7,145 @@
--> -->
<form [formGroup]="recurringDepositAccountSettingsForm"> <form [formGroup]="recurringDepositAccountSettingsForm">
<div class="layout-row-wrap gap-2px responsive-column"> <!-- ===================== Premature Option ===================== -->
<mat-checkbox labelPosition="before" formControlName="isMandatoryDeposit" class="margin-v flex-48"> <div class="rdps-section">
{{ 'labels.inputs.Is Mandatory Deposit' | translate }}? <div class="rdps-section__header">
</mat-checkbox> <span class="rdps-section__badge"><i class="fas fa-balance-scale"></i></span>
<span>Premature option</span>
<mat-checkbox class="flex-48 margin-v" labelPosition="before" formControlName="adjustAdvanceTowardsFuturePayments"> </div>
{{ 'labels.inputs.Adjust advance payments toward future installments' | translate }}? <div class="rdps-body">
</mat-checkbox> <div class="rdps-premature-row">
<mat-checkbox formControlName="loanFacility">Loan Facility</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="allowWithdrawal" class="margin-v flex-48"> <mat-checkbox formControlName="prematureWithdrawalOption">Premature Withdrawal option</mat-checkbox>
{{ 'labels.inputs.Allow Withdrawals' | translate }}? <mat-checkbox formControlName="tenureFlexibility">Tenure Flexibility</mat-checkbox>
</mat-checkbox> <mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
<mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Lock-in Period' | translate }}</h4> </div>
</div>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="lockinPeriodFrequency" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Recurring Deposit Details' | translate }}</h4>
@if (currency) {
<mifosx-input-amount
[currency]="currency"
[isRequired]="true"
class="flex-48"
[inputFormControl]="recurringDepositAccountSettingsForm.controls.mandatoryRecommendedDepositAmount"
[inputLabel]="'Recurring Deposit Amount'"
>
</mifosx-input-amount>
}
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Deposit Period' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Deposit Period' | translate }}</mat-label>
<input type="number" matInput formControlName="depositPeriod" required />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="depositPeriodFrequencyId" required>
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-checkbox labelPosition="before" formControlName="isCalendarInherited" class="margin-v flex-48">
{{ 'labels.inputs.Deposit Frequency Same as Group/Center meeting' | translate }}
</mat-checkbox>
@if (!recurringDepositAccountSettingsForm.value.isCalendarInherited) {
<mat-form-field class="flex-48" (click)="expectedFirstDepositOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Deposit Start Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="expectedFirstDepositOnDatePicker"
formControlName="expectedFirstDepositOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="expectedFirstDepositOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #expectedFirstDepositOnDatePicker></mat-datepicker>
</mat-form-field>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Deposit Frequency' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Deposit Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="recurringFrequency" required />
<mat-error>
{{ 'labels.inputs.Deposit Frequency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="recurringFrequencyType" required>
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Deposit Frequency Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
<mat-divider class="flex-98"></mat-divider>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Minimum Deposit Term' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="minDepositTerm" 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 class="flex-48">
<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 | translateKey: 'catalogs' }}
</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>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.And thereafter, in Multiples of' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="inMultiplesOfDepositTerm" />
</mat-form-field>
<mat-form-field class="flex-48">
<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 | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Maximum Deposit Term' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="maxDepositTerm" />
</mat-form-field>
<mat-form-field class="flex-48">
<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 | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-divider class="flex-98"></mat-divider>
<h4 class="mat-h4 flex-98">{{ 'labels.heading.For Pre-mature closure' | translate }}</h4>
<mat-checkbox class="flex-73 margin-v" labelPosition="before" formControlName="preClosurePenalApplicable">
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label>
<input type="number" matInput formControlName="preClosurePenalInterest" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId">
@for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) {
<mat-option [value]="preClosurePenalInterestOnType.id">
{{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<!-- <mat-divider class="flex-98"></mat-divider> -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Minimum Balance For Interest Calculation' | translate }}</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" />
</mat-form-field>
@if (taxGroup) {
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Tax Group' | translate }}</h4>
<mat-checkbox labelPosition="before" formControlName="withHoldTax" class="margin-v flex-48">
{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}?
</mat-checkbox>
@if (recurringDepositAccountSettingsForm.controls.taxGroupId) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Tax Group' | translate }}</mat-label>
<input type="text" matInput formControlName="taxGroupId" />
</mat-form-field>
}
}
</div> </div>
<!-- ===================== Lock-in Period ===================== -->
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-lock"></i></span>
<span>{{ 'labels.inputs.Lock-in Period' | translate }}</span>
</div>
<div class="rdps-body">
<div class="rdps-lockin-row">
<span>Enable Lock-in Period</span>
<mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
</div>
@if (recurringDepositAccountSettingsForm.value.enableLockinPeriod) {
<div class="rdps-lockin-fields">
<mat-form-field appearance="outline" class="rdps-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-label>Type*</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div>
</div>
<!-- ===================== 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>
<!-- ===================== Withhold Tax (if applicable) ===================== -->
@if (recurringDepositAccountSettingsForm.contains('withHoldTax')) {
<div class="rdps-section">
<div class="rdps-section__header">
<span class="rdps-section__badge"><i class="fas fa-percent"></i></span>
<span>{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}</span>
</div>
<div class="rdps-body">
<div class="rdps-lockin-row">
<span>{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}</span>
<mat-checkbox formControlName="withHoldTax"></mat-checkbox>
</div>
@if (
recurringDepositAccountSettingsForm.value.withHoldTax &&
recurringDepositAccountSettingsForm.contains('taxGroupId')
) {
<mat-form-field appearance="outline" class="rdps-field">
<mat-label>{{ 'labels.inputs.Tax Group' | translate }}</mat-label>
<input matInput formControlName="taxGroupId" readonly />
</mat-form-field>
}
</div>
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious> <button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon> <fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
@@ -6,27 +6,178 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
h4 { $accent: #0ea5e9;
font-weight: 500; $border: #e2e8f0;
margin: 1em 0; $gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdps-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;
}
span:not(.rdps-section__badge) {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
} }
h3 { .rdps-section__badge {
font-weight: 500; display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
} }
mat-divider { .rdps-body {
margin: 1em 0 2em; background: #fff;
padding: 18px 24px 24px;
} }
.margin-v { .rdps-body--list {
margin: 1em 0; padding: 0 24px;
} }
.margin-b { .rdps-field {
margin: 0 0 1em; width: 100%;
} }
.margin-t { .rdps-premature-row {
margin-top: 1em; display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
}
.rdps-layout {
display: grid;
gap: 8px 16px;
&--two {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 640px) {
grid-template-columns: 1fr;
}
}
&--mt {
margin-top: 12px;
}
}
.rdps-toggle {
display: flex;
align-items: center;
min-height: 40px;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
&--inline {
align-items: center;
}
}
.rdps-layout--additional {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.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;
@media (width <= 640px) {
grid-template-columns: 1fr;
}
}
.rdps-sub-header {
font-size: 13px;
font-weight: 600;
color: $gray-900;
margin: 16px 0 8px;
padding-bottom: 6px;
border-bottom: 1px solid $gray-200;
}
.rdps-checkbox-row {
display: flex;
align-items: center;
min-height: 56px;
mat-checkbox {
font-size: 14px;
color: $gray-900;
}
} }
@@ -24,8 +24,6 @@ import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** Custom Services */
/** /**
* Recurring Deposits Account Settings Step * Recurring Deposits Account Settings Step
*/ */
@@ -58,19 +56,15 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
/** Maximum date allowed. */ /** Maximum date allowed. */
maxDate = new Date(); maxDate = new Date();
/** Lockin Period Frequency Type Data */ /** Lockin Period Frequency Type Data */
lockinPeriodFrequencyTypeData: any; lockinPeriodFrequencyTypeData: any[] = [];
/** Period Frequency Type Data */ /** Period Frequency Type Data */
periodFrequencyTypeData: any; periodFrequencyTypeData: any[] = [];
/** Preclosure Penal Interest Type on Data */ /** Preclosure Penal Interest Type on Data */
preClosurePenalInterestOnTypeData: any; preClosurePenalInterestOnTypeData: any[] = [];
/** Tax Group */ /** Tax Group */
taxGroup: any; taxGroup: any;
currency: Currency | null = null; currency: Currency | null = null;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {SettingsService} settingsService Settings Service
*/
constructor() { constructor() {
this.createRecurringDepositAccountSettingsForm(); this.createRecurringDepositAccountSettingsForm();
this.buildDependencies(); this.buildDependencies();
@@ -82,14 +76,33 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
: this.recurringDepositsAccountTemplate; : this.recurringDepositsAccountTemplate;
if (recurringDepositsAccount) { if (recurringDepositsAccount) {
this.currency = recurringDepositsAccount.currency; this.currency = recurringDepositsAccount.currency;
// Patch CBS settings fields from product template
this.recurringDepositAccountSettingsForm.patchValue({ this.recurringDepositAccountSettingsForm.patchValue({
isMandatoryDeposit: recurringDepositsAccount.isMandatoryDeposit, loanFacility: recurringDepositsAccount.loanFacility ?? false,
adjustAdvanceTowardsFuturePayments: recurringDepositsAccount.adjustAdvanceTowardsFuturePayments, prematureWithdrawalOption: recurringDepositsAccount.prematureWithdrawalOption ?? false,
allowWithdrawal: recurringDepositsAccount.allowWithdrawal, tenureFlexibility: recurringDepositsAccount.tenureFlexibility ?? false,
lockinPeriodFrequency: recurringDepositsAccount.lockinPeriodFrequency, lienMarked: recurringDepositsAccount.lienMarked ?? false,
withdrawalFeeForTransfers: recurringDepositsAccount.withdrawalFeeForTransfers ?? false,
enableLockinPeriod: recurringDepositsAccount.enableLockinPeriod ?? false,
lockinPeriodFrequency: recurringDepositsAccount.lockinPeriodFrequency ?? '',
lockinPeriodFrequencyType: recurringDepositsAccount.lockinPeriodFrequencyType lockinPeriodFrequencyType: recurringDepositsAccount.lockinPeriodFrequencyType
? recurringDepositsAccount.lockinPeriodFrequencyType.id ? recurringDepositsAccount.lockinPeriodFrequencyType.id
: '', : '',
maturityInstructions: recurringDepositsAccount.maturityInstructions ?? false,
creditMaturityProceeds: recurringDepositsAccount.creditMaturityProceeds ?? false,
penaltyGracePeriodMissedInstallments: recurringDepositsAccount.penaltyGracePeriodMissedInstallments ?? false,
panelRequestApplicable: recurringDepositsAccount.panelRequestApplicable ?? false,
noPenaltyNomineeHeirClosure: recurringDepositsAccount.noPenaltyNomineeHeirClosure ?? false,
deceasedSettlement: recurringDepositsAccount.deceasedSettlement ?? false,
allowDebitTxnAfterDueDate: recurringDepositsAccount.allowDebitTxnAfterDueDate ?? false,
accountClosedZeroBalanceInDayClosing: recurringDepositsAccount.accountClosedZeroBalanceInDayClosing ?? false,
minBalanceForInterestCalculation: recurringDepositsAccount.minBalanceForInterestCalculation ?? '',
// Account-specific fields — auto-filled from product template (hidden from UI)
isMandatoryDeposit: recurringDepositsAccount.isMandatoryDeposit,
adjustAdvanceTowardsFuturePayments: recurringDepositsAccount.adjustAdvanceTowardsFuturePayments,
allowWithdrawal: recurringDepositsAccount.allowWithdrawal,
minDepositTerm: recurringDepositsAccount.minDepositTerm, minDepositTerm: recurringDepositsAccount.minDepositTerm,
minDepositTermTypeId: recurringDepositsAccount.minDepositTermType minDepositTermTypeId: recurringDepositsAccount.minDepositTermType
? recurringDepositsAccount.minDepositTermType.id ? recurringDepositsAccount.minDepositTermType.id
@@ -107,17 +120,16 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
preClosurePenalInterestOnTypeId: recurringDepositsAccount.preClosurePenalInterestOnType preClosurePenalInterestOnTypeId: recurringDepositsAccount.preClosurePenalInterestOnType
? recurringDepositsAccount.preClosurePenalInterestOnType.id ? recurringDepositsAccount.preClosurePenalInterestOnType.id
: '', : '',
minBalanceForInterestCalculation: recurringDepositsAccount.minBalanceForInterestCalculation,
depositPeriod: recurringDepositsAccount.minDepositTerm, depositPeriod: recurringDepositsAccount.minDepositTerm,
depositPeriodFrequencyId: recurringDepositsAccount.minDepositTermType.id, depositPeriodFrequencyId: recurringDepositsAccount.minDepositTermType
expectedFirstDepositOnDate: ? recurringDepositsAccount.minDepositTermType.id
recurringDepositsAccount.expectedFirstDepositOnDate && : '',
new Date(recurringDepositsAccount.expectedFirstDepositOnDate),
recurringFrequency: recurringDepositsAccount.recurringFrequency, recurringFrequency: recurringDepositsAccount.recurringFrequency,
recurringFrequencyType: recurringDepositsAccount.recurringFrequencyType recurringFrequencyType: recurringDepositsAccount.recurringFrequencyType
? recurringDepositsAccount.recurringFrequencyType.id ? recurringDepositsAccount.recurringFrequencyType.id
: '' : ''
}); });
if (recurringDepositsAccount.withHoldTax) { if (recurringDepositsAccount.withHoldTax) {
this.recurringDepositAccountSettingsForm.addControl('withHoldTax', new UntypedFormControl(false)); this.recurringDepositAccountSettingsForm.addControl('withHoldTax', new UntypedFormControl(false));
this.recurringDepositAccountSettingsForm.get('withHoldTax').valueChanges.subscribe((value: boolean) => { this.recurringDepositAccountSettingsForm.get('withHoldTax').valueChanges.subscribe((value: boolean) => {
@@ -135,7 +147,7 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
}); });
this.recurringDepositAccountSettingsForm this.recurringDepositAccountSettingsForm
.get('withHoldTax') .get('withHoldTax')
.patchValue(this.recurringDepositsAccountTemplate.withHoldTax); .patchValue(this.recurringDepositsAccountTemplate?.withHoldTax);
} else { } else {
this.recurringDepositAccountSettingsForm.removeControl('withHoldTax'); this.recurringDepositAccountSettingsForm.removeControl('withHoldTax');
} }
@@ -157,38 +169,38 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
} }
} }
/**
* Creates recurring deposits account terms form.
*/
createRecurringDepositAccountSettingsForm() { createRecurringDepositAccountSettingsForm() {
this.recurringDepositAccountSettingsForm = this.formBuilder.group({ this.recurringDepositAccountSettingsForm = this.formBuilder.group({
// CBS Settings (visible — same as product settings page)
loanFacility: [false],
prematureWithdrawalOption: [false],
tenureFlexibility: [false],
lienMarked: [false],
withdrawalFeeForTransfers: [false],
enableLockinPeriod: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
maturityInstructions: [false],
creditMaturityProceeds: [false],
penaltyGracePeriodMissedInstallments: [false],
panelRequestApplicable: [false],
noPenaltyNomineeHeirClosure: [false],
deceasedSettlement: [false],
allowDebitTxnAfterDueDate: [false],
accountClosedZeroBalanceInDayClosing: [false],
minBalanceForInterestCalculation: [''],
// Account-specific fields (hidden from UI — auto-filled from product template)
isMandatoryDeposit: [''], isMandatoryDeposit: [''],
adjustAdvanceTowardsFuturePayments: [''], adjustAdvanceTowardsFuturePayments: [''],
allowWithdrawal: [''], allowWithdrawal: [''],
lockinPeriodFrequency: [''], mandatoryRecommendedDepositAmount: [''],
lockinPeriodFrequencyType: [''], depositPeriod: [''],
mandatoryRecommendedDepositAmount: [ depositPeriodFrequencyId: [''],
'',
Validators.required
],
depositPeriod: [
'',
Validators.required
],
depositPeriodFrequencyId: [
'',
Validators.required
],
isCalendarInherited: [''], isCalendarInherited: [''],
expectedFirstDepositOnDate: [''], expectedFirstDepositOnDate: [''],
recurringFrequency: [ recurringFrequency: [''],
'', recurringFrequencyType: [''],
Validators.required
],
recurringFrequencyType: [
'',
Validators.required
],
minDepositTerm: [{ value: '', disabled: true }], minDepositTerm: [{ value: '', disabled: true }],
minDepositTermTypeId: [{ value: '', disabled: true }], minDepositTermTypeId: [{ value: '', disabled: true }],
inMultiplesOfDepositTerm: [{ value: '', disabled: true }], inMultiplesOfDepositTerm: [{ value: '', disabled: true }],
@@ -197,23 +209,16 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
maxDepositTermTypeId: [{ value: '', disabled: true }], maxDepositTermTypeId: [{ value: '', disabled: true }],
preClosurePenalApplicable: [{ value: '', disabled: true }], preClosurePenalApplicable: [{ value: '', disabled: true }],
preClosurePenalInterest: [{ value: '', disabled: true }], preClosurePenalInterest: [{ value: '', disabled: true }],
preClosurePenalInterestOnTypeId: [{ value: '', disabled: true }], preClosurePenalInterestOnTypeId: [{ value: '', disabled: true }]
minBalanceForInterestCalculation: [{ value: '', disabled: true }]
}); });
} }
/**
* Sets all select dropdown options.
*/
setOptions(recurringDepositsAccount: any) { setOptions(recurringDepositsAccount: any) {
this.lockinPeriodFrequencyTypeData = recurringDepositsAccount.lockinPeriodFrequencyTypeOptions; this.lockinPeriodFrequencyTypeData = recurringDepositsAccount.lockinPeriodFrequencyTypeOptions;
this.periodFrequencyTypeData = recurringDepositsAccount.periodFrequencyTypeOptions; this.periodFrequencyTypeData = recurringDepositsAccount.periodFrequencyTypeOptions;
this.preClosurePenalInterestOnTypeData = recurringDepositsAccount.preClosurePenalInterestOnTypeOptions; this.preClosurePenalInterestOnTypeData = recurringDepositsAccount.preClosurePenalInterestOnTypeOptions;
} }
/**
* Subscribes to value changes and sets new form controls accordingly.
*/
buildDependencies() { buildDependencies() {
this.recurringDepositAccountSettingsForm this.recurringDepositAccountSettingsForm
.get('isCalendarInherited') .get('isCalendarInherited')
@@ -230,9 +235,6 @@ export class RecurringDepositsAccountSettingsStepComponent implements OnInit, On
}); });
} }
/**
* Returns recurring deposits account settings form value.
*/
get recurringDepositAccountSettings() { get recurringDepositAccountSettings() {
return this.recurringDepositAccountSettingsForm.value; return this.recurringDepositAccountSettingsForm.value;
} }
@@ -7,69 +7,171 @@
--> -->
<form [formGroup]="recurringDepositAccountTermsForm"> <form [formGroup]="recurringDepositAccountTermsForm">
<div class="layout-row-wrap gap-2px responsive-column"> <!-- ===================== Interest Rate & Posting Terms ===================== -->
<mat-form-field class="flex-48"> <div class="rdpt-section">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label> <div class="rdpt-section__header">
<mat-select formControlName="interestCompoundingPeriodType" required> <span class="rdpt-section__badge"><i class="fas fa-chart-line"></i></span>
@for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { <span>Interest Rate &amp; Posting Terms</span>
<mat-option [value]="interestCompoundingPeriodType.id"> </div>
{{ interestCompoundingPeriodType.value | translateKey: 'catalogs' }} <div class="rdpt-body">
</mat-option> @if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate != null) {
} <div class="rdpt-banner">
</mat-select> <span class="rdpt-banner__icon">ℹ️</span>
<mat-error> <span>
{{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} Recurring Deposit base rate:
<strong>{{ 'labels.commons.required' | translate }}</strong> <b>{{ recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate }}%</b>
</mat-error> </span>
</mat-form-field> </div>
}
<mat-form-field class="flex-48"> <!-- Row 1: Nominal Annual Interest | Effective Date | Interest Calculation Type -->
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label> <div class="rdpt-grid rdpt-grid--three">
<mat-select formControlName="interestPostingPeriodType" required> <mat-form-field appearance="outline" class="rdpt-field">
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { <mat-label>Nominal Annual Interest</mat-label>
<mat-option [value]="interestPostingPeriodType.id"> <input matInput [value]="recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? ''" readonly />
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }} </mat-form-field>
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48"> <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 [value]="formatDate(recurringDepositsAccountProductTemplate?.effectiveDate)" readonly />
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { </mat-form-field>
<mat-option [value]="interestCalculationType.id">
{{ interestCalculationType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48"> <mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label> <mat-label>Interest Calculation Type</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required> <input
@for ( matInput
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; [value]="
track interestCalculationDaysInYearType recurringDepositsAccountProductTemplate?.interestCalculationTypeName ??
) { (recurringDepositsAccountProductTemplate?.interestCalculationType?.id
<mat-option [value]="interestCalculationDaysInYearType.id"> | find: recurringDepositsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value') ??
{{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }} ''
</mat-option> "
readonly
/>
</mat-form-field>
</div>
<!-- Row 2: Interest Calculate | Average Period | Interest Posting Period -->
<div class="rdpt-grid rdpt-grid--three">
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculated using' | translate }}</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) {
<mat-option [value]="interestCalculationType.id">
{{ interestCalculationType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Average Period</mat-label>
<input matInput [value]="recurringDepositsAccountProductTemplate?.averagePeriod ?? ''" readonly />
</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 | 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>
<!-- Row 3: Interest Compounding Period -->
<div class="rdpt-grid rdpt-grid--three">
<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 | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<!-- Footer: Auto Interest Post | Frequency Fix Day | Last Interest Date -->
<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">
<label class="rdpt-radio">
<input type="radio" [checked]="recurringDepositsAccountProductTemplate?.autoPost === true" disabled />
<span>Yes</span>
</label>
<label class="rdpt-radio">
<input type="radio" [checked]="recurringDepositsAccountProductTemplate?.autoPost === false" disabled />
<span>No</span>
</label>
</div>
</div>
<mat-form-field appearance="outline" class="rdpt-field">
<mat-label>Frequency (Fix Day)</mat-label>
<input matInput [value]="recurringDepositsAccountProductTemplate?.frequencyFixDay ?? ''" readonly />
<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 [value]="formatDate(recurringDepositsAccountProductTemplate?.lastInterestDate)" readonly />
</mat-form-field>
</div>
@if (recurringDepositAccountTermsForm.value.interestCalculationType === 1) {
<div class="rdpt-note">
<span>⚠️</span>
<span
>Daily product-based interest accrual enabled - interest calculated EOD on daily closing balance.</span
>
</div>
} }
</mat-select>
<mat-error> <!-- Days in Year (account-level field) -->
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} <div class="rdpt-footer__grid rdpt-footer__grid--single">
<strong>{{ 'labels.commons.required' | translate }}</strong> <mat-form-field appearance="outline" class="rdpt-field">
</mat-error> <mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label>
</mat-form-field> <mat-select formControlName="interestCalculationDaysInYearType" required>
@for (
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData;
track interestCalculationDaysInYearType
) {
<mat-option [value]="interestCalculationDaysInYearType.id">
{{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</div> </div>
<div class="layout-row align-center gap-2percent margin-t responsive-column"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
@@ -5,3 +5,167 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.rdpt-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,
span:not(.rdpt-section__badge) {
margin: 0;
font-size: 14px;
font-weight: 600;
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 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
&--three {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
&--three {
grid-template-columns: 1fr;
}
}
}
.rdpt-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.rdpt-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;
}
}
.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;
&--single {
grid-template-columns: repeat(1, 1fr);
padding-top: 12px;
}
@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-radio {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
cursor: default;
[type='radio'] {
width: 16px;
height: 16px;
accent-color: $accent;
}
}
.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;
}
@@ -13,6 +13,7 @@ import { SettingsService } from 'app/settings/settings.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FindPipe } from '../../../../pipes/find.pipe';
/** /**
* Recurring Deposits Terms Step * Recurring Deposits Terms Step
@@ -25,7 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatStepperPrevious, MatStepperPrevious,
FaIconComponent, FaIconComponent,
MatStepperNext MatStepperNext,
FindPipe
] ]
}) })
export class RecurringDepositsAccountTermsStepComponent implements OnInit, OnChanges { export class RecurringDepositsAccountTermsStepComponent implements OnInit, OnChanges {
@@ -42,13 +44,13 @@ export class RecurringDepositsAccountTermsStepComponent implements OnInit, OnCha
/** Recurring Deposits Account Terms Form */ /** Recurring Deposits Account Terms Form */
recurringDepositAccountTermsForm: UntypedFormGroup; recurringDepositAccountTermsForm: UntypedFormGroup;
/** Interest Compounding Period Type Data */ /** Interest Compounding Period Type Data */
interestCompoundingPeriodTypeData: any; interestCompoundingPeriodTypeData: any[] = [];
/** Interest Posting Period Type Data */ /** Interest Posting Period Type Data */
interestPostingPeriodTypeData: any; interestPostingPeriodTypeData: any[] = [];
/** Interest Calculation Type Data */ /** Interest Calculation Type Data */
interestCalculationTypeData: any; interestCalculationTypeData: any[] = [];
/** Interest Calculation Days in Year Data */ /** Interest Calculation Days in Year Data */
interestCalculationDaysInYearTypeData: any; interestCalculationDaysInYearTypeData: any[] = [];
/** /**
* @param {FormBuilder} formBuilder Form Builder * @param {FormBuilder} formBuilder Form Builder
@@ -61,17 +63,18 @@ export class RecurringDepositsAccountTermsStepComponent implements OnInit, OnCha
ngOnChanges() { ngOnChanges() {
if (this.recurringDepositsAccountProductTemplate) { if (this.recurringDepositsAccountProductTemplate) {
this.setOptions(); this.setOptions();
this.initializeTermsValues();
} }
} }
ngOnInit() { ngOnInit() {
this.maxDate = this.settingsService.businessDate; this.maxDate = this.settingsService.businessDate;
if (this.recurringDepositsAccountTemplate.id) { if (this.isEditMode()) {
this.recurringDepositAccountTermsForm.patchValue({ this.recurringDepositAccountTermsForm.patchValue({
interestCompoundingPeriodType: this.recurringDepositsAccountTemplate.interestCompoundingPeriodType.id, interestCompoundingPeriodType: this.recurringDepositsAccountTemplate?.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.recurringDepositsAccountTemplate.interestPostingPeriodType.id, interestPostingPeriodType: this.recurringDepositsAccountTemplate?.interestPostingPeriodType?.id,
interestCalculationType: this.recurringDepositsAccountTemplate.interestCalculationType.id, interestCalculationType: this.recurringDepositsAccountTemplate?.interestCalculationType?.id,
interestCalculationDaysInYearType: this.recurringDepositsAccountTemplate.interestCalculationDaysInYearType.id interestCalculationDaysInYearType: this.recurringDepositsAccountTemplate?.interestCalculationDaysInYearType?.id
}); });
} }
} }
@@ -110,17 +113,70 @@ export class RecurringDepositsAccountTermsStepComponent implements OnInit, OnCha
this.interestCalculationTypeData = this.recurringDepositsAccountProductTemplate.interestCalculationTypeOptions; this.interestCalculationTypeData = this.recurringDepositsAccountProductTemplate.interestCalculationTypeOptions;
this.interestCalculationDaysInYearTypeData = this.interestCalculationDaysInYearTypeData =
this.recurringDepositsAccountProductTemplate.interestCalculationDaysInYearTypeOptions; this.recurringDepositsAccountProductTemplate.interestCalculationDaysInYearTypeOptions;
if (!this.recurringDepositsAccountTemplate.id) { if (!this.isEditMode()) {
this.recurringDepositAccountTermsForm.patchValue({ this.recurringDepositAccountTermsForm.patchValue({
interestCompoundingPeriodType: this.recurringDepositsAccountProductTemplate.interestCompoundingPeriodType.id, interestCompoundingPeriodType: this.recurringDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.recurringDepositsAccountProductTemplate.interestPostingPeriodType.id, interestPostingPeriodType: this.recurringDepositsAccountProductTemplate?.interestPostingPeriodType?.id,
interestCalculationType: this.recurringDepositsAccountProductTemplate.interestCalculationType.id, interestCalculationType: this.recurringDepositsAccountProductTemplate?.interestCalculationType?.id,
interestCalculationDaysInYearType: interestCalculationDaysInYearType:
this.recurringDepositsAccountProductTemplate.interestCalculationDaysInYearType.id this.recurringDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id
}); });
} }
} }
/** True when component is used for account edit flow. */
private isEditMode(): boolean {
return !!this.recurringDepositsAccountTemplate?.depositProductId;
}
/**
* Ensures create flow always gets product defaults while edit flow keeps saved values.
*/
private initializeTermsValues(): void {
const isEdit = this.isEditMode();
const fallbackFromProduct = {
interestCompoundingPeriodType: this.recurringDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.recurringDepositsAccountProductTemplate?.interestPostingPeriodType?.id,
interestCalculationType: this.recurringDepositsAccountProductTemplate?.interestCalculationType?.id,
interestCalculationDaysInYearType:
this.recurringDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id
};
if (!isEdit) {
this.recurringDepositAccountTermsForm.patchValue(fallbackFromProduct);
return;
}
const current = this.recurringDepositAccountTermsForm.value;
this.recurringDepositAccountTermsForm.patchValue({
interestCompoundingPeriodType:
current.interestCompoundingPeriodType ?? fallbackFromProduct.interestCompoundingPeriodType,
interestPostingPeriodType: current.interestPostingPeriodType ?? fallbackFromProduct.interestPostingPeriodType,
interestCalculationType: current.interestCalculationType ?? fallbackFromProduct.interestCalculationType,
interestCalculationDaysInYearType:
current.interestCalculationDaysInYearType ?? fallbackFromProduct.interestCalculationDaysInYearType
});
}
/**
* Formats a date value (Date, [y,m,d] array, or string) to a display string.
*/
formatDate(value: any): string {
if (!value) return '';
if (Array.isArray(value)) {
const [
y,
m,
d
] = value;
return `${String(d).padStart(2, '0')}/${String(m).padStart(2, '0')}/${y}`;
}
if (value instanceof Date) {
return value.toLocaleDateString();
}
return String(value);
}
/** /**
* Returns recurring deposits account terms form value. * Returns recurring deposits account terms form value.
*/ */
@@ -1,4 +1,4 @@
<!-- <!--
Copyright since 2025 Mifos Initiative Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
@@ -61,14 +61,14 @@
@if (CurrentProduct.effectiveTo) { @if (CurrentProduct.effectiveTo) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span> <span class="flex-40">Effective To:</span>
<span class="flex-60">{{ CurrentProduct.effectiveTo }}</span> <span class="flex-60">{{ CurrentProduct.effectiveTo | dateFormat }}</span>
</div> </div>
} }
@if (CurrentProduct.endDate) { @if (CurrentProduct.endDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">End Date:</span> <span class="flex-40">End Date:</span>
<span class="flex-60">{{ CurrentProduct.endDate }}</span> <span class="flex-60">{{ CurrentProduct.endDate | dateFormat }}</span>
</div> </div>
} }
@@ -181,7 +181,7 @@
@if (CurrentProduct.lastInterestDate) { @if (CurrentProduct.lastInterestDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span> <span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ CurrentProduct.lastInterestDate }}</span> <span class="flex-60">{{ CurrentProduct.lastInterestDate | dateFormat }}</span>
</div> </div>
} }
@@ -1,4 +1,4 @@
/** /**
* Copyright since 2025 Mifos Initiative * Copyright since 2025 Mifos Initiative
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
@@ -24,6 +24,7 @@ import {
} from '@angular/material/table'; } from '@angular/material/table';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../../pipes/find.pipe'; import { FindPipe } from '../../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -47,6 +48,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRow, MatRow,
FaIconComponent, FaIconComponent,
FindPipe, FindPipe,
DateFormatPipe,
FormatNumberPipe, FormatNumberPipe,
YesnoPipe YesnoPipe
] ]
@@ -100,11 +100,6 @@
<span>Account Closed with Zero Balance in Day Closing</span> <span>Account Closed with Zero Balance in Day Closing</span>
</mat-checkbox> </mat-checkbox>
</div> </div>
<div class="sps-toggle sps-toggle--inline">
<mat-checkbox formControlName="withHoldTax">
<span>Withhold Tax is Applicable</span>
</mat-checkbox>
</div>
</div> </div>
</div> </div>
@@ -54,7 +54,6 @@ export class CurrentProductSettingsStepComponent implements OnInit {
withdrawalFeeForTransfers: this.CurrentProductsTemplate.withdrawalFeeForTransfers, withdrawalFeeForTransfers: this.CurrentProductsTemplate.withdrawalFeeForTransfers,
enforceMinRequiredBalance: this.CurrentProductsTemplate.enforceMinRequiredBalance, enforceMinRequiredBalance: this.CurrentProductsTemplate.enforceMinRequiredBalance,
minRequiredBalance: this.CurrentProductsTemplate.minRequiredBalance, minRequiredBalance: this.CurrentProductsTemplate.minRequiredBalance,
withHoldTax: this.CurrentProductsTemplate.withHoldTax ?? false,
balanceFrequency: isEditing ? this.CurrentProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod, balanceFrequency: isEditing ? this.CurrentProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod,
enableLockinPeriod: !!( enableLockinPeriod: !!(
this.CurrentProductsTemplate.lockinPeriodFrequency && this.CurrentProductsTemplate.lockinPeriodFrequency > 0 this.CurrentProductsTemplate.lockinPeriodFrequency && this.CurrentProductsTemplate.lockinPeriodFrequency > 0
@@ -107,7 +106,6 @@ export class CurrentProductSettingsStepComponent implements OnInit {
'', '',
[Validators.min(0)] [Validators.min(0)]
], ],
withHoldTax: [false],
balanceFrequency: ['Monthly'], balanceFrequency: ['Monthly'],
enableLockinPeriod: [false], enableLockinPeriod: [false],
lockinPeriodFrequency: [''], lockinPeriodFrequency: [''],
@@ -159,6 +157,8 @@ export class CurrentProductSettingsStepComponent implements OnInit {
get CurrentProductSettings() { get CurrentProductSettings() {
const formValue = { ...this.CurrentProductSettingsForm.value }; const formValue = { ...this.CurrentProductSettingsForm.value };
delete formValue.withHoldTax;
if (!formValue.allowOverdraft) { if (!formValue.allowOverdraft) {
delete formValue.overdraftLimit; delete formValue.overdraftLimit;
delete formValue.nominalAnnualInterestRateOverdraft; delete formValue.nominalAnnualInterestRateOverdraft;
@@ -75,21 +75,21 @@
@if (currentProduct.effectiveDate) { @if (currentProduct.effectiveDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span> <span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ currentProduct.effectiveDate }}</span> <span class="flex-60">{{ currentProduct.effectiveDate | dateFormat }}</span>
</div> </div>
} }
@if (currentProduct.effectiveTo) { @if (currentProduct.effectiveTo) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span> <span class="flex-40">Effective To:</span>
<span class="flex-60">{{ currentProduct.effectiveTo }}</span> <span class="flex-60">{{ currentProduct.effectiveTo | dateFormat }}</span>
</div> </div>
} }
@if (currentProduct.endDate) { @if (currentProduct.endDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">End Date:</span> <span class="flex-40">End Date:</span>
<span class="flex-60">{{ currentProduct.endDate }}</span> <span class="flex-60">{{ currentProduct.endDate | dateFormat }}</span>
</div> </div>
} }
@@ -208,7 +208,7 @@
@if (currentProduct.lastInterestDate) { @if (currentProduct.lastInterestDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span> <span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ currentProduct.lastInterestDate }}</span> <span class="flex-60">{{ currentProduct.lastInterestDate | dateFormat }}</span>
</div> </div>
} }
@@ -23,6 +23,7 @@ import {
MatRow MatRow
} from '@angular/material/table'; } from '@angular/material/table';
import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -46,6 +47,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRowDef, MatRowDef,
MatRow, MatRow,
ViewSavingsAccountingDetailsComponent, ViewSavingsAccountingDetailsComponent,
DateFormatPipe,
FormatNumberPipe, FormatNumberPipe,
YesnoPipe YesnoPipe
] ]
@@ -150,11 +150,11 @@ export class EditRecurringDepositProductComponent {
this.recurringDepositProductTermsForm.valid && this.recurringDepositProductTermsForm.valid &&
this.recurringDepositProductSettingsForm.valid && this.recurringDepositProductSettingsForm.valid &&
this.recurringDepositProductAccountingForm.valid && this.recurringDepositProductAccountingForm.valid &&
(this.recurringDepositProductDetailsForm.pristine || (!this.recurringDepositProductDetailsForm.pristine ||
this.recurringDepositProductCurrencyForm.pristine || !this.recurringDepositProductCurrencyForm.pristine ||
this.recurringDepositProductTermsForm.pristine || !this.recurringDepositProductTermsForm.pristine ||
this.recurringDepositProductSettingsForm.pristine || !this.recurringDepositProductSettingsForm.pristine ||
this.recurringDepositProductAccountingForm.pristine) !this.recurringDepositProductAccountingForm.pristine)
); );
} }
@@ -1,4 +1,4 @@
<!-- <!--
Copyright since 2025 Mifos Initiative Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
@@ -7,10 +7,10 @@
--> -->
<div class="responsive-column"> <div class="responsive-column">
<!-- ── Product Name ── --> <!-- ── Product Name ── -->
<h2 class="mat-h2 flex-100">{{ recurringDepositProduct.name }}</h2> <h2 class="mat-h2 flex-100">{{ recurringDepositProduct.name }}</h2>
<!-- ── Details ── --> <!-- ── Details ── -->
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -27,7 +27,7 @@
</div> </div>
} }
<!-- ── Product Classification ── --> <!-- ── Product Classification ── -->
@if ( @if (
recurringDepositProduct.accountType || recurringDepositProduct.accountType ||
recurringDepositProduct.customerSegment || recurringDepositProduct.customerSegment ||
@@ -63,14 +63,14 @@
@if (recurringDepositProduct.effectiveTo) { @if (recurringDepositProduct.effectiveTo) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span> <span class="flex-40">Effective To:</span>
<span class="flex-60">{{ recurringDepositProduct.effectiveTo }}</span> <span class="flex-60">{{ recurringDepositProduct.effectiveTo | dateFormat }}</span>
</div> </div>
} }
@if (recurringDepositProduct.endDate) { @if (recurringDepositProduct.endDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">End Date:</span> <span class="flex-40">End Date:</span>
<span class="flex-60">{{ recurringDepositProduct.endDate }}</span> <span class="flex-60">{{ recurringDepositProduct.endDate | dateFormat }}</span>
</div> </div>
} }
@@ -93,7 +93,7 @@
} }
} }
<!-- ── Currency ── --> <!-- ── Currency ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -144,7 +144,7 @@
</div> </div>
} }
<!-- ── Terms ── --> <!-- ── Terms ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -169,11 +169,22 @@
}}</span> }}</span>
</div> </div>
@if (recurringDepositProduct.interestCalculateSelector) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{
recurringDepositProduct.interestCalculateSelector
| find: recurringDepositProductsTemplate.interestCalculationTypeOption : 'id' : 'value'
}}</span>
</div>
}
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{ <span class="flex-60">{{
recurringDepositProduct.interestCalculationType recurringDepositProduct.interestCalculationType
| find: recurringDepositProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' | find: recurringDepositProductsTemplate.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span> }}</span>
</div> </div>
@@ -188,7 +199,7 @@
@if (recurringDepositProduct.effectiveDate) { @if (recurringDepositProduct.effectiveDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span> <span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ recurringDepositProduct.effectiveDate }}</span> <span class="flex-60">{{ recurringDepositProduct.effectiveDate | dateFormat }}</span>
</div> </div>
} }
@@ -218,11 +229,11 @@
@if (recurringDepositProduct.lastInterestDate) { @if (recurringDepositProduct.lastInterestDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span> <span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ recurringDepositProduct.lastInterestDate }}</span> <span class="flex-60">{{ recurringDepositProduct.lastInterestDate | dateFormat }}</span>
</div> </div>
} }
<!-- ── Settings ── --> <!-- ── Settings ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -313,7 +324,7 @@
</div> </div>
} }
<!-- ── Interest Rate Slabs ── --> <!-- ── Interest Rate Slabs ── -->
@if (recurringDepositProduct.rdSlabs?.length) { @if (recurringDepositProduct.rdSlabs?.length) {
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -334,7 +345,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="toPeriod"> <ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th> <th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td> <td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="periodTypeLabel"> <ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period</th> <th mat-header-cell *matHeaderCellDef>Period</th>
@@ -352,7 +363,7 @@
</div> </div>
} }
<!-- ── Charges ── --> <!-- ── Charges ── -->
@if (recurringDepositProduct.charges?.length) { @if (recurringDepositProduct.charges?.length) {
<div class="layout-row-wrap responsive-column flex-100"> <div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
@@ -380,7 +391,7 @@
</div> </div>
} }
<!-- ── Accounting ── --> <!-- ── Accounting ── -->
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3> <h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider> <mat-divider class="flex-100"></mat-divider>
@@ -54,14 +54,22 @@
<mat-form-field appearance="outline" class="rdpt-field"> <mat-form-field appearance="outline" class="rdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select <mat-select
formControlName="interestCalculationType" formControlName="interestCalculateSelector"
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}" matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
required
> >
@for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) { @for (opt of interestCalculationTypeOptionData; track opt.id) {
<mat-option [value]="interestCalculationType.id"> <mat-option [value]="opt.id">{{ opt.value }}</mat-option>
{{ interestCalculationType.value | translateKey: 'catalogs' }} }
</mat-option> </mat-select>
</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="interestCalculationType" required>
@for (mode of interestCalculationTypeData; track mode.id) {
<mat-option [value]="mode.id">{{ mode.value | translateKey: 'catalogs' }}</mat-option>
} }
</mat-select> </mat-select>
<mat-error> <mat-error>
@@ -69,19 +77,8 @@
<strong>{{ 'labels.commons.required' | translate }}</strong> <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div>
<div class="rdpt-grid rdpt-grid--three"> @if (recurringDepositProductTermsForm.value.interestCalculationType === 2) {
<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-form-field appearance="outline" class="rdpt-field">
<mat-label>Average Period</mat-label> <mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod"> <mat-select formControlName="averagePeriod">
@@ -142,7 +139,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
@if (recurringDepositProductTermsForm.value.interestCalculateSelector === 1) { @if (recurringDepositProductTermsForm.value.interestCalculationType === 1) {
<div class="rdpt-note"> <div class="rdpt-note">
<span>⚠️</span> <span>⚠️</span>
<span <span
@@ -49,10 +49,10 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.interestCompoundingPeriodTypeData = this.recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestCompoundingPeriodTypeData = this.recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.recurringDepositProductsTemplate.interestPostingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.recurringDepositProductsTemplate.interestPostingPeriodTypeOptions;
// Standard enum (1=Daily, 2=Average) → "Interest Calculate" dropdown → interestCalculationType form control
this.interestCalculationTypeData = this.recurringDepositProductsTemplate.interestCalculationTypeOptions; this.interestCalculationTypeData = this.recurringDepositProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationTypeOptionData = // CBS code values → "Interest Calculation Type" dropdown → interestCalculateSelector form control
this.recurringDepositProductsTemplate.interestCalculationTypeOption || this.interestCalculationTypeOptionData = this.recurringDepositProductsTemplate.interestCalculationTypeOption || [];
this.recurringDepositProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationDaysInYearTypeData = this.interestCalculationDaysInYearTypeData =
this.recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions; this.recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions;
this.periodFrequencyData = this.recurringDepositProductsTemplate.periodFrequencyOptions || []; this.periodFrequencyData = this.recurringDepositProductsTemplate.periodFrequencyOptions || [];
@@ -60,37 +60,33 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
const isEditing = !!this.recurringDepositProductsTemplate.id; const isEditing = !!this.recurringDepositProductsTemplate.id;
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
const savedEnumTypeId =
this.recurringDepositProductsTemplate.interestCalculationType?.id ||
this.interestCalculationTypeData?.[0]?.id ||
1;
const savedSelectorId = isEditing
? (this.recurringDepositProductsTemplate.interestCalculateSelector ??
this.interestCalculationTypeOptionData?.[0]?.id ??
null)
: (this.interestCalculationTypeOptionData?.[0]?.id ?? null);
this.recurringDepositProductTermsForm.patchValue({ this.recurringDepositProductTermsForm.patchValue({
nominalAnnualInterestRate: this.recurringDepositProductsTemplate.nominalAnnualInterestRate ?? 0, nominalAnnualInterestRate: this.recurringDepositProductsTemplate.nominalAnnualInterestRate ?? 0,
interestCompoundingPeriodType: this.recurringDepositProductsTemplate.interestCompoundingPeriodType?.id, interestCompoundingPeriodType: this.recurringDepositProductsTemplate.interestCompoundingPeriodType?.id,
interestPostingPeriodType: this.recurringDepositProductsTemplate.interestPostingPeriodType?.id, interestPostingPeriodType: this.recurringDepositProductsTemplate.interestPostingPeriodType?.id,
interestCalculationType: isEditing interestCalculationType: savedEnumTypeId,
? this.recurringDepositProductsTemplate.interestCalculationTypeOption?.id ||
this.interestCalculationTypeOptionData?.[0]?.id ||
1
: this.interestCalculationTypeOptionData?.[0]?.id || 1,
interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType?.id, interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType?.id,
effectiveDate: this.recurringDepositProductsTemplate.effectiveDate effectiveDate: this.recurringDepositProductsTemplate.effectiveDate
? this.parseDate(this.recurringDepositProductsTemplate.effectiveDate) ? this.parseDate(this.recurringDepositProductsTemplate.effectiveDate)
: '', : '',
interestCalculateSelector: isEditing interestCalculateSelector: savedSelectorId,
? this.recurringDepositProductsTemplate.interestCalculateSelector || 1
: this.recurringDepositProductsTemplate.interestCalculationType?.id ||
this.interestCalculationTypeData?.[0]?.id ||
1,
averagePeriod: isEditing ? this.recurringDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, averagePeriod: isEditing ? this.recurringDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod,
autoPost: isEditing ? (this.recurringDepositProductsTemplate.autoPost ?? true) : true, autoPost: isEditing ? (this.recurringDepositProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? this.recurringDepositProductsTemplate.frequencyFixDay || 30 : 30, frequencyFixDay: isEditing ? (this.recurringDepositProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate
? this.parseDate(this.recurringDepositProductsTemplate.lastInterestDate) ? this.parseDate(this.recurringDepositProductsTemplate.lastInterestDate)
: '' : ''
}); });
const currentInterestCalcType = this.recurringDepositProductTermsForm.get('interestCalculationType').value;
if (currentInterestCalcType && currentInterestCalcType > 2) {
this.recurringDepositProductTermsForm.patchValue({ interestCalculationType: 1 });
}
} }
private parseDate(dateValue: any): Date | null { private parseDate(dateValue: any): Date | null {
@@ -163,10 +159,6 @@ export class RecurringDepositProductTermsStepComponent implements OnInit {
if (formValue.lastInterestDate instanceof Date) { if (formValue.lastInterestDate instanceof Date) {
formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || '';
} }
if (formValue.interestCalculationType && formValue.interestCalculationType > 2) {
formValue.interestCalculationType = 1;
}
for (const key in formValue) { for (const key in formValue) {
if (formValue[key] === '') { if (formValue[key] === '') {
delete formValue[key]; delete formValue[key];
@@ -141,16 +141,16 @@
<span class="flex-60">{{ recurringDepositProduct.effectiveDate | dateFormat }}</span> <span class="flex-60">{{ recurringDepositProduct.effectiveDate | dateFormat }}</span>
</div> </div>
} }
<div class="flex-100 layout-row"> @if (recurringDepositProduct.interestCalculationTypeName) {
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculationType.value }}</span>
</div>
@if (recurringDepositProduct.interestCalculateSelector !== undefined) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Interest Calculate:</span> <span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculateSelector }}</span> <span class="flex-60">{{ recurringDepositProduct.interestCalculationTypeName }}</span>
</div> </div>
} }
<div class="flex-100 layout-row">
<span class="flex-40">Interest Calculate:</span>
<span class="flex-60">{{ recurringDepositProduct.interestCalculationType?.value }}</span>
</div>
@if (recurringDepositProduct.averagePeriod) { @if (recurringDepositProduct.averagePeriod) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Average Period:</span> <span class="flex-40">Average Period:</span>
@@ -1,4 +1,4 @@
<!-- <!--
Copyright since 2025 Mifos Initiative Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
@@ -61,14 +61,14 @@
@if (savingProduct.effectiveTo) { @if (savingProduct.effectiveTo) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span> <span class="flex-40">Effective To:</span>
<span class="flex-60">{{ savingProduct.effectiveTo }}</span> <span class="flex-60">{{ savingProduct.effectiveTo | dateFormat }}</span>
</div> </div>
} }
@if (savingProduct.endDate) { @if (savingProduct.endDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">End Date:</span> <span class="flex-40">End Date:</span>
<span class="flex-60">{{ savingProduct.endDate }}</span> <span class="flex-60">{{ savingProduct.endDate | dateFormat }}</span>
</div> </div>
} }
@@ -158,9 +158,7 @@
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{ <span class="flex-60">{{
savingProduct.interestCalculationType savingProduct.interestCalculateSelector | find: interestCalculationTypeOptionData : 'id' : 'value'
| find: interestCalculationTypeOptionData : 'id' : 'value'
| translateKey: 'catalogs'
}}</span> }}</span>
</div> </div>
@@ -176,15 +174,16 @@
@if (savingProduct.effectiveDate) { @if (savingProduct.effectiveDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span> <span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ savingProduct.effectiveDate }}</span> <span class="flex-60">{{ savingProduct.effectiveDate | dateFormat }}</span>
</div> </div>
} }
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Interest Calculate:</span> <span class="flex-40">Interest Calculate:</span>
<span class="flex-60">{{ <span class="flex-60">{{
savingProduct.interestCalculateSelector savingProduct.interestCalculationType
| find: savingProductsTemplate?.interestCalculationTypeOptions : 'id' : 'value' | find: savingProductsTemplate?.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span> }}</span>
</div> </div>
@@ -214,7 +213,7 @@
@if (savingProduct.lastInterestDate) { @if (savingProduct.lastInterestDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span> <span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ savingProduct.lastInterestDate }}</span> <span class="flex-60">{{ savingProduct.lastInterestDate | dateFormat }}</span>
</div> </div>
} }
@@ -1,4 +1,4 @@
/** /**
* Copyright since 2025 Mifos Initiative * Copyright since 2025 Mifos Initiative
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
@@ -24,6 +24,7 @@ import {
} from '@angular/material/table'; } from '@angular/material/table';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../../pipes/find.pipe'; import { FindPipe } from '../../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -47,6 +48,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRow, MatRow,
FaIconComponent, FaIconComponent,
FindPipe, FindPipe,
DateFormatPipe,
FormatNumberPipe, FormatNumberPipe,
YesnoPipe YesnoPipe
] ]
@@ -82,11 +82,15 @@ export class SavingProductSettingsStepComponent implements OnInit {
this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency &&
this.savingProductsTemplate.lockinPeriodFrequency > 0 this.savingProductsTemplate.lockinPeriodFrequency > 0
) { ) {
// Backend returns lockinPeriodFrequencyType.id as integer (0=DAYS,1=WEEKS,2=MONTHS,3=YEARS)
// but the form dropdown uses string IDs, so reverse-map the number back to string
const reverseFrequencyTypeMap: { [key: number]: string } = { 0: 'DAYS', 1: 'WEEKS', 2: 'MONTHS', 3: 'YEARS' };
const freqTypeId = this.savingProductsTemplate.lockinPeriodFrequencyType?.id;
const freqTypeString = reverseFrequencyTypeMap[freqTypeId] ?? '';
this.savingProductSettingsForm.patchValue({ this.savingProductSettingsForm.patchValue({
lockinPeriodFrequency: this.savingProductsTemplate.lockinPeriodFrequency, lockinPeriodFrequency: this.savingProductsTemplate.lockinPeriodFrequency,
lockinPeriodFrequencyType: lockinPeriodFrequencyType: freqTypeString
this.savingProductsTemplate.lockinPeriodFrequencyType &&
this.savingProductsTemplate.lockinPeriodFrequencyType.id
}); });
} }
} }
@@ -54,14 +54,22 @@
<mat-form-field appearance="outline" class="spt-field"> <mat-form-field appearance="outline" class="spt-field">
<mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select <mat-select
formControlName="interestCalculationType" formControlName="interestCalculateSelector"
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}" matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
required
> >
@for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) { @for (opt of interestCalculationTypeOptionData; track opt.id) {
<mat-option [value]="interestCalculationType.id"> <mat-option [value]="opt.id">{{ opt.value }}</mat-option>
{{ interestCalculationType.value | translateKey: 'catalogs' }} }
</mat-option> </mat-select>
</mat-form-field>
</div>
<div class="spt-grid spt-grid--three">
<mat-form-field appearance="outline" class="spt-field">
<mat-label>Interest Calculate</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (mode of interestCalculationTypeData; track mode.id) {
<mat-option [value]="mode.id">{{ mode.value | translateKey: 'catalogs' }}</mat-option>
} }
</mat-select> </mat-select>
<mat-error> <mat-error>
@@ -69,19 +77,8 @@
<strong>{{ 'labels.commons.required' | translate }}</strong> <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div>
<div class="spt-grid spt-grid--three"> @if (savingProductTermsForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="spt-field">
<mat-label>Interest Calculate</mat-label>
<mat-select formControlName="interestCalculateSelector" required>
@for (mode of interestCalculationTypeData; track mode.id) {
<mat-option [value]="mode.id">{{ mode.value }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (savingProductTermsForm.value.interestCalculateSelector === 2) {
<mat-form-field appearance="outline" class="spt-field"> <mat-form-field appearance="outline" class="spt-field">
<mat-label>Average Period</mat-label> <mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod"> <mat-select formControlName="averagePeriod">
@@ -142,7 +139,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
@if (savingProductTermsForm.value.interestCalculateSelector === 1) { @if (savingProductTermsForm.value.interestCalculationType === 1) {
<div class="spt-note"> <div class="spt-note">
<span>⚠️</span> <span>⚠️</span>
<span <span
@@ -59,59 +59,43 @@ export class SavingProductTermsStepComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.interestCompoundingPeriodTypeData = this.savingProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestCompoundingPeriodTypeData = this.savingProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.savingProductsTemplate.interestPostingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.savingProductsTemplate.interestPostingPeriodTypeOptions;
// interestCalculationTypeData = standard enum (1=Daily, 2=Average) → "Interest Calculate" dropdown → interestCalculationType form control
this.interestCalculationTypeData = this.savingProductsTemplate.interestCalculationTypeOptions; this.interestCalculationTypeData = this.savingProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationTypeOptionData = // interestCalculationTypeOptionData = CBS code values from DB → "Interest Calculation Type" dropdown → interestCalculateSelector form control
this.savingProductsTemplate.interestCalculationTypeOption || this.interestCalculationTypeOptionData = this.savingProductsTemplate.interestCalculationTypeOption || [];
this.savingProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationDaysInYearTypeData = this.savingProductsTemplate.interestCalculationDaysInYearTypeOptions; this.interestCalculationDaysInYearTypeData = this.savingProductsTemplate.interestCalculationDaysInYearTypeOptions;
this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || []; this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || [];
this.setYesNoOptions(this.savingProductsTemplate?.yesNoOptions); this.setYesNoOptions(this.savingProductsTemplate?.yesNoOptions);
const isEditing = !!this.savingProductsTemplate.id; const isEditing = !!this.savingProductsTemplate.id;
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
// interestCalculationType = standard enum id (1=Daily Balance, 2=Average Daily Balance)
const savedEnumTypeId =
this.savingProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1;
// interestCalculateSelector = CBS code value id from interestCalculationTypeOption
const savedSelectorId = isEditing
? (this.savingProductsTemplate.interestCalculateSelector ??
this.interestCalculationTypeOptionData?.[0]?.id ??
null)
: (this.interestCalculationTypeOptionData?.[0]?.id ?? null);
this.savingProductTermsForm.patchValue({ this.savingProductTermsForm.patchValue({
nominalAnnualInterestRate: isEditing nominalAnnualInterestRate: this.savingProductsTemplate.nominalAnnualInterestRate,
? this.savingProductsTemplate.nominalAnnualInterestRate interestCompoundingPeriodType: this.savingProductsTemplate.interestCompoundingPeriodType?.id,
: this.savingProductsTemplate.nominalAnnualInterestRate, interestPostingPeriodType: this.savingProductsTemplate.interestPostingPeriodType?.id,
interestCompoundingPeriodType: isEditing interestCalculationType: savedEnumTypeId,
? this.savingProductsTemplate.interestCompoundingPeriodType?.id interestCalculationDaysInYearType: this.savingProductsTemplate.interestCalculationDaysInYearType?.id,
: this.savingProductsTemplate.interestCompoundingPeriodType?.id,
interestPostingPeriodType: isEditing
? this.savingProductsTemplate.interestPostingPeriodType?.id
: this.savingProductsTemplate.interestPostingPeriodType?.id,
interestCalculationType: isEditing
? this.savingProductsTemplate.interestCalculationTypeOption?.id ||
this.interestCalculationTypeOptionData?.[0]?.id ||
1
: this.interestCalculationTypeOptionData?.[0]?.id || 1,
interestCalculationDaysInYearType: isEditing
? this.savingProductsTemplate.interestCalculationDaysInYearType?.id
: this.savingProductsTemplate.interestCalculationDaysInYearType?.id,
effectiveDate: this.savingProductsTemplate.effectiveDate effectiveDate: this.savingProductsTemplate.effectiveDate
? new Date(this.savingProductsTemplate.effectiveDate) ? new Date(this.savingProductsTemplate.effectiveDate)
: '', : '',
interestCalculateSelector: isEditing interestCalculateSelector: savedSelectorId,
? this.savingProductsTemplate.interestCalculateSelector || 1
: this.savingProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1,
averagePeriod: isEditing ? this.savingProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, averagePeriod: isEditing ? this.savingProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod,
autoPost: isEditing ? (this.savingProductsTemplate.autoPost ?? true) : true, autoPost: isEditing ? (this.savingProductsTemplate.autoPost ?? true) : true,
frequencyFixDay: isEditing ? this.savingProductsTemplate.frequencyFixDay || 30 : 30, frequencyFixDay: isEditing ? (this.savingProductsTemplate.frequencyFixDay ?? 30) : 30,
lastInterestDate: this.savingProductsTemplate.lastInterestDate lastInterestDate: this.savingProductsTemplate.lastInterestDate
? new Date(this.savingProductsTemplate.lastInterestDate) ? new Date(this.savingProductsTemplate.lastInterestDate)
: '' : ''
}); });
// Map code value IDs to backend enum values for interestCalculationType
// Backend expects: 1=Daily Balance, 2=Average Daily Balance
const currentInterestCalcType = this.savingProductTermsForm.get('interestCalculationType').value;
if (currentInterestCalcType && currentInterestCalcType > 2) {
// If it's a code value ID (like 340, 341), map it to backend enum
// Default to 1 (Daily Balance) for custom code values
this.savingProductTermsForm.patchValue({
interestCalculationType: 1
});
}
} }
private setYesNoOptions(options: any[]) { private setYesNoOptions(options: any[]) {
@@ -198,12 +182,6 @@ export class SavingProductTermsStepComponent implements OnInit {
formValue.lastInterestDate = this.dateUtils.formatDate(prevLastInterestDate, dateFormat) || ''; formValue.lastInterestDate = this.dateUtils.formatDate(prevLastInterestDate, dateFormat) || '';
} }
// Convert interestCalculationType from code value ID to backend enum value
// Backend expects: 1=Daily Balance, 2=Average Daily Balance
if (formValue.interestCalculationType && formValue.interestCalculationType > 2) {
formValue.interestCalculationType = 1; // Default to Daily Balance
}
return { return {
...formValue ...formValue
}; };
@@ -1,4 +1,4 @@
<!-- <!--
Copyright since 2025 Mifos Initiative Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
@@ -75,21 +75,21 @@
@if (savingProduct.effectiveDate) { @if (savingProduct.effectiveDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span> <span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ savingProduct.effectiveDate }}</span> <span class="flex-60">{{ savingProduct.effectiveDate | dateFormat }}</span>
</div> </div>
} }
@if (savingProduct.effectiveTo) { @if (savingProduct.effectiveTo) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span> <span class="flex-40">Effective To:</span>
<span class="flex-60">{{ savingProduct.effectiveTo }}</span> <span class="flex-60">{{ savingProduct.effectiveTo | dateFormat }}</span>
</div> </div>
} }
@if (savingProduct.endDate) { @if (savingProduct.endDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">End Date:</span> <span class="flex-40">End Date:</span>
<span class="flex-60">{{ savingProduct.endDate }}</span> <span class="flex-60">{{ savingProduct.endDate | dateFormat }}</span>
</div> </div>
} }
@@ -164,6 +164,13 @@
<span class="flex-60">{{ savingProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }}</span> <span class="flex-60">{{ savingProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }}</span>
</div> </div>
@if (savingProduct.interestCalculationTypeName) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculation Type' | translate }}:</span>
<span class="flex-60">{{ savingProduct.interestCalculationTypeName }}</span>
</div>
}
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{ savingProduct.interestCalculationType.value | translateKey: 'catalogs' }}</span> <span class="flex-60">{{ savingProduct.interestCalculationType.value | translateKey: 'catalogs' }}</span>
@@ -177,13 +184,13 @@
@if (savingProduct.effectiveDate) { @if (savingProduct.effectiveDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Effective Date:</span> <span class="flex-40">Effective Date:</span>
<span class="flex-60">{{ savingProduct.effectiveDate }}</span> <span class="flex-60">{{ savingProduct.effectiveDate | dateFormat }}</span>
</div> </div>
} }
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Interest Calculate:</span> <span class="flex-40">Interest Calculate:</span>
<span class="flex-60">{{ savingProduct.interestCalculateSelector }}</span> <span class="flex-60">{{ savingProduct.interestCalculationType?.value | translateKey: 'catalogs' }}</span>
</div> </div>
@if (savingProduct.averagePeriod) { @if (savingProduct.averagePeriod) {
@@ -208,7 +215,7 @@
@if (savingProduct.lastInterestDate) { @if (savingProduct.lastInterestDate) {
<div class="flex-100 layout-row"> <div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span> <span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ savingProduct.lastInterestDate }}</span> <span class="flex-60">{{ savingProduct.lastInterestDate | dateFormat }}</span>
</div> </div>
} }
@@ -1,4 +1,4 @@
/** /**
* Copyright since 2025 Mifos Initiative * Copyright since 2025 Mifos Initiative
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
@@ -24,6 +24,7 @@ import {
MatRow MatRow
} from '@angular/material/table'; } from '@angular/material/table';
import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
import { DateFormatPipe } from '../../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -48,6 +49,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRow, MatRow,
ViewSavingsAccountingDetailsComponent, ViewSavingsAccountingDetailsComponent,
FormatNumberPipe, FormatNumberPipe,
DateFormatPipe,
YesnoPipe YesnoPipe
] ]
}) })
@@ -34,8 +34,9 @@
<fa-icon icon="eye" size="sm"></fa-icon> <fa-icon icon="eye" size="sm"></fa-icon>
</ng-template> </ng-template>
<!-- Step 1: Product Classification + Details -->
<mat-step [stepControl]="savingsAccountDetailsForm"> <mat-step [stepControl]="savingsAccountDetailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template> <ng-template matStepLabel>Product Classification</ng-template>
<mifosx-savings-account-details-step <mifosx-savings-account-details-step
[savingsAccountTemplate]="savingsAccountTemplate" [savingsAccountTemplate]="savingsAccountTemplate"
@@ -44,14 +45,83 @@
</mifosx-savings-account-details-step> </mifosx-savings-account-details-step>
</mat-step> </mat-step>
<mat-step [stepControl]="savingsAccountTermsForm"> <!-- Step 2: Currency -->
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template> <mat-step>
<ng-template matStepLabel>Currency</ng-template>
<mifosx-savings-account-terms-step <mifosx-savings-account-currency-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
[savingsAccountProductTemplate]="savingsAccountProductTemplate" </mifosx-savings-account-currency-step>
></mifosx-savings-account-terms-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step> </mat-step>
<!-- Step 3: Term -->
<mat-step [stepControl]="savingsAccountTermsForm">
<ng-template matStepLabel>Term</ng-template>
<mifosx-savings-account-term-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-term-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="savingsAccountTermsForm.invalid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 4: Settings -->
<mat-step [stepControl]="savingsAccountSettingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-savings-account-settings-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-settings-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 5: Digital -->
<mat-step>
<ng-template matStepLabel>Digital</ng-template>
<mifosx-savings-account-digital-step [savingsAccountProductTemplate]="savingsAccountProductTemplate">
</mifosx-savings-account-digital-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 6: Charges -->
<mat-step> <mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
@@ -63,6 +133,7 @@
</mifosx-savings-account-charges-step> </mifosx-savings-account-charges-step>
</mat-step> </mat-step>
<!-- Step 7: Preview -->
@if (savingsAccountFormValid) { @if (savingsAccountFormValid) {
<mat-step completed> <mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
@@ -12,8 +12,11 @@ import { Router, ActivatedRoute } from '@angular/router';
/** Custom Components */ /** Custom Components */
import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component'; import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component';
import { SavingsAccountTermsStepComponent } from '../savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component'; import { SavingsAccountTermStepComponent } from '../savings-account-stepper/savings-account-term-step/savings-account-term-step.component';
import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component';
import { SavingsAccountChargesStepComponent } from '../savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component'; import { SavingsAccountChargesStepComponent } from '../savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component';
import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component';
import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component';
/** Custom Services */ /** Custom Services */
import { SavingsService } from '../savings.service'; import { SavingsService } from '../savings.service';
@@ -39,8 +42,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatStep, MatStep,
MatStepLabel, MatStepLabel,
SavingsAccountDetailsStepComponent, SavingsAccountDetailsStepComponent,
SavingsAccountTermsStepComponent, SavingsAccountCurrencyStepComponent,
SavingsAccountTermStepComponent,
SavingsAccountSettingsStepComponent,
SavingsAccountChargesStepComponent, SavingsAccountChargesStepComponent,
SavingsAccountDigitalStepComponent,
SavingsAccountPreviewStepComponent SavingsAccountPreviewStepComponent
] ]
}) })
@@ -56,75 +62,58 @@ export class CreateSavingsAccountComponent {
/** Savings Account Product Template */ /** Savings Account Product Template */
savingsAccountProductTemplate: any; savingsAccountProductTemplate: any;
/** Savings Account Details Step */
@ViewChild(SavingsAccountDetailsStepComponent, { static: true }) @ViewChild(SavingsAccountDetailsStepComponent, { static: true })
savingsAccountDetailsStep: SavingsAccountDetailsStepComponent; savingsAccountDetailsStep: SavingsAccountDetailsStepComponent;
/** Savings Account Terms Step */
@ViewChild(SavingsAccountTermsStepComponent, { static: true }) @ViewChild(SavingsAccountTermStepComponent, { static: true })
savingsAccountTermsStep: SavingsAccountTermsStepComponent; savingsAccountTermStep: SavingsAccountTermStepComponent;
/** Savings Account Charges Step */
@ViewChild(SavingsAccountSettingsStepComponent, { static: true })
savingsAccountSettingsStep: SavingsAccountSettingsStepComponent;
@ViewChild(SavingsAccountChargesStepComponent, { static: true }) @ViewChild(SavingsAccountChargesStepComponent, { static: true })
savingsAccountChargesStep: SavingsAccountChargesStepComponent; savingsAccountChargesStep: SavingsAccountChargesStepComponent;
/**
* Fetches savings account template from `resolve`
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {Dates} dateUtils Date Utils
* @param {SavingsService} savingsService Savings Service
* @param {SettingsService} settingsService Settings Service
*/
constructor() { constructor() {
this.route.data.subscribe((data: { savingsAccountTemplate: any }) => { this.route.data.subscribe((data: { savingsAccountTemplate: any }) => {
this.savingsAccountTemplate = data.savingsAccountTemplate; this.savingsAccountTemplate = data.savingsAccountTemplate;
}); });
} }
/**
* Sets savings account product template.
* @param {any} $event API response
*/
setTemplate($event: any) { setTemplate($event: any) {
this.savingsAccountProductTemplate = $event; this.savingsAccountProductTemplate = $event;
} }
/**
* Retrieves savings account details form.
*/
get savingsAccountDetailsForm() { get savingsAccountDetailsForm() {
return this.savingsAccountDetailsStep.savingsAccountDetailsForm; return this.savingsAccountDetailsStep.savingsAccountDetailsForm;
} }
/**
* Retrieves savings account terms form.
*/
get savingsAccountTermsForm() { get savingsAccountTermsForm() {
return this.savingsAccountTermsStep.savingsAccountTermsForm; return this.savingsAccountTermStep.savingsAccountTermForm;
}
get savingsAccountSettingsForm() {
return this.savingsAccountSettingsStep.savingsAccountSettingsForm;
} }
/**
* Checks validity of overall savings account form.
*/
get savingsAccountFormValid() { get savingsAccountFormValid() {
return this.savingsAccountDetailsForm.valid && this.savingsAccountTermsForm.valid; return (
this.savingsAccountDetailsForm.valid &&
this.savingsAccountTermsForm.valid &&
this.savingsAccountSettingsForm.valid
);
} }
/**
* Retrieves savings account object.
*/
get savingsAccount() { get savingsAccount() {
return { return {
...this.savingsAccountDetailsStep.savingsAccountDetails, ...this.savingsAccountDetailsStep.savingsAccountDetails,
...this.savingsAccountTermsStep.savingsAccountTerms, ...this.savingsAccountTermStep.savingsAccountTerms,
...this.savingsAccountSettingsStep.savingsAccountSettings,
...this.savingsAccountChargesStep.savingsAccountCharges ...this.savingsAccountChargesStep.savingsAccountCharges
}; };
} }
/**
* Creates a new share account.
*/
submit() { submit() {
// TODO: Update once language and date settings are setup
const locale = this.settingsService.language.code; const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
@@ -7,7 +7,7 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; import { Component, OnInit, Input, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { UntypedFormControl } from '@angular/forms'; import { UntypedFormControl } from '@angular/forms';
@@ -69,13 +69,25 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FormatNumberPipe FormatNumberPipe
] ]
}) })
export class SavingsAccountChargesStepComponent implements OnInit, OnChanges { export class SavingsAccountChargesStepComponent implements OnInit {
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private translateService = inject(TranslateService); private translateService = inject(TranslateService);
private _savingsAccountProductTemplate: any;
/** Savings Account Product Template */ /** Savings Account Product Template */
@Input() savingsAccountProductTemplate: any; @Input()
set savingsAccountProductTemplate(value: any) {
this._savingsAccountProductTemplate = value;
if (value) {
this.chargeData = value.chargeOptions || [];
this.chargesDataSource = (value.charges || []).map((charge: any) => ({ ...charge, id: charge.chargeId }));
}
}
get savingsAccountProductTemplate(): any {
return this._savingsAccountProductTemplate;
}
/** Savings Account Template */ /** Savings Account Template */
@Input() savingsAccountTemplate: any; @Input() savingsAccountTemplate: any;
/** Currency Code */ /** Currency Code */
@@ -126,14 +138,6 @@ export class SavingsAccountChargesStepComponent implements OnInit, OnChanges {
} }
} }
ngOnChanges() {
if (this.savingsAccountProductTemplate) {
this.chargeData = this.savingsAccountProductTemplate.chargeOptions;
this.chargesDataSource =
this.savingsAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
}
}
/** /**
* Adds the charge to charges table * Adds the charge to charges table
* @param {any} charge Charge * @param {any} charge Charge
@@ -0,0 +1,49 @@
<!--
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="sac-section">
<div class="sac-section__header">
<i class="fas fa-coins"></i>
<span>Currency &amp; Account Status Configuration</span>
</div>
<div class="sac-body">
<div class="sac-grid">
<mat-form-field appearance="outline" class="sac-field">
<mat-label>Currency</mat-label>
<input matInput [value]="savingsAccountProductTemplate?.currency?.name || ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="sac-field">
<mat-label>Decimal Places</mat-label>
<input matInput [value]="savingsAccountProductTemplate?.currency?.decimalPlaces || ''" readonly />
<mat-hint>Inherited from selected product</mat-hint>
</mat-form-field>
</div>
<div class="sac-alert">
<div class="sac-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<span class="sac-badge" [class.sac-badge--yes]="savingsAccountProductTemplate?.dormancyAlert">
{{ savingsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }}
</span>
</div>
<div class="sac-alert">
<div class="sac-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>
<span class="sac-badge" [class.sac-badge--yes]="savingsAccountProductTemplate?.nominationFacility">
{{ savingsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
</span>
</div>
</div>
</div>
@@ -0,0 +1,116 @@
/**
* 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;
.sac-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;
}
span {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.sac-body {
background: #fff;
}
.sac-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sac-field {
width: 100%;
}
.sac-alert {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 18px 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;
}
}
}
.sac-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
padding: 4px 14px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #fee2e2;
color: #b91c1c;
border: 1px solid #fecaca;
&--yes {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
@@ -0,0 +1,20 @@
/**
* 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 } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-savings-account-currency-step',
templateUrl: './savings-account-currency-step.component.html',
styleUrls: ['./savings-account-currency-step.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class SavingsAccountCurrencyStepComponent {
@Input() savingsAccountProductTemplate: any;
}
@@ -1,4 +1,4 @@
<!-- <!--
Copyright since 2025 Mifos Initiative Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public This Source Code Form is subject to the terms of the Mozilla Public
@@ -7,63 +7,119 @@
--> -->
<form [formGroup]="savingsAccountDetailsForm"> <form [formGroup]="savingsAccountDetailsForm">
<div class="layout-row-wrap gap-2px responsive-column"> <!-- Account Details -->
<mat-form-field class="flex-48"> <div class="sad-section">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label> <div class="sad-section__header">
<mat-select formControlName="productId" required> <i class="fas fa-info-circle"></i>
@for (product of productData; track product) { <h3>Account Details</h3>
<mat-option [value]="product.id"> </div>
{{ product.name }} <div class="sad-grid">
</mat-option> <mat-form-field appearance="outline" class="sad-field">
} <mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
</mat-select> <mat-select formControlName="productId" required>
<mat-error> @for (product of productData; track product) {
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} <mat-option [value]="product.id">{{ product.name }}</mat-option>
<strong>{{ 'labels.commons.required' | translate }}</strong> }
</mat-error> </mat-select>
</mat-form-field>
@if (savingsProductSelected) {
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error> <mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.inputs.Product Name' | 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>
}
@if (savingsProductSelected) { @if (savingsProductSelected) {
<mat-form-field class="flex-48"> <mat-form-field appearance="outline" class="sad-field" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId"> <input
@for (officer of fieldOfficerData; track officer) { matInput
<mat-option [value]="officer.id"> [min]="minDate"
{{ officer.displayName }} [max]="maxDate"
</mat-option> [matDatepicker]="submittedOnDatePicker"
} formControlName="submittedOnDate"
</mat-select> required
</mat-form-field> />
} <mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
@if (savingsProductSelected) { @if (savingsProductSelected) {
<mat-form-field class="flex-48"> <mat-form-field appearance="outline" class="sad-field">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<input matInput formControlName="externalId" /> <mat-select formControlName="fieldOfficerId">
</mat-form-field> @for (officer of fieldOfficerData; track officer) {
} <mat-option [value]="officer.id">{{ officer.displayName }}</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (savingsProductSelected) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
}
</div>
</div> </div>
<!-- Product Classification (read-only, shown after product is selected) -->
@if (savingsProductSelected) {
<div class="sad-section sad-section--mt">
<div class="sad-section__header">
<i class="fas fa-tags"></i>
<h3>Product Classification</h3>
<span class="sad-badge">From Product</span>
</div>
<div class="sad-grid sad-grid--classification">
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Account Type</mat-label>
<input matInput [value]="productTemplate.accountType || ''" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Customer Segment</mat-label>
<input matInput [value]="productTemplate.customerSegment || ''" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Product Code</mat-label>
<input matInput [value]="productTemplate.productCode || ''" readonly />
</mat-form-field>
@if (productTemplate.effectiveDate) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Effective Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.effectiveDate)" readonly />
</mat-form-field>
}
@if (productTemplate.endDate) {
<mat-form-field appearance="outline" class="sad-field">
<mat-label>End Date</mat-label>
<input matInput [value]="formatTemplateDate(productTemplate.endDate)" readonly />
</mat-form-field>
}
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Introducer Required</mat-label>
<input matInput [value]="productTemplate.introducerRequired ? 'Yes' : 'No'" readonly />
</mat-form-field>
<div class="sad-field sad-info-row">
<span class="sad-info-label">Share Linking Enabled</span>
<span class="sad-badge" [class.sad-badge--yes]="productTemplate.shareLinkingEnabled">
{{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }}
</span>
</div>
</div>
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled> <button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon> <fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
@@ -5,3 +5,101 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-900: #202124;
.sad-section {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
&--mt {
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;
flex: 1;
}
}
}
.sad-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sad-grid--classification {
grid-template-columns: repeat(3, 1fr);
@media (width <= 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sad-field {
width: 100%;
}
.sad-info-row {
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
}
.sad-info-label {
font-size: 14px;
color: $gray-900;
font-weight: 500;
}
.sad-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
&--yes {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
@@ -1,4 +1,4 @@
/** /**
* Copyright since 2025 Mifos Initiative * Copyright since 2025 Mifos Initiative
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
@@ -7,12 +7,13 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
/** Custom Services */ /** Custom Services */
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
import { SettingsService } from 'app/settings/settings.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -35,6 +36,8 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private savingsService = inject(SavingsService); private savingsService = inject(SavingsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
/** Savings Account Template */ /** Savings Account Template */
@Input() savingsAccountTemplate: any; @Input() savingsAccountTemplate: any;
@@ -57,6 +60,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
/** Savings Account Template with product data */ /** Savings Account Template with product data */
@Output() savingsAccountProductTemplate = new EventEmitter(); @Output() savingsAccountProductTemplate = new EventEmitter();
/** Local copy of product template for displaying classification info */
productTemplate: any = null;
/** /**
* Sets share account details form. * Sets share account details form.
* @param {FormBuilder} formBuilder Form Builder. * @param {FormBuilder} formBuilder Form Builder.
@@ -115,9 +121,11 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
this.savingsService this.savingsService
.getSavingsAccountTemplate(entityId, productId, this.savingsAccountTemplate.groupId ? true : false) .getSavingsAccountTemplate(entityId, productId, this.savingsAccountTemplate.groupId ? true : false)
.subscribe((response: any) => { .subscribe((response: any) => {
this.productTemplate = response;
this.savingsAccountProductTemplate.emit(response); this.savingsAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions; this.fieldOfficerData = response.fieldOfficerOptions;
this.savingsProductSelected = true; this.savingsProductSelected = true;
this.cdr.detectChanges();
if (!this.isFieldOfficerPatched && this.savingsAccountTemplate.fieldOfficerId) { if (!this.isFieldOfficerPatched && this.savingsAccountTemplate.fieldOfficerId) {
this.savingsAccountDetailsForm.get('fieldOfficerId').patchValue(this.savingsAccountTemplate.fieldOfficerId); this.savingsAccountDetailsForm.get('fieldOfficerId').patchValue(this.savingsAccountTemplate.fieldOfficerId);
this.isFieldOfficerPatched = true; this.isFieldOfficerPatched = true;
@@ -128,6 +136,32 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
}); });
} }
/**
* Formats template dates into the current CBS date format.
* Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects.
*/
formatTemplateDate(value: any): string {
if (!value) {
return '';
}
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
} else if (Array.isArray(value) && value.length >= 3) {
dateValue = new Date(value[0], value[1] - 1, value[2]);
} else {
dateValue = new Date(value);
}
if (!dateValue || Number.isNaN(dateValue.getTime())) {
return Array.isArray(value) ? value.join('/') : String(value);
}
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
/** /**
* Returns savings account form value. * Returns savings account form value.
*/ */
@@ -0,0 +1,234 @@
<!--
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">FROM PRODUCT</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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<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" disabled>
<option [ngValue]="false">No</option>
<option [ngValue]="true">Yes</option>
</select>
</label>
</div>
</div>
@if (accountFacilityLimits.length > 0) {
<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" [value]="row.generalLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.minorAccountLimit" disabled /></td>
<td><input type="text" class="spl-input" [value]="row.smallSavingLimit" disabled /></td>
</tr>
}
</tbody>
</table>
</div>
}
@if (depositLimitKyc?.general) {
<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" disabled>
<option>{{ depositLimitKyc.general.kycType }}</option>
</select>
</td>
<td><input type="text" class="spl-input" [value]="depositLimitKyc.general.limit" disabled /></td>
</tr>
<tr>
<td>Minor</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.minor.kycType" disabled>
<option>{{ depositLimitKyc.minor.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.minor.limit" disabled /></td>
</tr>
<tr>
<td>Small Saving</td>
<td>
<select class="spl-select" [ngModel]="depositLimitKyc.smallSaving.kycType" disabled>
<option>{{ depositLimitKyc.smallSaving.kycType }}</option>
</select>
</td>
<td><input type="number" class="spl-input" [value]="depositLimitKyc.smallSaving.limit" disabled /></td>
</tr>
</tbody>
</table>
</div>
}
<div class="spl-note">
<strong>Note:</strong>
These facilities are defined at product level and applied to this account. Small Saving Account limits are
strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular.
</div>
</div>
</div>
@@ -0,0 +1,199 @@
/**
* 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;
.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-table-wrap {
overflow-x: auto;
margin-top: 16px;
}
.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;
}
td:first-child {
background: #f8fafc;
font-weight: 600;
color: $text;
white-space: nowrap;
}
td {
font-size: 13px;
overflow: hidden;
}
}
.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: #f8fafc;
color: $muted;
}
.spl-select {
height: 30px;
border: 1px solid $border-strong;
border-radius: 2px;
padding: 0 6px;
background: #f8fafc;
color: $muted;
}
.spl-features {
margin-bottom: 16px;
}
.spl-features__header {
font-size: 13px;
font-weight: 600;
color: $muted;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.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;
font-weight: 600;
color: #334155;
i {
color: #2563eb;
font-size: 12px;
width: 14px;
text-align: center;
}
}
.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;
}
@@ -0,0 +1,69 @@
/**
* 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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
const DEFAULT_FACILITY_CONFIG = {
atmFacility: false,
atmWithdrawalFacility: false,
upiFacility: false,
posTransactionsFacility: false,
chequeBook: false,
impsFacility: false,
smsAlerts: false,
bbpsFacility: false,
insuranceCover: false,
bepgFacility: false,
neftFacility: false,
rtgsFacility: false,
interBankTotalCapFacility: false,
withdrawalFacility: false,
depositFacilityKyc: false,
virtualDebitCard: false
};
@Component({
selector: 'mifosx-savings-account-digital-step',
templateUrl: './savings-account-digital-step.component.html',
styleUrls: ['./savings-account-digital-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule
]
})
export class SavingsAccountDigitalStepComponent {
private _savingsAccountProductTemplate: any;
@Input()
set savingsAccountProductTemplate(value: any) {
this._savingsAccountProductTemplate = value;
}
get savingsAccountProductTemplate(): any {
return this._savingsAccountProductTemplate;
}
get accountFacilityConfig() {
return this._savingsAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG;
}
get accountFacilityLimits(): any[] {
return this._savingsAccountProductTemplate?.accountFacilityLimits || [];
}
get depositLimitKyc() {
return (
this._savingsAccountProductTemplate?.depositLimitKyc || {
general: { kycType: '', limit: '' },
minor: { kycType: '', limit: '' },
smallSaving: { kycType: '', limit: '' }
}
);
}
}
@@ -6,251 +6,558 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<div class="responsive-column"> <div class="preview-wrapper">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3> <div class="preview-section">
<div class="preview-section__header">
<mat-divider class="flex-fill"></mat-divider> <span class="preview-section__title">{{ 'labels.heading.Details' | translate }}</span>
<div class="flex-fill layout-row"> </div>
<span class="flex-40">{{ 'labels.inputs.Product' | translate }}</span> <div class="preview-body">
<span class="flex-60">{{ <div class="preview-grid preview-grid--three">
savingsAccount.productId | find: savingsAccountTemplate.productOptions : 'id' : 'name' <div class="preview-field">
}}</span> <span class="preview-label">{{ 'labels.inputs.Product' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.productId | find: savingsAccountTemplate?.productOptions : 'id' : 'name'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Submitted On' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.submittedOnDate | dateFormat }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Field Officer' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.fieldOfficerId
| find: savingsAccountProductTemplate?.fieldOfficerOptions : 'id' : 'displayName'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.External Id' | translate }}</span>
@if (savingsAccount?.externalId) {
<span class="preview-value">
<mifosx-external-identifier externalId="{{ savingsAccount.externalId }}"></mifosx-external-identifier>
</span>
} @else {
<span class="preview-value">{{ 'labels.inputs.Unassigned' | translate }}</span>
}
</div>
</div>
</div>
</div> </div>
<div class="flex-fill layout-row"> @if (
<span class="flex-40">{{ 'labels.inputs.Submitted On' | translate }}</span> savingsAccountProductTemplate?.accountType ||
<span class="flex-60">{{ savingsAccount.submittedOnDate | dateFormat }}</span> savingsAccountProductTemplate?.customerSegment ||
savingsAccountProductTemplate?.productCode ||
savingsAccountProductTemplate?.effectiveDate ||
savingsAccountProductTemplate?.endDate ||
savingsAccountProductTemplate?.introducerRequired !== undefined ||
savingsAccountProductTemplate?.shareLinkingEnabled !== undefined
) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">Product Classification</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (savingsAccountProductTemplate?.accountType) {
<div class="preview-field">
<span class="preview-label">Account Type</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.accountType }}</span>
</div>
}
@if (savingsAccountProductTemplate?.customerSegment) {
<div class="preview-field">
<span class="preview-label">Customer Segment</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.customerSegment }}</span>
</div>
}
@if (savingsAccountProductTemplate?.productCode) {
<div class="preview-field">
<span class="preview-label">Product Code</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.productCode }}</span>
</div>
}
@if (savingsAccountProductTemplate?.effectiveDate) {
<div class="preview-field">
<span class="preview-label">Effective Date</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.effectiveDate | dateFormat }}</span>
</div>
}
@if (savingsAccountProductTemplate?.endDate) {
<div class="preview-field">
<span class="preview-label">End Date</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.endDate | dateFormat }}</span>
</div>
}
@if (
savingsAccountProductTemplate?.introducerRequired !== undefined &&
savingsAccountProductTemplate?.introducerRequired !== null
) {
<div class="preview-field">
<span class="preview-label">Introducer Required</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.introducerRequired"
>
{{ savingsAccountProductTemplate?.introducerRequired | yesNo }}
</span>
</div>
}
@if (
savingsAccountProductTemplate?.shareLinkingEnabled !== undefined &&
savingsAccountProductTemplate?.shareLinkingEnabled !== null
) {
<div class="preview-field">
<span class="preview-label">Share Linking Enabled</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.shareLinkingEnabled"
>
{{ savingsAccountProductTemplate?.shareLinkingEnabled | yesNo }}
</span>
</div>
}
</div>
</div>
</div>
}
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Currency' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="preview-value">{{ savingsAccountTermsForm?.get('currencyCode')?.value }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.currency?.decimalPlaces }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Currency In Multiples Of' | translate }}</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.currency?.inMultiplesOf }}</span>
</div>
@if (
savingsAccountProductTemplate?.dormancyAlert !== undefined &&
savingsAccountProductTemplate?.dormancyAlert !== null
) {
<div class="preview-field">
<span class="preview-label">Dormancy Alert</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.dormancyAlert"
>
{{ savingsAccountProductTemplate?.dormancyAlert | yesNo }}
</span>
</div>
}
@if (
savingsAccountProductTemplate?.balanceSwapOption !== undefined &&
savingsAccountProductTemplate?.balanceSwapOption !== null
) {
<div class="preview-field">
<span class="preview-label">Balance Swap Option</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.balanceSwapOption"
>
{{ savingsAccountProductTemplate?.balanceSwapOption | yesNo }}
</span>
</div>
}
@if (savingsAccountProductTemplate?.msme !== undefined && savingsAccountProductTemplate?.msme !== null) {
<div class="preview-field">
<span class="preview-label">MSME</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="savingsAccountProductTemplate?.msme">
{{ savingsAccountProductTemplate?.msme | yesNo }}
</span>
</div>
}
@if (
savingsAccountProductTemplate?.nominationFacility !== undefined &&
savingsAccountProductTemplate?.nominationFacility !== null
) {
<div class="preview-field">
<span class="preview-label">Nomination Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.nominationFacility"
>
{{ savingsAccountProductTemplate?.nominationFacility | yesNo }}
</span>
</div>
}
@if (
savingsAccountProductTemplate?.autoDebitFacility !== undefined &&
savingsAccountProductTemplate?.autoDebitFacility !== null
) {
<div class="preview-field">
<span class="preview-label">Auto Debit Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.autoDebitFacility"
>
{{ savingsAccountProductTemplate?.autoDebitFacility | yesNo }}
</span>
</div>
}
</div>
</div>
</div> </div>
<div class="flex-fill layout-row"> <div class="preview-section">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span> <div class="preview-section__header">
@if (savingsAccount.externalId) { <span class="preview-section__title">{{ 'labels.heading.Terms' | translate }}</span>
<span class="flex-60"> </div>
<mifosx-external-identifier externalId="{{ savingsAccount.externalId }}"></mifosx-external-identifier> <div class="preview-body">
</span> <div class="preview-grid preview-grid--three">
} <div class="preview-field">
@if (!savingsAccount.externalId) { <span class="preview-label">{{ 'labels.inputs.Nominal Annual Interest' | translate }}</span>
<span class="flex-60"> <span class="preview-value">{{ savingsAccount?.nominalAnnualInterestRate | formatNumber }} %</span>
{{ 'labels.inputs.Unassigned' | translate }} </div>
</span> @if (savingsAccountProductTemplate?.interestCalculationTypeName) {
} <div class="preview-field">
<span class="preview-label">Interest Calculation Mode</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.interestCalculationTypeName }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Compounding Period' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestCompoundingPeriodType
| find: savingsAccountProductTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Posting Period' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestPostingPeriodType
| find: savingsAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Interest Calculated using' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestCalculationType
| find: savingsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Days in Year' | translate }}</span>
<span class="preview-value">{{
savingsAccount?.interestCalculationDaysInYearType
| find: savingsAccountProductTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
<div class="preview-field">
<span class="preview-label">Interest Calculation Selector</span>
<span class="preview-value">{{
savingsAccount?.interestCalculateSelector
| find: savingsAccountProductTemplate?.interestCalculationTypeOption : 'id' : 'value'
| translateKey: 'catalogs'
}}</span>
</div>
@if (savingsAccount?.averagePeriod) {
<div class="preview-field">
<span class="preview-label">Average Period</span>
<span class="preview-value">{{ savingsAccount?.averagePeriod }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Auto Post</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="savingsAccount?.autoPost">
{{ savingsAccount?.autoPost | yesNo }}
</span>
</div>
@if (savingsAccountProductTemplate?.lastInterestDate) {
<div class="preview-field">
<span class="preview-label">Last Interest Date</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.lastInterestDate | dateFormat }}</span>
</div>
}
@if (savingsAccount?.frequencyFixDay !== undefined && savingsAccount?.frequencyFixDay !== null) {
<div class="preview-field">
<span class="preview-label">Frequency Fix Day</span>
<span class="preview-value">{{ savingsAccount?.frequencyFixDay }}</span>
</div>
}
</div>
</div>
</div> </div>
<div class="flex-fill layout-row"> <div class="preview-section">
<span class="flex-40">{{ 'labels.inputs.Field Officer' | translate }}</span> <div class="preview-section__header">
<span class="flex-60">{{ <span class="preview-section__title">{{ 'labels.heading.Settings' | translate }}</span>
savingsAccount.fieldOfficerId | find: savingsAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName' </div>
}}</span> <div class="preview-body">
<div class="preview-grid preview-grid--three">
@if (savingsAccount?.minRequiredOpeningBalance) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Opening Balance' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccount?.withdrawalFeeForTransfers"
>
{{ savingsAccount?.withdrawalFeeForTransfers | yesNo }}
</span>
</div>
@if (savingsAccount?.lockinPeriodFrequency) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Lock-in Period' | translate }}</span>
<span class="preview-value">
{{ savingsAccount?.lockinPeriodFrequency }}
{{
savingsAccount?.lockinPeriodFrequencyType
| find: savingsAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value'
| translateKey: 'catalogs'
}}
</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="savingsAccount?.allowOverdraft">
{{ savingsAccount?.allowOverdraft | yesNo }}
</span>
</div>
@if (savingsAccount?.minOverdraftForInterestCalculation) {
<div class="preview-field">
<span class="preview-label">{{
'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate
}}</span>
<span class="preview-value">{{ savingsAccount?.minOverdraftForInterestCalculation | formatNumber }}</span>
</div>
}
@if (savingsAccount?.nominalAnnualInterestRateOverdraft) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
</div>
}
@if (savingsAccount?.overdraftLimit) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.overdraftLimit | formatNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccount?.enforceMinRequiredBalance"
>
{{ savingsAccount?.enforceMinRequiredBalance | yesNo }}
</span>
</div>
@if (savingsAccountProductTemplate?.enableLockinPeriod !== undefined) {
<div class="preview-field">
<span class="preview-label">Enable Lock-in Period</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.enableLockinPeriod"
>
{{ savingsAccountProductTemplate?.enableLockinPeriod | yesNo }}
</span>
</div>
}
@if (savingsAccountProductTemplate?.balanceFrequency) {
<div class="preview-field">
<span class="preview-label">Balance Frequency</span>
<span class="preview-value">{{ savingsAccountProductTemplate?.balanceFrequency }}</span>
</div>
}
@if (savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing !== undefined) {
<div class="preview-field">
<span class="preview-label">Account Closed Zero Balance In Day Closing</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing"
>
{{ savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing | yesNo }}
</span>
</div>
}
@if (savingsAccount?.minRequiredBalance) {
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Minimum Balance' | translate }}</span>
<span class="preview-value">{{ savingsAccount?.minRequiredBalance }}</span>
</div>
}
</div>
</div>
</div> </div>
@if (hasDigitalSection) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">Digital</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@for (facility of digitalFacilityLabels; track facility.key) {
<div class="preview-field">
<span class="preview-label">{{ facility.label }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="digitalPreview?.accountFacilityConfig?.[facility.key]"
>
{{ digitalPreview?.accountFacilityConfig?.[facility.key] | yesNo }}
</span>
</div>
}
</div>
@if (digitalPreview?.accountFacilityLimits?.length) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Limit Type</th>
<th>General Limit</th>
<th>Minor Account Limit</th>
<th>Small Saving Limit</th>
</tr>
</thead>
<tbody>
@for (row of digitalPreview.accountFacilityLimits; track row.limitType) {
<tr>
<td>{{ row.limitType }}</td>
<td>{{ row.generalLimit || 'Unassigned' }}</td>
<td>{{ row.minorAccountLimit || 'Unassigned' }}</td>
<td>{{ row.smallSavingLimit || 'Unassigned' }}</td>
</tr>
}
</tbody>
</table>
}
@if (digitalPreview?.depositLimitKyc?.general) {
<table class="mat-elevation-z1 preview-table">
<thead>
<tr>
<th>Account Type</th>
<th>KYC Type</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>{{ digitalPreview.depositLimitKyc.general.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.general.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Minor</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.minor?.limit || 'Unassigned' }}</td>
</tr>
<tr>
<td>Small Saving</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.kycType || 'Unassigned' }}</td>
<td>{{ digitalPreview.depositLimitKyc.smallSaving?.limit || 'Unassigned' }}</td>
</tr>
</tbody>
</table>
}
</div>
</div>
}
@if (activeClientMembers) { @if (activeClientMembers) {
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Client Members' | translate }}</h3> <div class="preview-section">
<mat-divider class="flex-fill"></mat-divider> <div class="preview-section__header">
<div class="flex-60"> <span class="preview-section__title">{{ 'labels.heading.Client Members' | translate }}</span>
<table mat-table #membersTable [dataSource]="dataSource"> </div>
<!-- ID Column --> <div class="preview-body">
<ng-container matColumnDef="id"> <table class="mat-elevation-z1 preview-table" mat-table #membersTable [dataSource]="dataSource">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th> <ng-container matColumnDef="id">
<td mat-cell *matCellDef="let element">{{ element.id }}</td> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
</ng-container> <td mat-cell *matCellDef="let element">{{ element.id }}</td>
<!-- Name Column --> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.displayName }}</td> <td mat-cell *matCellDef="let element">{{ element.displayName }}</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="membersDisplayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="membersDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: membersDisplayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: membersDisplayedColumns"></tr>
</table> </table>
</div>
</div> </div>
} }
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Terms' | translate }}</h3> @if (savingsAccount?.charges?.length) {
<div class="preview-section">
<mat-divider class="flex-fill"></mat-divider> <div class="preview-section__header">
<div class="flex-fill layout-row"> <span class="preview-section__title">{{ 'labels.inputs.Charges' | translate }}</span>
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}</span> </div>
<span class="flex-60">{{ savingsAccountTermsForm.get('currencyCode').value }}</span> <div class="preview-body">
</div> <table class="mat-elevation-z1 preview-table" mat-table [dataSource]="savingsAccount.charges">
<ng-container matColumnDef="name">
<div class="flex-fill layout-row"> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span> <td mat-cell *matCellDef="let charge">{{ charge.name + ', ' + charge.currency.displaySymbol }}</td>
<span class="flex-60">{{ savingsAccountTermsForm.get('decimal').value }}</span> </ng-container>
</div> <ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<div class="flex-fill layout-row"> <td mat-cell *matCellDef="let charge">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span> {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}
<span class="flex-60">{{ savingsAccount.nominalAnnualInterestRate | formatNumber }} %</span> </td>
</div> </ng-container>
<ng-container matColumnDef="amount">
<div class="flex-fill layout-row"> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span> <td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
<span class="flex-60">{{ </ng-container>
getCatalogTranslation( <ng-container matColumnDef="date">
savingsAccount.interestCompoundingPeriodType <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
| find: savingsAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' <td mat-cell *matCellDef="let charge">
) @if (
}}</span> charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee'
</div> ) {
<span>{{ (charge.dueDate | dateFormat) || 'Unassigned' }}</span>
<div class="flex-fill layout-row"> }
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span> @if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span class="flex-60">{{ <span>{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}</span>
getCatalogTranslation( }
savingsAccount.interestPostingPeriodType @if (
| find: savingsAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' !(
) charge.chargeTimeType.value === 'Monthly Fee' ||
}}</span> charge.chargeTimeType.value === 'Annual Fee' ||
</div> charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
<div class="flex-fill layout-row"> )
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span> ) {
<span class="flex-60">{{ <span>N/A</span>
getCatalogTranslation( }
savingsAccount.interestCalculationType </td>
| find: savingsAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value' </ng-container>
) <ng-container matColumnDef="repaymentsEvery">
}}</span> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
</div> <td mat-cell *matCellDef="let charge">{{ charge.feeInterval || 'Not Provided' }}</td>
</ng-container>
<div class="flex-fill layout-row"> <ng-container matColumnDef="chargeTimeType">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span> <th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<span class="flex-60">{{ <td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType.value }}</td>
getCatalogTranslation( </ng-container>
savingsAccount.interestCalculationDaysInYearType <tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
| find: savingsAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' <tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
) </table>
}}</span> </div>
</div>
@if (savingsAccount.minRequiredOpeningBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.withdrawalFeeForTransfers | yesNo }}</span>
</div>
@if (savingsAccount.lockinPeriodFrequency) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
savingsAccount.lockinPeriodFrequency +
' ' +
(savingsAccount.lockinPeriodFrequencyType
| find: savingsAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value')
)
}}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.allowOverdraft | yesNo }}</span>
</div>
@if (savingsAccount.minOverdraftForInterestCalculation) {
<div class="flex-fill layout-row">
<span class="flex-40"
>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}:</span
>
<span class="flex-60">{{ savingsAccount.minOverdraftForInterestCalculation | formatNumber }}</span>
</div>
}
@if (savingsAccount.nominalAnnualInterestRateOverdraft) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
</div>
}
@if (savingsAccount.overdraftLimit) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.overdraftLimit | formatNumber }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.enforceMinRequiredBalance | yesNo }}</span>
</div>
@if (savingsAccount.minRequiredBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ savingsAccount.minRequiredBalance }}</span>
</div>
}
@if (savingsAccount.charges.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="savingsAccount.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="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') {
<span>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
}
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span>
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}
</span>
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
) {
<span> N/A </span>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.feeInterval || 'Not Provided' }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div> </div>
} }
</div> </div>
@@ -6,27 +6,95 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
table { .preview-wrapper {
width: 100%; display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 0.5rem 0;
} }
.mat-elevation-z1 { .preview-section {
margin: 1em 0 1.5em; border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
&__header {
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
padding: 0.6rem 1rem;
}
&__title {
font-size: 0.95rem;
font-weight: 600;
color: #333;
text-transform: uppercase;
letter-spacing: 0.04em;
}
} }
h2, .preview-body {
h3, padding: 1rem 1.25rem;
h4 { }
margin: 0;
.preview-grid {
display: grid;
gap: 0.75rem 1.5rem;
&--three {
grid-template-columns: repeat(3, 1fr);
@media (width <= 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 480px) {
grid-template-columns: 1fr;
}
}
}
.preview-field {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.preview-label {
font-size: 0.72rem;
color: #888;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.preview-value {
font-size: 0.92rem;
color: #222;
font-weight: 500; font-weight: 500;
} }
span { .preview-badge {
margin: 0.5em 0; display: inline-block;
padding: 0.1rem 0.55rem;
border-radius: 12px;
font-size: 0.78rem;
font-weight: 600;
background: #ffebee;
color: #c62828;
width: fit-content;
&--yes {
background: #e8f5e9;
color: #2e7d32;
}
} }
mat-divider { .preview-table {
margin: 0 0 0.5em; width: 100%;
margin-top: 1rem;
} }
.margin-t { .margin-t {
@@ -7,8 +7,7 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, Input, Output, EventEmitter, OnChanges, inject } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnChanges } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { import {
MatTableDataSource, MatTableDataSource,
MatTable, MatTable,
@@ -62,7 +61,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
] ]
}) })
export class SavingsAccountPreviewStepComponent implements OnChanges { export class SavingsAccountPreviewStepComponent implements OnChanges {
private translateService = inject(TranslateService); readonly digitalFacilityLabels = [
{ key: 'atmFacility', label: 'ATM Facility' },
{ key: 'atmWithdrawalFacility', label: 'ATM Withdrawal' },
{ key: 'upiFacility', label: 'UPI Facility' },
{ key: 'posTransactionsFacility', label: 'POS Transactions' },
{ 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' },
{ key: 'withdrawalFacility', label: 'Withdrawal Facility' },
{ key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' },
{ key: 'virtualDebitCard', label: 'Virtual Debit Card' }
];
/** Savings Account Product Template */ /** Savings Account Product Template */
@Input() savingsAccountProductTemplate: any; @Input() savingsAccountProductTemplate: any;
@@ -102,7 +118,15 @@ export class SavingsAccountPreviewStepComponent implements OnChanges {
} }
} }
getCatalogTranslation(text: string): string { get digitalPreview(): any {
return this.translateService.instant('labels.catalogs.' + text); return this.savingsAccountProductTemplate || {};
}
get hasDigitalSection(): boolean {
return !!(
this.digitalPreview?.accountFacilityConfig ||
this.digitalPreview?.accountFacilityLimits?.length ||
this.digitalPreview?.depositLimitKyc
);
} }
} }
@@ -0,0 +1,113 @@
<!--
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]="savingsAccountSettingsForm">
<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>{{ 'labels.inputs.Minimum Opening Balance' | translate }}</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 (&#8377;)</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 (freq of periodFrequencyData; track freq.value) {
<mat-option [value]="freq.value">{{ freq.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>{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}</span>
</mat-checkbox>
</div>
</div>
</div>
</div>
</div>
<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 (savingsAccountSettingsForm.value.enableLockinPeriod) {
<div class="sps-lockin-fields">
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}*</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>{{ 'labels.inputs.Type' | translate }}*</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>
<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-layout sps-layout--additional">
<div class="sps-toggle sps-toggle--full">
<mat-checkbox formControlName="accountClosedZeroBalanceInDayClosing">
<span>Account Closed with Zero balance in Day Closing</span>
</mat-checkbox>
</div>
@if (savingsAccountProductTemplate?.minBalanceForInterestCalculation != null) {
<mat-form-field appearance="outline" class="sps-field sps-field--compact">
<mat-label>{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}</mat-label>
<input
matInput
[value]="savingsAccountProductTemplate.minBalanceForInterestCalculation | formatNumber"
readonly
/>
</mat-form-field>
}
</div>
</div>
</div>
</form>
@@ -0,0 +1,169 @@
/**
* 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;
}
span {
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--additional {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
@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;
}
}
@@ -0,0 +1,152 @@
/**
* 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 { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-savings-account-settings-step',
templateUrl: './savings-account-settings-step.component.html',
styleUrls: ['./savings-account-settings-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
FormatNumberPipe
]
})
export class SavingsAccountSettingsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private _savingsAccountProductTemplate: any;
@Input()
set savingsAccountProductTemplate(value: any) {
this._savingsAccountProductTemplate = value;
if (value && this.savingsAccountSettingsForm) {
this.applyTemplate(value);
}
}
get savingsAccountProductTemplate(): any {
return this._savingsAccountProductTemplate;
}
savingsAccountSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any[] = [];
periodFrequencyData: any[] = [
{ value: 'Daily' },
{ value: 'Weekly' },
{ value: 'Monthly' },
{ value: 'Quarterly' },
{ value: 'Half Yearly' },
{ value: 'Yearly' }];
constructor() {
this.createForm();
this.buildDependencies();
}
createForm() {
this.savingsAccountSettingsForm = this.formBuilder.group({
minRequiredOpeningBalance: [''],
withdrawalFeeForTransfers: [false],
enforceMinRequiredBalance: [false],
minRequiredBalance: [''],
balanceFrequency: ['Monthly'],
enableLockinPeriod: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
accountClosedZeroBalanceInDayClosing: [false],
allowOverdraft: [false]
});
}
ngOnInit() {
if (this._savingsAccountProductTemplate) {
this.applyTemplate(this._savingsAccountProductTemplate);
}
}
private applyTemplate(t: any) {
this.lockinPeriodFrequencyTypeData = t.lockinPeriodFrequencyTypeOptions || [];
const hasLockin = !!(t.lockinPeriodFrequency && t.lockinPeriodFrequency > 0);
this.savingsAccountSettingsForm.patchValue({
minRequiredOpeningBalance: t.minRequiredOpeningBalance,
withdrawalFeeForTransfers: t.withdrawalFeeForTransfers || false,
enforceMinRequiredBalance: t.enforceMinRequiredBalance || false,
minRequiredBalance: t.minRequiredBalance,
balanceFrequency: t.balanceFrequency || 'Monthly',
enableLockinPeriod: hasLockin,
lockinPeriodFrequency: hasLockin ? t.lockinPeriodFrequency : '',
lockinPeriodFrequencyType: hasLockin ? t.lockinPeriodFrequencyType?.id || '' : '',
accountClosedZeroBalanceInDayClosing: t.accountClosedZeroBalanceInDayClosing || false,
allowOverdraft: t.allowOverdraft || false
});
if (t.allowOverdraft) {
if (!this.savingsAccountSettingsForm.contains('minOverdraftForInterestCalculation')) {
this.savingsAccountSettingsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl(''));
this.savingsAccountSettingsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl(''));
this.savingsAccountSettingsForm.addControl('overdraftLimit', new UntypedFormControl(''));
}
this.savingsAccountSettingsForm.patchValue({
minOverdraftForInterestCalculation: t.minOverdraftForInterestCalculation,
nominalAnnualInterestRateOverdraft: t.nominalAnnualInterestRateOverdraft,
overdraftLimit: t.overdraftLimit
});
}
}
buildDependencies() {
this.savingsAccountSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enabled: boolean) => {
if (enabled) {
this.savingsAccountSettingsForm.get('lockinPeriodFrequency').setValidators([
Validators.required,
Validators.min(1)
]);
this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').setValidators([Validators.required]);
} else {
this.savingsAccountSettingsForm.get('lockinPeriodFrequency').setValue('');
this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').setValue('');
this.savingsAccountSettingsForm.get('lockinPeriodFrequency').clearValidators();
this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').clearValidators();
}
this.savingsAccountSettingsForm.get('lockinPeriodFrequency').updateValueAndValidity();
this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').updateValueAndValidity();
});
this.savingsAccountSettingsForm.get('allowOverdraft').valueChanges.subscribe((allowed: boolean) => {
if (allowed) {
this.savingsAccountSettingsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl(''));
this.savingsAccountSettingsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl(''));
this.savingsAccountSettingsForm.addControl('overdraftLimit', new UntypedFormControl(''));
} else {
this.savingsAccountSettingsForm.removeControl('minOverdraftForInterestCalculation');
this.savingsAccountSettingsForm.removeControl('nominalAnnualInterestRateOverdraft');
this.savingsAccountSettingsForm.removeControl('overdraftLimit');
}
});
}
get savingsAccountSettings() {
const value = { ...this.savingsAccountSettingsForm.getRawValue() };
if (!value.enableLockinPeriod) {
value.lockinPeriodFrequency = null;
value.lockinPeriodFrequencyType = null;
}
delete value.enableLockinPeriod;
delete value.balanceFrequency;
delete value.accountClosedZeroBalanceInDayClosing;
return value;
}
}
@@ -0,0 +1,170 @@
<!--
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]="savingsAccountTermForm">
<div class="sat-section">
<div class="sat-section__header">
<span class="sat-section__badge"><i class="fas fa-chart-line"></i></span>
<span>Interest Rate &amp; Posting Terms</span>
</div>
<div class="sat-body">
<!-- Banner showing current rate -->
<div class="sat-banner">
<span class="sat-banner__icon">ℹ️</span>
<span>
Savings Account base rate:
<b>{{ savingsAccountTermForm.get('nominalAnnualInterestRate').value || 0 }}%</b>
</span>
</div>
<!-- Row 1: Nominal Interest + Effective Date + Interest Calculation Type -->
<div class="sat-grid sat-grid--three">
<mat-form-field appearance="outline" class="sat-field">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
<input type="number" matInput formControlName="nominalAnnualInterestRate" required min="0" step="0.01" />
@if (savingsAccountTermForm.get('nominalAnnualInterestRate').hasError('required')) {
<mat-error>Nominal Annual Interest is <strong>required</strong></mat-error>
}
@if (savingsAccountTermForm.get('nominalAnnualInterestRate').hasError('min')) {
<mat-error>Must be zero or greater</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="sat-field" (click)="effectiveDatePicker.open()">
<mat-label>Effective Date</mat-label>
<input
matInput
[matDatepicker]="effectiveDatePicker"
formControlName="effectiveDate"
[min]="minDate"
[max]="maxDate"
/>
<mat-datepicker-toggle matSuffix [for]="effectiveDatePicker"></mat-datepicker-toggle>
<mat-datepicker #effectiveDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="sat-field">
<mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select formControlName="interestCalculateSelector">
@for (opt of interestCalculateSelectorData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<!-- Row 2: Interest Calculate (enum) + Average Period (conditional) + Interest Posting Period -->
<div class="sat-grid sat-grid--three">
<mat-form-field appearance="outline" class="sat-field">
<mat-label>Interest Calculate</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (opt of interestCalculationTypeData; track opt.id) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error
>{{ 'labels.inputs.Interest Calculation Type' | translate }} is <strong>required</strong></mat-error
>
</mat-form-field>
@if (savingsAccountTermForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="sat-field">
<mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod">
@for (period of periodFrequencyData; track period) {
<mat-option [value]="period.value">{{ period.value }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="sat-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select formControlName="interestPostingPeriodType" required>
@for (opt of interestPostingPeriodTypeData; track opt) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>{{ 'labels.inputs.Interest Posting Period' | translate }} is <strong>required</strong></mat-error>
</mat-form-field>
</div>
<!-- Row 3: Interest Compounding + Days in Year -->
<div class="sat-grid sat-grid--three">
<mat-form-field appearance="outline" class="sat-field">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (opt of interestCompoundingPeriodTypeData; track opt) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error
>{{ 'labels.inputs.Interest Compounding Period' | translate }} is <strong>required</strong></mat-error
>
</mat-form-field>
<mat-form-field appearance="outline" class="sat-field">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required>
@for (opt of interestCalculationDaysInYearTypeData; track opt) {
<mat-option [value]="opt.id">{{ opt.value | translateKey: 'catalogs' }}</mat-option>
}
</mat-select>
<mat-error>{{ 'labels.inputs.Days in Year' | translate }} is <strong>required</strong></mat-error>
</mat-form-field>
</div>
<!-- Footer: Auto Post + Frequency Fix Day + Last Interest Date -->
<div class="sat-footer">
<div class="sat-footer__grid">
<div class="sat-toggle-card">
<div>
<h4>Auto Interest Post</h4>
</div>
<div class="sat-toggle-card__choices">
@for (option of yesNoOptions; track option.value) {
<label class="sat-radio-label">
<input type="radio" formControlName="autoPost" [value]="option.value" />
<span>{{ option.label }}</span>
</label>
}
</div>
</div>
<mat-form-field appearance="outline" class="sat-field">
<mat-label>Frequency (Fix Day)</mat-label>
<input type="number" matInput formControlName="frequencyFixDay" min="0" />
<mat-hint>Days</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="sat-field" (click)="lastInterestDatePicker.open()">
<mat-label>Last Interest Date</mat-label>
<input
matInput
[matDatepicker]="lastInterestDatePicker"
formControlName="lastInterestDate"
[min]="minDate"
[max]="maxDate"
/>
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
</div>
@if (savingsAccountTermForm.value.interestCalculationType === 1) {
<div class="sat-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,166 @@
/**
* 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;
.sat-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;
span {
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.sat-section__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
.sat-body {
background: #fff;
padding: 18px 24px 24px;
}
.sat-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
margin-bottom: 18px;
border-radius: 8px;
background: #eff6ff;
border: 1px solid #bfdbfe;
font-size: 13px;
color: $gray-900;
&__icon {
font-size: 16px;
}
}
.sat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px 16px;
@media (width <= 900px) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.sat-field {
width: 100%;
}
.sat-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid $border;
&__grid {
display: grid;
grid-template-columns: auto 1fr 1fr;
gap: 4px 16px;
align-items: start;
@media (width <= 900px) {
grid-template-columns: 1fr 1fr;
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
}
.sat-toggle-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 14px;
border: 1px solid $border;
border-radius: 8px;
background: $gray-50;
min-height: 56px;
h4 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.04em;
}
&__choices {
display: flex;
gap: 16px;
align-items: center;
}
}
.sat-radio-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: $gray-900;
cursor: pointer;
[type='radio'] {
accent-color: $accent;
width: 16px;
height: 16px;
cursor: pointer;
}
}
.sat-note {
display: flex;
align-items: flex-start;
gap: 8px;
margin-top: 12px;
padding: 10px 14px;
border-radius: 8px;
background: #fffbeb;
border: 1px solid #fde68a;
font-size: 13px;
color: #92400e;
}
@@ -0,0 +1,154 @@
/**
* 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 { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
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-savings-account-term-step',
templateUrl: './savings-account-term-step.component.html',
styleUrls: ['./savings-account-term-step.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class SavingsAccountTermStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private _savingsAccountProductTemplate: any;
@Input()
set savingsAccountProductTemplate(value: any) {
this._savingsAccountProductTemplate = value;
if (value && this.savingsAccountTermForm) {
this.applyTemplate(value);
}
}
get savingsAccountProductTemplate(): any {
return this._savingsAccountProductTemplate;
}
savingsAccountTermForm: UntypedFormGroup;
interestCompoundingPeriodTypeData: any[] = [];
interestPostingPeriodTypeData: any[] = [];
interestCalculationTypeData: any[] = [];
interestCalculationDaysInYearTypeData: any[] = [];
interestCalculateSelectorData: any[] = [];
periodFrequencyData: any[] = [];
yesNoOptions = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
minDate = new Date(2000, 0, 1);
maxDate = new Date();
constructor() {
this.createForm();
}
createForm() {
this.savingsAccountTermForm = this.formBuilder.group({
currencyCode: [{ value: '', disabled: true }],
nominalAnnualInterestRate: [
'',
[
Validators.required,
Validators.min(0)
]
],
interestCompoundingPeriodType: [
'',
Validators.required
],
interestPostingPeriodType: [
'',
Validators.required
],
interestCalculationType: [
'',
Validators.required
],
interestCalculationDaysInYearType: [
'',
Validators.required
],
effectiveDate: [''],
interestCalculateSelector: [1],
averagePeriod: ['Monthly'],
autoPost: [true],
frequencyFixDay: [
30,
[Validators.min(0)]
],
lastInterestDate: ['']
});
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
// Apply template if it was set before the form was ready
if (this._savingsAccountProductTemplate) {
this.applyTemplate(this._savingsAccountProductTemplate);
}
}
private applyTemplate(t: any) {
this.maxDate = this.settingsService.businessDate;
this.interestCompoundingPeriodTypeData = t.interestCompoundingPeriodTypeOptions || [];
this.interestPostingPeriodTypeData = t.interestPostingPeriodTypeOptions || [];
this.interestCalculationTypeData = t.interestCalculationTypeOptions || [];
this.interestCalculateSelectorData = t.interestCalculationTypeOption || [];
this.interestCalculationDaysInYearTypeData = t.interestCalculationDaysInYearTypeOptions || [];
this.periodFrequencyData = t.periodFrequencyOptions?.length ? t.periodFrequencyOptions : [
{ value: 'Monthly' },
{ value: 'Quarterly' },
{ value: 'Half Yearly' },
{ value: 'Yearly' }];
const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly';
const savedEnumTypeId = t.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1;
const savedSelectorId = t.interestCalculateSelector ?? this.interestCalculateSelectorData?.[0]?.id ?? null;
this.savingsAccountTermForm.patchValue({
currencyCode: t.currency?.code,
nominalAnnualInterestRate: t.nominalAnnualInterestRate,
interestCompoundingPeriodType: t.interestCompoundingPeriodType?.id,
interestPostingPeriodType: t.interestPostingPeriodType?.id,
interestCalculationType: savedEnumTypeId,
interestCalculationDaysInYearType: t.interestCalculationDaysInYearType?.id,
effectiveDate: t.effectiveDate ? new Date(t.effectiveDate) : '',
interestCalculateSelector: savedSelectorId,
averagePeriod: t.averagePeriod || defaultPeriod,
autoPost: t.autoPost ?? true,
frequencyFixDay: t.frequencyFixDay ?? 30,
lastInterestDate: t.lastInterestDate ? new Date(t.lastInterestDate) : ''
});
}
get savingsAccountTerms() {
const value = { ...this.savingsAccountTermForm.getRawValue() };
const dateFormat = this.settingsService.dateFormat;
if (value.effectiveDate instanceof Date) {
value.effectiveDate = this.dateUtils.formatDate(value.effectiveDate, dateFormat) || '';
}
if (value.lastInterestDate instanceof Date) {
value.lastInterestDate = this.dateUtils.formatDate(value.lastInterestDate, dateFormat) || '';
}
delete value.currencyCode;
return value;
}
}
@@ -152,13 +152,6 @@
<mat-label>{{ 'labels.inputs.Minimum Balance' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Minimum Balance' | translate }}</mat-label>
<input type="number" matInput formControlName="minRequiredBalance" /> <input type="number" matInput formControlName="minRequiredBalance" />
</mat-form-field> </mat-form-field>
@if (savingsAccountTermsForm.controls.minBalanceForInterestCalculation.value) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" />
</mat-form-field>
}
</div> </div>
<div class="layout-row align-center gap-2percent margin-t responsive-column"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
+2 -2
View File
@@ -10,8 +10,8 @@
/* tslint:disable */ /* tslint:disable */
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '260420', 'version': '260422',
'hash': 'fe61c925' 'hash': 'e9a93771'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };