feat: add CFCFRMS module, bank menus, mock backend, and dashboard redesign
- CFCFRMS cyber fraud complaint management with dashboard, complaints, escalation, report suspect/mule/fraud txn, FMR submission, CFR lookup - Mock HTTP interceptor with 20 dummy complaints and all endpoints - Simulator service for I4C/MHA integration testing - 3-row menu bar with 22 items (7+8+8 layout) - Mega menus for General Ledger, Sub Ledger, Deposits, Share, Others, HRM, RBI with search and grouped columns - Dropdowns for Cashier, Clearing, NPA, CKYC, Advances, Letter Mgmt, Insurance, Digital - Dashboard redesign with regulatory, financial health, deposits, risk/NPA sections using static banking data - All translations added to en-US.json - FontAwesome icons registered for all new menu items Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0a32d7135e
commit
b3a4cce0cf
@@ -0,0 +1,220 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="escalate-page">
|
||||
<div class="page-header">
|
||||
<h2>{{ 'labels.menus.Escalate Complaint' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'labels.text.Forward complaint to the next bank in the transaction chain' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="escalate-layout">
|
||||
<!-- Escalation Form -->
|
||||
<mat-card class="escalate-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<fa-icon icon="arrow-up" class="mr-05"></fa-icon> {{ 'labels.text.Escalation Form' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (escalationResult) {
|
||||
<div class="success-result">
|
||||
<div class="success-icon">
|
||||
<fa-icon icon="check-circle" size="3x"></fa-icon>
|
||||
</div>
|
||||
<h3>{{ 'labels.text.Escalation Successful' | translate }}</h3>
|
||||
<div class="result-details">
|
||||
<div class="result-row">
|
||||
<span class="result-label">{{ 'labels.text.Reference Number' | translate }}</span>
|
||||
<span class="result-value mono">{{ escalationResult.referenceNumber }}</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">{{ 'labels.text.Ticket ID' | translate }}</span>
|
||||
<span class="result-value">{{ escalationResult.ticketId }}</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">{{ 'labels.text.Escalated To' | translate }}</span>
|
||||
<span class="result-value">{{ escalationResult.escalatedTo }}</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">{{ 'labels.text.Bank IFSC' | translate }}</span>
|
||||
<span class="result-value mono">{{ escalationResult.escalatedBankIfsc }}</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">{{ 'labels.text.Status' | translate }}</span>
|
||||
<span class="result-value">{{ escalationResult.status }}</span>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="result-label">{{ 'labels.text.Timestamp' | translate }}</span>
|
||||
<span class="result-value">{{ escalationResult.timestamp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="resetForm()" class="mt-16">
|
||||
<fa-icon icon="plus" class="mr-05"></fa-icon> {{ 'labels.text.New Escalation' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<form [formGroup]="escalateForm" (ngSubmit)="onSubmit()">
|
||||
<div class="ticket-lookup-row">
|
||||
<mat-form-field appearance="outline" class="ticket-field">
|
||||
<mat-label>{{ 'labels.text.Ticket ID' | translate }}</mat-label>
|
||||
<input matInput formControlName="ticketId" placeholder="e.g. CFCFRMS-2024-00001" />
|
||||
@if (escalateForm.get('ticketId')?.hasError('required') && escalateForm.get('ticketId')?.touched) {
|
||||
<mat-error>{{ 'labels.text.Ticket ID' | translate }} is required</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
type="button"
|
||||
(click)="lookupTicket()"
|
||||
[disabled]="isLookingUp"
|
||||
class="lookup-btn"
|
||||
>
|
||||
@if (isLookingUp) {
|
||||
<mat-spinner diameter="20"></mat-spinner>
|
||||
} @else {
|
||||
<fa-icon icon="search" class="mr-05"></fa-icon> {{ 'labels.text.Lookup' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lookedUpComplaint) {
|
||||
<div class="lookup-result">
|
||||
<div class="lookup-row">
|
||||
<span>{{ 'labels.text.Complainant' | translate }}:</span>
|
||||
<strong>{{ lookedUpComplaint.complainantName }}</strong>
|
||||
</div>
|
||||
<div class="lookup-row">
|
||||
<span>{{ 'labels.text.Fraud Amount' | translate }}:</span>
|
||||
<strong>{{ formatCurrency(lookedUpComplaint.fraudAmount) }}</strong>
|
||||
</div>
|
||||
<div class="lookup-row">
|
||||
<span>{{ 'labels.text.Suspect Account' | translate }}:</span>
|
||||
<strong class="mono">{{ lookedUpComplaint.suspectAccountNumber }}</strong>
|
||||
</div>
|
||||
<div class="lookup-row">
|
||||
<span>{{ 'labels.text.Current Status' | translate }}:</span>
|
||||
<cfcfrms-status-badge [status]="lookedUpComplaint.status"></cfcfrms-status-badge>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.text.Next Hop Account Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="nextHopAccountNumber"
|
||||
placeholder="{{ 'labels.text.Account Number' | translate }}"
|
||||
/>
|
||||
@if (
|
||||
escalateForm.get('nextHopAccountNumber')?.hasError('required') &&
|
||||
escalateForm.get('nextHopAccountNumber')?.touched
|
||||
) {
|
||||
<mat-error>{{ 'labels.text.Account number is required' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.text.Next Hop Bank IFSC' | translate }}</mat-label>
|
||||
<input matInput formControlName="nextHopBankIfsc" placeholder="e.g. SBIN0000001" />
|
||||
@if (
|
||||
escalateForm.get('nextHopBankIfsc')?.hasError('required') &&
|
||||
escalateForm.get('nextHopBankIfsc')?.touched
|
||||
) {
|
||||
<mat-error>{{ 'labels.text.IFSC code is required' | translate }}</mat-error>
|
||||
}
|
||||
@if (
|
||||
escalateForm.get('nextHopBankIfsc')?.hasError('pattern') && escalateForm.get('nextHopBankIfsc')?.touched
|
||||
) {
|
||||
<mat-error>{{ 'labels.text.Invalid IFSC format' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{ 'labels.text.Remarks' | translate }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="remarks"
|
||||
rows="4"
|
||||
placeholder="{{ 'labels.text.Reason for escalation...' | translate }}"
|
||||
></textarea>
|
||||
@if (escalateForm.get('remarks')?.hasError('required') && escalateForm.get('remarks')?.touched) {
|
||||
<mat-error>{{ 'labels.text.Remarks are required' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="isSubmitting"
|
||||
class="full-width submit-btn"
|
||||
>
|
||||
@if (isSubmitting) {
|
||||
<mat-spinner diameter="20" class="inline-spinner"></mat-spinner>
|
||||
} @else {
|
||||
<fa-icon icon="paper-plane" class="mr-05"></fa-icon> {{ 'labels.text.Submit Escalation' | translate }}
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Instructions Panel -->
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<fa-icon icon="info-circle" class="mr-05"></fa-icon> {{ 'labels.text.Escalation Guidelines' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="guidelines">
|
||||
<div class="guideline-item">
|
||||
<fa-icon icon="check" class="guideline-icon"></fa-icon>
|
||||
<p>{{ 'labels.text.Verify the complaint details before escalating to the next bank.' | translate }}</p>
|
||||
</div>
|
||||
<div class="guideline-item">
|
||||
<fa-icon icon="check" class="guideline-icon"></fa-icon>
|
||||
<p>
|
||||
{{
|
||||
'labels.text.Ensure the Next Hop Account and IFSC belong to the next bank in the fraud transaction chain.'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="guideline-item">
|
||||
<fa-icon icon="check" class="guideline-icon"></fa-icon>
|
||||
<p>{{ 'labels.text.Provide clear and detailed remarks explaining the escalation reason.' | translate }}</p>
|
||||
</div>
|
||||
<div class="guideline-item">
|
||||
<fa-icon icon="exclamation-triangle" class="guideline-icon warn"></fa-icon>
|
||||
<p>{{ 'labels.text.Escalation must be completed within the I4C/MHA mandated timeline.' | translate }}</p>
|
||||
</div>
|
||||
<div class="guideline-item">
|
||||
<fa-icon icon="exclamation-triangle" class="guideline-icon warn"></fa-icon>
|
||||
<p>
|
||||
{{
|
||||
'labels.text.Once escalated, the complaint will be forwarded to the target bank for action.' | translate
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bank-info">
|
||||
<h4>{{ 'labels.text.Current Bank Info' | translate }}</h4>
|
||||
<div class="bank-row">
|
||||
<span>{{ 'labels.text.Bank Name' | translate }}:</span> <strong>LincPay</strong>
|
||||
</div>
|
||||
<div class="bank-row">
|
||||
<span>{{ 'labels.text.Bank IFSC' | translate }}:</span> <strong class="mono">LINC0000001</strong>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.escalate-page {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.escalate-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.escalate-card,
|
||||
.instructions-card {
|
||||
border-radius: 8px !important;
|
||||
|
||||
mat-card-header {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
padding: 16px 20px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-lookup-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
|
||||
.ticket-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lookup-btn {
|
||||
margin-top: 4px;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.lookup-result {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.lookup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 8px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
// Success result
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px;
|
||||
color: #065f46;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
|
||||
.result-label {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instructions card
|
||||
.guidelines {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.guideline-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
|
||||
.guideline-icon {
|
||||
color: #10b981;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.warn {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.bank-info {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.bank-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CfcfrmsService } from '../cfcfrms.service';
|
||||
import { StatusBadgeComponent } from '../components/status-badge/status-badge.component';
|
||||
import { ConfirmDialogComponent } from '../components/confirm-dialog/confirm-dialog.component';
|
||||
import { Complaint, EscalationResponse } from '../models/cfcfrms.models';
|
||||
|
||||
@Component({
|
||||
selector: 'cfcfrms-escalate',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
FaIconComponent,
|
||||
TranslateModule,
|
||||
StatusBadgeComponent
|
||||
],
|
||||
templateUrl: './escalate.component.html',
|
||||
styleUrls: ['./escalate.component.scss']
|
||||
})
|
||||
export class EscalateComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
private cfcfrmsService = inject(CfcfrmsService);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
escalateForm: FormGroup;
|
||||
isSubmitting = false;
|
||||
isLookingUp = false;
|
||||
lookedUpComplaint: Complaint | null = null;
|
||||
escalationResult: EscalationResponse | null = null;
|
||||
|
||||
constructor() {
|
||||
this.escalateForm = this.fb.group({
|
||||
ticketId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
nextHopAccountNumber: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
nextHopBankIfsc: [
|
||||
'',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)
|
||||
]
|
||||
],
|
||||
remarks: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
lookupTicket(): void {
|
||||
const ticketId = this.escalateForm.get('ticketId')?.value?.trim();
|
||||
if (!ticketId) {
|
||||
this.snackBar.open('Enter a Ticket ID first', 'Close', { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLookingUp = true;
|
||||
this.lookedUpComplaint = null;
|
||||
|
||||
this.cfcfrmsService.getComplaintById(ticketId).subscribe({
|
||||
next: (complaint) => {
|
||||
this.lookedUpComplaint = complaint;
|
||||
this.isLookingUp = false;
|
||||
},
|
||||
error: () => {
|
||||
this.isLookingUp = false;
|
||||
this.snackBar.open('Ticket not found', 'Close', { duration: 3000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.escalateForm.invalid) {
|
||||
this.escalateForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const ticketId = this.escalateForm.get('ticketId')?.value;
|
||||
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '420px',
|
||||
data: {
|
||||
title: 'Confirm Escalation',
|
||||
message: `Are you sure you want to escalate ticket ${ticketId} to the next bank? This will forward the complaint for further investigation.`,
|
||||
confirmText: 'Escalate',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitEscalation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private submitEscalation(): void {
|
||||
this.isSubmitting = true;
|
||||
const formVal = this.escalateForm.value;
|
||||
|
||||
this.cfcfrmsService
|
||||
.escalateComplaint(formVal.ticketId, {
|
||||
ticketId: formVal.ticketId,
|
||||
nextHopAccountNumber: formVal.nextHopAccountNumber,
|
||||
nextHopBankIfsc: formVal.nextHopBankIfsc,
|
||||
remarks: formVal.remarks
|
||||
})
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.escalationResult = result;
|
||||
this.isSubmitting = false;
|
||||
this.snackBar.open('Complaint escalated successfully', 'Close', { duration: 3000 });
|
||||
},
|
||||
error: () => {
|
||||
this.isSubmitting = false;
|
||||
this.snackBar.open('Failed to escalate complaint', 'Close', { duration: 3000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.escalateForm.reset();
|
||||
this.lookedUpComplaint = null;
|
||||
this.escalationResult = null;
|
||||
}
|
||||
|
||||
formatCurrency(amount: number): string {
|
||||
if (amount == null) return '-';
|
||||
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(
|
||||
amount
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user