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,127 @@
|
||||
<!--
|
||||
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="cfr-lookup-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.text.CFR Lookup' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Query RBI Central Fraud Registry before customer onboarding' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Card -->
|
||||
<mat-card class="search-card">
|
||||
<mat-card-content>
|
||||
<div class="search-row">
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>{{ 'labels.text.Account Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="accountNumber"
|
||||
(keyup.enter)="onSearch()"
|
||||
placeholder="Enter account number to verify..."
|
||||
/>
|
||||
<button mat-icon-button matSuffix (click)="onSearch()" [disabled]="isSearching">
|
||||
<fa-icon icon="search"></fa-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="onSearch()" [disabled]="isSearching" class="search-btn">
|
||||
@if (isSearching) {
|
||||
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
|
||||
} @else {
|
||||
<fa-icon icon="search" class="mr-05"></fa-icon>
|
||||
}
|
||||
{{ 'labels.text.Search CFR' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="quick-search">
|
||||
<span class="quick-label">Quick Demo:</span>
|
||||
@for (acct of sampleAccounts; track acct) {
|
||||
<button mat-stroked-button class="quick-btn" (click)="quickLookup(acct)">{{ acct }}</button>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Results -->
|
||||
@if (isSearching) {
|
||||
<div class="loading-container">
|
||||
<mat-spinner diameter="48"></mat-spinner>
|
||||
<p>Querying RBI Central Fraud Registry...</p>
|
||||
</div>
|
||||
} @else if (hasSearched && result) {
|
||||
<mat-card class="result-card" [class.result-clear]="!result.found" [class.result-flagged]="result.found">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
@if (result.found) {
|
||||
<fa-icon icon="exclamation-triangle" class="warn-icon mr-05"></fa-icon>
|
||||
Fraud Records Found
|
||||
} @else {
|
||||
<fa-icon icon="check-circle" class="clear-icon mr-05"></fa-icon>
|
||||
No Fraud Records Found
|
||||
}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="account-info">
|
||||
<span class="info-label">{{ 'labels.text.Account Number' | translate }}:</span>
|
||||
<span class="info-value mono">{{ result.accountNumber }}</span>
|
||||
<span class="info-label ml-24">Query Time:</span>
|
||||
<span class="info-value">{{ result.queryTimestamp }}</span>
|
||||
</div>
|
||||
|
||||
@if (result.found && result.records.length > 0) {
|
||||
<table mat-table [dataSource]="result.records" class="full-width records-table">
|
||||
<ng-container matColumnDef="fraudReferenceNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>Reference</th>
|
||||
<td mat-cell *matCellDef="let row" class="mono">{{ row.fraudReferenceNumber }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fraudCategory">
|
||||
<th mat-header-cell *matHeaderCellDef>Category</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.fraudCategory }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="fraudAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Fraud Amount' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row" class="amount-cell">{{ formatCurrency(row.fraudAmount) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="reportingBank">
|
||||
<th mat-header-cell *matHeaderCellDef>Reporting Bank</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.reportingBank }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="dateReported">
|
||||
<th mat-header-cell *matHeaderCellDef>Date Reported</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.dateReported }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Status' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<span class="status-chip" [style.background]="getStatusColor(row.status)">{{ row.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
<div class="remarks-section">
|
||||
<h4>{{ 'labels.text.Remarks' | translate }}</h4>
|
||||
@for (record of result.records; track record.fraudReferenceNumber) {
|
||||
<div class="remark-item">
|
||||
<strong>{{ record.fraudReferenceNumber }}:</strong> {{ record.remarks }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (!result.found) {
|
||||
<div class="clear-message">
|
||||
<fa-icon icon="shield-alt" size="2x" class="clear-icon"></fa-icon>
|
||||
<p>This account has no entries in the Central Fraud Registry. Safe to proceed with onboarding.</p>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
@@ -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/.
|
||||
*/
|
||||
|
||||
.cfr-lookup-page {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
height: 56px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.quick-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.quick-label {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
font-size: 12px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
.result-flagged {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
.result-clear {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
.warn-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
.clear-icon {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
.ml-24 {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.records-table {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.amount-cell {
|
||||
font-weight: 600;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.remarks-section {
|
||||
background: #fef2f2;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.remark-item {
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
color: #7f1d1d;
|
||||
border-bottom: 1px solid #fee2e2;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear-message {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
color: #065f46;
|
||||
p {
|
||||
margin-top: 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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 { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CfcfrmsService } from '../cfcfrms.service';
|
||||
import { CfrLookupResponse, CfrRecord } from '../models/cfcfrms.models';
|
||||
|
||||
@Component({
|
||||
selector: 'cfcfrms-cfr-lookup',
|
||||
standalone: true,
|
||||
templateUrl: './cfr-lookup.component.html',
|
||||
styleUrls: ['./cfr-lookup.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
MatChipsModule,
|
||||
FaIconComponent,
|
||||
TranslateModule
|
||||
]
|
||||
})
|
||||
export class CfrLookupComponent {
|
||||
private service = inject(CfcfrmsService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
accountNumber = '';
|
||||
isSearching = false;
|
||||
result: CfrLookupResponse | null = null;
|
||||
hasSearched = false;
|
||||
|
||||
displayedColumns = [
|
||||
'fraudReferenceNumber',
|
||||
'fraudCategory',
|
||||
'fraudAmount',
|
||||
'reportingBank',
|
||||
'dateReported',
|
||||
'status'
|
||||
];
|
||||
|
||||
// Pre-populated sample accounts for quick demo
|
||||
sampleAccounts = [
|
||||
'50100268347291',
|
||||
'50100637291845',
|
||||
'50100745829361',
|
||||
'40120098765432',
|
||||
'99999999999999'
|
||||
];
|
||||
|
||||
onSearch(): void {
|
||||
if (!this.accountNumber || this.accountNumber.trim().length < 5) {
|
||||
this.snackBar.open('Please enter a valid account number', 'OK', { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
this.hasSearched = false;
|
||||
this.result = null;
|
||||
|
||||
this.service.lookupCfr(this.accountNumber.trim()).subscribe({
|
||||
next: (res) => {
|
||||
this.result = res;
|
||||
this.hasSearched = true;
|
||||
this.isSearching = false;
|
||||
},
|
||||
error: () => {
|
||||
this.isSearching = false;
|
||||
this.hasSearched = true;
|
||||
this.snackBar.open('Failed to query CFR', 'OK', { duration: 4000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
quickLookup(acct: string): void {
|
||||
this.accountNumber = acct;
|
||||
this.onSearch();
|
||||
}
|
||||
|
||||
formatCurrency(amount: number): string {
|
||||
return '₹' + (amount || 0).toLocaleString('en-IN');
|
||||
}
|
||||
|
||||
getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'CONFIRMED':
|
||||
return '#EF4444';
|
||||
case 'UNDER_INVESTIGATION':
|
||||
return '#F59E0B';
|
||||
case 'RESOLVED':
|
||||
return '#10B981';
|
||||
default:
|
||||
return '#6B7280';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user