feat: add Current Account module, DIP verification, charges integration, and translations

- Current Account module with full CRUD, transactions, charges, hold/release, block/unblock
- Current Products management under Products module
- DIP mobile number verification with real API call to /lookup/check
- Sanctions verification dialog
- Charges support for Current Account (chargeAppliesTo=5) in create/edit
- Fixed permissions from SAVINGSACCOUNT to CURRENTACCOUNT across all current account components
- Added all missing translation keys for Current Account in en-US.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-17 18:23:37 +05:30
co-authored by Claude Opus 4.6
parent 73aa69ee02
commit 0a32d7135e
210 changed files with 15048 additions and 155 deletions
@@ -0,0 +1,131 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select #charge>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
<mat-option [value]="charge">
{{ charge.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="flex-48 align-center">
<button type="button" mat-raised-button color="primary" (click)="addCharge(charge)" [disabled]="!charge.value">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargesDataSource.length > 0) {
<table class="flex-98 mat-elevation-z1" mat-table [dataSource]="chargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
<button mat-icon-button color="primary" (click)="editChargeAmount(charge)">
<fa-icon icon="pen"></fa-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') {
<span>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
}
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span>
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}
</span>
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
) {
<span> N/A </span>
}
@if (
charge.chargeTimeType.value === 'Weekly Fee' ||
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date'
) {
<button mat-icon-button color="primary" (click)="editChargeDate(charge)">
<fa-icon icon="pen"></fa-icon>
</button>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.feeInterval) {
<span>{{ charge.feeInterval }}</span>
}
@if (!charge.feeInterval) {
<span>{{ 'labels.inputs.Not Provided' | translate }}</span>
}
@if (charge.chargeTimeType.value === 'Weekly Fee' || charge.chargeTimeType.value === 'Monthly Fee') {
<button mat-icon-button color="primary" (click)="editChargeFeeInterval(charge)">
<fa-icon icon="pen"></fa-icon>
</button>
}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
}
<div class="flex-98 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>
</div>
@@ -0,0 +1,19 @@
/**
* 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/.
*/
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,265 @@
/**
* 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/.
*/
/** Angular Imports */
import { Component, OnInit, OnChanges, Input, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { UntypedFormControl } from '@angular/forms';
/** Custom Dialogs */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
/** Custom Models */
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
import { Dates } from 'app/core/utils/dates';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { TranslateService } from '@ngx-translate/core';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { ChargesFilterPipe } from '../../../pipes/charges-filter.pipe';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Charges Step
*/
@Component({
selector: 'mifosx-current-account-charges-step',
templateUrl: './current-account-charges-step.component.html',
styleUrls: ['./current-account-charges-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
MatStepperNext,
ChargesFilterPipe,
DateFormatPipe,
FormatNumberPipe
]
})
export class CurrentAccountChargesStepComponent implements OnInit, OnChanges {
private dialog = inject(MatDialog);
private dateUtils = inject(Dates);
private translateService = inject(TranslateService);
/** Current Account Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Currency Code */
@Input() currencyCode: UntypedFormControl;
@Input() activeClientMembers?: any;
/** Charge Data */
chargeData: any = [];
/** Charges Data Source */
chargesDataSource: {}[] = [];
/** Component is pristine if there has been no changes by user interaction */
pristine = true;
/** For Edit Current Account Form */
isChargesPatched = false;
/** Display columns for charges table */
displayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'date',
'repaymentsEvery',
'action'
];
/** Table Data Source */
dataSource: any;
/** Check for select all the Clients List */
selectAllItems = false;
/** Loan Purpose Options */
loanPurposeOptions: string[] = [];
/** Table Displayed Columns */
displayedColumn: string[] = [
'check',
'id',
'name'
];
ngOnInit() {
if (this.currentAccountTemplate) {
if (!this.isChargesPatched && this.currentAccountTemplate.charges) {
this.chargesDataSource =
this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
this.isChargesPatched = true;
} else {
this.chargesDataSource = [];
}
}
}
ngOnChanges() {
if (this.currentAccountProductTemplate) {
this.chargeData = this.currentAccountProductTemplate.chargeOptions;
this.chargesDataSource =
this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
}
}
/**
* Adds the charge to charges table
* @param {any} charge Charge
*/
addCharge(charge: any) {
this.chargesDataSource = this.chargesDataSource.concat([charge.value]);
charge.value = '';
this.pristine = false;
}
/**
* Edits the Charge Amount
* @param {any} charge Charge
*/
editChargeAmount(charge: any) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: this.translateService.instant('labels.inputs.Amount'),
value: charge.amount,
type: 'number',
required: false
})
];
const data = {
title: this.translateService.instant('labels.heading.Edit Charge Amount'),
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const newCharge = { ...charge, amount: response.data.value.amount };
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
this.pristine = false;
}
/**
* Edits the Charge Date
* @param {any} charge Charge
*/
editChargeDate(charge: any) {
const formfields: FormfieldBase[] = [
new DatepickerBase({
controlName: 'date',
label: this.translateService.instant('labels.inputs.Date'),
value: charge.dueDate || charge.feeOnMonthDay || '',
type: 'datetime-local',
required: false
})
];
const data = {
title: this.translateService.instant('labels.heading.Edit Charge Date'),
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
let newCharge: any;
const dateFormat = 'dd MMMM yyyy';
const date = this.dateUtils.formatDate(response.data.value.date, dateFormat);
switch (charge.chargeTimeType.value) {
case 'Specified due date':
case 'Weekly Fee':
newCharge = { ...charge, dueDate: date };
break;
case 'Annual Fee':
case 'Monthly Fee':
const dateMonthDay = this.dateUtils.formatDate(response.data.value.date, 'dd MMMM');
newCharge = { ...charge, feeOnMonthDay: dateMonthDay };
break;
}
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
this.pristine = false;
}
/**
* Edits the Charge Fee Interval
* @param {any} charge Charge
*/
editChargeFeeInterval(charge: any) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'feeInterval',
label: 'Fee Interval',
value: charge.feeInterval,
type: 'text',
required: false
})
];
const data = {
title: 'Edit Charge Fee Interval',
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const newCharge = { ...charge, feeInterval: response.data.value.feeInterval };
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
this.pristine = false;
}
/**
* Removes the charge from charges table
* @param {any} charge Charge
*/
deleteCharge(charge: any) {
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
this.chargesDataSource = this.chargesDataSource.concat([]);
this.pristine = false;
}
/**
* Returns current account charges
*/
get currentAccountCharges() {
return { charges: this.chargesDataSource };
}
}
@@ -0,0 +1,77 @@
<!--
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]="currentAccountDetailsForm">
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<mat-select formControlName="productId" required>
@for (product of productData; track product) {
<mat-option [value]="product.id">
{{ product.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId">
@for (officer of fieldOfficerData; track officer) {
<mat-option [value]="officer.id">
{{ officer.displayName }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
}
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="currentAccountDetailsForm.invalid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,7 @@
/**
* 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/.
*/
@@ -0,0 +1,137 @@
/**
* 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/.
*/
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Details Step
*/
@Component({
selector: 'mifosx-current-account-details-step',
templateUrl: './current-account-details-step.component.html',
styleUrls: ['./current-account-details-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
]
})
export class CurrentAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Product Data */
productData: any;
/** Field Officer Data */
fieldOfficerData: any;
/** For edit current account form */
isFieldOfficerPatched = false;
/** Current Account Details Form */
currentAccountDetailsForm: UntypedFormGroup;
currentAccountProductSelected = false;
/** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter();
/**
* Sets share account details form.
* @param {FormBuilder} formBuilder Form Builder.
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service.
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.createCurrentAccountDetailsForm();
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.buildDependencies();
if (this.currentAccountTemplate) {
this.productData = this.currentAccountTemplate.productOptions;
if (this.currentAccountTemplate.currentAccountProductId) {
this.currentAccountDetailsForm.patchValue({
productId: this.currentAccountTemplate.currentAccountProductId,
submittedOnDate:
this.currentAccountTemplate.timeline.submittedOnDate &&
new Date(this.currentAccountTemplate.timeline.submittedOnDate),
externalId: this.currentAccountTemplate.externalId
});
} else {
this.currentAccountDetailsForm.patchValue({
submittedOnDate: new Date()
});
}
}
}
/**
* Creates current account details form.
*/
createCurrentAccountDetailsForm() {
this.currentAccountDetailsForm = this.formBuilder.group({
productId: [
'',
Validators.required
],
submittedOnDate: [
'',
Validators.required
],
fieldOfficerId: [''],
externalId: ['']
});
}
/**
* Fetches current account product template on productId value changes
*/
buildDependencies() {
const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId;
this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
this.currentAccountsService
.getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false)
.subscribe((response: any) => {
this.currentAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.currentAccountProductSelected = true;
if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) {
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId);
this.isFieldOfficerPatched = true;
} else {
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue('');
}
});
});
}
/**
* Returns current account form value.
*/
get currentAccountDetails() {
return this.currentAccountDetailsForm.getRawValue();
}
}
@@ -0,0 +1,269 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="responsive-column">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Product' | translate }}</span>
<span class="flex-60">{{
currentAccount.productId | find: currentAccountTemplate.productOptions : 'id' : 'name'
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Submitted On' | translate }}</span>
<span class="flex-60">{{ currentAccount.submittedOnDate | dateFormat }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span>
@if (currentAccount.externalId) {
<span class="flex-60">
<mifosx-external-identifier externalId="{{ currentAccount.externalId }}"></mifosx-external-identifier>
</span>
}
@if (!currentAccount.externalId) {
<span class="flex-60">
{{ 'labels.inputs.Unassigned' | translate }}
</span>
}
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Field Officer' | translate }}</span>
<span class="flex-60">{{
currentAccount.fieldOfficerId | find: currentAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName'
}}</span>
</div>
@if (activeClientMembers) {
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Client Members' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-60">
<table mat-table #membersTable [dataSource]="dataSource">
<!-- ID Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.id }}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.displayName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="membersDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: membersDisplayedColumns"></tr>
</table>
</div>
}
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="flex-60">{{ currentAccountTermsForm.get('currencyCode').value }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="flex-60">{{ currentAccountTermsForm.get('decimal').value }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span>
<span class="flex-60">{{ currentAccount.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCompoundingPeriodType
| find: currentAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestPostingPeriodType
| find: currentAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCalculationType
| find: currentAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCalculationDaysInYearType
| find: currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
)
}}</span>
</div>
@if (currentAccount.minRequiredOpeningBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ currentAccount.withdrawalFeeForTransfers | yesNo }}</span>
</div>
@if (currentAccount.lockinPeriodFrequency) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.lockinPeriodFrequency +
' ' +
(currentAccount.lockinPeriodFrequencyType
| find: currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value')
)
}}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}:</span>
<span class="flex-60">{{ currentAccount.allowOverdraft | yesNo }}</span>
</div>
@if (currentAccount.minOverdraftForInterestCalculation) {
<div class="flex-fill layout-row">
<span class="flex-40"
>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}:</span
>
<span class="flex-60">{{ currentAccount.minOverdraftForInterestCalculation | formatNumber }}</span>
</div>
}
@if (currentAccount.nominalAnnualInterestRateOverdraft) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span>
<span class="flex-60">{{ currentAccount.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
</div>
}
@if (currentAccount.overdraftLimit) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span>
<span class="flex-60">{{ currentAccount.overdraftLimit | formatNumber }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.enforceMinRequiredBalance | yesNo }}</span>
</div>
@if (currentAccount.minRequiredBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.minRequiredBalance }}</span>
</div>
}
@if (currentAccount.charges.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="currentAccount.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') {
<span>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
}
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span>
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}
</span>
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
) {
<span> N/A </span>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.feeInterval || 'Not Provided' }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
}
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
<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 [routerLink]="['../..']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" (click)="submitEvent.emit()">
{{ 'labels.buttons.Submit' | translate }}
</button>
</div>
@@ -0,0 +1,34 @@
/**
* 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/.
*/
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
h2,
h3,
h4 {
margin: 0;
font-weight: 500;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,107 @@
/**
* 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/.
*/
/** Angular Imports */
import { Component, Input, Output, EventEmitter, OnChanges, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatDivider } from '@angular/material/divider';
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
import { MatStepperPrevious } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current account preview step
*/
@Component({
selector: 'mifosx-current-account-account-preview-step',
templateUrl: './current-account-preview-step.component.html',
styleUrls: ['./current-account-preview-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDivider,
ExternalIdentifierComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
FaIconComponent,
FindPipe,
DateFormatPipe,
FormatNumberPipe,
YesnoPipe
]
})
export class CurrentAccountPreviewStepComponent implements OnChanges {
private translateService = inject(TranslateService);
/** Current Account Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Current Account Terms Form */
@Input() currentAccountTermsForm: any;
/** Current Account */
@Input() currentAccount: any;
@Input() activeClientMembers?: any;
/** Table Data Source */
dataSource: any;
/** Display columns for charges table */
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'date',
'repaymentsEvery'
];
/** Columns to be displayed in active members table. */
membersDisplayedColumns: string[] = [
'id',
'name'
];
/** Form submission event */
@Output() submitEvent = new EventEmitter();
ngOnChanges(): void {
if (this.activeClientMembers?.length > 0) {
this.dataSource = new MatTableDataSource<any>(this.activeClientMembers.filter((member: any) => member.selected));
}
}
getCatalogTranslation(text: string): string {
return this.translateService.instant('labels.catalogs.' + text);
}
}
@@ -0,0 +1,174 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<form [formGroup]="currentAccountTermsForm">
<div class="layout-row-wrap gap-2percent responsive-column align-start-center">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<input matInput formControlName="currencyCode" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<input type="number" matInput formControlName="decimal" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
<input type="number" matInput formControlName="nominalAnnualInterestRate" required />
<mat-error>
{{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) {
<mat-option [value]="interestCompoundingPeriodType.id">
{{ interestCompoundingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select formControlName="interestPostingPeriodType" required>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Interest Calculated using' | translate }}</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) {
<mat-option [value]="interestCalculationType.id">
{{ interestCalculationType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required>
@for (
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData;
track interestCalculationDaysInYearType
) {
<mat-option [value]="interestCalculationDaysInYearType.id">
{{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Minimum Opening Balance' | translate }}</mat-label>
<input type="number" matInput formControlName="minRequiredOpeningBalance" />
</mat-form-field>
<mat-checkbox labelPosition="before" formControlName="withdrawalFeeForTransfers" class="margin-v flex-48">
{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}
</mat-checkbox>
<h4 class="mat-h4 flex-98">{{ 'labels.inputs.Lock-in Period' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="lockinPeriodFrequency" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-divider class="flex-98"></mat-divider>
<h3 class="mat-h3 flex-23">{{ 'labels.inputs.Overdraft' | translate }}</h3>
<mat-checkbox class="flex-73 margin-b" labelPosition="before" formControlName="allowOverdraft">
{{ 'labels.inputs.Is Overdraft Allowed' | translate }}
</mat-checkbox>
@if (currentAccountTermsForm.value.allowOverdraft) {
<div class="flex-fill layout-row-wrap gap-2percent responsive-column">
<mat-form-field class="flex-31">
<mat-label>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}</mat-label>
<input type="number" matInput formControlName="minOverdraftForInterestCalculation" />
</mat-form-field>
<mat-form-field class="flex-31">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}</mat-label>
<input type="number" matInput formControlName="nominalAnnualInterestRateOverdraft" />
</mat-form-field>
<mat-form-field class="flex-31">
<mat-label>{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}</mat-label>
<input type="number" matInput formControlName="overdraftLimit" />
</mat-form-field>
</div>
}
<mat-divider class="flex-98"></mat-divider>
<mat-checkbox labelPosition="before" formControlName="enforceMinRequiredBalance" class="margin-v flex-48">
{{ 'labels.inputs.Enforce Minimum Balance' | translate }}
</mat-checkbox>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Minimum Balance' | translate }}</mat-label>
<input type="number" matInput formControlName="minRequiredBalance" />
</mat-form-field>
@if (currentAccountTermsForm.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 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>
</form>
@@ -0,0 +1,32 @@
/**
* 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/.
*/
h4 {
font-weight: 500;
margin: 1em 0;
}
h3 {
font-weight: 500;
}
mat-divider {
margin: 1em 0 2em;
}
.margin-v {
margin: 1em 0;
}
.margin-b {
margin: 0 0 1em;
}
.margin-t {
margin-top: 1em;
}
@@ -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/.
*/
/** Angular Imports */
import { Component, OnChanges, OnInit, Input, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
Validators,
UntypedFormControl,
ReactiveFormsModule
} from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDivider } from '@angular/material/divider';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Terms Step
*/
@Component({
selector: 'mifosx-current-account-terms-step',
templateUrl: './current-account-terms-step.component.html',
styleUrls: ['./current-account-terms-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatDivider,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
]
})
export class CurrentAccountTermsStepComponent implements OnChanges, OnInit {
private formBuilder = inject(UntypedFormBuilder);
private settingsService = inject(SettingsService);
/** Current Account and Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Current Account Terms Form */
currentAccountTermsForm: UntypedFormGroup;
/** Lockin Period Frequency Type Data */
lockinPeriodFrequencyTypeData: any;
/** Interest Compounding Period Type Data */
interestCompoundingPeriodTypeData: any;
/** Interest Posting Period Type Data */
interestPostingPeriodTypeData: any;
/** Interest Calculation Type Data */
interestCalculationTypeData: any;
/** Interest Calculation Days in Year Data */
interestCalculationDaysInYearTypeData: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.createCurrentAccountTermsForm();
this.buildDependencies();
}
ngOnChanges() {
if (this.currentAccountProductTemplate) {
this.currentAccountTermsForm.patchValue({
currencyCode: this.currentAccountProductTemplate.currency.code,
decimal: this.currentAccountProductTemplate.currency.decimalPlaces,
minBalanceForInterestCalculation: this.currentAccountProductTemplate.minBalanceForInterestCalculation,
nominalAnnualInterestRate: this.currentAccountProductTemplate.nominalAnnualInterestRate,
interestCompoundingPeriodType: this.currentAccountProductTemplate.interestCompoundingPeriodType.id,
interestPostingPeriodType: this.currentAccountProductTemplate.interestPostingPeriodType.id,
interestCalculationType: this.currentAccountProductTemplate.interestCalculationType.id,
interestCalculationDaysInYearType: this.currentAccountProductTemplate.interestCalculationDaysInYearType.id,
minRequiredOpeningBalance: this.currentAccountProductTemplate.minRequiredOpeningBalance,
allowOverdraft: this.currentAccountProductTemplate.allowOverdraft,
overdraftLimit: this.currentAccountProductTemplate.overdraftLimit,
enforceMinRequiredBalance: this.currentAccountProductTemplate.enforceMinRequiredBalance,
minOverdraftForInterestCalculation: this.currentAccountProductTemplate.minOverdraftForInterestCalculation,
nominalAnnualInterestRateOverdraft: this.currentAccountProductTemplate.nominalAnnualInterestRateOverdraft,
minRequiredBalance: this.currentAccountProductTemplate.minRequiredBalance,
withdrawalFeeForTransfers: this.currentAccountProductTemplate.withdrawalFeeForTransfers
});
this.setOptions();
}
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
if (this.currentAccountTemplate) {
this.currentAccountTermsForm.patchValue({
nominalAnnualInterestRate: this.currentAccountTemplate.nominalAnnualInterestRate,
interestCompoundingPeriodType: this.currentAccountTemplate.interestCompoundingPeriodType.id,
interestPostingPeriodType: this.currentAccountTemplate.interestPostingPeriodType.id,
interestCalculationType: this.currentAccountTemplate.interestCalculationType.id,
interestCalculationDaysInYearType: this.currentAccountTemplate.interestCalculationDaysInYearType.id,
minRequiredOpeningBalance: this.currentAccountTemplate.minRequiredOpeningBalance,
withdrawalFeeForTransfers: this.currentAccountTemplate.withdrawalFeeForTransfers,
lockinPeriodFrequency: this.currentAccountTemplate.lockinPeriodFrequency,
lockinPeriodFrequencyType:
this.currentAccountTemplate.lockinPeriodFrequencyType &&
this.currentAccountTemplate.lockinPeriodFrequencyType.id,
allowOverdraft: this.currentAccountTemplate.allowOverdraft,
enforceMinRequiredBalance: this.currentAccountTemplate.enforceMinRequiredBalance,
minRequiredBalance: this.currentAccountTemplate.minRequiredBalance
});
}
}
/**
* Creates current account terms form.
*/
createCurrentAccountTermsForm() {
this.currentAccountTermsForm = this.formBuilder.group({
currencyCode: [{ value: '', disabled: true }],
decimal: [{ value: '', disabled: true }],
nominalAnnualInterestRate: [
'',
Validators.required
],
interestCompoundingPeriodType: [
'',
Validators.required
],
interestPostingPeriodType: [
'',
Validators.required
],
interestCalculationType: [
'',
Validators.required
],
interestCalculationDaysInYearType: [
'',
Validators.required
],
minRequiredOpeningBalance: [''],
withdrawalFeeForTransfers: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
allowOverdraft: [false],
enforceMinRequiredBalance: [false],
minRequiredBalance: [''],
minBalanceForInterestCalculation: [{ value: '', disabled: true }]
});
}
/**
* Sets all select dropdown options.
*/
setOptions() {
this.lockinPeriodFrequencyTypeData = this.currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions;
this.interestCompoundingPeriodTypeData = this.currentAccountProductTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.currentAccountProductTemplate.interestPostingPeriodTypeOptions;
this.interestCalculationTypeData = this.currentAccountProductTemplate.interestCalculationTypeOptions;
this.interestCalculationDaysInYearTypeData =
this.currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions;
}
/**
* Subscribes to value changes and sets new form controls accordingly.
*/
buildDependencies() {
this.currentAccountTermsForm.get('allowOverdraft').valueChanges.subscribe((allowOverdraft: any) => {
if (allowOverdraft) {
this.currentAccountTermsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl(''));
this.currentAccountTermsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl(''));
this.currentAccountTermsForm.addControl('overdraftLimit', new UntypedFormControl(''));
} else {
this.currentAccountTermsForm.removeControl('minOverdraftForInterestCalculation');
this.currentAccountTermsForm.removeControl('nominalAnnualInterestRateOverdraft');
this.currentAccountTermsForm.removeControl('overdraftLimit');
}
});
}
/**
* Returns current account terms form value.
*/
get currentAccountTerms() {
const payload = this.currentAccountTermsForm.getRawValue();
delete payload.currencyCode;
delete payload.decimal;
delete payload.minBalanceForInterestCalculation; // Backend is not accepting minBalanceForInterestCalculation value
return payload;
}
}
@@ -0,0 +1,46 @@
<!--
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/.
-->
@if (activeClientMembers) {
<div>
<table class="flex-98" mat-table [dataSource]="dataSource">
<!-- Check Column -->
<ng-container matColumnDef="check">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="toggleSelects()" [(ngModel)]="selectAllItems"></mat-checkbox>
</th>
<td mat-cell *matCellDef="let element">
<mat-checkbox (change)="toggleSelect()" [(ngModel)]="element.selected"></mat-checkbox>
</td>
</ng-container>
<!-- Position Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.id }}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.displayName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumn"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumn"></tr>
</table>
</div>
}
<div class="layout-row responsive-column align-center gap-2px margin-t">
<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]="selectedClientMembers.selectedMembers.length === 0">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
@@ -0,0 +1,19 @@
/**
* 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/.
*/
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,84 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, OnChanges, Input } from '@angular/core';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatCheckbox } from '@angular/material/checkbox';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-active-client-members',
templateUrl: './current-accounts-active-client-members.component.html',
styleUrls: ['./current-accounts-active-client-members.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCheckbox,
FormsModule,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
]
})
export class CurrentAccountActiveClientMembersComponent implements OnInit {
@Input() activeClientMembers?: any;
selectAllItems = false;
displayedColumn: string[] = [
'check',
'id',
'name'
];
constructor() {}
dataSource: any;
ngOnInit(): void {
this.dataSource = new MatTableDataSource<any>(this.activeClientMembers);
}
get selectedClientMembers() {
return { selectedMembers: this.activeClientMembers.filter((item: any) => item.selected) };
}
/** Toggle all checks */
toggleSelects() {
for (const member of this.activeClientMembers) {
member.selected = this.selectAllItems;
}
}
/** Check if all the checks are selected */
toggleSelect() {
const len = this.activeClientMembers.length;
this.selectAllItems =
len === 0 ? false : this.activeClientMembers.filter((item: any) => item.selected).length === len;
}
}