feat: add CFCFRMS module, bank menus, mock backend, and dashboard redesign
- CFCFRMS cyber fraud complaint management with dashboard, complaints, escalation, report suspect/mule/fraud txn, FMR submission, CFR lookup - Mock HTTP interceptor with 20 dummy complaints and all endpoints - Simulator service for I4C/MHA integration testing - 3-row menu bar with 22 items (7+8+8 layout) - Mega menus for General Ledger, Sub Ledger, Deposits, Share, Others, HRM, RBI with search and grouped columns - Dropdowns for Cashier, Clearing, NPA, CKYC, Advances, Letter Mgmt, Insurance, Digital - Dashboard redesign with regulatory, financial health, deposits, risk/NPA sections using static banking data - All translations added to en-US.json - FontAwesome icons registered for all new menu items 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
0a32d7135e
commit
b3a4cce0cf
@@ -0,0 +1,139 @@
|
||||
<!--
|
||||
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="cfcfrms-dashboard">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.menus.CFCFRMS Dashboard' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Cyber Fraud Complaint Management Overview' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Stat Cards -->
|
||||
<div class="stat-cards">
|
||||
<mat-card class="stat-card stat-total">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="file-alt" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats?.totalComplaints || 0 }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Total Complaints' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="stat-card stat-pending">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="clock" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats?.pendingReview || 0 }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Pending Review' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="stat-card stat-lien">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="lock" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats?.lienMarked || 0 }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Lien Marked' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="stat-card stat-recovered">
|
||||
<mat-card-content>
|
||||
<div class="stat-icon"><fa-icon icon="check-circle" size="2x"></fa-icon></div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ formatCurrency(stats?.fundsRecovered || 0) }}</span>
|
||||
<span class="stat-label">{{ 'labels.text.Funds Recovered' | translate }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="charts-row">
|
||||
<mat-card class="chart-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.text.Status Distribution' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="chart-container">
|
||||
<canvas #donutCanvas></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="chart-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.text.Fraud Amount by Status' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="chart-container">
|
||||
<canvas #barCanvas></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Recent Complaints Table -->
|
||||
<mat-card class="recent-complaints-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.text.Recent Complaints' | translate }}</mat-card-title>
|
||||
<span class="spacer"></span>
|
||||
<a mat-stroked-button color="primary" routerLink="/cfcfrms/complaints">{{
|
||||
'labels.text.View All' | translate
|
||||
}}</a>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (isLoading) {
|
||||
<div class="loading-container">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
</div>
|
||||
} @else if (recentComplaints.length === 0) {
|
||||
<div class="empty-state">
|
||||
<fa-icon icon="inbox" size="3x"></fa-icon>
|
||||
<p>{{ 'labels.text.No complaints found' | translate }}</p>
|
||||
</div>
|
||||
} @else {
|
||||
<table mat-table [dataSource]="recentComplaints" class="full-width">
|
||||
<ng-container matColumnDef="ticketId">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Ticket ID' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<a [routerLink]="['/cfcfrms/complaints', row.ticketId]" class="ticket-link">{{ row.ticketId }}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="complainantName">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Complainant' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.complainantName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fraudAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Fraud Amount' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ formatCurrency(row.fraudAmount) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Status' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<cfcfrms-status-badge [status]="row.status"></cfcfrms-status-badge>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="createdAt">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Created' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">{{ formatDate(row.createdAt) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<a mat-icon-button color="primary" [routerLink]="['/cfcfrms/complaints', row.ticketId]">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.cfcfrms-dashboard {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-left: 4px solid;
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&.stat-total {
|
||||
border-color: #1e88e5;
|
||||
.stat-icon {
|
||||
background: #1e88e5;
|
||||
}
|
||||
.stat-value {
|
||||
color: #1e88e5;
|
||||
}
|
||||
}
|
||||
&.stat-pending {
|
||||
border-color: #f59e0b;
|
||||
.stat-icon {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.stat-value {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
&.stat-lien {
|
||||
border-color: #eab308;
|
||||
.stat-icon {
|
||||
background: #eab308;
|
||||
}
|
||||
.stat-value {
|
||||
color: #92400e;
|
||||
}
|
||||
}
|
||||
&.stat-recovered {
|
||||
border-color: #10b981;
|
||||
.stat-icon {
|
||||
background: #10b981;
|
||||
}
|
||||
.stat-value {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-header {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.recent-complaints-card {
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ticket-link {
|
||||
color: #1e88e5;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px;
|
||||
color: #94a3b8;
|
||||
|
||||
p {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CfcfrmsService } from '../cfcfrms.service';
|
||||
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
|
||||
import { Complaint, DashboardStats, ComplaintStatus } from '../models/cfcfrms.models';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
@Component({
|
||||
selector: 'cfcfrms-dashboard',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatCardModule,
|
||||
MatTableModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatIconModule,
|
||||
FaIconComponent,
|
||||
TranslateModule,
|
||||
StatusBadgeComponent
|
||||
],
|
||||
templateUrl: './cfcfrms-dashboard.component.html',
|
||||
styleUrls: ['./cfcfrms-dashboard.component.scss']
|
||||
})
|
||||
export class CfcfrmsDashboardComponent implements OnInit, AfterViewInit {
|
||||
private cfcfrmsService = inject(CfcfrmsService);
|
||||
|
||||
stats: DashboardStats;
|
||||
recentComplaints: Complaint[] = [];
|
||||
isLoading = true;
|
||||
displayedColumns = [
|
||||
'ticketId',
|
||||
'complainantName',
|
||||
'fraudAmount',
|
||||
'status',
|
||||
'createdAt',
|
||||
'actions'
|
||||
];
|
||||
|
||||
@ViewChild('donutCanvas') donutCanvas: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('barCanvas') barCanvas: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
private donutChart: any;
|
||||
private barChart: any;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {}
|
||||
|
||||
loadData(): void {
|
||||
this.isLoading = true;
|
||||
this.cfcfrmsService.getStats().subscribe({
|
||||
next: (stats) => {
|
||||
this.stats = stats;
|
||||
setTimeout(() => this.renderCharts(), 100);
|
||||
},
|
||||
error: () => {
|
||||
this.stats = {
|
||||
totalComplaints: 0,
|
||||
pendingReview: 0,
|
||||
lienMarked: 0,
|
||||
fundsRecovered: 0,
|
||||
statusDistribution: {},
|
||||
amountByStatus: {}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.cfcfrmsService.getComplaints({ page: 0, size: 5 }).subscribe({
|
||||
next: (res) => {
|
||||
this.recentComplaints = res.content;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.recentComplaints = [];
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private renderCharts(): void {
|
||||
if (!this.stats) return;
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
RECEIVED: '#1E88E5',
|
||||
UNDER_REVIEW: '#F59E0B',
|
||||
LIEN_MARKED: '#EAB308',
|
||||
DEBIT_FROZEN: '#EF4444',
|
||||
FUNDS_MOVED: '#8B5CF6',
|
||||
RESOLVED: '#10B981',
|
||||
REJECTED: '#6B7280'
|
||||
};
|
||||
|
||||
// Donut chart
|
||||
if (this.donutCanvas) {
|
||||
const dist = this.stats.statusDistribution || {};
|
||||
const labels = Object.keys(dist);
|
||||
const data = Object.values(dist);
|
||||
const colors = labels.map((l) => statusColors[l] || '#999');
|
||||
|
||||
if (this.donutChart) this.donutChart.destroy();
|
||||
this.donutChart = new Chart(this.donutCanvas.nativeElement, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels.map((l) => l.replace('_', ' ')),
|
||||
datasets: [{ data, backgroundColor: colors, borderWidth: 2, borderColor: '#fff' }]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom', labels: { padding: 16, font: { size: 12 } } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Bar chart
|
||||
if (this.barCanvas) {
|
||||
const amt = this.stats.amountByStatus || {};
|
||||
const labels = Object.keys(amt);
|
||||
const data = Object.values(amt);
|
||||
const colors = labels.map((l) => statusColors[l] || '#999');
|
||||
|
||||
if (this.barChart) this.barChart.destroy();
|
||||
this.barChart = new Chart(this.barCanvas.nativeElement, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels.map((l) => l.replace('_', ' ')),
|
||||
datasets: [{ label: 'Fraud Amount (₹)', data, backgroundColor: colors, borderRadius: 4 }]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { callback: (v: any) => '₹' + (v / 1000).toFixed(0) + 'K' } },
|
||||
x: { ticks: { font: { size: 11 } } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(
|
||||
amount
|
||||
);
|
||||
}
|
||||
|
||||
formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user