feat: add CFCFRMS module, bank menus, mock backend, and dashboard redesign
- CFCFRMS cyber fraud complaint management with dashboard, complaints, escalation, report suspect/mule/fraud txn, FMR submission, CFR lookup - Mock HTTP interceptor with 20 dummy complaints and all endpoints - Simulator service for I4C/MHA integration testing - 3-row menu bar with 22 items (7+8+8 layout) - Mega menus for General Ledger, Sub Ledger, Deposits, Share, Others, HRM, RBI with search and grouped columns - Dropdowns for Cashier, Clearing, NPA, CKYC, Advances, Letter Mgmt, Insurance, Digital - Dashboard redesign with regulatory, financial health, deposits, risk/NPA sections using static banking data - All translations added to en-US.json - FontAwesome icons registered for all new menu items Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0a32d7135e
commit
b3a4cce0cf
@@ -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,
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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">₹{{ 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 → 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">
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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 & 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">₹ 125 Cr</div>
|
||||
<div class="stat-trend up">▲ 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">₹ 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">▲ 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">▼ 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">₹ 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">₹ 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">₹ 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">₹ 210 Cr</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-title">Per Employee Income</div>
|
||||
<div class="stat-card-value">₹ 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">₹ 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 < 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">₹ 1,240 Cr</div>
|
||||
<!-- ═══════ SECTION: Deposits & Advances ═══════ -->
|
||||
<div class="section-header deposits">
|
||||
<fa-icon icon="piggy-bank" class="section-icon"></fa-icon>
|
||||
<span>Deposits & 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">₹ 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">₹ 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">₹ 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 & 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: < 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: < 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 & Notifications</span>
|
||||
</div>
|
||||
<div class="alert-panel">
|
||||
<span>⚠ CRAR approaching minimum threshold.</span><br />
|
||||
<span>⚠ Interbank exposure nearing limit.</span><br />
|
||||
<span>⚠ 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">
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +2,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260317',
|
||||
'hash': 'g73aa69ee0'
|
||||
'version': '260318',
|
||||
'hash': 'g0a32d7135'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
@@ -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'] || '',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user