From b3a4cce0cfda4b60c8239832bf02f2791c471476 Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 18 Mar 2026 18:05:43 +0530 Subject: [PATCH] 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) --- src/app/app.module.ts | 2 + src/app/cfcfrms/cfcfrms-routing.module.ts | 87 ++ src/app/cfcfrms/cfcfrms-simulator.service.ts | 83 ++ src/app/cfcfrms/cfcfrms.module.ts | 35 + src/app/cfcfrms/cfcfrms.service.ts | 109 ++ .../cfr-lookup/cfr-lookup.component.html | 127 +++ .../cfr-lookup/cfr-lookup.component.scss | 174 +++ .../cfr-lookup/cfr-lookup.component.ts | 117 +++ .../complaint-detail.component.html | 319 ++++++ .../complaint-detail.component.scss | 284 +++++ .../complaint-detail.component.ts | 264 +++++ .../complaint-list.component.html | 162 +++ .../complaint-list.component.scss | 148 +++ .../complaint-list.component.ts | 221 ++++ .../confirm-dialog.component.ts | 61 ++ .../status-badge/status-badge.component.ts | 81 ++ .../cfcfrms-dashboard.component.html | 139 +++ .../cfcfrms-dashboard.component.scss | 196 ++++ .../dashboard/cfcfrms-dashboard.component.ts | 175 +++ .../cfcfrms/escalate/escalate.component.html | 220 ++++ .../cfcfrms/escalate/escalate.component.scss | 211 ++++ .../cfcfrms/escalate/escalate.component.ts | 169 +++ .../fmr-submission.component.html | 178 ++++ .../fmr-submission.component.scss | 108 ++ .../fmr-submission.component.ts | 192 ++++ src/app/cfcfrms/mock/cfcfrms-mock-data.ts | 849 +++++++++++++++ .../cfcfrms/mock/cfcfrms-mock.interceptor.ts | 618 +++++++++++ src/app/cfcfrms/models/cfcfrms.models.ts | 330 ++++++ .../report-fraud-txn.component.html | 138 +++ .../report-fraud-txn.component.scss | 111 ++ .../report-fraud-txn.component.ts | 159 +++ .../report-mule/report-mule.component.html | 133 +++ .../report-mule/report-mule.component.scss | 106 ++ .../report-mule/report-mule.component.ts | 149 +++ .../report-suspect.component.html | 132 +++ .../report-suspect.component.scss | 102 ++ .../report-suspect.component.ts | 145 +++ src/app/core/core.module.ts | 6 + .../breadcrumb/breadcrumb.component.scss | 4 +- src/app/core/shell/shell.component.html | 994 +++++++++++++++++- src/app/core/shell/shell.component.scss | 156 ++- src/app/core/shell/shell.component.ts | 31 + .../core/shell/sidenav/sidenav.component.html | 113 ++ .../core/shell/toolbar/toolbar.component.html | 292 +++-- .../core/shell/toolbar/toolbar.component.scss | 152 ++- .../core/shell/toolbar/toolbar.component.ts | 18 + .../home/dashboard/dashboard.component.html | 332 ++++-- .../home/dashboard/dashboard.component.scss | 728 +++++++++++-- src/app/home/dashboard/dashboard.component.ts | 9 +- src/app/shared/icons.module.ts | 46 +- src/assets/translations/en-US.json | 202 +++- src/environments/.env.ts | 4 +- src/environments/environment.prod.ts | 3 + src/environments/environment.ts | 3 + src/main.scss | 38 + 55 files changed, 9667 insertions(+), 298 deletions(-) create mode 100644 src/app/cfcfrms/cfcfrms-routing.module.ts create mode 100644 src/app/cfcfrms/cfcfrms-simulator.service.ts create mode 100644 src/app/cfcfrms/cfcfrms.module.ts create mode 100644 src/app/cfcfrms/cfcfrms.service.ts create mode 100644 src/app/cfcfrms/cfr-lookup/cfr-lookup.component.html create mode 100644 src/app/cfcfrms/cfr-lookup/cfr-lookup.component.scss create mode 100644 src/app/cfcfrms/cfr-lookup/cfr-lookup.component.ts create mode 100644 src/app/cfcfrms/complaint-detail/complaint-detail.component.html create mode 100644 src/app/cfcfrms/complaint-detail/complaint-detail.component.scss create mode 100644 src/app/cfcfrms/complaint-detail/complaint-detail.component.ts create mode 100644 src/app/cfcfrms/complaint-list/complaint-list.component.html create mode 100644 src/app/cfcfrms/complaint-list/complaint-list.component.scss create mode 100644 src/app/cfcfrms/complaint-list/complaint-list.component.ts create mode 100644 src/app/cfcfrms/components/confirm-dialog/confirm-dialog.component.ts create mode 100644 src/app/cfcfrms/components/status-badge/status-badge.component.ts create mode 100644 src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.html create mode 100644 src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.scss create mode 100644 src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.ts create mode 100644 src/app/cfcfrms/escalate/escalate.component.html create mode 100644 src/app/cfcfrms/escalate/escalate.component.scss create mode 100644 src/app/cfcfrms/escalate/escalate.component.ts create mode 100644 src/app/cfcfrms/fmr-submission/fmr-submission.component.html create mode 100644 src/app/cfcfrms/fmr-submission/fmr-submission.component.scss create mode 100644 src/app/cfcfrms/fmr-submission/fmr-submission.component.ts create mode 100644 src/app/cfcfrms/mock/cfcfrms-mock-data.ts create mode 100644 src/app/cfcfrms/mock/cfcfrms-mock.interceptor.ts create mode 100644 src/app/cfcfrms/models/cfcfrms.models.ts create mode 100644 src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.html create mode 100644 src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.scss create mode 100644 src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.ts create mode 100644 src/app/cfcfrms/report-mule/report-mule.component.html create mode 100644 src/app/cfcfrms/report-mule/report-mule.component.scss create mode 100644 src/app/cfcfrms/report-mule/report-mule.component.ts create mode 100644 src/app/cfcfrms/report-suspect/report-suspect.component.html create mode 100644 src/app/cfcfrms/report-suspect/report-suspect.component.scss create mode 100644 src/app/cfcfrms/report-suspect/report-suspect.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 75d6543a7..fdc1ad2ef 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/cfcfrms/cfcfrms-routing.module.ts b/src/app/cfcfrms/cfcfrms-routing.module.ts new file mode 100644 index 000000000..08fe11479 --- /dev/null +++ b/src/app/cfcfrms/cfcfrms-routing.module.ts @@ -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 {} diff --git a/src/app/cfcfrms/cfcfrms-simulator.service.ts b/src/app/cfcfrms/cfcfrms-simulator.service.ts new file mode 100644 index 000000000..8cae44c83 --- /dev/null +++ b/src/app/cfcfrms/cfcfrms-simulator.service.ts @@ -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 { + return this.http.post(`${this.baseUrl}/push-complaint`, payload); + } + + /** I4C pushes an urgent hold/freeze directive */ + pushHoldDirective(payload: SimHoldDirectiveRequest): Observable { + return this.http.post(`${this.baseUrl}/push-hold-directive`, payload); + } + + /** I4C pushes a batch of complaints (max 10) */ + pushBulkComplaints(payload: SimBulkPushRequest): Observable { + return this.http.post(`${this.baseUrl}/push-bulk-complaints`, payload); + } + + /** Bank reports action taken on a complaint */ + receiveBankUpdate(payload: SimBankUpdateRequest): Observable { + return this.http.post(`${this.baseUrl}/receive-bank-update`, payload); + } + + /** Bank reports that funds have been moved/escalated */ + receiveEscalation(payload: { ticketId: string }): Observable { + return this.http.post(`${this.baseUrl}/receive-escalation`, payload); + } + + /** Verify if an account is known/flagged */ + verifyAccount(payload: SimVerifyAccountRequest): Observable { + return this.http.post(`${this.baseUrl}/verify-account`, payload); + } + + /** Verify a transaction for risk/fraud flags */ + verifyTransaction(payload: SimVerifyTransactionRequest): Observable { + return this.http.post(`${this.baseUrl}/verify-transaction`, payload); + } + + /** CFCFRMS-side status view for a complaint by reference */ + getComplaintStatus(ref: string): Observable { + return this.http.get(`${this.baseUrl}/complaint-status/${ref}`); + } + + /** Bank TAT / compliance performance metrics */ + getBankPerformance(): Observable { + return this.http.get(`${this.baseUrl}/bank-performance`); + } +} diff --git a/src/app/cfcfrms/cfcfrms.module.ts b/src/app/cfcfrms/cfcfrms.module.ts new file mode 100644 index 000000000..eab2f268d --- /dev/null +++ b/src/app/cfcfrms/cfcfrms.module.ts @@ -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 {} diff --git a/src/app/cfcfrms/cfcfrms.service.ts b/src/app/cfcfrms/cfcfrms.service.ts new file mode 100644 index 000000000..4f66ef943 --- /dev/null +++ b/src/app/cfcfrms/cfcfrms.service.ts @@ -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): Observable { + return this.http.post(`${this.baseUrl}/complaints`, complaint); + } + + getComplaints(params?: { + page?: number; + size?: number; + status?: string; + search?: string; + dateFrom?: string; + dateTo?: string; + }): Observable> { + 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>(`${this.baseUrl}/complaints`, { params: httpParams }); + } + + getComplaintById(ticketId: string): Observable { + return this.http.get(`${this.baseUrl}/complaints/${ticketId}`); + } + + submitAction(ticketId: string, actionPayload: ComplaintAction): Observable { + return this.http.put(`${this.baseUrl}/complaints/${ticketId}/action`, actionPayload); + } + + getStats(): Observable { + return this.http.get(`${this.baseUrl}/complaints/stats`); + } + + getTimeline(ticketId: string): Observable { + return this.http.get(`${this.baseUrl}/complaints/${ticketId}/timeline`); + } + + escalateComplaint(ticketId: string, payload: EscalationRequest): Observable { + return this.http.post(`${this.baseUrl}/complaints/${ticketId}/escalate`, payload); + } + + // ─── Bank Proactive Reporting ─── + + reportSuspect(payload: ReportSuspectRequest): Observable { + return this.http.post(`${this.baseUrl}/report-suspect`, payload); + } + + reportMuleAccount(payload: ReportMuleAccountRequest): Observable { + return this.http.post(`${this.baseUrl}/report-mule-account`, payload); + } + + reportFraudTransaction(payload: ReportFraudTransactionRequest): Observable { + return this.http.post(`${this.baseUrl}/report-fraud-transaction`, payload); + } + + // ─── RBI Integration ─── + + private rbiBaseUrl = `${environment.serverUrl}/rbi`; + + submitFmr(payload: FmrSubmissionRequest): Observable { + return this.http.post(`${this.rbiBaseUrl}/fmr`, payload); + } + + lookupCfr(accountNumber: string): Observable { + const params = new HttpParams().set('accountNumber', accountNumber); + return this.http.get(`${this.rbiBaseUrl}/cfr/lookup`, { params }); + } +} diff --git a/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.html b/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.html new file mode 100644 index 000000000..a8a656ce5 --- /dev/null +++ b/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.html @@ -0,0 +1,127 @@ + + +
+ + + + + +
+ + {{ 'labels.text.Account Number' | translate }} + + + + +
+ + +
+
+ + + @if (isSearching) { +
+ +

Querying RBI Central Fraud Registry...

+
+ } @else if (hasSearched && result) { + + + + @if (result.found) { + + Fraud Records Found + } @else { + + No Fraud Records Found + } + + + + + + @if (result.found && result.records.length > 0) { + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Reference{{ row.fraudReferenceNumber }}Category{{ row.fraudCategory }}{{ 'labels.text.Fraud Amount' | translate }}{{ formatCurrency(row.fraudAmount) }}Reporting Bank{{ row.reportingBank }}Date Reported{{ row.dateReported }}{{ 'labels.text.Status' | translate }} + {{ row.status }} +
+ +
+

{{ 'labels.text.Remarks' | translate }}

+ @for (record of result.records; track record.fraudReferenceNumber) { +
+ {{ record.fraudReferenceNumber }}: {{ record.remarks }} +
+ } +
+ } @else if (!result.found) { +
+ +

This account has no entries in the Central Fraud Registry. Safe to proceed with onboarding.

+
+ } +
+
+ } +
diff --git a/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.scss b/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.scss new file mode 100644 index 000000000..acc409812 --- /dev/null +++ b/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.ts b/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.ts new file mode 100644 index 000000000..0ca72a72d --- /dev/null +++ b/src/app/cfcfrms/cfr-lookup/cfr-lookup.component.ts @@ -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'; + } + } +} diff --git a/src/app/cfcfrms/complaint-detail/complaint-detail.component.html b/src/app/cfcfrms/complaint-detail/complaint-detail.component.html new file mode 100644 index 000000000..0ae9b808f --- /dev/null +++ b/src/app/cfcfrms/complaint-detail/complaint-detail.component.html @@ -0,0 +1,319 @@ + + +
+ + + @if (isLoading) { +
+ +
+ } @else if (!complaint) { + + + +

{{ 'labels.text.Complaint not found' | translate }}

+ {{ 'labels.text.Back to List' | translate }} +
+
+ } @else { +
+ +
+ + + {{ 'labels.menus.Complaint Information' | translate }} + + + + +
+
+

{{ 'labels.text.Complainant Details' | translate }}

+
+ {{ 'labels.text.Name' | translate }} + {{ complaint.complainantName }} +
+
+ {{ 'labels.text.Mobile' | translate }} + {{ complaint.complainantMobile }} +
+
+ {{ 'labels.text.Email' | translate }} + {{ complaint.complainantEmail || '-' }} +
+
+ {{ 'labels.text.Address' | translate }} + {{ complaint.complainantAddress || '-' }} +
+
+ + + +
+

{{ 'labels.text.Fraud Details' | translate }}

+
+ {{ 'labels.text.Fraud Amount' | translate }} + {{ formatCurrency(complaint.fraudAmount) }} +
+
+ {{ 'labels.text.Fraud Type' | translate }} + {{ complaint.fraudType }} +
+
+ {{ 'labels.text.Description' | translate }} + {{ complaint.fraudDescription }} +
+
+ + + +
+

{{ 'labels.text.Suspect Account Details' | translate }}

+
+ {{ 'labels.text.Account Number' | translate }} + {{ complaint.suspectAccountNumber }} +
+
+ {{ 'labels.text.Account Name' | translate }} + {{ complaint.suspectAccountName }} +
+
+ {{ 'labels.text.Bank IFSC' | translate }} + {{ complaint.suspectBankIfsc }} +
+
+ {{ 'labels.text.Bank Name' | translate }} + {{ complaint.suspectBankName }} +
+
+ + + +
+

{{ 'labels.text.Source & Acknowledgement' | translate }}

+
+ {{ 'labels.text.Source Portal' | translate }} + {{ complaint.sourcePortal }} +
+
+ {{ 'labels.text.Ack. Number' | translate }} + {{ complaint.acknowledgementNumber }} +
+
+ {{ 'labels.text.Assigned Officer' | translate }} + {{ + complaint.assignedOfficer || ('labels.text.Unassigned' | translate) + }} +
+
+ {{ 'labels.text.Created' | translate }} + {{ formatDate(complaint.createdAt) }} +
+
+ {{ 'labels.text.Last Updated' | translate }} + {{ formatDate(complaint.updatedAt) }} +
+
+ + @if (complaint.fundsHeld > 0 || complaint.availableBalance > 0) { + +
+

{{ 'labels.text.Fund Details' | translate }}

+
+ {{ 'labels.text.Available Balance' | translate }} + {{ formatCurrency(complaint.availableBalance) }} +
+
+ {{ 'labels.text.Funds Held' | translate }} + {{ formatCurrency(complaint.fundsHeld) }} +
+
+ } + + @if (complaint.nextHopAccountNumber) { + +
+

{{ 'labels.text.Escalation Info' | translate }}

+
+ {{ 'labels.text.Next Hop Account' | translate }} + {{ complaint.nextHopAccountNumber }} +
+
+ {{ 'labels.text.Next Hop IFSC' | translate }} + {{ complaint.nextHopBankIfsc }} +
+
+ } +
+
+
+
+ + +
+ + + + {{ 'labels.text.Take Action' | translate }} + + + @if (isTerminal) { +
+ +

{{ 'labels.text.No further actions can be taken' | translate }}

+
+ } @else { +
+
+ {{ 'labels.text.Current Status' | translate }}: + +
+ + + {{ 'labels.text.Select Action' | translate }} + + @for (opt of actionOptions; track opt.value) { + {{ opt.label }} + } + + @if (actionForm.get('action')?.hasError('required') && actionForm.get('action')?.touched) { + {{ 'labels.text.Action is required' | translate }} + } + + + @if (showBalanceFields) { + + {{ 'labels.text.Available Balance (INR)' | translate }} + + @if ( + actionForm.get('availableBalance')?.hasError('required') && + actionForm.get('availableBalance')?.touched + ) { + {{ 'labels.text.Available balance is required' | translate }} + } + + + {{ 'labels.text.Funds Held (INR)' | translate }} + + @if (actionForm.get('fundsHeld')?.hasError('required') && actionForm.get('fundsHeld')?.touched) { + {{ 'labels.text.Funds held is required' | translate }} + } + + } + + @if (showNextHopFields) { + + {{ 'labels.text.Next Hop Account Number' | translate }} + + @if ( + actionForm.get('nextHopAccountNumber')?.hasError('required') && + actionForm.get('nextHopAccountNumber')?.touched + ) { + {{ 'labels.text.Account number is required' | translate }} + } + + + {{ 'labels.text.Next Hop Bank IFSC' | translate }} + + @if ( + actionForm.get('nextHopBankIfsc')?.hasError('required') && + actionForm.get('nextHopBankIfsc')?.touched + ) { + {{ 'labels.text.IFSC code is required' | translate }} + } + @if ( + actionForm.get('nextHopBankIfsc')?.hasError('pattern') && + actionForm.get('nextHopBankIfsc')?.touched + ) { + {{ 'labels.text.Invalid IFSC format' | translate }} + } + + } + + + {{ 'labels.text.Action Note' | translate }} + + @if (actionForm.get('actionNote')?.hasError('required') && actionForm.get('actionNote')?.touched) { + {{ 'labels.text.Action note is required' | translate }} + } + + + +
+ } +
+
+ + + + + {{ 'labels.text.Audit Timeline' | translate }} + + + @if (timeline.length === 0) { +
+ +

{{ 'labels.text.No timeline events yet' | translate }}

+
+ } @else { +
+ @for (event of timeline; track event.id) { +
+
+ +
+
+
+ {{ event.eventType.replace('_', ' ') }} + {{ formatDate(event.timestamp) }} +
+

{{ event.eventDescription }}

+ + {{ event.actor }} ({{ event.actorRole }}) + +
+
+ } +
+ } +
+
+
+
+ } +
diff --git a/src/app/cfcfrms/complaint-detail/complaint-detail.component.scss b/src/app/cfcfrms/complaint-detail/complaint-detail.component.scss new file mode 100644 index 000000000..03b27c749 --- /dev/null +++ b/src/app/cfcfrms/complaint-detail/complaint-detail.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/complaint-detail/complaint-detail.component.ts b/src/app/cfcfrms/complaint-detail/complaint-detail.component.ts new file mode 100644 index 000000000..deba53ee1 --- /dev/null +++ b/src/app/cfcfrms/complaint-detail/complaint-detail.component.ts @@ -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 = { + 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 = { + RECEIVED: '#1E88E5', + UNDER_REVIEW: '#F59E0B', + LIEN_MARKED: '#EAB308', + DEBIT_FROZEN: '#EF4444', + FUNDS_MOVED: '#8B5CF6', + RESOLVED: '#10B981', + REJECTED: '#6B7280', + ESCALATED: '#1E88E5' + }; + return colors[eventType] || '#94A3B8'; + } +} diff --git a/src/app/cfcfrms/complaint-list/complaint-list.component.html b/src/app/cfcfrms/complaint-list/complaint-list.component.html new file mode 100644 index 000000000..4acdaaa11 --- /dev/null +++ b/src/app/cfcfrms/complaint-list/complaint-list.component.html @@ -0,0 +1,162 @@ + + +
+ + + +
+ + + + +
+ + + + +
+ + {{ 'labels.text.Search by Ticket ID / Mobile' | translate }} + + + + + + {{ 'labels.text.Status' | translate }} + + {{ 'labels.text.All Statuses' | translate }} + @for (s of statuses; track s) { + {{ s.replace('_', ' ') }} + } + + + + + {{ 'labels.text.From Date' | translate }} + + + + + + + {{ 'labels.text.To Date' | translate }} + + + + + + +
+
+
+ + + + + @if (isLoading) { +
+ +
+ } @else if (complaints.length === 0) { +
+ +

{{ 'labels.text.No complaints match your filters' | translate }}

+ +
+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.text.Ticket ID' | translate }} + {{ row.ticketId }} + {{ 'labels.text.Complainant' | translate }}{{ row.complainantName }}{{ 'labels.text.Mobile' | translate }}{{ row.complainantMobile }}{{ 'labels.text.Fraud Amount' | translate }}{{ formatCurrency(row.fraudAmount) }}{{ 'labels.text.Suspect Account' | translate }}{{ row.suspectAccountNumber }}{{ 'labels.text.Status' | translate }} + + {{ 'labels.text.Created At' | translate }}{{ formatDate(row.createdAt) }}{{ 'labels.text.Actions' | translate }} + + + +
+
+ + + } +
+
+
diff --git a/src/app/cfcfrms/complaint-list/complaint-list.component.scss b/src/app/cfcfrms/complaint-list/complaint-list.component.scss new file mode 100644 index 000000000..53b979c08 --- /dev/null +++ b/src/app/cfcfrms/complaint-list/complaint-list.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/complaint-list/complaint-list.component.ts b/src/app/cfcfrms/complaint-list/complaint-list.component.ts new file mode 100644 index 000000000..b5c8de2bd --- /dev/null +++ b/src/app/cfcfrms/complaint-list/complaint-list.component.ts @@ -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' }); + } +} diff --git a/src/app/cfcfrms/components/confirm-dialog/confirm-dialog.component.ts b/src/app/cfcfrms/components/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 000000000..9543ba8f3 --- /dev/null +++ b/src/app/cfcfrms/components/confirm-dialog/confirm-dialog.component.ts @@ -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: ` +

{{ data.title }}

+ +

{{ data.message }}

+
+ + + + + `, + styles: [ + ` + p { + margin: 8px 0; + font-size: 14px; + } + ` + ] +}) +export class ConfirmDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData + ) {} + + onCancel(): void { + this.dialogRef.close(false); + } + + onConfirm(): void { + this.dialogRef.close(true); + } +} diff --git a/src/app/cfcfrms/components/status-badge/status-badge.component.ts b/src/app/cfcfrms/components/status-badge/status-badge.component.ts new file mode 100644 index 000000000..692ee8d15 --- /dev/null +++ b/src/app/cfcfrms/components/status-badge/status-badge.component.ts @@ -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: ` + + {{ statusLabel }} + + `, + 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 = { + 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; + } +} diff --git a/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.html b/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.html new file mode 100644 index 000000000..69f3cfece --- /dev/null +++ b/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.html @@ -0,0 +1,139 @@ + + +
+ + + +
+ + +
+
+ {{ stats?.totalComplaints || 0 }} + {{ 'labels.text.Total Complaints' | translate }} +
+
+
+ + +
+
+ {{ stats?.pendingReview || 0 }} + {{ 'labels.text.Pending Review' | translate }} +
+
+
+ + +
+
+ {{ stats?.lienMarked || 0 }} + {{ 'labels.text.Lien Marked' | translate }} +
+
+
+ + +
+
+ {{ formatCurrency(stats?.fundsRecovered || 0) }} + {{ 'labels.text.Funds Recovered' | translate }} +
+
+
+
+ + +
+ + + {{ 'labels.text.Status Distribution' | translate }} + + +
+ +
+
+
+ + + {{ 'labels.text.Fraud Amount by Status' | translate }} + + +
+ +
+
+
+
+ + + + + {{ 'labels.text.Recent Complaints' | translate }} + + {{ + 'labels.text.View All' | translate + }} + + + @if (isLoading) { +
+ +
+ } @else if (recentComplaints.length === 0) { +
+ +

{{ 'labels.text.No complaints found' | translate }}

+
+ } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.text.Ticket ID' | translate }} + {{ row.ticketId }} + {{ 'labels.text.Complainant' | translate }}{{ row.complainantName }}{{ 'labels.text.Fraud Amount' | translate }}{{ formatCurrency(row.fraudAmount) }}{{ 'labels.text.Status' | translate }} + + {{ 'labels.text.Created' | translate }}{{ formatDate(row.createdAt) }}{{ 'labels.text.Actions' | translate }} + + + +
+ } +
+
+
diff --git a/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.scss b/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.scss new file mode 100644 index 000000000..2ffe944f4 --- /dev/null +++ b/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.scss @@ -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; + } +} diff --git a/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.ts b/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.ts new file mode 100644 index 000000000..7afdf8d52 --- /dev/null +++ b/src/app/cfcfrms/dashboard/cfcfrms-dashboard.component.ts @@ -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; + @ViewChild('barCanvas') barCanvas: ElementRef; + + 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 = { + 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' }); + } +} diff --git a/src/app/cfcfrms/escalate/escalate.component.html b/src/app/cfcfrms/escalate/escalate.component.html new file mode 100644 index 000000000..c4a92f909 --- /dev/null +++ b/src/app/cfcfrms/escalate/escalate.component.html @@ -0,0 +1,220 @@ + + +
+ + +
+ + + + + {{ 'labels.text.Escalation Form' | translate }} + + + + @if (escalationResult) { +
+
+ +
+

{{ 'labels.text.Escalation Successful' | translate }}

+
+
+ {{ 'labels.text.Reference Number' | translate }} + {{ escalationResult.referenceNumber }} +
+
+ {{ 'labels.text.Ticket ID' | translate }} + {{ escalationResult.ticketId }} +
+
+ {{ 'labels.text.Escalated To' | translate }} + {{ escalationResult.escalatedTo }} +
+
+ {{ 'labels.text.Bank IFSC' | translate }} + {{ escalationResult.escalatedBankIfsc }} +
+
+ {{ 'labels.text.Status' | translate }} + {{ escalationResult.status }} +
+
+ {{ 'labels.text.Timestamp' | translate }} + {{ escalationResult.timestamp }} +
+
+ +
+ } @else { +
+
+ + {{ 'labels.text.Ticket ID' | translate }} + + @if (escalateForm.get('ticketId')?.hasError('required') && escalateForm.get('ticketId')?.touched) { + {{ 'labels.text.Ticket ID' | translate }} is required + } + + +
+ + @if (lookedUpComplaint) { +
+
+ {{ 'labels.text.Complainant' | translate }}: + {{ lookedUpComplaint.complainantName }} +
+
+ {{ 'labels.text.Fraud Amount' | translate }}: + {{ formatCurrency(lookedUpComplaint.fraudAmount) }} +
+
+ {{ 'labels.text.Suspect Account' | translate }}: + {{ lookedUpComplaint.suspectAccountNumber }} +
+
+ {{ 'labels.text.Current Status' | translate }}: + +
+
+ } + + + {{ 'labels.text.Next Hop Account Number' | translate }} + + @if ( + escalateForm.get('nextHopAccountNumber')?.hasError('required') && + escalateForm.get('nextHopAccountNumber')?.touched + ) { + {{ 'labels.text.Account number is required' | translate }} + } + + + + {{ 'labels.text.Next Hop Bank IFSC' | translate }} + + @if ( + escalateForm.get('nextHopBankIfsc')?.hasError('required') && + escalateForm.get('nextHopBankIfsc')?.touched + ) { + {{ 'labels.text.IFSC code is required' | translate }} + } + @if ( + escalateForm.get('nextHopBankIfsc')?.hasError('pattern') && escalateForm.get('nextHopBankIfsc')?.touched + ) { + {{ 'labels.text.Invalid IFSC format' | translate }} + } + + + + {{ 'labels.text.Remarks' | translate }} + + @if (escalateForm.get('remarks')?.hasError('required') && escalateForm.get('remarks')?.touched) { + {{ 'labels.text.Remarks are required' | translate }} + } + + + +
+ } +
+
+ + + + + + {{ 'labels.text.Escalation Guidelines' | translate }} + + + +
+
+ +

{{ 'labels.text.Verify the complaint details before escalating to the next bank.' | translate }}

+
+
+ +

+ {{ + 'labels.text.Ensure the Next Hop Account and IFSC belong to the next bank in the fraud transaction chain.' + | translate + }} +

+
+
+ +

{{ 'labels.text.Provide clear and detailed remarks explaining the escalation reason.' | translate }}

+
+
+ +

{{ 'labels.text.Escalation must be completed within the I4C/MHA mandated timeline.' | translate }}

+
+
+ +

+ {{ + 'labels.text.Once escalated, the complaint will be forwarded to the target bank for action.' | translate + }} +

+
+
+ +
+

{{ 'labels.text.Current Bank Info' | translate }}

+
+ {{ 'labels.text.Bank Name' | translate }}: LincPay +
+
+ {{ 'labels.text.Bank IFSC' | translate }}: LINC0000001 +
+
+
+
+
+
diff --git a/src/app/cfcfrms/escalate/escalate.component.scss b/src/app/cfcfrms/escalate/escalate.component.scss new file mode 100644 index 000000000..1b93850dc --- /dev/null +++ b/src/app/cfcfrms/escalate/escalate.component.scss @@ -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; + } + } +} diff --git a/src/app/cfcfrms/escalate/escalate.component.ts b/src/app/cfcfrms/escalate/escalate.component.ts new file mode 100644 index 000000000..16dc103c3 --- /dev/null +++ b/src/app/cfcfrms/escalate/escalate.component.ts @@ -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 + ); + } +} diff --git a/src/app/cfcfrms/fmr-submission/fmr-submission.component.html b/src/app/cfcfrms/fmr-submission/fmr-submission.component.html new file mode 100644 index 000000000..3a689b1cd --- /dev/null +++ b/src/app/cfcfrms/fmr-submission/fmr-submission.component.html @@ -0,0 +1,178 @@ + + +
+ + +
+ + + + + {{ 'labels.text.Fraud Monitoring Return (FMR)' | translate }} + + + + @if (result) { +
+
+

{{ 'labels.text.FMR Submitted Successfully' | translate }}

+
+
+ {{ 'labels.text.FMR Reference' | translate }}{{ result.fmrReferenceNumber }} +
+
+ {{ 'labels.text.Bank IFSC' | translate }}{{ result.reportingBankIfsc }} +
+
+ {{ 'labels.text.Fraud Category' | translate }}{{ result.fraudCategory }} +
+
+ {{ 'labels.text.Fraud Amount' | translate }}₹{{ result.fraudAmount?.toLocaleString() }} +
+
+ {{ 'labels.text.Status' | translate }} +
+
+ {{ 'labels.text.Submitted At' | translate }}{{ result.submittedAt }} +
+
+ {{ 'labels.text.Next FMR Due Date' | translate }}{{ result.nextDueDate }} +
+
+ +
+ } @else { +
+
+ + {{ 'labels.text.Reporting Bank IFSC' | translate }} + + + + {{ 'labels.text.Reporting Bank Name' | translate }} + + +
+ +
+ + {{ 'labels.text.Fraud Category' | translate }} + + @for (cat of fraudCategories; track cat) { + {{ cat }} + } + + + + {{ 'labels.text.Sub-Category' | translate }} + + @for (sub of fraudSubCategories; track sub) { + {{ sub }} + } + + +
+ + + {{ 'labels.text.Fraud Amount (INR)' | translate }} + + + +
+ + {{ 'labels.text.Account Number' | translate }} + + + + {{ 'labels.text.Account Holder Name' | translate }} + + +
+ +
+ + {{ 'labels.text.Date of Detection' | translate }} + + + + + + {{ 'labels.text.Date of Fraud' | translate }} + + + + +
+ + + {{ 'labels.text.Modus Operandi' | translate }} + + + + + {{ 'labels.text.Action Taken' | translate }} + + @for (action of actionsTaken; track action) { + {{ action }} + } + + + +
+ {{ + 'labels.text.Staff Involved in Fraud' | translate + }} +
+ + + {{ 'labels.text.Remarks' | translate }} + + + + +
+ } +
+
+
+
diff --git a/src/app/cfcfrms/fmr-submission/fmr-submission.component.scss b/src/app/cfcfrms/fmr-submission/fmr-submission.component.scss new file mode 100644 index 000000000..a3aec417f --- /dev/null +++ b/src/app/cfcfrms/fmr-submission/fmr-submission.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/fmr-submission/fmr-submission.component.ts b/src/app/cfcfrms/fmr-submission/fmr-submission.component.ts new file mode 100644 index 000000000..f38aa7ca0 --- /dev/null +++ b/src/app/cfcfrms/fmr-submission/fmr-submission.component.ts @@ -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; + } +} diff --git a/src/app/cfcfrms/mock/cfcfrms-mock-data.ts b/src/app/cfcfrms/mock/cfcfrms-mock-data.ts new file mode 100644 index 000000000..8b1f00481 --- /dev/null +++ b/src/app/cfcfrms/mock/cfcfrms-mock-data.ts @@ -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 + }; +} diff --git a/src/app/cfcfrms/mock/cfcfrms-mock.interceptor.ts b/src/app/cfcfrms/mock/cfcfrms-mock.interceptor.ts new file mode 100644 index 000000000..8ad8e369f --- /dev/null +++ b/src/app/cfcfrms/mock/cfcfrms-mock.interceptor.ts @@ -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 { + 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, next: HttpHandler): Observable> { + 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)); + } +} diff --git a/src/app/cfcfrms/models/cfcfrms.models.ts b/src/app/cfcfrms/models/cfcfrms.models.ts new file mode 100644 index 000000000..c6b54573f --- /dev/null +++ b/src/app/cfcfrms/models/cfcfrms.models.ts @@ -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 { + 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; +} diff --git a/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.html b/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.html new file mode 100644 index 000000000..5df0c900a --- /dev/null +++ b/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.html @@ -0,0 +1,138 @@ + + +
+ + +
+ + + + + {{ 'labels.text.Fraud Transaction Details' | translate }} + + + + @if (result) { +
+
+

{{ 'labels.text.Fraud Transaction Reported' | translate }}

+
+
+ {{ 'labels.text.Reference Number' | translate }}{{ result.referenceNumber }} +
+
+ {{ 'labels.text.Transaction ID' | translate }}{{ result.transactionId }} +
+
+ {{ 'labels.text.Status' | translate }}{{ result.status }} +
+
+ {{ 'labels.text.Timestamp' | translate }}{{ result.reportedAt }} +
+
+ +
+ } @else { +
+
+ + {{ 'labels.text.Transaction ID' | translate }} + + + + {{ 'labels.text.Amount (INR)' | translate }} + + +
+ +
+ + {{ 'labels.text.From Account' | translate }} + + + + {{ 'labels.text.To Account' | translate }} + + +
+ + + {{ 'labels.text.Transaction Date' | translate }} + + + + + +
+ + {{ 'labels.text.Fraud Type' | translate }} + + @for (ft of fraudTypes; track ft) { + {{ ft.replace('_', ' ') }} + } + + + + {{ 'labels.text.Channel' | translate }} + + @for (ch of channels; track ch) { + {{ ch }} + } + + +
+ + + {{ 'labels.text.Detected By' | translate }} + + + + + {{ 'labels.text.Description' | translate }} + + + + + {{ 'labels.text.Linked CFCFRMS Ticket' | translate }} + + + + +
+ } +
+
+
+
diff --git a/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.scss b/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.scss new file mode 100644 index 000000000..744868e97 --- /dev/null +++ b/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.ts b/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.ts new file mode 100644 index 000000000..6f9dc6d79 --- /dev/null +++ b/src/app/cfcfrms/report-fraud-txn/report-fraud-txn.component.ts @@ -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; + } +} diff --git a/src/app/cfcfrms/report-mule/report-mule.component.html b/src/app/cfcfrms/report-mule/report-mule.component.html new file mode 100644 index 000000000..9c6dbc760 --- /dev/null +++ b/src/app/cfcfrms/report-mule/report-mule.component.html @@ -0,0 +1,133 @@ + + +
+ + +
+ + + + {{ 'labels.text.Mule Account Details' | translate }} + + + + @if (result) { +
+
+

{{ 'labels.text.Mule Account Flagged' | translate }}

+
+
+ {{ 'labels.text.Reference Number' | translate }}{{ result.referenceNumber }} +
+
+ {{ 'labels.text.Account Number' | translate }}{{ result.accountNumber }} +
+
+ {{ 'labels.text.Mule Type' | translate }}{{ result.muleType }} +
+
+ {{ 'labels.text.Status' | translate }}{{ result.status }} +
+
+ {{ 'labels.text.Timestamp' | translate }}{{ result.reportedAt }} +
+
+ +
+ } @else { +
+ + {{ 'labels.text.Account Number' | translate }} + + @if (form.get('accountNumber')?.hasError('required') && form.get('accountNumber')?.touched) { + Account number is required + } + + + + {{ 'labels.text.Account Name' | translate }} + + + + + {{ 'labels.text.Bank IFSC' | translate }} + + + + + {{ 'labels.text.Mule Type' | translate }} + + @for (t of muleTypes; track t.value) { + {{ t.label }} + } + + + + + {{ 'labels.text.Transaction Pattern' | translate }} + + @for (p of transactionPatterns; track p) { + {{ p }} + } + + + + + {{ 'labels.text.Total Suspicious Amount (INR)' | translate }} + + + + + {{ 'labels.text.Linked Fraud Ticket IDs' | translate }} + + + + + {{ 'labels.text.Remarks' | translate }} + + + + +
+ } +
+
+
+
diff --git a/src/app/cfcfrms/report-mule/report-mule.component.scss b/src/app/cfcfrms/report-mule/report-mule.component.scss new file mode 100644 index 000000000..50c2bf4e1 --- /dev/null +++ b/src/app/cfcfrms/report-mule/report-mule.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/report-mule/report-mule.component.ts b/src/app/cfcfrms/report-mule/report-mule.component.ts new file mode 100644 index 000000000..48d18aeaa --- /dev/null +++ b/src/app/cfcfrms/report-mule/report-mule.component.ts @@ -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; + } +} diff --git a/src/app/cfcfrms/report-suspect/report-suspect.component.html b/src/app/cfcfrms/report-suspect/report-suspect.component.html new file mode 100644 index 000000000..c56416e4d --- /dev/null +++ b/src/app/cfcfrms/report-suspect/report-suspect.component.html @@ -0,0 +1,132 @@ + + +
+ + +
+ + + + {{ 'labels.text.Suspect Account Details' | translate }} + + + + @if (result) { +
+
+

{{ 'labels.text.Report Submitted' | translate }}

+
+
+ {{ 'labels.text.Reference Number' | translate }}{{ result.referenceNumber }} +
+
+ {{ 'labels.text.Account Number' | translate }}{{ result.accountNumber }} +
+
+ {{ 'labels.text.Status' | translate }}{{ result.status }} +
+
+ {{ 'labels.text.Timestamp' | translate }}{{ result.reportedAt }} +
+
+ +
+ } @else { +
+ + {{ 'labels.text.Account Number' | translate }} + + @if (form.get('accountNumber')?.hasError('required') && form.get('accountNumber')?.touched) { + Account number is required + } + + + + {{ 'labels.text.Account Name' | translate }} + + @if (form.get('accountName')?.hasError('required') && form.get('accountName')?.touched) { + Account name is required + } + + + + {{ 'labels.text.Bank IFSC' | translate }} + + @if (form.get('bankIfsc')?.hasError('pattern') && form.get('bankIfsc')?.touched) { + {{ 'labels.text.Invalid IFSC format' | translate }} + } + + + + {{ 'labels.text.Suspicion Reason' | translate }} + + @for (reason of suspicionReasons; track reason) { + {{ reason }} + } + + @if (form.get('suspicionReason')?.hasError('required') && form.get('suspicionReason')?.touched) { + Suspicion reason is required + } + + + + {{ 'labels.text.Detected By' | translate }} + + + + + {{ 'labels.text.Detection Date' | translate }} + + + + + + + {{ 'labels.text.Linked Transaction IDs' | translate }} + + + + + {{ 'labels.text.Remarks' | translate }} + + + + +
+ } +
+
+
+
diff --git a/src/app/cfcfrms/report-suspect/report-suspect.component.scss b/src/app/cfcfrms/report-suspect/report-suspect.component.scss new file mode 100644 index 000000000..3c3577da8 --- /dev/null +++ b/src/app/cfcfrms/report-suspect/report-suspect.component.scss @@ -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; +} diff --git a/src/app/cfcfrms/report-suspect/report-suspect.component.ts b/src/app/cfcfrms/report-suspect/report-suspect.component.ts new file mode 100644 index 000000000..c7435c1d7 --- /dev/null +++ b/src/app/cfcfrms/report-suspect/report-suspect.component.ts @@ -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; + } +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 9b4261cd7..70e4606ac 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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, diff --git a/src/app/core/shell/breadcrumb/breadcrumb.component.scss b/src/app/core/shell/breadcrumb/breadcrumb.component.scss index 7bd524941..190404a35 100644 --- a/src/app/core/shell/breadcrumb/breadcrumb.component.scss +++ b/src/app/core/shell/breadcrumb/breadcrumb.component.scss @@ -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; diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 596e393e6..957fa5ac7 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -27,10 +27,43 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if (subLedgerOpen) { +
+ + } + + + @if (glMenuOpen) { +
+ + } + - - - - - - + + + + + + + - - + +
+
+
Accounts
+ + + + + +
+
+
Agent
+ + + + + +
+
+
Reports
+ + + + + + +
+
- - - + + + + + + + +
+ +
+
Payroll
+ + + +
+ +
+
Tax & Compliance
+ + + +
+ +
+
Reports & Admin
+ + + + +
+
+
+ + + + +
+ +
+
General
+ + + + + +
+ +
+
Investment
+ + + + +
+ +
+
Reports & Admin
+ + + + +
+
+
+ + +
+ +
+
NPA Reports
+ + + + +
+ +
+
NPA Analysis
+ + + +
+ +
+
NPA Classification
+ + +
+
+
+ + + + + + + + + + + +
+ +
+
Loan Accounts
+ + + + +
+ +
+
Loan Reports
+ + + +
+ +
+
Recovery & Cash Flow
+ + +
+
+
+ + +
+ +
+
Notices & Renewals
+ + + + +
+ +
+
Printing
+ + + + +
+ +
+
Certificates
+ + + + +
+ +
+
Others
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + @if (rbiMenuOpen) { +
+ + } + @if (progressBarMode !== 'none') {
diff --git a/src/app/core/shell/shell.component.scss b/src/app/core/shell/shell.component.scss index 3483c5663..1eb89fc7f 100644 --- a/src/app/core/shell/shell.component.scss +++ b/src/app/core/shell/shell.component.scss @@ -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; diff --git a/src/app/core/shell/shell.component.ts b/src/app/core/shell/shell.component.ts index 195017f02..27aaf25b9 100644 --- a/src/app/core/shell/shell.component.ts +++ b/src/app/core/shell/shell.component.ts @@ -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. */ diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index f2a75c519..9e5abb6e0 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -171,6 +171,119 @@ {{ 'labels.menus.Analytics' | translate }} + + + + + + + {{ 'labels.menus.CFCFRMS Dashboard' | translate }} + + + + + + {{ 'labels.menus.Complaints' | translate }} + + + + + + {{ 'labels.menus.Escalate' | translate }} + + + + + + {{ 'labels.menus.Report Suspect' | translate }} + + + + + + {{ 'labels.menus.Report Mule' | translate }} + + + + + + {{ 'labels.menus.Report Fraud Txn' | translate }} + + + + + + + + + {{ 'labels.menus.FMR Submission' | translate }} + + + + + + {{ 'labels.menus.CFR Lookup' | translate }} + + + + } - -
-
-
- - + +@if (subLedgerOpen) { +
+ +} + + +@if (glMenuOpen) { +
+ +} diff --git a/src/app/core/shell/toolbar/toolbar.component.scss b/src/app/core/shell/toolbar/toolbar.component.scss index 3ef52cab1..a139d4f78 100644 --- a/src/app/core/shell/toolbar/toolbar.component.scss +++ b/src/app/core/shell/toolbar/toolbar.component.scss @@ -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; +} diff --git a/src/app/core/shell/toolbar/toolbar.component.ts b/src/app/core/shell/toolbar/toolbar.component.ts index afe5a0208..872e5977a 100644 --- a/src/app/core/shell/toolbar/toolbar.component.ts +++ b/src/app/core/shell/toolbar/toolbar.component.ts @@ -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]); } diff --git a/src/app/home/dashboard/dashboard.component.html b/src/app/home/dashboard/dashboard.component.html index ccc16f8ee..bad0a1fd4 100644 --- a/src/app/home/dashboard/dashboard.component.html +++ b/src/app/home/dashboard/dashboard.component.html @@ -7,132 +7,282 @@ -->
-
Core Banking System Dashboard
+
+ Core Banking System Dashboard + {{ today | date: 'mediumDate' }} +
- -
- - - {{ 'labels.placeholders.Search Activity' | translate }} - - - - - @for (activity of filteredActivities | async; track activity) { - - {{ activity.activity }} - - } - - + +
+ + Regulatory & Compliance
- - -
Regulatory & Compliance
-
-
-
CRR / SLR
-
₹ 125 Cr
-
▲ Within Limits
+
+ +
+
+
CRR / SRR
+
₹ 20.14 Cr
+
8% Within Limits
-
-
CRAR
-
13.42%
-
▲ Healthy
+ +
+
+
CRAR %
+
21%
+
Healthy (Min 9%)
-
-
DICGC (Samyak)
-
Updated
-
Last Sync: Today
+ +
+
DICSC
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FreezeInop.Rectified
Aadhaar2,0001,0001,100
PAN3,0002,0002,200
Mobile2,000—100
-
-
Interbank Exposure
-
72%
-
▼ Near Limit
+ +
+
+ Interbank Exposure — 20% +
+
+
+ Yes Bank3%Maintain +
+
+ IDBI11%Exceed +
+
+ Other Bank6%Exceed +
+
- -
Financial Health
-
-
-
Net Worth
-
₹ 342 Cr
+ +
+ + Financial Health +
+
+
+
+
Net Worth
+
₹ 342 Cr
- -
-
CD Ratio
-
68.2%
+
+
+
CD Ratio
+
65%
- -
-
Capital Working
-
₹ 210 Cr
+
+
+
Working Capital
+
₹ 210 Cr
- -
-
Per Employee Income
-
₹ 8.4 L
+
+
+
Per Employee Profit
+
₹ 10 Lakh
+
+
+
+
Advance < 25 Lakh
+
55%
- -
Deposits & Advances
-
-
-
Total Deposits
-
₹ 1,240 Cr
+ +
+ + Deposits & Advances +
+
+ +
+
+
+
Deposits vs Advances
+
+
+
+
Total Deposits
+
₹ 3,264.42 Cr
+
+
+
+
Total Advances
+
₹ 4,162.65 Cr
+
+
+
-
-
Total Advances
-
₹ 845 Cr
+ +
+
+
+
Priority Sector Lending (PSL)
+
+
65%
+
+
+ Weaker Section +
+
+
+ 15% +
+
+ MSME +
+
+
+ 7% +
+
-
-
Decay Analysis
-
Moderate
-
- -
-
TSWM
-
Active
+ +
+
+
+
Portfolio Quality
+
+
+
+
Decay Analysis
+
Moderate
+
+
+
TSWM
+
Active
+
+
- -
Risk & NPA Monitoring
-
-
-
Gross NPA
-
4.82%
+ +
+ + Risk & NPA Monitoring +
+
+ +
+
+
+
Gross NPA
+
+
+
+ 7% +
+
+
Above Threshold
+
RBI norm: < 5%
+
+
-
-
Net NPA
-
2.11%
+ +
+
+
+
Net NPA
+
+
+
+ 2% +
+
+
Within Limits
+
RBI norm: < 3%
+
+
-
-
Risk-wise Analysis
-
Stable
+ +
+
+
+
Risk-wise Analysis
+
+
+
+ + Low Risk + 62% +
+
+ + Medium Risk + 25% +
+
+ + High Risk + 13% +
+
+
Overall: Stable
- -
Alerts & Notifications
+ +
+ + Alerts & Notifications +
- ⚠ CRAR approaching minimum threshold.
- ⚠ Interbank exposure nearing limit.
- ⚠ RBI circular pending review. +
+ Gross NPA at 7% — above RBI threshold of 5%. Immediate review + required. +
+
+ IDBI interbank exposure at 11% — exceeds single-party limit. +
+
DICSC data synced successfully today.
+
+ RBI circular pending review — compliance action due by month end. +
- -
Analytics
+ +
+ + Analytics +