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
@@ -16,6 +16,7 @@ import { Route } from '../core/route/route.service';
/** Custom Components */
import { ReportsComponent } from './reports.component';
import { RunReportComponent } from './run-report/run-report.component';
import { SanctionsLogsComponent } from './sanctions-logs/sanctions-logs.component';
/** Custom Resolvers */
import { ReportsResolver } from './common-resolvers/reports.resolver';
@@ -37,6 +38,11 @@ const routes: Routes = [
path: '',
component: ReportsComponent
},
{
path: 'sanctions-logs',
data: { title: 'Sanctions Logs', breadcrumb: 'Sanctions Logs' },
component: SanctionsLogsComponent
},
{
path: ':filter',
data: { routeParamBreadcrumb: 'filter' },
+3 -1
View File
@@ -20,6 +20,7 @@ import { RunReportComponent } from './run-report/run-report.component';
import { TableAndSmsComponent } from './run-report/table-and-sms/table-and-sms.component';
import { ChartComponent } from './run-report/chart/chart.component';
import { PentahoComponent } from './run-report/pentaho/pentaho.component';
import { SanctionsLogsComponent } from './sanctions-logs/sanctions-logs.component';
/**
* Reports Module
@@ -34,7 +35,8 @@ import { PentahoComponent } from './run-report/pentaho/pentaho.component';
RunReportComponent,
TableAndSmsComponent,
ChartComponent,
PentahoComponent
PentahoComponent,
SanctionsLogsComponent
],
providers: [DecimalPipe]
})
@@ -0,0 +1,99 @@
<div class="container">
<div class="page-header">
<fa-icon icon="shield-alt" class="header-icon"></fa-icon>
<h2>Sanctions Screening Logs</h2>
</div>
<div class="filters-row">
<mat-form-field class="search-field">
<mat-label>Search by Name</mat-label>
<input matInput [value]="searchName" (keyup.enter)="applySearch($event.target.value)" placeholder="e.g. ERIC" />
<fa-icon matSuffix icon="search" class="search-icon"></fa-icon>
</mat-form-field>
<div class="filter-chips">
<button
mat-stroked-button
[class.active-filter]="activeFilter === null"
(click)="filterByResult(null)"
>
<fa-icon icon="list" class="chip-icon"></fa-icon> All
</button>
<button
mat-stroked-button
[class.active-filter]="activeFilter === 'CLEAR'"
class="chip-clear"
(click)="filterByResult('CLEAR')"
>
<fa-icon icon="check-circle" class="chip-icon"></fa-icon> Clear
</button>
<button
mat-stroked-button
[class.active-filter]="activeFilter === 'FLAGGED'"
class="chip-flagged"
(click)="filterByResult('FLAGGED')"
>
<fa-icon icon="exclamation-triangle" class="chip-icon"></fa-icon> Flagged
</button>
@if (searchName || activeFilter) {
<button mat-icon-button color="warn" matTooltip="Clear all filters" (click)="clearFilters()">
<fa-icon icon="times"></fa-icon>
</button>
}
</div>
</div>
@if (loading) {
<mat-progress-bar mode="indeterminate" color="primary"></mat-progress-bar>
}
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="screenedName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Screened Name</th>
<td mat-cell *matCellDef="let log">{{ log.screenedName }}</td>
</ng-container>
<ng-container matColumnDef="screeningResult">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Result</th>
<td mat-cell *matCellDef="let log">
<span class="result-badge" [ngClass]="log.screeningResult === 'CLEAR' ? 'badge-clear' : 'badge-flagged'">
@if (log.screeningResult === 'CLEAR') {
<fa-icon icon="check-circle"></fa-icon>
} @else {
<fa-icon icon="exclamation-triangle"></fa-icon>
}
{{ log.screeningResult }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="matchCount">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Matches</th>
<td mat-cell *matCellDef="let log">{{ log.matchCount || 0 }}</td>
</ng-container>
<ng-container matColumnDef="screenedBy">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Screened By</th>
<td mat-cell *matCellDef="let log">{{ log.screenedBy }}</td>
</ng-container>
<ng-container matColumnDef="screenedAt">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Screened At</th>
<td mat-cell *matCellDef="let log">{{ log.screenedAt | date: 'medium' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="select-row"></tr>
</table>
@if (!loading && dataSource.data.length === 0) {
<div class="no-data">
<fa-icon icon="folder-open" class="no-data-icon"></fa-icon>
<p>No screening logs found.</p>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
@@ -0,0 +1,120 @@
.page-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
.header-icon {
font-size: 24px;
color: #1074b9;
}
h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
}
.filters-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
.search-field {
flex: 1;
min-width: 250px;
max-width: 400px;
}
.search-icon {
color: #999;
margin-right: 4px;
}
}
.filter-chips {
display: flex;
align-items: center;
gap: 8px;
button {
font-size: 13px;
border-radius: 20px;
}
.chip-icon {
margin-right: 4px;
font-size: 12px;
}
.active-filter {
background-color: #1074b9;
color: #fff;
border-color: #1074b9;
}
.chip-clear.active-filter {
background-color: #388e3c;
border-color: #388e3c;
}
.chip-flagged.active-filter {
background-color: #d32f2f;
border-color: #d32f2f;
}
}
.result-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.badge-clear {
background: #e8f5e9;
color: #2e7d32;
}
.badge-flagged {
background: #ffebee;
color: #c62828;
}
.select-row {
cursor: pointer;
&:hover {
background: #f5f5f5;
}
}
.no-data {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
color: #999;
.no-data-icon {
font-size: 40px;
margin-bottom: 8px;
}
p {
font-size: 14px;
margin: 0;
}
}
mat-progress-bar {
margin-bottom: 8px;
}
@@ -0,0 +1,115 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader } from '@angular/material/sort';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatChipsModule } from '@angular/material/chips';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltip } from '@angular/material/tooltip';
import { NgClass, DatePipe } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { SanctionsLogsService } from './sanctions-logs.service';
@Component({
selector: 'mifosx-sanctions-logs',
templateUrl: './sanctions-logs.component.html',
styleUrls: ['./sanctions-logs.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTable,
MatSort,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatSortHeader,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator,
MatChipsModule,
MatProgressBarModule,
MatTooltip,
NgClass,
DatePipe,
FaIconComponent
]
})
export class SanctionsLogsComponent implements OnInit {
private sanctionsLogsService = inject(SanctionsLogsService);
displayedColumns: string[] = [
'screenedName',
'screeningResult',
'matchCount',
'screenedBy',
'screenedAt'
];
dataSource = new MatTableDataSource<any>();
searchName = '';
activeFilter: string | null = null;
loading = false;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
ngOnInit() {
this.loadLogs();
}
loadLogs() {
this.loading = true;
const filters: any = {};
if (this.activeFilter) {
filters.result = this.activeFilter;
}
if (this.searchName?.trim()) {
filters.name = this.searchName.trim();
}
this.sanctionsLogsService.getLogs(filters).subscribe({
next: (data: any) => {
this.dataSource = new MatTableDataSource(data || []);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.loading = false;
},
error: () => {
this.dataSource = new MatTableDataSource([]);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.loading = false;
}
});
}
applySearch(value: string) {
this.searchName = value;
this.loadLogs();
}
filterByResult(result: string | null) {
this.activeFilter = this.activeFilter === result ? null : result;
this.loadLogs();
}
clearFilters() {
this.searchName = '';
this.activeFilter = null;
this.loadLogs();
}
}
@@ -0,0 +1,19 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SanctionsLogsService {
private http = inject(HttpClient);
getLogs(filters?: { result?: string; name?: string }): Observable<any> {
let params = new HttpParams();
if (filters?.result) {
params = params.set('result', filters.result);
}
if (filters?.name) {
params = params.set('name', filters.name);
}
return this.http.get('/sanctions/logs', { params });
}
}