update some logic show option

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nitesh Yadav
2026-04-21 17:38:07 +05:30
co-authored by Claude Sonnet 4.6
parent e212bf7d79
commit e9a9377148
39 changed files with 2215 additions and 287 deletions
@@ -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;
@@ -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 (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>
} }
@@ -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
@@ -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';
@@ -47,6 +48,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatRow, MatRow,
ViewSavingsAccountingDetailsComponent, ViewSavingsAccountingDetailsComponent,
FormatNumberPipe, FormatNumberPipe,
DateFormatPipe,
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: '' }
}
);
}
}
@@ -22,6 +22,27 @@
<span class="flex-60">{{ savingsAccount.submittedOnDate | dateFormat }}</span> <span class="flex-60">{{ savingsAccount.submittedOnDate | dateFormat }}</span>
</div> </div>
@if (savingsAccountProductTemplate?.effectiveDate) {
<div class="flex-fill layout-row">
<span class="flex-40">Effective Date</span>
<span class="flex-60">{{ savingsAccountProductTemplate.effectiveDate | dateFormat }}</span>
</div>
}
@if (savingsAccountProductTemplate?.effectiveTo) {
<div class="flex-fill layout-row">
<span class="flex-40">Effective To</span>
<span class="flex-60">{{ savingsAccountProductTemplate.effectiveTo | dateFormat }}</span>
</div>
}
@if (savingsAccountProductTemplate?.endDate) {
<div class="flex-fill layout-row">
<span class="flex-40">End Date</span>
<span class="flex-60">{{ savingsAccountProductTemplate.endDate | dateFormat }}</span>
</div>
}
<div class="flex-fill layout-row"> <div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span> <span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span>
@if (savingsAccount.externalId) { @if (savingsAccount.externalId) {
@@ -74,7 +95,7 @@
<div class="flex-fill layout-row"> <div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span> <span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="flex-60">{{ savingsAccountTermsForm.get('decimal').value }}</span> <span class="flex-60">{{ savingsAccountProductTemplate?.currency?.decimalPlaces }}</span>
</div> </div>
<div class="flex-fill layout-row"> <div class="flex-fill layout-row">
@@ -82,6 +103,49 @@
<span class="flex-60">{{ savingsAccount.nominalAnnualInterestRate | formatNumber }} %</span> <span class="flex-60">{{ savingsAccount.nominalAnnualInterestRate | formatNumber }} %</span>
</div> </div>
@if (savingsAccountProductTemplate?.interestCalculationTypeName) {
<div class="flex-fill layout-row">
<span class="flex-40">Interest Calculation Mode</span>
<span class="flex-60">{{ savingsAccountProductTemplate.interestCalculationTypeName }}</span>
</div>
}
@if (savingsAccountProductTemplate?.lastInterestDate) {
<div class="flex-fill layout-row">
<span class="flex-40">Last Interest Date</span>
<span class="flex-60">{{ savingsAccountProductTemplate.lastInterestDate | dateFormat }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">Interest Calculation Selector</span>
<span class="flex-60">{{
getCatalogTranslation(
savingsAccount.interestCalculateSelector
| find: savingsAccountProductTemplate.interestCalculationTypeOption : 'id' : 'value'
)
}}</span>
</div>
@if (savingsAccount.averagePeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">Average Period</span>
<span class="flex-60">{{ savingsAccount.averagePeriod }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">Auto Post</span>
<span class="flex-60">{{ savingsAccount.autoPost | yesNo }}</span>
</div>
@if (savingsAccount.frequencyFixDay !== undefined && savingsAccount.frequencyFixDay !== null) {
<div class="flex-fill layout-row">
<span class="flex-40">Frequency Fix Day</span>
<span class="flex-60">{{ savingsAccount.frequencyFixDay }}</span>
</div>
}
<div class="flex-fill layout-row"> <div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ <span class="flex-60">{{
@@ -181,6 +245,27 @@
<span class="flex-60">{{ savingsAccount.enforceMinRequiredBalance | yesNo }}</span> <span class="flex-60">{{ savingsAccount.enforceMinRequiredBalance | yesNo }}</span>
</div> </div>
@if (savingsAccountProductTemplate?.enableLockinPeriod !== undefined) {
<div class="flex-fill layout-row">
<span class="flex-40">Enable Lock-in Period</span>
<span class="flex-60">{{ savingsAccountProductTemplate.enableLockinPeriod | yesNo }}</span>
</div>
}
@if (savingsAccountProductTemplate?.balanceFrequency) {
<div class="flex-fill layout-row">
<span class="flex-40">Balance Frequency</span>
<span class="flex-60">{{ savingsAccountProductTemplate.balanceFrequency }}</span>
</div>
}
@if (savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing !== undefined) {
<div class="flex-fill layout-row">
<span class="flex-40">Account Closed Zero Balance In Day Closing</span>
<span class="flex-60">{{ savingsAccountProductTemplate.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
}
@if (savingsAccount.minRequiredBalance) { @if (savingsAccount.minRequiredBalance) {
<div class="flex-fill layout-row"> <div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span> <span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
@@ -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 -3
View File
@@ -5,13 +5,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
/* tslint:disable */ /* tslint:disable */
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '260420', 'version': '260421',
'hash': 'fe61c925' 'hash': 'e212bf7d'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };