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,157 @@
<!--
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="tab-container mat-typography">
<div class="layout-row align-start">
<div class="m-b-10">
<h3>{{ 'labels.inputs.Charges' | translate }}</h3>
</div>
@if (this.chargesData) {
<div class="action-button m-b-20">
<button mat-raised-button color="primary" (click)="toggleCharges()">
{{
showInactiveCharges
? viewAllChargeButtons('View Active Charges')
: viewAllChargeButtons('View Inactive Charges')
}}
</button>
</div>
}
</div>
<div class="mat-elevation-z1 m-b-25">
<table mat-table #chargesTable [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name }}</td>
</ng-container>
<ng-container matColumnDef="feeOrPenalty">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Charge Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.penalty ? 'Penalty' : ('Fee' | translateKey: 'catalogs') }}</td>
</ng-container>
<ng-container matColumnDef="paymentDueAt">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Due At' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}</td>
</ng-container>
<ng-container matColumnDef="dueAsOf">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Due As Of' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.dueDate | dateFormat }}</td>
</ng-container>
<ng-container matColumnDef="repeatsOn">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repeats On (M/d)' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.feeOnMonthDay ? charge.feeOnMonthDay.join('/') : '' }}</td>
</ng-container>
<ng-container matColumnDef="calculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Calculation Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}</td>
</ng-container>
<ng-container matColumnDef="due">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Due' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</ng-container>
<ng-container matColumnDef="paid">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Paid' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amountPaid | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</ng-container>
<ng-container matColumnDef="waived">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Waived' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amountWaived | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</ng-container>
<ng-container matColumnDef="outstanding">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Outstanding' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amountOutstanding | currency: charge.currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (currentAccountData.status.value === 'Submitted and pending approval') {
<span>
<button
class="account-action-button"
mat-raised-button
color="primary"
matTooltip="{{ 'tooltips.Edit Charge' | translate }}"
*mifosxHasPermission="'UPDATE_CURRENTACCOUNTCHARGE'"
(click)="routeEdit($event); editCharge(charge)"
>
<i class="fa fa-pencil"></i>
</button>
<button
class="account-action-button"
mat-raised-button
color="warn"
matTooltip="{{ 'tooltips.Delete Charge' | translate }}"
*mifosxHasPermission="'DELETE_CURRENTACCOUNTCHARGE'"
(click)="routeEdit($event); deleteCharge(charge.id)"
>
<i class="fa fa-trash"></i>
</button>
</span>
}
@if (currentAccountData.status.value === 'Active' && charge.amountOutstanding !== 0) {
<span>
<button
class="account-action-button"
mat-raised-button
color="primary"
matTooltip="{{ 'tooltips.Pay Charge' | translate }}"
*mifosxHasPermission="'PAY_CURRENTACCOUNTCHARGE'"
(click)="routeEdit($event); payCharge(charge.id)"
>
<i class="fa fa-dollar"></i>
</button>
<button
class="account-action-button"
mat-raised-button
color="primary"
matTooltip="{{ 'tooltips.Waive Charge' | translate }}"
*mifosxHasPermission="'WAIVE_CURRENTACCOUNTCHARGE'"
(click)="routeEdit($event); waiveCharge(charge.id)"
>
<i class="fa fa-flag"></i>
</button>
@if (isRecurringCharge(charge)) {
<button
class="account-action-button"
mat-raised-button
color="primary"
matTooltip="{{ 'tooltips.Inactivate Charge' | translate }}"
*mifosxHasPermission="'INACTIVATE_CURRENTACCOUNTCHARGE'"
(click)="routeEdit($event); inactivateCharge(charge.id)"
>
<i class="fa fa-ban"></i>
</button>
}
</span>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="select-row" [routerLink]="[row.id]"></tr>
</table>
</div>
</div>
@@ -0,0 +1,35 @@
/**
* 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/.
*/
.tab-container {
padding: 1%;
margin: 1%;
h3 {
margin: 1% auto;
}
.action-button {
margin-left: auto;
}
table {
width: 100%;
.account-action-button {
min-width: 26px;
padding: 0 6px;
margin: 4px;
line-height: 25px;
}
.select-row:hover {
cursor: pointer;
}
}
}
@@ -0,0 +1,314 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
/** Custom Dialogs */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { WaiveChargeDialogComponent } from '../custom-dialogs/waive-charge-dialog/waive-charge-dialog.component';
import { InactivateChargeDialogComponent } from '../custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-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 { TranslateService } from '@ngx-translate/core';
import { CurrencyPipe } from '@angular/common';
import { MatTooltip } from '@angular/material/tooltip';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Charges Tab Component
*/
@Component({
selector: 'mifosx-charges-tab',
templateUrl: './charges-tab.component.html',
styleUrls: ['./charges-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatTooltip,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
CurrencyPipe,
DateFormatPipe
]
})
export class ChargesTabComponent implements OnInit {
private currentAccountsService = inject(CurrentAccountsService);
private route = inject(ActivatedRoute);
private dateUtils = inject(Dates);
private router = inject(Router);
private dialog = inject(MatDialog);
private settingsService = inject(SettingsService);
private translateService = inject(TranslateService);
/** Current Account Data */
currentAccountData: any;
/** Charges Data */
chargesData: any[];
/** Data source for charges table. */
dataSource: MatTableDataSource<any>;
/** Toggles Charges Table */
showInactiveCharges = false;
/** Columns to be displayed in charges table. */
displayedColumns: string[] = [
'name',
'feeOrPenalty',
'paymentDueAt',
'dueAsOf',
'repeatsOn',
'calculationType',
'due',
'paid',
'waived',
'outstanding',
'actions'
];
/** Charges Table Reference */
@ViewChild('chargesTable', { static: true }) chargesTableRef: MatTable<Element>;
/**
* Retrieves the Current account data from `resolve`.
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {ActivatedRoute} route Activated Route.
* @param {Router} router Router for navigation.
* @param {MatDialog} dialog Dialog reference.
* @param {Dates} dateUtils Date Utils.
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.route.parent.data.subscribe((data: { currentAccountData: any }) => {
this.currentAccountData = data.currentAccountData;
this.chargesData = this.currentAccountData.charges;
});
}
ngOnInit() {
const activeCharges = this.chargesData ? this.chargesData.filter((charge) => charge.isActive) : [];
this.dataSource = new MatTableDataSource(activeCharges);
}
/**
* Toggles datasource for active/inactive charges.
*/
toggleCharges() {
this.showInactiveCharges = !this.showInactiveCharges;
if (!this.showInactiveCharges) {
const activeCharges = this.chargesData.filter((charge) => charge.isActive);
this.dataSource.data = activeCharges;
} else {
const inActiveCharges = this.chargesData.filter((charge) => !charge.isActive);
this.dataSource.data = inActiveCharges;
}
this.chargesTableRef.renderRows();
}
/**
* Pays the charge.
* @param {any} chargeId Charge Id
*/
payCharge(chargeId: any) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: 'Amount',
value: '',
type: 'number',
required: true
}),
new DatepickerBase({
controlName: 'dueDate',
label: 'Payment Date',
value: '',
type: 'date',
required: true
})
];
const data = {
title: `Pay Charge ${chargeId}`,
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const payChargeDialogRef = this.dialog.open(FormDialogComponent, { data });
payChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const dataObject = {
...response.data.value,
dueDate: this.dateUtils.formatDate(response.data.value.dueDate, dateFormat),
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.currentAccountData.id, 'paycharge', dataObject, chargeId)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Waive's the charge
* @param {any} chargeId Charge Id
*/
waiveCharge(chargeId: any) {
const waiveChargeDialogRef = this.dialog.open(WaiveChargeDialogComponent, { data: { id: chargeId } });
waiveChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.currentAccountData.id, 'waive', {}, chargeId)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Inactivate's the charge
* @param {any} chargeId Charge Id
*/
inactivateCharge(chargeId: any) {
const inactivateChargeDialogRef = this.dialog.open(InactivateChargeDialogComponent, { data: { id: chargeId } });
inactivateChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.currentAccountData.id, 'inactivate', {}, chargeId)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Edits the charge
* @param {any} charge Charge
*/
editCharge(charge: any) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: 'Amount',
value: charge.amount || charge.amountOrPercentage,
type: 'number',
required: true
})
];
const data = {
title: `Edit Charge ${charge.id}`,
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editChargeDialogRef = this.dialog.open(FormDialogComponent, { data });
editChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const dataObject = {
...response.data.value,
dateFormat,
locale
};
this.currentAccountsService
.editCurrentAccountCharge(this.currentAccountData.id, dataObject, charge.id)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Deletes the charge
* @param {any} chargeId Charge Id
*/
deleteCharge(chargeId: any) {
const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `charge id:${chargeId}` }
});
deleteChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.currentAccountsService.deleteCurrentAccountCharge(this.currentAccountData.id, chargeId).subscribe(() => {
this.reload();
});
}
});
}
/**
* Checks if charge is recurring.
* @param {any} charge Charge
*/
isRecurringCharge(charge: any) {
return (
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Weekly Fee'
);
}
/**
* Stops the propagation to view charge page.
* @param $event Mouse Event
*/
routeEdit($event: MouseEvent) {
$event.stopPropagation();
}
/**
* Refetches data fot the component
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
*/
private reload() {
const url: string = this.router.url;
const refreshUrl: string = this.router.url.slice(
0,
this.router.url.indexOf('current-accounts') + 'current-accounts'.length
);
this.router.navigateByUrl(refreshUrl, { skipLocationChange: true }).then(() => this.router.navigate([url]));
}
viewAllChargeButtons(text: string): string {
return this.translateService.instant('labels.buttons.' + text);
}
}
@@ -0,0 +1,235 @@
<!--
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/.
-->
<mat-card class="account-card">
<mat-card-header class="header layout-column">
<mat-card-title-group class="header-title-group">
<div class="profile-image-container">
<div>
<img
mat-card-md-image
class="profile-image"
matTooltip="{{ 'tooltips.Current Account' | translate }}"
[src]="'assets/images/current_account_placeholder.png'"
/>
</div>
</div>
<div class="mat-typography account-card-title">
<mat-card-title class="layout-row layout-lt-md-column">
<div class="flex-60">
<h3>
@if (!currentAccountData.subStatus.block) {
<i
class="fa fa-stop"
[ngClass]="currentAccountData.status.code | statusLookup"
[matTooltip]="currentAccountData.status.value"
></i>
}
@if (currentAccountData.subStatus.block) {
<i
class="fa fa-stop"
[ngClass]="currentAccountData.subStatus.value | statusLookup"
[matTooltip]="currentAccountData.subStatus.value"
></i>
}
</h3>
<!-- Product Name Row -->
<span class="product-row">
<span class="m-r-5">{{ 'labels.inputs.Current Account Product' | translate }} :</span>
<span class="m-r-5">
<mifosx-long-text textValue="{{ currentAccountData.currentAccountProductName }}"></mifosx-long-text>
</span>
</span>
<!-- Account Number Row -->
<span class="account-number-row">
<span class="m-r-5">{{ 'labels.inputs.Account Number' | translate }} :</span>
<span class="m-r-5">
<mifosx-account-number
accountNo="{{ currentAccountData.accountNo }}"
display="left"
></mifosx-account-number>
</span>
</span>
<!-- External ID Row -->
@if (currentAccountData.externalId) {
<span class="external-id-row">
<span class="m-r-5">{{ 'labels.inputs.External Id' | translate }} :</span>
<span>{{ currentAccountData.externalId }}</span>
</span>
}
<span class="account-overview">
{{ 'labels.text.' + entityType | translate }} {{ 'labels.inputs.name' | translate }}:
{{ currentAccountData.clientName || currentAccountData.groupName }}
@if (currentAccountData.clientAccountNo) {
<span class="m-l-10">
<mifosx-account-number accountNo="{{ currentAccountData.clientAccountNo }}"></mifosx-account-number>
</span>
}
</span>
</div>
@if (!currentAccountData.status.rejected && !currentAccountData.status.submittedAndPendingApproval) {
<div class="flex-30">
<h3>{{ 'labels.heading.Account Overview' | translate }}</h3>
<table class="account-overview">
<tbody>
<tr>
<td>{{ 'labels.inputs.Current Balance' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.accountBalance | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Available Balance' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.availableBalance
| currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
</tbody>
</table>
</div>
}
<div class="flex-5">
<button
mat-icon-button
[matMenuTriggerFor]="accountMenu"
aria-label="Loan account actions"
yPosition="below"
>
<mat-icon matListIcon class="actions-menu">
<fa-icon icon="bars" size="sm"></fa-icon>
</mat-icon>
</button>
</div>
</mat-card-title>
</div>
<mat-menu #accountMenu="matMenu">
@for (item of buttonConfig.singleButtons; track item) {
<button mat-menu-item *mifosxHasPermission="item.taskPermissionName" (click)="doAction(item.name)">
<mat-icon matListIcon>
<fa-icon icon="{{ item.icon }}" size="sm"></fa-icon>
</mat-icon>
<span> {{ 'labels.menus.' + item.name | translate }} </span>
</button>
}
@if (buttonConfig.options && !currentAccountData.subStatus.block) {
<button mat-menu-item [matMenuTriggerFor]="More">{{ 'labels.menus.More' | translate }}</button>
<mat-menu #More="matMenu">
@for (option of buttonConfig.options; track option) {
<span>
<button mat-menu-item *mifosxHasPermission="option.taskPermissionName" (click)="doAction(option.name)">
{{ 'labels.menus.' + option.name | translate }}
</button>
</span>
}
</mat-menu>
}
</mat-menu>
</mat-card-title-group>
</mat-card-header>
<mat-card-content class="content">
@if (isActive) {
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
[routerLink]="['./general']"
routerLinkActive
#general="routerLinkActive"
[active]="general.isActive"
>
{{ 'labels.inputs.General' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./transactions']"
routerLinkActive
#transactions="routerLinkActive"
[active]="transactions.isActive"
>
{{ 'labels.inputs.Transactions' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./passbook']"
routerLinkActive
#passbook="routerLinkActive"
[active]="passbook.isActive"
>
{{ 'labels.inputs.Passbook' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./charges']"
routerLinkActive
#charges="routerLinkActive"
[active]="charges.isActive"
>
{{ 'labels.inputs.Charges' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./documents']"
routerLinkActive
#currentAccountDocuments="routerLinkActive"
[active]="currentAccountDocuments.isActive"
>
{{ 'labels.inputs.Documents' | translate }}
</a>
<a
mat-tab-link
*mifosxHasPermission="'READ_CURRENTACCOUNTNOTE'"
[routerLink]="['./notes']"
routerLinkActive
#notes="routerLinkActive"
[active]="notes.isActive"
>
{{ 'labels.inputs.Notes' | translate }}
</a>
<ng-container *mifosxHasPermission="'READ_CURRENTACCOUNTCHARGE'">
@if (currentAccountData.clientId) {
<a
mat-tab-link
[routerLink]="['./standing-instructions']"
routerLinkActive
#standingInstructions="routerLinkActive"
[active]="standingInstructions.isActive"
>
{{ 'labels.inputs.Standing Instructions' | translate }}
</a>
}
</ng-container>
@for (currentAccountDatatable of currentAccountDatatables; track currentAccountDatatable) {
<a
mat-tab-link
*mifosxHasPermission="'READ_' + currentAccountDatatable.registeredTableName"
[routerLink]="['./datatables', currentAccountDatatable.registeredTableName]"
routerLinkActive
#datatable="routerLinkActive"
[active]="datatable.isActive"
>
{{ currentAccountDatatable.registeredTableName }}
</a>
}
</nav>
}
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</mat-card-content>
</mat-card>
@@ -0,0 +1,17 @@
/**
* 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/.
*/
mat-card-title {
display: flex;
}
.product-row,
.account-number-row,
.external-id-row {
display: block;
}
@@ -0,0 +1,387 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute, Router, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
/** Custom Dialogs */
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { CalculateInterestDialogComponent } from './custom-dialogs/calculate-interest-dialog/calculate-interest-dialog.component';
import { PostInterestDialogComponent } from './custom-dialogs/post-interest-dialog/post-interest-dialog.component';
import { ToggleWithholdTaxDialogComponent } from './custom-dialogs/toggle-withhold-tax-dialog/toggle-withhold-tax-dialog.component';
/** Custom Buttons Configuration */
import { CurrentAccountButtonsConfiguration } from './current-accounts-buttons.config';
import { CurrentAccountsService } from '../current-accounts.service';
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
import { Currency } from 'app/shared/models/general.model';
import { TranslateService } from '@ngx-translate/core';
/** Environment Configuration */
import { environment } from '../../../environments/environment';
import {
MatCard,
MatCardHeader,
MatCardTitleGroup,
MatCardMdImage,
MatCardTitle,
MatCardContent
} from '@angular/material/card';
import { MatTooltip } from '@angular/material/tooltip';
import { NgClass, CurrencyPipe } from '@angular/common';
import { LongTextComponent } from '../../shared/long-text/long-text.component';
import { AccountNumberComponent } from '../../shared/account-number/account-number.component';
import { MatIconButton } from '@angular/material/button';
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
import { MatIcon } from '@angular/material/icon';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { StatusLookupPipe } from '../../pipes/status-lookup.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account View Component
*/
@Component({
selector: 'mifosx-current-account-account-view',
templateUrl: './current-account-view.component.html',
styleUrls: ['./current-account-view.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCardHeader,
MatCardTitleGroup,
MatCardMdImage,
MatTooltip,
MatCardTitle,
NgClass,
LongTextComponent,
AccountNumberComponent,
MatIconButton,
MatMenuTrigger,
MatIcon,
FaIconComponent,
MatMenu,
MatMenuItem,
MatTabNav,
MatTabLink,
RouterLinkActive,
MatTabNavPanel,
RouterOutlet,
CurrencyPipe,
StatusLookupPipe
]
})
export class CurrentAccountViewComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private currentAccountsService = inject(CurrentAccountsService);
private translateService = inject(TranslateService);
dialog = inject(MatDialog);
/** Current Account Data */
currentAccountData: any;
/** Current Account Data Tables */
currentAccountDatatables: any;
/** Button Configurations */
buttonConfig: CurrentAccountButtonsConfiguration;
/** Entity Type */
entityType: string;
isActive = false;
currency: Currency;
/**
* Fetches current account data from `resolve`
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountData: any; currentAccountDatatables: any }) => {
this.currentAccountData = data.currentAccountData;
this.currency = this.currentAccountData.currency;
this.currentAccountDatatables = data.currentAccountDatatables;
});
if (this.router.url.includes('clients')) {
this.entityType = 'Client';
} else if (this.router.url.includes('groups')) {
this.entityType = 'Group';
} else if (this.router.url.includes('centers')) {
this.entityType = 'Center';
}
}
ngOnInit() {
this.setConditionalButtons();
}
/**
* Adds options to button config. conditionaly.
*/
setConditionalButtons() {
const status = this.currentAccountData.status.value;
this.isActive = status === 'Active';
const subStatus = this.currentAccountData.subStatus;
this.buttonConfig = new CurrentAccountButtonsConfiguration(status, subStatus);
if (this.currentAccountData.clientId) {
this.buttonConfig.addOption({
name: 'Transfer Funds',
taskPermissionName: 'CREATE_ACCOUNTTRANSFER'
});
}
if (this.currentAccountData.externalId && environment.mifosInterbankTransfersEnabled) {
this.buttonConfig.addOption({
name: 'Interbank Transfer',
taskPermissionName: 'CREATE_ACCOUNTTRANSFER'
});
}
if (!this.currentAccountData.fieldOfficerId) {
this.buttonConfig.addOption({
name: 'Assign Staff',
taskPermissionName: 'UPDATESAVINGSOFFICER_CURRENTACCOUNT'
});
} else {
this.buttonConfig.addOption({
name: 'Unassign Staff',
taskPermissionName: 'REMOVESAVINGSOFFICER_CURRENTACCOUNT'
});
}
if (this.currentAccountData.charges) {
const charges: any[] = this.currentAccountData.charges;
charges.forEach((charge: any) => {
if (charge.name === 'Annual fee - INR') {
this.buttonConfig.addOption({
name: 'Apply Annual Fees',
taskPermissionName: 'APPLYANNUALFEE_CURRENTACCOUNT'
});
}
});
}
if (this.currentAccountData.taxGroup) {
if (this.currentAccountData.withHoldTax) {
this.buttonConfig.addOption({
name: 'Disable Withhold Tax',
taskPermissionName: 'UPDATEWITHHOLDTAX_CURRENTACCOUNT'
});
} else {
this.buttonConfig.addOption({
name: 'Enable Withhold Tax',
taskPermissionName: 'UPDATEWITHHOLDTAX_CURRENTACCOUNT'
});
}
}
}
/**
* Refetches data foe the component
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
*/
private reload() {
const url: string = this.router.url;
const refreshUrl: string = this.router.url.slice(
0,
this.router.url.indexOf('current-accounts') + 'current-accounts'.length
);
this.router.navigateByUrl(refreshUrl, { skipLocationChange: true }).then(() => this.router.navigate([url]));
}
/**
* Performs action button/option action.
* @param {string} name action name.
*/
doAction(name: string) {
switch (name) {
case 'Approve':
case 'Reject':
case 'Deposit':
case 'Activate':
case 'Close':
case 'Undo Approval':
case 'Post Interest As On':
case 'Assign Staff':
case 'Add Charge':
case 'Hold Amount':
case 'Block Account':
case 'Block Deposit':
case 'Block Withdrawal':
case 'Unassign Staff':
case 'Withdrawn by Client':
case 'Apply Annual Fees':
this.router.navigate([`actions/${name}`], { relativeTo: this.route });
break;
case 'Withdrawal':
this.router.navigate([`actions/Withdrawal`], { relativeTo: this.route });
break;
case 'Modify Application':
this.router.navigate(['edit'], { relativeTo: this.route });
break;
case 'Delete':
this.deleteCurrentAccount();
break;
case 'Calculate Interest':
this.calculateInterest();
break;
case 'Post Interest':
this.postInterest();
break;
case 'Enable Withhold Tax':
this.enableWithHoldTax();
break;
case 'Disable Withhold Tax':
this.disableWithHoldTax();
break;
case 'Transfer Funds':
const queryParams: any = {
interbank: false,
currentAccountId: this.currentAccountData.id,
accountType: 'fromcurrentaccount'
};
this.router.navigate(['transfer-funds/make-account-transfer'], {
relativeTo: this.route,
queryParams: queryParams,
state: { balance: this.currentAccountData.summary.availableBalance }
});
break;
case 'Interbank Transfer': {
const queryParams: any = {
interbank: true,
currentAccountId: this.currentAccountData.id,
accountType: 'interbank'
};
this.router.navigate(['transfer-funds/make-account-transfer'], {
relativeTo: this.route,
queryParams: queryParams,
state: { balance: this.currentAccountData.summary.availableBalance }
});
break;
}
case 'Unblock Account':
case 'Unblock Deposit':
case 'Unblock Withdrawal':
this.unblockCurrentAccount(name);
break;
}
}
/**
* Deletes Current Account.
*/
private deleteCurrentAccount() {
const deleteCurrentAccountDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `current account with id: ${this.currentAccountData.id}` }
});
deleteCurrentAccountDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.currentAccountsService.deleteCurrentAccount(this.currentAccountData.id).subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
}
});
}
/**
* Calculates current account's interest
*/
private calculateInterest() {
const calculateInterestAccountDialogRef = this.dialog.open(CalculateInterestDialogComponent);
calculateInterestAccountDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountCommand(this.currentAccountData.id, 'calculateInterest', {})
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Posts current account's interest
*/
private postInterest() {
const postInterestAccountDialogRef = this.dialog.open(PostInterestDialogComponent);
postInterestAccountDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountCommand(this.currentAccountData.id, 'postInterest', {})
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Enables withhold tax for current account.
*/
private enableWithHoldTax() {
const deleteCurrentAccountDialogRef = this.dialog.open(ToggleWithholdTaxDialogComponent, {
data: { isEnable: true }
});
deleteCurrentAccountDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountUpdateCommand(this.currentAccountData.id, 'updateWithHoldTax', { withHoldTax: true })
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Disables withhold tac for current account
*/
private disableWithHoldTax() {
const disableWithHoldTaxDialogRef = this.dialog.open(ToggleWithholdTaxDialogComponent, {
data: { isEnable: false }
});
disableWithHoldTaxDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountUpdateCommand(this.currentAccountData.id, 'updateWithHoldTax', { withHoldTax: false })
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Unblock Current Account.
*/
private unblockCurrentAccount(action: string) {
const unblockCurrentAccountDialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
heading: this.translateService.instant('labels.heading.Current Account'),
dialogContext:
this.translateService.instant('labels.dialogContext.Are you sure you want') +
action +
this.translateService.instant('this Current Account')
}
});
let command = 'unblock';
if (action === 'Unblock Deposit') {
command = 'unblockCredit';
}
if (action === 'Unblock Withdrawal') {
command = 'unblockDebit';
}
unblockCurrentAccountDialogRef.afterClosed().subscribe((response: { confirm: any }) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountCommand(this.currentAccountData.id, command, {})
.subscribe(() => {
this.reload();
});
}
});
}
}
@@ -0,0 +1,197 @@
/**
* 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/.
*/
/** Current Account Buttons Configuration */
export class CurrentAccountButtonsConfiguration {
optionArray: {
name: string;
taskPermissionName: string;
}[];
buttonsArray: {
name: string;
icon: string;
taskPermissionName: string;
}[];
constructor(status: string, substatus: any) {
this.setOptions(status);
this.setButtons(status, substatus);
}
get singleButtons() {
return this.buttonsArray;
}
get options() {
return this.optionArray;
}
setButtons(status: string, substatus: any) {
switch (status) {
case 'Active':
this.buttonsArray = [];
if (!substatus.block) {
if (!substatus.blockCredit) {
this.buttonsArray.push(
{
name: 'Deposit',
icon: 'arrow-up',
taskPermissionName: 'DEPOSIT_CURRENTACCOUNT'
},
{
name: 'Block Deposit',
icon: 'lock',
taskPermissionName: 'BLOCKDEPOSIT_CURRENTACCOUNT'
}
);
} else {
this.buttonsArray.push({
name: 'Unblock Deposit',
icon: 'lock-open',
taskPermissionName: 'UNBLOCKDEPOSIT_CURRENTACCOUNT'
});
}
if (!substatus.blockDebit) {
this.buttonsArray.push(
{
name: 'Withdrawal',
icon: 'arrow-down',
taskPermissionName: 'WITHDRAWAL_CURRENTACCOUNT'
},
{
name: 'Block Withdrawal',
icon: 'lock',
taskPermissionName: 'BLOCKWITHDRAWAL_CURRENTACCOUNT'
}
);
} else {
this.buttonsArray.push({
name: 'Unblock Withdrawal',
icon: 'lock-open',
taskPermissionName: 'UNBLOCKWITHDRAWAL_CURRENTACCOUNT'
});
}
this.buttonsArray = this.buttonsArray.concat([
{
name: 'Block Account',
icon: 'lock',
taskPermissionName: 'BLOCK_CURRENTACCOUNT'
},
{
name: 'Hold Amount',
icon: 'lock',
taskPermissionName: 'HOLDAMOUNT_CURRENTACCOUNT'
},
{
name: 'Calculate Interest',
icon: 'table',
taskPermissionName: 'CALCULATEINTEREST_CURRENTACCOUNT'
},
{
name: 'Post Interest As On',
icon: 'arrow-right',
taskPermissionName: 'POSTINTEREST_CURRENTACCOUNT'
}
]);
} else {
this.buttonsArray = [
{
name: 'Unblock Account',
icon: 'lock-open',
taskPermissionName: 'UNBLOCK_CURRENTACCOUNT'
}
];
}
break;
case 'Submitted and pending approval':
this.buttonsArray = [
{
name: 'Modify Application',
icon: 'pen',
taskPermissionName: 'UPDATE_CURRENTACCOUNT'
},
{
name: 'Approve',
icon: 'check',
taskPermissionName: 'APPROVE_CURRENTACCOUNT'
}
];
break;
case 'Approved':
this.buttonsArray = [
{
name: 'Undo Approval',
icon: 'undo',
taskPermissionName: 'APPROVALUNDO_CURRENTACCOUNT'
},
{
name: 'Activate',
icon: 'check',
taskPermissionName: 'ACTIVATE_CURRENTACCOUNT'
},
{
name: 'Add Charge',
icon: 'plus',
taskPermissionName: 'CREATE_CURRENTACCOUNTCHARGE'
}
];
break;
default:
this.buttonsArray = [];
}
}
setOptions(status: string) {
switch (status) {
case 'Active':
this.optionArray = [
{
name: 'Post Interest',
taskPermissionName: 'POSTINTEREST_CURRENTACCOUNT'
},
{
name: 'Add Charge',
taskPermissionName: 'CREATE_CURRENTACCOUNTCHARGE'
},
{
name: 'Close',
taskPermissionName: 'CLOSE_CURRENTACCOUNT'
}
];
break;
case 'Submitted and pending approval':
this.optionArray = [
{
name: 'Reject',
taskPermissionName: 'REJECT_CURRENTACCOUNT'
},
{
name: 'Withdrawn by Client',
taskPermissionName: 'WITHDRAW_CURRENTACCOUNT'
},
{
name: 'Add Charge',
taskPermissionName: 'CREATE_CURRENTACCOUNTCHARGE'
},
{
name: 'Delete',
taskPermissionName: 'DELETE_CURRENTACCOUNT'
}
];
break;
case 'Approved':
default:
this.optionArray = [];
}
}
addOption(option: { name: string; taskPermissionName: string }) {
this.optionArray.push(option);
}
}
@@ -0,0 +1,16 @@
<!--
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/.
-->
<mifosx-entity-documents-tab
[entityType]="entityType"
[entityId]="entityId"
[entityDocuments]="entityDocuments"
[callbackUpload]="uploadDocument"
[callbackDelete]="deleteDocument"
>
</mifosx-entity-documents-tab>
@@ -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,85 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { environment } from '../../../../environments/environment';
import { EntityDocumentsTabComponent } from '../../../shared/tabs/entity-documents-tab/entity-documents-tab.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-documents-tab',
templateUrl: './current-accounts-documents-tab.component.html',
styleUrls: ['./current-accounts-documents-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
EntityDocumentsTabComponent
]
})
export class CurrentAccountDocumentsTabComponent {
private route = inject(ActivatedRoute);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
dialog = inject(MatDialog);
/** Stores the resolved current account documents data */
entityDocuments: any;
/** Stores the current account Account Id */
entityId: string;
entityType = 'currentaccounts';
/**
* Retrieves the current account data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
*/
constructor() {
this.route.data.subscribe((data: { currentAccountDocuments: any }) => {
this.setCurrentAccountDocumentsData(data.currentAccountDocuments);
});
this.entityId = this.route.parent.snapshot.paramMap.get('currentAccountId');
}
setCurrentAccountDocumentsData(data: any) {
data.forEach((ele: any) => {
ele.docUrl =
this.settingsService.serverUrl +
'/currentaccounts/' +
ele.parentEntityId +
'/documents/' +
ele.id +
'/attachment?tenantIdentifier=' +
environment.fineractPlatformTenantId;
if (ele.fileName) {
if (
ele.fileName.toLowerCase().indexOf('.jpg') !== -1 ||
ele.fileName.toLowerCase().indexOf('.jpeg') !== -1 ||
ele.fileName.toLowerCase().indexOf('.png') !== -1
) {
ele.fileIsImage = true;
}
}
if (ele.type) {
if (ele.type.toLowerCase().indexOf('image') !== -1) {
ele.fileIsImage = true;
}
}
});
this.entityDocuments = data;
}
uploadDocument(formData: FormData): any {
return this.currentAccountsService.loadCurrentAccountDocument(this.entityId, formData);
}
deleteDocument(documentId: any) {
this.currentAccountsService.deleteCurrentAccountDocument(this.entityId, documentId).subscribe((res: any) => {});
}
}
@@ -0,0 +1,18 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'labels.heading.Calculate Interest' | translate }}</h1>
<div mat-dialog-content>
<p>{{ 'labels.text.Are you sure you want to calculate interest' | translate }} ?</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,39 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Calculate interest dialog component.
*/
@Component({
selector: 'mifosx-calculate-interest-dialog',
templateUrl: './calculate-interest-dialog.component.html',
styleUrls: ['./calculate-interest-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class CalculateInterestDialogComponent {
dialogRef = inject<MatDialogRef<CalculateInterestDialogComponent>>(MatDialogRef);
}
@@ -0,0 +1,18 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'tooltips.Inactivate Charge' | translate }}</h1>
<div mat-dialog-content>
<p>{{ 'labels.text.Are you sure you want to inactivate charge with id' | translate }}: {{ data.id }} ?</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,41 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Inactivate charge dialog component.
*/
@Component({
selector: 'mifosx-inactivate-charge-dialog',
templateUrl: './inactivate-charge-dialog.component.html',
styleUrls: ['./inactivate-charge-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class InactivateChargeDialogComponent {
dialogRef = inject<MatDialogRef<InactivateChargeDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
}
@@ -0,0 +1,18 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'labels.heading.Post Interest' | translate }}</h1>
<div mat-dialog-content>
<p>{{ 'labels.text.Are you sure you want to post interest' | translate }} ?</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,39 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Post interest dialog component.
*/
@Component({
selector: 'mifosx-post-interest-dialog',
templateUrl: './post-interest-dialog.component.html',
styleUrls: ['./post-interest-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class PostInterestDialogComponent {
dialogRef = inject<MatDialogRef<PostInterestDialogComponent>>(MatDialogRef);
}
@@ -0,0 +1,18 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'labels.heading.Release Amount' | translate }}</h1>
<div mat-dialog-content>
<p>{{ 'labels.text.Are you sure you want to release this amount' | translate }}</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="warn" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,35 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import {
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-release-amount-dialog',
templateUrl: './release-amount-dialog.component.html',
styleUrls: ['./release-amount-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class ReleaseAmountDialogComponent {
dialogRef = inject<MatDialogRef<ReleaseAmountDialogComponent>>(MatDialogRef);
}
@@ -0,0 +1,25 @@
<!--
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/.
-->
<h1 mat-dialog-title>
{{ (data.isEnable ? 'labels.heading.Enable' : 'labels.heading.Disable') | translate }}
{{ 'labels.heading.Withhold Tax' | translate }}
</h1>
<div mat-dialog-content>
<p>
{{ 'labels.text.Are you sure you want to' | translate }}
{{ (data.isEnable ? 'labels.heading.Enable' : 'labels.heading.Disable') | translate }}
{{ 'labels.text.withhold tax for this account' | translate }} ?
</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="{{ data.isEnable ? 'accent' : 'warn' }}" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,41 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Toggle withhold tax dialog dialog component.
*/
@Component({
selector: 'mifosx-toggle-withhold-tax-dialog',
templateUrl: './toggle-withhold-tax-dialog.component.html',
styleUrls: ['./toggle-withhold-tax-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class ToggleWithholdTaxDialogComponent {
dialogRef = inject<MatDialogRef<ToggleWithholdTaxDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
}
@@ -0,0 +1,18 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'labels.heading.Undo Transaction' | translate }}</h1>
<div mat-dialog-content>
<p>{{ 'labels.text.Are you sure you want to undo this transaction' | translate }}</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="warn" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,38 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Undo transaction dialog component.
*/
@Component({
selector: 'mifosx-undo-transaction-dialog',
templateUrl: './undo-transaction-dialog.component.html',
styleUrls: ['./undo-transaction-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class UndoTransactionDialogComponent {
dialogRef = inject<MatDialogRef<UndoTransactionDialogComponent>>(MatDialogRef);
}
@@ -0,0 +1,18 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'labels.buttons.Waive Charge' | translate }}</h1>
<div mat-dialog-content>
<p>{{ 'labels.text.Are you sure you want to waive charge with id' | translate }}: {{ data.id }} ?</p>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ confirm: true }">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
@@ -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,41 @@
/**
* 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, inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Waive charge dialog component.
*/
@Component({
selector: 'mifosx-waive-charge-dialog',
templateUrl: './waive-charge-dialog.component.html',
styleUrls: ['./waive-charge-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class WaiveChargeDialogComponent {
dialogRef = inject<MatDialogRef<WaiveChargeDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
}
@@ -0,0 +1,17 @@
<!--
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="tab-container mat-typography">
<mifosx-entity-datatable-tab
entityType="CurrentAccounts"
[entityId]="entityId"
[multiRowDatatableFlag]="multiRowDatatableFlag"
[entityDatatable]="entityDatatable"
>
</mifosx-entity-datatable-tab>
</div>
@@ -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,48 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EntityDatatableTabComponent } from '../../../shared/tabs/entity-datatable-tab/entity-datatable-tab.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Datatable Tabs Component
*/
@Component({
selector: 'mifosx-datatable-tabs',
templateUrl: './datatable-tabs.component.html',
styleUrls: ['./datatable-tabs.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
EntityDatatableTabComponent
]
})
export class DatatableTabsComponent {
private route = inject(ActivatedRoute);
entityId: string;
/** Current Account Datatable */
entityDatatable: any;
/** Multi Row Datatable Flag */
multiRowDatatableFlag: boolean;
/**
* Fetches Current Account and datatables data from `resolve`
* @param {ActivatedRoute} route Activated Route.
*/
constructor() {
this.entityId = this.route.parent.parent.snapshot.paramMap.get('currentAccountId');
this.route.data.subscribe((data: { currentAccountDatatable: any }) => {
this.entityDatatable = data.currentAccountDatatable;
this.multiRowDatatableFlag = this.entityDatatable.columnHeaders[0].columnName === 'id' ? true : false;
});
}
}
@@ -0,0 +1,367 @@
<!--
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 (isLoading) {
<div class="loader">
<div class="loading"></div>
</div>
}
@if (!isLoading) {
<div class="current-account-tables layout-row gap-2percent">
<div class="flex-49 layout-column gap-4percent">
@if (!currentAccountData.status.rejected && !currentAccountData.status.submittedAndPendingApproval) {
<div>
<h4 class="table-headers">{{ 'labels.heading.Current Account Details' | translate }}</h4>
<table>
<tbody>
<tr>
<td>{{ 'labels.inputs.External Id' | translate }}</td>
@if (currentAccountData.externalId) {
<td>
<mifosx-external-identifier
externalId="{{ currentAccountData.externalId }}"
></mifosx-external-identifier>
</td>
}
@if (!currentAccountData.externalId) {
<td>{{ 'labels.inputs.Not Provided' | translate }}</td>
}
</tr>
<tr>
<td>{{ 'labels.inputs.Activated On' | translate }}</td>
<td>
{{
currentAccountData.timeline.activatedOnDate
? (currentAccountData.timeline.activatedOnDate | dateFormat)
: 'Not Activated'
}}
</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Field Officer' | translate }}</td>
<td>{{ currentAccountData.fieldOfficerName ? currentAccountData.fieldOfficerName : 'Unassigned' }}</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Currency' | translate }}</td>
<td>{{ currentAccountData.currency.name }} [{{ currentAccountData.currency.code }}]</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Nominal Interest Rate' | translate }}</td>
<td class="r-amount">
@if (currentAccountData.nominalAnnualInterestRate) {
<span>{{ currentAccountData.nominalAnnualInterestRate | formatNumber }} %</span>
}
@if (!currentAccountData.nominalAnnualInterestRate) {
<span>0 %</span>
}
</td>
</tr>
</tbody>
</table>
</div>
} @else {
<div>
<h4 class="table-headers">{{ 'labels.heading.Current Account Details' | translate }}</h4>
<table>
<tbody>
<tr>
<td>{{ 'labels.inputs.Field Officer' | translate }}</td>
<td>{{ currentAccountData.fieldOfficerName ? currentAccountData.fieldOfficerName : 'Unassigned' }}</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Balance' | translate }}</td>
<td class="r-amount">
{{ currentAccountData.summary.accountBalance | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
</tbody>
</table>
</div>
}
@if (!currentAccountData.status.rejected && !currentAccountData.status.submittedAndPendingApproval) {
<div>
<h4 class="table-headers">{{ 'labels.heading.Performance History' | translate }}</h4>
<table>
<tbody>
@if (currentAccountData.summary.totalDeposits) {
<tr>
<td>{{ 'labels.inputs.Total Deposits' | translate }}</td>
<td class="r-amount">
{{ currentAccountData.summary.totalDeposits | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
@if (currentAccountData.summary.totalInterestEarned >= 0) {
<tr>
<td>{{ 'labels.inputs.Total Interest Earned' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.totalInterestEarned
| currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
@if (isActive) {
<div class="flex-49">
<h4 class="table-headers">{{ 'labels.heading.Account Summary' | translate }}</h4>
<table>
<tbody>
@if (currentAccountData.summary.totalWithdrawals) {
<tr>
<td>{{ 'labels.inputs.Total Withdrawls' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.totalWithdrawals | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.summary.totalWithdrawalFees) {
<tr>
<td>{{ 'labels.inputs.Withdrawals Fees' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.totalWithdrawalFees | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.summary.totalAnnualFees) {
<tr>
<td>{{ 'labels.inputs.Annual Fees' | translate }}</td>
<td class="r-amount">
{{ currentAccountData.summary.totalAnnualFees | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
@if (currentAccountData.summary.totalInterestEarned >= 0) {
<tr>
<td>{{ 'labels.inputs.Interest Earned' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.totalInterestEarned | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.summary.totalInterestPosted) {
<tr>
<td>{{ 'labels.inputs.Interest Posted' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.totalInterestPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.summary.interestNotPosted >= 0) {
<tr>
<td>{{ 'labels.inputs.Interest Earned Not Posted' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.interestNotPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.summary.totalOverdraftInterestDerived) {
<tr>
<td>{{ 'labels.inputs.Interest On Overdraft' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.totalOverdraftInterestDerived
| currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.summary.interestNotPosted < 0) {
<tr>
<td>{{ 'labels.inputs.Overdraft Interest Not Posted' | translate }}</td>
<td class="r-amount">
{{
currentAccountData.summary.interestNotPosted | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.nominalAnnualInterestRate) {
<tr>
<td>{{ 'labels.inputs.Nominal Interest Rate' | translate }}</td>
<td class="r-amount">{{ currentAccountData.nominalAnnualInterestRate | formatNumber }} %</td>
</tr>
}
<tr>
<td>{{ 'labels.inputs.Interest Compounding Period' | translate }}</td>
<td>{{ currentAccountData.interestCompoundingPeriodType.value | translateKey: 'catalogs' }}</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Interest Posting Period' | translate }}</td>
<td>{{ currentAccountData.interestPostingPeriodType.value | translateKey: 'catalogs' }}</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Interest Calculated Using' | translate }}</td>
<td>{{ currentAccountData.interestCalculationType.value | translateKey: 'catalogs' }}</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Days in Year' | translate }}</td>
<td>{{ currentAccountData.interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}</td>
</tr>
@if (currentAccountData.withdrawalFee) {
<tr>
<td>{{ 'labels.inputs.Withdrawal Fee' | translate }}</td>
<td class="r-amount">
{{ currentAccountData.withdrawalFee.amount | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
@if (currentAccountData.lastActiveTransactionDate) {
<tr>
<td>{{ 'labels.inputs.Last Active Transaction Date' | translate }}</td>
<td>{{ currentAccountData.lastActiveTransactionDate | dateFormat }}</td>
</tr>
}
@if (!(currentAccountData.subStatus.id === 0)) {
<tr>
<td>{{ 'labels.inputs.Substatus' | translate }}</td>
<td>{{ currentAccountData.subStatus.value }}</td>
</tr>
}
@if (currentAccountData.daysToInactive) {
<tr>
<td>{{ 'labels.inputs.Days to Inactive' | translate }}</td>
<td>{{ currentAccountData.daysToInactive }}</td>
</tr>
}
@if (currentAccountData.daysToDormancy) {
<tr>
<td>{{ 'labels.inputs.Days to Dormancy' | translate }}</td>
<td>{{ currentAccountData.daysToDormancy }}</td>
</tr>
}
@if (currentAccountData.daysToEscheat) {
<tr>
<td>{{ 'labels.inputs.Days to Escheat' | translate }}</td>
<td>{{ currentAccountData.daysToEscheat }}</td>
</tr>
}
@if (currentAccountData.annualFee) {
<tr>
<td>{{ 'labels.inputs.Annual Fee' | translate }}</td>
<td class="r-amount">
{{ currentAccountData.annualFee.amount | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
@if (currentAccountData.allowOverdraft) {
<tr>
<td>{{ 'labels.inputs.Overdraft Limit' | translate }}</td>
<td class="r-amount">
{{ currentAccountData.overdraftLimit | currency: currency.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
@if (currentAccountData.allowOverdraft) {
<tr>
<td>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}</td>
<td>
{{
currentAccountData.minOverdraftForInterestCalculation
| currency: currency.code : 'symbol-narrow' : '1.2-2'
}}
</td>
</tr>
}
@if (currentAccountData.minBalanceForInterestCalculation) {
<tr>
<td>{{ 'labels.inputs.Min Balance Required for Interest Calculation' | translate }}</td>
<td>{{ currentAccountData.minBalanceForInterestCalculation | formatNumber }}</td>
</tr>
}
@if (currentAccountData.minRequiredBalance) {
<tr>
<td>{{ 'labels.inputs.Minimum Required Balance' | translate }}</td>
<td>{{ currentAccountData.minRequiredBalance | formatNumber }}</td>
</tr>
}
@if (currentAccountData.enforceMinRequiredBalance) {
<tr>
<td>{{ 'labels.inputs.Enforce Minimum Required Balance' | translate }}</td>
<td>{{ currentAccountData.enforceMinRequiredBalance | yesNo }}</td>
</tr>
}
@if (currentAccountData.summary.lastInterestCalculationDate) {
<tr>
<td>{{ 'labels.inputs.Interest Recalculation Date' | translate }}</td>
<td>{{ currentAccountData.summary.lastInterestCalculationDate | dateFormat }}</td>
</tr>
}
@if (currentAccountData.onHoldFunds) {
<tr>
<td>{{ 'labels.inputs.On Hold Funds' | translate }}</td>
<td>
<a *mifosxHasPermission="'READ_CURRENTACCOUNT'">{{
currentAccountData.onHoldFunds | currency: currency.code : 'symbol-narrow' : '1.2-2'
}}</a>
</td>
</tr>
}
@if (currentAccountData.withHoldTax) {
<tr>
<td>{{ 'labels.inputs.Withhold Tax Group' | translate }}</td>
<td>{{ currentAccountData.taxGroup.name }}</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<div class="flex-49">
<h4 class="table-headers">{{ 'labels.heading.Account Summary' | translate }}</h4>
<table>
<tbody>
<tr>
<td>{{ 'labels.inputs.External Id' | translate }}</td>
@if (currentAccountData.externalId) {
<td>
<mifosx-external-identifier
externalId="{{ currentAccountData.externalId }}"
></mifosx-external-identifier>
</td>
}
@if (!currentAccountData.externalId) {
<td>{{ 'labels.inputs.Not Provided' | translate }}</td>
}
</tr>
<tr>
<td>{{ 'labels.inputs.Activated On' | translate }}</td>
<td>
{{
currentAccountData.timeline.activatedOnDate
? (currentAccountData.timeline.activatedOnDate | dateFormat)
: 'Not Activated'
}}
</td>
</tr>
<tr>
<td>{{ 'labels.inputs.Currency' | translate }}</td>
<td>{{ currentAccountData.currency.name }} [{{ currentAccountData.currency.code }}]</td>
</tr>
</tbody>
</table>
</div>
}
</div>
}
@@ -0,0 +1,242 @@
/**
* 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/.
*/
$bar: 160px;
$foo: $bar * 8;
$color: #e2e4ec;
.loader {
margin-bottom: 39px;
overflow: auto;
}
.sidenav {
overflow-x: hidden;
}
@keyframes move {
0% {
left: 0;
}
16.7% {
left: 0;
}
33.3% {
left: calc($foo / 2);
}
50% {
left: $foo - $bar;
}
66.7% {
left: calc($foo / 2);
}
83.3% {
left: 0;
}
100% {
left: 0;
}
}
@keyframes grow {
0% {
width: $bar;
}
16.7% {
width: calc($foo / 2);
}
33.3% {
width: calc($foo / 2);
}
50% {
width: $bar;
}
66.7% {
width: calc($foo / 2);
}
83.3% {
width: calc($foo / 2);
}
100% {
width: $bar;
}
}
.general-tab-container {
padding: 16px 20px;
width: 100%;
box-sizing: border-box;
position: relative;
background-color: var(--background-color, #f8f9fa);
color: var(--text-color, #333);
transition:
background-color 0.3s ease,
color 0.3s ease;
.page-header {
margin-bottom: 24px;
padding: 16px 0;
position: relative;
border-bottom: 1px solid var(--border-color, #e9ecef);
.header-title {
font-size: 1.75rem;
font-weight: 600;
margin: 0 0 8px;
line-height: 1.2;
color: var(--primary-color, #007bff);
position: relative;
}
.header-subtitle {
font-size: 0.95rem;
margin: 0;
line-height: 1.4;
color: var(--text-secondary, #6c757d);
position: relative;
}
}
.content-wrapper {
position: relative;
width: 100%;
}
.account-info-card {
background: var(--card-background, #fff);
border-radius: 8px;
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
overflow: hidden;
position: relative;
width: 100%;
max-width: 600px;
transition:
box-shadow 0.3s ease,
background-color 0.3s ease;
.card-header {
padding: 16px 20px;
background: var(--card-header-bg, #f8f9fa);
border-bottom: 1px solid var(--border-color, #e9ecef);
position: relative;
h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
line-height: 1.3;
color: var(--text-color, #333);
}
}
.card-content {
padding: 20px;
position: relative;
.info-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 10px 0;
border-bottom: 1px solid var(--border-light, #f1f3f4);
position: relative;
min-height: 24px;
&:last-child {
border-bottom: none;
}
.label {
font-weight: 500;
color: var(--text-secondary, #6c757d);
flex: 0 0 auto;
margin-right: 16px;
white-space: nowrap;
line-height: 1.4;
}
.value {
font-weight: 600;
color: var(--text-color, #333);
text-align: right;
flex: 1 1 auto;
line-height: 1.4;
&.currency {
color: var(--success-color, #28a745);
font-size: 1.05rem;
}
}
}
}
}
}
// Dark mode styles
[data-theme='dark'] .general-tab-container,
.dark-theme .general-tab-container {
--background-color: #1a1a1a;
--text-color: #e9ecef;
--text-secondary: #adb5bd;
--primary-color: #4dabf7;
--card-background: #2d3748;
--card-header-bg: #374151;
--border-color: #4a5568;
--border-light: #2d3748;
--success-color: #68d391;
}
// Responsive adjustments for mobile
@media (width <= 768px) {
.general-tab-container {
padding: 12px 16px;
.page-header {
margin-bottom: 20px;
padding: 12px 0;
.header-title {
font-size: 1.5rem;
}
.header-subtitle {
font-size: 0.9rem;
}
}
.account-info-card {
.card-header {
padding: 14px 16px;
}
.card-content {
padding: 16px;
.info-row {
padding: 8px 0;
.label {
margin-right: 12px;
}
}
}
}
}
}
@@ -0,0 +1,59 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Currency } from 'app/shared/models/general.model';
import { CurrencyPipe } from '@angular/common';
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
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';
@Component({
selector: 'mifosx-general-tab',
templateUrl: './general-tab.component.html',
styleUrls: ['./general-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
ExternalIdentifierComponent,
CurrencyPipe,
DateFormatPipe,
FormatNumberPipe,
YesnoPipe
]
})
export class GeneralTabComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
isLoading = true;
isActive = false;
entityType: string;
currentAccountData: any;
currency: Currency;
constructor() {
this.route.parent.parent.data.subscribe((data: { currentAccountData: any }) => {
this.currentAccountData = data.currentAccountData;
this.currency = this.currentAccountData.currency;
this.isLoading = false;
const status = this.currentAccountData.status.value;
this.isActive = status === 'Active';
});
if (this.router.url.includes('clients')) {
this.entityType = 'Client';
} else if (this.router.url.includes('groups')) {
this.entityType = 'Group';
} else if (this.router.url.includes('centers')) {
this.entityType = 'Center';
}
}
}
@@ -0,0 +1,16 @@
<!--
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/.
-->
<mifosx-entity-notes-tab
[entityId]="entityId"
[entityNotes]="entityNotes"
[callbackAdd]="addNote"
[callbackEdit]="editNote"
[callbackDelete]="deleteNote"
>
</mifosx-entity-notes-tab>
@@ -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,65 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AuthenticationService } from 'app/core/authentication/authentication.service';
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { EntityNotesTabComponent } from '../../../shared/tabs/entity-notes-tab/entity-notes-tab.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-notes-tab',
templateUrl: './notes-tab.component.html',
styleUrls: ['./notes-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
EntityNotesTabComponent
]
})
export class NotesTabComponent {
private route = inject(ActivatedRoute);
private currentAccountsService = inject(CurrentAccountsService);
private authenticationService = inject(AuthenticationService);
entityId: string;
username: string;
entityNotes: any;
constructor() {
const savedCredentials = this.authenticationService.getCredentials();
this.username = savedCredentials.username;
this.entityId = this.route.parent.snapshot.params['currentAccountId'];
this.route.data.subscribe((data: { currentAccountNotes: any }) => {
this.entityNotes = data.currentAccountNotes;
});
}
addNote(noteContent: any) {
this.currentAccountsService.createCurrentAccountNote(this.entityId, noteContent).subscribe((response: any) => {
this.entityNotes.push({
id: response.resourceId,
createdByUsername: this.username,
createdOn: new Date(),
note: noteContent.note
});
});
}
editNote(noteId: string, noteContent: any, index: number) {
this.currentAccountsService.editCurrentAccountNote(this.entityId, noteId, noteContent).subscribe(() => {
this.entityNotes[index].note = noteContent.note;
});
}
deleteNote(noteId: string, index: number) {
this.currentAccountsService.deleteCurrentAccountNote(this.entityId, noteId).subscribe(() => {
this.entityNotes.splice(index, 1);
});
}
}
@@ -0,0 +1,151 @@
<!--
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="passbook-container">
<!-- Date Range Filter -->
<div class="passbook-filter-bar">
<form [formGroup]="dateRangeForm" class="passbook-filter-form">
<mat-form-field appearance="outline" class="passbook-date-field" (click)="fromDatePicker.open()">
<mat-label>{{ 'labels.inputs.From Date' | translate }}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="fromDatePicker" formControlName="fromDate" />
<mat-datepicker-toggle matSuffix [for]="fromDatePicker"></mat-datepicker-toggle>
<mat-datepicker #fromDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="passbook-date-field" (click)="toDatePicker.open()">
<mat-label>{{ 'labels.inputs.To Date' | translate }}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="toDatePicker" formControlName="toDate" />
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
<mat-datepicker #toDatePicker></mat-datepicker>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="!dateRangeForm.valid"
(click)="filterTransactions()"
>
<mat-icon>filter_list</mat-icon>
{{ 'labels.buttons.Filter' | translate }}
</button>
@if (isFiltered) {
<button mat-stroked-button type="button" (click)="clearFilter()">
<mat-icon>clear</mat-icon>
{{ 'labels.buttons.Clear' | translate }}
</button>
}
<span class="passbook-filter-spacer"></span>
<button mat-raised-button color="accent" type="button" (click)="printPassbook()">
<mat-icon>print</mat-icon>
{{ 'labels.buttons.Print Passbook' | translate }}
</button>
</form>
</div>
<!-- Passbook Preview (Printable Content) -->
<div class="passbook-preview" #passbookContent>
<div class="passbook-header">
<h2>{{ 'labels.heading.Current Account Passbook' | translate }}</h2>
<h3>{{ productName }}</h3>
</div>
<!-- Account Info -->
<div class="passbook-info-grid">
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Account Number' | translate }}</div>
<div class="value">{{ accountNo }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Account Holder' | translate }}</div>
<div class="value">{{ clientName }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Current Account Product' | translate }}</div>
<div class="value">{{ productName }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Opening Date' | translate }}</div>
<div class="value">{{ activatedOnDate | dateFormat }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Status' | translate }}</div>
<div class="value">{{ status }}</div>
</div>
<div class="passbook-info-item">
<div class="label">{{ 'labels.inputs.Interest Rate' | translate }}</div>
<div class="value">{{ interestRate }}%</div>
</div>
</div>
<!-- Balance Summary -->
<div class="passbook-balance-row">
<div class="balance-item">
<span>{{ 'labels.inputs.Current Balance' | translate }}: </span>
<b>{{ accountBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</b>
</div>
<div class="balance-item">
<span>{{ 'labels.inputs.Available Balance' | translate }}: </span>
<b>{{ availableBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</b>
</div>
</div>
<!-- Transactions Table -->
@if (filteredTransactions.length > 0) {
<table>
<thead>
<tr>
<th>#</th>
<th>{{ 'labels.inputs.Date' | translate }}</th>
<th>{{ 'labels.inputs.Description' | translate }}</th>
<th class="text-right">{{ 'labels.inputs.Debit' | translate }}</th>
<th class="text-right">{{ 'labels.inputs.Credit' | translate }}</th>
<th class="text-right">{{ 'labels.inputs.Balance' | translate }}</th>
</tr>
</thead>
<tbody>
@for (txn of filteredTransactions; track txn.id; let i = $index) {
<tr [class.reversed]="txn.reversed">
<td>{{ i + 1 }}</td>
<td>{{ txn.date | dateFormat }}</td>
<td>{{ txn.transactionType.value }}</td>
<td class="text-right">
@if (isDebit(txn.transactionType)) {
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
}
</td>
<td class="text-right">
@if (!isDebit(txn.transactionType)) {
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
}
</td>
<td class="text-right">
{{ txn.runningBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
</td>
</tr>
}
</tbody>
</table>
} @else {
<div class="passbook-no-data">
<mat-icon>info_outline</mat-icon>
<span>{{ 'labels.text.No transactions found for the selected date range.' | translate }}</span>
</div>
}
<!-- Footer -->
<div class="passbook-footer">
{{ 'labels.text.Generated on' | translate }}: {{ maxDate | dateFormat }}
&nbsp;|&nbsp;
{{ 'labels.text.Total Transactions' | translate }}: {{ filteredTransactions.length }}
</div>
</div>
</div>
@@ -0,0 +1,206 @@
/**
* 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/.
*/
.passbook-container {
padding: 16px;
}
// Filter Bar
.passbook-filter-bar {
margin-bottom: 16px;
}
.passbook-filter-form {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.passbook-date-field {
width: 180px;
}
.passbook-filter-spacer {
flex: 1;
}
// Passbook Preview
.passbook-preview {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 24px;
background: #fff;
}
.passbook-header {
text-align: center;
margin-bottom: 20px;
border-bottom: 2px solid #1976d2;
padding-bottom: 12px;
h2 {
color: #1976d2;
font-size: 20px;
margin: 0 0 4px;
font-weight: 600;
}
h3 {
font-size: 14px;
font-weight: 400;
color: #666;
margin: 0;
}
}
.passbook-info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 20px;
margin-bottom: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
}
.passbook-info-item {
.label {
font-size: 10px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
font-size: 13px;
color: #333;
margin-top: 2px;
}
}
.passbook-balance-row {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
padding: 10px 14px;
background: #e3f2fd;
border-radius: 6px;
font-size: 14px;
.balance-item {
b {
color: #1976d2;
}
}
}
// Transactions Table
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th {
background: #1976d2;
color: #fff;
padding: 8px 10px;
text-align: left;
font-weight: 600;
font-size: 12px;
}
td {
padding: 6px 10px;
border-bottom: 1px solid #e0e0e0;
}
tr:nth-child(even) {
background: #fafafa;
}
tr:hover {
background: #f0f0f0;
}
.reversed {
text-decoration: line-through;
color: #999;
}
}
.text-right {
text-align: right;
}
.passbook-no-data {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 32px;
color: #888;
font-size: 14px;
mat-icon {
color: #bbb;
}
}
.passbook-footer {
margin-top: 16px;
font-size: 11px;
color: #999;
text-align: center;
border-top: 1px solid #e0e0e0;
padding-top: 10px;
}
// Responsive
@media (width <= 768px) {
.passbook-info-grid {
grid-template-columns: 1fr 1fr;
}
.passbook-balance-row {
flex-direction: column;
gap: 8px;
}
.passbook-filter-form {
flex-direction: column;
align-items: stretch;
}
.passbook-date-field {
width: 100%;
}
.passbook-filter-spacer {
display: none;
}
}
@media (width <= 480px) {
.passbook-info-grid {
grid-template-columns: 1fr;
}
}
// Print-specific styles
@media print {
.passbook-filter-bar {
display: none !important;
}
.passbook-preview {
border: none;
padding: 0;
}
}
@@ -0,0 +1,186 @@
/**
* 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, OnDestroy, inject, ElementRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { CurrencyPipe } from '@angular/common';
import { Subject, takeUntil } from 'rxjs';
/** Custom Services */
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { Currency } from 'app/shared/models/general.model';
import {
CurrentAccountTransaction,
CurrentAccountTransactionType
} from 'app/current-accounts/models/current-account-transaction.model';
import { MatIcon } from '@angular/material/icon';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-passbook-tab',
templateUrl: './passbook-tab.component.html',
styleUrls: ['./passbook-tab.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatIcon,
FaIconComponent,
DateFormatPipe,
CurrencyPipe
]
})
export class PassbookTabComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute);
private formBuilder = inject(UntypedFormBuilder);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
private destroy$ = new Subject<void>();
@ViewChild('passbookContent') passbookContent: ElementRef;
/** Account data */
accountNo: string = '';
clientName: string = '';
productName: string = '';
currency: Currency | null = null;
status: string = '';
interestRate: number = 0;
accountBalance: number = 0;
availableBalance: number = 0;
activatedOnDate: any = null;
/** Transactions */
allTransactions: CurrentAccountTransaction[] = [];
filteredTransactions: CurrentAccountTransaction[] = [];
/** Date range form */
minDate = new Date(2000, 0, 1);
maxDate = new Date();
dateRangeForm: any;
isFiltered = false;
ngOnInit(): void {
this.maxDate = this.settingsService.businessDate;
this.route.parent.data.pipe(takeUntil(this.destroy$)).subscribe((data: { currentAccountData: any }) => {
const account = data.currentAccountData;
this.accountNo = account.accountNo || '';
this.clientName = account.clientName || account.groupName || '';
this.productName = account.currentAccountProductName || '';
this.currency = account.currency || null;
this.status = account.status?.value || '';
this.interestRate = account.nominalAnnualInterestRate || 0;
this.accountBalance = account.summary?.accountBalance || 0;
this.availableBalance = account.summary?.availableBalance || 0;
this.activatedOnDate = account.timeline?.activatedOnDate || null;
this.allTransactions = account.transactions || [];
this.filteredTransactions = [...this.allTransactions];
});
this.dateRangeForm = this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
]
});
}
isDebit(transactionType: CurrentAccountTransactionType): boolean {
return (
transactionType.withdrawal === true ||
transactionType.feeDeduction === true ||
transactionType.overdraftInterest === true ||
transactionType.withholdTax === true
);
}
filterTransactions(): void {
const fromDate = this.dateRangeForm.value.fromDate;
const toDate = this.dateRangeForm.value.toDate;
if (!fromDate || !toDate) return;
const from = new Date(fromDate).getTime();
const to = new Date(toDate).getTime();
this.filteredTransactions = this.allTransactions.filter((txn: CurrentAccountTransaction) => {
const txnDate = this.getTransactionDateAsTime(txn.date);
return txnDate >= from && txnDate <= to;
});
this.isFiltered = true;
}
clearFilter(): void {
this.dateRangeForm.reset();
this.filteredTransactions = [...this.allTransactions];
this.isFiltered = false;
}
private getTransactionDateAsTime(date: any): number {
if (Array.isArray(date) && date.length >= 3) {
return new Date(date[0], date[1] - 1, date[2]).getTime();
}
return new Date(date).getTime();
}
printPassbook(): void {
const content = this.passbookContent?.nativeElement?.innerHTML;
if (!content) return;
const printWindow = window.open('', 'Passbook', 'height=700,width=900');
printWindow.document.write(`
<html>
<head>
<title>Current Account Passbook - ${this.accountNo}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; padding: 20px; color: #333; }
.passbook-header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #1976d2; padding-bottom: 12px; }
.passbook-header h2 { color: #1976d2; font-size: 18px; margin-bottom: 4px; }
.passbook-header h3 { font-size: 14px; font-weight: 400; color: #666; }
.passbook-info-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px 16px; margin-bottom: 16px; padding: 10px; background: #f5f5f5; border-radius: 4px; }
.passbook-info-item { font-size: 12px; }
.passbook-info-item .label { font-weight: 600; color: #666; text-transform: uppercase; font-size: 10px; }
.passbook-info-item .value { color: #333; }
.passbook-balance-row { display: flex; justify-content: space-between; margin-bottom: 16px; padding: 8px 10px; background: #e3f2fd; border-radius: 4px; font-size: 13px; }
.passbook-balance-row .balance-item b { color: #1976d2; }
table { width: 100%; border-collapse: collapse; font-size: 11px; }
th { background: #1976d2; color: #fff; padding: 6px 8px; text-align: left; font-weight: 600; }
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; }
tr:nth-child(even) { background: #fafafa; }
.text-right { text-align: right; }
.passbook-footer { margin-top: 16px; font-size: 10px; color: #999; text-align: center; border-top: 1px solid #e0e0e0; padding-top: 8px; }
.reversed { text-decoration: line-through; color: #999; }
</style>
</head>
<body>
${content}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
printWindow.close();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
@@ -0,0 +1,101 @@
<!--
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="tab-container mat-typography">
<div class="m-b-10">
<h3>{{ 'labels.heading.All Standing Instructions' | translate }}</h3>
</div>
<div class="mat-elevation-z1 m-b-25">
<table mat-table #instructionsTable [dataSource]="dataSource">
<ng-container matColumnDef="client">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client' | translate }}</th>
<td mat-cell *matCellDef="let instruction">
{{ instruction.fromClient.displayName }}-{{ instruction.fromClient.id }}
</td>
</ng-container>
<ng-container matColumnDef="fromAccount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.From Account' | translate }}</th>
<td mat-cell *matCellDef="let instruction">
{{ instruction.fromAccount.accountNo }} ({{ instruction.fromAccountType.value }})
</td>
</ng-container>
<ng-container matColumnDef="beneficiary">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Beneficiary' | translate }}</th>
<td mat-cell *matCellDef="let instruction">{{ instruction.toClient.displayName }}</td>
</ng-container>
<ng-container matColumnDef="toAccount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.To Account' | translate }}</th>
<td mat-cell *matCellDef="let instruction">
{{ instruction.toAccount.accountNo }} ({{ instruction.toAccountType.value }})
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let instruction">{{ instruction.instructionType.value }}/{{ instruction.amount }}</td>
</ng-container>
<ng-container matColumnDef="validity">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Validity' | translate }}</th>
<td mat-cell *matCellDef="let instruction">
{{ instruction.validFrom | dateFormat }} {{ 'labels.inputs.To' | translate }}
{{ instruction.validTill | dateFormat }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let instruction">
@if (instruction.status.value !== 'Deleted') {
<span>
<button
class="account-action-button"
mat-raised-button
color="primary"
matTooltip="{{ 'tooltips.Edit Standing Instruction' | translate }}"
*mifosxHasPermission="'UPDATE_STANDINGINSTRUCTION'"
>
<i class="fa fa-edit"></i>
</button>
</span>
}
@if (instruction.status.value !== 'Deleted') {
<span>
<button
class="account-action-button"
mat-raised-button
color="warn"
matTooltip="{{ 'tooltips.Delete Standing Instruction' | translate }}"
*mifosxHasPermission="'DELETE_STANDINGINSTRUCTION'"
(click)="deleteStandingInstruction(instruction.id)"
>
<i class="fa fa-times"></i>
</button>
</span>
}
<button
class="account-action-button"
mat-raised-button
color="primary"
matTooltip="{{ 'tooltips.View Standing Instruction' | translate }}"
*mifosxHasPermission="'READ_STANDINGINSTRUCTION'"
>
<i class="fa fa-eye"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
</div>
@@ -0,0 +1,31 @@
/**
* 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/.
*/
.tab-container {
padding: 1%;
margin: 1%;
h3 {
margin: 1% auto;
}
.action-button {
margin-left: auto;
}
table {
width: 100%;
.account-action-button {
min-width: 26px;
padding: 0 6px;
margin: 4px;
line-height: 25px;
}
}
}
@@ -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/.
*/
/** Angular Imports */
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { AccountTransfersService } from 'app/account-transfers/account-transfers.service';
import { SettingsService } from 'app/settings/settings.service';
/** Dialog Components */
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { MatTooltip } from '@angular/material/tooltip';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Standing Instructions Tab
*/
@Component({
selector: 'mifosx-standing-instructions-tab',
templateUrl: './standing-instructions-tab.component.html',
styleUrls: ['./standing-instructions-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatTooltip,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
DateFormatPipe
]
})
export class StandingInstructionsTabComponent implements OnInit {
private route = inject(ActivatedRoute);
private currentAccountsService = inject(CurrentAccountsService);
private dialog = inject(MatDialog);
private accountTransfersService = inject(AccountTransfersService);
private settingsService = inject(SettingsService);
/** Current Account Data */
currentAccountData: any;
/** Instructions Data */
instructionsData: any[];
/** Data source for instructions table. */
dataSource = new MatTableDataSource();
/** Columns to be displayed in instructions table. */
displayedColumns: string[] = [
'client',
'fromAccount',
'beneficiary',
'toAccount',
'amount',
'validity',
'actions'
];
/** Instruction Table Reference */
@ViewChild('instructionsTable', { static: true }) instructionTableRef: MatTable<Element>;
/**
* Retrieves Current Account Data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.route.parent.data.subscribe((data: { currentAccountData: any }) => {
this.currentAccountData = data.currentAccountData;
});
}
ngOnInit() {
this.getStandingInstructions();
}
/**
* Retrieves standing instructions and initializes instructions table.
*/
getStandingInstructions() {
const clientId = this.currentAccountData.clientId;
const clientName = this.currentAccountData.clientName;
const accountId = this.currentAccountData.id;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
this.currentAccountsService
.getStandingInstructions(clientId, clientName, accountId, locale, dateFormat)
.subscribe((response: any) => {
this.instructionsData = response.pageItems;
this.dataSource.data = this.instructionsData;
this.instructionTableRef.renderRows();
});
}
deleteStandingInstruction(instructionId: any) {
const deleteStandingInstructionDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `standing instruction id: ${instructionId}` }
});
deleteStandingInstructionDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.accountTransfersService.deleteStandingInstrucions(instructionId).subscribe(() => {});
}
});
}
}
@@ -0,0 +1,58 @@
<!--
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 class="m-t-20 layout-column" [formGroup]="transactionsReportForm" (ngSubmit)="generate()">
<div class="layout-row layout-align-center gap-3percent">
<mat-form-field class="flex-30" (click)="fromDatePicker.open()">
<mat-label>{{ 'labels.inputs.From Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="fromDatePicker"
required
formControlName="fromDate"
/>
<mat-datepicker-toggle matSuffix [for]="fromDatePicker"></mat-datepicker-toggle>
<mat-datepicker #fromDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.From Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-30" (click)="toDatePicker.open()">
<mat-label>{{ 'labels.inputs.To Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="toDatePicker"
required
formControlName="toDate"
/>
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
<mat-datepicker #toDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.To Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="generate-button layout-row layout-xs-column layout-align-center gap-5percent">
<button type="button" mat-raised-button [routerLink]="['../']">{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" [disabled]="!transactionsReportForm.valid">
<fa-icon icon="cogs" class="m-r-10"></fa-icon>{{ 'labels.buttons.Generate Report' | translate }} &nbsp;
</button>
</div>
</form>
@if (!hideOutput) {
<iframe [src]="pentahoUrl" frameborder="0" width="100%" height="750px;"></iframe>
}
@@ -0,0 +1,13 @@
/**
* 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/.
*/
.generate-button {
max-height: 2%;
padding: 1% 0 2% 6%;
align-self: center;
}
@@ -0,0 +1,120 @@
/**
* 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, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
/** Custom Services */
import { ReportsService } from 'app/reports/reports.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Export Client Current Account Transactions Component
*/
@Component({
selector: 'mifosx-export-transactions',
templateUrl: './export-transactions.component.html',
styleUrls: ['./export-transactions.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent
]
})
export class ExportTransactionsComponent implements OnInit {
private sanitizer = inject(DomSanitizer);
private reportsService = inject(ReportsService);
private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Transactions Report Form */
transactionsReportForm: any;
/** substitute for resolver */
hideOutput = true;
/** trusted resource url for pentaho output */
pentahoUrl: any;
/** Current Account Id */
currentAccountId: any;
/**
* Fetches current account data from grandparent's `resolve`
* @param {DomSanitizer} sanitizer DOM Sanitizer
* @param {ReportsService} reportsService Reports Service
* @param {FormBuilder} formBuilder Form Builder
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {SettingsService} settingsService Settings Service
*/
constructor() {
this.route.parent.parent.data.subscribe((data: { currentAccountData: any }) => {
this.currentAccountId = data.currentAccountData.accountNo;
});
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createTransactionsReportForm();
}
/**
* Creates the transactions report form.
*/
createTransactionsReportForm() {
this.transactionsReportForm = this.formBuilder.group({
fromDate: [
'',
Validators.required
],
toDate: [
'',
Validators.required
]
});
}
/**
* Generates client current account transactions report.
*/
generate() {
const data = {
'output-type': 'PDF',
R_startDate: this.dateUtils.formatDate(
this.transactionsReportForm.value.fromDate,
this.settingsService.dateFormat
),
R_endDate: this.dateUtils.formatDate(this.transactionsReportForm.value.toDate, this.settingsService.dateFormat),
R_currentAccountId: this.currentAccountId
};
this.reportsService
.getPentahoRunReportData(
'Client Current Account Transactions',
data,
'default',
this.settingsService.language.code,
this.settingsService.dateFormat
)
.subscribe((res: any) => {
const contentType = res.headers.get('Content-Type');
const file = new Blob([res.body], { type: contentType });
const filecontent = URL.createObjectURL(file);
this.pentahoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(filecontent);
this.hideOutput = false;
});
}
}
@@ -0,0 +1,185 @@
<!--
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="container">
<div class="mat-elevation-z1 m-b-25" [hidden]="!accountWithTransactions">
<div class="layout-row align-end">
@if (checkStatus()) {
<div class="action-button m-b-20 layout-row gap-20px">
<mat-checkbox [formControl]="hideReversedParam" (click)="hideReversed()" class="accruals">
{{ 'labels.inputs.Hide Reversed' | translate }}</mat-checkbox
>
<mat-checkbox [formControl]="hideAccrualsParam" (click)="hideAccruals()" class="accruals">
{{ 'labels.inputs.Hide Accruals' | translate }}</mat-checkbox
>
<button mat-raised-button color="primary" [routerLink]="['export']">
{{ 'labels.buttons.Export' | translate }}
</button>
</div>
}
</div>
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="row">
<th mat-header-cell *matHeaderCellDef mat-sort-header>#</th>
<td mat-cell *matCellDef="let transaction; let idx = index" (click)="showTransactions(transaction)">
{{ idx + 1 }}
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Id' | translate }}</th>
<td
mat-cell
*matCellDef="let transaction"
class="td-select"
[ngClass]="currentAccountTransactionColor(transaction)"
(click)="showTransactions(transaction)"
>
{{ transaction.id }}
</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
{{ 'labels.inputs.Transaction Date' | translate }}
</th>
<td
mat-cell
*matCellDef="let transaction"
[ngClass]="currentAccountTransactionColor(transaction)"
(click)="showTransactions(transaction)"
>
{{ transaction.date | dateFormat }}
</td>
</ng-container>
<ng-container matColumnDef="externalId">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.External Id' | translate }}</th>
<td mat-cell *matCellDef="let transaction" [ngClass]="currentAccountTransactionColor(transaction)">
<mifosx-external-identifier externalId="{{ transaction.externalId }}"></mifosx-external-identifier>
</td>
</ng-container>
<ng-container matColumnDef="transactionType">
<th mat-header-cell class="center" *matHeaderCellDef>{{ 'labels.inputs.Transaction Type' | translate }}</th>
<td
mat-cell
*matCellDef="let transaction"
[ngClass]="currentAccountTransactionColor(transaction)"
(click)="showTransactions(transaction)"
>
{{ transaction.transactionType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="debit">
<th mat-header-cell class="r-amount" *matHeaderCellDef>{{ 'labels.inputs.Debit' | translate }}</th>
<td
mat-cell
class="r-amount"
*matCellDef="let transaction"
[ngClass]="currentAccountTransactionColor(transaction)"
(click)="showTransactions(transaction)"
>
{{
isDebit(transaction.transactionType)
? (transaction.amount | currency: currency?.code || 'USD' : 'symbol-narrow' : '1.2-2')
: 'N/A'
}}
</td>
</ng-container>
<ng-container matColumnDef="credit">
<th mat-header-cell class="r-amount" *matHeaderCellDef>{{ 'labels.inputs.Credit' | translate }}</th>
<td
mat-cell
class="r-amount"
*matCellDef="let transaction"
[ngClass]="currentAccountTransactionColor(transaction)"
(click)="showTransactions(transaction)"
>
{{
!isDebit(transaction.transactionType)
? (transaction.amount | currency: currency?.code || 'USD' : 'symbol-narrow' : '1.2-2')
: 'N/A'
}}
</td>
</ng-container>
<ng-container matColumnDef="balance">
<th mat-header-cell class="r-amount" *matHeaderCellDef>{{ 'labels.inputs.Balance' | translate }}</th>
<td
mat-cell
class="r-amount"
*matCellDef="let transaction"
[ngClass]="currentAccountTransactionColor(transaction)"
(click)="showTransactions(transaction)"
>
{{ transaction.runningBalance | currency: currency?.code || 'USD' : 'symbol-narrow' : '1.2-2' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell class="center" *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell class="center" *matCellDef="let transaction">
<button mat-icon-button [matMenuTriggerFor]="transactionMenu" aria-label="" class="action-button">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #transactionMenu="matMenu">
<button mat-menu-item (click)="showTransactions(transaction)">
<mat-icon><fa-icon icon="eye" size="sm"></fa-icon></mat-icon>
<span>{{ 'labels.text.View Transaction' | translate }}</span>
</button>
@if (!transaction.reversed) {
<span>
<button
mat-menu-item
*mifosxHasPermission="'ADJUSTTRANSACTION_CURRENTACCOUNT'"
(click)="undoTransaction(transaction, $event)"
>
<mat-icon><fa-icon icon="undo" size="sm"></fa-icon></mat-icon>
<span>{{ 'tooltips.Undo Transaction' | translate }}</span>
</button>
</span>
}
<button
mat-menu-item
class="account-action-button"
(click)="routeEdit($event)"
[routerLink]="[transaction.id, 'reciept']"
>
<mat-icon><fa-icon icon="receipt" size="sm"></fa-icon></mat-icon>
<span>{{ 'tooltips.View Receipts' | translate }}</span>
</button>
<button
mat-menu-item
(click)="routeEdit($event)"
[routerLink]="['/', 'journal-entry', 'view', 'S' + transaction.id]"
>
<mat-icon><fa-icon icon="table-cells" size="sm"></fa-icon></mat-icon>
<span>{{ 'tooltips.View Journal Entries' | translate }}</span>
</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="select-row"></tr>
</table>
<mat-paginator [pageSize]="50" [pageSizeOptions]="[50, 100, 200]" showFirstLastButtons></mat-paginator>
</div>
<div class="alert" [hidden]="accountWithTransactions">
<div class="message">
<i class="fa fa-exclamation-circle alert-check"></i>
{{ 'labels.text.No transaction was found' | translate }}
</div>
</div>
</div>
@@ -0,0 +1,42 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use 'assets/styles/helper';
.tab-container {
padding: 1%;
margin: 1%;
h3 {
margin: 1% auto;
}
.action-button {
margin-left: auto;
}
table {
width: 100%;
.account-action-button {
min-width: 26px;
padding: 0 0 3px;
margin: 0 10%;
line-height: 25px;
.accruals {
padding-top: 1%;
}
}
}
}
.select-row:hover {
cursor: pointer;
font-weight: 800;
}
@@ -0,0 +1,270 @@
/**
* 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, ViewChild, inject } from '@angular/core';
import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader } from '@angular/material/sort';
import { CurrencyPipe, NgClass } from '@angular/common';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
import {
CurrentAccountTransaction,
CurrentAccountTransactionType
} from 'app/current-accounts/models/current-account-transaction.model';
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { UndoTransactionDialogComponent } from '../custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { Currency } from 'app/shared/models/general.model';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatButton, MatIconButton } from '@angular/material/button';
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
import { MatIcon } from '@angular/material/icon';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Transactions Tab Component.
*/
@Component({
selector: 'mifosx-transactions-tab',
templateUrl: './transactions-tab.component.html',
styleUrls: ['./transactions-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatTable,
MatSort,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatSortHeader,
MatCellDef,
MatCell,
NgClass,
ExternalIdentifierComponent,
MatIconButton,
MatMenuTrigger,
MatIcon,
MatMenu,
MatMenuItem,
FaIconComponent,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator,
DateFormatPipe,
CurrencyPipe
]
})
export class TransactionsTabComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
private dateUtils = inject(Dates);
/** Current Account Status */
status: any;
/** Currency */
currency: Currency | null = null;
/** Transactions Data */
transactionsData: CurrentAccountTransaction[] = [];
/** Form control to handle accural parameter */
hideAccrualsParam: UntypedFormControl;
hideReversedParam: UntypedFormControl;
/** Columns to be displayed in transactions table. */
displayedColumns: string[] = [
'row',
'id',
'date',
'externalId',
'transactionType',
'debit',
'credit',
'balance',
'actions'
];
/** Data source for transactions table. */
dataSource: MatTableDataSource<any>;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
accountWithTransactions = false;
accountId: string;
/**
* Retrieves current account data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
*/
constructor() {
this.route.parent.parent.data.subscribe((data: { currentAccountData: any }) => {
this.transactionsData = data.currentAccountData.transactions;
this.status = data.currentAccountData.status.value;
this.currency = data.currentAccountData.currency || null;
});
this.accountId = this.route.parent.parent.snapshot.params['currentAccountId'];
}
ngOnInit() {
this.hideAccrualsParam = new UntypedFormControl(false);
this.hideReversedParam = new UntypedFormControl(false);
this.setTransactions();
}
setTransactions(): void {
this.dataSource = new MatTableDataSource(this.transactionsData);
this.accountWithTransactions = this.transactionsData && this.transactionsData.length > 0;
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
/**
* Checks if transaction is debit.
* @param {any} transactionType Transaction Type
*/
isDebit(transactionType: CurrentAccountTransactionType) {
return (
transactionType.withdrawal === true ||
transactionType.feeDeduction === true ||
transactionType.overdraftInterest === true ||
transactionType.withholdTax === true
);
}
isAccrual(transactionType: CurrentAccountTransactionType): boolean {
return transactionType.accrual || transactionType.code === 'currentAccountTransactionType.accrual';
}
/**
* Checks transaction status.
*/
checkStatus() {
if (
this.status === 'Active' ||
this.status === 'Closed' ||
this.status === 'Transfer in progress' ||
this.status === 'Transfer on hold' ||
this.status === 'Premature Closed' ||
this.status === 'Matured'
) {
return true;
}
return false;
}
/**
* Show Transactions Details
* @param transactionsData Transactions Data
*/
showTransactions(transactionsData: CurrentAccountTransaction) {
if (transactionsData.transfer) {
this.router.navigate([`../transfer-funds/account-transfers/${transactionsData.transfer.id}`], {
relativeTo: this.route
});
} else {
this.router.navigate(
[
transactionsData.id,
'general'
],
{ relativeTo: this.route }
);
}
}
/**
* Stops the propagation to view pages.
* @param $event Mouse Event
*/
routeEdit($event: MouseEvent) {
$event.stopPropagation();
}
hideAccruals() {
this.filterTransactions(this.hideReversedParam.value, this.hideAccrualsParam.value);
}
hideReversed() {
this.filterTransactions(this.hideReversedParam.value, this.hideAccrualsParam.value);
}
filterTransactions(hideReversed: boolean, hideAccrual: boolean): void {
let transactions: CurrentAccountTransaction[] = this.transactionsData;
if (hideAccrual || hideReversed) {
transactions = this.transactionsData.filter((t: CurrentAccountTransaction) => {
return !(hideReversed && t.reversed) && !(hideAccrual && t.transactionType.accrual);
});
}
this.dataSource = new MatTableDataSource(transactions);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
currentAccountTransactionColor(transaction: CurrentAccountTransaction): string {
if (transaction.reversed) {
return 'strike';
} else if (transaction.transfer) {
return 'transfer';
} else if (transaction.transactionType.accrual) {
return 'accrual';
} else {
return '';
}
}
undoTransaction(transactionData: CurrentAccountTransaction): void {
const undoTransactionAccountDialogRef = this.dialog.open(UndoTransactionDialogComponent);
undoTransactionAccountDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const data = {
transactionDate: this.dateUtils.parseDate(transactionData.date),
transactionAmount: 0,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.accountId, 'undo', data, transactionData.id)
.subscribe(() => {
this.reload();
});
}
});
}
private reload() {
const clientId = this.route.parent.parent.snapshot.params['clientId'];
const url: string = this.router.url;
this.router
.navigateByUrl(`/clients/${clientId}/current-accounts`, { skipLocationChange: true })
.then(() => this.router.navigate([url]));
}
}
@@ -0,0 +1,108 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-card>
<form [formGroup]="editTransactionForm" (ngSubmit)="submit()">
<mat-card-content class="layout-column">
<mat-form-field (click)="dueDatePicker.open()">
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="dueDatePicker"
formControlName="transactionDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dueDatePicker></mat-datepicker>
@if (editTransactionForm.controls.transactionDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Transaction Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mifosx-input-amount
[currency]="currency"
[isRequired]="true"
[inputFormControl]="editTransactionForm.controls.transactionAmount"
[inputLabel]="'Transaction Amount'"
>
</mifosx-input-amount>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Payment Type' | translate }}</mat-label>
<mat-select formControlName="paymentTypeId">
@for (paymentType of paymentTypeOptions; track paymentType) {
<mat-option [value]="paymentType.id">
{{ paymentType.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="flex-fill">
<span class="expandcollapsebutton m-l-10 m-t-40 flex-75" (click)="addPaymentDetails()">
<mat-slide-toggle>
<div [className]="showPaymentDetails ? 'enabled' : 'disabled'">
<span class="m-l-10">{{ 'labels.inputs.Show Payment Details' | translate }}</span>
</div>
</mat-slide-toggle>
</span>
</div>
@if (editTransactionForm.controls['accountNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Account Number' | translate }}</mat-label>
<input type="number" formControlName="accountNumber" matInput />
</mat-form-field>
}
@if (editTransactionForm.controls['checkNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Cheque' | translate }}</mat-label>
<input type="number" formControlName="checkNumber" matInput />
</mat-form-field>
}
@if (editTransactionForm.controls['routingCode']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Routing Code' | translate }}</mat-label>
<input formControlName="routingCode" matInput />
</mat-form-field>
}
@if (editTransactionForm.controls['receiptNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Receipt Number' | translate }}</mat-label>
<input formControlName="receiptNumber" matInput />
</mat-form-field>
}
@if (editTransactionForm.controls['bankNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Bank' | translate }}</mat-label>
<input formControlName="bankNumber" matInput />
</mat-form-field>
}
<mat-card-actions class="layout-row layout-xs-column layout-align-center gap-5px">
<button type="button" mat-raised-button [routerLink]="['../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!editTransactionForm.valid">
{{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</mat-card-content>
</form>
</mat-card>
</div>
@@ -0,0 +1,11 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
width: 37rem;
}
@@ -0,0 +1,164 @@
/**
* 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, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
Validators,
UntypedFormControl,
ReactiveFormsModule
} from '@angular/forms';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Currency } from 'app/shared/models/general.model';
import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Edit Transaction component.
*/
@Component({
selector: 'mifosx-edit-transaction',
templateUrl: './edit-transaction.component.html',
styleUrls: ['./edit-transaction.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
InputAmountComponent,
MatSlideToggle
]
})
export class EditTransactionComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private route = inject(ActivatedRoute);
private router = inject(Router);
private dateUtils = inject(Dates);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
/** Minimum Due Date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum Due Date allowed. */
maxDate = new Date();
/** Current account transaction form. */
editTransactionForm: UntypedFormGroup;
/** current account transaction payment options. */
paymentTypeOptions: {
id: number;
name: string;
description: string;
isCashPayment: boolean;
position: number;
}[];
/** Flag to enable payment details fields. */
showPaymentDetails: Boolean = false;
/** current account's Id */
currentAccountId: string;
/** Transaction Template */
transactionTemplateData: any;
currency: Currency;
/**
* Retrieves the Current Account transaction template data from `resolve`.
* @param {FormBuilder} formBuilder Form Builder.
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service.
* @param {ActivatedRoute} route Activated Route.
* @param {Dates} dateUtils Date Utils.
* @param {Router} router Router for navigation.
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountTransactionTemplate: any }) => {
this.transactionTemplateData = data.currentAccountTransactionTemplate;
if (data.currentAccountTransactionTemplate.currency) {
this.currency = data.currentAccountTransactionTemplate.currency;
}
this.paymentTypeOptions = this.transactionTemplateData.paymentTypeOptions;
});
this.currentAccountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the Current Account account transaction form when component loads.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createEditTransactionForm();
this.editTransactionForm.patchValue({
transactionDate: this.transactionTemplateData.date && new Date(this.transactionTemplateData.date),
transactionAmount: this.transactionTemplateData.amount,
paymentTypeId: this.transactionTemplateData.paymentTypeId
});
}
/**
* Method to create the Current Account Transaction Form.
*/
createEditTransactionForm() {
this.editTransactionForm = this.formBuilder.group({
transactionDate: [
'',
Validators.required
],
transactionAmount: [
'',
Validators.required
],
paymentTypeId: ['']
});
}
/**
* Method to add payment detail fields to the UI.
*/
addPaymentDetails() {
this.showPaymentDetails = !this.showPaymentDetails;
if (this.showPaymentDetails) {
this.editTransactionForm.addControl('accountNumber', new UntypedFormControl(''));
this.editTransactionForm.addControl('checkNumber', new UntypedFormControl(''));
this.editTransactionForm.addControl('routingCode', new UntypedFormControl(''));
this.editTransactionForm.addControl('receiptNumber', new UntypedFormControl(''));
this.editTransactionForm.addControl('bankNumber', new UntypedFormControl(''));
} else {
this.editTransactionForm.removeControl('accountNumber');
this.editTransactionForm.removeControl('checkNumber');
this.editTransactionForm.removeControl('routingCode');
this.editTransactionForm.removeControl('receiptNumber');
this.editTransactionForm.removeControl('bankNumber');
}
}
/**
* Method to submit the transaction details.
*/
submit() {
const editTransactionFormData = this.editTransactionForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevTransactionDate: Date = this.editTransactionForm.value.transactionDate;
if (editTransactionFormData.transactionDate instanceof Date) {
editTransactionFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat);
}
const data = {
...editTransactionFormData,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.currentAccountId, 'modify', data, this.transactionTemplateData.id)
.subscribe((res) => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,33 @@
<!--
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/.
-->
<mat-card class="container">
<div class="back-button">
<button type="button" color="primary" mat-raised-button [routerLink]="['../../']">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>Back
</button>
</div>
<div *ngIf="pentahoUrl; else noData" class="pdf-container">
<iframe [src]="pentahoUrl" class="pdf-iframe"></iframe>
</div>
<ng-template #noData>
<div class="error-message">
<h3>Unable to Load Receipt</h3>
<p>The transaction receipt could not be loaded. This may be due to:</p>
<ul>
<li>Missing or incomplete report configuration</li>
<li>Report generation service unavailable</li>
<li>Invalid transaction data</li>
</ul>
<p><strong>Please contact your system administrator for assistance.</strong></p>
<p class="console-hint">Technical details may be available in the browser console (F12).</p>
</div>
</ng-template>
</mat-card>
@@ -0,0 +1,58 @@
/**
* 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/.
*/
.container {
max-width: 50rem;
margin: 0 auto;
.back-button {
margin-bottom: 1rem;
}
.pdf-container {
width: 100%;
height: calc(100vh - 250px);
min-height: 600px;
display: flex;
justify-content: center;
align-items: center;
background-color: #525659;
.pdf-iframe {
width: 100%;
height: 100%;
border: none;
}
}
.error-message {
padding: 2rem;
text-align: center;
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
margin: 1rem 0;
h3 {
color: #856404;
margin-bottom: 1rem;
}
ul {
text-align: left;
display: inline-block;
margin: 1rem 0;
}
.console-hint {
font-size: 0.875rem;
color: #6c757d;
margin-top: 1rem;
}
}
}
@@ -0,0 +1,72 @@
/**
* 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, OnDestroy, inject } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { HttpResponse } from '@angular/common/http';
/**
* View Transaction Reciept Component
*/
@Component({
selector: 'mifosx-view-reciept',
templateUrl: './view-reciept.component.html',
styleUrls: ['./view-reciept.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent
]
})
export class ViewRecieptComponent implements OnInit, OnDestroy {
private sanitizer = inject(DomSanitizer);
private route = inject(ActivatedRoute);
/** trusted resource url for pentaho output */
pentahoUrl: SafeResourceUrl | null = null;
/** Transaction Reciept Data */
transactionRecieptData: HttpResponse<Blob>;
/** Blob URL for cleanup */
private blobUrl: string | null = null;
/**
* Fetches transaction reciept `resolve`
* @param {DomSanitizer} sanitizer DOM Sanitizer
* @param {ActivatedRoute} route Activated Route
*/
constructor() {
this.route.data.subscribe((data: { currentAccountTransactionReciept: HttpResponse<Blob> }) => {
this.transactionRecieptData = data.currentAccountTransactionReciept;
});
}
ngOnInit() {
if (!this.transactionRecieptData || !this.transactionRecieptData.body) {
return;
}
const contentType = this.transactionRecieptData.headers.get('Content-Type');
const file = new Blob([this.transactionRecieptData.body], { type: contentType });
if (file.size === 0) {
return;
}
this.blobUrl = URL.createObjectURL(file);
this.pentahoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.blobUrl);
}
ngOnDestroy() {
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
}
}
}
@@ -0,0 +1,9 @@
<!--
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/.
-->
<p>{{ 'labels.heading.current-account-transaction-datatable-tab works' | translate }}!</p>
@@ -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,22 @@
/**
* 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 } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-transaction-datatable-tab',
templateUrl: './current-accounts-transaction-datatable-tab.component.html',
styleUrls: ['./current-accounts-transaction-datatable-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class CurrentAccountTransactionDatatableTabComponent {
constructor() {}
}
@@ -0,0 +1,128 @@
<!--
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="container transaction-buttons m-b-20 align-end-center gap-2percent" align="end">
@if (
!(
transactionData.interestedPostedAsOn === false &&
(transactionData.transactionType.id === 17 || transactionData.transactionType.id === 3)
)
) {
<span>
<span *mifosxHasPermission="'ADJUSTTRANSACTION_CURRENTACCOUNT'">
@if (
transactionData.transactionType.value === 'Transfer' ||
transactionData.reversed === 'true' ||
transactionData.transactionType.id === 3 ||
transactionData.transactionType.id === 17
) {
<button mat-raised-button color="primary" [routerLink]="'edit'">
<fa-icon icon="edit" class="m-r-10"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
</button>
}
</span>
</span>
}
@if (!transactionData.reversed && transactionData.transactionType.amountHold) {
<span>
<button
mat-raised-button
color="primary"
*mifosxHasPermission="'RELEASEAMOUNT_CURRENTACCOUNT'"
(click)="releaseAmount()"
>
<fa-icon icon="lock-open" class="m-r-10"></fa-icon>{{ 'labels.buttons.Release Amount' | translate }}
</button>
</span>
}
@if (allowUndo()) {
<span>
<button
mat-raised-button
color="warn"
*mifosxHasPermission="'UNDOTRANSACTION_CURRENTACCOUNT'"
(click)="undoTransaction()"
>
<fa-icon icon="undo" class="m-r-10"></fa-icon>{{ 'labels.buttons.Undo' | translate }}
</button>
</span>
}
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div class="layout-row-wrap card-content">
<div class="flex-100" [ngClass]="transactionColor()"></div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Id' | translate }}
</div>
<div class="flex-50">
{{ transactionData.id }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Transaction Type' | translate }}
</div>
<div class="flex-50">
{{ transactionData.transactionType.value | translateKey: 'catalogs' }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Transaction Date' | translate }}
</div>
<div class="flex-50">
{{ transactionData.date | dateFormat }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Currency' | translate }}
</div>
<div class="flex-50">
{{ transactionData.currency.displayLabel }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Amount' | translate }}
</div>
<div class="flex-50">
{{ transactionData.amount | currency: transactionData.currency.code : 'symbol-narrow' : '1.2-2' }}
</div>
@if (transactionData.note) {
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Note' | translate }}
</div>
}
@if (transactionData.note) {
<div class="flex-50">
{{ transactionData.note }}
</div>
}
@if (transactionData.paymentDetailData) {
<mifosx-transaction-payment-detail [paymentDetailData]="transactionData.paymentDetailData">
</mifosx-transaction-payment-detail>
}
</div>
<div class="layout-row layout-align-center gap-2percent column-on-mobile">
<button type="button" mat-raised-button color="primary" [routerLink]="['../../']">
{{ 'labels.buttons.Back' | translate }}
</button>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,45 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
.container {
max-width: 37rem;
.card-content {
div {
margin: 1rem 0;
word-wrap: break-word;
}
mifosx-transaction-payment-detail {
width: 100%;
margin-bottom: 20px;
}
}
}
.transaction-buttons {
margin-top: 3%;
}
.active {
background-color: $status-active;
height: 4px;
}
.linked {
background-color: $status-approved;
height: 4px;
}
.undo {
background-color: $status-active-overdue;
height: 4px;
}
@@ -0,0 +1,106 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
import { ReleaseAmountDialogComponent } from 'app/current-accounts/current-account-view/custom-dialogs/release-amount-dialog/release-amount-dialog.component';
import { UndoTransactionDialogComponent } from 'app/current-accounts/current-account-view/custom-dialogs/undo-transaction-dialog/undo-transaction-dialog.component';
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { NgClass, CurrencyPipe } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TransactionPaymentDetailComponent } from '../../../../../shared/transaction-payment-detail/transaction-payment-detail.component';
import { DateFormatPipe } from '../../../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-transaction-general-tab',
templateUrl: './current-accounts-transaction-general-tab.component.html',
styleUrls: ['./current-accounts-transaction-general-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
NgClass,
TransactionPaymentDetailComponent,
CurrencyPipe,
DateFormatPipe
]
})
export class CurrentAccountTransactionGeneralTabComponent {
private currentAccountsService = inject(CurrentAccountsService);
private route = inject(ActivatedRoute);
private dateUtils = inject(Dates);
private router = inject(Router);
dialog = inject(MatDialog);
private settingsService = inject(SettingsService);
accountId: string;
transactionId: string;
transactionData: any;
constructor() {
this.route.data.subscribe((data: { currentAccountTransaction: any }) => {
this.accountId = this.route.parent.snapshot.params['currentAccountId'];
this.transactionData = data.currentAccountTransaction;
});
}
allowUndo(): boolean {
if (this.transactionData.reversed && this.transactionData.transactionType.amountHold) {
return false;
}
return !this.transactionData.reversed;
}
releaseAmount(): void {
const releaseAmountDialogRef = this.dialog.open(ReleaseAmountDialogComponent);
releaseAmountDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
const data = {};
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.accountId, 'releaseAmount', data, this.transactionData.id)
.subscribe(() => {
this.router.navigate(['../..'], { relativeTo: this.route });
});
}
});
}
undoTransaction(): void {
const undoTransactionAccountDialogRef = this.dialog.open(UndoTransactionDialogComponent);
undoTransactionAccountDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const data = {
transactionDate: this.dateUtils.formatDate(
this.transactionData.date && new Date(this.transactionData.date),
dateFormat
),
transactionAmount: 0,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.accountId, 'undo', data, this.transactionData.id)
.subscribe(() => {
this.router.navigate(['../..'], { relativeTo: this.route });
});
}
});
}
transactionColor(): string {
if (this.transactionData.reversed) {
return 'undo';
}
return 'active';
}
}
@@ -0,0 +1,17 @@
<!--
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="tab-container mat-typography">
<mifosx-entity-datatable-tab
entityType="Current Account Transaction"
[entityId]="entityId"
[multiRowDatatableFlag]="multiRowDatatableFlag"
[entityDatatable]="entityDatatable"
>
</mifosx-entity-datatable-tab>
</div>
@@ -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,39 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EntityDatatableTabComponent } from '../../../../../shared/tabs/entity-datatable-tab/entity-datatable-tab.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-datatable-transaction-tab',
templateUrl: './datatable-transaction-tab.component.html',
styleUrls: ['./datatable-transaction-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
EntityDatatableTabComponent
]
})
export class DatatableTransactionTabComponent {
private route = inject(ActivatedRoute);
/** Transaction Id */
entityId: string;
/** Current Account Datatable */
entityDatatable: any;
/** Multi Row Datatable Flag */
multiRowDatatableFlag: boolean;
constructor() {
this.entityId = this.route.parent.parent.snapshot.paramMap.get('id');
this.route.data.subscribe((data: { transactionDatatable: any }) => {
this.entityDatatable = data.transactionDatatable;
this.multiRowDatatableFlag = this.entityDatatable.columnHeaders[0].columnName === 'id' ? true : false;
});
}
}
@@ -0,0 +1,37 @@
<!--
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="container">
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
[routerLink]="['./general']"
routerLinkActive
#general="routerLinkActive"
[active]="general.isActive"
>
{{ 'labels.heading.General' | translate }}
</a>
@for (entityDatatable of entityDatatables; track entityDatatable) {
<a
mat-tab-link
[routerLink]="['./datatables', entityDatatable.registeredTableName]"
routerLinkActive
#datatable="routerLinkActive"
[active]="datatable.isActive"
*mifosxHasPermission="'READ_' + entityDatatable.registeredTableName"
>
{{ entityDatatable.registeredTableName }}
</a>
}
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
@@ -0,0 +1,18 @@
/**
* 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/.
*/
.container {
max-width: 37rem;
.content {
div {
margin: 1rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,50 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-view-transaction',
templateUrl: './view-transaction.component.html',
styleUrls: ['./view-transaction.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTabNav,
MatTabLink,
RouterLinkActive,
MatTabNavPanel,
RouterOutlet
]
})
export class ViewTransactionComponent {
private route = inject(ActivatedRoute);
dialog = inject(MatDialog);
/** Transaction data. */
transactionData: any;
accountId: any;
/** Transaction Data Tables */
entityDatatables: any;
/**
* @param {Router} router Router for navigation.
* @param {MatDialog} dialog Dialog reference.
*/
constructor() {
this.route.data.subscribe((data: { transactionDatatables: any }) => {
this.accountId = this.route.snapshot.params['currentAccountId'];
this.entityDatatables = data.transactionDatatables;
});
}
}
@@ -0,0 +1,155 @@
<!--
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 (currentAccountData.status.value === 'Active' && chargeData.amountOutstanding !== 0) {
<div class="layout-row align-end gap-2percent layout-xs-column container m-b-20">
<button mat-raised-button color="accent" *mifosxHasPermission="'PAY_CURRENTACCOUNTCHARGE'" (click)="payCharge()">
<fa-icon icon="dollar-sign" class="m-r-10"></fa-icon>
Pay
</button>
<button
mat-raised-button
color="primary"
*mifosxHasPermission="'WAIVE_CURRENTACCOUNTCHARGE'"
(click)="waiveCharge()"
>
<fa-icon icon="flag" class="m-r-10"></fa-icon>
Waive
</button>
@if (isRecurringCharge()) {
<span>
<button
mat-raised-button
color="warn"
*mifosxHasPermission="'INACTIVATE_CURRENTACCOUNTCHARGE'"
(click)="inactivateCharge()"
>
<fa-icon icon="trash" class="m-r-10"></fa-icon>
Inactivate
</button>
</span>
}
</div>
}
@if (currentAccountData.status.value === 'Submitted and pending approval') {
<div class="layout-row align-end gap-2percent layout-xs-column container m-b-20">
<button
mat-raised-button
color="primary"
*mifosxHasPermission="'UPDATE_CURRENTACCOUNTCHARGE'"
(click)="editCharge()"
>
<fa-icon icon="edit" class="m-r-10"></fa-icon>
Edit
</button>
<button
mat-raised-button
color="warn"
*mifosxHasPermission="'DELETE_CURRENTACCOUNTCHARGE'"
(click)="deleteCharge()"
>
<fa-icon icon="trash" class="m-r-10"></fa-icon>
Delete
</button>
</div>
}
<div class="container">
<mat-card>
<mat-card-content>
<div class="layout-row-wrap">
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Name' | translate }}
</div>
<div class="flex-50">
{{ chargeData.name }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Charge Type' | translate }}
</div>
<div class="flex-50">
{{ chargeData.penalty ? 'Penalty' : ('Fee' | translateKey: 'catalogs') }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Currency' | translate }}
</div>
<div class="flex-50">
{{ chargeData.currency.displayLabel }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Payment due at' | translate }}
</div>
<div class="flex-50">
{{ chargeData.chargeTimeType.value | translateKey: 'catalogs' }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Payment due as of' | translate }}
</div>
<div class="flex-50">
{{ chargeData.dueDate | dateFormat }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Calculation Type' | translate }}
</div>
<div class="flex-50">
{{ chargeData.chargeCalculationType.value | translateKey: 'catalogs' }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Due' | translate }}
</div>
<div class="flex-50">
{{ chargeData.amount | formatNumber }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Paid' | translate }}
</div>
<div class="flex-50">
{{ chargeData.amountPaid | formatNumber }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Waived' | translate }}
</div>
<div class="flex-50">
{{ chargeData.amountWaived | formatNumber }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Outstanding' | translate }}
</div>
<div class="flex-50">
{{ chargeData.amountOutstanding | formatNumber }}
</div>
</div>
<div class="layout-row layout-align-center gap-2percent column-on-mobile">
<button type="button" mat-raised-button color="primary" [routerLink]="['../']">
{{ 'labels.buttons.Back' | translate }}
</button>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,18 @@
/**
* 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/.
*/
.container {
max-width: 37rem;
.content {
div {
margin: 1rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,233 @@
/**
* 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, inject } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
/** Custom Dialogs */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { WaiveChargeDialogComponent } from '../custom-dialogs/waive-charge-dialog/waive-charge-dialog.component';
import { InactivateChargeDialogComponent } from '../custom-dialogs/inactivate-charge-dialog/inactivate-charge-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-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 { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* View Charge Component.
*/
@Component({
selector: 'mifosx-view-charge',
templateUrl: './view-charge.component.html',
styleUrls: ['./view-charge.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
DateFormatPipe,
FormatNumberPipe
]
})
export class ViewChargeComponent {
private currentAccountsService = inject(CurrentAccountsService);
private route = inject(ActivatedRoute);
private dateUtils = inject(Dates);
private router = inject(Router);
dialog = inject(MatDialog);
private settingsService = inject(SettingsService);
/** Charge data. */
chargeData: any;
/** Current Account Data */
currentAccountData: any;
/**
* Retrieves the Charge data from `resolve`.
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {ActivatedRoute} route Activated Route.
* @param {Router} router Router for navigation.
* @param {MatDialog} dialog Dialog reference.
* @param {Dates} dateUtils Date Utils.
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountCharge: any }) => {
this.chargeData = data.currentAccountCharge;
});
this.route.data.subscribe((data: { currentAccountData: any }) => {
this.currentAccountData = data.currentAccountData;
});
}
/**
* Pays the charge.
*/
payCharge() {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: 'Amount',
value: '',
type: 'number',
required: true
}),
new DatepickerBase({
controlName: 'dueDate',
label: 'Payment Date',
value: '',
type: 'date',
required: true
})
];
const data = {
title: 'Pay Charge',
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const payChargeDialogRef = this.dialog.open(FormDialogComponent, { data });
payChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const dataObject = {
...response.data.value,
dueDate: this.dateUtils.formatDate(response.data.value.dueDate, dateFormat),
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.chargeData.accountId, 'pay', dataObject, this.chargeData.id)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Waive's the charge
*/
waiveCharge() {
const waiveChargeDialogRef = this.dialog.open(WaiveChargeDialogComponent, { data: { id: this.chargeData.id } });
waiveChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.chargeData.accountId, 'waive', {}, this.chargeData.id)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Inactivate's the charge
*/
inactivateCharge() {
const inactivateChargeDialogRef = this.dialog.open(InactivateChargeDialogComponent, {
data: { id: this.chargeData.id }
});
inactivateChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.confirm) {
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.chargeData.accountId, 'inactivate', {}, this.chargeData.id)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Edits the charge
*/
editCharge() {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: 'Amount',
value: this.chargeData.amount || this.chargeData.amountOrPercentage,
type: 'number',
required: true
})
];
const data = {
title: 'Edit Charge',
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editChargeDialogRef = this.dialog.open(FormDialogComponent, { data });
editChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const dataObject = {
...response.data.value,
dateFormat,
locale
};
this.currentAccountsService
.editCurrentAccountCharge(this.chargeData.accountId, dataObject, this.chargeData.id)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Deletes the charge
*/
deleteCharge() {
const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `charge id:${this.chargeData.id}` }
});
deleteChargeDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.currentAccountsService
.deleteCurrentAccountCharge(this.chargeData.accountId, this.chargeData.id)
.subscribe(() => {
this.reload();
});
}
});
}
/**
* Checks if charge is recurring
*/
isRecurringCharge() {
const chargeTimeType = this.chargeData.chargeTimeType.value;
return chargeTimeType === 'Monthly Fee' || chargeTimeType === 'Annual Fee' || chargeTimeType === 'Weekly Fee';
}
/**
* Refetches data fot the component
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
*/
private reload() {
const url: string = this.router.url.replace(`/${this.chargeData.id}`, '');
const refreshUrl: string = this.router.url.slice(
0,
this.router.url.indexOf('current-accounts') + 'current-accounts'.length
);
this.router.navigateByUrl(refreshUrl, { skipLocationChange: true }).then(() => this.router.navigate([url]));
}
}