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:
co-authored by
Claude Opus 4.6
parent
73aa69ee02
commit
0a32d7135e
+151
@@ -0,0 +1,151 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="passbook-container">
|
||||
<!-- Date Range Filter -->
|
||||
<div class="passbook-filter-bar">
|
||||
<form [formGroup]="dateRangeForm" class="passbook-filter-form">
|
||||
<mat-form-field appearance="outline" class="passbook-date-field" (click)="fromDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.From Date' | translate }}</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="fromDatePicker" formControlName="fromDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="fromDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="passbook-date-field" (click)="toDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.To Date' | translate }}</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="toDatePicker" formControlName="toDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!dateRangeForm.valid"
|
||||
(click)="filterTransactions()"
|
||||
>
|
||||
<mat-icon>filter_list</mat-icon>
|
||||
{{ 'labels.buttons.Filter' | translate }}
|
||||
</button>
|
||||
|
||||
@if (isFiltered) {
|
||||
<button mat-stroked-button type="button" (click)="clearFilter()">
|
||||
<mat-icon>clear</mat-icon>
|
||||
{{ 'labels.buttons.Clear' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<span class="passbook-filter-spacer"></span>
|
||||
|
||||
<button mat-raised-button color="accent" type="button" (click)="printPassbook()">
|
||||
<mat-icon>print</mat-icon>
|
||||
{{ 'labels.buttons.Print Passbook' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Passbook Preview (Printable Content) -->
|
||||
<div class="passbook-preview" #passbookContent>
|
||||
<div class="passbook-header">
|
||||
<h2>{{ 'labels.heading.Current Account Passbook' | translate }}</h2>
|
||||
<h3>{{ productName }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Account Info -->
|
||||
<div class="passbook-info-grid">
|
||||
<div class="passbook-info-item">
|
||||
<div class="label">{{ 'labels.inputs.Account Number' | translate }}</div>
|
||||
<div class="value">{{ accountNo }}</div>
|
||||
</div>
|
||||
<div class="passbook-info-item">
|
||||
<div class="label">{{ 'labels.inputs.Account Holder' | translate }}</div>
|
||||
<div class="value">{{ clientName }}</div>
|
||||
</div>
|
||||
<div class="passbook-info-item">
|
||||
<div class="label">{{ 'labels.inputs.Current Account Product' | translate }}</div>
|
||||
<div class="value">{{ productName }}</div>
|
||||
</div>
|
||||
<div class="passbook-info-item">
|
||||
<div class="label">{{ 'labels.inputs.Opening Date' | translate }}</div>
|
||||
<div class="value">{{ activatedOnDate | dateFormat }}</div>
|
||||
</div>
|
||||
<div class="passbook-info-item">
|
||||
<div class="label">{{ 'labels.inputs.Status' | translate }}</div>
|
||||
<div class="value">{{ status }}</div>
|
||||
</div>
|
||||
<div class="passbook-info-item">
|
||||
<div class="label">{{ 'labels.inputs.Interest Rate' | translate }}</div>
|
||||
<div class="value">{{ interestRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Balance Summary -->
|
||||
<div class="passbook-balance-row">
|
||||
<div class="balance-item">
|
||||
<span>{{ 'labels.inputs.Current Balance' | translate }}: </span>
|
||||
<b>{{ accountBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</b>
|
||||
</div>
|
||||
<div class="balance-item">
|
||||
<span>{{ 'labels.inputs.Available Balance' | translate }}: </span>
|
||||
<b>{{ availableBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions Table -->
|
||||
@if (filteredTransactions.length > 0) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{{ 'labels.inputs.Date' | translate }}</th>
|
||||
<th>{{ 'labels.inputs.Description' | translate }}</th>
|
||||
<th class="text-right">{{ 'labels.inputs.Debit' | translate }}</th>
|
||||
<th class="text-right">{{ 'labels.inputs.Credit' | translate }}</th>
|
||||
<th class="text-right">{{ 'labels.inputs.Balance' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (txn of filteredTransactions; track txn.id; let i = $index) {
|
||||
<tr [class.reversed]="txn.reversed">
|
||||
<td>{{ i + 1 }}</td>
|
||||
<td>{{ txn.date | dateFormat }}</td>
|
||||
<td>{{ txn.transactionType.value }}</td>
|
||||
<td class="text-right">
|
||||
@if (isDebit(txn.transactionType)) {
|
||||
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@if (!isDebit(txn.transactionType)) {
|
||||
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ txn.runningBalance | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
} @else {
|
||||
<div class="passbook-no-data">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
<span>{{ 'labels.text.No transactions found for the selected date range.' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="passbook-footer">
|
||||
{{ 'labels.text.Generated on' | translate }}: {{ maxDate | dateFormat }}
|
||||
|
|
||||
{{ 'labels.text.Total Transactions' | translate }}: {{ filteredTransactions.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.passbook-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// Filter Bar
|
||||
.passbook-filter-bar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.passbook-filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.passbook-date-field {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.passbook-filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Passbook Preview
|
||||
.passbook-preview {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.passbook-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #1976d2;
|
||||
padding-bottom: 12px;
|
||||
|
||||
h2 {
|
||||
color: #1976d2;
|
||||
font-size: 20px;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.passbook-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px 20px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.passbook-info-item {
|
||||
.label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.passbook-balance-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 14px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
|
||||
.balance-item {
|
||||
b {
|
||||
color: #1976d2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions Table
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
th {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.reversed {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.passbook-no-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 32px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
|
||||
mat-icon {
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.passbook-footer {
|
||||
margin-top: 16px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (width <= 768px) {
|
||||
.passbook-info-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.passbook-balance-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.passbook-filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.passbook-date-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.passbook-filter-spacer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.passbook-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// Print-specific styles
|
||||
@media print {
|
||||
.passbook-filter-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.passbook-preview {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, OnDestroy, inject, ElementRef, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UntypedFormBuilder, Validators } from '@angular/forms';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
/** Custom Services */
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { Currency } from 'app/shared/models/general.model';
|
||||
import {
|
||||
CurrentAccountTransaction,
|
||||
CurrentAccountTransactionType
|
||||
} from 'app/current-accounts/models/current-account-transaction.model';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-passbook-tab',
|
||||
templateUrl: './passbook-tab.component.html',
|
||||
styleUrls: ['./passbook-tab.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatIcon,
|
||||
FaIconComponent,
|
||||
DateFormatPipe,
|
||||
CurrencyPipe
|
||||
]
|
||||
})
|
||||
export class PassbookTabComponent implements OnInit, OnDestroy {
|
||||
private route = inject(ActivatedRoute);
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@ViewChild('passbookContent') passbookContent: ElementRef;
|
||||
|
||||
/** Account data */
|
||||
accountNo: string = '';
|
||||
clientName: string = '';
|
||||
productName: string = '';
|
||||
currency: Currency | null = null;
|
||||
status: string = '';
|
||||
interestRate: number = 0;
|
||||
accountBalance: number = 0;
|
||||
availableBalance: number = 0;
|
||||
activatedOnDate: any = null;
|
||||
|
||||
/** Transactions */
|
||||
allTransactions: CurrentAccountTransaction[] = [];
|
||||
filteredTransactions: CurrentAccountTransaction[] = [];
|
||||
|
||||
/** Date range form */
|
||||
minDate = new Date(2000, 0, 1);
|
||||
maxDate = new Date();
|
||||
dateRangeForm: any;
|
||||
isFiltered = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
|
||||
this.route.parent.data.pipe(takeUntil(this.destroy$)).subscribe((data: { currentAccountData: any }) => {
|
||||
const account = data.currentAccountData;
|
||||
this.accountNo = account.accountNo || '';
|
||||
this.clientName = account.clientName || account.groupName || '';
|
||||
this.productName = account.currentAccountProductName || '';
|
||||
this.currency = account.currency || null;
|
||||
this.status = account.status?.value || '';
|
||||
this.interestRate = account.nominalAnnualInterestRate || 0;
|
||||
this.accountBalance = account.summary?.accountBalance || 0;
|
||||
this.availableBalance = account.summary?.availableBalance || 0;
|
||||
this.activatedOnDate = account.timeline?.activatedOnDate || null;
|
||||
this.allTransactions = account.transactions || [];
|
||||
this.filteredTransactions = [...this.allTransactions];
|
||||
});
|
||||
|
||||
this.dateRangeForm = this.formBuilder.group({
|
||||
fromDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
toDate: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
isDebit(transactionType: CurrentAccountTransactionType): boolean {
|
||||
return (
|
||||
transactionType.withdrawal === true ||
|
||||
transactionType.feeDeduction === true ||
|
||||
transactionType.overdraftInterest === true ||
|
||||
transactionType.withholdTax === true
|
||||
);
|
||||
}
|
||||
|
||||
filterTransactions(): void {
|
||||
const fromDate = this.dateRangeForm.value.fromDate;
|
||||
const toDate = this.dateRangeForm.value.toDate;
|
||||
|
||||
if (!fromDate || !toDate) return;
|
||||
|
||||
const from = new Date(fromDate).getTime();
|
||||
const to = new Date(toDate).getTime();
|
||||
|
||||
this.filteredTransactions = this.allTransactions.filter((txn: CurrentAccountTransaction) => {
|
||||
const txnDate = this.getTransactionDateAsTime(txn.date);
|
||||
return txnDate >= from && txnDate <= to;
|
||||
});
|
||||
this.isFiltered = true;
|
||||
}
|
||||
|
||||
clearFilter(): void {
|
||||
this.dateRangeForm.reset();
|
||||
this.filteredTransactions = [...this.allTransactions];
|
||||
this.isFiltered = false;
|
||||
}
|
||||
|
||||
private getTransactionDateAsTime(date: any): number {
|
||||
if (Array.isArray(date) && date.length >= 3) {
|
||||
return new Date(date[0], date[1] - 1, date[2]).getTime();
|
||||
}
|
||||
return new Date(date).getTime();
|
||||
}
|
||||
|
||||
printPassbook(): void {
|
||||
const content = this.passbookContent?.nativeElement?.innerHTML;
|
||||
if (!content) return;
|
||||
|
||||
const printWindow = window.open('', 'Passbook', 'height=700,width=900');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Current Account Passbook - ${this.accountNo}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; padding: 20px; color: #333; }
|
||||
.passbook-header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #1976d2; padding-bottom: 12px; }
|
||||
.passbook-header h2 { color: #1976d2; font-size: 18px; margin-bottom: 4px; }
|
||||
.passbook-header h3 { font-size: 14px; font-weight: 400; color: #666; }
|
||||
.passbook-info-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px 16px; margin-bottom: 16px; padding: 10px; background: #f5f5f5; border-radius: 4px; }
|
||||
.passbook-info-item { font-size: 12px; }
|
||||
.passbook-info-item .label { font-weight: 600; color: #666; text-transform: uppercase; font-size: 10px; }
|
||||
.passbook-info-item .value { color: #333; }
|
||||
.passbook-balance-row { display: flex; justify-content: space-between; margin-bottom: 16px; padding: 8px 10px; background: #e3f2fd; border-radius: 4px; font-size: 13px; }
|
||||
.passbook-balance-row .balance-item b { color: #1976d2; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||
th { background: #1976d2; color: #fff; padding: 6px 8px; text-align: left; font-weight: 600; }
|
||||
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; }
|
||||
tr:nth-child(even) { background: #fafafa; }
|
||||
.text-right { text-align: right; }
|
||||
.passbook-footer { margin-top: 16px; font-size: 10px; color: #999; text-align: center; border-top: 1px solid #e0e0e0; padding-top: 8px; }
|
||||
.reversed { text-decoration: line-through; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
printWindow.print();
|
||||
printWindow.close();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user