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
@@ -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' },
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user