s
This commit is contained in:
parent
eb1d24e807
commit
1dcfa081e0
@ -1,4 +1,3 @@
|
|||||||
// app/api/auth/logout/route.ts
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
@ -8,21 +7,18 @@ export async function DELETE() {
|
|||||||
// Clear the authentication cookie.
|
// Clear the authentication cookie.
|
||||||
// This MUST match the name of the cookie set during login.
|
// This MUST match the name of the cookie set during login.
|
||||||
// In your login handler, the cookie is named "auth_token".
|
// In your login handler, the cookie is named "auth_token".
|
||||||
(await
|
const cookieStore = await cookies();
|
||||||
// Clear the authentication cookie.
|
cookieStore.delete("auth_token");
|
||||||
// This MUST match the name of the cookie set during login.
|
|
||||||
// In your login handler, the cookie is named "auth_token".
|
|
||||||
cookies()).delete("auth_token");
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: true, message: "Logged out successfully" },
|
{ success: true, message: "Logged out successfully" },
|
||||||
{ status: 200 }
|
{ status: 200 },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout API error:", error);
|
console.error("Logout API error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Internal server error during logout" },
|
{ success: false, message: "Internal server error during logout" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
// This ensures this component is rendered only on the client side
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Approve } from "@/app/features/Pages/Approve/Approve";
|
import { Approve } from "@/app/features/Pages/Approve/Approve";
|
||||||
|
|
||||||
|
|
||||||
export default function ApprovePage() {
|
export default function ApprovePage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import "./LoginModal.scss"; // Adjust path based on your actual structure
|
|||||||
// Define the props interface for LoginModal
|
// Define the props interface for LoginModal
|
||||||
type LoginModalProps = {
|
type LoginModalProps = {
|
||||||
onLogin: (email: string, password: string) => Promise<boolean>;
|
onLogin: (email: string, password: string) => Promise<boolean>;
|
||||||
|
authMessage: string;
|
||||||
|
clearAuthMessage: () => void;
|
||||||
};
|
};
|
||||||
// LoginModal component
|
// LoginModal component
|
||||||
export default function LoginModal({
|
export default function LoginModal({ onLogin }: LoginModalProps) {
|
||||||
onLogin,
|
|
||||||
}: LoginModalProps) {
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import ThemeRegistry from "@/config/ThemeRegistry";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import ReduxProvider from "./redux/ReduxProvider";
|
import ReduxProvider from "./redux/ReduxProvider";
|
||||||
import "../styles/globals.scss";
|
import "../styles/globals.scss";
|
||||||
|
import { InitializeAuth } from "./redux/InitializeAuth";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Your App",
|
title: "Your App",
|
||||||
@ -17,6 +18,7 @@ export default function RootLayout({
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<ReduxProvider>
|
<ReduxProvider>
|
||||||
|
<InitializeAuth />
|
||||||
<ThemeRegistry>{children}</ThemeRegistry>
|
<ThemeRegistry>{children}</ThemeRegistry>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -14,42 +14,30 @@ import {
|
|||||||
import { clearAuthMessage, login } from "../redux/auth/authSlice";
|
import { clearAuthMessage, login } from "../redux/auth/authSlice";
|
||||||
import "./page.scss";
|
import "./page.scss";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPageClient() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
||||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
const authMessage = useSelector(selectAuthMessage);
|
const authMessage = useSelector(selectAuthMessage);
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const dispatch = useDispatch<AppDispatch>(); // Initialize useDispatch
|
|
||||||
|
|
||||||
// Effect to handle redirection after login or if already logged in
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn && status === "succeeded") {
|
if (isLoggedIn && status === "succeeded") {
|
||||||
router.replace(redirectPath); // Redirect to intended path on successful login
|
router.replace(redirectPath);
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, status, router, redirectPath]);
|
}, [isLoggedIn, status, router, redirectPath]);
|
||||||
|
|
||||||
// Function to handle login attempt, now dispatches Redux thunk
|
|
||||||
const handleLogin = async (email: string, password: string) => {
|
const handleLogin = async (email: string, password: string) => {
|
||||||
// Dispatch the login async thunk
|
|
||||||
const resultAction = await dispatch(login({ email, password }));
|
const resultAction = await dispatch(login({ email, password }));
|
||||||
|
return login.fulfilled.match(resultAction);
|
||||||
// Check if login was successful based on the thunk's result
|
|
||||||
if (login.fulfilled.match(resultAction)) {
|
|
||||||
return true; // Login successful
|
|
||||||
} else {
|
|
||||||
return false; // Login failed
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to clear authentication message, now dispatches Redux action
|
|
||||||
const handleClearAuthMessage = () => {
|
const handleClearAuthMessage = () => {
|
||||||
dispatch(clearAuthMessage());
|
dispatch(clearAuthMessage());
|
||||||
};
|
};
|
||||||
|
|
||||||
// If user is already logged in, show a redirecting message
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
@ -1,78 +1,10 @@
|
|||||||
// app/login/page.tsx
|
import { Suspense } from "react";
|
||||||
|
import LoginPageClient from "./LoginPageClient";
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useEffect, Suspense } from "react";
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import LoginModal from "../features/Auth/LoginModal";
|
|
||||||
import Modal from "../components/Modal/Modal";
|
|
||||||
import "./page.scss";
|
|
||||||
|
|
||||||
function LoginPageContent() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
|
||||||
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
||||||
|
|
||||||
console.log("LoginPageContent rendered", isLoggedIn); // Debugging log to check if the component renders
|
|
||||||
useEffect(() => {
|
|
||||||
const checkAuthStatus = async () => {
|
|
||||||
// Optionally implement
|
|
||||||
};
|
|
||||||
checkAuthStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLogin = async (email: string, password: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
router.replace(redirectPath);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login failed:", error);
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="page-container">
|
|
||||||
<div className="page-container__content">
|
|
||||||
<h1 className="page-container__title">Payment Backoffice</h1>
|
|
||||||
<p className="page-container__text">
|
|
||||||
Please log in to access the backoffice.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={true}
|
|
||||||
onClose={() => {}}
|
|
||||||
title="Login to Backoffice"
|
|
||||||
>
|
|
||||||
<LoginModal onLogin={handleLogin} />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Export wrapped in Suspense
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<LoginPageContent />
|
<LoginPageClient />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/redux/InitializeAuth.tsx
Normal file
16
app/redux/InitializeAuth.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { initializeAuth } from "./auth/authSlice";
|
||||||
|
import { AppDispatch } from "./types";
|
||||||
|
|
||||||
|
export function InitializeAuth() {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(initializeAuth());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -1,26 +1,13 @@
|
|||||||
// app/redux/ReduxProvider.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React from "react";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { makeStore } from "./store";
|
import { store } from "./store";
|
||||||
import { initializeAuth } from "./auth/authSlice";
|
|
||||||
|
|
||||||
export default function ReduxProvider({
|
export default function ReduxProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
// Create a store instance for this client-side session
|
return <Provider store={store}>{children}</Provider>;
|
||||||
const storeRef = useRef<ReturnType<typeof makeStore>>(makeStore());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Dispatch initializeAuth when the ReduxProvider component mounts on the client.
|
|
||||||
// This ensures your Redux isLoggedIn state is synced with localStorage after a page refresh.
|
|
||||||
if (storeRef.current) {
|
|
||||||
storeRef.current.dispatch(initializeAuth() as any);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <Provider store={storeRef.current}>{children}</Provider>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const login = createAsyncThunk(
|
|||||||
"auth/login",
|
"auth/login",
|
||||||
async (
|
async (
|
||||||
{ email, password }: { email: string; password: string },
|
{ email, password }: { email: string; password: string },
|
||||||
{ rejectWithValue }
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/auth/login", {
|
const response = await fetch("/api/auth/login", {
|
||||||
@ -47,14 +47,13 @@ export const login = createAsyncThunk(
|
|||||||
localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync
|
localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync
|
||||||
}
|
}
|
||||||
return data.message || "Login successful";
|
return data.message || "Login successful";
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
// Handle network errors or other unexpected issues
|
// Handle network errors or other unexpected issues
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return rejectWithValue(error.message || "Network error during login");
|
return rejectWithValue(error.message || "Network error during login");
|
||||||
}
|
}
|
||||||
return rejectWithValue("Network error during login");
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Async Thunk for Logout
|
// Async Thunk for Logout
|
||||||
@ -79,14 +78,13 @@ export const logout = createAsyncThunk(
|
|||||||
localStorage.removeItem("userToken"); // Clear client-side flag
|
localStorage.removeItem("userToken"); // Clear client-side flag
|
||||||
}
|
}
|
||||||
return data.message || "Logged out successfully";
|
return data.message || "Logged out successfully";
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
// Handle network errors
|
// Handle network errors
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return rejectWithValue(error.message || "Network error during logout");
|
return rejectWithValue(error.message || "Network error during logout");
|
||||||
}
|
}
|
||||||
return rejectWithValue("Network error during logout");
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the authentication slice
|
// Create the authentication slice
|
||||||
|
|||||||
@ -15,9 +15,3 @@ export const makeStore = () => {
|
|||||||
|
|
||||||
// Create the store instance
|
// Create the store instance
|
||||||
export const store = makeStore();
|
export const store = makeStore();
|
||||||
|
|
||||||
// Infer the type of makeStore
|
|
||||||
export type AppStore = ReturnType<typeof makeStore>;
|
|
||||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
|
||||||
export type RootState = ReturnType<AppStore["getState"]>;
|
|
||||||
export type AppDispatch = AppStore["dispatch"];
|
|
||||||
|
|||||||
41
payment-iq/.gitignore
vendored
41
payment-iq/.gitignore
vendored
@ -1,41 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
|
||||||
.env*
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# 🛠️ Backoffice Admin Panel
|
|
||||||
|
|
||||||
A modern backoffice admin panel built with [Next.js](https://nextjs.org/) and [React](https://react.dev), providing a scalable and maintainable foundation for managing internal tools, content, and operations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Tech Stack
|
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/) – App Router with SSR support
|
|
||||||
- [React](https://reactjs.org/)
|
|
||||||
- [TypeScript](https://www.typescriptlang.org/)
|
|
||||||
- [Material UI](https://mui.com/) – UI component library
|
|
||||||
- [Axios](https://axios-http.com/) – API client
|
|
||||||
- [Jest](https://jestjs.io/) / [React Testing Library](https://testing-library.com/) – Unit & integration testing
|
|
||||||
- [ESLint](https://eslint.org/) / [Prettier](https://prettier.io/) – Code quality & formatting
|
|
||||||
- [dotenv](https://github.com/motdotla/dotenv) – Environment variable management
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Getting Started
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.luckyigaming.com/Mitchell/payment-backoffice.git
|
|
||||||
cd backoffice
|
|
||||||
|
|
||||||
yarn run dev
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { cookies } from "next/headers";
|
|
||||||
|
|
||||||
// This is your POST handler for the login endpoint
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const { email, password } = await request.json();
|
|
||||||
|
|
||||||
// --- Replace with your ACTUAL authentication logic ---
|
|
||||||
// In a real application, you would:
|
|
||||||
// 1. Query your database for the user by email.
|
|
||||||
// 2. Hash the provided password and compare it to the stored hashed password.
|
|
||||||
// 3. If credentials match, generate a secure JWT (JSON Web Token) or session ID.
|
|
||||||
// 4. Store the token/session ID securely (e.g., in a database or Redis).
|
|
||||||
|
|
||||||
// Mock authentication for demonstration purposes:
|
|
||||||
if (email === "admin@example.com" && password === "password123") {
|
|
||||||
const authToken = "mock-jwt-token-12345"; // Replace with a real, securely generated token
|
|
||||||
|
|
||||||
// Set the authentication token as an HTTP-only cookie
|
|
||||||
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
|
|
||||||
// which mitigates XSS attacks.
|
|
||||||
(
|
|
||||||
await // Set the authentication token as an HTTP-only cookie
|
|
||||||
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
|
|
||||||
// which mitigates XSS attacks.
|
|
||||||
cookies()
|
|
||||||
).set("auth_token", authToken, {
|
|
||||||
httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts
|
|
||||||
secure: process.env.NODE_ENV === "production", // Use secure in production (HTTPS)
|
|
||||||
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
||||||
path: "/", // Available across the entire site
|
|
||||||
sameSite: "lax", // Protects against CSRF
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: true, message: "Login successful" },
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Invalid credentials" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login API error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Internal server error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// app/api/auth/logout/route.ts
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { cookies } from "next/headers";
|
|
||||||
|
|
||||||
// This is your DELETE handler for the logout endpoint
|
|
||||||
export async function DELETE(request: Request) {
|
|
||||||
try {
|
|
||||||
// Clear the authentication cookie.
|
|
||||||
// This MUST match the name of the cookie set during login.
|
|
||||||
// In your login handler, the cookie is named "auth_token".
|
|
||||||
cookies().delete("auth_token");
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: true, message: "Logged out successfully" },
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Logout API error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Internal server error during logout" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
// app/api/dashboard/admin/users/route.ts
|
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
|
|
||||||
const users = [
|
|
||||||
{
|
|
||||||
merchantId: 100987998,
|
|
||||||
id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
|
|
||||||
name: "Jacob",
|
|
||||||
username: "lspaddy",
|
|
||||||
firstName: "Paddy",
|
|
||||||
lastName: "Man",
|
|
||||||
email: "patrick@omegasys.eu",
|
|
||||||
phone: "",
|
|
||||||
jobTitle: "",
|
|
||||||
enabled: true,
|
|
||||||
authorities: [
|
|
||||||
"ROLE_IIN",
|
|
||||||
"ROLE_FIRST_APPROVER",
|
|
||||||
"ROLE_RULES_ADMIN",
|
|
||||||
"ROLE_TRANSACTION_VIEWER",
|
|
||||||
"ROLE_IIN_ADMIN",
|
|
||||||
"ROLE_USER_PSP_ACCOUNT",
|
|
||||||
],
|
|
||||||
allowedMerchantIds: [100987998],
|
|
||||||
created: "2025-05-04T15:32:48.432Z",
|
|
||||||
disabledBy: null,
|
|
||||||
disabledDate: null,
|
|
||||||
disabledReason: null,
|
|
||||||
incidentNotes: false,
|
|
||||||
lastLogin: "",
|
|
||||||
lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
|
|
||||||
marketingNewsletter: false,
|
|
||||||
releaseNotes: false,
|
|
||||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
|
|
||||||
twoFactorCondition: "required",
|
|
||||||
twoFactorCredentials: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
const body = await request.json();
|
|
||||||
const { firstName, lastName, email, phone, role } = body;
|
|
||||||
|
|
||||||
// Add the new user to the existing users array (in-memory, not persistent)
|
|
||||||
const newUser = {
|
|
||||||
merchantId: 100987998,
|
|
||||||
name: "Jacob",
|
|
||||||
id: "382eed15-1e21-41fa-b1f3-0c1adb3af714",
|
|
||||||
username: "lsterence",
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
phone,
|
|
||||||
jobTitle: "",
|
|
||||||
role,
|
|
||||||
enabled: true,
|
|
||||||
authorities: ["ROLE_IIN", "ROLE_FIRST_APPROVER", "ROLE_RULES_ADMIN"],
|
|
||||||
allowedMerchantIds: [100987998],
|
|
||||||
created: "2025-05-04T15:32:48.432Z",
|
|
||||||
disabledBy: null,
|
|
||||||
disabledDate: null,
|
|
||||||
disabledReason: null,
|
|
||||||
incidentNotes: false,
|
|
||||||
lastLogin: "",
|
|
||||||
lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
|
|
||||||
marketingNewsletter: false,
|
|
||||||
releaseNotes: false,
|
|
||||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
|
|
||||||
twoFactorCondition: "required",
|
|
||||||
twoFactorCredentials: [],
|
|
||||||
};
|
|
||||||
users.push(newUser);
|
|
||||||
|
|
||||||
return NextResponse.json(users, { status: 201 });
|
|
||||||
}
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import { GridColDef } from "@mui/x-data-grid";
|
|
||||||
|
|
||||||
export const AuditColumns: GridColDef[] = [
|
|
||||||
{ field: "actionType", headerName: "Action Type", width: 130 },
|
|
||||||
{
|
|
||||||
field: "timeStampOfTheAction",
|
|
||||||
headerName: "Timestamp of the action",
|
|
||||||
width: 130,
|
|
||||||
},
|
|
||||||
{ field: "adminUsername", headerName: "Admin username", width: 130 },
|
|
||||||
{ field: "adminId", headerName: "Admin ID", width: 130 },
|
|
||||||
{ field: "affectedUserId", headerName: "Affected user ID", width: 130 },
|
|
||||||
{ field: "adminIPAddress", headerName: "Admin IP address", width: 130 },
|
|
||||||
{ field: "reasonNote", headerName: "Reason/Note", width: 130 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AuditData = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
actionType: "Create",
|
|
||||||
timeStampOfTheAction: "2023-03-01T12:00:00",
|
|
||||||
adminUsername: "admin1",
|
|
||||||
adminId: "12345",
|
|
||||||
affectedUserId: "67890",
|
|
||||||
adminIPAddress: "192.168.1.1",
|
|
||||||
reasonNote: "New user created",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
actionType: "Update",
|
|
||||||
timeStampOfTheAction: "2023-03-02T12:00:00",
|
|
||||||
adminUsername: "admin2",
|
|
||||||
adminId: "54321",
|
|
||||||
affectedUserId: "09876",
|
|
||||||
adminIPAddress: "192.168.2.2",
|
|
||||||
reasonNote: "User details updated",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
actionType: "Delete",
|
|
||||||
timeStampOfTheAction: "2023-03-03T12:00:00",
|
|
||||||
adminUsername: "admin3",
|
|
||||||
adminId: "98765",
|
|
||||||
affectedUserId: "45678",
|
|
||||||
adminIPAddress: "192.168.3.3",
|
|
||||||
reasonNote: "User deleted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
actionType: "Create",
|
|
||||||
timeStampOfTheAction: "2023-03-04T12:00:00",
|
|
||||||
adminUsername: "admin4",
|
|
||||||
adminId: "98765",
|
|
||||||
affectedUserId: "45678",
|
|
||||||
adminIPAddress: "192.168.3.3",
|
|
||||||
reasonNote: "New user created",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
actionType: "Update",
|
|
||||||
timeStampOfTheAction: "2023-03-05T12:00:00",
|
|
||||||
adminUsername: "admin2",
|
|
||||||
adminId: "98765",
|
|
||||||
affectedUserId: "45678",
|
|
||||||
adminIPAddress: "192.168.3.3",
|
|
||||||
reasonNote: "User details updated",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AuditSearchLabels = [
|
|
||||||
{ label: "Action Type", field: "actionType", type: "text" },
|
|
||||||
{ label: "Date / Time", field: "dateTime", type: "date" },
|
|
||||||
{
|
|
||||||
label: "affectedUserId",
|
|
||||||
field: "Affected user ID",
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Admin ID",
|
|
||||||
field: "adminId",
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Admin username",
|
|
||||||
field: "adminUsername",
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData";
|
|
||||||
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const actionType = searchParams.get("actionType");
|
|
||||||
const affectedUserId = searchParams.get("affectedUserId");
|
|
||||||
const adminId = searchParams.get("adminId");
|
|
||||||
const adminUsername = searchParams.get("adminUsername");
|
|
||||||
const timeStampOfTheAction = searchParams.get("dateTime");
|
|
||||||
|
|
||||||
let filteredRows = [...AuditData];
|
|
||||||
|
|
||||||
if (actionType) {
|
|
||||||
filteredRows = filteredRows.filter(
|
|
||||||
(tx) => tx.actionType.toLocaleLowerCase() === actionType.toLocaleLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (affectedUserId) {
|
|
||||||
filteredRows = filteredRows.filter(
|
|
||||||
(tx) => tx.affectedUserId.toLowerCase() === affectedUserId.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adminId) {
|
|
||||||
filteredRows = filteredRows.filter(
|
|
||||||
(tx) => tx.adminId === adminId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (adminUsername) {
|
|
||||||
filteredRows = filteredRows.filter(
|
|
||||||
(tx) => tx.adminUsername === adminUsername,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeStampOfTheAction) {
|
|
||||||
filteredRows = filteredRows.filter(
|
|
||||||
(tx) =>
|
|
||||||
tx.timeStampOfTheAction.split(" ")[0] ===
|
|
||||||
formatToDateTimeString(timeStampOfTheAction).split(" ")[0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
tableRows: filteredRows,
|
|
||||||
tableColumns: AuditColumns,
|
|
||||||
tableSearchLabels: AuditSearchLabels,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,256 +0,0 @@
|
|||||||
import { GridColDef } from "@mui/x-data-grid";
|
|
||||||
|
|
||||||
export const depositTransactionDummyData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
userId: 17,
|
|
||||||
merchandId: 100987998,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Completed",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
userId: 17,
|
|
||||||
merchandId: 100987998,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Completed",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
userId: 17,
|
|
||||||
merchandId: 100987997,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Completed",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
userId: 19,
|
|
||||||
merchandId: 100987997,
|
|
||||||
transactionId: 1049136973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Completed",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
userId: 19,
|
|
||||||
merchandId: 100987998,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Completed",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
userId: 27,
|
|
||||||
merchandId: 100987997,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Pending",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
userId: 175,
|
|
||||||
merchandId: 100987938,
|
|
||||||
transactionId: 1049136973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Pending",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
userId: 172,
|
|
||||||
merchandId: 100987938,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Card",
|
|
||||||
status: "Pending",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-12 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
userId: 174,
|
|
||||||
merchandId: 100987938,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Bank Transfer",
|
|
||||||
status: "Inprogress",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
userId: 7,
|
|
||||||
merchandId: 100987998,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Bank Transfer",
|
|
||||||
status: "Inprogress",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
userId: 1,
|
|
||||||
merchandId: 100987998,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
depositMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
options: [
|
|
||||||
{ value: "Pending", label: "Pending" },
|
|
||||||
{ value: "Completed", label: "Completed" },
|
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
|
||||||
{ value: "Error", label: "Error" },
|
|
||||||
],
|
|
||||||
amount: 4000,
|
|
||||||
currency: "EUR",
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const depositTransactionsColumns: GridColDef[] = [
|
|
||||||
{ field: "userId", headerName: "User ID", width: 130 },
|
|
||||||
{ field: "merchandId", headerName: "Merchant ID", width: 130 },
|
|
||||||
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
|
||||||
{ field: "depositMethod", headerName: "Deposit Method", width: 130 },
|
|
||||||
{ field: "status", headerName: "Status", width: 130 },
|
|
||||||
{ field: "actions", headerName: "Actions", width: 150 },
|
|
||||||
{ field: "amount", headerName: "Amount", width: 130 },
|
|
||||||
{ field: "currency", headerName: "Currency", width: 130 },
|
|
||||||
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
|
||||||
{ field: "errorInfo", headerName: "Error Info", width: 130 },
|
|
||||||
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const depositTransactionsSearchLabels = [
|
|
||||||
{ label: "User", field: "userId", type: "text" },
|
|
||||||
{ label: "Transaction ID", field: "transactionId", type: "text" },
|
|
||||||
{
|
|
||||||
label: "Transaction Reference ID",
|
|
||||||
field: "transactionReferenceId",
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Currency",
|
|
||||||
field: "currency",
|
|
||||||
type: "select",
|
|
||||||
options: ["USD", "EUR", "GBP"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Status",
|
|
||||||
field: "status",
|
|
||||||
type: "select",
|
|
||||||
options: ["Pending", "Inprogress", "Completed", "Failed"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Payment Method",
|
|
||||||
field: "depositMethod",
|
|
||||||
type: "select",
|
|
||||||
options: ["Card", "Bank Transfer"],
|
|
||||||
},
|
|
||||||
{ label: "Date / Time", field: "dateTime", type: "date" },
|
|
||||||
];
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import {
|
|
||||||
depositTransactionDummyData,
|
|
||||||
depositTransactionsColumns,
|
|
||||||
depositTransactionsSearchLabels,
|
|
||||||
} from "./mockData";
|
|
||||||
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const status = searchParams.get("status");
|
|
||||||
const userId = searchParams.get("userId");
|
|
||||||
const depositMethod = searchParams.get("depositMethod");
|
|
||||||
const merchandId = searchParams.get("merchandId");
|
|
||||||
const transactionId = searchParams.get("transactionId");
|
|
||||||
const dateTime = searchParams.get("dateTime");
|
|
||||||
|
|
||||||
let filteredTransactions = [...depositTransactionDummyData];
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.userId.toString() === userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.status.toLowerCase() === status.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depositMethod) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.depositMethod.toLowerCase() === depositMethod.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (merchandId) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.merchandId.toString() === merchandId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (transactionId) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.transactionId.toString() === transactionId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateTime) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) =>
|
|
||||||
tx.dateTime.split(" ")[0] ===
|
|
||||||
formatToDateTimeString(dateTime).split(" ")[0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
tableRows: filteredTransactions,
|
|
||||||
tableSearchLabels: depositTransactionsSearchLabels,
|
|
||||||
tableColumns: depositTransactionsColumns,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
import { GridColDef } from "@mui/x-data-grid";
|
|
||||||
|
|
||||||
export const withdrawalTransactionDummyData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
userId: 17,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
userId: 17,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
userId: 17,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Complete",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
userId: 19,
|
|
||||||
transactionId: 1049136973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Completed",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-18 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
userId: 19,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
userId: 27,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
userId: 1,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
userId: 172,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Card",
|
|
||||||
status: "Pending",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-12 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
userId: 174,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Inprogress",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
userId: 1,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
userId: 1,
|
|
||||||
transactionId: 1049131973,
|
|
||||||
withdrawalMethod: "Bank Transfer",
|
|
||||||
status: "Error",
|
|
||||||
amount: 4000,
|
|
||||||
dateTime: "2025-06-17 10:10:30",
|
|
||||||
errorInfo: "-",
|
|
||||||
fraudScore: "frad score 1234",
|
|
||||||
manualCorrectionFlag: "-",
|
|
||||||
informationWhoApproved: "-",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const withdrawalTransactionsColumns: GridColDef[] = [
|
|
||||||
{ field: "userId", headerName: "User ID", width: 130 },
|
|
||||||
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
|
||||||
{ field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 },
|
|
||||||
{ field: "status", headerName: "Status", width: 130 },
|
|
||||||
{ field: "amount", headerName: "Amount", width: 130 },
|
|
||||||
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
|
||||||
{ field: "errorInfo", headerName: "Error Info", width: 130 },
|
|
||||||
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
|
|
||||||
{
|
|
||||||
field: "manualCorrectionFlag",
|
|
||||||
headerName: "Manual Correction Flag",
|
|
||||||
width: 130,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "informationWhoApproved",
|
|
||||||
headerName: "Information who approved",
|
|
||||||
width: 130,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const withdrawalTransactionsSearchLabels = [
|
|
||||||
{
|
|
||||||
label: "Status",
|
|
||||||
field: "status",
|
|
||||||
type: "select",
|
|
||||||
options: ["Pending", "Inprogress", "Completed", "Failed"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Payment Method",
|
|
||||||
field: "depositMethod",
|
|
||||||
type: "select",
|
|
||||||
options: ["Card", "Bank Transfer"],
|
|
||||||
},
|
|
||||||
{ label: "Date / Time", field: "dateTime", type: "date" },
|
|
||||||
];
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import {
|
|
||||||
withdrawalTransactionDummyData,
|
|
||||||
withdrawalTransactionsColumns,
|
|
||||||
withdrawalTransactionsSearchLabels,
|
|
||||||
} from "./mockData";
|
|
||||||
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const userId = searchParams.get("userId");
|
|
||||||
const status = searchParams.get("status");
|
|
||||||
const dateTime = searchParams.get("dateTime");
|
|
||||||
const withdrawalMethod = searchParams.get("withdrawalMethod");
|
|
||||||
|
|
||||||
let filteredTransactions = [...withdrawalTransactionDummyData];
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.userId.toString() === userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) => tx.status.toLowerCase() === status.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withdrawalMethod) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) =>
|
|
||||||
tx.withdrawalMethod.toLowerCase() === withdrawalMethod.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateTime) {
|
|
||||||
filteredTransactions = filteredTransactions.filter(
|
|
||||||
(tx) =>
|
|
||||||
tx.dateTime.split(" ")[0] ===
|
|
||||||
formatToDateTimeString(dateTime).split(" ")[0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
tableRows: filteredTransactions,
|
|
||||||
tableSearchLabels: withdrawalTransactionsSearchLabels,
|
|
||||||
tableColumns: withdrawalTransactionsColumns,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
.modal__overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2);
|
|
||||||
position: relative;
|
|
||||||
min-width: 320px;
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 2rem 1.5rem 1.5rem 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__close {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #888;
|
|
||||||
transition: color 0.2s;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
color: #333;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__body {
|
|
||||||
// Example element block for modal content
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #222;
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import "./Modal.scss";
|
|
||||||
|
|
||||||
interface ModalProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose?: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
overlayClassName?: string;
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
title,
|
|
||||||
className = "",
|
|
||||||
}) => {
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={"modal__overlay"}
|
|
||||||
onClick={onClose}
|
|
||||||
data-testid="modal-overlay"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`modal${className ? " " + className : ""}`}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
data-testid="modal-content"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="modal__close"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
{title && <h2 className="modal__title">{title}</h2>}
|
|
||||||
<div className={"modal__body"}>{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Modal;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
.page-link__container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 1px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: var(--hover-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.page-link__text {
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
margin-left: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
// app/components/PageLinks/PageLinks.tsx
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; // Keep this import
|
|
||||||
import clsx from "clsx"; // Utility to merge class names
|
|
||||||
import "./PageLinks.scss"; // Keep this import
|
|
||||||
|
|
||||||
// Define the props interface for your PageLinks component
|
|
||||||
// It now extends ISidebarLink and includes isShowIcon
|
|
||||||
interface IPageLinksProps extends ISidebarLink {
|
|
||||||
isShowIcon?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageLinks component
|
|
||||||
export default function PageLinks({
|
|
||||||
title,
|
|
||||||
path,
|
|
||||||
icon: Icon, // Destructure icon as Icon
|
|
||||||
}: // isShowIcon, // If you plan to use this prop, uncomment it and add logic
|
|
||||||
IPageLinksProps) {
|
|
||||||
return (
|
|
||||||
// Corrected Link usage for Next.js 13/14 App Router:
|
|
||||||
// - Removed `passHref` and `legacyBehavior`
|
|
||||||
// - Applied `className` directly to the Link component
|
|
||||||
// - Removed the nested `<a>` tag
|
|
||||||
<Link href={path} className={clsx("page-link", "page-link__container")}>
|
|
||||||
{/* Conditionally render Icon if it exists */}
|
|
||||||
{Icon && <Icon />}
|
|
||||||
<span className="page-link__text">{title}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Box, Chip, Typography, Button } from '@mui/material';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
interface SearchFiltersProps {
|
|
||||||
filters: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchFilters = ({ filters }: SearchFiltersProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const filterLabels: Record<string, string> = {
|
|
||||||
userId: "User",
|
|
||||||
state: "State",
|
|
||||||
startDate: "Start Date",
|
|
||||||
// Add others here
|
|
||||||
};
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const handleDeleteFilter = (key: string) => {
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
params.delete(key);
|
|
||||||
router.push(`?${params.toString()}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const onClearAll = () => {
|
|
||||||
router.push("?");
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderChip = (label: string, value: string, key: string) => (
|
|
||||||
<Chip
|
|
||||||
key={key}
|
|
||||||
label={
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontWeight: key === "state" ? "bold" : "normal" }}
|
|
||||||
>
|
|
||||||
{label}: {value}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
onDelete={() => handleDeleteFilter(key)}
|
|
||||||
sx={{ mr: 1, mb: 1 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box display="flex" alignItems="center" flexWrap="wrap" sx={{ p: 2 }}>
|
|
||||||
{Object.entries(filters).map(([key, value]) =>
|
|
||||||
value ? renderChip(filterLabels[key] ?? key, value, key) : null
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Object.values(filters).some(Boolean) && (
|
|
||||||
<Button
|
|
||||||
onClick={onClearAll}
|
|
||||||
sx={{ ml: 1, textDecoration: "underline", color: "black" }}
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default SearchFilters;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
export default function BackOfficeUsersPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* This page will now be rendered on the client-side */}
|
|
||||||
hello
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import Users from "@/app/features/pages/Admin/Users/users";
|
|
||||||
|
|
||||||
export default async function BackOfficeUsersPage() {
|
|
||||||
const baseUrl =
|
|
||||||
process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL
|
|
||||||
? `https://${process.env.VERCEL_URL}`
|
|
||||||
: "http://localhost:3000";
|
|
||||||
const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, {
|
|
||||||
cache: "no-store", // 👈 disables caching for SSR freshness
|
|
||||||
});
|
|
||||||
const users = await res.json();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Users users={users} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
// This ensures this component is rendered only on the client side
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Approve } from "@/app/features/pages/Approve/Approve";
|
|
||||||
|
|
||||||
|
|
||||||
export default function ApprovePage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* This page will now be rendered on the client-side */}
|
|
||||||
<Approve />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import DataTable from "@/app/features/DataTable/DataTable";
|
|
||||||
import { getAudits } from "@/app/services/audits";
|
|
||||||
|
|
||||||
export default async function AuditPage({
|
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
||||||
}) {
|
|
||||||
// Await searchParams before processing
|
|
||||||
const params = await searchParams;
|
|
||||||
// Create a safe query string by filtering only string values
|
|
||||||
const safeParams: Record<string, string> = {};
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
safeParams[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const query = new URLSearchParams(safeParams).toString();
|
|
||||||
const data = await getAudits({ query });
|
|
||||||
|
|
||||||
return <DataTable data={data} />;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
// This ensures this component is rendered only on the client side
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function InvestigatePage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* This page will now be rendered on the client-side */}
|
|
||||||
<h1>Investigate</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
// 'use client';
|
|
||||||
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function KycPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Typography variant="h4" gutterBottom>
|
|
||||||
KYC Overview
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { LayoutWrapper } from "../features/dashboard/layout/layoutWrapper";
|
|
||||||
import { MainContent } from "../features/dashboard/layout/mainContent";
|
|
||||||
import SideBar from "../features/dashboard/sidebar/Sidebar";
|
|
||||||
import Header from "../features/dashboard/header/Header";
|
|
||||||
|
|
||||||
const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
useEffect(() => {
|
|
||||||
// if (process.env.NODE_ENV === "development") {
|
|
||||||
import("../../mock/browser").then(({ worker }) => {
|
|
||||||
worker.start();
|
|
||||||
});
|
|
||||||
// }
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayoutWrapper>
|
|
||||||
<SideBar />
|
|
||||||
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
|
|
||||||
<MainContent>
|
|
||||||
<Header />
|
|
||||||
{children}
|
|
||||||
</MainContent>
|
|
||||||
</div>
|
|
||||||
</LayoutWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DashboardLayout;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// app/dashboard/loading.tsx
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
|
||||||
import { styled } from "@mui/system";
|
|
||||||
|
|
||||||
const LoaderWrapper = styled("div")({
|
|
||||||
height: "100vh",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<LoaderWrapper>
|
|
||||||
<CircularProgress />
|
|
||||||
</LoaderWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { DashboardHomePage } from "../features/pages/DashboardHomePage/DashboardHomePage";
|
|
||||||
|
|
||||||
const DashboardPage = () => {
|
|
||||||
return <DashboardHomePage />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DashboardPage;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export default function RulesPage() {
|
|
||||||
return (
|
|
||||||
<div style={{ width: "100%" }}>
|
|
||||||
<h2>Rules Overview - SSR</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import DataTable from "@/app/features/DataTable/DataTable";
|
|
||||||
import { getTransactions } from "@/app/services/transactions";
|
|
||||||
|
|
||||||
export default async function DepositTransactionPage({
|
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
||||||
}) {
|
|
||||||
// Await searchParams before processing
|
|
||||||
const params = await searchParams;
|
|
||||||
// Create a safe query string by filtering only string values
|
|
||||||
const safeParams: Record<string, string> = {};
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
safeParams[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const query = new URLSearchParams(safeParams).toString();
|
|
||||||
const transactionType = "deposits";
|
|
||||||
const data = await getTransactions({ transactionType, query });
|
|
||||||
|
|
||||||
return <DataTable data={data} />;
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export default async function HistoryTransactionPage() {
|
|
||||||
return <div>History Transactions Page</div>;
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import DataTable from "@/app/features/DataTable/DataTable";
|
|
||||||
import { getTransactions } from "@/app/services/transactions";
|
|
||||||
|
|
||||||
export default async function WithdrawalTransactionPage({
|
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
||||||
}) {
|
|
||||||
// Await searchParams before processing
|
|
||||||
const params = await searchParams;
|
|
||||||
// Create a safe query string by filtering only string values
|
|
||||||
const safeParams: Record<string, string> = {};
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
safeParams[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const query = new URLSearchParams(safeParams).toString();
|
|
||||||
const transactionType = "withdrawal";
|
|
||||||
const data = await getTransactions({ transactionType, query });
|
|
||||||
|
|
||||||
return <DataTable data={data} />;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
.account-iq {
|
|
||||||
.account-iq__icon {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #4ecdc4;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { Box } from "@mui/material";
|
|
||||||
import { SectionCard } from "../SectionCard/SectionCard";
|
|
||||||
import "./AccountIQ.scss";
|
|
||||||
|
|
||||||
export const AccountIQ = () => {
|
|
||||||
return (
|
|
||||||
<Box className="account-iq">
|
|
||||||
<SectionCard
|
|
||||||
title="AccountIQ"
|
|
||||||
icon={<div className="account-iq__icon">AIQ</div>}
|
|
||||||
items={[
|
|
||||||
{ title: "Automatically reconcile your transactions" },
|
|
||||||
{ title: "Live wallet balances from providers" },
|
|
||||||
{ title: "Gaming provider financial overviews" },
|
|
||||||
{ title: "Learn more" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
TextField,
|
|
||||||
MenuItem,
|
|
||||||
Button,
|
|
||||||
Drawer,
|
|
||||||
FormControl,
|
|
||||||
Select,
|
|
||||||
Typography,
|
|
||||||
Stack,
|
|
||||||
debounce,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
|
||||||
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
|
||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import { ISearchLabel } from "../DataTable/types";
|
|
||||||
|
|
||||||
export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initialParams = Object.fromEntries(searchParams.entries());
|
|
||||||
setFormValues(initialParams);
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
const updateURL = useMemo(
|
|
||||||
() =>
|
|
||||||
debounce((newValues: Record<string, string>) => {
|
|
||||||
const updatedParams = new URLSearchParams();
|
|
||||||
Object.entries(newValues).forEach(([key, value]) => {
|
|
||||||
if (value) updatedParams.set(key, value);
|
|
||||||
});
|
|
||||||
router.push(`?${updatedParams.toString()}`);
|
|
||||||
}, 500),
|
|
||||||
[router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: string) => {
|
|
||||||
const updatedValues = { ...formValues, [field]: value };
|
|
||||||
setFormValues(updatedValues);
|
|
||||||
updateURL(updatedValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setFormValues({});
|
|
||||||
router.push("?");
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDrawer =
|
|
||||||
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
|
||||||
if (
|
|
||||||
event.type === "keydown" &&
|
|
||||||
((event as React.KeyboardEvent).key === "Tab" ||
|
|
||||||
(event as React.KeyboardEvent).key === "Shift")
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpen(open);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ width: "185px" }}>
|
|
||||||
<Button
|
|
||||||
sx={{
|
|
||||||
borderRadius: "8px",
|
|
||||||
textTransform: "none",
|
|
||||||
backgroundColor: "#f5f5f5",
|
|
||||||
color: "#555",
|
|
||||||
padding: "6px 12px",
|
|
||||||
boxShadow: "inset 0 0 0 1px #ddd",
|
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: "16px",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
"& .MuiButton-startIcon": {
|
|
||||||
borderRadius: "4px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "#e0e0e0",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
startIcon={<SearchIcon />}
|
|
||||||
onClick={toggleDrawer(true)}
|
|
||||||
>
|
|
||||||
Advanced Search
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
|
|
||||||
<Box sx={{ width: 400 }} role="presentation">
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
||||||
<Box p={2}>
|
|
||||||
<Box sx={{ display: "flex", gap: "60px" }}>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Search
|
|
||||||
</Typography>
|
|
||||||
<Box display="flex" justifyContent="flex-end" gap={2}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={<SearchIcon />}
|
|
||||||
onClick={() => console.log("Params:", formValues)}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
onClick={resetForm}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Stack spacing={2}>
|
|
||||||
{labels?.map(({ label, field, type, options }) => (
|
|
||||||
<Box key={field}>
|
|
||||||
<Typography variant="body2" fontWeight={600} mb={0.5}>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{type === "text" && (
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
value={formValues[field] || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFieldChange(field, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === "select" && (
|
|
||||||
<FormControl fullWidth size="small">
|
|
||||||
<Select
|
|
||||||
value={formValues[field] || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFieldChange(field, e.target.value)
|
|
||||||
}
|
|
||||||
displayEmpty
|
|
||||||
>
|
|
||||||
<MenuItem value="">
|
|
||||||
<em>{label}</em>
|
|
||||||
</MenuItem>
|
|
||||||
{options?.map((option) => (
|
|
||||||
<MenuItem value={option} key={option}>
|
|
||||||
{option}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === "date" && (
|
|
||||||
<DatePicker
|
|
||||||
value={
|
|
||||||
formValues[field] ? new Date(formValues[field]) : null
|
|
||||||
}
|
|
||||||
onChange={(newValue) =>
|
|
||||||
handleFieldChange(
|
|
||||||
field,
|
|
||||||
newValue?.toISOString() || ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
slotProps={{
|
|
||||||
textField: { fullWidth: true, size: "small" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</Box>
|
|
||||||
</Drawer>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,197 +0,0 @@
|
|||||||
/* app/styles/LoginModal.scss (BEM Methodology) */
|
|
||||||
|
|
||||||
// Variables for consistent styling
|
|
||||||
$primary-color: #2563eb; // Blue-600 equivalent
|
|
||||||
$primary-hover-color: #1d4ed8; // Blue-700 equivalent
|
|
||||||
$success-color: #16a34a; // Green-600 equivalent
|
|
||||||
$error-color: #dc2626; // Red-600 equivalent
|
|
||||||
$text-color-dark: #1f2937; // Gray-800 equivalent
|
|
||||||
$text-color-medium: #4b5563; // Gray-700 equivalent
|
|
||||||
$text-color-light: #6b7280; // Gray-600 equivalent
|
|
||||||
$border-color: #d1d5db; // Gray-300 equivalent
|
|
||||||
$bg-color-light: #f3f4f6; // Gray-100 equivalent
|
|
||||||
$bg-color-white: #ffffff;
|
|
||||||
|
|
||||||
/* --- Login Modal Block (.login-modal) --- */
|
|
||||||
.login-modal__overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(17, 24, 39, 0.75); // Gray-900 75% opacity
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 50;
|
|
||||||
padding: 1rem; // p-4
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-modal__content {
|
|
||||||
background-color: $bg-color-white;
|
|
||||||
border-radius: 0.75rem; // rounded-xl
|
|
||||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
||||||
0 10px 10px -5px rgba(0, 0, 0, 0.04); // shadow-2xl
|
|
||||||
padding: 2rem; // p-8
|
|
||||||
width: 100%;
|
|
||||||
max-width: 28rem; // max-w-md
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
transition: all 0.3s ease-in-out; // transition-all duration-300
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-modal__title {
|
|
||||||
font-size: 1.875rem; // text-3xl
|
|
||||||
font-weight: 700; // font-bold
|
|
||||||
color: $text-color-dark;
|
|
||||||
margin-bottom: 1.5rem; // mb-6
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Login Form Block (.login-form) --- */
|
|
||||||
.login-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem; // space-y-6
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__group {
|
|
||||||
// No specific styles needed here, just a container for label/input
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem; // text-sm
|
|
||||||
font-weight: 500; // font-medium
|
|
||||||
color: $text-color-medium;
|
|
||||||
margin-bottom: 0.25rem; // mb-1
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__input {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 1rem; // px-4 py-2
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: 0.5rem; // rounded-lg
|
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm
|
|
||||||
font-size: 0.875rem; // sm:text-sm
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-color: $primary-color; // focus:border-blue-500
|
|
||||||
box-shadow: 0 0 0 1px $primary-color, 0 0 0 3px rgba($primary-color, 0.5); // focus:ring-blue-500
|
|
||||||
}
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__message {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.875rem; // text-sm
|
|
||||||
font-weight: 500; // font-medium
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__message--success {
|
|
||||||
color: $success-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__message--error {
|
|
||||||
color: $error-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__button {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.75rem 1rem; // py-3 px-4
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 0.5rem; // rounded-lg
|
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm
|
|
||||||
font-size: 1.125rem; // text-lg
|
|
||||||
font-weight: 600; // font-semibold
|
|
||||||
color: $bg-color-white;
|
|
||||||
background-color: $primary-color;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; // transition duration-300 ease-in-out
|
|
||||||
&:hover {
|
|
||||||
background-color: darken($primary-color, 5%); // blue-700 equivalent
|
|
||||||
}
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5),
|
|
||||||
0 0 0 4px rgba($primary-color, 0.5); // focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
|
|
||||||
}
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form__spinner {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
height: 1.25rem; // h-5
|
|
||||||
width: 1.25rem; // w-5
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Page Container Block (.page-container) --- */
|
|
||||||
.page-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: $bg-color-light;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: "Inter", sans-serif; // Assuming Inter font is used
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__content {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 56rem; // max-w-4xl
|
|
||||||
background-color: $bg-color-white;
|
|
||||||
border-radius: 0.75rem; // rounded-xl
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg
|
|
||||||
padding: 2rem; // p-8
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__title {
|
|
||||||
font-size: 2.25rem; // text-4xl
|
|
||||||
font-weight: 700; // font-bold
|
|
||||||
color: $text-color-dark;
|
|
||||||
margin-bottom: 1.5rem; // mb-6
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__message--logged-in {
|
|
||||||
font-size: 1.25rem; // text-xl
|
|
||||||
color: $success-color;
|
|
||||||
margin-bottom: 1rem; // mb-4
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__text {
|
|
||||||
color: $text-color-medium;
|
|
||||||
margin-bottom: 1.5rem; // mb-6
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__button--logout {
|
|
||||||
padding: 0.75rem 1.5rem; // px-6 py-3
|
|
||||||
background-color: $error-color;
|
|
||||||
color: $bg-color-white;
|
|
||||||
font-weight: 600; // font-semibold
|
|
||||||
border-radius: 0.5rem; // rounded-lg
|
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md
|
|
||||||
transition: background-color 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
"use client"; // This MUST be the very first line of the file
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import "./LoginModal.scss"; // Adjust path based on your actual structure
|
|
||||||
|
|
||||||
// Define the props interface for LoginModal
|
|
||||||
type LoginModalProps = {
|
|
||||||
onLogin: (email: string, password: string) => Promise<boolean>;
|
|
||||||
authMessage: string;
|
|
||||||
clearAuthMessage: () => void;
|
|
||||||
};
|
|
||||||
// LoginModal component
|
|
||||||
export default function LoginModal({
|
|
||||||
onLogin,
|
|
||||||
authMessage,
|
|
||||||
clearAuthMessage,
|
|
||||||
}: LoginModalProps) {
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
console.log("LoginModal rendered"); // Debugging log to check if the component renders
|
|
||||||
|
|
||||||
// Effect to clear authentication messages when email or password inputs change
|
|
||||||
useEffect(() => {
|
|
||||||
// clearAuthMessage();
|
|
||||||
}, [
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
// clearAuthMessage
|
|
||||||
]); // Dependency array ensures effect runs when these change
|
|
||||||
|
|
||||||
// Handler for form submission
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault(); // Prevent default form submission behavior
|
|
||||||
setIsLoading(true); // Set loading state to true
|
|
||||||
await onLogin(email, password); // Call the passed onLogin function (now uncommented)
|
|
||||||
setIsLoading(false); // Set loading state back to false after login attempt
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
// The content of the login modal, without the modal overlay/wrapper
|
|
||||||
<form onSubmit={handleSubmit} className="login-form">
|
|
||||||
{/* Email input field */}
|
|
||||||
<div className="login-form__group">
|
|
||||||
<label htmlFor="email" className="login-form__label">
|
|
||||||
Email Address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
className="login-form__input"
|
|
||||||
placeholder="admin@example.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password input field */}
|
|
||||||
<div className="login-form__group">
|
|
||||||
<label htmlFor="password" className="login-form__label">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
className="login-form__input"
|
|
||||||
placeholder="password123"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="login-form__button"
|
|
||||||
disabled={isLoading} // Disable button while loading
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
// SVG spinner for loading state
|
|
||||||
<svg
|
|
||||||
className="login-form__spinner"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
{/* BEM class name */}
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
"Login" // Button text for normal state
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,199 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
FormControl,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
FormControlLabel,
|
|
||||||
Checkbox,
|
|
||||||
Stack,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
|
||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|
||||||
import AdvancedSearch from "../AdvancedSearch/AdvancedSearch";
|
|
||||||
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
|
|
||||||
import { exportData } from "@/app/utils/exportData";
|
|
||||||
import { IDataTable } from "./types";
|
|
||||||
|
|
||||||
|
|
||||||
interface IDataTableProps<TRow, TColumn> {
|
|
||||||
data: IDataTable<TRow, TColumn>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TWithId = { id: number };
|
|
||||||
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
|
|
||||||
data: IDataTableProps<TRow, TColumn>,
|
|
||||||
) => {
|
|
||||||
const { tableRows, tableColumns, tableSearchLabels } = data.data;
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
|
|
||||||
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
|
|
||||||
const [rows, setRows] = useState<TRow[]>(tableRows);
|
|
||||||
|
|
||||||
const filters = Object.fromEntries(searchParams.entries());
|
|
||||||
|
|
||||||
const handleClickField = (field: string, value: string) => {
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
params.set(field, value);
|
|
||||||
router.push(`?${params.toString()}`);
|
|
||||||
router.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusChange = (id: number, newStatus: string) => {
|
|
||||||
setRows(
|
|
||||||
rows.map((row) => (row.id === id ? { ...row, status: newStatus } : row)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColumnsWithDropdown = (columns: TColumn[]): GridColDef[] => {
|
|
||||||
return columns?.map((col) => {
|
|
||||||
if (col.field === "actions") {
|
|
||||||
return {
|
|
||||||
...col,
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
|
||||||
const row = tableRows.find((r) => r.id === params.id) as {
|
|
||||||
id: number;
|
|
||||||
status?: string;
|
|
||||||
options?: { value: string; label: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = row?.options;
|
|
||||||
if (!options) return params.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
value={params.value ?? row.status}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleStatusChange(params.id as number, e.target.value)
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
|
||||||
"& .MuiSelect-select": { py: 0.5 },
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{options.map((option) => (
|
|
||||||
<MenuItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return col;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
p={2}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
label="Search"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onChange={(e) => console.log(`setSearchQuery(${e.target.value})`)}
|
|
||||||
sx={{ width: 300 }}
|
|
||||||
/>
|
|
||||||
<AdvancedSearch labels={tableSearchLabels} />
|
|
||||||
<SearchFilters filters={filters} />
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<FileUploadIcon />}
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<DataGrid
|
|
||||||
rows={tableRows}
|
|
||||||
columns={getColumnsWithDropdown(tableColumns)}
|
|
||||||
initialState={{
|
|
||||||
pagination: { paginationModel: { pageSize: 50 } },
|
|
||||||
}}
|
|
||||||
pageSizeOptions={[50, 100]}
|
|
||||||
sx={{
|
|
||||||
border: 0,
|
|
||||||
cursor: "pointer",
|
|
||||||
"& .MuiDataGrid-cell": {
|
|
||||||
py: 1,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onCellClick={(params) => {
|
|
||||||
if (params.field !== "actions") {
|
|
||||||
handleClickField(params.field, params.value as string);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Export Dialog */}
|
|
||||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
|
||||||
<DialogTitle>Export Transactions</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
|
||||||
<Select
|
|
||||||
value={fileType}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFileType(e.target.value as "csv" | "xls" | "xlsx")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItem value="csv">CSV</MenuItem>
|
|
||||||
<MenuItem value="xls">XLS</MenuItem>
|
|
||||||
<MenuItem value="xlsx">XLSX</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={onlyCurrentTable}
|
|
||||||
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Only export the results in the current table"
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() =>
|
|
||||||
exportData(
|
|
||||||
tableRows,
|
|
||||||
tableColumns,
|
|
||||||
fileType,
|
|
||||||
onlyCurrentTable,
|
|
||||||
setOpen,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DataTable;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
export interface ISearchLabel {
|
|
||||||
label: string;
|
|
||||||
field: string;
|
|
||||||
type: string;
|
|
||||||
options?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDataTable<TRow, TColumn> {
|
|
||||||
tableRows: TRow[];
|
|
||||||
tableColumns: TColumn[];
|
|
||||||
tableSearchLabels: ISearchLabel[];
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
.date-range-picker {
|
|
||||||
.date-range-picker__date-typo {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Box, Typography, Paper, Popover } from "@mui/material";
|
|
||||||
import { DateRange, Range, DateRangeProps } from "react-date-range";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
import "react-date-range/dist/styles.css";
|
|
||||||
import "react-date-range/dist/theme/default.css";
|
|
||||||
|
|
||||||
import "./DateRangePicker.scss";
|
|
||||||
|
|
||||||
export const DateRangePicker = () => {
|
|
||||||
const [range, setRange] = useState<Range[]>([
|
|
||||||
{
|
|
||||||
startDate: new Date(),
|
|
||||||
endDate: new Date(),
|
|
||||||
key: "selection",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const handleSelect: DateRangeProps["onChange"] = (ranges) => {
|
|
||||||
if (ranges.selection) {
|
|
||||||
setRange([ranges.selection]);
|
|
||||||
if (ranges.selection.endDate !== ranges.selection.startDate) {
|
|
||||||
setAnchorEl(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const id = open ? "date-range-popover" : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className="date-range-picker">
|
|
||||||
<Popover
|
|
||||||
id={id}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={handleClose}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper>
|
|
||||||
<DateRange
|
|
||||||
editableDateInputs={true}
|
|
||||||
onChange={handleSelect}
|
|
||||||
moveRangeOnFirstSelection={false}
|
|
||||||
ranges={range}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
onClick={handleClick}
|
|
||||||
className="date-range-picker__date-typo"
|
|
||||||
>
|
|
||||||
{format(range[0].startDate ?? new Date(), "PPP")} -{" "}
|
|
||||||
{format(range[0].endDate ?? new Date(), "PPP")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
.documentation {
|
|
||||||
.documentation__icon {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import DescriptionIcon from "@mui/icons-material/Description";
|
|
||||||
import { SectionCard } from "../SectionCard/SectionCard";
|
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import "./Documentation.scss";
|
|
||||||
|
|
||||||
export const Documentation = () => {
|
|
||||||
return (
|
|
||||||
<Box className="documentation">
|
|
||||||
<SectionCard
|
|
||||||
title="Documentation"
|
|
||||||
icon={
|
|
||||||
<DescriptionIcon className="documentation__icon" fontSize="small" />
|
|
||||||
}
|
|
||||||
items={[
|
|
||||||
{ title: "Provider Integration Overview" },
|
|
||||||
{ title: "APIs Introduction" },
|
|
||||||
{ title: "Documentation Overview" },
|
|
||||||
{ title: "How-Tos" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
.fetch-report {
|
|
||||||
padding: 23px;
|
|
||||||
margin: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
Typography,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
Button,
|
|
||||||
Stack,
|
|
||||||
Box,
|
|
||||||
Paper,
|
|
||||||
IconButton,
|
|
||||||
} from "@mui/material";
|
|
||||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
|
||||||
|
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
import { DateRangePicker } from "../DateRangePicker/DateRangePicker";
|
|
||||||
|
|
||||||
import "./FetchReport.scss";
|
|
||||||
|
|
||||||
export const FetchReport = () => {
|
|
||||||
const [state, setState] = useState("");
|
|
||||||
const [psp, setPsp] = useState("");
|
|
||||||
const [reportType, setReportType] = useState("");
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
// Download logic goes here
|
|
||||||
alert("Report downloaded");
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDownloadEnabled = state && psp && reportType;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper className="fetch-report" elevation={3}>
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}>
|
|
||||||
<Typography variant="h6" fontWeight="bold">
|
|
||||||
Fetch Report
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<CalendarTodayIcon fontSize="small" />
|
|
||||||
<Typography variant="body2">
|
|
||||||
<DateRangePicker />
|
|
||||||
</Typography>
|
|
||||||
<IconButton size="small">
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel>Select state (defaults to All)</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={state}
|
|
||||||
onChange={(e) => setState(e.target.value)}
|
|
||||||
label="Select state (defaults to All)"
|
|
||||||
>
|
|
||||||
<MenuItem value="successful">Successful</MenuItem>
|
|
||||||
<MenuItem value="failed">Failed</MenuItem>
|
|
||||||
<MenuItem value="canceled">Canceled</MenuItem>
|
|
||||||
|
|
||||||
{/* Add more states */}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel>Select PSPs (defaults to All)</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={psp}
|
|
||||||
onChange={(e) => setPsp(e.target.value)}
|
|
||||||
label="Select PSPs (defaults to All)"
|
|
||||||
>
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem value="a1">A1</MenuItem>
|
|
||||||
<MenuItem value="ahub">AHUB</MenuItem>
|
|
||||||
<MenuItem value="aibms">AIBMS</MenuItem>
|
|
||||||
|
|
||||||
{/* Add more PSPs */}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel>Select report type</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={reportType}
|
|
||||||
onChange={(e) => setReportType(e.target.value)}
|
|
||||||
label="Select report type"
|
|
||||||
>
|
|
||||||
<MenuItem value="allTransactionsReport">
|
|
||||||
All Transactions Report
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="depositReport">Deposit Report</MenuItem>
|
|
||||||
<MenuItem value="widthdrawReport">WithDraw Report</MenuItem>
|
|
||||||
{/* Add more types */}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Box textAlign="center" mt={2}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleDownload}
|
|
||||||
disabled={!isDownloadEnabled}
|
|
||||||
sx={{ minWidth: 200 }}
|
|
||||||
>
|
|
||||||
Download Report
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
.general-health-card {
|
|
||||||
.general-health-card__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.general-health-card__right-side {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.general-health-card__stat-items {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { Box, Card, CardContent, Typography, IconButton } from "@mui/material";
|
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
|
||||||
|
|
||||||
import { DateRangePicker } from "../DateRangePicker/DateRangePicker";
|
|
||||||
import { StatItem } from "./components/StatItem";
|
|
||||||
import "./GeneralHealthCard.scss";
|
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{ label: "TOTAL", value: 5, change: "-84.85%" },
|
|
||||||
{ label: "SUCCESSFUL", value: 10, change: "100%" },
|
|
||||||
{ label: "ACCEPTANCE RATE", value: "0%", change: "-100%" },
|
|
||||||
{ label: "AMOUNT", value: "€0.00", change: "-100%" },
|
|
||||||
{ label: "ATV", value: "€0.00", change: "-100%" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const GeneralHealthCard = () => {
|
|
||||||
return (
|
|
||||||
<Card className="general-health-card">
|
|
||||||
<CardContent>
|
|
||||||
<Box className="general-health-card__header">
|
|
||||||
<Typography variant="h5" fontWeight="bold">
|
|
||||||
General Health
|
|
||||||
</Typography>
|
|
||||||
<Box className="general-health-card__right-side">
|
|
||||||
<CalendarTodayIcon fontSize="small" />
|
|
||||||
<Typography variant="body2">
|
|
||||||
<DateRangePicker />
|
|
||||||
</Typography>
|
|
||||||
<IconButton size="small">
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box className="general-health-card__stat-items">
|
|
||||||
{stats.map((item, i) => (
|
|
||||||
<StatItem key={item.label + i} {...item} />
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
.static-item {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
|
|
||||||
.static-item__percentage {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { Box, Typography } from "@mui/material";
|
|
||||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
|
||||||
import "./StatItem.scss";
|
|
||||||
|
|
||||||
export const StatItem = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
change,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
change: string;
|
|
||||||
}) => (
|
|
||||||
<Box className="static-item">
|
|
||||||
<Typography variant="body2" fontWeight="bold" color="text.secondary">
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6" fontWeight="bold" mt={0.5}>
|
|
||||||
{value}
|
|
||||||
</Typography>
|
|
||||||
<Box className="static-item__percentage">
|
|
||||||
<ArrowDropDownIcon fontSize="small" />
|
|
||||||
{/* <ArrowDropUp fontSize='small' /> */}
|
|
||||||
<Typography variant="caption">{change}</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
export interface IUser {
|
|
||||||
merchantId: number;
|
|
||||||
name?: string;
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
jobTitle: string;
|
|
||||||
enabled: boolean;
|
|
||||||
authorities: string[];
|
|
||||||
allowedMerchantIds: number[];
|
|
||||||
created: string;
|
|
||||||
disabledBy: string | null;
|
|
||||||
disabledDate: string | null;
|
|
||||||
disabledReason: string | null;
|
|
||||||
incidentNotes: boolean;
|
|
||||||
lastLogin: string;
|
|
||||||
lastMandatoryUpdated: string;
|
|
||||||
marketingNewsletter: boolean;
|
|
||||||
releaseNotes: boolean;
|
|
||||||
requiredActions: string[];
|
|
||||||
twoFactorCondition: string;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
twoFactorCredentials: any[]; // Assuming this is an array that could contain any type of data
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Card, CardContent, Typography, Stack } from "@mui/material";
|
|
||||||
import { IUser } from "./interfaces";
|
|
||||||
import UserTopBar from "@/app/features/UserRoles/AddUser/AddUser";
|
|
||||||
import EditUser from "@/app/features/UserRoles/EditUser/EditUser";
|
|
||||||
import Modal from "@/app/components/Modal/Modal";
|
|
||||||
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
|
||||||
|
|
||||||
interface UsersProps {
|
|
||||||
users: IUser[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Users: React.FC<UsersProps> = ({ users }) => {
|
|
||||||
const [showAddUser, setShowAddUser] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<UserTopBar onAddUser={() => setShowAddUser(true)} />
|
|
||||||
{users.map((user: IUser) => (
|
|
||||||
<Card key={user.id} sx={{ mb: 2 }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6">{user.username}</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Merchant ID: {user.merchantId}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* You can render more UI here for additional properties */}
|
|
||||||
<Stack direction="row" spacing={1} mt={1}>
|
|
||||||
<UserRoleCard
|
|
||||||
username={user.lastName}
|
|
||||||
name={user.name || ""}
|
|
||||||
email={user.email}
|
|
||||||
isAdmin={true}
|
|
||||||
lastLogin="small"
|
|
||||||
roles={user.authorities}
|
|
||||||
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
|
||||||
/>
|
|
||||||
{/* Add more chips or UI elements for other data */}
|
|
||||||
</Stack>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={showAddUser}
|
|
||||||
onClose={() => setShowAddUser(false)}
|
|
||||||
title="Add User"
|
|
||||||
>
|
|
||||||
<EditUser />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Users;
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
TextField,
|
|
||||||
IconButton,
|
|
||||||
InputAdornment,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Checkbox,
|
|
||||||
Paper,
|
|
||||||
MenuItem,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
FormControl,
|
|
||||||
SelectChangeEvent
|
|
||||||
} from '@mui/material';
|
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
{
|
|
||||||
merchantId: '100987998',
|
|
||||||
txId: '1049078821',
|
|
||||||
userId: 17,
|
|
||||||
userEmail: 'dhkheni1@yopmail.com',
|
|
||||||
kycStatus: 'N/A',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
merchantId: '100987998',
|
|
||||||
txId: '1049078821',
|
|
||||||
userId: 18,
|
|
||||||
userEmail: 'dhkheni1@yopmail.com',
|
|
||||||
kycStatus: 'N/A',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
merchantId: '100987998',
|
|
||||||
txId: '1049078821',
|
|
||||||
userId: 19,
|
|
||||||
userEmail: 'dhkheni1@yopmail.com',
|
|
||||||
kycStatus: 'N/A',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Approve = () => {
|
|
||||||
const [age, setAge] = useState('');
|
|
||||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleCheckboxChange = (userId: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const isChecked = event.target.checked;
|
|
||||||
setSelectedRows((prevSelected: number[]) =>
|
|
||||||
isChecked
|
|
||||||
? [...prevSelected, userId]
|
|
||||||
: prevSelected.filter((id) => id !== userId)
|
|
||||||
);
|
|
||||||
console.log('Selected IDs:', isChecked
|
|
||||||
? [...selectedRows, userId]
|
|
||||||
: selectedRows.filter((id) => id !== userId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeAge = (event: SelectChangeEvent) => {
|
|
||||||
setAge(event.target.value as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box p={2}>
|
|
||||||
<Box mb={2} display="flex" justifyContent="space-between" alignItems="center">
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
placeholder="Filter by tags or search by keyword"
|
|
||||||
size="small"
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton>
|
|
||||||
<SearchIcon />
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box sx={{ width: '100px' }}>
|
|
||||||
{/* <IconButton onClick={handleMenuOpen}> */}
|
|
||||||
{/* <MoreVertIcon /> */}
|
|
||||||
{/* </IconButton> */}
|
|
||||||
{/* <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> */}
|
|
||||||
{/* <MenuItem onClick={handleMenuClose}>Action 1</MenuItem> */}
|
|
||||||
{/* <MenuItem onClick={handleMenuClose}>Action 2</MenuItem> */}
|
|
||||||
{/* </Menu> */}
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="demo-simple-select-label">Action</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="demo-simple-select-label"
|
|
||||||
id="demo-simple-select"
|
|
||||||
value={age}
|
|
||||||
label="Age"
|
|
||||||
onChange={handleChangeAge}
|
|
||||||
>
|
|
||||||
<MenuItem value={10}>Ten</MenuItem>
|
|
||||||
<MenuItem value={20}>Twenty</MenuItem>
|
|
||||||
<MenuItem value={30}>Thirty</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell padding="checkbox"><Checkbox /></TableCell>
|
|
||||||
<TableCell>Merchant-id</TableCell>
|
|
||||||
<TableCell>Tx-id</TableCell>
|
|
||||||
<TableCell>User</TableCell>
|
|
||||||
<TableCell>User email</TableCell>
|
|
||||||
<TableCell>KYC Status</TableCell>
|
|
||||||
<TableCell>KYC PSP</TableCell>
|
|
||||||
<TableCell>KYC PSP status</TableCell>
|
|
||||||
<TableCell>KYC ID status</TableCell>
|
|
||||||
<TableCell>KYC address status</TableCell>
|
|
||||||
<TableCell>KYC liveness status</TableCell>
|
|
||||||
<TableCell>KYC age status</TableCell>
|
|
||||||
<TableCell>KYC peps and sanctions</TableCell>
|
|
||||||
<TableCell>Suspected</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{rows.map((row, idx) => (
|
|
||||||
<TableRow key={idx}>
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox checked={selectedRows.includes(row.userId)}
|
|
||||||
onChange={handleCheckboxChange(row.userId)} /></TableCell>
|
|
||||||
<TableCell>{row.merchantId}</TableCell>
|
|
||||||
<TableCell>{row.txId}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<a href={`/user/${row.userId}`} target="_blank" rel="noopener noreferrer">
|
|
||||||
{row.userId}
|
|
||||||
</a>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{row.userEmail}</TableCell>
|
|
||||||
<TableCell>{row.kycStatus}</TableCell>
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import { GeneralHealthCard } from "../../GeneralHealthCard/GeneralHealthCard";
|
|
||||||
import { TransactionsWaitingApproval } from "../../TransactionsWaitingApproval/TransactionsWaitingApproval";
|
|
||||||
import { FetchReport } from "../../FetchReports/FetchReports";
|
|
||||||
import { Documentation } from "../../Documentation/Documentation";
|
|
||||||
import { AccountIQ } from "../../AccountIQ/AccountIQ";
|
|
||||||
import { WhatsNew } from "../../WhatsNew/WhatsNew";
|
|
||||||
import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOverview";
|
|
||||||
|
|
||||||
export const DashboardHomePage = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Conditional rendering of the Generic Modal, passing LoginModal as children */}
|
|
||||||
<Box sx={{ p: 2 }}>
|
|
||||||
<GeneralHealthCard />
|
|
||||||
</Box>
|
|
||||||
<TransactionsOverView />
|
|
||||||
<FetchReport />
|
|
||||||
<TransactionsWaitingApproval />
|
|
||||||
<Documentation />
|
|
||||||
<AccountIQ />
|
|
||||||
<WhatsNew />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
.pie-charts {
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
|
||||||
import "./PieCharts.scss";
|
|
||||||
const data = [
|
|
||||||
{ name: "Group A", value: 100 },
|
|
||||||
{ name: "Group B", value: 200 },
|
|
||||||
{ name: "Group C", value: 400 },
|
|
||||||
{ name: "Group D", value: 300 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const COLORS = ["#4caf50", "#ff9800", "#f44336", "#9e9e9e"];
|
|
||||||
|
|
||||||
const RADIAN = Math.PI / 180;
|
|
||||||
const renderCustomizedLabel = ({
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
midAngle,
|
|
||||||
innerRadius,
|
|
||||||
outerRadius,
|
|
||||||
percent,
|
|
||||||
// index
|
|
||||||
}: any) => {
|
|
||||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
|
||||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
|
||||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
fill="white"
|
|
||||||
textAnchor={x > cx ? "start" : "end"}
|
|
||||||
dominantBaseline="central"
|
|
||||||
>
|
|
||||||
{`${(percent * 100).toFixed(0)}%`}
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const PieCharts = () => {
|
|
||||||
return (
|
|
||||||
<Box className="pie-charts">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={data}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
label={renderCustomizedLabel}
|
|
||||||
outerRadius="80%" // Percentage-based radius
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<Cell
|
|
||||||
key={`cell-${index}`}
|
|
||||||
fill={COLORS[index % COLORS.length]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
.section-card {
|
|
||||||
padding: 16px;
|
|
||||||
margin: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.section-card__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
.section-card__title {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.section-card__icon-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import {
|
|
||||||
CardContent,
|
|
||||||
Typography,
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Paper,
|
|
||||||
Box,
|
|
||||||
IconButton,
|
|
||||||
} from "@mui/material";
|
|
||||||
|
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
import { ISectionCardProps } from "./types";
|
|
||||||
|
|
||||||
import "./SectionCard.scss";
|
|
||||||
|
|
||||||
export const SectionCard = ({ title, icon, items }: ISectionCardProps) => (
|
|
||||||
<Paper className="section-card" elevation={3}>
|
|
||||||
<CardContent>
|
|
||||||
<Box className="section-card__header">
|
|
||||||
<Box className="section-card__title">
|
|
||||||
{icon}
|
|
||||||
<Typography variant="h6" fontWeight="bold">
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box className="section-card__icon-wrapper">
|
|
||||||
<IconButton size="small">
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Divider />
|
|
||||||
<List dense disablePadding>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<ListItem key={item.title + index} disableGutters>
|
|
||||||
<ListItemText primary={item.title} secondary={item.date} />
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</CardContent>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
export interface ISectionItem {
|
|
||||||
title: string;
|
|
||||||
date?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISectionCardProps {
|
|
||||||
title: string;
|
|
||||||
icon: ReactNode;
|
|
||||||
items: ISectionItem[];
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
.transaction-overview {
|
|
||||||
padding: 23px;
|
|
||||||
margin: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.transaction-overview__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-overview__chart-table {
|
|
||||||
padding: 16px;
|
|
||||||
margin: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 32px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.transactions-overview-table {
|
|
||||||
.transactions-overview-table__state-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 73px;
|
|
||||||
|
|
||||||
.transactions-overview-table__state {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
} from "@mui/material";
|
|
||||||
|
|
||||||
import "./TransactionsOverViewTable.scss";
|
|
||||||
|
|
||||||
const data1 = [
|
|
||||||
{ state: "Success", count: 120, percentage: "60%", color: "green" },
|
|
||||||
{ state: "Pending", count: 50, percentage: "25%", color: "orange" },
|
|
||||||
{ state: "Failed", count: 20, percentage: "10%", color: "red" },
|
|
||||||
{ state: "Other", count: 10, percentage: "5%", color: "gray" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TransactionsOverViewTable = () => {
|
|
||||||
return (
|
|
||||||
<TableContainer className="transactions-overview-table" component={Paper}>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell align="center">State</TableCell>
|
|
||||||
<TableCell align="center">Count</TableCell>
|
|
||||||
<TableCell align="center">Percentage</TableCell>
|
|
||||||
<TableCell align="center">Action</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{data1.map((row, i) => (
|
|
||||||
<TableRow key={row.state + i}>
|
|
||||||
<TableCell align="center">
|
|
||||||
<Box className="transactions-overview-table__state-wrapper">
|
|
||||||
<Box
|
|
||||||
className="transactions-overview-table__state"
|
|
||||||
sx={{
|
|
||||||
bgcolor: row.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{row.state}
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{row.count}</TableCell>
|
|
||||||
<TableCell align="center">{row.percentage}</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<Button variant="outlined" size="small">
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Box, Button, IconButton, Paper, Typography } from "@mui/material";
|
|
||||||
import { PieCharts } from "../PieCharts/PieCharts";
|
|
||||||
|
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
import { TransactionsOverViewTable } from "./components/TransactionsOverViewTable";
|
|
||||||
|
|
||||||
import "./TransactionsOverView.scss";
|
|
||||||
|
|
||||||
export const TransactionsOverView = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<Paper className="transaction-overview" elevation={3}>
|
|
||||||
{/* Title and All Transactions Button */}
|
|
||||||
<Box className="transaction-overview__header">
|
|
||||||
<Typography variant="h5" fontWeight="bold">
|
|
||||||
Transactions Overview (Last 24h)
|
|
||||||
</Typography>
|
|
||||||
<Box>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => router.push("dashboard/transactions")}
|
|
||||||
>
|
|
||||||
All Transactions
|
|
||||||
</Button>
|
|
||||||
<IconButton size="small">
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Chart and Table */}
|
|
||||||
<Box className="transaction-overview__chart-table">
|
|
||||||
<PieCharts />
|
|
||||||
<TransactionsOverViewTable />
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
.transactions-waiting-approval {
|
|
||||||
padding: 16px;
|
|
||||||
margin: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
Paper,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
||||||
import CancelIcon from "@mui/icons-material/Cancel";
|
|
||||||
|
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
||||||
|
|
||||||
import "./TransactionsWaitingApproval.scss";
|
|
||||||
const transactions = [
|
|
||||||
{
|
|
||||||
id: "1049078821",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078822",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078823",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078824",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078821",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078822",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078823",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078824",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078821",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078822",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078823",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1049078824",
|
|
||||||
user: "17",
|
|
||||||
created: "2025-06-17 16:45",
|
|
||||||
type: "BestPayWithdrawal",
|
|
||||||
amount: "-787.49 TRY",
|
|
||||||
psp: "BestPay",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TransactionsWaitingApproval = () => {
|
|
||||||
return (
|
|
||||||
<Paper elevation={3} className="transactions-waiting-approval">
|
|
||||||
<Box sx={{ p: 3 }}>
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}>
|
|
||||||
<Typography variant="h5" fontWeight="bold">
|
|
||||||
Transactions Waiting for Approval
|
|
||||||
</Typography>
|
|
||||||
<Box>
|
|
||||||
<Button variant="outlined">All Pending Withdrawals</Button>
|
|
||||||
<IconButton size="small">
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>{" "}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TableContainer
|
|
||||||
component={Paper}
|
|
||||||
sx={{
|
|
||||||
maxHeight: 400, // Set desired height
|
|
||||||
overflow: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<strong>ID</strong>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<strong>User</strong>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<strong>Created</strong>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<strong>Type</strong>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<strong>Amount</strong>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<strong>PSP</strong>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<strong>Action</strong>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{transactions.map((tx, i) => (
|
|
||||||
<TableRow key={tx.id + i}>
|
|
||||||
<TableCell>{tx.id}</TableCell>
|
|
||||||
<TableCell>{tx.user}</TableCell>
|
|
||||||
<TableCell>{tx.created}</TableCell>
|
|
||||||
<TableCell>{tx.type}</TableCell>
|
|
||||||
<TableCell>{tx.amount}</TableCell>
|
|
||||||
<TableCell>{tx.psp}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<IconButton color="success">
|
|
||||||
<CheckCircleIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton color="error">
|
|
||||||
<CancelIcon />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
.add-user {
|
|
||||||
position: sticky;
|
|
||||||
top: 40px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button--primary {
|
|
||||||
background: #1976d2;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button--secondary {
|
|
||||||
background: #e0e0e0;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { Add } from "@mui/icons-material";
|
|
||||||
import React from "react";
|
|
||||||
import "./AddUser.scss";
|
|
||||||
|
|
||||||
const AddUser: React.FC<{
|
|
||||||
onAddUser?: () => void;
|
|
||||||
onExport?: () => void;
|
|
||||||
}> = ({ onAddUser, onExport }) => {
|
|
||||||
return (
|
|
||||||
<div className="add-user">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onAddUser}
|
|
||||||
className="add-user__button add-user__button--primary"
|
|
||||||
>
|
|
||||||
<Add />
|
|
||||||
Add User
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onExport}
|
|
||||||
className="add-user__button add-user__button--secondary"
|
|
||||||
disabled
|
|
||||||
title="Export to Excel (coming soon)"
|
|
||||||
>
|
|
||||||
Export to Excel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddUser;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
.edit-user {
|
|
||||||
margin-top: 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
flex: 1 1 20%;
|
|
||||||
min-width: 150px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.3s ease;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #0070f3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__button-container {
|
|
||||||
flex-basis: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
button {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 10px 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 100px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:first-child {
|
|
||||||
color: var(--button-primary);
|
|
||||||
border-color: var(--button-primary);
|
|
||||||
}
|
|
||||||
button:last-child {
|
|
||||||
color: var(--button-secondary);
|
|
||||||
border-color: var(--button-secondary);
|
|
||||||
margin-left: 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { IEditUserForm, EditUserField } from "../User.interfaces";
|
|
||||||
import { createRole } from "@/services/roles.services";
|
|
||||||
import "./EditUser.scss";
|
|
||||||
|
|
||||||
const EditUser = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const [form, setForm] = React.useState<IEditUserForm>({
|
|
||||||
firstName: "",
|
|
||||||
lastName: "",
|
|
||||||
email: "",
|
|
||||||
role: "",
|
|
||||||
phone: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const name = e.target.name as EditUserField;
|
|
||||||
const value = e.target.value;
|
|
||||||
if (name === "phone") {
|
|
||||||
const filtered = value.replace(/[^0-9+\-\s()]/g, "");
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
phone: filtered,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetForm = () => {
|
|
||||||
setForm({
|
|
||||||
firstName: "",
|
|
||||||
lastName: "",
|
|
||||||
email: "",
|
|
||||||
role: "",
|
|
||||||
phone: "",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createRole(form);
|
|
||||||
router.refresh(); // <- refreshes the page (SSR re-runs)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (err: any) {
|
|
||||||
console.log(err.message || "Error creating role");
|
|
||||||
// setError(err.message || "Error creating role");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="edit-user" onSubmit={handleSubmit}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="First Name"
|
|
||||||
name="firstName"
|
|
||||||
value={form.firstName}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Last Name"
|
|
||||||
name="lastName"
|
|
||||||
value={form.lastName}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
name="email"
|
|
||||||
value={form.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Role"
|
|
||||||
name="role"
|
|
||||||
value={form.role}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
placeholder="Phone"
|
|
||||||
name="phone"
|
|
||||||
value={form.phone}
|
|
||||||
maxLength={15}
|
|
||||||
pattern="[0-9+\-\s()]*"
|
|
||||||
onChange={handleChange}
|
|
||||||
inputMode="tel"
|
|
||||||
autoComplete="tel"
|
|
||||||
/>
|
|
||||||
<div className="edit-user__button-container">
|
|
||||||
<button type="submit">Save</button>
|
|
||||||
<button type="button" onClick={handleResetForm}>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditUser;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
export interface IEditUserForm {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
phone: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EditUserField =
|
|
||||||
| "firstName"
|
|
||||||
| "lastName"
|
|
||||||
| "email"
|
|
||||||
| "role"
|
|
||||||
| "phone";
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.user-card {
|
|
||||||
&__edit {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__edit-transition {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease-in-out;
|
|
||||||
|
|
||||||
&--open {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Avatar,
|
|
||||||
Typography,
|
|
||||||
Chip,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Stack,
|
|
||||||
Box,
|
|
||||||
} from "@mui/material";
|
|
||||||
import {
|
|
||||||
Edit,
|
|
||||||
Delete,
|
|
||||||
Visibility,
|
|
||||||
VpnKey,
|
|
||||||
InfoOutlined,
|
|
||||||
AdminPanelSettings,
|
|
||||||
History,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import EditUser from "./EditUser/EditUser";
|
|
||||||
import "./User.scss";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
lastLogin: string;
|
|
||||||
merchants: string[];
|
|
||||||
roles: string[];
|
|
||||||
extraRolesCount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserRoleCard({
|
|
||||||
username,
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
isAdmin,
|
|
||||||
roles,
|
|
||||||
extraRolesCount,
|
|
||||||
}: Props) {
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
|
|
||||||
const handleEditClick = () => {
|
|
||||||
setIsEditing(!isEditing);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card sx={{ mb: 2, minWidth: "100%" }}>
|
|
||||||
<CardContent>
|
|
||||||
{/* Header */}
|
|
||||||
<Stack direction="row" alignItems="center" spacing={2}>
|
|
||||||
<Avatar>{username?.slice(0, 2).toUpperCase()}</Avatar>
|
|
||||||
<Box flexGrow={1}>
|
|
||||||
<Typography fontWeight="bold">{username}</Typography>
|
|
||||||
<Typography variant="body2">{name}</Typography>
|
|
||||||
<Typography variant="caption">{email}</Typography>
|
|
||||||
</Box>
|
|
||||||
{isAdmin && (
|
|
||||||
<Chip icon={<AdminPanelSettings />} label="Admin" size="small" />
|
|
||||||
)}
|
|
||||||
<IconButton>
|
|
||||||
<History />
|
|
||||||
</IconButton>
|
|
||||||
<Tooltip title="Edit">
|
|
||||||
<IconButton onClick={handleEditClick}>
|
|
||||||
<Edit />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="View">
|
|
||||||
<IconButton>
|
|
||||||
<Visibility />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Reset Password">
|
|
||||||
<IconButton>
|
|
||||||
<VpnKey />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delete">
|
|
||||||
<IconButton>
|
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{/* Merchants + Roles */}
|
|
||||||
<Box mt={2}>
|
|
||||||
<Typography fontWeight="bold">Merchants</Typography>
|
|
||||||
{/* <Stack direction="row" spacing={1} mt={1}>
|
|
||||||
{merchants.map((m) => (
|
|
||||||
<Chip key={m} label={m} size="small" />
|
|
||||||
))}
|
|
||||||
</Stack> */}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box mt={2}>
|
|
||||||
<Typography fontWeight="bold">
|
|
||||||
Roles
|
|
||||||
<Tooltip title="Roles assigned to this user">
|
|
||||||
<InfoOutlined fontSize="small" />
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" spacing={1} mt={1} flexWrap="wrap">
|
|
||||||
<Stack direction="row" spacing={1}>
|
|
||||||
{roles.map((role) => (
|
|
||||||
<Chip key={role} label={role} size="small" />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
{extraRolesCount && <Chip label={`+${extraRolesCount}`} />}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
<div
|
|
||||||
className={`user-card__edit-transition${
|
|
||||||
isEditing ? " user-card__edit-transition--open" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isEditing && <EditUser />}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
.whats-new {
|
|
||||||
.whats-new__wifi-icon {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { Box } from "@mui/material";
|
|
||||||
import { SectionCard } from "../SectionCard/SectionCard";
|
|
||||||
import WifiIcon from "@mui/icons-material/Wifi";
|
|
||||||
import "./WhatsNew.scss";
|
|
||||||
|
|
||||||
export const WhatsNew = () => {
|
|
||||||
return (
|
|
||||||
<Box className="whats-new">
|
|
||||||
<SectionCard
|
|
||||||
title="What’s New"
|
|
||||||
icon={<WifiIcon className="whats-new__wifi-icon" fontSize="small" />}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
title: "Sneak Peek – Discover the New Rules Hub Feature",
|
|
||||||
date: "13 May 2025",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title:
|
|
||||||
"New security measures for anonymizing sensitive configuration values, effective December 2nd",
|
|
||||||
date: "31 Oct 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Introducing Our New Transactions and Rule Views",
|
|
||||||
date: "23 Oct 2024",
|
|
||||||
},
|
|
||||||
{ title: "Introducing Our New Status Page", date: "09 Sept 2024" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
.header {
|
|
||||||
.header__toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.header__left-group {
|
|
||||||
width: 100px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px; // optional spacing between menu and dropdown
|
|
||||||
}
|
|
||||||
|
|
||||||
.header__right-group {
|
|
||||||
margin-left: auto; // pushes it to the far right
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { AppBar, Toolbar, IconButton } from "@mui/material";
|
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
|
||||||
import Dropdown from "./dropDown/DropDown";
|
|
||||||
import AccountMenu from "./accountMenu/AccountMenu";
|
|
||||||
import "./Header.scss";
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
// const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
|
|
||||||
// // Handle menu open
|
|
||||||
// const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
// setAnchorEl(event.currentTarget);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // Handle menu close
|
|
||||||
// const handleMenuClose = () => {
|
|
||||||
// setAnchorEl(null);
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleChange = () => {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppBar
|
|
||||||
className="header"
|
|
||||||
position="sticky"
|
|
||||||
color="transparent"
|
|
||||||
elevation={0}
|
|
||||||
sx={{ borderBottom: "1px solid #22242626" }}
|
|
||||||
>
|
|
||||||
<Toolbar className="header__toolbar">
|
|
||||||
<div className="header__left-group">
|
|
||||||
<IconButton edge="start" color="inherit" aria-label="menu">
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Dropdown onChange={handleChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="header__right-group">
|
|
||||||
<AccountMenu />
|
|
||||||
</div>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
IconButton,
|
|
||||||
ListItemIcon,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
} from "@mui/material";
|
|
||||||
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
|
||||||
import LogoutIcon from "@mui/icons-material/Logout";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { selectIsLoggedIn, selectStatus } from "@/app/redux/auth/selectors";
|
|
||||||
import { logout } from "@/app/redux/auth/authSlice";
|
|
||||||
import { AppDispatch, RootState } from "@/app/redux/types";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
export default function AccountMenu() {
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Select relevant state from your auth slice
|
|
||||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
|
||||||
const authStatus = useSelector(selectStatus);
|
|
||||||
|
|
||||||
// Determine if we're currently in the process of logging out
|
|
||||||
const isLoggingOut = authStatus === "loading";
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
// Dispatch the logout thunk
|
|
||||||
const resultAction = await dispatch(logout());
|
|
||||||
|
|
||||||
// Check if logout was successful based on the action result
|
|
||||||
if (logout.fulfilled.match(resultAction)) {
|
|
||||||
console.log("Logout successful, redirecting...");
|
|
||||||
router.push("/login"); // Redirect to your login page
|
|
||||||
} else {
|
|
||||||
// Handle logout failure (e.g., show an error message)
|
|
||||||
console.error("Logout failed:", resultAction.payload);
|
|
||||||
// You might want to display a toast or alert here
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("[isLoggedin]", isLoggedIn);
|
|
||||||
|
|
||||||
// Only show the logout button if the user is logged in
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton onClick={handleClick} color="inherit">
|
|
||||||
<AccountCircleIcon />
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={open}
|
|
||||||
onClose={handleClose}
|
|
||||||
onClick={handleClose}
|
|
||||||
transformOrigin={{ horizontal: "right", vertical: "top" }}
|
|
||||||
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
|
|
||||||
>
|
|
||||||
<MenuItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<AccountCircleIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<Typography variant="inherit">Account</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<SettingsIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<Typography variant="inherit">Settings</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<LogoutIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={handleLogout}
|
|
||||||
disabled={isLoggingOut}
|
|
||||||
startIcon={
|
|
||||||
isLoggingOut ? (
|
|
||||||
<CircularProgress size={20} color="inherit" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isLoggingOut ? "Logging Out..." : "Logout"}
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
.sidebar-dropdown__container {
|
|
||||||
.page-link__container {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
.page-link__text {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
SelectChangeEvent,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { PAGE_LINKS } from "@/app/features/dashboard/sidebar/SidebarLink.constants";
|
|
||||||
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
|
|
||||||
import PageLinks from "../../../../components/PageLinks/PageLinks";
|
|
||||||
import "./DropDown.scss";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange?: (event: SelectChangeEvent<string>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SidebarDropdown({ onChange }: Props) {
|
|
||||||
const [value, setValue] = React.useState("");
|
|
||||||
|
|
||||||
const handleChange = (event: SelectChangeEvent<string>) => {
|
|
||||||
setValue(event.target.value);
|
|
||||||
onChange?.(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl fullWidth variant="outlined" sx={{ minWidth: 200 }}>
|
|
||||||
<InputLabel id="sidebar-dropdown-label">Navigate To</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="sidebar-dropdown-label"
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
label="Navigate To"
|
|
||||||
MenuProps={{
|
|
||||||
PaperProps: {
|
|
||||||
style: {
|
|
||||||
maxHeight: 200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<em className="em">Select a page</em>
|
|
||||||
<MenuItem value="" disabled></MenuItem>
|
|
||||||
<div className="sidebar-dropdown__container">
|
|
||||||
{PAGE_LINKS.map((link: ISidebarLink) => (
|
|
||||||
<PageLinks key={link.path} title={link.title} path={link.path} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { styled } from "@mui/system";
|
|
||||||
|
|
||||||
export const LayoutWrapper = styled("div")({
|
|
||||||
display: "flex",
|
|
||||||
width: "100%",
|
|
||||||
height: "100vh",
|
|
||||||
});
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { styled } from '@mui/system';
|
|
||||||
|
|
||||||
export const MainContent = styled('div')(({ theme }) => ({
|
|
||||||
marginLeft: '240px',
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
minHeight: '100vh',
|
|
||||||
width: 'calc(100% - 240px)',
|
|
||||||
}));
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { PAGE_LINKS } from "@/app/features/dashboard/sidebar/SidebarLink.constants";
|
|
||||||
import PageLinks from "../../../components/PageLinks/PageLinks";
|
|
||||||
import "./sideBar.scss";
|
|
||||||
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|
||||||
|
|
||||||
const SideBar = () => {
|
|
||||||
const [openMenus, setOpenMenus] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const toggleMenu = (title: string) => {
|
|
||||||
setOpenMenus((prev) => ({ ...prev, [title]: !prev[title] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="sidebar">
|
|
||||||
<div className="sidebar__header">
|
|
||||||
<span>
|
|
||||||
Betrise cashir
|
|
||||||
<DashboardIcon fontSize="small" className="sidebar__icon-spacing" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{PAGE_LINKS.map((link) =>
|
|
||||||
link.children ? (
|
|
||||||
<div key={link.title}>
|
|
||||||
<button
|
|
||||||
onClick={() => toggleMenu(link.title)}
|
|
||||||
className="sidebar__dropdown-button"
|
|
||||||
>
|
|
||||||
{link.icon && <link.icon />}
|
|
||||||
<span className="sidebar__text">{link.title}</span>
|
|
||||||
<span className="sidebar__arrow">
|
|
||||||
{!openMenus[link.title] ? (
|
|
||||||
<KeyboardArrowRightIcon />
|
|
||||||
) : (
|
|
||||||
<KeyboardArrowDownIcon />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{openMenus[link.title] && (
|
|
||||||
<div className="sidebar__submenu">
|
|
||||||
{link.children.map((child) => (
|
|
||||||
<PageLinks
|
|
||||||
key={child.path}
|
|
||||||
title={child.title}
|
|
||||||
path={child.path}
|
|
||||||
icon={child.icon}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PageLinks
|
|
||||||
key={link.path}
|
|
||||||
title={link.title}
|
|
||||||
path={link.path}
|
|
||||||
icon={link.icon}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SideBar;
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import HomeIcon from "@mui/icons-material/Home";
|
|
||||||
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
|
|
||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
|
||||||
import VerifiedUserIcon from "@mui/icons-material/VerifiedUser";
|
|
||||||
import PeopleIcon from "@mui/icons-material/People";
|
|
||||||
import GavelIcon from "@mui/icons-material/Gavel";
|
|
||||||
import HubIcon from "@mui/icons-material/Hub";
|
|
||||||
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
|
|
||||||
import InsightsIcon from "@mui/icons-material/Insights";
|
|
||||||
import ListAltIcon from "@mui/icons-material/ListAlt";
|
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
|
||||||
|
|
||||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|
||||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
|
||||||
import FactCheckIcon from "@mui/icons-material/FactCheck";
|
|
||||||
|
|
||||||
|
|
||||||
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
|
|
||||||
|
|
||||||
export const PAGE_LINKS: ISidebarLink[] = [
|
|
||||||
{ title: "Home", path: "/dashboard", icon: HomeIcon },
|
|
||||||
|
|
||||||
{
|
|
||||||
title: "Transaction",
|
|
||||||
path: "/dashboard/transactions",
|
|
||||||
icon: AccountBalanceWalletIcon, children: [
|
|
||||||
{
|
|
||||||
title: "Deposits",
|
|
||||||
path: "/dashboard/transactions/deposits",
|
|
||||||
icon: ArrowDownwardIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Withdrawals",
|
|
||||||
path: "/dashboard/transactions/withdrawals",
|
|
||||||
icon: ArrowUpwardIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Transaction History",
|
|
||||||
path: "/dashboard/transactions/history",
|
|
||||||
icon: HistoryIcon,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ title: "Approve", path: "/dashboard/approve", icon: CheckCircleIcon },
|
|
||||||
{ title: "Investigate", path: "/dashboard/investigate", icon: SearchIcon },
|
|
||||||
{ title: "KYC", path: "/dashboard/kyc", icon: VerifiedUserIcon },
|
|
||||||
{
|
|
||||||
title: "User Accounts",
|
|
||||||
path: "/dashboard/user-accounts",
|
|
||||||
icon: PeopleIcon,
|
|
||||||
},
|
|
||||||
// { title: 'Analytics', path: '/analytics', icon: BarChartIcon },
|
|
||||||
{ title: "Rules", path: "/dashboard/rules", icon: GavelIcon },
|
|
||||||
{ title: "Rules Hub", path: "/dashboard/rules-hub", icon: HubIcon },
|
|
||||||
{
|
|
||||||
title: "Admin",
|
|
||||||
path: "/dashboard/admin",
|
|
||||||
icon: AdminPanelSettingsIcon,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: "Manage Users",
|
|
||||||
path: "/dashboard/admin/users",
|
|
||||||
icon: PeopleIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Transactions",
|
|
||||||
path: "/dashboard/admin/transactions",
|
|
||||||
icon: ListAltIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
path: "dashboard/admin/settings",
|
|
||||||
icon: SettingsIcon,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ title: "Account IQ", path: "/dashboard/account-iq", icon: InsightsIcon },
|
|
||||||
{ title: "Audits", path: "/dashboard/audits", icon: FactCheckIcon },
|
|
||||||
// { title: 'Documentation', path: '/documentation', icon: DescriptionIcon },
|
|
||||||
// { title: 'Support', path: '/support', icon: SupportAgentIcon },
|
|
||||||
// { title: 'System Status', path: '/system-status', icon: WarningAmberIcon },
|
|
||||||
];
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { ElementType } from "react";
|
|
||||||
|
|
||||||
export interface ISidebarLink {
|
|
||||||
title: string;
|
|
||||||
path: string;
|
|
||||||
icon?: ElementType;
|
|
||||||
children?: ISidebarLink[];
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
.sidebar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 240px;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--background-primary);
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 16px;
|
|
||||||
z-index: 1100;
|
|
||||||
border-right: 1px solid #333;
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
&__icon-spacing {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__dropdown-button {
|
|
||||||
padding: 10px 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__submenu {
|
|
||||||
margin-left: 28px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-left: 2px solid rgba(255, 255, 255, 0.1);
|
|
||||||
padding-left: 8px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: white;
|
|
||||||
padding: 8px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import ThemeRegistry from "@/config/ThemeRegistry";
|
|
||||||
import type { Metadata } from "next";
|
|
||||||
import ReduxProvider from "./redux/ReduxProvider";
|
|
||||||
import "../styles/globals.scss";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Your App",
|
|
||||||
description: "Generated by Next.js",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<ReduxProvider>
|
|
||||||
<ThemeRegistry>{children}</ThemeRegistry>
|
|
||||||
</ReduxProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
// Variables for consistent styling
|
|
||||||
$primary-color: #2563eb; // Blue-600 equivalent
|
|
||||||
$primary-hover-color: #1d4ed8; // Blue-700 equivalent
|
|
||||||
$success-color: #16a34a; // Green-600 equivalent
|
|
||||||
$error-color: #dc2626; // Red-600 equivalent
|
|
||||||
$text-color-dark: #1f2937; // Gray-800 equivalent
|
|
||||||
$text-color-medium: #4b5563; // Gray-700 equivalent
|
|
||||||
$text-color-light: #6b7280; // Gray-600 equivalent
|
|
||||||
$border-color: #d1d5db; // Gray-300 equivalent
|
|
||||||
$bg-color-light: #f3f4f6; // Gray-100 equivalent
|
|
||||||
$bg-color-white: #ffffff;
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: $bg-color-light;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: "Inter", sans-serif; // Assuming Inter font is used
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__content {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 56rem; // max-w-4xl
|
|
||||||
background-color: $bg-color-white;
|
|
||||||
border-radius: 0.75rem; // rounded-xl
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg
|
|
||||||
padding: 2rem; // p-8
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__title {
|
|
||||||
font-size: 2.25rem; // text-4xl
|
|
||||||
font-weight: 700; // font-bold
|
|
||||||
color: $text-color-dark;
|
|
||||||
margin-bottom: 1.5rem; // mb-6
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__message--logged-in {
|
|
||||||
font-size: 1.25rem; // text-xl
|
|
||||||
color: $success-color;
|
|
||||||
margin-bottom: 1rem; // mb-4
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__text {
|
|
||||||
color: $text-color-medium;
|
|
||||||
margin-bottom: 1.5rem; // mb-6
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container__button--logout {
|
|
||||||
padding: 0.75rem 1.5rem; // px-6 py-3
|
|
||||||
background-color: $error-color;
|
|
||||||
color: $bg-color-white;
|
|
||||||
font-weight: 600; // font-semibold
|
|
||||||
border-radius: 0.5rem; // rounded-lg
|
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md
|
|
||||||
transition: background-color 0.3s ease-in-out;
|
|
||||||
&:hover {
|
|
||||||
background-color: darken($error-color, 5%); // red-700 equivalent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { DashboardHomePage } from "./features/Pages/DashboardHomePage/DashboardHomePage";
|
|
||||||
|
|
||||||
const DashboardPage = () => {
|
|
||||||
return <DashboardHomePage />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DashboardPage;
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
// app/redux/ReduxProvider.tsx
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import { makeStore } from "./store";
|
|
||||||
import { initializeAuth } from "./auth/authSlice";
|
|
||||||
|
|
||||||
export default function ReduxProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
// Create a store instance for this client-side session
|
|
||||||
const storeRef = useRef<ReturnType<typeof makeStore>>(makeStore());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Dispatch initializeAuth when the ReduxProvider component mounts on the client.
|
|
||||||
// This ensures your Redux isLoggedIn state is synced with localStorage after a page refresh.
|
|
||||||
if (storeRef.current) {
|
|
||||||
storeRef.current.dispatch(initializeAuth() as any);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <Provider store={storeRef.current}>{children}</Provider>;
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
interface AdvancedSearchState {
|
|
||||||
keyword: string;
|
|
||||||
transactionID: string;
|
|
||||||
transactionReferenceId: string;
|
|
||||||
user: string;
|
|
||||||
currency: string;
|
|
||||||
state: string;
|
|
||||||
statusDescription: string;
|
|
||||||
transactionType: string;
|
|
||||||
paymentMethod: string;
|
|
||||||
psps: string;
|
|
||||||
initialPsps: string;
|
|
||||||
merchants: string;
|
|
||||||
startDate: null | string;
|
|
||||||
endDate: null | string;
|
|
||||||
lastUpdatedFrom: null | string;
|
|
||||||
lastUpdatedTo: null | string;
|
|
||||||
minAmount: string;
|
|
||||||
maxAmount: string;
|
|
||||||
channel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: AdvancedSearchState = {
|
|
||||||
keyword: "",
|
|
||||||
transactionID: "",
|
|
||||||
transactionReferenceId: "",
|
|
||||||
user: "",
|
|
||||||
currency: "",
|
|
||||||
state: "",
|
|
||||||
statusDescription: "",
|
|
||||||
transactionType: "",
|
|
||||||
paymentMethod: "",
|
|
||||||
psps: "",
|
|
||||||
initialPsps: "",
|
|
||||||
merchants: "",
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
lastUpdatedFrom: null,
|
|
||||||
lastUpdatedTo: null,
|
|
||||||
minAmount: "",
|
|
||||||
maxAmount: "",
|
|
||||||
channel: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const advancedSearchSlice = createSlice({
|
|
||||||
name: "advancedSearch",
|
|
||||||
initialState,
|
|
||||||
reducers: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default advancedSearchSlice.reducer;
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
// Define the initial state for the authentication slice
|
|
||||||
interface AuthState {
|
|
||||||
isLoggedIn: boolean;
|
|
||||||
authMessage: string;
|
|
||||||
status: "idle" | "loading" | "succeeded" | "failed";
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: AuthState = {
|
|
||||||
isLoggedIn: false,
|
|
||||||
authMessage: "",
|
|
||||||
status: "idle",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Async Thunk for Login
|
|
||||||
// This handles the API call to your Next.js login Route Handler
|
|
||||||
export const login = createAsyncThunk(
|
|
||||||
"auth/login",
|
|
||||||
async (
|
|
||||||
{ email, password }: { email: string; password: string },
|
|
||||||
{ rejectWithValue }
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// If the server responded with an error status (e.g., 401, 400, 500)
|
|
||||||
return rejectWithValue(data.message || "Login failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// On successful login, the backend sets the HTTP-only cookie.
|
|
||||||
// We'll set a client-side flag (like localStorage) for immediate UI updates,
|
|
||||||
// though the primary source of truth for auth is the HTTP-only cookie.
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// Ensure localStorage access is client-side
|
|
||||||
localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync
|
|
||||||
}
|
|
||||||
return data.message || "Login successful";
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle network errors or other unexpected issues
|
|
||||||
return rejectWithValue(error.message || "Network error during login");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Async Thunk for Logout
|
|
||||||
// This handles the API call to your Next.js logout Route Handler
|
|
||||||
export const logout = createAsyncThunk(
|
|
||||||
"auth/logout",
|
|
||||||
async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/auth/logout", {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// If the server responded with an error status
|
|
||||||
return rejectWithValue(data.message || "Logout failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// Ensure localStorage access is client-side
|
|
||||||
localStorage.removeItem("userToken"); // Clear client-side flag
|
|
||||||
}
|
|
||||||
return data.message || "Logged out successfully";
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle network errors
|
|
||||||
return rejectWithValue(error.message || "Network error during logout");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the authentication slice
|
|
||||||
const authSlice = createSlice({
|
|
||||||
name: "auth",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
// Reducer to set an authentication message (e.g., from UI actions)
|
|
||||||
setAuthMessage: (state, action: PayloadAction<string>) => {
|
|
||||||
state.authMessage = action.payload;
|
|
||||||
},
|
|
||||||
// Reducer to clear the authentication message
|
|
||||||
clearAuthMessage: (state) => {
|
|
||||||
state.authMessage = "";
|
|
||||||
},
|
|
||||||
// Reducer to initialize login status from client-side storage (e.g., on app load)
|
|
||||||
// This is useful for cases where middleware might not redirect immediately,
|
|
||||||
// or for client-side rendering of protected content based on initial state.
|
|
||||||
initializeAuth: (state) => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// Ensure this runs only on the client
|
|
||||||
const userToken = localStorage.getItem("userToken");
|
|
||||||
state.isLoggedIn = userToken === "mock-authenticated";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder
|
|
||||||
// Login Thunk Reducers
|
|
||||||
.addCase(login.pending, (state) => {
|
|
||||||
state.status = "loading";
|
|
||||||
state.error = null;
|
|
||||||
state.authMessage = "Attempting login...";
|
|
||||||
})
|
|
||||||
.addCase(login.fulfilled, (state, action) => {
|
|
||||||
state.status = "succeeded";
|
|
||||||
state.isLoggedIn = true;
|
|
||||||
state.authMessage = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(login.rejected, (state, action) => {
|
|
||||||
state.status = "failed";
|
|
||||||
state.isLoggedIn = false;
|
|
||||||
state.error = action.payload as string;
|
|
||||||
state.authMessage = action.payload as string; // Display error message
|
|
||||||
})
|
|
||||||
// Logout Thunk Reducers
|
|
||||||
.addCase(logout.pending, (state) => {
|
|
||||||
state.status = "loading";
|
|
||||||
state.error = null;
|
|
||||||
state.authMessage = "Logging out...";
|
|
||||||
})
|
|
||||||
.addCase(logout.fulfilled, (state, action) => {
|
|
||||||
state.status = "succeeded";
|
|
||||||
state.isLoggedIn = false;
|
|
||||||
state.authMessage = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(logout.rejected, (state, action) => {
|
|
||||||
state.status = "failed";
|
|
||||||
state.isLoggedIn = true; // Stay logged in if logout failed
|
|
||||||
state.error = action.payload as string;
|
|
||||||
state.authMessage = action.payload as string; // Display error message
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setAuthMessage, clearAuthMessage, initializeAuth } =
|
|
||||||
authSlice.actions;
|
|
||||||
|
|
||||||
export default authSlice.reducer;
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { RootState } from "../types";
|
|
||||||
|
|
||||||
export const selectIsLoggedIn = (state: RootState) => state.auth.isLoggedIn;
|
|
||||||
export const selectStatus = (state: RootState) => state.auth?.status;
|
|
||||||
export const selectError = (state: RootState) => state.auth?.error;
|
|
||||||
export const selectAuthMessage = (state: RootState) => state.auth?.authMessage;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
|
||||||
import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
|
|
||||||
import authReducer from "./auth/authSlice";
|
|
||||||
|
|
||||||
export const makeStore = () => {
|
|
||||||
return configureStore({
|
|
||||||
reducer: {
|
|
||||||
advancedSearch: advancedSearchReducer,
|
|
||||||
auth: authReducer,
|
|
||||||
},
|
|
||||||
// Enable Redux DevTools
|
|
||||||
devTools: process.env.NODE_ENV !== "production",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the store instance
|
|
||||||
export const store = makeStore();
|
|
||||||
|
|
||||||
// Infer the type of makeStore
|
|
||||||
export type AppStore = ReturnType<typeof makeStore>;
|
|
||||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
|
||||||
export type RootState = ReturnType<AppStore["getState"]>;
|
|
||||||
export type AppDispatch = AppStore["dispatch"];
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user