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:
Aditya
2026-03-18 18:05:43 +05:30
co-authored by Claude Opus 4.6
parent 0a32d7135e
commit b3a4cce0cf
55 changed files with 9667 additions and 298 deletions
+2
View File
@@ -52,6 +52,7 @@ import { RemittancesModule } from './remittances/remittances.module';
import { SupersetAnalyticsModule } from './superset-analytics/superset-analytics.module';
import { CkycModule } from './ckyc/ckyc.module';
import { RbiCircularsModule } from './rbi-circulars/rbi-circulars.module';
import { CfcfrmsModule } from './cfcfrms/cfcfrms.module';
import { PortalModule } from '@angular/cdk/portal';
/** Main Routing Module */
@@ -135,6 +136,7 @@ export function HttpLoaderFactory(http: HttpClient) {
SupersetAnalyticsModule,
CkycModule,
RbiCircularsModule,
CfcfrmsModule,
AppRoutingModule,
NotFoundComponent,
CallbackComponent,
+87
View File
@@ -0,0 +1,87 @@
/**
* 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 { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service';
import { CfcfrmsDashboardComponent } from './dashboard/cfcfrms-dashboard.component';
import { ComplaintListComponent } from './complaint-list/complaint-list.component';
import { ComplaintDetailComponent } from './complaint-detail/complaint-detail.component';
import { EscalateComponent } from './escalate/escalate.component';
import { ReportSuspectComponent } from './report-suspect/report-suspect.component';
import { ReportMuleComponent } from './report-mule/report-mule.component';
import { ReportFraudTxnComponent } from './report-fraud-txn/report-fraud-txn.component';
import { FmrSubmissionComponent } from './fmr-submission/fmr-submission.component';
import { CfrLookupComponent } from './cfr-lookup/cfr-lookup.component';
const routes: Routes = [
Route.withShell([
{
path: 'cfcfrms',
data: { title: 'CFCFRMS', breadcrumb: 'CFCFRMS' },
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: CfcfrmsDashboardComponent,
data: { title: 'CFCFRMS Dashboard', breadcrumb: 'Dashboard' }
},
{
path: 'complaints',
component: ComplaintListComponent,
data: { title: 'Complaints', breadcrumb: 'Complaints' }
},
{
path: 'complaints/:ticketId',
component: ComplaintDetailComponent,
data: { title: 'Complaint Detail', breadcrumb: 'Detail' }
},
{
path: 'escalate',
component: EscalateComponent,
data: { title: 'Escalate Complaint', breadcrumb: 'Escalate' }
},
{
path: 'report-suspect',
component: ReportSuspectComponent,
data: { title: 'Report Suspect Account', breadcrumb: 'Report Suspect' }
},
{
path: 'report-mule',
component: ReportMuleComponent,
data: { title: 'Report Mule Account', breadcrumb: 'Report Mule' }
},
{
path: 'report-fraud-txn',
component: ReportFraudTxnComponent,
data: { title: 'Report Fraud Transaction', breadcrumb: 'Fraud Transaction' }
},
{
path: 'fmr-submission',
component: FmrSubmissionComponent,
data: { title: 'FMR Submission', breadcrumb: 'FMR Submission' }
},
{
path: 'cfr-lookup',
component: CfrLookupComponent,
data: { title: 'CFR Lookup', breadcrumb: 'CFR Lookup' }
}
]
}
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CfcfrmsRoutingModule {}
@@ -0,0 +1,83 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import {
SimPushComplaintRequest,
SimPushComplaintResponse,
SimHoldDirectiveRequest,
SimHoldDirectiveResponse,
SimBulkPushRequest,
SimBulkPushResponse,
SimBankUpdateRequest,
SimBankUpdateResponse,
SimVerifyAccountRequest,
SimVerifyAccountResponse,
SimVerifyTransactionRequest,
SimVerifyTransactionResponse,
SimComplaintStatusResponse,
SimBankPerformanceResponse
} from './models/cfcfrms.models';
@Injectable({
providedIn: 'root'
})
export class CfcfrmsSimulatorService {
private http = inject(HttpClient);
private baseUrl = environment.cfcfrmsApiUrl
? environment.cfcfrmsApiUrl.replace('/cfcfrms', '/simulator/cfcfrms')
: `${environment.serverUrl}/simulator/cfcfrms`;
/** I4C pushes a complaint to the bank (creates a real ticket) */
pushComplaint(payload: SimPushComplaintRequest): Observable<SimPushComplaintResponse> {
return this.http.post<SimPushComplaintResponse>(`${this.baseUrl}/push-complaint`, payload);
}
/** I4C pushes an urgent hold/freeze directive */
pushHoldDirective(payload: SimHoldDirectiveRequest): Observable<SimHoldDirectiveResponse> {
return this.http.post<SimHoldDirectiveResponse>(`${this.baseUrl}/push-hold-directive`, payload);
}
/** I4C pushes a batch of complaints (max 10) */
pushBulkComplaints(payload: SimBulkPushRequest): Observable<SimBulkPushResponse> {
return this.http.post<SimBulkPushResponse>(`${this.baseUrl}/push-bulk-complaints`, payload);
}
/** Bank reports action taken on a complaint */
receiveBankUpdate(payload: SimBankUpdateRequest): Observable<SimBankUpdateResponse> {
return this.http.post<SimBankUpdateResponse>(`${this.baseUrl}/receive-bank-update`, payload);
}
/** Bank reports that funds have been moved/escalated */
receiveEscalation(payload: { ticketId: string }): Observable<any> {
return this.http.post<any>(`${this.baseUrl}/receive-escalation`, payload);
}
/** Verify if an account is known/flagged */
verifyAccount(payload: SimVerifyAccountRequest): Observable<SimVerifyAccountResponse> {
return this.http.post<SimVerifyAccountResponse>(`${this.baseUrl}/verify-account`, payload);
}
/** Verify a transaction for risk/fraud flags */
verifyTransaction(payload: SimVerifyTransactionRequest): Observable<SimVerifyTransactionResponse> {
return this.http.post<SimVerifyTransactionResponse>(`${this.baseUrl}/verify-transaction`, payload);
}
/** CFCFRMS-side status view for a complaint by reference */
getComplaintStatus(ref: string): Observable<SimComplaintStatusResponse> {
return this.http.get<SimComplaintStatusResponse>(`${this.baseUrl}/complaint-status/${ref}`);
}
/** Bank TAT / compliance performance metrics */
getBankPerformance(): Observable<SimBankPerformanceResponse> {
return this.http.get<SimBankPerformanceResponse>(`${this.baseUrl}/bank-performance`);
}
}
+35
View File
@@ -0,0 +1,35 @@
/**
* 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 { NgModule } from '@angular/core';
import { CfcfrmsRoutingModule } from './cfcfrms-routing.module';
import { CfcfrmsDashboardComponent } from './dashboard/cfcfrms-dashboard.component';
import { ComplaintListComponent } from './complaint-list/complaint-list.component';
import { ComplaintDetailComponent } from './complaint-detail/complaint-detail.component';
import { EscalateComponent } from './escalate/escalate.component';
import { ReportSuspectComponent } from './report-suspect/report-suspect.component';
import { ReportMuleComponent } from './report-mule/report-mule.component';
import { ReportFraudTxnComponent } from './report-fraud-txn/report-fraud-txn.component';
import { FmrSubmissionComponent } from './fmr-submission/fmr-submission.component';
import { CfrLookupComponent } from './cfr-lookup/cfr-lookup.component';
@NgModule({
imports: [
CfcfrmsRoutingModule,
CfcfrmsDashboardComponent,
ComplaintListComponent,
ComplaintDetailComponent,
EscalateComponent,
ReportSuspectComponent,
ReportMuleComponent,
ReportFraudTxnComponent,
FmrSubmissionComponent,
CfrLookupComponent
]
})
export class CfcfrmsModule {}
+109
View File
@@ -0,0 +1,109 @@
/**
* 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 { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
Complaint,
ComplaintAction,
DashboardStats,
TimelineEvent,
PagedResponse,
EscalationRequest,
EscalationResponse,
ReportSuspectRequest,
ReportSuspectResponse,
ReportMuleAccountRequest,
ReportMuleAccountResponse,
ReportFraudTransactionRequest,
ReportFraudTransactionResponse,
FmrSubmissionRequest,
FmrSubmissionResponse,
CfrLookupResponse
} from './models/cfcfrms.models';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class CfcfrmsService {
private http = inject(HttpClient);
private baseUrl = environment.cfcfrmsApiUrl || `${environment.serverUrl}/cfcfrms`;
createComplaint(complaint: Partial<Complaint>): Observable<Complaint> {
return this.http.post<Complaint>(`${this.baseUrl}/complaints`, complaint);
}
getComplaints(params?: {
page?: number;
size?: number;
status?: string;
search?: string;
dateFrom?: string;
dateTo?: string;
}): Observable<PagedResponse<Complaint>> {
let httpParams = new HttpParams();
if (params) {
if (params.page != null) httpParams = httpParams.set('page', params.page.toString());
if (params.size != null) httpParams = httpParams.set('size', params.size.toString());
if (params.status) httpParams = httpParams.set('status', params.status);
if (params.search) httpParams = httpParams.set('search', params.search);
if (params.dateFrom) httpParams = httpParams.set('dateFrom', params.dateFrom);
if (params.dateTo) httpParams = httpParams.set('dateTo', params.dateTo);
}
return this.http.get<PagedResponse<Complaint>>(`${this.baseUrl}/complaints`, { params: httpParams });
}
getComplaintById(ticketId: string): Observable<Complaint> {
return this.http.get<Complaint>(`${this.baseUrl}/complaints/${ticketId}`);
}
submitAction(ticketId: string, actionPayload: ComplaintAction): Observable<Complaint> {
return this.http.put<Complaint>(`${this.baseUrl}/complaints/${ticketId}/action`, actionPayload);
}
getStats(): Observable<DashboardStats> {
return this.http.get<DashboardStats>(`${this.baseUrl}/complaints/stats`);
}
getTimeline(ticketId: string): Observable<TimelineEvent[]> {
return this.http.get<TimelineEvent[]>(`${this.baseUrl}/complaints/${ticketId}/timeline`);
}
escalateComplaint(ticketId: string, payload: EscalationRequest): Observable<EscalationResponse> {
return this.http.post<EscalationResponse>(`${this.baseUrl}/complaints/${ticketId}/escalate`, payload);
}
// ─── Bank Proactive Reporting ───
reportSuspect(payload: ReportSuspectRequest): Observable<ReportSuspectResponse> {
return this.http.post<ReportSuspectResponse>(`${this.baseUrl}/report-suspect`, payload);
}
reportMuleAccount(payload: ReportMuleAccountRequest): Observable<ReportMuleAccountResponse> {
return this.http.post<ReportMuleAccountResponse>(`${this.baseUrl}/report-mule-account`, payload);
}
reportFraudTransaction(payload: ReportFraudTransactionRequest): Observable<ReportFraudTransactionResponse> {
return this.http.post<ReportFraudTransactionResponse>(`${this.baseUrl}/report-fraud-transaction`, payload);
}
// ─── RBI Integration ───
private rbiBaseUrl = `${environment.serverUrl}/rbi`;
submitFmr(payload: FmrSubmissionRequest): Observable<FmrSubmissionResponse> {
return this.http.post<FmrSubmissionResponse>(`${this.rbiBaseUrl}/fmr`, payload);
}
lookupCfr(accountNumber: string): Observable<CfrLookupResponse> {
const params = new HttpParams().set('accountNumber', accountNumber);
return this.http.get<CfrLookupResponse>(`${this.rbiBaseUrl}/cfr/lookup`, { params });
}
}
@@ -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';
}
}
}
@@ -0,0 +1,319 @@
<!--
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="complaint-detail-page">
<div class="page-header">
<a mat-icon-button routerLink="/cfcfrms/complaints" matTooltip="{{ 'labels.text.Back to Complaints' | translate }}">
<fa-icon icon="arrow-left"></fa-icon>
</a>
<div>
<h2>{{ 'labels.menus.Complaint Detail' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Ticket ID' | translate }}: {{ ticketId }}</p>
</div>
</div>
@if (isLoading) {
<div class="loading-container">
<mat-spinner diameter="48"></mat-spinner>
</div>
} @else if (!complaint) {
<mat-card class="error-card">
<mat-card-content>
<fa-icon icon="exclamation-triangle" size="2x"></fa-icon>
<p>{{ 'labels.text.Complaint not found' | translate }}</p>
<a mat-stroked-button routerLink="/cfcfrms/complaints">{{ 'labels.text.Back to List' | translate }}</a>
</mat-card-content>
</mat-card>
} @else {
<div class="detail-layout">
<!-- LEFT: Complaint Info -->
<div class="detail-left">
<mat-card class="info-card">
<mat-card-header>
<mat-card-title>{{ 'labels.menus.Complaint Information' | translate }}</mat-card-title>
<span class="spacer"></span>
<cfcfrms-status-badge [status]="complaint.status"></cfcfrms-status-badge>
</mat-card-header>
<mat-card-content>
<div class="info-grid">
<div class="info-section">
<h4>{{ 'labels.text.Complainant Details' | translate }}</h4>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Name' | translate }}</span>
<span class="info-value">{{ complaint.complainantName }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Mobile' | translate }}</span>
<span class="info-value">{{ complaint.complainantMobile }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Email' | translate }}</span>
<span class="info-value">{{ complaint.complainantEmail || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Address' | translate }}</span>
<span class="info-value">{{ complaint.complainantAddress || '-' }}</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="info-section">
<h4>{{ 'labels.text.Fraud Details' | translate }}</h4>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Fraud Amount' | translate }}</span>
<span class="info-value amount">{{ formatCurrency(complaint.fraudAmount) }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Fraud Type' | translate }}</span>
<span class="info-value">{{ complaint.fraudType }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Description' | translate }}</span>
<span class="info-value">{{ complaint.fraudDescription }}</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="info-section">
<h4>{{ 'labels.text.Suspect Account Details' | translate }}</h4>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Account Number' | translate }}</span>
<span class="info-value mono">{{ complaint.suspectAccountNumber }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Account Name' | translate }}</span>
<span class="info-value">{{ complaint.suspectAccountName }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Bank IFSC' | translate }}</span>
<span class="info-value mono">{{ complaint.suspectBankIfsc }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Bank Name' | translate }}</span>
<span class="info-value">{{ complaint.suspectBankName }}</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="info-section">
<h4>{{ 'labels.text.Source & Acknowledgement' | translate }}</h4>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Source Portal' | translate }}</span>
<span class="info-value">{{ complaint.sourcePortal }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Ack. Number' | translate }}</span>
<span class="info-value mono">{{ complaint.acknowledgementNumber }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Assigned Officer' | translate }}</span>
<span class="info-value">{{
complaint.assignedOfficer || ('labels.text.Unassigned' | translate)
}}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Created' | translate }}</span>
<span class="info-value">{{ formatDate(complaint.createdAt) }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Last Updated' | translate }}</span>
<span class="info-value">{{ formatDate(complaint.updatedAt) }}</span>
</div>
</div>
@if (complaint.fundsHeld > 0 || complaint.availableBalance > 0) {
<mat-divider></mat-divider>
<div class="info-section">
<h4>{{ 'labels.text.Fund Details' | translate }}</h4>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Available Balance' | translate }}</span>
<span class="info-value">{{ formatCurrency(complaint.availableBalance) }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Funds Held' | translate }}</span>
<span class="info-value">{{ formatCurrency(complaint.fundsHeld) }}</span>
</div>
</div>
}
@if (complaint.nextHopAccountNumber) {
<mat-divider></mat-divider>
<div class="info-section">
<h4>{{ 'labels.text.Escalation Info' | translate }}</h4>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Next Hop Account' | translate }}</span>
<span class="info-value mono">{{ complaint.nextHopAccountNumber }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'labels.text.Next Hop IFSC' | translate }}</span>
<span class="info-value mono">{{ complaint.nextHopBankIfsc }}</span>
</div>
</div>
}
</div>
</mat-card-content>
</mat-card>
</div>
<!-- RIGHT: Action Panel + Timeline -->
<div class="detail-right">
<!-- Action Panel -->
<mat-card class="action-card">
<mat-card-header>
<mat-card-title>{{ 'labels.text.Take Action' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (isTerminal) {
<div class="terminal-state">
<fa-icon [icon]="complaint.status === 'RESOLVED' ? 'check-circle' : 'times-circle'" size="2x"></fa-icon>
<p>{{ 'labels.text.No further actions can be taken' | translate }}</p>
</div>
} @else {
<form [formGroup]="actionForm" (ngSubmit)="onSubmitAction()">
<div class="current-status">
<span class="label">{{ 'labels.text.Current Status' | translate }}:</span>
<cfcfrms-status-badge [status]="complaint.status"></cfcfrms-status-badge>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Select Action' | translate }}</mat-label>
<mat-select formControlName="action">
@for (opt of actionOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
@if (actionForm.get('action')?.hasError('required') && actionForm.get('action')?.touched) {
<mat-error>{{ 'labels.text.Action is required' | translate }}</mat-error>
}
</mat-form-field>
@if (showBalanceFields) {
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Available Balance (INR)' | translate }}</mat-label>
<input matInput type="number" formControlName="availableBalance" placeholder="0.00" />
@if (
actionForm.get('availableBalance')?.hasError('required') &&
actionForm.get('availableBalance')?.touched
) {
<mat-error>{{ 'labels.text.Available balance is required' | translate }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Funds Held (INR)' | translate }}</mat-label>
<input matInput type="number" formControlName="fundsHeld" placeholder="0.00" />
@if (actionForm.get('fundsHeld')?.hasError('required') && actionForm.get('fundsHeld')?.touched) {
<mat-error>{{ 'labels.text.Funds held is required' | translate }}</mat-error>
}
</mat-form-field>
}
@if (showNextHopFields) {
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Next Hop Account Number' | translate }}</mat-label>
<input
matInput
formControlName="nextHopAccountNumber"
placeholder="{{ 'labels.text.Account Number' | translate }}"
/>
@if (
actionForm.get('nextHopAccountNumber')?.hasError('required') &&
actionForm.get('nextHopAccountNumber')?.touched
) {
<mat-error>{{ 'labels.text.Account number is required' | translate }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Next Hop Bank IFSC' | translate }}</mat-label>
<input matInput formControlName="nextHopBankIfsc" placeholder="e.g. SBIN0000001" />
@if (
actionForm.get('nextHopBankIfsc')?.hasError('required') &&
actionForm.get('nextHopBankIfsc')?.touched
) {
<mat-error>{{ 'labels.text.IFSC code is required' | translate }}</mat-error>
}
@if (
actionForm.get('nextHopBankIfsc')?.hasError('pattern') &&
actionForm.get('nextHopBankIfsc')?.touched
) {
<mat-error>{{ 'labels.text.Invalid IFSC format' | translate }}</mat-error>
}
</mat-form-field>
}
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Action Note' | translate }}</mat-label>
<textarea
matInput
formControlName="actionNote"
rows="3"
placeholder="{{ 'labels.text.Enter remarks...' | translate }}"
></textarea>
@if (actionForm.get('actionNote')?.hasError('required') && actionForm.get('actionNote')?.touched) {
<mat-error>{{ 'labels.text.Action note is required' | translate }}</mat-error>
}
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Action' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
<!-- Timeline -->
<mat-card class="timeline-card">
<mat-card-header>
<mat-card-title>{{ 'labels.text.Audit Timeline' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (timeline.length === 0) {
<div class="empty-timeline">
<fa-icon icon="history" size="2x"></fa-icon>
<p>{{ 'labels.text.No timeline events yet' | translate }}</p>
</div>
} @else {
<div class="timeline">
@for (event of timeline; track event.id) {
<div class="timeline-item">
<div class="timeline-marker" [style.background]="getTimelineColor(event.eventType)">
<fa-icon [icon]="getTimelineIcon(event.eventType)" size="sm"></fa-icon>
</div>
<div class="timeline-content">
<div class="timeline-header">
<strong>{{ event.eventType.replace('_', ' ') }}</strong>
<span class="timeline-time">{{ formatDate(event.timestamp) }}</span>
</div>
<p class="timeline-desc">{{ event.eventDescription }}</p>
<span class="timeline-actor">
<fa-icon icon="user" size="xs"></fa-icon> {{ event.actor }} ({{ event.actorRole }})
</span>
</div>
</div>
}
</div>
}
</mat-card-content>
</mat-card>
</div>
</div>
}
</div>
@@ -0,0 +1,284 @@
/**
* 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/.
*/
.complaint-detail-page {
padding: 24px;
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #0d1b2a;
}
.subtitle {
margin: 2px 0 0;
color: #64748b;
font-size: 14px;
}
}
.loading-container {
display: flex;
justify-content: center;
padding: 80px;
}
.error-card {
text-align: center;
padding: 48px;
color: #94a3b8;
p {
margin: 12px 0;
}
}
.detail-layout {
display: grid;
grid-template-columns: 1fr 420px;
gap: 24px;
align-items: start;
@media (max-width: 1280px) {
grid-template-columns: 1fr;
}
}
.detail-left,
.detail-right {
display: flex;
flex-direction: column;
gap: 20px;
}
.info-card,
.action-card,
.timeline-card {
border-radius: 8px !important;
}
.info-card {
mat-card-header {
display: flex;
align-items: center;
padding: 16px 20px;
.spacer {
flex: 1;
}
}
mat-card-content {
padding: 0 20px 20px !important;
}
}
.info-grid {
mat-divider {
margin: 16px 0;
}
}
.info-section {
h4 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 700;
color: #0d1b2a;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
.info-row {
display: flex;
padding: 6px 0;
.info-label {
flex: 0 0 160px;
font-size: 13px;
color: #64748b;
}
.info-value {
font-size: 14px;
color: #1e293b;
word-break: break-word;
&.amount {
font-weight: 700;
color: #0d1b2a;
font-size: 16px;
}
&.mono {
font-family: 'Courier New', monospace;
}
}
}
.mono {
font-family: 'Courier New', monospace;
}
.action-card {
mat-card-header {
padding: 16px 20px 0;
}
mat-card-content {
padding: 16px 20px 20px !important;
}
}
.terminal-state {
text-align: center;
padding: 24px;
color: #64748b;
p {
margin-top: 12px;
font-size: 14px;
}
}
.current-status {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
.label {
font-size: 13px;
color: #64748b;
}
}
.full-width {
width: 100%;
}
.submit-btn {
margin-top: 8px;
height: 44px;
}
.inline-spinner {
display: inline-block;
}
.mr-05 {
margin-right: 4px;
}
.timeline-card {
mat-card-header {
padding: 16px 20px 0;
}
mat-card-content {
padding: 16px 20px 20px !important;
}
}
.empty-timeline {
text-align: center;
padding: 32px;
color: #94a3b8;
p {
margin-top: 8px;
font-size: 14px;
}
}
.timeline {
position: relative;
&::before {
content: '';
position: absolute;
left: 16px;
top: 0;
bottom: 0;
width: 2px;
background: #e2e8f0;
}
}
.timeline-item {
display: flex;
gap: 16px;
position: relative;
padding-bottom: 20px;
&:last-child {
padding-bottom: 0;
}
}
.timeline-marker {
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
flex-shrink: 0;
z-index: 1;
}
.timeline-content {
flex: 1;
padding-top: 4px;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
strong {
font-size: 14px;
color: #0d1b2a;
}
}
.timeline-time {
font-size: 12px;
color: #94a3b8;
}
.timeline-desc {
margin: 4px 0;
font-size: 13px;
color: #475569;
}
.timeline-actor {
font-size: 12px;
color: #94a3b8;
display: flex;
align-items: center;
gap: 4px;
}
@@ -0,0 +1,264 @@
/**
* 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 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
import { Complaint, ComplaintStatus, TimelineEvent } from '../models/cfcfrms.models';
@Component({
selector: 'cfcfrms-complaint-detail',
standalone: true,
imports: [
CommonModule,
RouterModule,
ReactiveFormsModule,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatIconModule,
MatProgressSpinnerModule,
MatDividerModule,
MatDialogModule,
MatSnackBarModule,
MatTooltipModule,
FaIconComponent,
TranslateModule,
StatusBadgeComponent
],
templateUrl: './complaint-detail.component.html',
styleUrls: ['./complaint-detail.component.scss']
})
export class ComplaintDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private fb = inject(FormBuilder);
private cfcfrmsService = inject(CfcfrmsService);
private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar);
complaint: Complaint | null = null;
timeline: TimelineEvent[] = [];
isLoading = true;
isSubmitting = false;
ticketId: string;
actionForm: FormGroup;
actionOptions: { value: ComplaintStatus; label: string }[] = [
{ value: 'LIEN_MARKED', label: 'Mark Lien' },
{ value: 'DEBIT_FROZEN', label: 'Freeze Debit' },
{ value: 'FUNDS_MOVED', label: 'Move Funds' },
{ value: 'RESOLVED', label: 'Resolve' },
{ value: 'REJECTED', label: 'Reject' }
];
get isTerminal(): boolean {
return this.complaint?.status === 'RESOLVED' || this.complaint?.status === 'REJECTED';
}
get selectedAction(): string {
return this.actionForm?.get('action')?.value || '';
}
get showBalanceFields(): boolean {
return this.selectedAction === 'LIEN_MARKED' || this.selectedAction === 'DEBIT_FROZEN';
}
get showNextHopFields(): boolean {
return this.selectedAction === 'FUNDS_MOVED';
}
ngOnInit(): void {
this.ticketId = this.route.snapshot.paramMap.get('ticketId')!;
this.initForm();
this.loadComplaint();
this.loadTimeline();
}
private initForm(): void {
this.actionForm = this.fb.group({
action: [
'',
Validators.required
],
availableBalance: [null],
fundsHeld: [null],
nextHopAccountNumber: [''],
nextHopBankIfsc: [''],
actionNote: [
'',
Validators.required
]
});
this.actionForm.get('action')!.valueChanges.subscribe((action: string) => {
const balanceCtrl = this.actionForm.get('availableBalance')!;
const fundsCtrl = this.actionForm.get('fundsHeld')!;
const nextAccCtrl = this.actionForm.get('nextHopAccountNumber')!;
const nextIfscCtrl = this.actionForm.get('nextHopBankIfsc')!;
// Reset validators
balanceCtrl.clearValidators();
fundsCtrl.clearValidators();
nextAccCtrl.clearValidators();
nextIfscCtrl.clearValidators();
if (action === 'LIEN_MARKED' || action === 'DEBIT_FROZEN') {
balanceCtrl.setValidators([
Validators.required,
Validators.min(0)
]);
fundsCtrl.setValidators([
Validators.required,
Validators.min(0)
]);
} else if (action === 'FUNDS_MOVED') {
nextAccCtrl.setValidators([Validators.required]);
nextIfscCtrl.setValidators([
Validators.required,
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
]);
}
balanceCtrl.updateValueAndValidity();
fundsCtrl.updateValueAndValidity();
nextAccCtrl.updateValueAndValidity();
nextIfscCtrl.updateValueAndValidity();
});
}
loadComplaint(): void {
this.isLoading = true;
this.cfcfrmsService.getComplaintById(this.ticketId).subscribe({
next: (complaint) => {
this.complaint = complaint;
this.isLoading = false;
},
error: () => {
this.isLoading = false;
this.snackBar.open('Failed to load complaint details', 'Close', { duration: 3000 });
}
});
}
loadTimeline(): void {
this.cfcfrmsService.getTimeline(this.ticketId).subscribe({
next: (events) => (this.timeline = events),
error: () => (this.timeline = [])
});
}
onSubmitAction(): void {
if (this.actionForm.invalid) {
this.actionForm.markAllAsTouched();
return;
}
const actionLabel = this.actionOptions.find((a) => a.value === this.selectedAction)?.label || this.selectedAction;
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '420px',
data: {
title: 'Confirm Action',
message: `Are you sure you want to "${actionLabel}" on ticket ${this.ticketId}? This action cannot be undone.`,
confirmText: 'Proceed',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (confirmed) {
this.submitAction();
}
});
}
private submitAction(): void {
this.isSubmitting = true;
const payload = this.actionForm.value;
this.cfcfrmsService.submitAction(this.ticketId, payload).subscribe({
next: (updated) => {
this.complaint = updated;
this.isSubmitting = false;
this.actionForm.reset();
this.loadTimeline();
this.snackBar.open('Action submitted successfully', 'Close', { duration: 3000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to submit action', 'Close', { duration: 3000 });
}
});
}
formatCurrency(amount: number): string {
if (amount == null) return '-';
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(
amount
);
}
formatDate(dateStr: string): string {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
getTimelineIcon(eventType: string): string {
const icons: Record<string, string> = {
RECEIVED: 'inbox',
UNDER_REVIEW: 'search',
LIEN_MARKED: 'lock',
DEBIT_FROZEN: 'snowflake',
FUNDS_MOVED: 'exchange-alt',
RESOLVED: 'check-circle',
REJECTED: 'times-circle',
ESCALATED: 'arrow-up'
};
return icons[eventType] || 'circle';
}
getTimelineColor(eventType: string): string {
const colors: Record<string, string> = {
RECEIVED: '#1E88E5',
UNDER_REVIEW: '#F59E0B',
LIEN_MARKED: '#EAB308',
DEBIT_FROZEN: '#EF4444',
FUNDS_MOVED: '#8B5CF6',
RESOLVED: '#10B981',
REJECTED: '#6B7280',
ESCALATED: '#1E88E5'
};
return colors[eventType] || '#94A3B8';
}
}
@@ -0,0 +1,162 @@
<!--
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="complaint-list-page">
<div class="page-header">
<div>
<h2>{{ 'labels.menus.Complaint Management' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.I4C/MHA CFCFRMS Complaint Tickets' | translate }}</p>
</div>
<button mat-raised-button color="accent" (click)="exportCsv()" [disabled]="complaints.length === 0">
<fa-icon icon="file-csv" class="mr-05"></fa-icon> {{ 'labels.text.Export CSV' | translate }}
</button>
</div>
<!-- Filter Chips -->
<div class="filter-chips">
<button mat-stroked-button [class.active-chip]="activeChip === 'ALL'" (click)="onChipClick('ALL')">
{{ 'labels.text.All' | translate }}
</button>
<button mat-stroked-button [class.active-chip]="activeChip === 'PENDING'" (click)="onChipClick('PENDING')">
<span class="chip-dot pending"></span> {{ 'labels.text.Pending' | translate }}
</button>
<button mat-stroked-button [class.active-chip]="activeChip === 'ACTIVE'" (click)="onChipClick('ACTIVE')">
<span class="chip-dot active"></span> {{ 'labels.text.Active' | translate }}
</button>
<button mat-stroked-button [class.active-chip]="activeChip === 'RESOLVED'" (click)="onChipClick('RESOLVED')">
<span class="chip-dot resolved"></span> {{ 'labels.text.Resolved' | translate }}
</button>
</div>
<!-- Filter Bar -->
<mat-card class="filter-bar">
<mat-card-content>
<div class="filter-row">
<mat-form-field appearance="outline" class="filter-field">
<mat-label>{{ 'labels.text.Search by Ticket ID / Mobile' | translate }}</mat-label>
<input
matInput
[(ngModel)]="searchQuery"
(keyup.enter)="onSearch()"
placeholder="{{ 'labels.text.Enter ticket ID or mobile...' | translate }}"
/>
<button mat-icon-button matSuffix (click)="onSearch()">
<fa-icon icon="search"></fa-icon>
</button>
</mat-form-field>
<mat-form-field appearance="outline" class="filter-field">
<mat-label>{{ 'labels.text.Status' | translate }}</mat-label>
<mat-select [(ngModel)]="statusFilter" (selectionChange)="onStatusFilterChange()">
<mat-option value="">{{ 'labels.text.All Statuses' | translate }}</mat-option>
@for (s of statuses; track s) {
<mat-option [value]="s">{{ s.replace('_', ' ') }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="filter-field">
<mat-label>{{ 'labels.text.From Date' | translate }}</mat-label>
<input matInput [matDatepicker]="fromPicker" [(ngModel)]="dateFrom" (dateChange)="onSearch()" />
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="filter-field">
<mat-label>{{ 'labels.text.To Date' | translate }}</mat-label>
<input matInput [matDatepicker]="toPicker" [(ngModel)]="dateTo" (dateChange)="onSearch()" />
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
<button mat-icon-button matTooltip="{{ 'labels.text.Clear Filters' | translate }}" (click)="clearFilters()">
<fa-icon icon="times-circle"></fa-icon>
</button>
</div>
</mat-card-content>
</mat-card>
<!-- Table -->
<mat-card class="table-card">
<mat-card-content>
@if (isLoading) {
<div class="loading-container">
<mat-spinner diameter="40"></mat-spinner>
</div>
} @else if (complaints.length === 0) {
<div class="empty-state">
<fa-icon icon="inbox" size="3x"></fa-icon>
<p>{{ 'labels.text.No complaints match your filters' | translate }}</p>
<button mat-stroked-button (click)="clearFilters()">{{ 'labels.text.Clear Filters' | translate }}</button>
</div>
} @else {
<div class="table-responsive">
<table mat-table [dataSource]="complaints" 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="complainantMobile">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Mobile' | translate }}</th>
<td mat-cell *matCellDef="let row">{{ row.complainantMobile }}</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="suspectAccountNumber">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.text.Suspect Account' | translate }}</th>
<td mat-cell *matCellDef="let row" class="mono">{{ row.suspectAccountNumber }}</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 At' | 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]"
matTooltip="{{ 'labels.text.View Details' | translate }}"
>
<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>
</div>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageIndex]="pageIndex"
[pageSizeOptions]="[10, 25, 50]"
(page)="onPageChange($event)"
showFirstLastButtons
>
</mat-paginator>
}
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,148 @@
/**
* 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/.
*/
.complaint-list-page {
padding: 24px;
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #0d1b2a;
}
.subtitle {
margin: 4px 0 0;
color: #64748b;
font-size: 14px;
}
}
.filter-chips {
display: flex;
gap: 10px;
margin-bottom: 16px;
button {
border-radius: 20px !important;
font-size: 13px;
}
.active-chip {
background: #1e88e5 !important;
color: #fff !important;
border-color: #1e88e5 !important;
}
.chip-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
&.pending {
background: #f59e0b;
}
&.active {
background: #1e88e5;
}
&.resolved {
background: #10b981;
}
}
}
.filter-bar {
margin-bottom: 16px;
border-radius: 8px !important;
mat-card-content {
padding: 12px 16px !important;
}
}
.filter-row {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
.filter-field {
flex: 1;
min-width: 180px;
}
}
.table-card {
border-radius: 8px !important;
mat-card-content {
padding: 0 !important;
}
}
.table-responsive {
overflow-x: auto;
}
.full-width {
width: 100%;
}
.ticket-link {
color: #1e88e5;
text-decoration: none;
font-weight: 600;
&:hover {
text-decoration: underline;
}
}
.amount-cell {
font-weight: 600;
color: #0d1b2a;
}
.mono {
font-family: 'Courier New', monospace;
font-size: 13px;
}
.loading-container {
display: flex;
justify-content: center;
padding: 48px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 64px;
color: #94a3b8;
p {
margin: 12px 0;
font-size: 14px;
}
}
.mr-05 {
margin-right: 4px;
}
@@ -0,0 +1,221 @@
/**
* 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, ViewChild, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatTableModule } from '@angular/material/table';
import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
import { Complaint, ComplaintStatus } from '../models/cfcfrms.models';
type FilterChip = 'ALL' | 'PENDING' | 'ACTIVE' | 'RESOLVED';
@Component({
selector: 'cfcfrms-complaint-list',
standalone: true,
imports: [
CommonModule,
RouterModule,
FormsModule,
MatCardModule,
MatTableModule,
MatPaginatorModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatSelectModule,
MatChipsModule,
MatIconModule,
MatProgressSpinnerModule,
MatDatepickerModule,
MatNativeDateModule,
MatSnackBarModule,
MatTooltipModule,
FaIconComponent,
TranslateModule,
StatusBadgeComponent
],
templateUrl: './complaint-list.component.html',
styleUrls: ['./complaint-list.component.scss']
})
export class ComplaintListComponent implements OnInit {
private cfcfrmsService = inject(CfcfrmsService);
private snackBar = inject(MatSnackBar);
@ViewChild(MatPaginator) paginator: MatPaginator;
complaints: Complaint[] = [];
totalElements = 0;
pageSize = 10;
pageIndex = 0;
isLoading = true;
searchQuery = '';
statusFilter = '';
dateFrom: Date | null = null;
dateTo: Date | null = null;
activeChip: FilterChip = 'ALL';
displayedColumns = [
'ticketId',
'complainantName',
'complainantMobile',
'fraudAmount',
'suspectAccountNumber',
'status',
'createdAt',
'actions'
];
statuses: ComplaintStatus[] = [
'RECEIVED',
'UNDER_REVIEW',
'LIEN_MARKED',
'DEBIT_FROZEN',
'FUNDS_MOVED',
'RESOLVED',
'REJECTED'
];
ngOnInit(): void {
this.loadComplaints();
}
loadComplaints(): void {
this.isLoading = true;
const params: any = {
page: this.pageIndex,
size: this.pageSize
};
if (this.searchQuery) params.search = this.searchQuery;
if (this.statusFilter) params.status = this.statusFilter;
if (this.dateFrom) params.dateFrom = this.dateFrom.toISOString().split('T')[0];
if (this.dateTo) params.dateTo = this.dateTo.toISOString().split('T')[0];
this.cfcfrmsService.getComplaints(params).subscribe({
next: (res) => {
this.complaints = res.content;
this.totalElements = res.totalElements;
this.isLoading = false;
},
error: () => {
this.complaints = [];
this.totalElements = 0;
this.isLoading = false;
this.snackBar.open('Failed to load complaints', 'Close', { duration: 3000 });
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadComplaints();
}
onSearch(): void {
this.pageIndex = 0;
this.loadComplaints();
}
onStatusFilterChange(): void {
this.pageIndex = 0;
this.activeChip = 'ALL';
this.loadComplaints();
}
onChipClick(chip: FilterChip): void {
this.activeChip = chip;
switch (chip) {
case 'ALL':
this.statusFilter = '';
break;
case 'PENDING':
this.statusFilter = 'RECEIVED,UNDER_REVIEW';
break;
case 'ACTIVE':
this.statusFilter = 'LIEN_MARKED,DEBIT_FROZEN,FUNDS_MOVED';
break;
case 'RESOLVED':
this.statusFilter = 'RESOLVED,REJECTED';
break;
}
this.pageIndex = 0;
this.loadComplaints();
}
clearFilters(): void {
this.searchQuery = '';
this.statusFilter = '';
this.dateFrom = null;
this.dateTo = null;
this.activeChip = 'ALL';
this.pageIndex = 0;
this.loadComplaints();
}
exportCsv(): void {
const headers = [
'Ticket ID',
'Complainant',
'Mobile',
'Fraud Amount',
'Suspect Account',
'Status',
'Created At'
];
const rows = this.complaints.map((c) => [
c.ticketId,
c.complainantName,
c.complainantMobile,
c.fraudAmount.toString(),
c.suspectAccountNumber,
c.status,
c.createdAt
]);
const csvContent = [
headers.join(','),
...rows.map((r) => r.join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `cfcfrms_complaints_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
URL.revokeObjectURL(link.href);
this.snackBar.open('CSV exported successfully', 'Close', { duration: 3000 });
}
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' });
}
}
@@ -0,0 +1,61 @@
/**
* 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 { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
export interface ConfirmDialogData {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
}
@Component({
selector: 'cfcfrms-confirm-dialog',
standalone: true,
imports: [
CommonModule,
MatDialogModule,
MatButtonModule
],
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content>
<p>{{ data.message }}</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ data.cancelText || 'Cancel' }}</button>
<button mat-raised-button color="primary" (click)="onConfirm()">{{ data.confirmText || 'Confirm' }}</button>
</mat-dialog-actions>
`,
styles: [
`
p {
margin: 8px 0;
font-size: 14px;
}
`
]
})
export class ConfirmDialogComponent {
constructor(
public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData
) {}
onCancel(): void {
this.dialogRef.close(false);
}
onConfirm(): void {
this.dialogRef.close(true);
}
}
@@ -0,0 +1,81 @@
/**
* 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, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ComplaintStatus } from '../../models/cfcfrms.models';
@Component({
selector: 'cfcfrms-status-badge',
standalone: true,
imports: [CommonModule],
template: `
<span class="status-badge" [ngClass]="'status-' + (status | lowercase)">
{{ statusLabel }}
</span>
`,
styles: [
`
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.status-received {
background: #dbeafe;
color: #1e40af;
}
.status-under_review {
background: #fef3c7;
color: #92400e;
}
.status-lien_marked {
background: #fef9c3;
color: #854d0e;
}
.status-debit_frozen {
background: #fee2e2;
color: #991b1b;
}
.status-funds_moved {
background: #ede9fe;
color: #5b21b6;
}
.status-resolved {
background: #d1fae5;
color: #065f46;
}
.status-rejected {
background: #f3f4f6;
color: #374151;
}
`
]
})
export class StatusBadgeComponent {
@Input() status: ComplaintStatus;
private readonly labels: Record<ComplaintStatus, string> = {
RECEIVED: 'Received',
UNDER_REVIEW: 'Under Review',
LIEN_MARKED: 'Lien Marked',
DEBIT_FROZEN: 'Debit Frozen',
FUNDS_MOVED: 'Funds Moved',
RESOLVED: 'Resolved',
REJECTED: 'Rejected'
};
get statusLabel(): string {
return this.labels[this.status] || this.status;
}
}
@@ -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' });
}
}
@@ -0,0 +1,220 @@
<!--
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="escalate-page">
<div class="page-header">
<h2>{{ 'labels.menus.Escalate Complaint' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Forward complaint to the next bank in the transaction chain' | translate }}</p>
</div>
<div class="escalate-layout">
<!-- Escalation Form -->
<mat-card class="escalate-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="arrow-up" class="mr-05"></fa-icon> {{ 'labels.text.Escalation Form' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (escalationResult) {
<div class="success-result">
<div class="success-icon">
<fa-icon icon="check-circle" size="3x"></fa-icon>
</div>
<h3>{{ 'labels.text.Escalation Successful' | translate }}</h3>
<div class="result-details">
<div class="result-row">
<span class="result-label">{{ 'labels.text.Reference Number' | translate }}</span>
<span class="result-value mono">{{ escalationResult.referenceNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Ticket ID' | translate }}</span>
<span class="result-value">{{ escalationResult.ticketId }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Escalated To' | translate }}</span>
<span class="result-value">{{ escalationResult.escalatedTo }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Bank IFSC' | translate }}</span>
<span class="result-value mono">{{ escalationResult.escalatedBankIfsc }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Status' | translate }}</span>
<span class="result-value">{{ escalationResult.status }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Timestamp' | translate }}</span>
<span class="result-value">{{ escalationResult.timestamp }}</span>
</div>
</div>
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New Escalation' | translate }}
</button>
</div>
} @else {
<form [formGroup]="escalateForm" (ngSubmit)="onSubmit()">
<div class="ticket-lookup-row">
<mat-form-field appearance="outline" class="ticket-field">
<mat-label>{{ 'labels.text.Ticket ID' | translate }}</mat-label>
<input matInput formControlName="ticketId" placeholder="e.g. CFCFRMS-2024-00001" />
@if (escalateForm.get('ticketId')?.hasError('required') && escalateForm.get('ticketId')?.touched) {
<mat-error>{{ 'labels.text.Ticket ID' | translate }} is required</mat-error>
}
</mat-form-field>
<button
mat-raised-button
type="button"
(click)="lookupTicket()"
[disabled]="isLookingUp"
class="lookup-btn"
>
@if (isLookingUp) {
<mat-spinner diameter="20"></mat-spinner>
} @else {
<fa-icon icon="search" class="mr-05"></fa-icon> {{ 'labels.text.Lookup' | translate }}
}
</button>
</div>
@if (lookedUpComplaint) {
<div class="lookup-result">
<div class="lookup-row">
<span>{{ 'labels.text.Complainant' | translate }}:</span>
<strong>{{ lookedUpComplaint.complainantName }}</strong>
</div>
<div class="lookup-row">
<span>{{ 'labels.text.Fraud Amount' | translate }}:</span>
<strong>{{ formatCurrency(lookedUpComplaint.fraudAmount) }}</strong>
</div>
<div class="lookup-row">
<span>{{ 'labels.text.Suspect Account' | translate }}:</span>
<strong class="mono">{{ lookedUpComplaint.suspectAccountNumber }}</strong>
</div>
<div class="lookup-row">
<span>{{ 'labels.text.Current Status' | translate }}:</span>
<cfcfrms-status-badge [status]="lookedUpComplaint.status"></cfcfrms-status-badge>
</div>
</div>
}
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Next Hop Account Number' | translate }}</mat-label>
<input
matInput
formControlName="nextHopAccountNumber"
placeholder="{{ 'labels.text.Account Number' | translate }}"
/>
@if (
escalateForm.get('nextHopAccountNumber')?.hasError('required') &&
escalateForm.get('nextHopAccountNumber')?.touched
) {
<mat-error>{{ 'labels.text.Account number is required' | translate }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Next Hop Bank IFSC' | translate }}</mat-label>
<input matInput formControlName="nextHopBankIfsc" placeholder="e.g. SBIN0000001" />
@if (
escalateForm.get('nextHopBankIfsc')?.hasError('required') &&
escalateForm.get('nextHopBankIfsc')?.touched
) {
<mat-error>{{ 'labels.text.IFSC code is required' | translate }}</mat-error>
}
@if (
escalateForm.get('nextHopBankIfsc')?.hasError('pattern') && escalateForm.get('nextHopBankIfsc')?.touched
) {
<mat-error>{{ 'labels.text.Invalid IFSC format' | translate }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Remarks' | translate }}</mat-label>
<textarea
matInput
formControlName="remarks"
rows="4"
placeholder="{{ 'labels.text.Reason for escalation...' | translate }}"
></textarea>
@if (escalateForm.get('remarks')?.hasError('required') && escalateForm.get('remarks')?.touched) {
<mat-error>{{ 'labels.text.Remarks are required' | translate }}</mat-error>
}
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Escalation' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
<!-- Instructions Panel -->
<mat-card class="instructions-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="info-circle" class="mr-05"></fa-icon> {{ 'labels.text.Escalation Guidelines' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="guidelines">
<div class="guideline-item">
<fa-icon icon="check" class="guideline-icon"></fa-icon>
<p>{{ 'labels.text.Verify the complaint details before escalating to the next bank.' | translate }}</p>
</div>
<div class="guideline-item">
<fa-icon icon="check" class="guideline-icon"></fa-icon>
<p>
{{
'labels.text.Ensure the Next Hop Account and IFSC belong to the next bank in the fraud transaction chain.'
| translate
}}
</p>
</div>
<div class="guideline-item">
<fa-icon icon="check" class="guideline-icon"></fa-icon>
<p>{{ 'labels.text.Provide clear and detailed remarks explaining the escalation reason.' | translate }}</p>
</div>
<div class="guideline-item">
<fa-icon icon="exclamation-triangle" class="guideline-icon warn"></fa-icon>
<p>{{ 'labels.text.Escalation must be completed within the I4C/MHA mandated timeline.' | translate }}</p>
</div>
<div class="guideline-item">
<fa-icon icon="exclamation-triangle" class="guideline-icon warn"></fa-icon>
<p>
{{
'labels.text.Once escalated, the complaint will be forwarded to the target bank for action.' | translate
}}
</p>
</div>
</div>
<div class="bank-info">
<h4>{{ 'labels.text.Current Bank Info' | translate }}</h4>
<div class="bank-row">
<span>{{ 'labels.text.Bank Name' | translate }}:</span> <strong>LincPay</strong>
</div>
<div class="bank-row">
<span>{{ 'labels.text.Bank IFSC' | translate }}:</span> <strong class="mono">LINC0000001</strong>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,211 @@
/**
* 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/.
*/
.escalate-page {
padding: 24px;
max-width: 1200px;
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;
}
}
.escalate-layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 24px;
align-items: start;
@media (max-width: 1280px) {
grid-template-columns: 1fr;
}
}
.escalate-card,
.instructions-card {
border-radius: 8px !important;
mat-card-header {
padding: 16px 20px 0;
}
mat-card-content {
padding: 16px 20px 20px !important;
}
}
.ticket-lookup-row {
display: flex;
gap: 12px;
align-items: flex-start;
.ticket-field {
flex: 1;
}
.lookup-btn {
margin-top: 4px;
height: 56px;
}
}
.lookup-result {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
.lookup-row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 14px;
span {
color: #64748b;
}
}
}
.full-width {
width: 100%;
}
.submit-btn {
margin-top: 8px;
height: 44px;
}
.inline-spinner {
display: inline-block;
}
.mr-05 {
margin-right: 4px;
}
.mt-16 {
margin-top: 16px;
}
.mono {
font-family: 'Courier New', monospace;
}
// Success result
.success-result {
text-align: center;
padding: 24px 0;
.success-icon {
color: #10b981;
margin-bottom: 12px;
}
h3 {
margin: 0 0 20px;
color: #065f46;
font-size: 20px;
}
}
.result-details {
text-align: left;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
padding: 16px;
.result-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 14px;
.result-label {
color: #64748b;
}
.result-value {
font-weight: 600;
color: #1e293b;
}
}
}
// Instructions card
.guidelines {
margin-bottom: 20px;
}
.guideline-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 8px 0;
.guideline-icon {
color: #10b981;
margin-top: 2px;
flex-shrink: 0;
&.warn {
color: #f59e0b;
}
}
p {
margin: 0;
font-size: 13px;
color: #475569;
line-height: 1.5;
}
}
.bank-info {
background: #f8fafc;
border-radius: 8px;
padding: 16px;
border: 1px solid #e2e8f0;
h4 {
margin: 0 0 8px;
font-size: 13px;
font-weight: 700;
color: #0d1b2a;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.bank-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 14px;
span {
color: #64748b;
}
}
}
@@ -0,0 +1,169 @@
/**
* 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 { RouterModule } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
import { Complaint, EscalationResponse } from '../models/cfcfrms.models';
@Component({
selector: 'cfcfrms-escalate',
standalone: true,
imports: [
CommonModule,
RouterModule,
ReactiveFormsModule,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatProgressSpinnerModule,
MatDialogModule,
MatSnackBarModule,
FaIconComponent,
TranslateModule,
StatusBadgeComponent
],
templateUrl: './escalate.component.html',
styleUrls: ['./escalate.component.scss']
})
export class EscalateComponent {
private fb = inject(FormBuilder);
private cfcfrmsService = inject(CfcfrmsService);
private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar);
escalateForm: FormGroup;
isSubmitting = false;
isLookingUp = false;
lookedUpComplaint: Complaint | null = null;
escalationResult: EscalationResponse | null = null;
constructor() {
this.escalateForm = this.fb.group({
ticketId: [
'',
Validators.required
],
nextHopAccountNumber: [
'',
Validators.required
],
nextHopBankIfsc: [
'',
[
Validators.required,
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
]
],
remarks: [
'',
Validators.required
]
});
}
lookupTicket(): void {
const ticketId = this.escalateForm.get('ticketId')?.value?.trim();
if (!ticketId) {
this.snackBar.open('Enter a Ticket ID first', 'Close', { duration: 3000 });
return;
}
this.isLookingUp = true;
this.lookedUpComplaint = null;
this.cfcfrmsService.getComplaintById(ticketId).subscribe({
next: (complaint) => {
this.lookedUpComplaint = complaint;
this.isLookingUp = false;
},
error: () => {
this.isLookingUp = false;
this.snackBar.open('Ticket not found', 'Close', { duration: 3000 });
}
});
}
onSubmit(): void {
if (this.escalateForm.invalid) {
this.escalateForm.markAllAsTouched();
return;
}
const ticketId = this.escalateForm.get('ticketId')?.value;
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '420px',
data: {
title: 'Confirm Escalation',
message: `Are you sure you want to escalate ticket ${ticketId} to the next bank? This will forward the complaint for further investigation.`,
confirmText: 'Escalate',
cancelText: 'Cancel'
}
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (confirmed) {
this.submitEscalation();
}
});
}
private submitEscalation(): void {
this.isSubmitting = true;
const formVal = this.escalateForm.value;
this.cfcfrmsService
.escalateComplaint(formVal.ticketId, {
ticketId: formVal.ticketId,
nextHopAccountNumber: formVal.nextHopAccountNumber,
nextHopBankIfsc: formVal.nextHopBankIfsc,
remarks: formVal.remarks
})
.subscribe({
next: (result) => {
this.escalationResult = result;
this.isSubmitting = false;
this.snackBar.open('Complaint escalated successfully', 'Close', { duration: 3000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to escalate complaint', 'Close', { duration: 3000 });
}
});
}
resetForm(): void {
this.escalateForm.reset();
this.lookedUpComplaint = null;
this.escalationResult = null;
}
formatCurrency(amount: number): string {
if (amount == null) return '-';
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(
amount
);
}
}
@@ -0,0 +1,178 @@
<!--
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="report-page">
<div class="page-header">
<h2>{{ 'labels.text.FMR Submission' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Submit Fraud Monitoring Return to RBI FUA System' | translate }}</p>
</div>
<div class="report-layout">
<mat-card class="report-card wide-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="university" class="mr-05"></fa-icon>
{{ 'labels.text.Fraud Monitoring Return (FMR)' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (result) {
<div class="success-result">
<div class="success-icon"><fa-icon icon="check-circle" size="3x"></fa-icon></div>
<h3>{{ 'labels.text.FMR Submitted Successfully' | translate }}</h3>
<div class="result-details">
<div class="result-row">
<span class="result-label">{{ 'labels.text.FMR Reference' | translate }}</span
><span class="result-value mono">{{ result.fmrReferenceNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Bank IFSC' | translate }}</span
><span class="result-value mono">{{ result.reportingBankIfsc }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Fraud Category' | translate }}</span
><span class="result-value">{{ result.fraudCategory }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Fraud Amount' | translate }}</span
><span class="result-value">&#8377;{{ result.fraudAmount?.toLocaleString() }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Status' | translate }}</span
><span class="result-value status-submitted">{{ result.status }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Submitted At' | translate }}</span
><span class="result-value">{{ result.submittedAt }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Next FMR Due Date' | translate }}</span
><span class="result-value">{{ result.nextDueDate }}</span>
</div>
</div>
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New FMR' | translate }}
</button>
</div>
} @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Reporting Bank IFSC' | translate }}</mat-label>
<input matInput formControlName="reportingBankIfsc" readonly />
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Reporting Bank Name' | translate }}</mat-label>
<input matInput formControlName="reportingBankName" readonly />
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Fraud Category' | translate }}</mat-label>
<mat-select formControlName="fraudCategory">
@for (cat of fraudCategories; track cat) {
<mat-option [value]="cat">{{ cat }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Sub-Category' | translate }}</mat-label>
<mat-select formControlName="fraudSubCategory">
@for (sub of fraudSubCategories; track sub) {
<mat-option [value]="sub">{{ sub }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Fraud Amount (INR)' | translate }}</mat-label>
<input matInput type="number" formControlName="fraudAmount" placeholder="0.00" />
</mat-form-field>
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Account Number' | translate }}</mat-label>
<input matInput formControlName="accountNumber" />
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Account Holder Name' | translate }}</mat-label>
<input matInput formControlName="accountHolderName" />
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Date of Detection' | translate }}</mat-label>
<input matInput [matDatepicker]="detPicker" formControlName="dateOfDetection" />
<mat-datepicker-toggle matSuffix [for]="detPicker"></mat-datepicker-toggle>
<mat-datepicker #detPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Date of Fraud' | translate }}</mat-label>
<input matInput [matDatepicker]="fraudPicker" formControlName="dateOfFraud" />
<mat-datepicker-toggle matSuffix [for]="fraudPicker"></mat-datepicker-toggle>
<mat-datepicker #fraudPicker></mat-datepicker>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Modus Operandi' | translate }}</mat-label>
<textarea
matInput
formControlName="modus"
rows="3"
placeholder="Describe the method used to commit fraud..."
></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Action Taken' | translate }}</mat-label>
<mat-select formControlName="actionTaken">
@for (action of actionsTaken; track action) {
<mat-option [value]="action">{{ action }}</mat-option>
}
</mat-select>
</mat-form-field>
<div class="checkbox-row">
<mat-checkbox formControlName="staffInvolved" color="warn">{{
'labels.text.Staff Involved in Fraud' | translate
}}</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Remarks' | translate }}</mat-label>
<textarea
matInput
formControlName="remarks"
rows="2"
placeholder="{{ 'labels.text.Enter remarks...' | translate }}"
></textarea>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit to RBI' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,108 @@
/**
* 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/.
*/
.report-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;
}
}
.report-layout {
display: flex;
gap: 24px;
}
.report-card {
flex: 1;
max-width: 700px;
}
.wide-card {
max-width: 900px;
}
.full-width {
width: 100%;
}
.half-width {
flex: 1;
}
.form-row {
display: flex;
gap: 16px;
}
.mr-05 {
margin-right: 0.5rem;
}
.mt-16 {
margin-top: 16px;
}
.submit-btn {
margin-top: 8px;
height: 48px;
font-size: 16px;
}
.inline-spinner {
display: inline-block;
margin-right: 8px;
}
.checkbox-row {
margin: 8px 0 16px;
}
.success-result {
text-align: center;
padding: 32px 16px;
.success-icon {
color: #10b981;
margin-bottom: 16px;
}
h3 {
color: #10b981;
margin-bottom: 24px;
}
}
.result-details {
text-align: left;
max-width: 500px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.result-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
}
.result-label {
color: #666;
font-weight: 500;
}
.result-value {
font-weight: 600;
}
.mono {
font-family: 'Roboto Mono', monospace;
}
.status-submitted {
color: #1e88e5;
font-weight: 700;
}
@@ -0,0 +1,192 @@
/**
* 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 { ReactiveFormsModule, FormBuilder, FormGroup, Validators } 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 { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'cfcfrms-fmr-submission',
standalone: true,
templateUrl: './fmr-submission.component.html',
styleUrls: ['./fmr-submission.component.scss'],
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
MatDatepickerModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDialogModule,
FaIconComponent,
TranslateModule
]
})
export class FmrSubmissionComponent {
private fb = inject(FormBuilder);
private service = inject(CfcfrmsService);
private snackBar = inject(MatSnackBar);
private dialog = inject(MatDialog);
form: FormGroup = this.fb.group({
reportingBankIfsc: [
'LINC0000001',
Validators.required
],
reportingBankName: [
'LincPay Digital Bank',
Validators.required
],
fraudCategory: [
'',
Validators.required
],
fraudSubCategory: [
'',
Validators.required
],
fraudAmount: [
null,
[
Validators.required,
Validators.min(1)
]
],
accountNumber: [
'',
Validators.required
],
accountHolderName: [
'',
Validators.required
],
dateOfDetection: [
'',
Validators.required
],
dateOfFraud: [
'',
Validators.required
],
modus: [
'',
Validators.required
],
actionTaken: [
'',
Validators.required
],
staffInvolved: [false],
remarks: ['']
});
isSubmitting = false;
result: any = null;
fraudCategories = [
'Advance Fee Fraud',
'Card / Internet Fraud',
'Cheque / DD Fraud',
'Deposit Related Fraud',
'Forex Fraud',
'KYC Related Fraud',
'Loan Related Fraud',
'Misappropriation / Criminal Breach of Trust',
'Cyber Fraud',
'Other'
];
fraudSubCategories = [
'Phishing / Vishing',
'UPI Fraud',
'Internet Banking Fraud',
'SIM Swap',
'QR Code Scam',
'OTP Fraud',
'Account Takeover',
'Identity Theft',
'Social Engineering',
'Other'
];
actionsTaken = [
'FIR filed with local police',
'Complaint filed with Cyber Cell',
'Internal investigation initiated',
'Account frozen / debited',
'Funds recovered partially',
'Funds recovered fully',
'Reported to I4C/CFCFRMS',
'No action (under review)'
];
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
title: 'Confirm FMR Submission',
message: 'Submit this Fraud Monitoring Return to RBI FUA system? This is a regulatory submission.'
}
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (!confirmed) return;
this.isSubmitting = true;
const val = this.form.value;
const payload = {
...val,
dateOfDetection: new Date(val.dateOfDetection).toISOString().split('T')[0],
dateOfFraud: new Date(val.dateOfFraud).toISOString().split('T')[0]
};
this.service.submitFmr(payload).subscribe({
next: (res) => {
this.result = res;
this.isSubmitting = false;
this.snackBar.open('FMR submitted to RBI successfully', 'OK', { duration: 4000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to submit FMR', 'OK', { duration: 4000 });
}
});
});
}
resetForm(): void {
this.form.reset({
reportingBankIfsc: 'LINC0000001',
reportingBankName: 'LincPay Digital Bank',
staffInvolved: false
});
this.result = null;
}
}
+849
View File
@@ -0,0 +1,849 @@
/**
* 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 { Complaint, ComplaintStatus, DashboardStats, TimelineEvent } from '../models/cfcfrms.models';
// ─── Helper: generate dates relative to now ───
function daysAgo(n: number): string {
const d = new Date();
d.setDate(d.getDate() - n);
return d.toISOString();
}
function hoursAgo(n: number): string {
const d = new Date();
d.setHours(d.getHours() - n);
return d.toISOString();
}
// ─── 20 Realistic Dummy Complaints ───
export const MOCK_COMPLAINTS: Complaint[] = [
{
ticketId: 'CFCFRMS-2024-00001',
complainantName: 'Rajesh Kumar Sharma',
complainantMobile: '9876543210',
complainantEmail: 'rajesh.sharma@gmail.com',
complainantAddress: '42, MG Road, Sector 18, Noida, UP 201301',
fraudAmount: 250000,
fraudType: 'UPI_FRAUD',
fraudDescription:
'Received fake KYC update link via SMS. After clicking, ₹2.5L was debited via multiple UPI transactions within 10 minutes.',
suspectAccountNumber: '50100268347291',
suspectAccountName: 'Vikram Enterprises',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240115-00981',
status: 'RECEIVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: '',
createdAt: daysAgo(0),
updatedAt: daysAgo(0)
},
{
ticketId: 'CFCFRMS-2024-00002',
complainantName: 'Priya Nair',
complainantMobile: '8765432109',
complainantEmail: 'priya.nair@yahoo.com',
complainantAddress: '15, Anna Salai, T. Nagar, Chennai, TN 600017',
fraudAmount: 487500,
fraudType: 'OLX_FRAUD',
fraudDescription:
'Victim was selling a sofa on OLX. Scammer sent a QR code claiming it was for payment. Scanning the QR debited ₹4,87,500.',
suspectAccountNumber: '50100399281746',
suspectAccountName: 'Deepak Trading Co',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240115-01102',
status: 'UNDER_REVIEW',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 312000,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: 'Officer Meena K.',
createdAt: daysAgo(1),
updatedAt: hoursAgo(6)
},
{
ticketId: 'CFCFRMS-2024-00003',
complainantName: 'Amit Patel',
complainantMobile: '7654321098',
complainantEmail: 'amit.patel@outlook.com',
complainantAddress: '78, SG Highway, Bodakdev, Ahmedabad, GJ 380054',
fraudAmount: 150000,
fraudType: 'LOAN_APP_FRAUD',
fraudDescription:
'Downloaded fake loan app from link shared on WhatsApp. App collected bank details and debited ₹1.5L in two transactions.',
suspectAccountNumber: '50100412738956',
suspectAccountName: 'QuickCash Solutions',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'MHA_PORTAL',
acknowledgementNumber: 'ACK-MHA-20240114-00437',
status: 'LIEN_MARKED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 89000,
fundsHeld: 150000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Lien placed on suspect account. Balance sufficient to cover fraud amount.',
assignedOfficer: 'Officer Suresh R.',
createdAt: daysAgo(3),
updatedAt: daysAgo(1)
},
{
ticketId: 'CFCFRMS-2024-00004',
complainantName: 'Sunita Devi',
complainantMobile: '6543210987',
complainantEmail: '',
complainantAddress: 'Village Rampur, Block Sadar, Varanasi, UP 221002',
fraudAmount: 75000,
fraudType: 'CUSTOMER_CARE_FRAUD',
fraudDescription:
'Called fake customer care number from Google. Scammer installed AnyDesk on victim phone and transferred ₹75,000.',
suspectAccountNumber: '50100528463190',
suspectAccountName: 'Rahul Verma',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240113-00215',
status: 'DEBIT_FROZEN',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 42000,
fundsHeld: 75000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Debit freeze applied. Full fraud amount held. Account holder notified via registered mobile.',
assignedOfficer: 'Officer Suresh R.',
createdAt: daysAgo(5),
updatedAt: daysAgo(2)
},
{
ticketId: 'CFCFRMS-2024-00005',
complainantName: 'Mohammed Irfan',
complainantMobile: '9988776655',
complainantEmail: 'irfan.m@gmail.com',
complainantAddress: '23, Charminar Road, Hyderabad, TS 500002',
fraudAmount: 890000,
fraudType: 'INVESTMENT_FRAUD',
fraudDescription:
'Lured into fake stock trading app via Telegram group. Invested ₹8.9L over two weeks. Unable to withdraw funds.',
suspectAccountNumber: '50100637291845',
suspectAccountName: 'Global Trade Hub',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240112-00876',
status: 'FUNDS_MOVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 12500,
fundsHeld: 0,
nextHopAccountNumber: '91820034567812',
nextHopBankIfsc: 'SBIN0001234',
actionNote: 'Only ₹12,500 available. Majority funds moved to SBI account. Escalating to SBI.',
assignedOfficer: 'Officer Meena K.',
createdAt: daysAgo(8),
updatedAt: daysAgo(4)
},
{
ticketId: 'CFCFRMS-2024-00006',
complainantName: 'Kavitha Reddy',
complainantMobile: '8877665544',
complainantEmail: 'kavitha.r@hotmail.com',
complainantAddress: '56, Indiranagar, Bangalore, KA 560038',
fraudAmount: 320000,
fraudType: 'PART_TIME_JOB_FRAUD',
fraudDescription:
'Scammed via Telegram part-time job offer. Made initial ₹5,000 "investment" then lured into paying ₹3.2L as "tax" and "processing fee".',
suspectAccountNumber: '50100745829361',
suspectAccountName: 'EarnMore Digital Services',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240110-00543',
status: 'RESOLVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Full amount recovered and refunded to complainant via NEFT. Case closed.',
assignedOfficer: 'Officer Anita S.',
createdAt: daysAgo(15),
updatedAt: daysAgo(5)
},
{
ticketId: 'CFCFRMS-2024-00007',
complainantName: 'Arvind Singh',
complainantMobile: '7766554433',
complainantEmail: 'arvind.singh@gmail.com',
complainantAddress: '12, Civil Lines, Jaipur, RJ 302006',
fraudAmount: 45000,
fraudType: 'UPI_FRAUD',
fraudDescription:
'Received call from person posing as bank official. Tricked into sharing OTP. ₹45,000 debited via UPI.',
suspectAccountNumber: '50100854917263',
suspectAccountName: 'Sanjay Meena',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'MHA_PORTAL',
acknowledgementNumber: 'ACK-MHA-20240109-00129',
status: 'REJECTED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Suspect account does not belong to LincPay. IFSC mismatch — ticket redirected to correct bank (PNB).',
assignedOfficer: 'Officer Suresh R.',
createdAt: daysAgo(18),
updatedAt: daysAgo(16)
},
{
ticketId: 'CFCFRMS-2024-00008',
complainantName: 'Lakshmi Iyer',
complainantMobile: '9654321076',
complainantEmail: 'lakshmi.iyer@gmail.com',
complainantAddress: '34, Marine Drive, Kochi, KL 682031',
fraudAmount: 560000,
fraudType: 'MATRIMONY_FRAUD',
fraudDescription:
'Met scammer on matrimony site. Paid ₹5.6L over 2 months for "visa processing" and "medical emergency".',
suspectAccountNumber: '50100963845712',
suspectAccountName: 'Premium Match Services',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240108-00321',
status: 'LIEN_MARKED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 234000,
fundsHeld: 560000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Lien marked. Sufficient balance to cover full fraud amount.',
assignedOfficer: 'Officer Anita S.',
createdAt: daysAgo(10),
updatedAt: daysAgo(7)
},
{
ticketId: 'CFCFRMS-2024-00009',
complainantName: 'Rajan Tiwari',
complainantMobile: '8543219876',
complainantEmail: 'rajan.t@yahoo.com',
complainantAddress: '67, Boring Road, Patna, BR 800001',
fraudAmount: 198000,
fraudType: 'ELECTRICITY_BILL_FRAUD',
fraudDescription:
'Received SMS about pending electricity bill with threat of disconnection. Clicked link and ₹1.98L was stolen via screen-sharing app.',
suspectAccountNumber: '50100172839465',
suspectAccountName: 'Power Solutions India',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240107-00654',
status: 'UNDER_REVIEW',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 56000,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: 'Officer Meena K.',
createdAt: daysAgo(2),
updatedAt: daysAgo(1)
},
{
ticketId: 'CFCFRMS-2024-00010',
complainantName: 'Deepa Nayak',
complainantMobile: '7432108765',
complainantEmail: 'deepa.n@gmail.com',
complainantAddress: '89, Panjim, Goa 403001',
fraudAmount: 125000,
fraudType: 'COURIER_FRAUD',
fraudDescription:
'Fake FedEx call claiming parcel with contraband. Transferred to fake "cyber cell officer" who demanded ₹1.25L as "security deposit".',
suspectAccountNumber: '50100281934567',
suspectAccountName: 'SecurePay Hub',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'MHA_PORTAL',
acknowledgementNumber: 'ACK-MHA-20240106-00087',
status: 'RECEIVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: '',
createdAt: daysAgo(1),
updatedAt: daysAgo(1)
},
{
ticketId: 'CFCFRMS-2024-00011',
complainantName: 'Suresh Babu',
complainantMobile: '9321654870',
complainantEmail: 'suresh.b@gmail.com',
complainantAddress: '22, Gandhi Nagar, Vijayawada, AP 520003',
fraudAmount: 1500000,
fraudType: 'INVESTMENT_FRAUD',
fraudDescription:
'Joined fake crypto trading group on Telegram. Deposited ₹15L over 3 months. Website is now down, funds lost.',
suspectAccountNumber: '50100391726845',
suspectAccountName: 'CryptoVault Exchange',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240105-01234',
status: 'DEBIT_FROZEN',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 340000,
fundsHeld: 340000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Only ₹3.4L available in account. Rest already moved. Debit frozen on available balance.',
assignedOfficer: 'Officer Suresh R.',
createdAt: daysAgo(6),
updatedAt: daysAgo(3)
},
{
ticketId: 'CFCFRMS-2024-00012',
complainantName: 'Ananya Gupta',
complainantMobile: '8210543976',
complainantEmail: 'ananya.g@outlook.com',
complainantAddress: '45, Connaught Place, New Delhi 110001',
fraudAmount: 95000,
fraudType: 'OLX_FRAUD',
fraudDescription:
'Tried to buy an iPhone on OLX. Seller sent fake payment link. ₹95,000 debited upon entering card details.',
suspectAccountNumber: '50100402819375',
suspectAccountName: 'FastDeal Online',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240104-00789',
status: 'RESOLVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Funds recovered and refunded to complainant.',
assignedOfficer: 'Officer Anita S.',
createdAt: daysAgo(20),
updatedAt: daysAgo(10)
},
{
ticketId: 'CFCFRMS-2024-00013',
complainantName: 'Vikas Joshi',
complainantMobile: '9109876543',
complainantEmail: 'vikas.j@gmail.com',
complainantAddress: '78, FC Road, Pune, MH 411004',
fraudAmount: 67000,
fraudType: 'UPI_FRAUD',
fraudDescription:
'Received collect request on PhonePe with message "Refund of ₹67,000". Approved the collect request and lost funds.',
suspectAccountNumber: '50100513928476',
suspectAccountName: 'Mohan Kumar',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240103-00456',
status: 'RECEIVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: '',
createdAt: hoursAgo(4),
updatedAt: hoursAgo(4)
},
{
ticketId: 'CFCFRMS-2024-00014',
complainantName: 'Geeta Menon',
complainantMobile: '8098765432',
complainantEmail: 'geeta.menon@gmail.com',
complainantAddress: '90, MG Road, Thrissur, KL 680001',
fraudAmount: 430000,
fraudType: 'LOAN_APP_FRAUD',
fraudDescription:
'Downloaded "InstaCash" loan app. After providing bank details, ₹4.3L was transferred out in 5 transactions within 30 minutes.',
suspectAccountNumber: '50100624738291',
suspectAccountName: 'InstaCash Finance',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240102-00567',
status: 'LIEN_MARKED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 178000,
fundsHeld: 430000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Full fraud amount covered by lien. Investigation ongoing.',
assignedOfficer: 'Officer Meena K.',
createdAt: daysAgo(7),
updatedAt: daysAgo(4)
},
{
ticketId: 'CFCFRMS-2024-00015',
complainantName: 'Pankaj Mishra',
complainantMobile: '7987654321',
complainantEmail: 'pankaj.m@gmail.com',
complainantAddress: '11, Hazratganj, Lucknow, UP 226001',
fraudAmount: 210000,
fraudType: 'SEXTORTION',
fraudDescription:
'Victim blackmailed after video call on dating app. Paid ₹2.1L across 4 transactions to prevent video leak.',
suspectAccountNumber: '50100735829461',
suspectAccountName: 'Nikhil Yadav',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20240101-00890',
status: 'FUNDS_MOVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 5000,
fundsHeld: 0,
nextHopAccountNumber: '40120098765432',
nextHopBankIfsc: 'HDFC0001234',
actionNote: 'Funds already moved to HDFC account. Escalated.',
assignedOfficer: 'Officer Suresh R.',
createdAt: daysAgo(12),
updatedAt: daysAgo(8)
},
{
ticketId: 'CFCFRMS-2024-00016',
complainantName: 'Rekha Pandey',
complainantMobile: '6876543210',
complainantEmail: 'rekha.p@yahoo.com',
complainantAddress: '33, Lalbagh Road, Mangalore, KA 575001',
fraudAmount: 38000,
fraudType: 'CUSTOMER_CARE_FRAUD',
fraudDescription:
'Googled customer care for Paytm. Called fake number. Agent installed TeamViewer and debited ₹38,000.',
suspectAccountNumber: '50100846729153',
suspectAccountName: 'Digital Pay Services',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'MHA_PORTAL',
acknowledgementNumber: 'ACK-MHA-20240101-00044',
status: 'UNDER_REVIEW',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 18500,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: 'Officer Anita S.',
createdAt: daysAgo(2),
updatedAt: hoursAgo(12)
},
{
ticketId: 'CFCFRMS-2024-00017',
complainantName: 'Dinesh Choudhary',
complainantMobile: '9765432180',
complainantEmail: 'dinesh.c@gmail.com',
complainantAddress: '55, Ashram Road, Ahmedabad, GJ 380009',
fraudAmount: 780000,
fraudType: 'INVESTMENT_FRAUD',
fraudDescription:
'Invested in fake forex trading platform "ForexPro". Platform blocked withdrawal after total deposit of ₹7.8L.',
suspectAccountNumber: '50100957382614',
suspectAccountName: 'ForexPro Trading LLP',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20241230-01567',
status: 'DEBIT_FROZEN',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 520000,
fundsHeld: 520000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Partial amount frozen. ₹2.6L already transferred out.',
assignedOfficer: 'Officer Meena K.',
createdAt: daysAgo(9),
updatedAt: daysAgo(5)
},
{
ticketId: 'CFCFRMS-2024-00018',
complainantName: 'Fatima Begum',
complainantMobile: '8654321097',
complainantEmail: '',
complainantAddress: '12, Chowk Bazaar, Bhopal, MP 462001',
fraudAmount: 55000,
fraudType: 'ELECTRICITY_BILL_FRAUD',
fraudDescription:
'Received WhatsApp message about electricity bill overdue. Called the number provided. Agent made victim install AnyDesk. ₹55,000 debited.',
suspectAccountNumber: '50100168293475',
suspectAccountName: 'Ravi Shankar',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20241229-00234',
status: 'RESOLVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Funds recovered from suspect account and refunded.',
assignedOfficer: 'Officer Suresh R.',
createdAt: daysAgo(25),
updatedAt: daysAgo(15)
},
{
ticketId: 'CFCFRMS-2024-00019',
complainantName: 'Manoj Kumar',
complainantMobile: '9543210876',
complainantEmail: 'manoj.k@gmail.com',
complainantAddress: '67, Park Street, Kolkata, WB 700016',
fraudAmount: 175000,
fraudType: 'PART_TIME_JOB_FRAUD',
fraudDescription:
'Joined "Amazon Review" job scam on Telegram. Initially earned small amounts, then lost ₹1.75L as "upgrade fees".',
suspectAccountNumber: '50100279384651',
suspectAccountName: 'SmartWork Hub',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20241228-00901',
status: 'LIEN_MARKED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 98000,
fundsHeld: 175000,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: 'Lien marked successfully. Full amount available.',
assignedOfficer: 'Officer Anita S.',
createdAt: daysAgo(4),
updatedAt: daysAgo(2)
},
{
ticketId: 'CFCFRMS-2024-00020',
complainantName: 'Shreya Das',
complainantMobile: '8432109765',
complainantEmail: 'shreya.d@gmail.com',
complainantAddress: '101, Bandra West, Mumbai, MH 400050',
fraudAmount: 2100000,
fraudType: 'INVESTMENT_FRAUD',
fraudDescription:
'Invested ₹21L in fake IPO allotment scheme promoted via Instagram ads. Payment split across 3 accounts.',
suspectAccountNumber: '50100388274916',
suspectAccountName: 'IPO Goldmine Pvt Ltd',
suspectBankIfsc: 'LINC0000001',
suspectBankName: 'LincPay Digital Bank',
sourcePortal: 'I4C_CFCFRMS',
acknowledgementNumber: 'ACK-I4C-20241227-01789',
status: 'RECEIVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: '',
createdAt: hoursAgo(2),
updatedAt: hoursAgo(2)
}
];
// ─── Timeline Events keyed by ticketId ───
export const MOCK_TIMELINES: { [ticketId: string]: TimelineEvent[] } = {
'CFCFRMS-2024-00001': [
{
id: 1,
ticketId: 'CFCFRMS-2024-00001',
eventType: 'COMPLAINT_RECEIVED',
eventDescription: 'Complaint received from I4C CFCFRMS portal.',
actor: 'System',
actorRole: 'SYSTEM',
timestamp: daysAgo(0),
metadata: {}
}
],
'CFCFRMS-2024-00002': [
{
id: 1,
ticketId: 'CFCFRMS-2024-00002',
eventType: 'COMPLAINT_RECEIVED',
eventDescription: 'Complaint received from I4C CFCFRMS portal.',
actor: 'System',
actorRole: 'SYSTEM',
timestamp: daysAgo(1),
metadata: {}
},
{
id: 2,
ticketId: 'CFCFRMS-2024-00002',
eventType: 'ASSIGNED',
eventDescription: 'Assigned to Officer Meena K. for review.',
actor: 'Admin',
actorRole: 'ADMIN',
timestamp: hoursAgo(18),
metadata: {}
},
{
id: 3,
ticketId: 'CFCFRMS-2024-00002',
eventType: 'UNDER_REVIEW',
eventDescription: 'Investigation initiated. Checking suspect account transaction history.',
actor: 'Officer Meena K.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: hoursAgo(6),
metadata: {}
}
],
'CFCFRMS-2024-00003': [
{
id: 1,
ticketId: 'CFCFRMS-2024-00003',
eventType: 'COMPLAINT_RECEIVED',
eventDescription: 'Complaint received from MHA Portal.',
actor: 'System',
actorRole: 'SYSTEM',
timestamp: daysAgo(3),
metadata: {}
},
{
id: 2,
ticketId: 'CFCFRMS-2024-00003',
eventType: 'ASSIGNED',
eventDescription: 'Assigned to Officer Suresh R.',
actor: 'Admin',
actorRole: 'ADMIN',
timestamp: daysAgo(3),
metadata: {}
},
{
id: 3,
ticketId: 'CFCFRMS-2024-00003',
eventType: 'UNDER_REVIEW',
eventDescription: 'Account details verified. ₹1.5L fraud amount confirmed.',
actor: 'Officer Suresh R.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(2),
metadata: {}
},
{
id: 4,
ticketId: 'CFCFRMS-2024-00003',
eventType: 'LIEN_MARKED',
eventDescription: 'Lien placed on suspect account. Balance ₹2,39,000. Full fraud amount secured.',
actor: 'Officer Suresh R.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(1),
metadata: { availableBalance: 89000, fundsHeld: 150000 }
}
],
'CFCFRMS-2024-00004': [
{
id: 1,
ticketId: 'CFCFRMS-2024-00004',
eventType: 'COMPLAINT_RECEIVED',
eventDescription: 'Complaint received from I4C CFCFRMS portal.',
actor: 'System',
actorRole: 'SYSTEM',
timestamp: daysAgo(5),
metadata: {}
},
{
id: 2,
ticketId: 'CFCFRMS-2024-00004',
eventType: 'ASSIGNED',
eventDescription: 'Assigned to Officer Suresh R.',
actor: 'Admin',
actorRole: 'ADMIN',
timestamp: daysAgo(5),
metadata: {}
},
{
id: 3,
ticketId: 'CFCFRMS-2024-00004',
eventType: 'LIEN_MARKED',
eventDescription: 'Lien placed on suspect account.',
actor: 'Officer Suresh R.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(3),
metadata: {}
},
{
id: 4,
ticketId: 'CFCFRMS-2024-00004',
eventType: 'DEBIT_FROZEN',
eventDescription: 'Debit freeze applied. Full ₹75,000 held. Account holder notified via registered mobile.',
actor: 'Officer Suresh R.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(2),
metadata: { availableBalance: 42000, fundsHeld: 75000 }
}
],
'CFCFRMS-2024-00005': [
{
id: 1,
ticketId: 'CFCFRMS-2024-00005',
eventType: 'COMPLAINT_RECEIVED',
eventDescription: 'Complaint received from I4C CFCFRMS portal.',
actor: 'System',
actorRole: 'SYSTEM',
timestamp: daysAgo(8),
metadata: {}
},
{
id: 2,
ticketId: 'CFCFRMS-2024-00005',
eventType: 'ASSIGNED',
eventDescription: 'Assigned to Officer Meena K.',
actor: 'Admin',
actorRole: 'ADMIN',
timestamp: daysAgo(8),
metadata: {}
},
{
id: 3,
ticketId: 'CFCFRMS-2024-00005',
eventType: 'LIEN_MARKED',
eventDescription: 'Only ₹12,500 available in account.',
actor: 'Officer Meena K.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(6),
metadata: {}
},
{
id: 4,
ticketId: 'CFCFRMS-2024-00005',
eventType: 'DEBIT_FROZEN',
eventDescription: 'Account debit frozen.',
actor: 'Officer Meena K.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(5),
metadata: {}
},
{
id: 5,
ticketId: 'CFCFRMS-2024-00005',
eventType: 'FUNDS_MOVED',
eventDescription: 'Majority of funds already moved to SBI account 91820034567812. Escalation initiated.',
actor: 'Officer Meena K.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(4),
metadata: { nextHopAccountNumber: '91820034567812', nextHopBankIfsc: 'SBIN0001234' }
}
],
'CFCFRMS-2024-00006': [
{
id: 1,
ticketId: 'CFCFRMS-2024-00006',
eventType: 'COMPLAINT_RECEIVED',
eventDescription: 'Complaint received from I4C CFCFRMS portal.',
actor: 'System',
actorRole: 'SYSTEM',
timestamp: daysAgo(15),
metadata: {}
},
{
id: 2,
ticketId: 'CFCFRMS-2024-00006',
eventType: 'ASSIGNED',
eventDescription: 'Assigned to Officer Anita S.',
actor: 'Admin',
actorRole: 'ADMIN',
timestamp: daysAgo(15),
metadata: {}
},
{
id: 3,
ticketId: 'CFCFRMS-2024-00006',
eventType: 'LIEN_MARKED',
eventDescription: 'Full amount liened.',
actor: 'Officer Anita S.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(13),
metadata: {}
},
{
id: 4,
ticketId: 'CFCFRMS-2024-00006',
eventType: 'DEBIT_FROZEN',
eventDescription: 'Debit frozen on suspect account.',
actor: 'Officer Anita S.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(10),
metadata: {}
},
{
id: 5,
ticketId: 'CFCFRMS-2024-00006',
eventType: 'RESOLVED',
eventDescription: 'Full amount ₹3,20,000 recovered and refunded to complainant via NEFT. Case closed.',
actor: 'Officer Anita S.',
actorRole: 'COMPLIANCE_OFFICER',
timestamp: daysAgo(5),
metadata: {}
}
]
};
// ─── Dashboard Stats (computed from mock data) ───
export function computeMockStats(): DashboardStats {
const statusDist: { [key: string]: number } = {};
const amountDist: { [key: string]: number } = {};
let pendingReview = 0;
let lienMarked = 0;
let fundsRecovered = 0;
MOCK_COMPLAINTS.forEach((c) => {
statusDist[c.status] = (statusDist[c.status] || 0) + 1;
amountDist[c.status] = (amountDist[c.status] || 0) + c.fraudAmount;
if (c.status === 'RECEIVED' || c.status === 'UNDER_REVIEW') pendingReview++;
if (c.status === 'LIEN_MARKED') lienMarked++;
if (c.status === 'RESOLVED') fundsRecovered += c.fraudAmount;
});
return {
totalComplaints: MOCK_COMPLAINTS.length,
pendingReview,
lienMarked,
fundsRecovered,
statusDistribution: statusDist as any,
amountByStatus: amountDist as any
};
}
@@ -0,0 +1,618 @@
/**
* 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 { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { MOCK_COMPLAINTS, MOCK_TIMELINES, computeMockStats } from './cfcfrms-mock-data';
import { Complaint, ComplaintStatus, TimelineEvent, EscalationResponse, PagedResponse } from '../models/cfcfrms.models';
// ─── Mutable in-memory state (survives during session) ───
const complaints = [...MOCK_COMPLAINTS];
const timelines: { [key: string]: TimelineEvent[] } = JSON.parse(JSON.stringify(MOCK_TIMELINES));
let nextEventId = 1000;
let nextTicketSeq = 21;
function now(): string {
return new Date().toISOString();
}
function genTicketId(): string {
const seq = String(nextTicketSeq++).padStart(5, '0');
return `CFCFRMS-2024-${seq}`;
}
function genRefNumber(): string {
return `ESC-${Date.now()}-${Math.floor(Math.random() * 9000 + 1000)}`;
}
function addTimeline(
ticketId: string,
eventType: string,
description: string,
actor: string,
actorRole: string,
meta: any = {}
): void {
if (!timelines[ticketId]) timelines[ticketId] = [];
timelines[ticketId].push({
id: ++nextEventId,
ticketId,
eventType,
eventDescription: description,
actor,
actorRole,
timestamp: now(),
metadata: meta
});
}
// ─── Bank-side handlers ───
function handleGetComplaints(url: URL): PagedResponse<Complaint> {
const page = parseInt(url.searchParams.get('page') || '0', 10);
const size = parseInt(url.searchParams.get('size') || '10', 10);
const status = url.searchParams.get('status') || '';
const search = (url.searchParams.get('search') || '').toLowerCase();
const dateFrom = url.searchParams.get('dateFrom') || '';
const dateTo = url.searchParams.get('dateTo') || '';
let filtered = [...complaints];
if (status) {
filtered = filtered.filter((c) => c.status === status);
}
if (search) {
filtered = filtered.filter(
(c) =>
c.ticketId.toLowerCase().includes(search) ||
c.complainantMobile.includes(search) ||
c.complainantName.toLowerCase().includes(search)
);
}
if (dateFrom) {
const from = new Date(dateFrom);
filtered = filtered.filter((c) => new Date(c.createdAt) >= from);
}
if (dateTo) {
const to = new Date(dateTo);
to.setHours(23, 59, 59, 999);
filtered = filtered.filter((c) => new Date(c.createdAt) <= to);
}
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const totalElements = filtered.length;
const totalPages = Math.ceil(totalElements / size);
const start = page * size;
const content = filtered.slice(start, start + size);
return { content, totalElements, totalPages, size, number: page, first: page === 0, last: page >= totalPages - 1 };
}
function handleGetComplaintById(ticketId: string): Complaint | null {
return complaints.find((c) => c.ticketId === ticketId) || null;
}
function handleSubmitAction(ticketId: string, body: any): Complaint | null {
const idx = complaints.findIndex((c) => c.ticketId === ticketId);
if (idx === -1) return null;
const complaint = complaints[idx];
complaint.status = body.action as ComplaintStatus;
complaint.actionNote = body.actionNote || complaint.actionNote;
complaint.updatedAt = now();
if (body.availableBalance != null) complaint.availableBalance = body.availableBalance;
if (body.fundsHeld != null) complaint.fundsHeld = body.fundsHeld;
if (body.nextHopAccountNumber) complaint.nextHopAccountNumber = body.nextHopAccountNumber;
if (body.nextHopBankIfsc) complaint.nextHopBankIfsc = body.nextHopBankIfsc;
addTimeline(
ticketId,
body.action,
body.actionNote || `Status changed to ${body.action}`,
'Current User',
'COMPLIANCE_OFFICER',
{
availableBalance: body.availableBalance,
fundsHeld: body.fundsHeld,
nextHopAccountNumber: body.nextHopAccountNumber,
nextHopBankIfsc: body.nextHopBankIfsc
}
);
return complaint;
}
function handleGetTimeline(ticketId: string): TimelineEvent[] {
return (timelines[ticketId] || []).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
function handleEscalate(ticketId: string, body: any): EscalationResponse | null {
const complaint = complaints.find((c) => c.ticketId === ticketId);
if (!complaint) return null;
complaint.nextHopAccountNumber = body.nextHopAccountNumber;
complaint.nextHopBankIfsc = body.nextHopBankIfsc;
complaint.status = 'FUNDS_MOVED';
complaint.updatedAt = now();
addTimeline(
ticketId,
'ESCALATED',
`Escalated to bank ${body.nextHopBankIfsc}. Remarks: ${body.remarks}`,
'Current User',
'COMPLIANCE_OFFICER',
body
);
return {
ticketId,
referenceNumber: genRefNumber(),
escalatedTo: body.nextHopBankIfsc,
escalatedBankIfsc: body.nextHopBankIfsc,
status: 'ESCALATED',
timestamp: now()
};
}
function handleCreateComplaint(body: any): Complaint {
const ticket: Complaint = {
ticketId: genTicketId(),
complainantName: body.complainantName || 'Unknown',
complainantMobile: body.complainantMobile || '',
complainantEmail: body.complainantEmail || '',
complainantAddress: body.complainantAddress || '',
fraudAmount: body.fraudAmount || 0,
fraudType: body.fraudType || 'UNKNOWN',
fraudDescription: body.fraudDescription || '',
suspectAccountNumber: body.suspectAccountNumber || '',
suspectAccountName: body.suspectAccountName || '',
suspectBankIfsc: body.suspectBankIfsc || 'LINC0000001',
suspectBankName: body.suspectBankName || 'LincPay Digital Bank',
sourcePortal: body.sourcePortal || 'I4C_CFCFRMS',
acknowledgementNumber: body.acknowledgementNumber || `ACK-${Date.now()}`,
status: 'RECEIVED',
currentBankIfsc: 'LINC0000001',
currentBankName: 'LincPay Digital Bank',
availableBalance: 0,
fundsHeld: 0,
nextHopAccountNumber: '',
nextHopBankIfsc: '',
actionNote: '',
assignedOfficer: '',
createdAt: now(),
updatedAt: now()
};
complaints.unshift(ticket);
addTimeline(ticket.ticketId, 'COMPLAINT_RECEIVED', 'Complaint registered.', 'System', 'SYSTEM');
return ticket;
}
// ─── Simulator handlers ───
function handleSimPushComplaint(body: any): any {
const ticket = handleCreateComplaint(body);
return { success: true, message: 'Complaint pushed to bank', ticketId: ticket.ticketId, status: ticket.status };
}
function handleSimPushHoldDirective(body: any): any {
const ticketId = body.ticketId || body.referenceNumber;
const complaint = complaints.find((c) => c.ticketId === ticketId);
if (!complaint) return { success: false, message: 'Ticket not found' };
complaint.status = 'DEBIT_FROZEN';
complaint.fundsHeld = body.holdAmount || complaint.fraudAmount;
complaint.updatedAt = now();
addTimeline(
ticketId,
'DEBIT_FROZEN',
`Urgent hold directive from I4C. Amount: ${complaint.fundsHeld}`,
'I4C System',
'REGULATOR'
);
return {
success: true,
message: 'Hold directive applied',
ticketId,
status: 'DEBIT_FROZEN',
fundsHeld: complaint.fundsHeld
};
}
function handleSimPushBulk(body: any): any {
const items = body.complaints || [];
const results = items.slice(0, 10).map((c: any) => handleSimPushComplaint(c));
return { success: true, totalPushed: results.length, results };
}
function handleSimReceiveBankUpdate(body: any): any {
const complaint = complaints.find((c) => c.ticketId === body.ticketId);
if (!complaint) return { success: false, message: 'Ticket not found' };
return {
success: true,
ticketId: body.ticketId,
currentStatus: complaint.status,
lastUpdated: complaint.updatedAt,
actionNote: complaint.actionNote
};
}
function handleSimReceiveEscalation(body: any): any {
const complaint = complaints.find((c) => c.ticketId === body.ticketId);
if (!complaint) return { success: false, message: 'Ticket not found' };
return {
success: true,
ticketId: body.ticketId,
escalatedTo: complaint.nextHopBankIfsc || 'N/A',
fundsHeld: complaint.fundsHeld,
currentStatus: complaint.status
};
}
function handleSimVerifyAccount(body: any): any {
const acctNo = body.accountNumber || '';
const match = complaints.find((c) => c.suspectAccountNumber === acctNo);
return {
accountNumber: acctNo,
exists: !!match,
accountName: match?.suspectAccountName || null,
bankIfsc: match?.suspectBankIfsc || null,
bankName: match?.suspectBankName || null,
isFlagged: !!match,
flagReason: match ? `Linked to CFCFRMS ticket ${match.ticketId}` : null
};
}
function handleSimVerifyTransaction(body: any): any {
return {
transactionId: body.transactionId || `TXN-${Date.now()}`,
accountNumber: body.accountNumber || '',
amount: body.amount || 0,
status: 'VERIFIED',
timestamp: now(),
riskScore: Math.floor(Math.random() * 40 + 60),
flagged: true,
remarks: 'Transaction linked to suspected fraud account.'
};
}
function handleSimComplaintStatus(ref: string): any {
const complaint = complaints.find((c) => c.ticketId === ref || c.acknowledgementNumber === ref);
if (!complaint) return { found: false, message: 'Complaint not found' };
return {
found: true,
ticketId: complaint.ticketId,
acknowledgementNumber: complaint.acknowledgementNumber,
status: complaint.status,
bankAction: complaint.actionNote || 'Pending',
lastUpdated: complaint.updatedAt,
assignedOfficer: complaint.assignedOfficer || 'Unassigned'
};
}
function handleSimBankPerformance(): any {
const total = complaints.length;
const resolved = complaints.filter((c) => c.status === 'RESOLVED').length;
const rejected = complaints.filter((c) => c.status === 'REJECTED').length;
const frozen = complaints.filter((c) => c.status === 'DEBIT_FROZEN' || c.status === 'LIEN_MARKED').length;
const pending = complaints.filter((c) => c.status === 'RECEIVED' || c.status === 'UNDER_REVIEW').length;
return {
bankIfsc: 'LINC0000001',
bankName: 'LincPay Digital Bank',
totalTickets: total,
resolvedCount: resolved,
rejectedCount: rejected,
frozenCount: frozen,
pendingCount: pending,
averageTATHours: 18.5,
complianceScore: 92.4,
responseRate: total > 0 ? Math.round(((total - pending) / total) * 100 * 10) / 10 : 0,
periodFrom: new Date(Date.now() - 30 * 86400000).toISOString().split('T')[0],
periodTo: new Date().toISOString().split('T')[0]
};
}
// ─── Bank Proactive Reporting handlers ───
// In-memory stores for reported items
const reportedSuspects: any[] = [];
const reportedMules: any[] = [];
const reportedFraudTxns: any[] = [];
const fmrSubmissions: any[] = [];
// Dummy CFR registry data
const cfrRegistry: { [acctNo: string]: any[] } = {
'50100268347291': [
{
fraudReferenceNumber: 'CFR-2024-00891',
fraudCategory: 'UPI_FRAUD',
fraudAmount: 250000,
reportingBank: 'LincPay Digital Bank',
reportingBankIfsc: 'LINC0000001',
dateReported: '2024-01-10',
status: 'CONFIRMED',
remarks: 'Account involved in multiple UPI fraud complaints.'
}
],
'50100637291845': [
{
fraudReferenceNumber: 'CFR-2024-01234',
fraudCategory: 'INVESTMENT_FRAUD',
fraudAmount: 890000,
reportingBank: 'LincPay Digital Bank',
reportingBankIfsc: 'LINC0000001',
dateReported: '2024-01-08',
status: 'CONFIRMED',
remarks: 'Mule account used for crypto investment scam.'
},
{
fraudReferenceNumber: 'CFR-2024-01235',
fraudCategory: 'INVESTMENT_FRAUD',
fraudAmount: 450000,
reportingBank: 'State Bank of India',
reportingBankIfsc: 'SBIN0001234',
dateReported: '2023-12-20',
status: 'CONFIRMED',
remarks: 'Cross-bank fraud trail from SBI complaint.'
}
],
'50100745829361': [
{
fraudReferenceNumber: 'CFR-2024-00567',
fraudCategory: 'PART_TIME_JOB_FRAUD',
fraudAmount: 320000,
reportingBank: 'LincPay Digital Bank',
reportingBankIfsc: 'LINC0000001',
dateReported: '2024-01-05',
status: 'RESOLVED',
remarks: 'Funds recovered. Account closed.'
}
],
'40120098765432': [
{
fraudReferenceNumber: 'CFR-2024-00999',
fraudCategory: 'SEXTORTION',
fraudAmount: 210000,
reportingBank: 'HDFC Bank',
reportingBankIfsc: 'HDFC0001234',
dateReported: '2024-01-02',
status: 'UNDER_INVESTIGATION',
remarks: 'Next-hop account in sextortion chain.'
}
]
};
function handleReportSuspect(body: any): any {
const ref = `SUS-${Date.now()}-${Math.floor(Math.random() * 9000 + 1000)}`;
const record = { ...body, referenceNumber: ref, status: 'REPORTED', reportedAt: now() };
reportedSuspects.push(record);
return {
success: true,
referenceNumber: ref,
accountNumber: body.accountNumber,
status: 'REPORTED',
reportedAt: record.reportedAt,
message: 'Suspect account reported to I4C successfully.'
};
}
function handleReportMuleAccount(body: any): any {
const ref = `MULE-${Date.now()}-${Math.floor(Math.random() * 9000 + 1000)}`;
const record = { ...body, referenceNumber: ref, status: 'FLAGGED', reportedAt: now() };
reportedMules.push(record);
return {
success: true,
referenceNumber: ref,
accountNumber: body.accountNumber,
muleType: body.muleType,
status: 'FLAGGED',
reportedAt: record.reportedAt,
message: 'Mule account flagged and reported to I4C/CFCFRMS.'
};
}
function handleReportFraudTransaction(body: any): any {
const ref = `FTX-${Date.now()}-${Math.floor(Math.random() * 9000 + 1000)}`;
const record = { ...body, referenceNumber: ref, status: 'REPORTED', reportedAt: now() };
reportedFraudTxns.push(record);
return {
success: true,
referenceNumber: ref,
transactionId: body.transactionId,
status: 'REPORTED',
reportedAt: record.reportedAt,
message: 'Fraud transaction reported to I4C successfully.'
};
}
function handleFmrSubmission(body: any): any {
const ref = `FMR-${new Date().getFullYear()}-${String(fmrSubmissions.length + 1).padStart(6, '0')}`;
const nextDue = new Date();
nextDue.setDate(nextDue.getDate() + 90);
const record = {
...body,
fmrReferenceNumber: ref,
status: 'SUBMITTED',
submittedAt: now(),
nextDueDate: nextDue.toISOString().split('T')[0]
};
fmrSubmissions.push(record);
return {
success: true,
fmrReferenceNumber: ref,
reportingBankIfsc: body.reportingBankIfsc,
fraudCategory: body.fraudCategory,
fraudAmount: body.fraudAmount,
status: 'SUBMITTED',
submittedAt: record.submittedAt,
nextDueDate: record.nextDueDate,
message: 'FMR submitted to RBI FUA system successfully.'
};
}
function handleCfrLookup(url: URL): any {
const accountNumber = url.searchParams.get('accountNumber') || '';
const records = cfrRegistry[accountNumber] || [];
return { accountNumber, found: records.length > 0, records, queryTimestamp: now() };
}
// ─── Route matcher utility ───
function matchPath(pathname: string, pattern: string): { match: boolean; params: { [k: string]: string } } {
const patParts = pattern.split('/').filter(Boolean);
const urlParts = pathname.split('/').filter(Boolean);
if (patParts.length !== urlParts.length) return { match: false, params: {} };
const params: { [k: string]: string } = {};
for (let i = 0; i < patParts.length; i++) {
if (patParts[i].startsWith(':')) {
params[patParts[i].slice(1)] = urlParts[i];
} else if (patParts[i] !== urlParts[i]) {
return { match: false, params: {} };
}
}
return { match: true, params };
}
function parseUrl(raw: string): URL {
try {
return new URL(raw);
} catch {
return new URL(raw, 'http://localhost');
}
}
// ─── Class-based Interceptor ───
@Injectable()
export class CfcfrmsMockInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const url = parseUrl(req.url);
const path = url.pathname;
const method = req.method;
const isCfcfrms =
path.includes('/cfcfrms/complaints') || path.includes('/cfcfrms/complaint') || path.includes('/cfcfrms/report-');
const isSimulator = path.includes('/simulator/cfcfrms');
const isRbi = path.includes('/rbi/fmr') || path.includes('/rbi/cfr');
if (!isCfcfrms && !isSimulator && !isRbi) {
return next.handle(req);
}
// Extract relative path
let relativePath = '';
if (isRbi) {
const idx = path.indexOf('/rbi');
relativePath = path.substring(idx);
} else if (isSimulator) {
const idx = path.indexOf('/simulator/cfcfrms');
relativePath = path.substring(idx + '/simulator/cfcfrms'.length);
} else {
const idx = path.indexOf('/cfcfrms');
relativePath = path.substring(idx + '/cfcfrms'.length);
}
let responseBody: any = { error: 'Not Found' };
let status = 200;
const mockDelay = Math.floor(Math.random() * 300 + 150);
// ──── Bank-side routes ────
if (!isSimulator) {
if (method === 'POST' && relativePath === '/complaints') {
responseBody = handleCreateComplaint(req.body);
} else if (method === 'GET' && relativePath === '/complaints/stats') {
responseBody = computeMockStats();
} else {
const timelineMatch = matchPath(relativePath, '/complaints/:ticketId/timeline');
if (method === 'GET' && timelineMatch.match) {
responseBody = handleGetTimeline(timelineMatch.params['ticketId']);
} else {
const actionMatch = matchPath(relativePath, '/complaints/:ticketId/action');
if (method === 'PUT' && actionMatch.match) {
const result = handleSubmitAction(actionMatch.params['ticketId'], req.body);
if (!result) {
responseBody = { error: 'Ticket not found' };
status = 404;
} else responseBody = result;
} else {
const escMatch = matchPath(relativePath, '/complaints/:ticketId/escalate');
if (method === 'POST' && escMatch.match) {
const result = handleEscalate(escMatch.params['ticketId'], req.body);
if (!result) {
responseBody = { error: 'Ticket not found' };
status = 404;
} else responseBody = result;
} else {
const detailMatch = matchPath(relativePath, '/complaints/:ticketId');
if (method === 'GET' && detailMatch.match) {
const result = handleGetComplaintById(detailMatch.params['ticketId']);
if (!result) {
responseBody = { error: 'Ticket not found' };
status = 404;
} else responseBody = result;
} else if (method === 'GET' && (relativePath === '/complaints' || relativePath === '/complaints/')) {
responseBody = handleGetComplaints(url);
}
}
}
}
}
// Bank proactive reporting routes
if (method === 'POST' && relativePath === '/report-suspect') {
responseBody = handleReportSuspect(req.body);
} else if (method === 'POST' && relativePath === '/report-mule-account') {
responseBody = handleReportMuleAccount(req.body);
} else if (method === 'POST' && relativePath === '/report-fraud-transaction') {
responseBody = handleReportFraudTransaction(req.body);
}
}
// ──── RBI routes ────
if (isRbi) {
if (method === 'POST' && relativePath === '/rbi/fmr') {
responseBody = handleFmrSubmission(req.body);
} else if (method === 'GET' && relativePath.startsWith('/rbi/cfr/lookup')) {
responseBody = handleCfrLookup(url);
}
}
// ──── Simulator routes ────
if (isSimulator) {
if (method === 'POST' && relativePath === '/push-complaint') {
responseBody = handleSimPushComplaint(req.body);
} else if (method === 'POST' && relativePath === '/push-hold-directive') {
responseBody = handleSimPushHoldDirective(req.body);
} else if (method === 'POST' && relativePath === '/push-bulk-complaints') {
responseBody = handleSimPushBulk(req.body);
} else if (method === 'POST' && relativePath === '/receive-bank-update') {
responseBody = handleSimReceiveBankUpdate(req.body);
} else if (method === 'POST' && relativePath === '/receive-escalation') {
responseBody = handleSimReceiveEscalation(req.body);
} else if (method === 'POST' && relativePath === '/verify-account') {
responseBody = handleSimVerifyAccount(req.body);
} else if (method === 'POST' && relativePath === '/verify-transaction') {
responseBody = handleSimVerifyTransaction(req.body);
} else {
const statusMatch = matchPath(relativePath, '/complaint-status/:ref');
if (method === 'GET' && statusMatch.match) {
responseBody = handleSimComplaintStatus(statusMatch.params['ref']);
} else if (method === 'GET' && relativePath === '/bank-performance') {
responseBody = handleSimBankPerformance();
}
}
}
return of(new HttpResponse({ status, body: responseBody })).pipe(delay(mockDelay));
}
}
+330
View File
@@ -0,0 +1,330 @@
/**
* 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/.
*/
export type ComplaintStatus =
| 'RECEIVED'
| 'UNDER_REVIEW'
| 'LIEN_MARKED'
| 'DEBIT_FROZEN'
| 'FUNDS_MOVED'
| 'RESOLVED'
| 'REJECTED';
export interface Complaint {
ticketId: string;
complainantName: string;
complainantMobile: string;
complainantEmail: string;
complainantAddress: string;
fraudAmount: number;
fraudType: string;
fraudDescription: string;
suspectAccountNumber: string;
suspectAccountName: string;
suspectBankIfsc: string;
suspectBankName: string;
sourcePortal: string;
acknowledgementNumber: string;
status: ComplaintStatus;
currentBankIfsc: string;
currentBankName: string;
availableBalance: number;
fundsHeld: number;
nextHopAccountNumber: string;
nextHopBankIfsc: string;
actionNote: string;
assignedOfficer: string;
createdAt: string;
updatedAt: string;
}
export interface ComplaintAction {
action: ComplaintStatus;
availableBalance?: number;
fundsHeld?: number;
nextHopAccountNumber?: string;
nextHopBankIfsc?: string;
actionNote: string;
}
export interface DashboardStats {
totalComplaints: number;
pendingReview: number;
lienMarked: number;
fundsRecovered: number;
statusDistribution: { [key in ComplaintStatus]?: number };
amountByStatus: { [key in ComplaintStatus]?: number };
}
export interface TimelineEvent {
id: number;
ticketId: string;
eventType: string;
eventDescription: string;
actor: string;
actorRole: string;
timestamp: string;
metadata: { [key: string]: any };
}
export interface PagedResponse<T> {
content: T[];
totalElements: number;
totalPages: number;
size: number;
number: number;
first: boolean;
last: boolean;
}
export interface EscalationRequest {
ticketId: string;
nextHopAccountNumber: string;
nextHopBankIfsc: string;
remarks: string;
}
export interface EscalationResponse {
ticketId: string;
referenceNumber: string;
escalatedTo: string;
escalatedBankIfsc: string;
status: string;
timestamp: string;
}
// ─── Simulator Models ───
export interface SimPushComplaintRequest {
complainantName: string;
complainantMobile: string;
complainantEmail?: string;
complainantAddress?: string;
fraudAmount: number;
fraudType: string;
fraudDescription: string;
suspectAccountNumber: string;
suspectAccountName: string;
suspectBankIfsc: string;
suspectBankName?: string;
sourcePortal: string;
acknowledgementNumber: string;
}
export interface SimPushComplaintResponse {
success: boolean;
message: string;
ticketId: string;
status: ComplaintStatus;
}
export interface SimHoldDirectiveRequest {
ticketId: string;
holdAmount: number;
reason?: string;
}
export interface SimHoldDirectiveResponse {
success: boolean;
message: string;
ticketId: string;
status: string;
fundsHeld: number;
}
export interface SimBulkPushRequest {
complaints: SimPushComplaintRequest[];
}
export interface SimBulkPushResponse {
success: boolean;
totalPushed: number;
results: SimPushComplaintResponse[];
}
export interface SimBankUpdateRequest {
ticketId: string;
}
export interface SimBankUpdateResponse {
success: boolean;
ticketId: string;
currentStatus: ComplaintStatus;
lastUpdated: string;
actionNote: string;
}
export interface SimVerifyAccountRequest {
accountNumber: string;
}
export interface SimVerifyAccountResponse {
accountNumber: string;
exists: boolean;
accountName: string | null;
bankIfsc: string | null;
bankName: string | null;
isFlagged: boolean;
flagReason: string | null;
}
export interface SimVerifyTransactionRequest {
transactionId?: string;
accountNumber: string;
amount: number;
}
export interface SimVerifyTransactionResponse {
transactionId: string;
accountNumber: string;
amount: number;
status: string;
timestamp: string;
riskScore: number;
flagged: boolean;
remarks: string;
}
export interface SimComplaintStatusResponse {
found: boolean;
ticketId?: string;
acknowledgementNumber?: string;
status?: ComplaintStatus;
bankAction?: string;
lastUpdated?: string;
assignedOfficer?: string;
message?: string;
}
export interface SimBankPerformanceResponse {
bankIfsc: string;
bankName: string;
totalTickets: number;
resolvedCount: number;
rejectedCount: number;
frozenCount: number;
pendingCount: number;
averageTATHours: number;
complianceScore: number;
responseRate: number;
periodFrom: string;
periodTo: string;
}
// ─── Bank Proactive Reporting Models ───
export interface ReportSuspectRequest {
accountNumber: string;
accountName: string;
bankIfsc: string;
suspicionReason: string;
detectedBy: string;
detectedOn: string;
linkedTransactions?: string[];
remarks?: string;
}
export interface ReportSuspectResponse {
success: boolean;
referenceNumber: string;
accountNumber: string;
status: string;
reportedAt: string;
message: string;
}
export interface ReportMuleAccountRequest {
accountNumber: string;
accountName: string;
bankIfsc: string;
muleType: 'INDIVIDUAL' | 'BUSINESS' | 'SHELL_COMPANY';
linkedFraudTickets: string[];
transactionPattern: string;
totalSuspiciousAmount: number;
remarks?: string;
}
export interface ReportMuleAccountResponse {
success: boolean;
referenceNumber: string;
accountNumber: string;
muleType: string;
status: string;
reportedAt: string;
message: string;
}
export interface ReportFraudTransactionRequest {
transactionId: string;
fromAccount: string;
toAccount: string;
amount: number;
transactionDate: string;
fraudType: string;
channel: string;
detectedBy: string;
description: string;
linkedTicketId?: string;
}
export interface ReportFraudTransactionResponse {
success: boolean;
referenceNumber: string;
transactionId: string;
status: string;
reportedAt: string;
message: string;
}
// ─── RBI Integration Models ───
export interface FmrSubmissionRequest {
reportingBankIfsc: string;
reportingBankName: string;
fraudCategory: string;
fraudSubCategory: string;
fraudAmount: number;
accountNumber: string;
accountHolderName: string;
dateOfDetection: string;
dateOfFraud: string;
modus: string;
actionTaken: string;
staffInvolved: boolean;
remarks?: string;
}
export interface FmrSubmissionResponse {
success: boolean;
fmrReferenceNumber: string;
reportingBankIfsc: string;
fraudCategory: string;
fraudAmount: number;
status: string;
submittedAt: string;
nextDueDate: string;
message: string;
}
export interface CfrLookupResponse {
accountNumber: string;
found: boolean;
records: CfrRecord[];
queryTimestamp: string;
}
export interface CfrRecord {
fraudReferenceNumber: string;
fraudCategory: string;
fraudAmount: number;
reportingBank: string;
reportingBankIfsc: string;
dateReported: string;
status: string;
remarks: string;
}
@@ -0,0 +1,138 @@
<!--
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="report-page">
<div class="page-header">
<h2>{{ 'labels.text.Report Fraud Transaction' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Report a detected fraud transaction to I4C' | translate }}</p>
</div>
<div class="report-layout">
<mat-card class="report-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="exclamation-triangle" class="mr-05"></fa-icon>
{{ 'labels.text.Fraud Transaction Details' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (result) {
<div class="success-result">
<div class="success-icon"><fa-icon icon="check-circle" size="3x"></fa-icon></div>
<h3>{{ 'labels.text.Fraud Transaction Reported' | translate }}</h3>
<div class="result-details">
<div class="result-row">
<span class="result-label">{{ 'labels.text.Reference Number' | translate }}</span
><span class="result-value mono">{{ result.referenceNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Transaction ID' | translate }}</span
><span class="result-value mono">{{ result.transactionId }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Status' | translate }}</span
><span class="result-value">{{ result.status }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Timestamp' | translate }}</span
><span class="result-value">{{ result.reportedAt }}</span>
</div>
</div>
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New Report' | translate }}
</button>
</div>
} @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Transaction ID' | translate }}</mat-label>
<input matInput formControlName="transactionId" placeholder="e.g. TXN-20240115-001" />
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Amount (INR)' | translate }}</mat-label>
<input matInput type="number" formControlName="amount" placeholder="0.00" />
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.From Account' | translate }}</mat-label>
<input matInput formControlName="fromAccount" placeholder="Source account number" />
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.To Account' | translate }}</mat-label>
<input matInput formControlName="toAccount" placeholder="Destination account number" />
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Transaction Date' | translate }}</mat-label>
<input matInput [matDatepicker]="txnPicker" formControlName="transactionDate" />
<mat-datepicker-toggle matSuffix [for]="txnPicker"></mat-datepicker-toggle>
<mat-datepicker #txnPicker></mat-datepicker>
</mat-form-field>
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Fraud Type' | translate }}</mat-label>
<mat-select formControlName="fraudType">
@for (ft of fraudTypes; track ft) {
<mat-option [value]="ft">{{ ft.replace('_', ' ') }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'labels.text.Channel' | translate }}</mat-label>
<mat-select formControlName="channel">
@for (ch of channels; track ch) {
<mat-option [value]="ch">{{ ch }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Detected By' | translate }}</mat-label>
<input matInput formControlName="detectedBy" placeholder="Officer / Fraud Detection System" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Description' | translate }}</mat-label>
<textarea
matInput
formControlName="description"
rows="3"
placeholder="Describe the fraud transaction..."
></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Linked CFCFRMS Ticket' | translate }}</mat-label>
<input matInput formControlName="linkedTicketId" placeholder="e.g. CFCFRMS-2024-00001 (optional)" />
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Report' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,111 @@
/**
* 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/.
*/
.report-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;
}
}
.report-layout {
display: flex;
gap: 24px;
}
.report-card {
flex: 1;
max-width: 700px;
}
.full-width {
width: 100%;
}
.mr-05 {
margin-right: 0.5rem;
}
.mt-16 {
margin-top: 16px;
}
.submit-btn {
margin-top: 8px;
height: 48px;
font-size: 16px;
}
.inline-spinner {
display: inline-block;
margin-right: 8px;
}
.success-result {
text-align: center;
padding: 32px 16px;
.success-icon {
color: #10b981;
margin-bottom: 16px;
}
h3 {
color: #10b981;
margin-bottom: 24px;
}
}
.result-details {
text-align: left;
max-width: 500px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.result-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
}
.result-label {
color: #666;
font-weight: 500;
}
.result-value {
font-weight: 600;
}
.mono {
font-family: 'Roboto Mono', monospace;
}
.form-row {
display: flex;
gap: 16px;
}
.half-width {
flex: 1;
}
@@ -0,0 +1,159 @@
/**
* 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 { ReactiveFormsModule, FormBuilder, FormGroup, Validators } 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 { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'cfcfrms-report-fraud-txn',
standalone: true,
templateUrl: './report-fraud-txn.component.html',
styleUrls: ['./report-fraud-txn.component.scss'],
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
MatDatepickerModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDialogModule,
FaIconComponent,
TranslateModule
]
})
export class ReportFraudTxnComponent {
private fb = inject(FormBuilder);
private service = inject(CfcfrmsService);
private snackBar = inject(MatSnackBar);
private dialog = inject(MatDialog);
form: FormGroup = this.fb.group({
transactionId: [
'',
Validators.required
],
fromAccount: [
'',
Validators.required
],
toAccount: [
'',
Validators.required
],
amount: [
null,
[
Validators.required,
Validators.min(1)
]
],
transactionDate: [
'',
Validators.required
],
fraudType: [
'',
Validators.required
],
channel: [
'',
Validators.required
],
detectedBy: [
'',
Validators.required
],
description: [
'',
Validators.required
],
linkedTicketId: ['']
});
isSubmitting = false;
result: any = null;
fraudTypes = [
'UPI_FRAUD',
'NEFT_FRAUD',
'RTGS_FRAUD',
'IMPS_FRAUD',
'CARD_FRAUD',
'NET_BANKING_FRAUD',
'CHEQUE_FRAUD',
'OTHER'
];
channels = [
'UPI',
'NEFT',
'RTGS',
'IMPS',
'Internet Banking',
'Mobile Banking',
'Debit Card',
'Credit Card',
'ATM',
'Branch'
];
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { title: 'Confirm Fraud Report', message: 'Report this fraud transaction to I4C?' }
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (!confirmed) return;
this.isSubmitting = true;
const val = this.form.value;
const payload = {
...val,
transactionDate: new Date(val.transactionDate).toISOString()
};
this.service.reportFraudTransaction(payload).subscribe({
next: (res) => {
this.result = res;
this.isSubmitting = false;
this.snackBar.open('Fraud transaction reported successfully', 'OK', { duration: 4000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to report fraud transaction', 'OK', { duration: 4000 });
}
});
});
}
resetForm(): void {
this.form.reset();
this.result = null;
}
}
@@ -0,0 +1,133 @@
<!--
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="report-page">
<div class="page-header">
<h2>{{ 'labels.text.Report Mule Account' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Flag a mule account proactively to I4C/CFCFRMS' | translate }}</p>
</div>
<div class="report-layout">
<mat-card class="report-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="user-slash" class="mr-05"></fa-icon> {{ 'labels.text.Mule Account Details' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (result) {
<div class="success-result">
<div class="success-icon"><fa-icon icon="check-circle" size="3x"></fa-icon></div>
<h3>{{ 'labels.text.Mule Account Flagged' | translate }}</h3>
<div class="result-details">
<div class="result-row">
<span class="result-label">{{ 'labels.text.Reference Number' | translate }}</span
><span class="result-value mono">{{ result.referenceNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Account Number' | translate }}</span
><span class="result-value mono">{{ result.accountNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Mule Type' | translate }}</span
><span class="result-value">{{ result.muleType }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Status' | translate }}</span
><span class="result-value status-flagged">{{ result.status }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Timestamp' | translate }}</span
><span class="result-value">{{ result.reportedAt }}</span>
</div>
</div>
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New Report' | translate }}
</button>
</div>
} @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Account Number' | translate }}</mat-label>
<input matInput formControlName="accountNumber" placeholder="e.g. 50100268347291" />
@if (form.get('accountNumber')?.hasError('required') && form.get('accountNumber')?.touched) {
<mat-error>Account number is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Account Name' | translate }}</mat-label>
<input matInput formControlName="accountName" placeholder="Account holder name" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Bank IFSC' | translate }}</mat-label>
<input matInput formControlName="bankIfsc" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Mule Type' | translate }}</mat-label>
<mat-select formControlName="muleType">
@for (t of muleTypes; track t.value) {
<mat-option [value]="t.value">{{ t.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Transaction Pattern' | translate }}</mat-label>
<mat-select formControlName="transactionPattern">
@for (p of transactionPatterns; track p) {
<mat-option [value]="p">{{ p }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Total Suspicious Amount (INR)' | translate }}</mat-label>
<input matInput type="number" formControlName="totalSuspiciousAmount" placeholder="0.00" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Linked Fraud Ticket IDs' | translate }}</mat-label>
<input
matInput
formControlName="linkedFraudTickets"
placeholder="e.g. CFCFRMS-2024-00001, CFCFRMS-2024-00002"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Remarks' | translate }}</mat-label>
<textarea
matInput
formControlName="remarks"
rows="3"
placeholder="{{ 'labels.text.Enter remarks...' | translate }}"
></textarea>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Report' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,106 @@
/**
* 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/.
*/
.report-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;
}
}
.report-layout {
display: flex;
gap: 24px;
}
.report-card {
flex: 1;
max-width: 700px;
}
.full-width {
width: 100%;
}
.mr-05 {
margin-right: 0.5rem;
}
.mt-16 {
margin-top: 16px;
}
.submit-btn {
margin-top: 8px;
height: 48px;
font-size: 16px;
}
.inline-spinner {
display: inline-block;
margin-right: 8px;
}
.success-result {
text-align: center;
padding: 32px 16px;
.success-icon {
color: #10b981;
margin-bottom: 16px;
}
h3 {
color: #10b981;
margin-bottom: 24px;
}
}
.result-details {
text-align: left;
max-width: 500px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.result-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
}
.result-label {
color: #666;
font-weight: 500;
}
.result-value {
font-weight: 600;
}
.mono {
font-family: 'Roboto Mono', monospace;
}
.status-flagged {
color: #ef4444;
font-weight: 700;
}
@@ -0,0 +1,149 @@
/**
* 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 { ReactiveFormsModule, FormBuilder, FormGroup, Validators } 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 { MatSelectModule } from '@angular/material/select';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'cfcfrms-report-mule',
standalone: true,
templateUrl: './report-mule.component.html',
styleUrls: ['./report-mule.component.scss'],
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDialogModule,
FaIconComponent,
TranslateModule
]
})
export class ReportMuleComponent {
private fb = inject(FormBuilder);
private service = inject(CfcfrmsService);
private snackBar = inject(MatSnackBar);
private dialog = inject(MatDialog);
form: FormGroup = this.fb.group({
accountNumber: [
'',
[
Validators.required,
Validators.minLength(8)
]
],
accountName: [
'',
Validators.required
],
bankIfsc: [
'LINC0000001',
[
Validators.required,
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
]
],
muleType: [
'',
Validators.required
],
linkedFraudTickets: [''],
transactionPattern: [
'',
Validators.required
],
totalSuspiciousAmount: [
null,
[
Validators.required,
Validators.min(1)
]
],
remarks: ['']
});
isSubmitting = false;
result: any = null;
muleTypes = [
{ value: 'INDIVIDUAL', label: 'Individual Account' },
{ value: 'BUSINESS', label: 'Business / Proprietor Account' },
{ value: 'SHELL_COMPANY', label: 'Shell Company Account' }
];
transactionPatterns = [
'Rapid inflow-outflow within hours',
'Multiple small credits, single large withdrawal',
'Circular fund routing across accounts',
'High volume with no legitimate business',
'Dormant account suddenly activated with large transactions',
'Pattern matches known mule network'
];
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { title: 'Confirm Mule Report', message: 'Flag this account as a mule account and report to I4C/CFCFRMS?' }
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (!confirmed) return;
this.isSubmitting = true;
const val = this.form.value;
const payload = {
...val,
linkedFraudTickets: val.linkedFraudTickets
? val.linkedFraudTickets
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
: []
};
this.service.reportMuleAccount(payload).subscribe({
next: (res) => {
this.result = res;
this.isSubmitting = false;
this.snackBar.open('Mule account reported successfully', 'OK', { duration: 4000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to report mule account', 'OK', { duration: 4000 });
}
});
});
}
resetForm(): void {
this.form.reset({ bankIfsc: 'LINC0000001' });
this.result = null;
}
}
@@ -0,0 +1,132 @@
<!--
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="report-page">
<div class="page-header">
<h2>{{ 'labels.text.Report Suspect Account' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Report internally detected suspect account to I4C' | translate }}</p>
</div>
<div class="report-layout">
<mat-card class="report-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="user-shield" class="mr-05"></fa-icon> {{ 'labels.text.Suspect Account Details' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (result) {
<div class="success-result">
<div class="success-icon"><fa-icon icon="check-circle" size="3x"></fa-icon></div>
<h3>{{ 'labels.text.Report Submitted' | translate }}</h3>
<div class="result-details">
<div class="result-row">
<span class="result-label">{{ 'labels.text.Reference Number' | translate }}</span
><span class="result-value mono">{{ result.referenceNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Account Number' | translate }}</span
><span class="result-value mono">{{ result.accountNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Status' | translate }}</span
><span class="result-value">{{ result.status }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Timestamp' | translate }}</span
><span class="result-value">{{ result.reportedAt }}</span>
</div>
</div>
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New Report' | translate }}
</button>
</div>
} @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Account Number' | translate }}</mat-label>
<input matInput formControlName="accountNumber" placeholder="e.g. 50100268347291" />
@if (form.get('accountNumber')?.hasError('required') && form.get('accountNumber')?.touched) {
<mat-error>Account number is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Account Name' | translate }}</mat-label>
<input matInput formControlName="accountName" placeholder="Account holder name" />
@if (form.get('accountName')?.hasError('required') && form.get('accountName')?.touched) {
<mat-error>Account name is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Bank IFSC' | translate }}</mat-label>
<input matInput formControlName="bankIfsc" placeholder="e.g. LINC0000001" />
@if (form.get('bankIfsc')?.hasError('pattern') && form.get('bankIfsc')?.touched) {
<mat-error>{{ 'labels.text.Invalid IFSC format' | translate }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Suspicion Reason' | translate }}</mat-label>
<mat-select formControlName="suspicionReason">
@for (reason of suspicionReasons; track reason) {
<mat-option [value]="reason">{{ reason }}</mat-option>
}
</mat-select>
@if (form.get('suspicionReason')?.hasError('required') && form.get('suspicionReason')?.touched) {
<mat-error>Suspicion reason is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Detected By' | translate }}</mat-label>
<input matInput formControlName="detectedBy" placeholder="Officer name / system" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Detection Date' | translate }}</mat-label>
<input matInput [matDatepicker]="picker" formControlName="detectedOn" />
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Linked Transaction IDs' | translate }}</mat-label>
<input matInput formControlName="linkedTransactions" placeholder="Comma-separated TXN IDs" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Remarks' | translate }}</mat-label>
<textarea
matInput
formControlName="remarks"
rows="3"
placeholder="{{ 'labels.text.Enter remarks...' | translate }}"
></textarea>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Report' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,102 @@
/**
* 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/.
*/
.report-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;
}
}
.report-layout {
display: flex;
gap: 24px;
}
.report-card {
flex: 1;
max-width: 700px;
}
.full-width {
width: 100%;
}
.mr-05 {
margin-right: 0.5rem;
}
.mt-16 {
margin-top: 16px;
}
.submit-btn {
margin-top: 8px;
height: 48px;
font-size: 16px;
}
.inline-spinner {
display: inline-block;
margin-right: 8px;
}
.success-result {
text-align: center;
padding: 32px 16px;
.success-icon {
color: #10b981;
margin-bottom: 16px;
}
h3 {
color: #10b981;
margin-bottom: 24px;
}
}
.result-details {
text-align: left;
max-width: 500px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.result-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
}
.result-label {
color: #666;
font-weight: 500;
}
.result-value {
font-weight: 600;
}
.mono {
font-family: 'Roboto Mono', monospace;
}
@@ -0,0 +1,145 @@
/**
* 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 { ReactiveFormsModule, FormBuilder, FormGroup, Validators } 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 { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'cfcfrms-report-suspect',
standalone: true,
templateUrl: './report-suspect.component.html',
styleUrls: ['./report-suspect.component.scss'],
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
MatDatepickerModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDialogModule,
FaIconComponent,
TranslateModule
]
})
export class ReportSuspectComponent {
private fb = inject(FormBuilder);
private service = inject(CfcfrmsService);
private snackBar = inject(MatSnackBar);
private dialog = inject(MatDialog);
form: FormGroup = this.fb.group({
accountNumber: [
'',
[
Validators.required,
Validators.minLength(8)
]
],
accountName: [
'',
Validators.required
],
bankIfsc: [
'LINC0000001',
[
Validators.required,
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
]
],
suspicionReason: [
'',
Validators.required
],
detectedBy: [
'',
Validators.required
],
detectedOn: [
'',
Validators.required
],
linkedTransactions: [''],
remarks: ['']
});
isSubmitting = false;
result: any = null;
suspicionReasons = [
'Unusual transaction pattern',
'Multiple fraud complaints linked',
'High-value transactions to unknown beneficiaries',
'Account used as pass-through',
'KYC document mismatch',
'Tip-off from law enforcement',
'Internal fraud detection system alert',
'Other'
];
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { title: 'Confirm Report', message: 'Are you sure you want to report this suspect account to I4C?' }
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (!confirmed) return;
this.isSubmitting = true;
const val = this.form.value;
const payload = {
...val,
detectedOn: new Date(val.detectedOn).toISOString(),
linkedTransactions: val.linkedTransactions
? val.linkedTransactions
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
: []
};
this.service.reportSuspect(payload).subscribe({
next: (res) => {
this.result = res;
this.isSubmitting = false;
this.snackBar.open('Suspect account reported successfully', 'OK', { duration: 4000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to report suspect account', 'OK', { duration: 4000 });
}
});
});
}
resetForm(): void {
this.form.reset({ bankIfsc: 'LINC0000001' });
this.result = null;
}
}
+6
View File
@@ -35,6 +35,7 @@ import { ApiPrefixInterceptor } from './http/api-prefix.interceptor';
import { ErrorHandlerInterceptor } from './http/error-handler.interceptor';
import { CacheInterceptor } from './http/cache.interceptor';
import { AuthenticationInterceptor } from './authentication/authentication.interceptor';
import { CfcfrmsMockInterceptor } from '../cfcfrms/mock/cfcfrms-mock.interceptor';
/** Custom Strategies */
import { RouteReusableStrategy } from './route/route-reusable-strategy';
@@ -72,6 +73,11 @@ import { ContentComponent } from './shell/content/content.component';
AuthenticationService,
AuthenticationGuard,
AuthenticationInterceptor,
{
provide: HTTP_INTERCEPTORS,
useClass: CfcfrmsMockInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthenticationInterceptor,
@@ -18,8 +18,8 @@
.breadcrumb {
list-style: none;
padding: 0;
margin: 1.5rem 0;
line-height: 40px;
margin: 0.5rem 0;
line-height: 32px;
.breadcrumb-title {
font-size: 1.25rem;
File diff suppressed because it is too large Load Diff
+150 -6
View File
@@ -34,30 +34,31 @@ $color: #e2e4ec;
.sidenav {
overflow-x: hidden;
position: relative;
}
.sub-menu-bar {
display: flex;
flex-wrap: nowrap;
align-items: center;
background-color: #1074b9;
padding: 2px 0.5rem 2px calc(48px + 2rem);
padding: 0 100px 0 48px;
width: 100%;
z-index: 1;
z-index: 2;
position: relative;
font-family: Roboto, 'Helvetica Neue', sans-serif;
font-size: 14px;
font-size: 13px;
font-weight: 500;
letter-spacing: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.sub-menu-link {
display: inline-flex;
align-items: center;
padding: 6px 0.5rem;
flex: 1;
padding: 11px 2px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
@@ -81,6 +82,149 @@ $color: #e2e4ec;
font-size: 12px;
}
.sub-menu-spacer {
visibility: hidden;
pointer-events: none;
}
.menu-item-icon {
margin-right: 10px;
width: 16px;
color: #667085;
font-size: 14px;
}
/* Deposits Mega Panel (inside mat-menu overlay) */
.deposit-menu-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 24px;
padding: 8px 12px;
min-width: 600px;
}
.deposit-menu-group {
.deposit-group-title {
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #1074b9;
padding: 4px 16px 6px;
margin-bottom: 2px;
border-bottom: 2px solid #1074b9;
}
}
/* General Ledger Mega Menu */
.gl-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
.gl-mega-menu {
position: absolute;
top: 100px;
left: 50%;
transform: translateX(-50%);
background: #fff;
width: 860px;
max-width: 95vw;
padding: 20px 24px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22);
border-radius: 0 0 10px 10px;
z-index: 1000;
border-top: 3px solid #0d47a1;
}
.gl-search {
width: 100%;
padding: 10px 14px;
margin-bottom: 16px;
border: 1px solid #d0d5dd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
background: #f9fafb;
&:focus {
outline: none;
border-color: #0d47a1;
background: #fff;
box-shadow: 0 0 0 3px rgba(13, 71, 161, 0.1);
}
}
.gl-columns {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.gl-group-title {
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #0d47a1;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 2px solid #0d47a1;
}
.gl-link {
display: flex;
align-items: center;
padding: 7px 6px;
text-decoration: none;
color: #344054;
font-size: 13px;
cursor: pointer;
border-radius: 5px;
transition:
background 0.15s,
color 0.15s;
&:hover {
background: #e3f2fd;
color: #0d47a1;
}
&.hidden {
display: none;
}
}
.gl-icon {
width: 18px;
margin-right: 8px;
color: #667085;
font-size: 13px;
.gl-link:hover & {
color: #0d47a1;
}
}
/* RBI Mega Menu */
.rbi-mega-menu {
width: 1100px;
max-width: 97vw;
max-height: 80vh;
overflow-y: auto;
border-top-color: #b71c1c;
}
.rbi-columns {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
@keyframes move {
0% {
left: 0;
+31
View File
@@ -66,6 +66,33 @@ export class ShellComponent implements OnInit, OnDestroy {
/** Subscription to progress bar. */
progressBar$: Subscription;
/** Sub Ledger mega menu state */
subLedgerOpen = false;
subLedgerSearch = '';
/** General Ledger mega menu state */
glMenuOpen = false;
glSearch = '';
/** RBI mega menu state */
rbiMenuOpen = false;
rbiSearch = '';
matchesSubLedgerSearch(label: string): boolean {
if (!this.subLedgerSearch) return true;
return label.toLowerCase().includes(this.subLedgerSearch.toLowerCase());
}
matchesGlSearch(label: string): boolean {
if (!this.glSearch) return true;
return label.toLowerCase().includes(this.glSearch.toLowerCase());
}
matchesRbiSearch(label: string): boolean {
if (!this.rbiSearch) return true;
return label.toLowerCase().includes(this.rbiSearch.toLowerCase());
}
/**
* Subscribes to progress bar to update its mode.
*/
@@ -85,6 +112,10 @@ export class ShellComponent implements OnInit, OnDestroy {
this.cdr.detectChanges();
}
openConfigWizard(): void {
// Placeholder — config wizard is handled by toolbar component
}
/**
* Unsubscribes from progress bar.
*/
@@ -171,6 +171,119 @@
</mat-icon>
<a matLine>{{ 'labels.menus.Analytics' | translate }}</a>
</mat-list-item>
<!-- CFCFRMS Section -->
<div class="nav-item-sep mt-1">
<mat-divider></mat-divider>
<span class="text-muted">{{ 'labels.menus.CFCFRMS' | translate }}</span>
</div>
<mat-list-item
[routerLink]="['/cfcfrms/dashboard']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.CFCFRMS Dashboard' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: true }"
>
<mat-icon matListIcon>
<fa-icon icon="shield-alt" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.CFCFRMS Dashboard' | translate }}</a>
</mat-list-item>
<mat-list-item
[routerLink]="['/cfcfrms/complaints']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.Complaints' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="list-alt" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.Complaints' | translate }}</a>
</mat-list-item>
<mat-list-item
[routerLink]="['/cfcfrms/escalate']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.Escalate Complaint' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="arrow-up" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.Escalate' | translate }}</a>
</mat-list-item>
<mat-list-item
[routerLink]="['/cfcfrms/report-suspect']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.Report Suspect' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="user-shield" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.Report Suspect' | translate }}</a>
</mat-list-item>
<mat-list-item
[routerLink]="['/cfcfrms/report-mule']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.Report Mule Account' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="user-slash" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.Report Mule' | translate }}</a>
</mat-list-item>
<mat-list-item
[routerLink]="['/cfcfrms/report-fraud-txn']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.Report Fraud Transaction' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="exclamation-triangle" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.Report Fraud Txn' | translate }}</a>
</mat-list-item>
<!-- RBI Integration Section -->
<div class="nav-item-sep mt-1">
<mat-divider></mat-divider>
<span class="text-muted">{{ 'labels.menus.RBI Integration' | translate }}</span>
</div>
<mat-list-item
[routerLink]="['/cfcfrms/fmr-submission']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.FMR Submission' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="university" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.FMR Submission' | translate }}</a>
</mat-list-item>
<mat-list-item
[routerLink]="['/cfcfrms/cfr-lookup']"
[matTooltipPosition]="tooltipPosition"
matTooltip="{{ 'tooltips.CFR Lookup' | translate }}"
routerLinkActive="active-menu"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-icon matListIcon>
<fa-icon icon="search" size="sm"></fa-icon>
</mat-icon>
<a matLine>{{ 'labels.menus.CFR Lookup' | translate }}</a>
</mat-list-item>
<div class="nav-item-sep mt-1">
<mat-divider></mat-divider>
<span class="text-muted">{{ 'labels.menus.Main Items' | translate }}</span>
</div>
<mat-list-item
[routerLink]="['/rbi-circulars']"
[matTooltipPosition]="tooltipPosition"
+214 -78
View File
@@ -35,87 +35,14 @@
</button>
}
<div class="tab-nav-bar">
<a
#institution
class="tab-link"
[matMenuTriggerFor]="institutionMenu"
#institutionMenuTrigger="matMenuTrigger"
tabindex="0"
*mifosxHasPermission="'READ_INSTITUTION'"
>
<fa-icon class="mr-05" icon="university" size="lg"></fa-icon>
{{ 'labels.menus.Institution' | translate }}
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
<span class="hide-lt-md">
<a
#tasks
class="tab-link tasks-link"
[routerLink]="['/checker-inbox-and-tasks/checker-inbox']"
tabindex="0"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<fa-icon class="mr-05" icon="check" size="lg"></fa-icon>
{{ 'labels.menus.Tasks' | translate }}
</a>
<a
#accounting
class="tab-link accounting-link"
[routerLink]="['/accounting']"
tabindex="0"
*mifosxHasPermission="'READ_ACCOUNTING'"
>
<fa-icon class="mr-05" icon="money-bill-alt" size="lg"></fa-icon>
{{ 'labels.menus.Accounting' | translate }}
</a>
<a
#reports
class="tab-link"
[matMenuTriggerFor]="reportsMenu"
#reportsMenuTrigger="matMenuTrigger"
tabindex="0"
*mifosxHasPermission="'READ_REPORTS'"
>
<fa-icon class="mr-05" icon="chart-bar" size="lg"></fa-icon>
{{ 'labels.menus.Reports' | translate }}
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
<a
#admin
class="tab-link"
[matMenuTriggerFor]="adminMenu"
#adminMenuTrigger="matMenuTrigger"
tabindex="0"
*mifosxHasPermission="'READ_ADMIN'"
>
<fa-icon class="mr-05" icon="shield-alt" size="lg"></fa-icon>
{{ 'labels.menus.Admin' | translate }}
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
<a #configWizard class="tab-link" (click)="openDialog()" tabindex="0" *mifosxHasPermission="'READ_CONFIG_WIZARD'">
<fa-icon class="mr-05" icon="info" size="lg"></fa-icon>
{{ 'labels.menus.Configuration Wizard' | translate }}
</a>
<a class="tab-link" [matMenuTriggerFor]="ledgerMenu" tabindex="0">
<fa-icon class="mr-05" icon="book" size="lg"></fa-icon>
Ledger
<fa-icon icon="caret-down" class="ml-05"></fa-icon>
</a>
</span>
</div>
<span class="toolbar-spacer"></span>
<div #notifications>
<mifosx-notifications-tray #notificationsTray class="hide-lt-md" tabindex="0"></mifosx-notifications-tray>
</div>
<div #themeToggle>
<mifosx-theme-toggle tabindex="0"></mifosx-theme-toggle>
</div>
<div #appMenu>
<button mat-icon-button class="ml-1 img-button" [matMenuTriggerFor]="applicationMenu" tabindex="0">
<img src="assets/images/user_placeholder.png" [alt]="'labels.headings.User Profile' | translate" />
@@ -144,11 +71,220 @@
</span>
</mat-menu>
<!-- Ledger Menu -->
<mat-menu #ledgerMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item tabindex="0">Sub Ledger</button>
<button mat-menu-item tabindex="0">General Ledger</button>
</mat-menu>
<!-- Sub Ledger Mega Menu -->
@if (subLedgerOpen) {
<div class="subledger-backdrop" (click)="subLedgerOpen = false"></div>
<div class="subledger-mega-menu">
<input
type="text"
class="subledger-search"
placeholder="Search menu..."
(input)="subLedgerSearch = $event.target.value"
/>
<div class="subledger-columns">
<!-- Transactions -->
<div class="subledger-group">
<div class="subledger-group-title">Transactions</div>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Cash / Clearing / Transfer')"
(click)="subLedgerOpen = false"
>
<fa-icon icon="money-bill" class="subledger-icon"></fa-icon> Cash / Clearing / Transfer
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Batch Transaction')" (click)="subLedgerOpen = false">
<fa-icon icon="layer-group" class="subledger-icon"></fa-icon> Batch Transaction
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Draft Reconcile')" (click)="subLedgerOpen = false">
<fa-icon icon="file-invoice" class="subledger-icon"></fa-icon> Draft Reconcile
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Insurance Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="shield-alt" class="subledger-icon"></fa-icon> Insurance Entry
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Share Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="chart-line" class="subledger-icon"></fa-icon> Share Entry
</a>
</div>
<!-- Instruments -->
<div class="subledger-group">
<div class="subledger-group-title">Instruments</div>
<a class="subledger-link" [class.hidden]="!matchesSearch('Draft / Payorder')" (click)="subLedgerOpen = false">
<fa-icon icon="file-signature" class="subledger-icon"></fa-icon> Draft / Payorder
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Passbook Issue')" (click)="subLedgerOpen = false">
<fa-icon icon="book" class="subledger-icon"></fa-icon> Passbook Issue
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Cheque Book')" (click)="subLedgerOpen = false">
<fa-icon icon="credit-card" class="subledger-icon"></fa-icon> Cheque Book
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('DD Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="money-check-alt" class="subledger-icon"></fa-icon> DD Entry
</a>
</div>
<!-- Deposits -->
<div class="subledger-group">
<div class="subledger-group-title">Deposits</div>
<a class="subledger-link" [class.hidden]="!matchesSearch('FD / RD Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="piggy-bank" class="subledger-icon"></fa-icon> FD / RD Entry
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('DDS Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="coins" class="subledger-icon"></fa-icon> DDS Entry
</a>
</div>
<!-- Customer -->
<div class="subledger-group">
<div class="subledger-group-title">Customer</div>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Client Master')"
routerLink="/clients"
(click)="subLedgerOpen = false"
>
<fa-icon icon="user" class="subledger-icon"></fa-icon> Client Master
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Account Master')" (click)="subLedgerOpen = false">
<fa-icon icon="id-card" class="subledger-icon"></fa-icon> Account Master
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Aadhaar Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="address-card" class="subledger-icon"></fa-icon> Aadhaar Entry
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Minor to Major')" (click)="subLedgerOpen = false">
<fa-icon icon="child" class="subledger-icon"></fa-icon> Minor to Major
</a>
</div>
<!-- Account Status -->
<div class="subledger-group">
<div class="subledger-group-title">Account Status</div>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Inoperative to Operative')"
(click)="subLedgerOpen = false"
>
<fa-icon icon="lock-open" class="subledger-icon"></fa-icon> Inoperative &rarr; Operative
</a>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Account Status Master')"
(click)="subLedgerOpen = false"
>
<fa-icon icon="cog" class="subledger-icon"></fa-icon> Account Status Master
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Restrict Account')" (click)="subLedgerOpen = false">
<fa-icon icon="ban" class="subledger-icon"></fa-icon> Restrict Account
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Account Death')" (click)="subLedgerOpen = false">
<fa-icon icon="user-slash" class="subledger-icon"></fa-icon> Account Death
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Closed Account')" (click)="subLedgerOpen = false">
<fa-icon icon="times" class="subledger-icon"></fa-icon> Closed Account
</a>
</div>
<!-- Others -->
<div class="subledger-group">
<div class="subledger-group-title">Others</div>
<a class="subledger-link" [class.hidden]="!matchesSearch('Suit File Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="balance-scale" class="subledger-icon"></fa-icon> Suit File Entry
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Locker Entry')" (click)="subLedgerOpen = false">
<fa-icon icon="lock" class="subledger-icon"></fa-icon> Locker Entry
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Holiday Marking')" (click)="subLedgerOpen = false">
<fa-icon icon="calendar" class="subledger-icon"></fa-icon> Holiday Marking
</a>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Open Closed Register')"
(click)="subLedgerOpen = false"
>
<fa-icon icon="book-open" class="subledger-icon"></fa-icon> Open Closed Register
</a>
</div>
</div>
</div>
}
<!-- General Ledger Mega Menu -->
@if (glMenuOpen) {
<div class="subledger-backdrop" (click)="glMenuOpen = false"></div>
<div class="subledger-mega-menu">
<input
type="text"
class="subledger-search"
placeholder="Search GL reports..."
(input)="glSearch = $event.target.value"
/>
<div class="subledger-columns">
<div class="subledger-group">
<div class="subledger-group-title">Financial Statements</div>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Trial Balance')" (click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="subledger-icon"></fa-icon> Trial Balance</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Profit and Loss')" (click)="glMenuOpen = false"
><fa-icon icon="chart-line" class="subledger-icon"></fa-icon> Profit and Loss</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Consolidate Balance Sheet')"
(click)="glMenuOpen = false"
><fa-icon icon="file-alt" class="subledger-icon"></fa-icon> Consolidate Balance Sheet</a
>
</div>
<div class="subledger-group">
<div class="subledger-group-title">GL Reports</div>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('GL Average Balance')" (click)="glMenuOpen = false"
><fa-icon icon="calculator" class="subledger-icon"></fa-icon> GL Average Balance</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('GL Mapping for Report')"
(click)="glMenuOpen = false"
><fa-icon icon="project-diagram" class="subledger-icon"></fa-icon> GL Mapping for Report</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Account wise Balance List')"
(click)="glMenuOpen = false"
><fa-icon icon="list" class="subledger-icon"></fa-icon> Account wise Balance List</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Distribution of Balance')"
(click)="glMenuOpen = false"
><fa-icon icon="chart-pie" class="subledger-icon"></fa-icon> Distribution of Balance</a
>
</div>
<div class="subledger-group">
<div class="subledger-group-title">Account Reports</div>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Client wise Balance Report')"
(click)="glMenuOpen = false"
><fa-icon icon="users" class="subledger-icon"></fa-icon> Client wise Balance Report</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Contingent Expense')" (click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="subledger-icon"></fa-icon> Contingent Expense</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Banker Head Report')" (click)="glMenuOpen = false"
><fa-icon icon="university" class="subledger-icon"></fa-icon> Banker Head Report</a
>
</div>
<div class="subledger-group">
<div class="subledger-group-title">Registers</div>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Sundry Register')" (click)="glMenuOpen = false"
><fa-icon icon="clipboard-list" class="subledger-icon"></fa-icon> Sundry Register</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Voucher Register')" (click)="glMenuOpen = false"
><fa-icon icon="receipt" class="subledger-icon"></fa-icon> Voucher Register</a
>
</div>
</div>
</div>
}
<!-- Toolbar Menus -->
<mat-menu #reportsMenu="matMenu" [overlapTrigger]="false">
+125 -27
View File
@@ -7,28 +7,25 @@
*/
#mifosx-toolbar {
position: relative;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
box-shadow: none;
background: transparent;
color: #fff;
pointer-events: none;
min-height: 0;
height: 44px;
padding: 0 4px;
.tab-nav-bar {
display: flex;
align-items: center;
padding: 0 1rem;
* {
pointer-events: auto;
}
.tab-link {
min-width: auto;
padding: 15px 0.5rem;
text-decoration: none;
font-weight: 500;
font-size: 14px;
color: rgb(255 255 255 / 70%);
&:hover {
color: #fff;
}
cursor: pointer;
.mat-mdc-icon-button {
color: #fff;
}
.img-button {
@@ -39,20 +36,121 @@
}
}
.ml-05 {
margin-left: 0.35rem;
font-size: 12px;
}
.ml-1 {
margin-left: 1rem;
margin-left: 0.5rem;
}
}
.mr-1 {
margin-right: 1rem;
}
::host-context(.dark-theme) #mifosx-toolbar {
// transparent, inherits from sub-menu-bar background
}
.toolbar-spacer {
flex: 1 1 auto;
}
/* Toolbar Deposit Mega Grid */
.toolbar-deposit-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 24px;
padding: 8px 12px;
min-width: 600px;
}
.toolbar-deposit-group {
.toolbar-deposit-title {
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #1074b9;
padding: 4px 16px 6px;
margin-bottom: 2px;
border-bottom: 2px solid #1074b9;
}
}
/* Sub Ledger Mega Menu */
.subledger-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
.subledger-mega-menu {
position: absolute;
top: 64px;
left: 50%;
transform: translateX(-50%);
background: #fff;
width: 800px;
max-width: 95vw;
padding: 16px 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
border-radius: 0 0 8px 8px;
z-index: 1000;
}
.subledger-search {
width: 100%;
padding: 8px 12px;
margin-bottom: 14px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
&:focus {
outline: none;
border-color: #1074b9;
}
}
.subledger-columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.subledger-group-title {
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #2c3e50;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 2px solid #1074b9;
}
.subledger-link {
display: flex;
align-items: center;
padding: 5px 4px;
text-decoration: none;
color: #333;
font-size: 13px;
cursor: pointer;
border-radius: 4px;
transition: background 0.15s;
&:hover {
background: #e3f2fd;
color: #1074b9;
}
&.hidden {
display: none;
}
}
.subledger-icon {
width: 18px;
margin-right: 8px;
color: #666;
font-size: 13px;
}
@@ -102,6 +102,19 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec
/** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = true;
/** Sub Ledger mega menu state */
subLedgerOpen = false;
subLedgerSearch = '';
/** General Ledger mega menu state */
glMenuOpen = false;
glSearch = '';
matchesGlSearch(label: string): boolean {
if (!this.glSearch) return true;
return label.toLowerCase().includes(this.glSearch.toLowerCase());
}
/** Instance of sidenav. */
@Input() sidenav: MatSidenav;
/** Sidenav collapse event. */
@@ -251,6 +264,11 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec
}
}
matchesSearch(label: string): boolean {
if (!this.subLedgerSearch) return true;
return label.toLowerCase().includes(this.subLedgerSearch.toLowerCase());
}
navigateMenu(routePath: string): void {
this.router.navigate([routePath]);
}
+241 -91
View File
@@ -7,132 +7,282 @@
-->
<div class="cbs-dashboard-container">
<div class="cbs-header">Core Banking System Dashboard</div>
<div class="cbs-header">
<span class="header-title">Core Banking System Dashboard</span>
<span class="header-date">{{ today | date: 'mediumDate' }}</span>
</div>
<div class="cbs-content">
<!-- Search Activity -->
<div class="search-row">
<mat-card-content>
<mat-form-field class="search-field">
<mat-label>{{ 'labels.placeholders.Search Activity' | translate }}</mat-label>
<input matInput [matAutocomplete]="activitySearch" [formControl]="searchText" />
</mat-form-field>
<mat-autocomplete #activitySearch="matAutocomplete">
@for (activity of filteredActivities | async; track activity) {
<mat-option [routerLink]="activity.path">
<span>{{ activity.activity }}</span>
</mat-option>
}
</mat-autocomplete>
</mat-card-content>
<!-- ═══════ SECTION: Regulatory & Compliance ═══════ -->
<div class="section-header regulatory">
<fa-icon icon="shield-alt" class="section-icon"></fa-icon>
<span>Regulatory &amp; Compliance</span>
</div>
<!-- Regulatory & Compliance -->
<div class="section-title">Regulatory & Compliance</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-title">CRR / SLR</div>
<div class="stat-card-value">&#8377; 125 Cr</div>
<div class="stat-trend up">&#9650; Within Limits</div>
<div class="cards-row four-col">
<!-- CRR / SRR -->
<div class="dash-card mini">
<div class="mini-icon blue"><fa-icon icon="lock"></fa-icon></div>
<div class="mini-label">CRR / SRR</div>
<div class="mini-value">&#8377; 20.14 Cr</div>
<div class="mini-sub">8% <span class="tag-success">Within Limits</span></div>
</div>
<div class="stat-card">
<div class="stat-card-title">CRAR</div>
<div class="stat-card-value">13.42%</div>
<div class="stat-trend up">&#9650; Healthy</div>
<!-- CRAR -->
<div class="dash-card mini">
<div class="mini-icon green"><fa-icon icon="chart-line"></fa-icon></div>
<div class="mini-label">CRAR %</div>
<div class="mini-value">21%</div>
<div class="mini-sub"><span class="tag-success">Healthy (Min 9%)</span></div>
</div>
<div class="stat-card">
<div class="stat-card-title">DICGC (Samyak)</div>
<div class="stat-card-value">Updated</div>
<div class="stat-trend">Last Sync: Today</div>
<!-- DICSC with table inside -->
<div class="dash-card compact">
<div class="compact-header"><fa-icon icon="database" class="compact-icon purple-text"></fa-icon> DICSC</div>
<table class="mini-table">
<thead>
<tr>
<th></th>
<th>Freeze</th>
<th>Inop.</th>
<th>Rectified</th>
</tr>
</thead>
<tbody>
<tr>
<td class="row-label">Aadhaar</td>
<td>2,000</td>
<td>1,000</td>
<td>1,100</td>
</tr>
<tr>
<td class="row-label">PAN</td>
<td>3,000</td>
<td>2,000</td>
<td>2,200</td>
</tr>
<tr>
<td class="row-label">Mobile</td>
<td>2,000</td>
<td>—</td>
<td>100</td>
</tr>
</tbody>
</table>
</div>
<div class="stat-card">
<div class="stat-card-title">Interbank Exposure</div>
<div class="stat-card-value">72%</div>
<div class="stat-trend down">&#9660; Near Limit</div>
<!-- Interbank Exposure with details inside -->
<div class="dash-card compact">
<div class="compact-header">
<fa-icon icon="exchange" class="compact-icon orange-text"></fa-icon> Interbank Exposure — 20%
</div>
<div class="exposure-list">
<div class="exposure-row">
<span>Yes Bank</span><span class="exposure-pct">3%</span><span class="badge maintain">Maintain</span>
</div>
<div class="exposure-row">
<span>IDBI</span><span class="exposure-pct">11%</span><span class="badge exceed">Exceed</span>
</div>
<div class="exposure-row">
<span>Other Bank</span><span class="exposure-pct">6%</span><span class="badge exceed">Exceed</span>
</div>
</div>
</div>
</div>
<!-- Financial Health -->
<div class="section-title">Financial Health</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-title">Net Worth</div>
<div class="stat-card-value">&#8377; 342 Cr</div>
<!-- ═══════ SECTION: Financial Health ═══════ -->
<div class="section-header financial">
<fa-icon icon="chart-bar" class="section-icon"></fa-icon>
<span>Financial Health</span>
</div>
<div class="cards-row five-col">
<div class="dash-card mini">
<div class="mini-icon blue"><fa-icon icon="university"></fa-icon></div>
<div class="mini-label">Net Worth</div>
<div class="mini-value">&#8377; 342 Cr</div>
</div>
<div class="stat-card">
<div class="stat-card-title">CD Ratio</div>
<div class="stat-card-value">68.2%</div>
<div class="dash-card mini">
<div class="mini-icon green"><fa-icon icon="chart-pie"></fa-icon></div>
<div class="mini-label">CD Ratio</div>
<div class="mini-value">65%</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Capital Working</div>
<div class="stat-card-value">&#8377; 210 Cr</div>
<div class="dash-card mini">
<div class="mini-icon purple"><fa-icon icon="cogs"></fa-icon></div>
<div class="mini-label">Working Capital</div>
<div class="mini-value">&#8377; 210 Cr</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Per Employee Income</div>
<div class="stat-card-value">&#8377; 8.4 L</div>
<div class="dash-card mini">
<div class="mini-icon orange"><fa-icon icon="user-tie"></fa-icon></div>
<div class="mini-label">Per Employee Profit</div>
<div class="mini-value">&#8377; 10 Lakh</div>
</div>
<div class="dash-card mini">
<div class="mini-icon red"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<div class="mini-label">Advance &lt; 25 Lakh</div>
<div class="mini-value">55%</div>
</div>
</div>
<!-- Deposits & Advances -->
<div class="section-title">Deposits & Advances</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-title">Total Deposits</div>
<div class="stat-card-value">&#8377; 1,240 Cr</div>
<!-- ═══════ SECTION: Deposits & Advances ═══════ -->
<div class="section-header deposits">
<fa-icon icon="piggy-bank" class="section-icon"></fa-icon>
<span>Deposits &amp; Advances</span>
</div>
<div class="cards-row three-col">
<!-- Deposits & Advances Summary -->
<div class="dash-card">
<div class="card-top">
<div class="card-icon-wrap blue"><fa-icon icon="coins"></fa-icon></div>
<div class="card-label">Deposits vs Advances</div>
</div>
<div class="da-compare">
<div class="da-item">
<div class="da-label">Total Deposits</div>
<div class="da-value deposit">&#8377; 3,264.42 Cr</div>
<div class="da-bar"><div class="da-fill deposit-fill" style="width: 78%"></div></div>
</div>
<div class="da-item">
<div class="da-label">Total Advances</div>
<div class="da-value advance">&#8377; 4,162.65 Cr</div>
<div class="da-bar"><div class="da-fill advance-fill" style="width: 100%"></div></div>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Total Advances</div>
<div class="stat-card-value">&#8377; 845 Cr</div>
<!-- PSL -->
<div class="dash-card">
<div class="card-top">
<div class="card-icon-wrap green"><fa-icon icon="flag"></fa-icon></div>
<div class="card-label">Priority Sector Lending (PSL)</div>
</div>
<div class="card-big-value">65%</div>
<div class="psl-breakdown">
<div class="psl-row">
<span class="psl-label">Weaker Section</span>
<div class="psl-bar-wrap">
<div class="psl-bar weaker" style="width: 15%"></div>
</div>
<span class="psl-pct">15%</span>
</div>
<div class="psl-row">
<span class="psl-label">MSME</span>
<div class="psl-bar-wrap">
<div class="psl-bar msme" style="width: 7%"></div>
</div>
<span class="psl-pct">7%</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Decay Analysis</div>
<div class="stat-card-value">Moderate</div>
</div>
<div class="stat-card">
<div class="stat-card-title">TSWM</div>
<div class="stat-card-value">Active</div>
<!-- Decay & TSWM -->
<div class="dash-card">
<div class="card-top">
<div class="card-icon-wrap purple"><fa-icon icon="chart-bar"></fa-icon></div>
<div class="card-label">Portfolio Quality</div>
</div>
<div class="quality-grid">
<div class="quality-item">
<div class="quality-label">Decay Analysis</div>
<div class="quality-badge moderate">Moderate</div>
</div>
<div class="quality-item">
<div class="quality-label">TSWM</div>
<div class="quality-badge active">Active</div>
</div>
</div>
</div>
</div>
<!-- Risk & NPA Monitoring -->
<div class="section-title">Risk & NPA Monitoring</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-title">Gross NPA</div>
<div class="stat-card-value">4.82%</div>
<!-- ═══════ SECTION: Risk & NPA Monitoring ═══════ -->
<div class="section-header risk">
<fa-icon icon="exclamation-triangle" class="section-icon"></fa-icon>
<span>Risk &amp; NPA Monitoring</span>
</div>
<div class="cards-row three-col">
<!-- Gross NPA -->
<div class="dash-card npa-card">
<div class="card-top">
<div class="card-icon-wrap red"><fa-icon icon="chart-line"></fa-icon></div>
<div class="card-label">Gross NPA</div>
</div>
<div class="npa-gauge">
<div class="npa-circle gross">
<span class="npa-pct">7%</span>
</div>
<div class="npa-detail">
<div class="npa-status warning"><fa-icon icon="exclamation-triangle"></fa-icon> Above Threshold</div>
<div class="npa-note">RBI norm: &lt; 5%</div>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Net NPA</div>
<div class="stat-card-value">2.11%</div>
<!-- Net NPA -->
<div class="dash-card npa-card">
<div class="card-top">
<div class="card-icon-wrap orange"><fa-icon icon="chart-line"></fa-icon></div>
<div class="card-label">Net NPA</div>
</div>
<div class="npa-gauge">
<div class="npa-circle net">
<span class="npa-pct">2%</span>
</div>
<div class="npa-detail">
<div class="npa-status success"><fa-icon icon="check-circle"></fa-icon> Within Limits</div>
<div class="npa-note">RBI norm: &lt; 3%</div>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Risk-wise Analysis</div>
<div class="stat-card-value">Stable</div>
<!-- Risk Analysis -->
<div class="dash-card">
<div class="card-top">
<div class="card-icon-wrap blue"><fa-icon icon="shield-alt"></fa-icon></div>
<div class="card-label">Risk-wise Analysis</div>
</div>
<div class="risk-summary">
<div class="risk-item">
<span class="risk-dot green"></span>
<span>Low Risk</span>
<span class="risk-val">62%</span>
</div>
<div class="risk-item">
<span class="risk-dot yellow"></span>
<span>Medium Risk</span>
<span class="risk-val">25%</span>
</div>
<div class="risk-item">
<span class="risk-dot red"></span>
<span>High Risk</span>
<span class="risk-val">13%</span>
</div>
</div>
<div class="card-status info"><fa-icon icon="info-circle"></fa-icon> Overall: Stable</div>
</div>
</div>
<!-- Alerts & Notifications -->
<div class="section-title">Alerts & Notifications</div>
<!-- ═══════ SECTION: Alerts & Notifications ═══════ -->
<div class="section-header alerts">
<fa-icon icon="bell" class="section-icon"></fa-icon>
<span>Alerts &amp; Notifications</span>
</div>
<div class="alert-panel">
<span>&#9888; CRAR approaching minimum threshold.</span><br />
<span>&#9888; Interbank exposure nearing limit.</span><br />
<span>&#9888; RBI circular pending review.</span>
<div class="alert-item warning">
<fa-icon icon="exclamation-triangle"></fa-icon> Gross NPA at 7% — above RBI threshold of 5%. Immediate review
required.
</div>
<div class="alert-item warning">
<fa-icon icon="exclamation-triangle"></fa-icon> IDBI interbank exposure at 11% — exceeds single-party limit.
</div>
<div class="alert-item info"><fa-icon icon="info-circle"></fa-icon> DICSC data synced successfully today.</div>
<div class="alert-item info">
<fa-icon icon="info-circle"></fa-icon> RBI circular pending review — compliance action due by month end.
</div>
</div>
<!-- Existing Charts -->
<div class="section-title">Analytics</div>
<!-- ═══════ SECTION: Analytics Charts ═══════ -->
<div class="section-header analytics">
<fa-icon icon="chart-bar" class="section-icon"></fa-icon>
<span>Analytics</span>
</div>
<div class="charts-section">
<div class="top-card">
<div class="trends-bar">
+661 -67
View File
@@ -6,27 +6,45 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$navy: #0d1b2a;
$blue: #1e88e5;
$green: #10b981;
$orange: #f59e0b;
$red: #ef4444;
$purple: #7c3aed;
.cbs-dashboard-container {
background: #f4f6f9;
background: #f0f2f5;
min-height: 100vh;
}
.cbs-header {
background: #1f3b73;
background: linear-gradient(135deg, $navy 0%, #1b3a5c 100%);
color: white;
padding: 15px 25px;
font-size: 20px;
font-weight: bold;
padding: 6px 24px;
font-size: 16px;
font-weight: 700;
display: flex;
justify-content: space-between;
align-items: center;
margin: 0;
.header-date {
font-size: 11px;
font-weight: 400;
opacity: 0.8;
}
}
.cbs-content {
padding: 20px;
padding: 0 24px 12px;
max-width: 1440px;
margin: 0 auto;
}
.search-row {
width: 100%;
margin-bottom: 10px;
margin: 0;
mat-card-content {
width: 100%;
}
@@ -34,69 +52,625 @@
.search-field {
width: 100%;
}
}
.section-title {
margin: 20px 0 10px;
font-size: 16px;
font-weight: bold;
color: #333;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 15px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 6px rgb(0 0 0 / 8%);
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 12%);
.mat-mdc-form-field {
margin-bottom: 0;
}
}
.stat-card-title {
/* ─── Section Headers ─── */
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin: 6px 0 6px;
padding: 5px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.3px;
.section-icon {
font-size: 16px;
}
&.regulatory {
background: #e3f2fd;
color: #0d47a1;
}
&.financial {
background: #e8f5e9;
color: #1b5e20;
}
&.deposits {
background: #f3e5f5;
color: #6a1b9a;
}
&.risk {
background: #fce4ec;
color: #b71c1c;
}
&.alerts {
background: #fff3e0;
color: #e65100;
}
&.analytics {
background: #e0f2f1;
color: #004d40;
}
}
/* ─── Card Grid Layouts ─── */
.cards-row {
display: grid;
gap: 12px;
}
.four-col {
grid-template-columns: repeat(4, 1fr);
}
.five-col {
grid-template-columns: repeat(5, 1fr);
}
.three-col {
grid-template-columns: repeat(3, 1fr);
}
.two-col {
grid-template-columns: repeat(2, 1fr);
}
/* ─── Dashboard Cards ─── */
.dash-card {
background: #fff;
border-radius: 8px;
padding: 14px;
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
border: 1px solid #e8ecf0;
transition:
box-shadow 0.2s,
transform 0.15s;
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 12%);
transform: translateY(-1px);
}
}
.card-top {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.card-icon-wrap {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
flex-shrink: 0;
&.blue {
background: $blue;
}
&.green {
background: $green;
}
&.orange {
background: $orange;
}
&.red {
background: $red;
}
&.purple {
background: $purple;
}
}
.card-label {
font-size: 13px;
color: #777;
font-weight: 600;
color: #475569;
}
.card-big-value {
font-size: 32px;
font-weight: 800;
color: $navy;
margin-bottom: 8px;
}
.stat-card-value {
font-size: 22px;
font-weight: bold;
color: #1f3b73;
}
.stat-trend {
/* ─── Card Status Badges ─── */
.card-status {
font-size: 12px;
margin-top: 5px;
color: #888;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
display: inline-flex;
align-items: center;
gap: 5px;
&.up {
color: green;
&.success {
background: #ecfdf5;
color: #065f46;
}
&.down {
color: red;
&.warning {
background: #fffbeb;
color: #92400e;
}
&.info {
background: #eff6ff;
color: #1e40af;
}
}
.alert-panel {
background: #fff3f3;
border-left: 4px solid red;
padding: 10px 15px;
border-radius: 6px;
/* ─── CRR/SRR Metric Rows ─── */
.card-metrics {
margin-bottom: 10px;
}
.metric-row {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #f1f5f9;
&:last-child {
border-bottom: none;
}
}
.metric-label {
font-size: 12px;
color: #94a3b8;
}
.metric-value {
font-size: 14px;
font-weight: 700;
color: $navy;
}
/* ─── DICSC Mini Table ─── */
.dicsc-card {
padding: 14px;
}
.mini-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
margin-top: 4px;
th {
text-align: right;
font-weight: 600;
color: #64748b;
padding: 4px 6px;
border-bottom: 2px solid #e2e8f0;
font-size: 11px;
&:first-child {
text-align: left;
}
}
td {
text-align: right;
padding: 5px 6px;
color: $navy;
font-weight: 600;
border-bottom: 1px solid #f1f5f9;
}
.row-label {
text-align: left;
color: #64748b;
font-weight: 500;
}
}
/* ─── Interbank Exposure ─── */
.exposure-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.exposure-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
padding: 4px 0;
color: #475569;
}
.badge {
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
&.maintain {
background: #ecfdf5;
color: #065f46;
}
&.exceed {
background: #fef2f2;
color: #991b1b;
}
}
/* ─── Mini Cards (Financial Health) ─── */
.dash-card.mini {
text-align: center;
padding: 16px 12px;
}
.mini-icon {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
font-size: 18px;
color: #fff;
&.blue {
background: $blue;
}
&.green {
background: $green;
}
&.purple {
background: $purple;
}
&.orange {
background: $orange;
}
&.red {
background: $red;
}
}
.mini-label {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
}
.mini-value {
font-size: 22px;
font-weight: 800;
color: $navy;
}
.mini-sub {
font-size: 11px;
color: #64748b;
margin-top: 4px;
}
.tag-success {
background: #ecfdf5;
color: #065f46;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
}
.tag-warning {
background: #fffbeb;
color: #92400e;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
}
.tag-danger {
background: #fef2f2;
color: #991b1b;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
}
/* ─── Compact Detail Cards ─── */
.dash-card.compact {
padding: 12px 16px;
}
.compact-header {
font-size: 13px;
line-height: 1.8;
color: #333;
font-weight: 700;
color: #475569;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.compact-icon {
font-size: 14px;
&.purple-text {
color: $purple;
}
&.orange-text {
color: $orange;
}
}
.exposure-pct {
font-weight: 700;
color: $navy;
margin-left: auto;
margin-right: 8px;
}
/* ─── Deposits vs Advances ─── */
.da-compare {
display: flex;
flex-direction: column;
gap: 14px;
}
.da-item {
.da-label {
font-size: 12px;
color: #64748b;
margin-bottom: 2px;
}
.da-value {
font-size: 20px;
font-weight: 800;
margin-bottom: 6px;
&.deposit {
color: $blue;
}
&.advance {
color: $purple;
}
}
}
.da-bar {
height: 8px;
background: #f1f5f9;
border-radius: 4px;
overflow: hidden;
}
.da-fill {
height: 100%;
border-radius: 4px;
transition: width 1s ease;
&.deposit-fill {
background: linear-gradient(90deg, $blue, #42a5f5);
}
&.advance-fill {
background: linear-gradient(90deg, $purple, #a78bfa);
}
}
/* ─── PSL Breakdown ─── */
.psl-breakdown {
margin-top: 8px;
}
.psl-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.psl-label {
font-size: 12px;
color: #64748b;
width: 100px;
flex-shrink: 0;
}
.psl-bar-wrap {
flex: 1;
height: 10px;
background: #f1f5f9;
border-radius: 5px;
overflow: hidden;
}
.psl-bar {
height: 100%;
border-radius: 5px;
&.weaker {
background: $orange;
}
&.msme {
background: $green;
}
}
.psl-pct {
font-size: 13px;
font-weight: 700;
color: $navy;
width: 36px;
text-align: right;
}
/* ─── Portfolio Quality ─── */
.quality-grid {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 8px;
}
.quality-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.quality-label {
font-size: 13px;
color: #475569;
}
.quality-badge {
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 20px;
&.moderate {
background: #fffbeb;
color: #92400e;
}
&.active {
background: #ecfdf5;
color: #065f46;
}
}
/* ─── NPA Gauge Cards ─── */
.npa-gauge {
display: flex;
align-items: center;
gap: 16px;
margin-top: 4px;
}
.npa-circle {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.gross {
background: conic-gradient($red 0% 7%, #fee2e2 7% 100%);
box-shadow: inset 0 0 0 12px #fff;
}
&.net {
background: conic-gradient($green 0% 2%, #ecfdf5 2% 100%);
box-shadow: inset 0 0 0 12px #fff;
}
}
.npa-pct {
font-size: 18px;
font-weight: 800;
color: $navy;
}
.npa-detail {
.npa-status {
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 4px;
&.success {
color: #065f46;
}
&.warning {
color: #b45309;
}
}
.npa-note {
font-size: 11px;
color: #94a3b8;
}
}
/* ─── Risk Summary ─── */
.risk-summary {
display: flex;
flex-direction: column;
gap: 8px;
margin: 8px 0 12px;
}
.risk-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #475569;
}
.risk-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
&.green {
background: $green;
}
&.yellow {
background: $orange;
}
&.red {
background: $red;
}
}
.risk-val {
margin-left: auto;
font-weight: 700;
color: $navy;
}
/* ─── Alerts ─── */
.alert-panel {
background: #fff;
border-radius: 10px;
padding: 14px 18px;
border: 1px solid #e8ecf0;
display: flex;
flex-direction: column;
gap: 8px;
}
.alert-item {
font-size: 13px;
padding: 8px 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
&.warning {
background: #fffbeb;
color: #92400e;
border-left: 3px solid $orange;
}
&.info {
background: #eff6ff;
color: #1e40af;
border-left: 3px solid $blue;
}
}
/* ─── Charts ─── */
.charts-section {
display: flex;
flex-direction: column;
@@ -125,29 +699,49 @@
margin-top: 30px;
text-align: center;
font-size: 12px;
color: #888;
color: #94a3b8;
padding: 15px 0;
}
/* ─── Responsive ─── */
@media screen and (width <= 1200px) {
.four-col {
grid-template-columns: repeat(2, 1fr);
}
.five-col {
grid-template-columns: repeat(3, 1fr);
}
.three-col {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (width <= 768px) {
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 10px;
.four-col,
.five-col,
.three-col {
grid-template-columns: 1fr;
}
.stat-card-value {
.card-big-value {
font-size: 26px;
}
.mini-value {
font-size: 18px;
}
.charts-section {
.bottom-card {
flex-direction: column;
.cbs-header {
flex-direction: column;
gap: 4px;
text-align: center;
}
.amount-collected,
.amount-disbursed {
flex: 1 0 100%;
width: 100%;
}
.charts-section .bottom-card {
flex-direction: column;
.amount-collected,
.amount-disbursed {
flex: 1 0 100%;
width: 100%;
}
}
}
@@ -14,10 +14,11 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { activities } from '../activities';
import { MatAutocompleteTrigger, MatAutocomplete, MatOption } from '@angular/material/autocomplete';
import { AsyncPipe } from '@angular/common';
import { AsyncPipe, DatePipe } from '@angular/common';
import { ClientTrendsBarComponent } from './client-trends-bar/client-trends-bar.component';
import { AmountDisbursedPieComponent } from './amount-disbursed-pie/amount-disbursed-pie.component';
import { AmountCollectedPieComponent } from './amount-collected-pie/amount-collected-pie.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Dashboard component.
@@ -34,12 +35,16 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
ClientTrendsBarComponent,
AmountDisbursedPieComponent,
AmountCollectedPieComponent,
AsyncPipe
AsyncPipe,
DatePipe,
FaIconComponent
]
})
export class DashboardComponent implements OnInit {
private router = inject(Router);
today = new Date();
/** Array of all user activities */
userActivity: string[];
/** Array of most recent user activities */
+44 -2
View File
@@ -147,7 +147,28 @@ import {
faProjectDiagram,
faFolder,
faSatelliteDish,
faMobileAlt
faMobileAlt,
faBalanceScale,
faCaretUp,
faChild,
faCreditCard,
faFileCsv,
faFileInvoice,
faFileSignature,
faHistory,
faInbox,
faLayerGroup,
faListAlt,
faMoneyCheckAlt,
faPaperPlane,
faUserSlash,
faClipboardList,
faMap,
faCamera,
faSlidersH,
faPrint,
faCertificate,
faPaperclip
} from '@fortawesome/free-solid-svg-icons';
/**
@@ -301,7 +322,28 @@ export class IconsModule {
faProjectDiagram,
faFolder,
faSatelliteDish,
faMobileAlt
faMobileAlt,
faBalanceScale,
faCaretUp,
faChild,
faCreditCard,
faFileCsv,
faFileInvoice,
faFileSignature,
faHistory,
faInbox,
faLayerGroup,
faListAlt,
faMoneyCheckAlt,
faPaperPlane,
faUserSlash,
faClipboardList,
faMap,
faCamera,
faSlidersH,
faPrint,
faCertificate,
faPaperclip
);
}
}
+201 -1
View File
@@ -2986,6 +2986,20 @@
"More": "More",
"Analytics": "Analytics",
"RBI Circulars": "RBI Circulars & Notifications",
"CFCFRMS": "CFCFRMS",
"CFCFRMS Dashboard": "CFCFRMS Dashboard",
"Complaints": "Complaints",
"Complaint Management": "Complaint Management",
"Complaint Detail": "Complaint Detail",
"Complaint Information": "Complaint Information",
"Escalate": "Escalate",
"Escalate Complaint": "Escalate Complaint",
"Report Suspect": "Report Suspect",
"Report Mule": "Report Mule",
"Report Fraud Txn": "Report Fraud Txn",
"RBI Integration": "RBI Integration",
"FMR Submission": "FMR Submission",
"CFR Lookup": "CFR Lookup",
"Navigation": "Navigation",
"Notifications": "Notifications",
"Organization": "Organization",
@@ -3918,7 +3932,185 @@
"Failed to confirm payment. Please try again or contact support.": "Failed to confirm payment. Please try again or contact support.",
"Failed to assign payout. Please try again.": "Failed to assign payout. Please try again.",
"Remittance not found. Please check the reference number and try again.": "Remittance not found. Please check the reference number and try again.",
"Unable to load vendors. Please check your connection and try again.": "Unable to load vendors. Please check your connection and try again."
"Unable to load vendors. Please check your connection and try again.": "Unable to load vendors. Please check your connection and try again.",
"Cyber Fraud Complaint Management Overview": "Cyber Fraud Complaint Management Overview",
"Total Complaints": "Total Complaints",
"Pending Review": "Pending Review",
"Lien Marked": "Lien Marked",
"Funds Recovered": "Funds Recovered",
"Status Distribution": "Status Distribution",
"Fraud Amount by Status": "Fraud Amount by Status",
"Recent Complaints": "Recent Complaints",
"View All": "View All",
"No complaints found": "No complaints found",
"I4C/MHA CFCFRMS Complaint Tickets": "I4C/MHA CFCFRMS Complaint Tickets",
"Export CSV": "Export CSV",
"All": "All",
"Pending": "Pending",
"Active": "Active",
"Resolved": "Resolved",
"Search by Ticket ID / Mobile": "Search by Ticket ID / Mobile",
"Enter ticket ID or mobile...": "Enter ticket ID or mobile...",
"Status": "Status",
"All Statuses": "All Statuses",
"From Date": "From Date",
"To Date": "To Date",
"No complaints match your filters": "No complaints match your filters",
"Clear Filters": "Clear Filters",
"Ticket ID": "Ticket ID",
"Complainant": "Complainant",
"Mobile": "Mobile",
"Fraud Amount": "Fraud Amount",
"Suspect Account": "Suspect Account",
"Created At": "Created At",
"Actions": "Actions",
"View Details": "View Details",
"Back to Complaints": "Back to Complaints",
"Complaint not found": "Complaint not found",
"Back to List": "Back to List",
"Complainant Details": "Complainant Details",
"Name": "Name",
"Email": "Email",
"Address": "Address",
"Fraud Details": "Fraud Details",
"Fraud Type": "Fraud Type",
"Description": "Description",
"Suspect Account Details": "Suspect Account Details",
"Account Number": "Account Number",
"Account Name": "Account Name",
"Bank IFSC": "Bank IFSC",
"Bank Name": "Bank Name",
"Source & Acknowledgement": "Source & Acknowledgement",
"Source Portal": "Source Portal",
"Ack. Number": "Ack. Number",
"Assigned Officer": "Assigned Officer",
"Unassigned": "Unassigned",
"Created": "Created",
"Last Updated": "Last Updated",
"Fund Details": "Fund Details",
"Available Balance": "Available Balance",
"Funds Held": "Funds Held",
"Escalation Info": "Escalation Info",
"Next Hop Account": "Next Hop Account",
"Next Hop IFSC": "Next Hop IFSC",
"Take Action": "Take Action",
"No further actions can be taken": "No further actions can be taken",
"Current Status": "Current Status",
"Select Action": "Select Action",
"Mark Lien": "Mark Lien",
"Freeze Debit": "Freeze Debit",
"Move Funds": "Move Funds",
"Resolve": "Resolve",
"Reject": "Reject",
"Available Balance (INR)": "Available Balance (₹)",
"Funds Held (INR)": "Funds Held (₹)",
"Next Hop Account Number": "Next Hop Account Number",
"Next Hop Bank IFSC": "Next Hop Bank IFSC",
"Action Note": "Action Note",
"Enter remarks...": "Enter remarks...",
"Action is required": "Action is required",
"Available balance is required": "Available balance is required",
"Funds held is required": "Funds held is required",
"Account number is required": "Account number is required",
"IFSC code is required": "IFSC code is required",
"Invalid IFSC format": "Invalid IFSC format (e.g. SBIN0000001)",
"Action note is required": "Action note is required",
"Submit Action": "Submit Action",
"Confirm Action": "Confirm Action",
"Audit Timeline": "Audit Timeline",
"No timeline events yet": "No timeline events yet",
"Forward complaint to the next bank in the transaction chain": "Forward complaint to the next bank in the transaction chain",
"Escalation Form": "Escalation Form",
"Escalation Successful": "Escalation Successful",
"Reference Number": "Reference Number",
"Escalated To": "Escalated To",
"Timestamp": "Timestamp",
"New Escalation": "New Escalation",
"Lookup": "Lookup",
"Enter a Ticket ID first": "Enter a Ticket ID first",
"Ticket not found": "Ticket not found",
"Confirm Escalation": "Confirm Escalation",
"Remarks": "Remarks",
"Reason for escalation...": "Reason for escalation...",
"Remarks are required": "Remarks are required",
"Submit Escalation": "Submit Escalation",
"Escalation Guidelines": "Escalation Guidelines",
"Verify the complaint details before escalating to the next bank.": "Verify the complaint details before escalating to the next bank.",
"Ensure the Next Hop Account and IFSC belong to the next bank in the fraud transaction chain.": "Ensure the Next Hop Account and IFSC belong to the next bank in the fraud transaction chain.",
"Provide clear and detailed remarks explaining the escalation reason.": "Provide clear and detailed remarks explaining the escalation reason.",
"Escalation must be completed within the I4C/MHA mandated timeline.": "Escalation must be completed within the I4C/MHA mandated timeline.",
"Once escalated, the complaint will be forwarded to the target bank for action.": "Once escalated, the complaint will be forwarded to the target bank for action.",
"Current Bank Info": "Current Bank Info",
"CSV exported successfully": "CSV exported successfully",
"Failed to load complaints": "Failed to load complaints",
"Failed to load complaint details": "Failed to load complaint details",
"Action submitted successfully": "Action submitted successfully",
"Failed to submit action": "Failed to submit action",
"Complaint escalated successfully": "Complaint escalated successfully",
"Failed to escalate complaint": "Failed to escalate complaint",
"Report Suspect Account": "Report Suspect Account",
"Report internally detected suspect account to I4C": "Report internally detected suspect account to I4C",
"Suspect Account Details": "Suspect Account Details",
"Report Submitted": "Report Submitted",
"New Report": "New Report",
"Suspicion Reason": "Suspicion Reason",
"Detected By": "Detected By",
"Detection Date": "Detection Date",
"Linked Transaction IDs": "Linked Transaction IDs",
"Submit Report": "Submit Report",
"Report Mule Account": "Report Mule Account",
"Flag a mule account proactively to I4C/CFCFRMS": "Flag a mule account proactively to I4C/CFCFRMS",
"Mule Account Details": "Mule Account Details",
"Mule Account Flagged": "Mule Account Flagged",
"Mule Type": "Mule Type",
"Transaction Pattern": "Transaction Pattern",
"Total Suspicious Amount (INR)": "Total Suspicious Amount (₹)",
"Linked Fraud Ticket IDs": "Linked Fraud Ticket IDs",
"Report Fraud Transaction": "Report Fraud Transaction",
"Report a detected fraud transaction to I4C": "Report a detected fraud transaction to I4C",
"Fraud Transaction Details": "Fraud Transaction Details",
"Fraud Transaction Reported": "Fraud Transaction Reported",
"Transaction ID": "Transaction ID",
"Amount (INR)": "Amount (₹)",
"From Account": "From Account",
"To Account": "To Account",
"Transaction Date": "Transaction Date",
"Channel": "Channel",
"Linked CFCFRMS Ticket": "Linked CFCFRMS Ticket",
"FMR Submission": "FMR Submission",
"Submit Fraud Monitoring Return to RBI FUA System": "Submit Fraud Monitoring Return to RBI FUA System",
"Fraud Monitoring Return (FMR)": "Fraud Monitoring Return (FMR)",
"FMR Submitted Successfully": "FMR Submitted Successfully",
"FMR Reference": "FMR Reference",
"Fraud Category": "Fraud Category",
"Sub-Category": "Sub-Category",
"Fraud Amount (INR)": "Fraud Amount (₹)",
"Account Holder Name": "Account Holder Name",
"Date of Detection": "Date of Detection",
"Date of Fraud": "Date of Fraud",
"Modus Operandi": "Modus Operandi",
"Action Taken": "Action Taken",
"Staff Involved in Fraud": "Staff Involved in Fraud",
"Submitted At": "Submitted At",
"Next FMR Due Date": "Next FMR Due Date",
"New FMR": "New FMR",
"Reporting Bank IFSC": "Reporting Bank IFSC",
"Reporting Bank Name": "Reporting Bank Name",
"Submit to RBI": "Submit to RBI",
"CFR Lookup": "CFR Lookup",
"Query RBI Central Fraud Registry before customer onboarding": "Query RBI Central Fraud Registry before customer onboarding",
"Search CFR": "Search CFR",
"Quick Demo": "Quick Demo",
"Querying RBI Central Fraud Registry...": "Querying RBI Central Fraud Registry...",
"Fraud Records Found": "Fraud Records Found",
"No Fraud Records Found": "No Fraud Records Found",
"Query Time": "Query Time",
"Reference": "Reference",
"Category": "Category",
"Amount": "Amount",
"Reporting Bank": "Reporting Bank",
"Date Reported": "Date Reported",
"This account has no entries in the Central Fraud Registry. Safe to proceed with onboarding.": "This account has no entries in the Central Fraud Registry. Safe to proceed with onboarding."
},
"auditTrail": {
"actions": {
@@ -4029,6 +4221,14 @@
"Add Filters": "Add Filters",
"Analytics": "Analytics Dashboard (Apache Superset)",
"RBI Circulars": "RBI Circulars & Notifications",
"CFCFRMS Dashboard": "CFCFRMS Cyber Fraud Complaint Dashboard",
"Complaints": "Complaint Ticket Management",
"Escalate Complaint": "Escalate Complaint to Next Bank",
"Report Suspect": "Report Suspect Account to I4C",
"Report Mule Account": "Flag Mule Account to I4C/CFCFRMS",
"Report Fraud Transaction": "Report Detected Fraud Transaction",
"FMR Submission": "Submit Fraud Monitoring Return to RBI",
"CFR Lookup": "Query RBI Central Fraud Registry",
"CKYC": "Central KYC Management",
"Current Account": "Current Account",
"Print Loan Application": "Print Loan Application",
+2 -2
View File
@@ -2,8 +2,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '260317',
'hash': 'g73aa69ee0'
'version': '260318',
'hash': 'g0a32d7135'
},
'allow_switching_backend_instance': true
};
+3
View File
@@ -126,6 +126,9 @@ export const environment = {
openProjectEnabled:
loadedEnv['openProjectEnabled'] === 'true' || loadedEnv['openProjectEnabled'] === true || false,
/** CFCFRMS (Cyber Fraud Complaint) Integration */
cfcfrmsApiUrl: loadedEnv['cfcfrmsApiUrl'] || '',
/** Apache Superset Analytics Integration */
supersetEnabled: loadedEnv['supersetEnabled'] === 'true' || loadedEnv['supersetEnabled'] === true || false,
supersetUrl: loadedEnv['supersetUrl'] || '',
+3
View File
@@ -129,6 +129,9 @@ export const environment = {
/** OpenProject Integration (via Fineract backend) */
openProjectEnabled: loadedEnv.openProjectEnabled === 'true' || loadedEnv.openProjectEnabled === true || false,
/** CFCFRMS (Cyber Fraud Complaint) Integration */
cfcfrmsApiUrl: loadedEnv.cfcfrmsApiUrl || '',
/** Apache Superset Analytics Integration */
supersetEnabled: loadedEnv.supersetEnabled === 'true' || loadedEnv.supersetEnabled === true || true,
supersetUrl: loadedEnv.supersetUrl || 'https://dashboard.solwio.in',
+38
View File
@@ -33,6 +33,44 @@
z-index: 9999;
}
// ===== Sub-menu Mega Panels (rendered in CDK overlay) =====
.deposits-mega-panel,
.hrm-mega-panel {
max-width: 700px !important;
}
.share-mega-panel,
.others-mega-panel {
max-width: 850px !important;
}
.share-grid,
.others-grid {
grid-template-columns: repeat(4, 1fr) !important;
}
.others-grid,
.npa-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.npa-mega-panel,
.advances-mega-panel {
max-width: 800px !important;
}
.advances-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.letter-mega-panel {
max-width: 850px !important;
}
.letter-grid {
grid-template-columns: repeat(4, 1fr) !important;
}
// ===== Material Select Panel Styles (Material Design 3) =====
.mat-mdc-select-panel {
background: var(--md-sys-color-surface-container, #fff);