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
@@ -220,6 +220,39 @@
</mat-checkbox>
}
<div class="sanctions-verify-section flex-98">
@if (!sanctionsVerified && !sanctionsBlocked) {
<button
mat-raised-button
color="primary"
type="button"
class="verify-client-btn"
[disabled]="!createClientForm.get('firstname')?.value && !createClientForm.get('fullname')?.value"
(click)="verifySanctions()"
>
<fa-icon icon="shield-alt" class="mr-05"></fa-icon>
Verify Client
</button>
}
@if (sanctionsVerified) {
<div class="sanctions-result">
<fa-icon icon="check-circle" class="text-success"></fa-icon>
<span class="text-success">Client verified — No match found in sanctions list.</span>
</div>
}
@if (sanctionsBlocked) {
<div class="sanctions-blocked">
<fa-icon icon="exclamation-triangle" class="text-danger"></fa-icon>
<span class="text-danger">
<strong>SANCTIONS MATCH FOUND</strong> — Client cannot be onboarded. Account opening is blocked.
</span>
<button mat-stroked-button color="warn" type="button" class="recheck-btn" (click)="verifySanctions()">
Re-check
</button>
</div>
}
</div>
<mat-divider class="flex-98"></mat-divider>
<div class="dip-info flex-98">
@@ -356,7 +389,7 @@
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
<button mat-raised-button matStepperNext [disabled]="sanctionsBlocked">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
@@ -172,6 +172,64 @@ mat-checkbox {
}
}
.sanctions-verify-section {
margin: 12px 0;
.verify-client-btn {
height: 40px;
font-size: 13px;
white-space: nowrap;
}
.sanctions-progress {
mat-progress-bar {
margin-bottom: 8px;
}
.sanctions-msg {
font-size: 13px;
color: #555;
line-height: 1.5;
margin: 0;
}
}
.sanctions-result {
display: flex;
align-items: center;
gap: 8px;
.text-success {
color: #388e3c;
font-size: 14px;
font-weight: 600;
}
}
}
.sanctions-blocked {
display: flex;
align-items: center;
gap: 8px;
background: #ffebee;
border: 1px solid #ef9a9a;
border-left: 4px solid #d32f2f;
border-radius: 4px;
padding: 10px 16px;
margin: 12px 0;
.text-danger {
color: #d32f2f;
font-size: 13px;
}
.recheck-btn {
margin-left: auto;
font-size: 12px;
white-space: nowrap;
}
}
.external-id-status {
font-size: 12px;
font-weight: 500;
@@ -31,7 +31,10 @@ import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { MatDialog } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { SanctionsVerifyDialogComponent } from './sanctions-verify-dialog.component';
import { DipVerifyDialogComponent } from './dip-verify-dialog.component';
/**
* Create Client Component
@@ -59,6 +62,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
private settingsService = inject(SettingsService);
private clientService = inject(ClientsService);
externalNationalIdService = inject(ExternalNationalIdService);
private dialog = inject(MatDialog);
/** Subject to trigger unsubscription on destroy */
private destroy$ = new Subject<void>();
@@ -98,6 +102,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
savingProductOptions: any;
/** Mobile number DIP verification status */
mobileVerified = false;
/** Sanctions verification state */
sanctionsVerified = false;
/** Sanctions match was found — blocks account opening */
sanctionsBlocked = false;
/**
* @param {FormBuilder} formBuilder Form Builder
@@ -258,10 +266,57 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
}
/**
* Verify mobile number with DIP.
* Verify mobile number with DIP via dialog.
*/
verifyWithDIP() {
this.mobileVerified = true;
const mobileNo = this.createClientForm.get('mobileNo')?.value || '';
const dialogRef = this.dialog.open(DipVerifyDialogComponent, {
width: '500px',
data: { mobileNo }
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.mobileVerified = true;
}
});
}
/**
* Verify client name against sanctions lists via dialog.
* Passes the client name to the dialog for real API screening.
* If a match is found, sanctionsBlocked is set to true blocking further steps.
*/
verifySanctions() {
const firstname = this.createClientForm.get('firstname')?.value || '';
const middlename = this.createClientForm.get('middlename')?.value || '';
const lastname = this.createClientForm.get('lastname')?.value || '';
const fullname = this.createClientForm.get('fullname')?.value || '';
const name =
fullname ||
[
firstname,
middlename,
lastname
]
.filter(Boolean)
.join(' ');
const dialogRef = this.dialog.open(SanctionsVerifyDialogComponent, {
width: '550px',
data: { name }
});
dialogRef.afterClosed().subscribe((verified: boolean) => {
if (verified === true) {
// CLEAR — no match found
this.sanctionsVerified = true;
this.sanctionsBlocked = false;
} else if (verified === false) {
// FLAGGED — match found, block account opening
this.sanctionsVerified = false;
this.sanctionsBlocked = true;
}
});
}
getDateLabel(legalFormId: number, values: string[]): string {
@@ -0,0 +1,343 @@
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatButtonModule } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ClientsService } from 'app/clients/clients.service';
@Component({
selector: 'mifosx-dip-verify-dialog',
template: `
<h2 mat-dialog-title>
<fa-icon icon="satellite-dish" class="dialog-title-icon"></fa-icon>
DIP Verification
</h2>
<mat-dialog-content>
@if (verifying) {
<mat-progress-bar mode="determinate" [value]="progress" color="primary"></mat-progress-bar>
<p class="progress-percent">{{ progress }}%</p>
<div class="verify-info">
<fa-icon icon="mobile-alt" class="info-icon"></fa-icon>
<p class="verify-msg">
Please wait while the system verifies the entered mobile number against the
<strong>Mobile Number Revocation List (MNRL)</strong> available on the
<strong>Digital Intelligence Platform (DIP)</strong> of the Department of Telecommunications (DoT),
Government of India.
</p>
</div>
<div class="warning-row">
<fa-icon icon="exclamation-triangle" class="warning-icon"></fa-icon>
<span class="warning-text">This may take a few seconds. Do not close or refresh this window.</span>
</div>
}
@if (!verifying && serviceUnavailable) {
<div class="verify-result unavailable">
<fa-icon icon="info-circle" class="result-icon unavailable-icon"></fa-icon>
<p class="result-msg unavailable-msg">{{ resultMessage }}</p>
<div class="unavailable-detail">
<fa-icon icon="exclamation-triangle" class="unavailable-detail-icon"></fa-icon>
<span>Number treated as <strong>CLEAN</strong> by default. Proceed with caution.</span>
</div>
</div>
}
@if (!verifying && !suspected && !serviceUnavailable && !error) {
<div class="verify-result">
<fa-icon icon="check-circle" class="result-icon"></fa-icon>
<p class="result-msg">Mobile number verified — Not found in MNRL revocation list.</p>
</div>
}
@if (!verifying && suspected) {
<div class="verify-result suspected">
<fa-icon icon="exclamation-circle" class="result-icon suspected-icon"></fa-icon>
<p class="result-msg suspected-msg">{{ resultMessage }}</p>
@if (suspectedReason) {
<div class="suspected-detail">
<p><strong>Status:</strong> {{ suspectedStatus }}</p>
<p><strong>Reason:</strong> {{ suspectedReason }}</p>
<p><strong>Reported by:</strong> {{ suspectedReportedBy }}</p>
</div>
}
</div>
}
@if (!verifying && error) {
<div class="verify-result error">
<fa-icon icon="times-circle" class="result-icon error-icon"></fa-icon>
<p class="result-msg error-msg">{{ error }}</p>
</div>
}
</mat-dialog-content>
<mat-dialog-actions align="end">
@if (!verifying) {
<button mat-raised-button color="primary" (click)="close()">OK</button>
}
</mat-dialog-actions>
`,
styles: [
`
:host {
display: block;
min-width: 450px;
}
h2[mat-dialog-title] {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: 16px 24px;
font-size: 18px;
font-weight: 600;
color: #1074b9;
}
.dialog-title-icon {
font-size: 20px;
}
mat-dialog-content {
padding: 16px 24px;
}
mat-progress-bar {
margin-bottom: 4px;
}
.progress-percent {
text-align: center;
font-size: 22px;
font-weight: 700;
color: #1074b9;
margin: 4px 0 12px;
}
.verify-info {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: #e3f2fd;
border-radius: 8px;
margin-bottom: 12px;
margin-top: 12px;
}
.info-icon {
font-size: 24px;
color: #1074b9;
margin-top: 2px;
flex-shrink: 0;
}
.verify-msg {
font-size: 14px;
color: #333;
line-height: 1.6;
margin: 0;
}
.warning-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #fff3e0;
border-radius: 6px;
}
.warning-icon {
font-size: 16px;
color: #e65100;
flex-shrink: 0;
}
.warning-text {
font-size: 13px;
color: #e65100;
font-weight: 500;
}
.verify-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px 0;
}
.result-icon {
font-size: 48px;
color: #388e3c;
}
.result-msg {
font-size: 16px;
font-weight: 600;
color: #388e3c;
margin: 0;
text-align: center;
}
.suspected-icon {
color: #e65100;
}
.suspected-msg {
color: #e65100;
}
.suspected-detail {
background: #fff3e0;
border-radius: 8px;
padding: 12px 16px;
width: 100%;
text-align: left;
}
.suspected-detail p {
margin: 4px 0;
font-size: 14px;
font-weight: 400;
color: #333;
}
.unavailable-icon {
color: #1565c0;
}
.unavailable-msg {
color: #1565c0;
}
.unavailable-detail {
display: flex;
align-items: center;
gap: 8px;
background: #e3f2fd;
border-radius: 8px;
padding: 10px 16px;
width: 100%;
font-size: 13px;
color: #333;
}
.unavailable-detail-icon {
font-size: 16px;
color: #1565c0;
flex-shrink: 0;
}
.error-icon {
color: #d32f2f;
}
.error-msg {
color: #d32f2f;
}
mat-dialog-actions {
padding: 8px 24px 16px;
}
`
],
imports: [
CommonModule,
MatDialogModule,
MatProgressBarModule,
MatButtonModule,
FaIconComponent
]
})
export class DipVerifyDialogComponent implements OnInit, OnDestroy {
private dialogRef = inject(MatDialogRef<DipVerifyDialogComponent>);
private data: { mobileNo: string } = inject(MAT_DIALOG_DATA);
private clientsService = inject(ClientsService);
verifying = true;
progress = 0;
suspected = false;
serviceUnavailable = false;
error: string | null = null;
resultMessage = '';
suspectedStatus = '';
suspectedReason = '';
suspectedReportedBy = '';
private subscription: any;
private intervalId: any;
private apiResponse: any = null;
private apiError: any = null;
private apiDone = false;
ngOnInit() {
this.dialogRef.disableClose = true;
// Animate progress bar over ~3 seconds (0 → 100)
this.intervalId = setInterval(() => {
this.progress += 2;
if (this.progress >= 100) {
clearInterval(this.intervalId);
this.intervalId = null;
this.tryShowResult();
}
}, 60);
// Fire the API call in parallel
this.subscription = this.clientsService.checkDipLookup(this.data.mobileNo).subscribe({
next: (res: any) => {
this.apiResponse = res;
this.apiDone = true;
this.tryShowResult();
},
error: (err: any) => {
this.apiError = err;
this.apiDone = true;
this.tryShowResult();
}
});
}
/** Show result only after both progress bar completes and API responds */
private tryShowResult() {
if (!this.apiDone || this.progress < 100) {
return;
}
this.verifying = false;
this.dialogRef.disableClose = false;
if (this.apiError) {
this.error =
this.apiError?.error?.message || this.apiError?.message || 'DIP verification failed. Please try again.';
return;
}
const res = this.apiResponse;
if (res?.data?.status === 'SERVICE_UNAVAILABLE') {
this.serviceUnavailable = true;
this.resultMessage = res.message || 'DIP/MNRL lookup service is unavailable. Number treated as CLEAN by default.';
} else if (res?.data?.suspected) {
this.suspected = true;
this.resultMessage = res.message || `Mobile number is ${res.data.status} — found in DIP records`;
const detail = res.data.suspectedDetail;
if (detail) {
this.suspectedStatus = detail.dipStatus || res.data.status;
this.suspectedReason = detail.reason || '';
this.suspectedReportedBy = detail.reportedBy || '';
}
}
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
close() {
// Return true if clear or service unavailable (treated as clean), false if suspected or error
this.dialogRef.close(!this.suspected && !this.error);
}
}
@@ -0,0 +1,271 @@
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatDialogRef, MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatButtonModule } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ClientsService } from 'app/clients/clients.service';
@Component({
selector: 'mifosx-sanctions-verify-dialog',
template: `
<h2 mat-dialog-title>
<fa-icon icon="shield-alt" class="dialog-title-icon"></fa-icon>
Sanctions Verification
</h2>
<mat-dialog-content>
@if (verifying) {
<mat-progress-bar mode="indeterminate" color="primary"></mat-progress-bar>
<p class="sanctions-msg">
Checking <strong>{{ clientName }}</strong> against sanctions list<br />
<strong>UN Security Council</strong> &amp;
<strong>Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231)</strong>
</p>
}
@if (!verifying && !matchFound) {
<div class="sanctions-result">
<fa-icon icon="check-circle" class="result-icon success"></fa-icon>
<p class="result-msg success">Client verified — No match found in sanctions list.</p>
</div>
}
@if (!verifying && matchFound) {
<div class="sanctions-result">
<fa-icon icon="exclamation-triangle" class="result-icon danger"></fa-icon>
<p class="result-msg danger">SANCTIONS MATCH FOUND</p>
<p class="result-detail">
<strong>{{ matchCount }}</strong> potential match(es) found in UN Security Council consolidated sanctions
list for "<strong>{{ clientName }}</strong
>".
</p>
<div class="match-list">
@for (match of matches; track match.id) {
<div class="match-item">
<div class="match-name">{{ match.fullName }}</div>
<div class="match-meta">
<span class="match-type">{{ match.entryType }}</span>
<span class="match-ref">{{ match.referenceNumber }}</span>
@if (match.unListType) {
<span class="match-list-type">{{ match.unListType }}</span>
}
</div>
@if (match.aliases && match.aliases.length > 0) {
<div class="match-aliases"><em>Also known as:</em> {{ match.aliases.join(', ') }}</div>
}
</div>
}
</div>
<p class="block-msg">This client cannot be onboarded. Account opening is blocked due to sanctions match.</p>
</div>
}
@if (!verifying && errorOccurred) {
<div class="sanctions-result">
<fa-icon icon="exclamation-circle" class="result-icon warning"></fa-icon>
<p class="result-msg warning">Verification failed. Please try again.</p>
<p class="result-detail">{{ errorMessage }}</p>
</div>
}
</mat-dialog-content>
<mat-dialog-actions align="end">
@if (!verifying && !matchFound && !errorOccurred) {
<button mat-raised-button color="primary" (click)="close(true)">OK</button>
}
@if (!verifying && matchFound) {
<button mat-raised-button color="warn" (click)="close(false)">Close</button>
}
@if (!verifying && errorOccurred) {
<button mat-raised-button (click)="close(false)">Close</button>
}
</mat-dialog-actions>
`,
styles: [
`
:host {
display: block;
min-width: 500px;
}
h2[mat-dialog-title] {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: 16px 24px;
font-size: 18px;
font-weight: 600;
color: #1074b9;
}
.dialog-title-icon {
font-size: 20px;
}
mat-dialog-content {
padding: 16px 24px;
max-height: 60vh;
}
.sanctions-msg {
font-size: 14px;
color: #555;
line-height: 1.6;
margin: 16px 0 0;
text-align: center;
}
.sanctions-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 0;
}
.result-icon {
font-size: 48px;
}
.result-icon.success {
color: #388e3c;
}
.result-icon.danger {
color: #d32f2f;
}
.result-icon.warning {
color: #f57c00;
}
.result-msg {
font-size: 16px;
font-weight: 600;
margin: 0;
text-align: center;
}
.result-msg.success {
color: #388e3c;
}
.result-msg.danger {
color: #d32f2f;
}
.result-msg.warning {
color: #f57c00;
}
.result-detail {
font-size: 14px;
color: #555;
text-align: center;
margin: 4px 0;
}
.match-list {
width: 100%;
max-height: 200px;
overflow-y: auto;
margin: 8px 0;
}
.match-item {
background: #fce4ec;
border-left: 4px solid #d32f2f;
padding: 8px 12px;
margin-bottom: 6px;
border-radius: 4px;
}
.match-name {
font-weight: 600;
font-size: 14px;
color: #333;
}
.match-meta {
display: flex;
gap: 8px;
margin-top: 4px;
flex-wrap: wrap;
}
.match-type,
.match-ref,
.match-list-type {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
background: #ffcdd2;
color: #b71c1c;
}
.match-aliases {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.block-msg {
font-size: 14px;
font-weight: 600;
color: #d32f2f;
background: #ffebee;
padding: 10px 16px;
border-radius: 4px;
text-align: center;
margin-top: 8px;
width: 100%;
}
mat-dialog-actions {
padding: 8px 24px 16px;
}
`
],
imports: [
CommonModule,
MatDialogModule,
MatProgressBarModule,
MatButtonModule,
FaIconComponent
]
})
export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
private dialogRef = inject(MatDialogRef<SanctionsVerifyDialogComponent>);
private data: { name: string } = inject(MAT_DIALOG_DATA);
private clientsService = inject(ClientsService);
clientName = '';
verifying = true;
matchFound = false;
matchCount = 0;
matches: any[] = [];
errorOccurred = false;
errorMessage = '';
ngOnInit() {
this.clientName = this.data?.name || '';
this.dialogRef.disableClose = true;
this.clientsService.screenSanctions(this.clientName).subscribe({
next: (result: any) => {
this.verifying = false;
this.dialogRef.disableClose = false;
if (result.matchFound) {
this.matchFound = true;
this.matchCount = result.matchCount || 0;
this.matches = result.matches || [];
}
},
error: (err: any) => {
this.verifying = false;
this.dialogRef.disableClose = false;
this.errorOccurred = true;
this.errorMessage = err?.error?.message || err?.message || 'Unable to connect to sanctions screening service.';
}
});
}
ngOnDestroy() {}
close(verified: boolean) {
this.dialogRef.close(verified);
}
}
@@ -288,6 +288,11 @@ const routes: Routes = [
path: 'savings-accounts',
loadChildren: () => import('../savings/savings.module').then((m) => m.SavingsModule)
},
{
path: 'current-accounts',
loadChildren: () =>
import('../current-accounts/current-accounts.module').then((m) => m.CurrentAccountsModule)
},
{
path: 'recurring-deposits-accounts',
loadChildren: () =>
@@ -274,6 +274,13 @@
>
{{ 'labels.buttons.New Fixed Deposits Account' | translate }}
</button>
<button
mat-menu-item
*mifosxHasPermission="'CREATE_CURRENTACCOUNT'"
[routerLink]="['current-accounts', 'create']"
>
{{ 'labels.buttons.New Current Account' | translate }}
</button>
</mat-menu>
<mat-menu #Actions="matMenu">
+19
View File
@@ -490,6 +490,15 @@ export class ClientsService {
return this.http.post(`/clients/${clientId}/kyc`, kycData);
}
/**
* Screen a client name against the UN Security Council consolidated sanctions list.
* @param name The name to screen
* @returns Observable with screening result (status: FLAGGED or CLEAR)
*/
screenSanctions(name: string): Observable<any> {
return this.http.post('/sanctions/screen', { name });
}
/**
* Updates existing KYC details for a client.
* @param clientId Client ID
@@ -550,6 +559,16 @@ export class ClientsService {
return this.http.post(`/clients/${clientId}/ckyc/documents`, formData);
}
/**
* Check mobile number against the DIP (Digital Intelligence Platform) MNRL.
* Calls an external service directly, bypassing Fineract interceptors.
* @param mobileNo Mobile number to verify
*/
checkDipLookup(mobileNo: string): Observable<any> {
const httpParams = new HttpParams().set('mobileNumber', mobileNo);
return this.http.get('/lookup/check', { params: httpParams });
}
getClientForm60(clientId: string): Observable<any> {
return this.http.get(`/clients/${clientId}/form60`);
}
@@ -116,6 +116,10 @@ export class CreateClientComponent {
areFormvalids(): boolean {
let areValids = this.clientGeneralForm.valid;
// Block form if sanctions match found
if (this.clientGeneralStep.sanctionsBlocked) {
return false;
}
if (this.clientTemplate.isAddressEnabled) {
areValids = areValids && this.clientAddressStep.address.address.length > 0;
}
+51
View File
@@ -27,6 +27,57 @@
<mat-sidenav-content class="sidenav">
<!-- Toolbar -->
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
<!-- Sub Menu Bar -->
<div class="sub-menu-bar">
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="money-bill" class="sub-menu-icon" size="lg"></fa-icon>Cashier
</a>
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="user-tie" class="sub-menu-icon" size="lg"></fa-icon>Officer
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="depositsMenu">
<fa-icon icon="piggy-bank" class="sub-menu-icon" size="lg"></fa-icon>Deposits
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="exchange" class="sub-menu-icon" size="lg"></fa-icon>Clearing
</a>
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="money-check" class="sub-menu-icon" size="lg"></fa-icon>HRM
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="shareMenu">
<fa-icon icon="coins" class="sub-menu-icon" size="lg"></fa-icon>Share
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="othersMenu">
<fa-icon icon="folder-open" class="sub-menu-icon" size="lg"></fa-icon>Others
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="exclamation-triangle" class="sub-menu-icon" size="lg"></fa-icon>NPA Module
</a>
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="fingerprint" class="sub-menu-icon" size="lg"></fa-icon>CKYC Management
</a>
</div>
<!-- Sub Menu Dropdowns -->
<mat-menu #depositsMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item tabindex="0">Saving</button>
<button mat-menu-item tabindex="0">Current</button>
<button mat-menu-item tabindex="0">Fixed Deposit</button>
<button mat-menu-item tabindex="0">Recurring</button>
<button mat-menu-item tabindex="0">DDS</button>
</mat-menu>
<mat-menu #shareMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item tabindex="0">Nominal</button>
</mat-menu>
<mat-menu #othersMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item tabindex="0">Locker</button>
<button mat-menu-item tabindex="0">Investment</button>
</mat-menu>
<!-- Progress Bar -->
@if (progressBarMode !== 'none') {
<div>
+45
View File
@@ -36,6 +36,51 @@ $color: #e2e4ec;
overflow-x: hidden;
}
.sub-menu-bar {
display: flex;
flex-wrap: nowrap;
align-items: center;
background-color: #1074b9;
padding: 2px 0.5rem 2px calc(48px + 2rem);
width: 100%;
z-index: 1;
position: relative;
font-family: Roboto, 'Helvetica Neue', sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sub-menu-link {
display: inline-flex;
align-items: center;
padding: 6px 0.5rem;
text-decoration: none;
font-weight: 500;
font-size: 14px;
color: rgb(255 255 255 / 70%);
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
&:hover {
color: #fff;
}
}
.sub-menu-icon {
margin-right: 0.35rem;
}
.sub-menu-caret {
margin-left: 0.35rem;
font-size: 12px;
}
@keyframes move {
0% {
left: 0;
+5 -1
View File
@@ -23,6 +23,8 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
import { BreadcrumbComponent } from './breadcrumb/breadcrumb.component';
import { ContentComponent } from './content/content.component';
import { FooterComponent } from '../../shared/footer/footer.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatMenuModule } from '@angular/material/menu';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -43,7 +45,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
BreadcrumbComponent,
ContentComponent,
FooterComponent,
AsyncPipe
AsyncPipe,
FaIconComponent,
MatMenuModule
]
})
export class ShellComponent implements OnInit, OnDestroy {
@@ -46,6 +46,7 @@
>
<fa-icon class="mr-05" icon="university" size="lg"></fa-icon>
{{ 'labels.menus.Institution' | translate }}
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
<span class="hide-lt-md">
@@ -79,6 +80,7 @@
>
<fa-icon class="mr-05" icon="chart-bar" size="lg"></fa-icon>
{{ 'labels.menus.Reports' | translate }}
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
<a
#admin
@@ -90,35 +92,22 @@
>
<fa-icon class="mr-05" icon="shield-alt" size="lg"></fa-icon>
{{ 'labels.menus.Admin' | translate }}
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
<a #configWizard class="tab-link" (click)="openDialog()" tabindex="0" *mifosxHasPermission="'READ_CONFIG_WIZARD'">
<fa-icon class="mr-05" icon="info" size="lg"></fa-icon>
{{ 'labels.menus.Configuration Wizard' | translate }}
</a>
<a class="tab-link" [matMenuTriggerFor]="ledgerMenu" tabindex="0">
<fa-icon class="mr-05" icon="book" size="lg"></fa-icon>
Ledger
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
</span>
</div>
<span class="toolbar-spacer"></span>
<div #globalSearch class="white-text-search">
<mifosx-search-tool tabindex="0"></mifosx-search-tool>
</div>
<div #languageSelector class="white-text-language">
<mifosx-language-selector class="ml-1 language hide-lt-md" tabindex="0"></mifosx-language-selector>
</div>
<button
mat-icon-button
[matMenuTriggerFor]="languageMenu"
class="show-lt-md"
tabindex="0"
[attr.aria-label]="'labels.menus.Language Selector' | translate"
matTooltip="{{ 'labels.menus.Language Selector' | translate }}"
>
<mat-icon>language</mat-icon>
</button>
<div #notifications>
<mifosx-notifications-tray #notificationsTray class="hide-lt-md" tabindex="0"></mifosx-notifications-tray>
</div>
@@ -134,11 +123,6 @@
</div>
</mat-toolbar>
<!-- Language selector menu for small screen view -->
<mat-menu #languageMenu="matMenu">
<mifosx-language-selector class="ml-1 language"></mifosx-language-selector>
</mat-menu>
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item routerLink="/clients" tabindex="0" *mifosxHasPermission="'READ_CLIENT'">
{{ 'labels.menus.Clients' | translate }}
@@ -160,6 +144,12 @@
</span>
</mat-menu>
<!-- Ledger Menu -->
<mat-menu #ledgerMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item tabindex="0">Sub Ledger</button>
<button mat-menu-item tabindex="0">General Ledger</button>
</mat-menu>
<!-- Toolbar Menus -->
<mat-menu #reportsMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item [routerLink]="['/reports']" tabindex="0" *mifosxHasPermission="'READ_REPORT'">
@@ -317,54 +307,6 @@
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateSelfService, selfService)">
{{ 'labels.buttons.Back' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateGlobalSearch, globalSearch)"
>
{{ 'labels.buttons.Next' | translate }}
</button>
</div>
</ng-template>
<ng-template #templateGlobalSearch let-popover="popover">
<h2>{{ 'labels.menus.Global Search' | translate }}</h2>
<p class="mw300">{{ 'labels.text.Navigate system selecting entity' | translate }}</p>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateConfigWizard, configWizard)"
>
{{ 'labels.buttons.Back' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateLanguageSelector, languageSelector)"
>
{{ 'labels.buttons.Next' | translate }}
</button>
</div>
</ng-template>
<ng-template #templateLanguageSelector let-popover="popover">
<h2>{{ 'labels.menus.Language Selector' | translate }}</h2>
<p class="mw300">{{ 'labels.text.Organization available languages' | translate }}</p>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateGlobalSearch, globalSearch)"
>
{{ 'labels.buttons.Back' | translate }}
</button>
<button
mat-raised-button
color="primary"
@@ -385,7 +327,7 @@
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateLanguageSelector, languageSelector)"
(click)="popover.close(); showPopover(templateConfigWizard, configWizard)"
>
{{ 'labels.buttons.Back' | translate }}
</button>
@@ -8,10 +8,7 @@
#mifosx-toolbar {
position: relative;
box-shadow:
0 3px 1px -2px rgb(0 0 0 / 20%),
0 2px 2px 0 rgb(0 0 0 / 14%),
0 1px 5px 0 rgb(0 0 0 / 12%);
box-shadow: none;
.tab-nav-bar {
display: flex;
@@ -21,7 +18,7 @@
.tab-link {
min-width: auto;
padding: 15px 1rem;
padding: 15px 0.5rem;
text-decoration: none;
font-weight: 500;
font-size: 14px;
@@ -34,21 +31,6 @@
cursor: pointer;
}
.search-bar {
.search {
max-width: 400px;
width: 100%;
font-size: 1rem;
}
}
.language {
min-width: 80px;
width: 80px;
font-size: 0.875rem;
margin-top: 0.375rem;
}
.img-button {
img {
width: 32px;
@@ -57,6 +39,11 @@
}
}
.ml-05 {
margin-left: 0.35rem;
font-size: 12px;
}
.ml-1 {
margin-left: 1rem;
}
@@ -66,40 +53,6 @@
}
}
// Show language icon button only on small screens (when full selector is hidden)
.show-lt-md {
display: none;
}
@media (width <= 768px) {
.show-lt-md {
display: inline-flex;
}
}
.toolbar-spacer {
flex: 1 1 auto;
}
.white-text-language ::ng-deep mifosx-language-selector {
color: white !important;
/* stylelint-disable-next-line selector-max-universal */
* {
background-color: transparent !important;
color: white !important;
}
}
.white-text-search ::ng-deep mifosx-search-tool {
color: white !important;
/* stylelint-disable-next-line selector-max-universal */
* {
background-color: transparent !important;
color: white !important;
margin-bottom: 0;
}
}
.white-text-language {
margin-top: 27px;
}
+2 -1
View File
@@ -18,7 +18,8 @@ export class Charges {
{ id: 1, code: 'chargeAppliesTo.loan', value: 'Loan' },
{ id: 2, code: 'chargeAppliesTo.savings', value: 'Savings' },
{ id: 3, code: 'chargeAppliesTo.client', value: 'Client' },
{ id: 4, code: 'chargeAppliesTo.shares', value: 'Shares' }
{ id: 4, code: 'chargeAppliesTo.shares', value: 'Shares' },
{ id: 5, code: 'chargeAppliesTo.currentAccount', value: 'Current Account' }
];
}
}
@@ -0,0 +1,78 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable, forkJoin, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
import { ClientsService } from 'app/clients/clients.service';
import { GroupsService } from 'app/groups/groups.service';
import { OrganizationService } from 'app/organization/organization.service';
/**
* Current Account Actions data resolver.
*/
@Injectable()
export class CurrentAccountActionsResolver {
private currentAccountsService = inject(CurrentAccountsService);
private clientsService = inject(ClientsService);
private groupsService = inject(GroupsService);
private organizationService = inject(OrganizationService);
/**
* Returns the Current account actions data.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const actionName = route.paramMap.get('name');
const currentAccountId =
route.paramMap.get('currentAccountId') || route.parent.parent.paramMap.get('currentAccountId');
switch (actionName) {
case 'Assign Staff':
return this.currentAccountsService.getCurrentAccountData(currentAccountId).pipe(
switchMap((account: any) => {
if (!account.clientId && !account.groupId) {
return of({ ...account, fieldOfficerOptions: [] });
}
const entityObs = account.clientId
? this.clientsService.getClientData(account.clientId)
: this.groupsService.getGroupData(account.groupId);
return entityObs.pipe(
switchMap((entity: any) => this.organizationService.getStaff(entity.officeId, true)),
map((staff: any) => ({
...account,
fieldOfficerOptions: staff
}))
);
})
);
case 'Add Charge':
return this.currentAccountsService.getCurrentAccountChargeTemplateResource(currentAccountId);
case 'Withdrawal':
case 'Deposit':
case 'Hold Amount':
return this.currentAccountsService.getCurrentAccountTransactionTemplateResource(currentAccountId);
case 'Close':
return forkJoin([
this.currentAccountsService.getCurrentAccountTransactionTemplateResource(currentAccountId),
this.currentAccountsService.getCurrentAccountData(currentAccountId)
]);
case 'Apply Annual Fees':
return this.currentAccountsService.getCurrentAccountData(currentAccountId);
default:
return undefined;
}
}
}
@@ -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/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account data and template resolver.
*/
@Injectable()
export class CurrentAccountAndTemplateResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account data and template.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.paramMap.get('currentAccountId');
return this.currentAccountsService.getCurrentAccountAndTemplate(currentAccountId, true);
}
}
@@ -0,0 +1,36 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account Charge data resolver.
*/
@Injectable()
export class CurrentAccountChargeResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account Charge data.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.parent.paramMap.get('currentAccountId');
const chargeId = route.paramMap.get('id');
return this.currentAccountsService.getCurrentAccountCharge(currentAccountId, chargeId);
}
}
@@ -0,0 +1,36 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account Template resolver.
*/
@Injectable()
export class CurrentAccountTemplateResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Shares Account Template.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const entityId = route.paramMap.get('clientId') || route.paramMap.get('groupId') || route.paramMap.get('centerId');
const isGroup = route.paramMap.get('groupId') || route.paramMap.get('centerId') ? true : false;
return this.currentAccountsService.getCurrentAccountTemplate(entityId, undefined, isGroup);
}
}
@@ -0,0 +1,36 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account Transaction Template data resolver.
*/
@Injectable()
export class CurrentAccountTransactionTemplateResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account Transaction Template data.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.parent.paramMap.get('currentAccountId');
const transactionId = route.paramMap.get('id');
return this.currentAccountsService.getCurrentAccountTransactionTemplate(currentAccountId, transactionId);
}
}
@@ -0,0 +1,36 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account Transaction data resolver.
*/
@Injectable()
export class CurrentAccountTransactionResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account Transaction data.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.parent.paramMap.get('currentAccountId');
const transactionId = route.parent.paramMap.get('id');
return this.currentAccountsService.getCurrentAccountTransaction(currentAccountId, transactionId);
}
}
@@ -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/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account data resolver.
*/
@Injectable()
export class CurrentAccountViewResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account data.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.paramMap.get('currentAccountId');
return this.currentAccountsService.getCurrentAccountData(currentAccountId);
}
}
@@ -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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Account Datatable data resolver.
*/
@Injectable()
export class CurrentAccountDatatableResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account's Datatable data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const accountId =
route.parent.parent.paramMap.get('currentAccountId') ||
route.parent.parent.paramMap.get('fixedDepositAccountId') ||
route.parent.parent.paramMap.get('recurringDepositAccountId');
const datatableName = route.paramMap.get('datatableName');
return this.currentAccountsService.getCurrentAccountDatatable(accountId, datatableName);
}
}
@@ -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/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
/**
* Current Accounts Datatables data resolver.
*/
@Injectable()
export class CurrentAccountDatatablesResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account's Datatables data.
* @returns {Observable<any>}
*/
resolve(): Observable<any> {
return this.currentAccountsService.getCurrentAccountDatatables();
}
}
@@ -0,0 +1,28 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { CurrentAccountsService } from '../current-accounts.service';
@Injectable({
providedIn: 'root'
})
export class CurrentAccountDocumentsResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.parent.paramMap.get('currentAccountId');
return this.currentAccountsService.getCurrentAccountDocuments(currentAccountId);
}
}
@@ -0,0 +1,28 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { CurrentAccountsService } from '../current-accounts.service';
@Injectable({
providedIn: 'root'
})
export class CurrentAccountNotesResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Current Account data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const currentAccountId = route.parent.paramMap.get('currentAccountId');
return this.currentAccountsService.getCurrentAccountNotes(currentAccountId);
}
}
@@ -0,0 +1,47 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { ReportsService } from 'app/reports/reports.service';
import { SettingsService } from 'app/settings/settings.service';
/**
* Current Account Transaction Reciept resolver.
*/
@Injectable()
export class CurrentAccountTransactionRecieptResolver {
private reportsService = inject(ReportsService);
private settingsService = inject(SettingsService);
/**
* Returns the Current Account Transaction Reciept
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const transactionId = route.paramMap.get('id');
const data = {
'output-type': 'PDF',
R_transactionId: transactionId
};
return this.reportsService.getPentahoRunReportData(
'Current Account Transaction Receipt',
data,
'default',
this.settingsService.language.code,
this.settingsService.dateFormat
);
}
}
@@ -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/.
*/
// Angular Imports
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
// rxjs Imports
import { Observable } from 'rxjs';
// Custom Service
import { CurrentAccountsService } from '../current-accounts.service';
@Injectable()
export class TransactionDatatableResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
* Returns the Transactions Account's Datatable data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const accountId = route.parent.parent.paramMap.get('id');
const datatableName = route.paramMap.get('datatableName');
return this.currentAccountsService.getCurrentAccountTransactionDatatable(accountId, datatableName);
}
}
@@ -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/.
*/
// Angular Imports
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
// rxjs Imports
import { Observable } from 'rxjs';
// Custom Service
import { CurrentAccountsService } from '../current-accounts.service';
@Injectable()
export class TransactionDatatablesResolver {
private currentAccountsService = inject(CurrentAccountsService);
/**
*
* @param route
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.currentAccountsService.getCurrentAccountTransactionDatatables();
}
}
@@ -0,0 +1,80 @@
<!--
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-stepper
orientation="vertical"
[linear]="false"
class="mat-elevation-z8"
labelPosition="bottom"
#currentAccountProductStepper
>
<ng-template matStepperIcon="number">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="done">
<fa-icon icon="check" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="error">
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
</ng-template>
<ng-template matStepperIcon="preview">
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="currentAccountDetailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-current-account-details-step
[currentAccountTemplate]="currentAccountTemplate"
(currentAccountProductTemplate)="setTemplate($event)"
>
</mifosx-current-account-details-step>
</mat-step>
<mat-step [stepControl]="currentAccountTermsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-current-account-terms-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
></mifosx-current-account-terms-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-current-account-charges-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
[currentAccountTemplate]="currentAccountTemplate"
[currencyCode]="currentAccountTermsForm.get('currencyCode')"
>
</mifosx-current-account-charges-step>
</mat-step>
@if (currentAccountFormValid) {
<mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-current-account-preview-step
[currentAccountProductTemplate]="currentAccountProductTemplate"
[currentAccountTemplate]="currentAccountTemplate"
[currentAccountTermsForm]="currentAccountTermsForm"
[currentAccount]="currentAccount"
(submitEvent)="submit()"
>
</mifosx-current-account-preview-step>
</mat-step>
}
</mat-stepper>
</div>
@@ -0,0 +1,77 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// Container styling with proper spacing and margins
.container {
margin: 1.5rem auto;
max-width: 75rem;
width: 100%;
padding: 0 1.5rem;
box-sizing: border-box;
}
// Material Stepper styling with proper spacing
:host ::ng-deep mat-stepper {
background: transparent;
.mat-step-header {
padding: 1rem 1.5rem;
margin-bottom: 0.5rem;
}
.mat-step-content {
padding: 1.5rem;
margin: 1rem 0;
}
}
// Form field spacing within steps
:host ::ng-deep mat-step-content {
mat-form-field {
margin-bottom: 1rem;
width: 100%;
}
.layout-row-wrap {
gap: 1rem;
margin-bottom: 1rem;
}
}
// Responsive design
@media (width <= 768px) {
.container {
padding: 0 1rem;
margin: 1rem auto;
}
:host ::ng-deep mat-stepper {
.mat-step-content {
padding: 1rem;
margin: 0.75rem 0;
}
.mat-step-header {
padding: 0.75rem 1rem;
}
}
}
@media (width <= 480px) {
.container {
padding: 0 0.75rem;
margin: 0.75rem auto;
}
:host ::ng-deep mat-stepper {
.mat-step-content {
padding: 0.75rem;
margin: 0.5rem 0;
}
}
}
@@ -0,0 +1,162 @@
/**
* 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, ViewChild, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
/** Custom Components */
import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component';
import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component';
import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component';
/** Custom Services */
import { CurrentAccountsService } from '../current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Create Current Account Component
*/
@Component({
selector: 'mifosx-create-current-account-account',
templateUrl: './create-current-account.component.html',
styleUrls: ['./create-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
FaIconComponent,
MatStep,
MatStepLabel,
CurrentAccountDetailsStepComponent,
CurrentAccountTermsStepComponent,
CurrentAccountChargesStepComponent,
CurrentAccountPreviewStepComponent
]
})
export class CreateCurrentAccountComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private dateUtils = inject(Dates);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
/** Current Account Template */
currentAccountTemplate: any;
/** Current Account Product Template */
currentAccountProductTemplate: any;
/** Current Account Details Step */
@ViewChild(CurrentAccountDetailsStepComponent, { static: true })
currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
/** Current Account Terms Step */
@ViewChild(CurrentAccountTermsStepComponent, { static: true })
currentAccountTermsStep: CurrentAccountTermsStepComponent;
/** Current Account Charges Step */
@ViewChild(CurrentAccountChargesStepComponent, { static: true })
currentAccountChargesStep: CurrentAccountChargesStepComponent;
/**
* Fetches current account template from `resolve`
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {Dates} dateUtils Date Utils
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {SettingsService} settingsService Settings Service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountTemplate: any }) => {
this.currentAccountTemplate = data.currentAccountTemplate;
});
}
/**
* Sets current account product template.
* @param {any} $event API response
*/
setTemplate($event: any) {
this.currentAccountProductTemplate = $event;
}
/**
* Retrieves current account details form.
*/
get currentAccountDetailsForm() {
return this.currentAccountDetailsStep.currentAccountDetailsForm;
}
/**
* Retrieves current account terms form.
*/
get currentAccountTermsForm() {
return this.currentAccountTermsStep.currentAccountTermsForm;
}
/**
* Checks validity of overall current account form.
*/
get currentAccountFormValid() {
return this.currentAccountDetailsForm.valid && this.currentAccountTermsForm.valid;
}
/**
* Retrieves current account object.
*/
get currentAccount() {
return {
...this.currentAccountDetailsStep.currentAccountDetails,
...this.currentAccountTermsStep.currentAccountTerms,
...this.currentAccountChargesStep.currentAccountCharges
};
}
/**
* Creates a new share account.
*/
submit() {
// TODO: Update once language and date settings are setup
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM';
const currentAccount = {
...this.currentAccount,
charges: this.currentAccount.charges.map((charge: any) => ({
chargeId: charge.id,
amount: charge.amount,
dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate,
feeOnMonthDay: charge.feeOnMonthDay
? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat)
: charge.feeOnMonthDay,
feeInterval: charge.feeInterval
})),
submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat),
dateFormat,
monthDayFormat,
locale
};
if (this.currentAccountTemplate.clientId) {
currentAccount.clientId = this.currentAccountTemplate.clientId;
} else {
currentAccount.groupId = this.currentAccountTemplate.groupId;
}
this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => {
this.router.navigate(
[
'../',
response.resourceId
],
{ relativeTo: this.route }
);
});
}
}
@@ -0,0 +1,46 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container mat-elevation-z8">
<mat-card>
<form [formGroup]="activateCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field class="flex-fill" (click)="activatedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Activated On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="activatedOnDatePicker"
required
formControlName="activatedOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
@if (activateCurrentAccountForm.controls.activatedOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Activated On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!activateCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -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/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Activate Current Account Component
*/
@Component({
selector: 'mifosx-activate-current-account',
templateUrl: './activate-current-account.component.html',
styleUrls: ['./activate-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class ActivateCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Activate Current Account form. */
activateCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the activate current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createActivateCurrentAccountForm();
}
/**
* Creates the activate current account form.
*/
createActivateCurrentAccountForm() {
this.activateCurrentAccountForm = this.formBuilder.group({
activatedOnDate: [
'',
Validators.required
]
});
}
/**
* Submits the form and activates the current account,
* if successful redirects to the current account.
*/
submit() {
const activateCurrentAccountFormData = this.activateCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevActivatedOnDate: Date = this.activateCurrentAccountForm.value.activatedOnDate;
if (activateCurrentAccountFormData.activatedOnDate instanceof Date) {
activateCurrentAccountFormData.activatedOnDate = this.dateUtils.formatDate(prevActivatedOnDate, dateFormat);
}
const data = {
...activateCurrentAccountFormData,
dateFormat,
locale
};
this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'activate', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,126 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="currentAccountChargeForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field>
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select required formControlName="chargeId">
@for (currentAccountCharge of currentAccountChargeOptions; track currentAccountCharge) {
<mat-option [value]="currentAccountCharge.id">
{{ currentAccountCharge.name + ' (' + currentAccountCharge.currency.name + ')' }}
</mat-option>
}
</mat-select>
@if (currentAccountChargeForm.controls.chargeId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
@if (chargeDetails) {
<div class="layout-column">
<mat-form-field>
<mat-label>{{ 'labels.inputs.Amount' | translate }}</mat-label>
<input type="number" required matInput formControlName="amount" />
@if (currentAccountChargeForm.controls.amount.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Charge Calculation' | translate }}</mat-label>
<mat-select formControlName="chargeCalculationType">
@for (chargeCalculation of chargeDetails.chargeCalculationTypeOptions; track chargeCalculation) {
<mat-option [value]="chargeCalculation.id">
{{ chargeCalculation.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Charge time type' | translate }}</mat-label>
<mat-select formControlName="chargeTimeType">
@for (chargeTime of chargeDetails.chargeTimeTypeOptions; track chargeTime) {
<mat-option [value]="chargeTime.id">
{{ chargeTime.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (currentAccountChargeForm.contains('dueDate')) {
<mat-form-field (click)="dueDatePicker.open()">
<mat-label>{{ 'labels.inputs.Due for collection on' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="dueDatePicker"
required
formControlName="dueDate"
/>
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dueDatePicker></mat-datepicker>
@if (currentAccountChargeForm.controls.dueDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due for collection on' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
@if (currentAccountChargeForm.contains('feeOnMonthDay')) {
<mat-form-field (click)="feeOnMonthDayPicker.open()">
<mat-label>{{ 'labels.inputs.Due On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="feeOnMonthDayPicker"
required
formControlName="feeOnMonthDay"
/>
<mat-datepicker-toggle matSuffix [for]="feeOnMonthDayPicker"></mat-datepicker-toggle>
<mat-datepicker #feeOnMonthDayPicker></mat-datepicker>
@if (currentAccountChargeForm.controls.feeOnMonthDay.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
@if (currentAccountChargeForm.contains('feeInterval')) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Repeats Every' | translate }}</mat-label>
<input matInput formControlName="feeInterval" />
</mat-form-field>
}
</div>
}
</div>
<mat-card-actions class="layout-row layout-xs-column layout-align-center gap-5px">
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!currentAccountChargeForm.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,174 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** 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';
/** Custom Services */
import { CurrentAccountsService } from '../../current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Add Current Account Charge component.
*/
@Component({
selector: 'mifosx-add-charge-current-account',
templateUrl: './add-charge-current-account.component.html',
styleUrls: ['./add-charge-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class AddChargeCurrentAccountComponent 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();
/** Add Current Account Charge form. */
currentAccountChargeForm: UntypedFormGroup;
/** current account charge options. */
currentAccountChargeOptions: any;
/** current account Id of the current account. */
currentAccountId: string;
/** charge details */
chargeDetails: any;
/**
* Retrieves charge template data from `resolve`
* @param {FormBuilder} formBuilder Form Builder
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {Dates} dateUtils Date Utils
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {SettingsService} settingsService Settings Service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountActionData: any }) => {
this.currentAccountChargeOptions = data.currentAccountActionData.chargeOptions;
});
this.currentAccountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the Current Account Charge form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createCurrentAccountChargeForm();
this.buildDependencies();
}
buildDependencies() {
this.currentAccountChargeForm.controls.chargeId.valueChanges.subscribe((chargeId) => {
this.currentAccountsService.getChargeTemplate(chargeId).subscribe((data: any) => {
this.chargeDetails = data;
const chargeTimeType = data.chargeTimeType.id;
if (
data.chargeTimeType.value === 'Withdrawal Fee' ||
data.chargeTimeType.value === 'Current Account No Activity Fee'
) {
this.chargeDetails.dueDateNotRequired = true;
}
if (data.chargeTimeType.value === 'Annual Fee' || data.chargeTimeType.value === 'Monthly Fee') {
this.chargeDetails.chargeTimeTypeAnnualOrMonth = true;
}
if (!this.chargeDetails.dueDateNotRequired && !this.chargeDetails.chargeTimeTypeAnnualOrMonth) {
this.currentAccountChargeForm.addControl('dueDate', new UntypedFormControl('', Validators.required));
} else {
this.currentAccountChargeForm.removeControl('dueDate');
}
if (!this.chargeDetails.dueDateNotRequired && this.chargeDetails.chargeTimeTypeAnnualOrMonth) {
this.currentAccountChargeForm.addControl('feeOnMonthDay', new UntypedFormControl('', Validators.required));
} else {
this.currentAccountChargeForm.removeControl('feeOnMonthDay');
}
if (chargeTimeType.value === 'Monthly Fee') {
this.currentAccountChargeForm.addControl(
'feeInterval',
new UntypedFormControl(data.feeInterval, Validators.required)
);
} else {
this.currentAccountChargeForm.removeControl('feeInterval');
}
this.currentAccountChargeForm.patchValue({
amount: data.amount,
chargeCalculationType: data.chargeCalculationType.id,
chargeTimeType: data.chargeTimeType.id
});
});
});
}
/**
* Creates the Current Account Charge form.
*/
createCurrentAccountChargeForm() {
this.currentAccountChargeForm = this.formBuilder.group({
chargeId: [
'',
Validators.required
],
amount: [
'',
Validators.required
],
chargeCalculationType: [{ value: '', disabled: true }],
chargeTimeType: [{ value: '', disabled: true }]
});
}
/**
* Submits current account charge.
*/
submit() {
const currentAccountCharge = this.currentAccountChargeForm.value;
currentAccountCharge.locale = this.settingsService.language.code;
if (!currentAccountCharge.feeInterval) {
currentAccountCharge.feeInterval = this.chargeDetails.feeInterval;
}
if (this.chargeDetails.dueDateNotRequired !== true) {
if (this.chargeDetails.chargeTimeTypeAnnualOrMonth) {
const monthDayFormat = 'MMMM-dd'; // TODO: Update once language and date settings are setup
currentAccountCharge.monthDayFormat = monthDayFormat;
if (currentAccountCharge.feeOnMonthDay) {
const prevDate = this.currentAccountChargeForm.value.feeOnMonthDay;
currentAccountCharge.feeOnMonthDay = this.dateUtils.formatDate(prevDate, monthDayFormat);
}
} else {
const dateFormat = this.settingsService.dateFormat;
currentAccountCharge.dateFormat = dateFormat;
if (currentAccountCharge.dueDate) {
const prevDate = this.currentAccountChargeForm.value.dueDate;
currentAccountCharge.dueDate = this.dateUtils.formatDate(prevDate, dateFormat);
}
}
}
this.currentAccountsService
.createCurrentAccountCharge(this.currentAccountId, 'charges', currentAccountCharge)
.subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,57 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="applyAnnualFeesForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field (click)="dueDatePicker.open()">
<mat-label>{{ 'labels.inputs.Annual Fees Transaction Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="dueDatePicker"
required
formControlName="dueDate"
/>
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dueDatePicker></mat-datepicker>
@if (applyAnnualFeesForm.controls.dueDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Amount' | translate }}</mat-label>
<input type="number" formControlName="amount" required matInput />
@if (applyAnnualFeesForm.controls.amount.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!applyAnnualFeesForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,125 @@
/**
* 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, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Apply Annual Fees Component
*/
@Component({
selector: 'mifosx-apply-annual-fees-current-account',
templateUrl: './apply-annual-fees-current-account.component.html',
styleUrls: ['./apply-annual-fees-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class ApplyAnnualFeesCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Apply annual fees form. */
applyAnnualFeesForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/** Annual Fees charge Id */
chargeId: any;
/** Current Account Data */
currentAccountData: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
this.route.data.subscribe((data: { currentAccountActionData: any }) => {
this.currentAccountData = data.currentAccountActionData;
});
}
/**
* Creates the apply annual fees form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createApplyAnnualFeesForm();
this.applyCharge();
}
/**
* Creates the apply annual fees form.
*/
createApplyAnnualFeesForm() {
this.applyAnnualFeesForm = this.formBuilder.group({
dueDate: [
'',
Validators.required
],
amount: ['']
});
}
/**
* Retireves apply annual fees charge for ID and amount.
*/
applyCharge() {
const charges: any[] = this.currentAccountData.charges;
charges.forEach((charge: any) => {
if (charge.name === 'Annual fee - INR') {
this.chargeId = charge.id;
this.applyAnnualFeesForm.get('amount').patchValue(charge.amount);
}
});
}
/**
* Submits the form and applies the annual fees,
* if successful redirects to the current account.
*/
submit() {
const applyAnnualFeesFormData = this.applyAnnualFeesForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevApprovedOnDate: Date = this.applyAnnualFeesForm.value.dueDate;
if (applyAnnualFeesFormData.dueDate instanceof Date) {
applyAnnualFeesFormData.dueDate = this.dateUtils.formatDate(prevApprovedOnDate, dateFormat);
}
const data = {
...applyAnnualFeesFormData,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountChargesCommand(this.accountId, 'paycharge', data, this.chargeId)
.subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,51 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="approveCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field (click)="approvedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Approved On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="approvedOnDatePicker"
required
formControlName="approvedOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="approvedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #approvedOnDatePicker></mat-datepicker>
@if (approveCurrentAccountForm.controls.approvedOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Approved On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!approveCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,104 @@
/**
* 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, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Approve Current Account Component
*/
@Component({
selector: 'mifosx-approve-current-account',
templateUrl: './approve-current-account.component.html',
styleUrls: ['./approve-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
CdkTextareaAutosize
]
})
export class ApproveCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Approve Current Account form. */
approveCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the approve current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createApproveCurrentAccountForm();
}
/**
* Creates the approve current account form.
*/
createApproveCurrentAccountForm() {
this.approveCurrentAccountForm = this.formBuilder.group({
approvedOnDate: [
'',
Validators.required
],
note: ['']
});
}
/**
* Submits the form and approves the current account,
* if successful redirects to the current account.
*/
submit() {
const approveCurrentAccountFormData = this.approveCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevApprovedOnDate: Date = this.approveCurrentAccountForm.value.approvedOnDate;
if (approveCurrentAccountFormData.approvedOnDate instanceof Date) {
approveCurrentAccountFormData.approvedOnDate = this.dateUtils.formatDate(prevApprovedOnDate, dateFormat);
}
const data = {
...approveCurrentAccountFormData,
dateFormat,
locale
};
this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'approve', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,121 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container mat-elevation-z8">
<mat-card>
<form [formGroup]="closeCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field (click)="closedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Closed On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="closedOnDatePicker"
required
formControlName="closedOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="closedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #closedOnDatePicker></mat-datepicker>
@if (closeCurrentAccountForm.controls.closedOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Closed On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-checkbox class="m-b-25 m-t-15" formControlName="withdrawBalance">
{{ 'labels.inputs.Withdraw Balance' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="postInterestValidationOnClosure">
{{ 'labels.inputs.Is Interest Posting required on closure date' | translate }}?
</mat-checkbox>
@if (closeCurrentAccountForm.controls['withdrawBalance'].value) {
<div class="layout-column">
@if (closeCurrentAccountForm.controls['amount']) {
<mat-form-field class="m-t-20">
<mat-label>{{ 'labels.inputs.Transaction Amount' | translate }}</mat-label>
<input type="number" required matInput formControlName="amount" />
</mat-form-field>
}
@if (closeCurrentAccountForm.controls['paymentTypeId']) {
<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 (closeCurrentAccountForm.controls['accountNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Account Number' | translate }}</mat-label>
<input type="number" formControlName="accountNumber" matInput />
</mat-form-field>
}
@if (closeCurrentAccountForm.controls['checkNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Cheque' | translate }}</mat-label>
<input type="number" formControlName="checkNumber" matInput />
</mat-form-field>
}
@if (closeCurrentAccountForm.controls['routingCode']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Routing Code' | translate }}</mat-label>
<input formControlName="routingCode" matInput />
</mat-form-field>
}
@if (closeCurrentAccountForm.controls['receiptNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Receipt Number' | translate }}</mat-label>
<input formControlName="receiptNumber" matInput />
</mat-form-field>
}
@if (closeCurrentAccountForm.controls['bankNumber']) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Bank' | translate }}</mat-label>
<input formControlName="bankNumber" matInput />
</mat-form-field>
}
</div>
}
<mat-form-field>
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="warn" [disabled]="!closeCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,165 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
Validators,
UntypedFormControl,
ReactiveFormsModule
} from '@angular/forms';
import { ActivatedRoute, Router, 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 { MatCheckbox } from '@angular/material/checkbox';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Close Current Account Component
*/
@Component({
selector: 'mifosx-close-current-account',
templateUrl: './close-current-account.component.html',
styleUrls: ['./close-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatSlideToggle,
CdkTextareaAutosize
]
})
export class CloseCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Close Current Account form. */
closeCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/** Flag to enable payment details fields. */
addPaymentDetailsFlag = false;
/** Payment Type Options */
paymentTypeOptions: any;
/** Transaction Amount */
transactionAmount: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountActionData: any }) => {
this.paymentTypeOptions = data.currentAccountActionData[0].paymentTypeOptions;
this.transactionAmount = data.currentAccountActionData[1].summary.accountBalance;
});
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the close current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createCloseCurrentAccountForm();
this.buildDependencies();
}
/**
* Creates the close current account form.
*/
createCloseCurrentAccountForm() {
this.closeCurrentAccountForm = this.formBuilder.group({
closedOnDate: [
'',
Validators.required
],
withdrawBalance: [false],
postInterestValidationOnClosure: [false],
note: ['']
});
}
/**
* Subscribe to value changes of withdraw balance checkbox.
*/
buildDependencies() {
this.closeCurrentAccountForm.get('withdrawBalance').valueChanges.subscribe((value: boolean) => {
if (value) {
this.closeCurrentAccountForm.addControl(
'amount',
new UntypedFormControl({ value: this.transactionAmount, disabled: true })
);
this.closeCurrentAccountForm.addControl('paymentTypeId', new UntypedFormControl(''));
} else {
this.closeCurrentAccountForm.removeControl('amount');
this.closeCurrentAccountForm.removeControl('paymentTypeId');
}
});
}
/**
* Method to add payment detail fields to the UI.
*/
addPaymentDetails() {
this.addPaymentDetailsFlag = !this.addPaymentDetailsFlag;
if (this.addPaymentDetailsFlag) {
this.closeCurrentAccountForm.addControl('accountNumber', new UntypedFormControl(''));
this.closeCurrentAccountForm.addControl('checkNumber', new UntypedFormControl(''));
this.closeCurrentAccountForm.addControl('routingCode', new UntypedFormControl(''));
this.closeCurrentAccountForm.addControl('receiptNumber', new UntypedFormControl(''));
this.closeCurrentAccountForm.addControl('bankNumber', new UntypedFormControl(''));
} else {
this.closeCurrentAccountForm.removeControl('accountNumber');
this.closeCurrentAccountForm.removeControl('checkNumber');
this.closeCurrentAccountForm.removeControl('routingCode');
this.closeCurrentAccountForm.removeControl('receiptNumber');
this.closeCurrentAccountForm.removeControl('bankNumber');
}
}
/**
* Submits the form and closes the current account,
* if successful redirects to the current account.
*/
submit() {
const closeCurrentAccountFormData = this.closeCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevClosedOnDate: Date = this.closeCurrentAccountForm.value.closedOnDate;
if (closeCurrentAccountFormData.closedOnDate instanceof Date) {
closeCurrentAccountFormData.closedOnDate = this.dateUtils.formatDate(prevClosedOnDate, dateFormat);
}
const data = {
...closeCurrentAccountFormData,
dateFormat,
locale
};
this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'close', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,47 @@
<!--
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 (actions['Approve']) {
<mifosx-approve-current-account></mifosx-approve-current-account>
}
@if (actions['Reject']) {
<mifosx-reject-current-account></mifosx-reject-current-account>
}
@if (actions['Activate']) {
<mifosx-activate-current-account></mifosx-activate-current-account>
}
@if (actions['Undo Approval']) {
<mifosx-undo-approval-current-account></mifosx-undo-approval-current-account>
}
@if (actions['Post Interest As On']) {
<mifosx-post-interest-as-on-current-account></mifosx-post-interest-as-on-current-account>
}
@if (actions['Assign Staff']) {
<mifosx-current-account-assign-staff></mifosx-current-account-assign-staff>
}
@if (actions['Unassign Staff']) {
<mifosx-current-account-unassign-staff></mifosx-current-account-unassign-staff>
}
@if (actions['Withdrawn by Client']) {
<mifosx-withdraw-by-client-current-account></mifosx-withdraw-by-client-current-account>
}
@if (actions['Add Charge']) {
<mifosx-add-charge-current-account></mifosx-add-charge-current-account>
}
@if (actions['Withdrawal'] || actions['Deposit']) {
<mifosx-current-account-transactions></mifosx-current-account-transactions>
}
@if (actions['Close']) {
<mifosx-close-current-account></mifosx-close-current-account>
}
@if (actions['Apply Annual Fees']) {
<mifosx-apply-annual-fees-current-account></mifosx-apply-annual-fees-current-account>
}
@if (actions['Hold Amount'] || actions['Block Account'] || actions['Block Deposit'] || actions['Block Withdrawal']) {
<mifosx-manage-current-account [currency]="currency"> </mifosx-manage-current-account>
}
@@ -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,116 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Currency } from 'app/shared/models/general.model';
import { ApproveCurrentAccountComponent } from './approve-current-account/approve-current-account.component';
import { RejectCurrentAccountComponent } from './reject-current-account/reject-current-account.component';
import { ActivateCurrentAccountComponent } from './activate-current-account/activate-current-account.component';
import { UndoApprovalCurrentAccountComponent } from './undo-approval-current-account/undo-approval-current-account.component';
import { PostInterestAsOnCurrentAccountComponent } from './post-interest-as-on-current-account/post-interest-as-on-current-account.component';
import { CurrentAccountAssignStaffComponent } from './current-account-assign-staff/current-account-assign-staff.component';
import { CurrentAccountUnassignStaffComponent } from './current-account-unassign-staff/current-account-unassign-staff.component';
import { WithdrawByClientCurrentAccountComponent } from './withdraw-by-client-current-account/withdraw-by-client-current-account.component';
import { AddChargeCurrentAccountComponent } from './add-charge-current-account/add-charge-current-account.component';
import { CurrentAccountTransactionsComponent } from './current-account-transactions/current-account-transactions.component';
import { CloseCurrentAccountComponent } from './close-current-account/close-current-account.component';
import { ApplyAnnualFeesCurrentAccountComponent } from './apply-annual-fees-current-account/apply-annual-fees-current-account.component';
import { ManageCurrentAccountComponent } from './manage-current-account/manage-current-account.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current account actions component.
*/
@Component({
selector: 'mifosx-current-account-account-actions',
templateUrl: './current-account-actions.component.html',
styleUrls: ['./current-account-actions.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
ApproveCurrentAccountComponent,
RejectCurrentAccountComponent,
ActivateCurrentAccountComponent,
UndoApprovalCurrentAccountComponent,
PostInterestAsOnCurrentAccountComponent,
CurrentAccountAssignStaffComponent,
CurrentAccountUnassignStaffComponent,
WithdrawByClientCurrentAccountComponent,
AddChargeCurrentAccountComponent,
CurrentAccountTransactionsComponent,
CloseCurrentAccountComponent,
ApplyAnnualFeesCurrentAccountComponent,
ManageCurrentAccountComponent
]
})
export class CurrentAccountActionsComponent {
private route = inject(ActivatedRoute);
/** Flag object to store possible actions and render appropriate UI to the user */
actions: {
Approve: boolean;
Reject: boolean;
Withdrawal: boolean;
Deposit: boolean;
Activate: boolean;
Close: boolean;
'Undo Approval': boolean;
'Post Interest As On': boolean;
'Assign Staff': boolean;
'Add Charge': boolean;
'Unassign Staff': boolean;
'Withdrawn by Client': boolean;
'Apply Annual Fees': boolean;
'Hold Amount': boolean;
'Block Account': boolean;
'Unblock Account': boolean;
'Block Deposit': boolean;
'Unblock Deposit': boolean;
'Block Withdrawal': boolean;
'Unblock Withdrawal': boolean;
} = {
Approve: false,
Reject: false,
Withdrawal: false,
Deposit: false,
Activate: false,
Close: false,
'Undo Approval': false,
'Post Interest As On': false,
'Assign Staff': false,
'Add Charge': false,
'Unassign Staff': false,
'Withdrawn by Client': false,
'Apply Annual Fees': false,
'Hold Amount': false,
'Block Account': false,
'Unblock Account': false,
'Block Deposit': false,
'Unblock Deposit': false,
'Block Withdrawal': false,
'Unblock Withdrawal': false
};
currency: Currency;
/**
* @param {ActivatedRoute} route Activated Route
*/
constructor() {
this.route.data.subscribe((data: { currentAccountActionData: any }) => {
if (data.currentAccountActionData) {
this.currency = data.currentAccountActionData.currency;
}
});
const name = this.route.snapshot.params['name'] as string;
if (name in this.actions) {
this.actions[name as keyof typeof this.actions] = true;
}
}
}
@@ -0,0 +1,63 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="currentAccountAssignStaffForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.To Current Account Officer' | translate }}</mat-label>
<mat-select formControlName="toCurrentAccountOfficerId">
@for (officer of fieldOfficerData; track officer) {
<mat-option [value]="officer.id">
{{ officer.displayName }}
</mat-option>
}
</mat-select>
@if (currentAccountAssignStaffForm.controls.toCurrentAccountOfficerId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.To Current Account Officer' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field (click)="assignmentDatePicker.open()">
<mat-label>{{ 'labels.inputs.Assignment Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="assignmentDatePicker"
required
formControlName="assignmentDate"
/>
<mat-datepicker-toggle matSuffix [for]="assignmentDatePicker"></mat-datepicker-toggle>
<mat-datepicker #assignmentDatePicker></mat-datepicker>
@if (currentAccountAssignStaffForm.controls.assignmentDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Assignment Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!currentAccountAssignStaffForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,116 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Assign Staff Component
*/
@Component({
selector: 'mifosx-current-account-account-assign-staff',
templateUrl: './current-account-assign-staff.component.html',
styleUrls: ['./current-account-assign-staff.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class CurrentAccountAssignStaffComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Current Account Assign Staff form. */
currentAccountAssignStaffForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/** Field Officer Data */
fieldOfficerData: any;
/** Current Account Data */
currentAccountData: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
this.route.data.subscribe((data: { currentAccountActionData: any }) => {
this.currentAccountData = data.currentAccountActionData;
});
}
/**
* Creates the current account assign staff form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.fieldOfficerData = this.currentAccountData.fieldOfficerOptions;
this.createCurrentAccountAssignStaffForm();
}
/**
* Creates the current account assign staff form.
*/
createCurrentAccountAssignStaffForm() {
this.currentAccountAssignStaffForm = this.formBuilder.group({
toCurrentAccountOfficerId: [
'',
Validators.required
],
assignmentDate: [
'',
Validators.required
]
});
}
/**
* Submits the form and assigns staff the current account,
* if successful redirects to the current account.
*/
submit() {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const formValue = this.currentAccountAssignStaffForm.value;
const assignmentDate =
formValue.assignmentDate instanceof Date
? this.dateUtils.formatDate(formValue.assignmentDate, dateFormat)
: formValue.assignmentDate;
const data = {
toCurrentAccountOfficerId: formValue.toCurrentAccountOfficerId,
assignmentDate,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountCommand(this.accountId, 'assignCurrentAccountOfficer', data)
.subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,132 @@
<!--
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-elevation-z8">
<mat-card>
@if (transactionType.withdrawal) {
<mat-card-title class="form-heading">
{{ 'labels.heading.Withdraw Money From Current Account' | translate }}
</mat-card-title>
}
@if (transactionType.deposit) {
<mat-card-title class="form-heading">
{{ 'labels.heading.Deposit Money To Current Account' | translate }}
</mat-card-title>
}
<form [formGroup]="currentAccountTransactionForm" (ngSubmit)="submit()">
<mat-card-content>
<div 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 (currentAccountTransactionForm.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]="currentAccountTransactionForm.controls.transactionAmount"
[inputLabel]="'Transaction Amount'"
>
</mifosx-input-amount>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Payment Type' | translate }}</mat-label>
<mat-select required formControlName="paymentTypeId">
@for (paymentType of paymentTypeOptions; track paymentType) {
<mat-option [value]="paymentType.id">
{{ paymentType.name }}
</mat-option>
}
</mat-select>
@if (currentAccountTransactionForm.controls.paymentTypeId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Payment Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</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]="addPaymentDetailsFlag ? 'enabled' : 'disabled'">
<span class="m-l-10">{{ 'labels.inputs.Show Payment Details' | translate }}</span>
</div>
</mat-slide-toggle>
</span>
</div>
@if (addPaymentDetailsFlag) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Account Number' | translate }}</mat-label>
<input type="number" formControlName="accountNumber" matInput />
</mat-form-field>
}
@if (addPaymentDetailsFlag) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Cheque' | translate }}</mat-label>
<input type="number" formControlName="checkNumber" matInput />
</mat-form-field>
}
@if (addPaymentDetailsFlag) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Routing Code' | translate }}</mat-label>
<input formControlName="routingCode" matInput />
</mat-form-field>
}
@if (addPaymentDetailsFlag) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Receipt Number' | translate }}</mat-label>
<input formControlName="receiptNumber" matInput />
</mat-form-field>
}
@if (addPaymentDetailsFlag) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Bank' | translate }}</mat-label>
<input formControlName="bankNumber" matInput />
</mat-form-field>
}
<mat-form-field>
<mat-label>{{ 'labels.inputs.Notes' | translate }}</mat-label>
<textarea formControlName="note" matInput cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
<mat-card-actions class="layout-row layout-xs-column layout-align-center gap-5px">
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!currentAccountTransactionForm.valid">
{{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</div>
</mat-card-content>
</form>
</mat-card>
</div>
@@ -0,0 +1,29 @@
/**
* 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/.
*/
.expandcollapsebutton {
margin-top: -7px;
}
.container {
max-width: 37rem;
}
.right-input {
text-align: right;
}
.right-label {
padding-right: 25px !important;
}
.form-heading {
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
@@ -0,0 +1,169 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** 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';
/** Custom Services */
import { CurrentAccountsService } from '../../current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
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 { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Create current account transactions component.
*/
@Component({
selector: 'mifosx-current-account-transactions',
templateUrl: './current-account-transactions.component.html',
styleUrls: ['./current-account-transactions.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
InputAmountComponent,
MatSlideToggle,
CdkTextareaAutosize
]
})
export class CurrentAccountTransactionsComponent 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. */
currentAccountTransactionForm: UntypedFormGroup;
/** current account transaction payment options. */
paymentTypeOptions: {
id: number;
name: string;
description: string;
isCashPayment: boolean;
position: number;
}[];
/** Flag to enable payment details fields. */
addPaymentDetailsFlag: Boolean = false;
/** transaction type flag to render required UI */
transactionType: { deposit: boolean; withdrawal: boolean } = { deposit: false, withdrawal: false };
/** transaction command for submit request */
transactionCommand: string;
/** current account's Id */
currentAccountId: string;
currency: Currency | null = null;
/**
* 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 Settings Service
*/
constructor() {
this.route.data.subscribe((data: { currentAccountActionData: any }) => {
this.paymentTypeOptions = data.currentAccountActionData.paymentTypeOptions;
if (data.currentAccountActionData.currency) {
this.currency = data.currentAccountActionData.currency;
}
});
this.transactionCommand = this.route.snapshot.params['name'].toLowerCase();
this.transactionType[this.transactionCommand as 'deposit' | 'withdrawal'] = true;
this.currentAccountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the Current Account account transaction form when component loads.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createCurrentAccountTransactionForm();
}
/**
* Method to create the Current Account Transaction Form.
*/
createCurrentAccountTransactionForm() {
this.currentAccountTransactionForm = this.formBuilder.group({
transactionDate: [
this.settingsService.businessDate,
Validators.required
],
transactionAmount: [
0,
Validators.required
],
paymentTypeId: [
'',
Validators.required
],
note: ['']
});
}
/**
* Method to add payment detail fields to the UI.
*/
addPaymentDetails() {
this.addPaymentDetailsFlag = !this.addPaymentDetailsFlag;
if (this.addPaymentDetailsFlag) {
this.currentAccountTransactionForm.addControl('accountNumber', new UntypedFormControl(''));
this.currentAccountTransactionForm.addControl('checkNumber', new UntypedFormControl(''));
this.currentAccountTransactionForm.addControl('routingCode', new UntypedFormControl(''));
this.currentAccountTransactionForm.addControl('receiptNumber', new UntypedFormControl(''));
this.currentAccountTransactionForm.addControl('bankNumber', new UntypedFormControl(''));
} else {
this.currentAccountTransactionForm.removeControl('accountNumber');
this.currentAccountTransactionForm.removeControl('checkNumber');
this.currentAccountTransactionForm.removeControl('routingCode');
this.currentAccountTransactionForm.removeControl('receiptNumber');
this.currentAccountTransactionForm.removeControl('bankNumber');
}
}
/**
* Method to submit the transaction details.
*/
submit() {
const currentAccountTransactionFormData = this.currentAccountTransactionForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevTransactionDate: Date = this.currentAccountTransactionForm.value.transactionDate;
if (currentAccountTransactionFormData.transactionDate instanceof Date) {
currentAccountTransactionFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat);
}
const data = {
...currentAccountTransactionFormData,
dateFormat,
locale
};
data['transactionAmount'] = data['transactionAmount'] * 1;
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.currentAccountId, this.transactionCommand, data)
.subscribe((res) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,44 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="currentAccountUnassignStaffForm" (ngSubmit)="submit()">
<mat-card-content>
<mat-form-field class="flex-fill" (click)="unassignedDatePicker.open()">
<mat-label>{{ 'labels.inputs.Unassignment Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="unassignedDatePicker"
required
formControlName="unassignedDate"
/>
<mat-datepicker-toggle matSuffix [for]="unassignedDatePicker"></mat-datepicker-toggle>
<mat-datepicker #unassignedDatePicker></mat-datepicker>
@if (currentAccountUnassignStaffForm.controls.unassignedDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Unassignment Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!currentAccountUnassignStaffForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,103 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Unassign Staff Component
*/
@Component({
selector: 'mifosx-current-account-account-unassign-staff',
templateUrl: './current-account-unassign-staff.component.html',
styleUrls: ['./current-account-unassign-staff.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class CurrentAccountUnassignStaffComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Current Account Unassign Staff form. */
currentAccountUnassignStaffForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the current account unassign staff form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createCurrentAccountUnassignStaffForm();
}
/**
* Creates the current account unassign staff form.
*/
createCurrentAccountUnassignStaffForm() {
this.currentAccountUnassignStaffForm = this.formBuilder.group({
unassignedDate: [
'',
Validators.required
]
});
}
/**
* Submits the form and unassigns staff of the current account,
* if successful redirects to the current account.
*/
submit() {
const currentAccountUnassignStaffFormData = this.currentAccountUnassignStaffForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevUnassignmentDate: Date = this.currentAccountUnassignStaffForm.value.unassignedDate;
if (currentAccountUnassignStaffFormData.unassignedDate instanceof Date) {
currentAccountUnassignStaffFormData.unassignedDate = this.dateUtils.formatDate(prevUnassignmentDate, dateFormat);
}
const data = {
...currentAccountUnassignStaffFormData,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountCommand(this.accountId, 'unassignCurrentAccountOfficer', data)
.subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,81 @@
<!--
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-elevation-z8">
<mat-card>
@if (transactionType.holdamount) {
<mat-card-title>{{ 'labels.heading.Hold Amount' | translate }}</mat-card-title>
}
@if (transactionType.blockaccount) {
<mat-card-title>{{ 'labels.heading.Block Current Account' | translate }}</mat-card-title>
}
<form [formGroup]="manageCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content class="layout-row">
<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
<mat-select required formControlName="reasonForBlock">
@for (codeValue of reasonOptions; track codeValue) {
<mat-option [value]="codeValue.id">
{{ codeValue.name }}
</mat-option>
}
</mat-select>
@if (manageCurrentAccountForm.controls.reasonForBlock.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
@if (transactionType.holdamount) {
<mat-form-field class="flex-100" (click)="transactionDatePicker.open()">
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="transactionDatePicker"
required
formControlName="transactionDate"
/>
<mat-datepicker-toggle matSuffix [for]="transactionDatePicker"></mat-datepicker-toggle>
<mat-datepicker #transactionDatePicker></mat-datepicker>
@if (manageCurrentAccountForm.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>
}
@if (transactionType.holdamount) {
<mifosx-input-amount
class="flex-100"
[currency]="currency"
[isRequired]="true"
[inputFormControl]="manageCurrentAccountForm.controls.transactionAmount"
[inputLabel]="'Transaction Amount'"
>
</mifosx-input-amount>
}
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!manageCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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,194 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
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 { SystemService } from 'app/system/system.service';
import { MatCard, MatCardTitle, MatCardContent, MatCardActions } from '@angular/material/card';
import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type TransactionCommandType = 'holdamount' | 'blockaccount' | 'blockdeposit' | 'blockwithdrawal';
interface TransactionType {
holdamount: boolean;
blockaccount: boolean;
blockdeposit: boolean;
blockwithdrawal: boolean;
}
@Component({
selector: 'mifosx-manage-current-account',
templateUrl: './manage-current-account.component.html',
styleUrls: ['./manage-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCardTitle,
InputAmountComponent
]
})
export class ManageCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private systemService = inject(SystemService);
private settingsService = inject(SettingsService);
@Input() currency: Currency;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Manage Current Account form. */
manageCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
currentAccountId: string;
transactionCommand: TransactionCommandType;
reasonOptions: any = [];
transactionType: TransactionType = {
holdamount: false,
blockaccount: false,
blockdeposit: false,
blockwithdrawal: false
};
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.transactionCommand = this.route.snapshot.params['name'].toLowerCase().replaceAll(' ', '');
this.transactionType[this.transactionCommand] = true;
this.currentAccountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the post interest current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createManageCurrentAccountForm();
if (
this.transactionType.holdamount ||
this.transactionType.blockaccount ||
this.transactionType.blockdeposit ||
this.transactionType.blockwithdrawal
) {
this.getCodeValues();
}
}
getCodeValues() {
let codeName = 'CurrentAccountTransactionFreezeReasons'; // Default Hold Amount
if (this.transactionType.blockaccount) {
codeName = 'CurrentAccountBlockReasons';
} else if (this.transactionType.blockdeposit) {
codeName = 'CreditTransactionFreezeReasons';
} else if (this.transactionType.blockwithdrawal) {
codeName = 'DebitTransactionFreezeReasons';
}
this.systemService.getCodes().subscribe((codes: any) => {
codes.some((code: any) => {
if (code.name === codeName) {
this.systemService.getCodeValues(code.id).subscribe((codeValues: any) => {
this.reasonOptions = codeValues;
return true;
});
}
return false;
});
});
}
/**
* Creates the manage current account form.
*/
createManageCurrentAccountForm() {
if (this.transactionType.holdamount) {
this.manageCurrentAccountForm = this.formBuilder.group({
reasonForBlock: [
'',
Validators.required
],
transactionDate: [
'',
Validators.required
],
transactionAmount: [
0.0,
Validators.required
]
});
} else {
this.manageCurrentAccountForm = this.formBuilder.group({
reasonForBlock: [
'',
Validators.required
]
});
}
}
submit() {
let command = '';
let payload: { transactionAmount?: number; [key: string]: any } = {};
if (this.transactionType.holdamount) {
const manageCurrentAccountFormData = this.manageCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevTransactionDate: Date = this.manageCurrentAccountForm.value.transactionDate;
if (manageCurrentAccountFormData.transactionDate instanceof Date) {
manageCurrentAccountFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat);
}
payload = {
...manageCurrentAccountFormData,
dateFormat,
locale
};
command = 'holdAmount';
payload['transactionAmount'] = payload['transactionAmount'] * 1;
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.currentAccountId, command, payload)
.subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
} else {
payload = {
...this.manageCurrentAccountForm.value
};
command = 'block';
if (this.transactionType.blockdeposit) {
command = 'blockCredit';
} else if (this.transactionType.blockwithdrawal) {
command = 'blockDebit';
}
this.currentAccountsService
.executeCurrentAccountCommand(this.currentAccountId, command, payload)
.subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
}
@@ -0,0 +1,44 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="postInterestCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<mat-form-field class="flex-100" (click)="transactionDatePicker.open()">
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="transactionDatePicker"
required
formControlName="transactionDate"
/>
<mat-datepicker-toggle matSuffix [for]="transactionDatePicker"></mat-datepicker-toggle>
<mat-datepicker #transactionDatePicker></mat-datepicker>
@if (postInterestCurrentAccountForm.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>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!postInterestCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,104 @@
/**
* 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, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Post Interest Current Account Component
*/
@Component({
selector: 'mifosx-post-interest-as-on-current-account',
templateUrl: './post-interest-as-on-current-account.component.html',
styleUrls: ['./post-interest-as-on-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
]
})
export class PostInterestAsOnCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Post Interest Current Account form. */
postInterestCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the post interest current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createPostInterestCurrentAccountForm();
}
/**
* Creates the post interest current account form.
*/
createPostInterestCurrentAccountForm() {
this.postInterestCurrentAccountForm = this.formBuilder.group({
transactionDate: [
'',
Validators.required
]
});
}
/**
* Submits the form and post interests of the current account,
* if successful redirects to the current account.
*/
submit() {
const postInterestCurrentAccountFormData = this.postInterestCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevTransactionDate: Date = this.postInterestCurrentAccountForm.value.transactionDate;
if (postInterestCurrentAccountFormData.transactionDate instanceof Date) {
postInterestCurrentAccountFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat);
}
const data = {
...postInterestCurrentAccountFormData,
IsPostInterestAsOn: true,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountTransactionsCommand(this.accountId, 'postInterestAsOn', data)
.subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,51 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="rejectCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field (click)="rejectedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Rejected On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="rejectedOnDatePicker"
required
formControlName="rejectedOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="rejectedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #rejectedOnDatePicker></mat-datepicker>
@if (rejectCurrentAccountForm.controls.rejectedOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Rejected On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="warn" [disabled]="!rejectCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,104 @@
/**
* 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, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Reject Current Account Component
*/
@Component({
selector: 'mifosx-reject-current-account',
templateUrl: './reject-current-account.component.html',
styleUrls: ['./reject-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
CdkTextareaAutosize
]
})
export class RejectCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Reject Current Account form. */
rejectCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the reject current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createRejectCurrentAccountForm();
}
/**
* Creates the reject current account form.
*/
createRejectCurrentAccountForm() {
this.rejectCurrentAccountForm = this.formBuilder.group({
rejectedOnDate: [
'',
Validators.required
],
note: ['']
});
}
/**
* Submits the form and rejects the current account,
* if successful redirects to the current account.
*/
submit() {
const rejectCurrentAccountFormData = this.rejectCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevRejectedOnDate: Date = this.rejectCurrentAccountForm.value.rejectedOnDate;
if (rejectCurrentAccountFormData.rejectedOnDate instanceof Date) {
rejectCurrentAccountFormData.rejectedOnDate = this.dateUtils.formatDate(prevRejectedOnDate, dateFormat);
}
const data = {
...rejectCurrentAccountFormData,
dateFormat,
locale
};
this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'reject', data).subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,29 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="undoApprovalCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<mat-form-field class="flex-fill">
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="warn" [disabled]="!undoApprovalCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -0,0 +1,80 @@
/**
* 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, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Undo Approval Current Account Component
*/
@Component({
selector: 'mifosx-undo-approval-current-account',
templateUrl: './undo-approval-current-account.component.html',
styleUrls: ['./undo-approval-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
CdkTextareaAutosize
]
})
export class UndoApprovalCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private route = inject(ActivatedRoute);
private router = inject(Router);
/** Undo Approval Current Account form. */
undoApprovalCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the undo-approval current account form.
*/
ngOnInit() {
this.createUndoApprovalCurrentAccountForm();
}
/**
* Creates the undo-approval current account form.
*/
createUndoApprovalCurrentAccountForm() {
this.undoApprovalCurrentAccountForm = this.formBuilder.group({
note: ['']
});
}
/**
* Submits the form and undo the approval of share account,
* if successful redirects to the share account.
*/
submit() {
const data = {
...this.undoApprovalCurrentAccountForm.value
};
this.currentAccountsService.executeCurrentAccountCommand(this.accountId, 'undoapproval', data).subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,51 @@
<!--
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-elevation-z8">
<mat-card>
<form [formGroup]="withdrawCurrentAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field (click)="withdrawnOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Withdrawn On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="withdrawnOnDatePicker"
required
formControlName="withdrawnOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="withdrawnOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #withdrawnOnDatePicker></mat-datepicker>
@if (withdrawCurrentAccountForm.controls.withdrawnOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Withdrawn On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!withdrawCurrentAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</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 {
max-width: 37rem;
}
@@ -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/.
*/
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, 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 { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Withdrawn by Client Current Account Component
*/
@Component({
selector: 'mifosx-withdraw-by-client-current-account',
templateUrl: './withdraw-by-client-current-account.component.html',
styleUrls: ['./withdraw-by-client-current-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
CdkTextareaAutosize
]
})
export class WithdrawByClientCurrentAccountComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private dateUtils = inject(Dates);
private route = inject(ActivatedRoute);
private router = inject(Router);
private settingsService = inject(SettingsService);
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Withdraw Current Account form. */
withdrawCurrentAccountForm: UntypedFormGroup;
/** Current Account Id */
accountId: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service
* @param {Dates} dateUtils Date Utils
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.accountId = this.route.snapshot.params['currentAccountId'];
}
/**
* Creates the withdraw current account form.
*/
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createWithdrawCurrentAccountForm();
}
/**
* Creates the withdraw current account form.
*/
createWithdrawCurrentAccountForm() {
this.withdrawCurrentAccountForm = this.formBuilder.group({
withdrawnOnDate: [
'',
Validators.required
],
note: ['']
});
}
/**
* Submits the form and withdraws the current account by client,
* if successful redirects to the current account.
*/
submit() {
const withdrawCurrentAccountFormData = this.withdrawCurrentAccountForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevWithdrawnOnDate: Date = this.withdrawCurrentAccountForm.value.withdrawnOnDate;
if (withdrawCurrentAccountFormData.withdrawnOnDate instanceof Date) {
withdrawCurrentAccountFormData.withdrawnOnDate = this.dateUtils.formatDate(prevWithdrawnOnDate, dateFormat);
}
const data = {
...withdrawCurrentAccountFormData,
dateFormat,
locale
};
this.currentAccountsService
.executeCurrentAccountCommand(this.accountId, 'withdrawnByApplicant', data)
.subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,131 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select #charge>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
<mat-option [value]="charge">
{{ charge.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="flex-48 align-center">
<button type="button" mat-raised-button color="primary" (click)="addCharge(charge)" [disabled]="!charge.value">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargesDataSource.length > 0) {
<table class="flex-98 mat-elevation-z1" mat-table [dataSource]="chargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
<button mat-icon-button color="primary" (click)="editChargeAmount(charge)">
<fa-icon icon="pen"></fa-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') {
<span>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
}
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span>
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}
</span>
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
) {
<span> N/A </span>
}
@if (
charge.chargeTimeType.value === 'Weekly Fee' ||
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date'
) {
<button mat-icon-button color="primary" (click)="editChargeDate(charge)">
<fa-icon icon="pen"></fa-icon>
</button>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.feeInterval) {
<span>{{ charge.feeInterval }}</span>
}
@if (!charge.feeInterval) {
<span>{{ 'labels.inputs.Not Provided' | translate }}</span>
}
@if (charge.chargeTimeType.value === 'Weekly Fee' || charge.chargeTimeType.value === 'Monthly Fee') {
<button mat-icon-button color="primary" (click)="editChargeFeeInterval(charge)">
<fa-icon icon="pen"></fa-icon>
</button>
}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
}
<div class="flex-98 layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
@@ -0,0 +1,19 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,265 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, OnChanges, Input, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { UntypedFormControl } from '@angular/forms';
/** Custom Dialogs */
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
/** Custom Models */
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base';
import { Dates } from 'app/core/utils/dates';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { TranslateService } from '@ngx-translate/core';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { ChargesFilterPipe } from '../../../pipes/charges-filter.pipe';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Charges Step
*/
@Component({
selector: 'mifosx-current-account-charges-step',
templateUrl: './current-account-charges-step.component.html',
styleUrls: ['./current-account-charges-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
MatStepperNext,
ChargesFilterPipe,
DateFormatPipe,
FormatNumberPipe
]
})
export class CurrentAccountChargesStepComponent implements OnInit, OnChanges {
private dialog = inject(MatDialog);
private dateUtils = inject(Dates);
private translateService = inject(TranslateService);
/** Current Account Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Currency Code */
@Input() currencyCode: UntypedFormControl;
@Input() activeClientMembers?: any;
/** Charge Data */
chargeData: any = [];
/** Charges Data Source */
chargesDataSource: {}[] = [];
/** Component is pristine if there has been no changes by user interaction */
pristine = true;
/** For Edit Current Account Form */
isChargesPatched = false;
/** Display columns for charges table */
displayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'date',
'repaymentsEvery',
'action'
];
/** Table Data Source */
dataSource: any;
/** Check for select all the Clients List */
selectAllItems = false;
/** Loan Purpose Options */
loanPurposeOptions: string[] = [];
/** Table Displayed Columns */
displayedColumn: string[] = [
'check',
'id',
'name'
];
ngOnInit() {
if (this.currentAccountTemplate) {
if (!this.isChargesPatched && this.currentAccountTemplate.charges) {
this.chargesDataSource =
this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
this.isChargesPatched = true;
} else {
this.chargesDataSource = [];
}
}
}
ngOnChanges() {
if (this.currentAccountProductTemplate) {
this.chargeData = this.currentAccountProductTemplate.chargeOptions;
this.chargesDataSource =
this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || [];
}
}
/**
* Adds the charge to charges table
* @param {any} charge Charge
*/
addCharge(charge: any) {
this.chargesDataSource = this.chargesDataSource.concat([charge.value]);
charge.value = '';
this.pristine = false;
}
/**
* Edits the Charge Amount
* @param {any} charge Charge
*/
editChargeAmount(charge: any) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'amount',
label: this.translateService.instant('labels.inputs.Amount'),
value: charge.amount,
type: 'number',
required: false
})
];
const data = {
title: this.translateService.instant('labels.heading.Edit Charge Amount'),
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const newCharge = { ...charge, amount: response.data.value.amount };
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
this.pristine = false;
}
/**
* Edits the Charge Date
* @param {any} charge Charge
*/
editChargeDate(charge: any) {
const formfields: FormfieldBase[] = [
new DatepickerBase({
controlName: 'date',
label: this.translateService.instant('labels.inputs.Date'),
value: charge.dueDate || charge.feeOnMonthDay || '',
type: 'datetime-local',
required: false
})
];
const data = {
title: this.translateService.instant('labels.heading.Edit Charge Date'),
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
let newCharge: any;
const dateFormat = 'dd MMMM yyyy';
const date = this.dateUtils.formatDate(response.data.value.date, dateFormat);
switch (charge.chargeTimeType.value) {
case 'Specified due date':
case 'Weekly Fee':
newCharge = { ...charge, dueDate: date };
break;
case 'Annual Fee':
case 'Monthly Fee':
const dateMonthDay = this.dateUtils.formatDate(response.data.value.date, 'dd MMMM');
newCharge = { ...charge, feeOnMonthDay: dateMonthDay };
break;
}
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
this.pristine = false;
}
/**
* Edits the Charge Fee Interval
* @param {any} charge Charge
*/
editChargeFeeInterval(charge: any) {
const formfields: FormfieldBase[] = [
new InputBase({
controlName: 'feeInterval',
label: 'Fee Interval',
value: charge.feeInterval,
type: 'text',
required: false
})
];
const data = {
title: 'Edit Charge Fee Interval',
layout: { addButtonText: 'Confirm' },
formfields: formfields
};
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
editNoteDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
const newCharge = { ...charge, feeInterval: response.data.value.feeInterval };
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
this.chargesDataSource = this.chargesDataSource.concat([]);
}
});
this.pristine = false;
}
/**
* Removes the charge from charges table
* @param {any} charge Charge
*/
deleteCharge(charge: any) {
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
this.chargesDataSource = this.chargesDataSource.concat([]);
this.pristine = false;
}
/**
* Returns current account charges
*/
get currentAccountCharges() {
return { charges: this.chargesDataSource };
}
}
@@ -0,0 +1,77 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<form [formGroup]="currentAccountDetailsForm">
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<mat-select formControlName="productId" required>
@for (product of productData; track product) {
<mat-option [value]="product.id">
{{ product.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
<mat-select formControlName="fieldOfficerId">
@for (officer of fieldOfficerData; track officer) {
<mat-option [value]="officer.id">
{{ officer.displayName }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (currentAccountProductSelected) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
}
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="currentAccountDetailsForm.invalid">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,7 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@@ -0,0 +1,137 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Details Step
*/
@Component({
selector: 'mifosx-current-account-details-step',
templateUrl: './current-account-details-step.component.html',
styleUrls: ['./current-account-details-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
]
})
export class CurrentAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Product Data */
productData: any;
/** Field Officer Data */
fieldOfficerData: any;
/** For edit current account form */
isFieldOfficerPatched = false;
/** Current Account Details Form */
currentAccountDetailsForm: UntypedFormGroup;
currentAccountProductSelected = false;
/** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter();
/**
* Sets share account details form.
* @param {FormBuilder} formBuilder Form Builder.
* @param {CurrentAccountsService} currentAccountsService Current Accounts Service.
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.createCurrentAccountDetailsForm();
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.buildDependencies();
if (this.currentAccountTemplate) {
this.productData = this.currentAccountTemplate.productOptions;
if (this.currentAccountTemplate.currentAccountProductId) {
this.currentAccountDetailsForm.patchValue({
productId: this.currentAccountTemplate.currentAccountProductId,
submittedOnDate:
this.currentAccountTemplate.timeline.submittedOnDate &&
new Date(this.currentAccountTemplate.timeline.submittedOnDate),
externalId: this.currentAccountTemplate.externalId
});
} else {
this.currentAccountDetailsForm.patchValue({
submittedOnDate: new Date()
});
}
}
}
/**
* Creates current account details form.
*/
createCurrentAccountDetailsForm() {
this.currentAccountDetailsForm = this.formBuilder.group({
productId: [
'',
Validators.required
],
submittedOnDate: [
'',
Validators.required
],
fieldOfficerId: [''],
externalId: ['']
});
}
/**
* Fetches current account product template on productId value changes
*/
buildDependencies() {
const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId;
this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
this.currentAccountsService
.getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false)
.subscribe((response: any) => {
this.currentAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.currentAccountProductSelected = true;
if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) {
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId);
this.isFieldOfficerPatched = true;
} else {
this.currentAccountDetailsForm.get('fieldOfficerId').patchValue('');
}
});
});
}
/**
* Returns current account form value.
*/
get currentAccountDetails() {
return this.currentAccountDetailsForm.getRawValue();
}
}
@@ -0,0 +1,269 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="responsive-column">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Product' | translate }}</span>
<span class="flex-60">{{
currentAccount.productId | find: currentAccountTemplate.productOptions : 'id' : 'name'
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Submitted On' | translate }}</span>
<span class="flex-60">{{ currentAccount.submittedOnDate | dateFormat }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span>
@if (currentAccount.externalId) {
<span class="flex-60">
<mifosx-external-identifier externalId="{{ currentAccount.externalId }}"></mifosx-external-identifier>
</span>
}
@if (!currentAccount.externalId) {
<span class="flex-60">
{{ 'labels.inputs.Unassigned' | translate }}
</span>
}
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Field Officer' | translate }}</span>
<span class="flex-60">{{
currentAccount.fieldOfficerId | find: currentAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName'
}}</span>
</div>
@if (activeClientMembers) {
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Client Members' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-60">
<table mat-table #membersTable [dataSource]="dataSource">
<!-- ID Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.id }}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.displayName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="membersDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: membersDisplayedColumns"></tr>
</table>
</div>
}
<h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="flex-60">{{ currentAccountTermsForm.get('currencyCode').value }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="flex-60">{{ currentAccountTermsForm.get('decimal').value }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span>
<span class="flex-60">{{ currentAccount.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCompoundingPeriodType
| find: currentAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestPostingPeriodType
| find: currentAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated using' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCalculationType
| find: currentAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value'
)
}}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.interestCalculationDaysInYearType
| find: currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value'
)
}}</span>
</div>
@if (currentAccount.minRequiredOpeningBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Opening Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.minRequiredOpeningBalance | formatNumber }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ currentAccount.withdrawalFeeForTransfers | yesNo }}</span>
</div>
@if (currentAccount.lockinPeriodFrequency) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">{{
getCatalogTranslation(
currentAccount.lockinPeriodFrequency +
' ' +
(currentAccount.lockinPeriodFrequencyType
| find: currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value')
)
}}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Is Overdraft Allowed' | translate }}:</span>
<span class="flex-60">{{ currentAccount.allowOverdraft | yesNo }}</span>
</div>
@if (currentAccount.minOverdraftForInterestCalculation) {
<div class="flex-fill layout-row">
<span class="flex-40"
>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}:</span
>
<span class="flex-60">{{ currentAccount.minOverdraftForInterestCalculation | formatNumber }}</span>
</div>
}
@if (currentAccount.nominalAnnualInterestRateOverdraft) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}:</span>
<span class="flex-60">{{ currentAccount.nominalAnnualInterestRateOverdraft | formatNumber }} %</span>
</div>
}
@if (currentAccount.overdraftLimit) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}:</span>
<span class="flex-60">{{ currentAccount.overdraftLimit | formatNumber }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Enforce Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.enforceMinRequiredBalance | yesNo }}</span>
</div>
@if (currentAccount.minRequiredBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Balance' | translate }}:</span>
<span class="flex-60">{{ currentAccount.minRequiredBalance }}</span>
</div>
}
@if (currentAccount.charges.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="currentAccount.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Date' | translate }}</th>
<td mat-cell *matCellDef="let charge">
@if (charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee') {
<span>
{{ (charge.dueDate | dateFormat) || 'Unassigned' }}
</span>
}
@if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') {
<span>
{{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }}
</span>
}
@if (
!(
charge.chargeTimeType.value === 'Monthly Fee' ||
charge.chargeTimeType.value === 'Annual Fee' ||
charge.chargeTimeType.value === 'Specified due date' ||
charge.chargeTimeType.value === 'Weekly Fee'
)
) {
<span> N/A </span>
}
</td>
</ng-container>
<ng-container matColumnDef="repaymentsEvery">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Repayments Every' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.feeInterval || 'Not Provided' }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
}
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button [routerLink]="['../..']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" (click)="submitEvent.emit()">
{{ 'labels.buttons.Submit' | translate }}
</button>
</div>
@@ -0,0 +1,34 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
h2,
h3,
h4 {
margin: 0;
font-weight: 500;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,107 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, Input, Output, EventEmitter, OnChanges, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatDivider } from '@angular/material/divider';
import { ExternalIdentifierComponent } from '../../../shared/external-identifier/external-identifier.component';
import { MatStepperPrevious } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current account preview step
*/
@Component({
selector: 'mifosx-current-account-account-preview-step',
templateUrl: './current-account-preview-step.component.html',
styleUrls: ['./current-account-preview-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDivider,
ExternalIdentifierComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
FaIconComponent,
FindPipe,
DateFormatPipe,
FormatNumberPipe,
YesnoPipe
]
})
export class CurrentAccountPreviewStepComponent implements OnChanges {
private translateService = inject(TranslateService);
/** Current Account Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Current Account Terms Form */
@Input() currentAccountTermsForm: any;
/** Current Account */
@Input() currentAccount: any;
@Input() activeClientMembers?: any;
/** Table Data Source */
dataSource: any;
/** Display columns for charges table */
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType',
'date',
'repaymentsEvery'
];
/** Columns to be displayed in active members table. */
membersDisplayedColumns: string[] = [
'id',
'name'
];
/** Form submission event */
@Output() submitEvent = new EventEmitter();
ngOnChanges(): void {
if (this.activeClientMembers?.length > 0) {
this.dataSource = new MatTableDataSource<any>(this.activeClientMembers.filter((member: any) => member.selected));
}
}
getCatalogTranslation(text: string): string {
return this.translateService.instant('labels.catalogs.' + text);
}
}
@@ -0,0 +1,174 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<form [formGroup]="currentAccountTermsForm">
<div class="layout-row-wrap gap-2percent responsive-column align-start-center">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<input matInput formControlName="currencyCode" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<input type="number" matInput formControlName="decimal" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
<input type="number" matInput formControlName="nominalAnnualInterestRate" required />
<mat-error>
{{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label>
<mat-select formControlName="interestCompoundingPeriodType" required>
@for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) {
<mat-option [value]="interestCompoundingPeriodType.id">
{{ interestCompoundingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select formControlName="interestPostingPeriodType" required>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Interest Calculated using' | translate }}</mat-label>
<mat-select formControlName="interestCalculationType" required>
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) {
<mat-option [value]="interestCalculationType.id">
{{ interestCalculationType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label>
<mat-select formControlName="interestCalculationDaysInYearType" required>
@for (
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData;
track interestCalculationDaysInYearType
) {
<mat-option [value]="interestCalculationDaysInYearType.id">
{{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Minimum Opening Balance' | translate }}</mat-label>
<input type="number" matInput formControlName="minRequiredOpeningBalance" />
</mat-form-field>
<mat-checkbox labelPosition="before" formControlName="withdrawalFeeForTransfers" class="margin-v flex-48">
{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}
</mat-checkbox>
<h4 class="mat-h4 flex-98">{{ 'labels.inputs.Lock-in Period' | translate }}</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="lockinPeriodFrequency" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-divider class="flex-98"></mat-divider>
<h3 class="mat-h3 flex-23">{{ 'labels.inputs.Overdraft' | translate }}</h3>
<mat-checkbox class="flex-73 margin-b" labelPosition="before" formControlName="allowOverdraft">
{{ 'labels.inputs.Is Overdraft Allowed' | translate }}
</mat-checkbox>
@if (currentAccountTermsForm.value.allowOverdraft) {
<div class="flex-fill layout-row-wrap gap-2percent responsive-column">
<mat-form-field class="flex-31">
<mat-label>{{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}</mat-label>
<input type="number" matInput formControlName="minOverdraftForInterestCalculation" />
</mat-form-field>
<mat-form-field class="flex-31">
<mat-label>{{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}</mat-label>
<input type="number" matInput formControlName="nominalAnnualInterestRateOverdraft" />
</mat-form-field>
<mat-form-field class="flex-31">
<mat-label>{{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}</mat-label>
<input type="number" matInput formControlName="overdraftLimit" />
</mat-form-field>
</div>
}
<mat-divider class="flex-98"></mat-divider>
<mat-checkbox labelPosition="before" formControlName="enforceMinRequiredBalance" class="margin-v flex-48">
{{ 'labels.inputs.Enforce Minimum Balance' | translate }}
</mat-checkbox>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Minimum Balance' | translate }}</mat-label>
<input type="number" matInput formControlName="minRequiredBalance" />
</mat-form-field>
@if (currentAccountTermsForm.controls.minBalanceForInterestCalculation.value) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" />
</mat-form-field>
}
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</form>
@@ -0,0 +1,32 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
h4 {
font-weight: 500;
margin: 1em 0;
}
h3 {
font-weight: 500;
}
mat-divider {
margin: 1em 0 2em;
}
.margin-v {
margin: 1em 0;
}
.margin-b {
margin: 0 0 1em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,199 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/** Angular Imports */
import { Component, OnChanges, OnInit, Input, inject } from '@angular/core';
import {
UntypedFormGroup,
UntypedFormBuilder,
Validators,
UntypedFormControl,
ReactiveFormsModule
} from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDivider } from '@angular/material/divider';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Current Account Terms Step
*/
@Component({
selector: 'mifosx-current-account-terms-step',
templateUrl: './current-account-terms-step.component.html',
styleUrls: ['./current-account-terms-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatDivider,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
]
})
export class CurrentAccountTermsStepComponent implements OnChanges, OnInit {
private formBuilder = inject(UntypedFormBuilder);
private settingsService = inject(SettingsService);
/** Current Account and Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Current Account Terms Form */
currentAccountTermsForm: UntypedFormGroup;
/** Lockin Period Frequency Type Data */
lockinPeriodFrequencyTypeData: any;
/** Interest Compounding Period Type Data */
interestCompoundingPeriodTypeData: any;
/** Interest Posting Period Type Data */
interestPostingPeriodTypeData: any;
/** Interest Calculation Type Data */
interestCalculationTypeData: any;
/** Interest Calculation Days in Year Data */
interestCalculationDaysInYearTypeData: any;
/**
* @param {FormBuilder} formBuilder Form Builder
* @param {SettingsService} settingsService Setting service
*/
constructor() {
this.createCurrentAccountTermsForm();
this.buildDependencies();
}
ngOnChanges() {
if (this.currentAccountProductTemplate) {
this.currentAccountTermsForm.patchValue({
currencyCode: this.currentAccountProductTemplate.currency.code,
decimal: this.currentAccountProductTemplate.currency.decimalPlaces,
minBalanceForInterestCalculation: this.currentAccountProductTemplate.minBalanceForInterestCalculation,
nominalAnnualInterestRate: this.currentAccountProductTemplate.nominalAnnualInterestRate,
interestCompoundingPeriodType: this.currentAccountProductTemplate.interestCompoundingPeriodType.id,
interestPostingPeriodType: this.currentAccountProductTemplate.interestPostingPeriodType.id,
interestCalculationType: this.currentAccountProductTemplate.interestCalculationType.id,
interestCalculationDaysInYearType: this.currentAccountProductTemplate.interestCalculationDaysInYearType.id,
minRequiredOpeningBalance: this.currentAccountProductTemplate.minRequiredOpeningBalance,
allowOverdraft: this.currentAccountProductTemplate.allowOverdraft,
overdraftLimit: this.currentAccountProductTemplate.overdraftLimit,
enforceMinRequiredBalance: this.currentAccountProductTemplate.enforceMinRequiredBalance,
minOverdraftForInterestCalculation: this.currentAccountProductTemplate.minOverdraftForInterestCalculation,
nominalAnnualInterestRateOverdraft: this.currentAccountProductTemplate.nominalAnnualInterestRateOverdraft,
minRequiredBalance: this.currentAccountProductTemplate.minRequiredBalance,
withdrawalFeeForTransfers: this.currentAccountProductTemplate.withdrawalFeeForTransfers
});
this.setOptions();
}
}
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
if (this.currentAccountTemplate) {
this.currentAccountTermsForm.patchValue({
nominalAnnualInterestRate: this.currentAccountTemplate.nominalAnnualInterestRate,
interestCompoundingPeriodType: this.currentAccountTemplate.interestCompoundingPeriodType.id,
interestPostingPeriodType: this.currentAccountTemplate.interestPostingPeriodType.id,
interestCalculationType: this.currentAccountTemplate.interestCalculationType.id,
interestCalculationDaysInYearType: this.currentAccountTemplate.interestCalculationDaysInYearType.id,
minRequiredOpeningBalance: this.currentAccountTemplate.minRequiredOpeningBalance,
withdrawalFeeForTransfers: this.currentAccountTemplate.withdrawalFeeForTransfers,
lockinPeriodFrequency: this.currentAccountTemplate.lockinPeriodFrequency,
lockinPeriodFrequencyType:
this.currentAccountTemplate.lockinPeriodFrequencyType &&
this.currentAccountTemplate.lockinPeriodFrequencyType.id,
allowOverdraft: this.currentAccountTemplate.allowOverdraft,
enforceMinRequiredBalance: this.currentAccountTemplate.enforceMinRequiredBalance,
minRequiredBalance: this.currentAccountTemplate.minRequiredBalance
});
}
}
/**
* Creates current account terms form.
*/
createCurrentAccountTermsForm() {
this.currentAccountTermsForm = this.formBuilder.group({
currencyCode: [{ value: '', disabled: true }],
decimal: [{ value: '', disabled: true }],
nominalAnnualInterestRate: [
'',
Validators.required
],
interestCompoundingPeriodType: [
'',
Validators.required
],
interestPostingPeriodType: [
'',
Validators.required
],
interestCalculationType: [
'',
Validators.required
],
interestCalculationDaysInYearType: [
'',
Validators.required
],
minRequiredOpeningBalance: [''],
withdrawalFeeForTransfers: [false],
lockinPeriodFrequency: [''],
lockinPeriodFrequencyType: [''],
allowOverdraft: [false],
enforceMinRequiredBalance: [false],
minRequiredBalance: [''],
minBalanceForInterestCalculation: [{ value: '', disabled: true }]
});
}
/**
* Sets all select dropdown options.
*/
setOptions() {
this.lockinPeriodFrequencyTypeData = this.currentAccountProductTemplate.lockinPeriodFrequencyTypeOptions;
this.interestCompoundingPeriodTypeData = this.currentAccountProductTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.currentAccountProductTemplate.interestPostingPeriodTypeOptions;
this.interestCalculationTypeData = this.currentAccountProductTemplate.interestCalculationTypeOptions;
this.interestCalculationDaysInYearTypeData =
this.currentAccountProductTemplate.interestCalculationDaysInYearTypeOptions;
}
/**
* Subscribes to value changes and sets new form controls accordingly.
*/
buildDependencies() {
this.currentAccountTermsForm.get('allowOverdraft').valueChanges.subscribe((allowOverdraft: any) => {
if (allowOverdraft) {
this.currentAccountTermsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl(''));
this.currentAccountTermsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl(''));
this.currentAccountTermsForm.addControl('overdraftLimit', new UntypedFormControl(''));
} else {
this.currentAccountTermsForm.removeControl('minOverdraftForInterestCalculation');
this.currentAccountTermsForm.removeControl('nominalAnnualInterestRateOverdraft');
this.currentAccountTermsForm.removeControl('overdraftLimit');
}
});
}
/**
* Returns current account terms form value.
*/
get currentAccountTerms() {
const payload = this.currentAccountTermsForm.getRawValue();
delete payload.currencyCode;
delete payload.decimal;
delete payload.minBalanceForInterestCalculation; // Backend is not accepting minBalanceForInterestCalculation value
return payload;
}
}
@@ -0,0 +1,46 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
@if (activeClientMembers) {
<div>
<table class="flex-98" mat-table [dataSource]="dataSource">
<!-- Check Column -->
<ng-container matColumnDef="check">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="toggleSelects()" [(ngModel)]="selectAllItems"></mat-checkbox>
</th>
<td mat-cell *matCellDef="let element">
<mat-checkbox (change)="toggleSelect()" [(ngModel)]="element.selected"></mat-checkbox>
</td>
</ng-container>
<!-- Position Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client ID' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.id }}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Client Name' | translate }}</th>
<td mat-cell *matCellDef="let element">{{ element.displayName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumn"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumn"></tr>
</table>
</div>
}
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="selectedClientMembers.selectedMembers.length === 0">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
@@ -0,0 +1,19 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
.margin-t {
margin-top: 1em;
}
@@ -0,0 +1,84 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, OnChanges, Input } from '@angular/core';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatCheckbox } from '@angular/material/checkbox';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-account-active-client-members',
templateUrl: './current-accounts-active-client-members.component.html',
styleUrls: ['./current-accounts-active-client-members.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCheckbox,
FormsModule,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
]
})
export class CurrentAccountActiveClientMembersComponent implements OnInit {
@Input() activeClientMembers?: any;
selectAllItems = false;
displayedColumn: string[] = [
'check',
'id',
'name'
];
constructor() {}
dataSource: any;
ngOnInit(): void {
this.dataSource = new MatTableDataSource<any>(this.activeClientMembers);
}
get selectedClientMembers() {
return { selectedMembers: this.activeClientMembers.filter((item: any) => item.selected) };
}
/** Toggle all checks */
toggleSelects() {
for (const member of this.activeClientMembers) {
member.selected = this.selectAllItems;
}
}
/** Check if all the checks are selected */
toggleSelect() {
const len = this.activeClientMembers.length;
this.selectAllItems =
len === 0 ? false : this.activeClientMembers.filter((item: any) => item.selected).length === len;
}
}
@@ -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>

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