feat: add CFCFRMS module, bank menus, mock backend, and dashboard redesign

- CFCFRMS cyber fraud complaint management with dashboard, complaints,
  escalation, report suspect/mule/fraud txn, FMR submission, CFR lookup
- Mock HTTP interceptor with 20 dummy complaints and all endpoints
- Simulator service for I4C/MHA integration testing
- 3-row menu bar with 22 items (7+8+8 layout)
- Mega menus for General Ledger, Sub Ledger, Deposits, Share, Others,
  HRM, RBI with search and grouped columns
- Dropdowns for Cashier, Clearing, NPA, CKYC, Advances, Letter Mgmt,
  Insurance, Digital
- Dashboard redesign with regulatory, financial health, deposits,
  risk/NPA sections using static banking data
- All translations added to en-US.json
- FontAwesome icons registered for all new menu items

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-18 18:05:43 +05:30
co-authored by Claude Opus 4.6
parent 0a32d7135e
commit b3a4cce0cf
55 changed files with 9667 additions and 298 deletions
@@ -0,0 +1,133 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="report-page">
<div class="page-header">
<h2>{{ 'labels.text.Report Mule Account' | translate }}</h2>
<p class="subtitle">{{ 'labels.text.Flag a mule account proactively to I4C/CFCFRMS' | translate }}</p>
</div>
<div class="report-layout">
<mat-card class="report-card">
<mat-card-header>
<mat-card-title>
<fa-icon icon="user-slash" class="mr-05"></fa-icon> {{ 'labels.text.Mule Account Details' | translate }}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (result) {
<div class="success-result">
<div class="success-icon"><fa-icon icon="check-circle" size="3x"></fa-icon></div>
<h3>{{ 'labels.text.Mule Account Flagged' | translate }}</h3>
<div class="result-details">
<div class="result-row">
<span class="result-label">{{ 'labels.text.Reference Number' | translate }}</span
><span class="result-value mono">{{ result.referenceNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Account Number' | translate }}</span
><span class="result-value mono">{{ result.accountNumber }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Mule Type' | translate }}</span
><span class="result-value">{{ result.muleType }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Status' | translate }}</span
><span class="result-value status-flagged">{{ result.status }}</span>
</div>
<div class="result-row">
<span class="result-label">{{ 'labels.text.Timestamp' | translate }}</span
><span class="result-value">{{ result.reportedAt }}</span>
</div>
</div>
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New Report' | translate }}
</button>
</div>
} @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Account Number' | translate }}</mat-label>
<input matInput formControlName="accountNumber" placeholder="e.g. 50100268347291" />
@if (form.get('accountNumber')?.hasError('required') && form.get('accountNumber')?.touched) {
<mat-error>Account number is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Account Name' | translate }}</mat-label>
<input matInput formControlName="accountName" placeholder="Account holder name" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Bank IFSC' | translate }}</mat-label>
<input matInput formControlName="bankIfsc" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Mule Type' | translate }}</mat-label>
<mat-select formControlName="muleType">
@for (t of muleTypes; track t.value) {
<mat-option [value]="t.value">{{ t.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Transaction Pattern' | translate }}</mat-label>
<mat-select formControlName="transactionPattern">
@for (p of transactionPatterns; track p) {
<mat-option [value]="p">{{ p }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Total Suspicious Amount (INR)' | translate }}</mat-label>
<input matInput type="number" formControlName="totalSuspiciousAmount" placeholder="0.00" />
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Linked Fraud Ticket IDs' | translate }}</mat-label>
<input
matInput
formControlName="linkedFraudTickets"
placeholder="e.g. CFCFRMS-2024-00001, CFCFRMS-2024-00002"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.text.Remarks' | translate }}</mat-label>
<textarea
matInput
formControlName="remarks"
rows="3"
placeholder="{{ 'labels.text.Enter remarks...' | translate }}"
></textarea>
</mat-form-field>
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="isSubmitting"
class="full-width submit-btn"
>
@if (isSubmitting) {
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
} @else {
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Report' | translate }}
}
</button>
</form>
}
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,106 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.report-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.subtitle {
margin: 4px 0 0;
color: #666;
font-size: 14px;
}
}
.report-layout {
display: flex;
gap: 24px;
}
.report-card {
flex: 1;
max-width: 700px;
}
.full-width {
width: 100%;
}
.mr-05 {
margin-right: 0.5rem;
}
.mt-16 {
margin-top: 16px;
}
.submit-btn {
margin-top: 8px;
height: 48px;
font-size: 16px;
}
.inline-spinner {
display: inline-block;
margin-right: 8px;
}
.success-result {
text-align: center;
padding: 32px 16px;
.success-icon {
color: #10b981;
margin-bottom: 16px;
}
h3 {
color: #10b981;
margin-bottom: 24px;
}
}
.result-details {
text-align: left;
max-width: 500px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.result-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
}
.result-label {
color: #666;
font-weight: 500;
}
.result-value {
font-weight: 600;
}
.mono {
font-family: 'Roboto Mono', monospace;
}
.status-flagged {
color: #ef4444;
font-weight: 700;
}
@@ -0,0 +1,149 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { CfcfrmsService } from '../cfcfrms.service';
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'cfcfrms-report-mule',
standalone: true,
templateUrl: './report-mule.component.html',
styleUrls: ['./report-mule.component.scss'],
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDialogModule,
FaIconComponent,
TranslateModule
]
})
export class ReportMuleComponent {
private fb = inject(FormBuilder);
private service = inject(CfcfrmsService);
private snackBar = inject(MatSnackBar);
private dialog = inject(MatDialog);
form: FormGroup = this.fb.group({
accountNumber: [
'',
[
Validators.required,
Validators.minLength(8)
]
],
accountName: [
'',
Validators.required
],
bankIfsc: [
'LINC0000001',
[
Validators.required,
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
]
],
muleType: [
'',
Validators.required
],
linkedFraudTickets: [''],
transactionPattern: [
'',
Validators.required
],
totalSuspiciousAmount: [
null,
[
Validators.required,
Validators.min(1)
]
],
remarks: ['']
});
isSubmitting = false;
result: any = null;
muleTypes = [
{ value: 'INDIVIDUAL', label: 'Individual Account' },
{ value: 'BUSINESS', label: 'Business / Proprietor Account' },
{ value: 'SHELL_COMPANY', label: 'Shell Company Account' }
];
transactionPatterns = [
'Rapid inflow-outflow within hours',
'Multiple small credits, single large withdrawal',
'Circular fund routing across accounts',
'High volume with no legitimate business',
'Dormant account suddenly activated with large transactions',
'Pattern matches known mule network'
];
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { title: 'Confirm Mule Report', message: 'Flag this account as a mule account and report to I4C/CFCFRMS?' }
});
dialogRef.afterClosed().subscribe((confirmed) => {
if (!confirmed) return;
this.isSubmitting = true;
const val = this.form.value;
const payload = {
...val,
linkedFraudTickets: val.linkedFraudTickets
? val.linkedFraudTickets
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
: []
};
this.service.reportMuleAccount(payload).subscribe({
next: (res) => {
this.result = res;
this.isSubmitting = false;
this.snackBar.open('Mule account reported successfully', 'OK', { duration: 4000 });
},
error: () => {
this.isSubmitting = false;
this.snackBar.open('Failed to report mule account', 'OK', { duration: 4000 });
}
});
});
}
resetForm(): void {
this.form.reset({ bankIfsc: 'LINC0000001' });
this.result = null;
}
}