diff --git a/app/AuthBootstrap.tsx b/app/AuthBootstrap.tsx index 8327fca..92ed37a 100644 --- a/app/AuthBootstrap.tsx +++ b/app/AuthBootstrap.tsx @@ -5,7 +5,6 @@ import { useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; import { AppDispatch } from "@/app/redux/types"; import { validateAuth } from "./redux/auth/authSlice"; -import { fetchMetadata } from "./redux/metadata/metadataSlice"; export function AuthBootstrap() { const dispatch = useDispatch(); const startedRef = useRef(false); diff --git a/app/api/dashboard/admin/users/[id]/route.ts b/app/api/dashboard/admin/users/[id]/route.ts index 16f7230..50f15fc 100644 --- a/app/api/dashboard/admin/users/[id]/route.ts +++ b/app/api/dashboard/admin/users/[id]/route.ts @@ -71,7 +71,6 @@ export async function PUT( // Transform the request body to match backend format const transformedBody = transformUserUpdateData(body); - console.log("[PUT /api/v1/users/{id}] - transformed body", transformedBody); // Get the auth token from cookies const { cookies } = await import("next/headers"); diff --git a/app/api/dashboard/audits/mockData.ts b/app/api/dashboard/audits/mockData.ts deleted file mode 100644 index 596c5cd..0000000 --- a/app/api/dashboard/audits/mockData.ts +++ /dev/null @@ -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", - }, -]; diff --git a/app/api/dashboard/audits/route.ts b/app/api/dashboard/audits/route.ts index afac8c9..dfe76b8 100644 --- a/app/api/dashboard/audits/route.ts +++ b/app/api/dashboard/audits/route.ts @@ -1,69 +1,80 @@ import { NextRequest, NextResponse } from "next/server"; -import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData"; + +const AUDITS_BASE_URL = + process.env.AUDITS_BASE_URL || + process.env.BE_BASE_URL || + "http://localhost:8583"; +const COOKIE_NAME = "auth_token"; + +const DEFAULT_LIMIT = "25"; +const DEFAULT_PAGE = "1"; export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); + try { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; - const actionType = searchParams.get("actionType"); - const affectedUserId = searchParams.get("affectedUserId"); - const adminId = searchParams.get("adminId"); - const adminUsername = searchParams.get("adminUsername"); - - const dateTimeStart = searchParams.get("dateTime_start"); - const dateTimeEnd = searchParams.get("dateTime_end"); - - 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 (dateTimeStart && dateTimeEnd) { - const start = new Date(dateTimeStart); - const end = new Date(dateTimeEnd); - - // Validate the date range to ensure it’s correct - if (isNaN(start.getTime()) || isNaN(end.getTime())) { + if (!token) { return NextResponse.json( - { - error: "Invalid date range", - }, - { status: 400 } + { message: "Missing Authorization header" }, + { status: 401 } ); } - filteredRows = filteredRows.filter(tx => { - const txDate = new Date(tx.timeStampOfTheAction); + const { searchParams } = new URL(request.url); + const proxiedParams = new URLSearchParams(); - // Validate if the timestamp is a valid date - if (isNaN(txDate.getTime())) { - return false; // Skip invalid dates - } - - return txDate >= start && txDate <= end; + // Forward provided params + searchParams.forEach((value, key) => { + if (value == null || value === "") return; + proxiedParams.append(key, value); }); - } - return NextResponse.json({ - tableRows: filteredRows, - tableColumns: AuditColumns, - tableSearchLabels: AuditSearchLabels, - }); + if (!proxiedParams.has("limit")) { + proxiedParams.set("limit", DEFAULT_LIMIT); + } + if (!proxiedParams.has("page")) { + proxiedParams.set("page", DEFAULT_PAGE); + } + + const backendUrl = `${AUDITS_BASE_URL}/api/v1/audit${ + proxiedParams.size ? `?${proxiedParams.toString()}` : "" + }`; + + const response = await fetch(backendUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); + + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ message: "Failed to fetch audits" })); + return NextResponse.json( + { + success: false, + message: errorData?.message || "Failed to fetch audits", + }, + { status: response.status } + ); + } + + const data = await response.json(); + console.log("[AUDITS] data:", data); + return NextResponse.json(data, { status: response.status }); + } catch (err: unknown) { + console.log("[AUDITS] error:", err); + + console.error("Proxy GET /api/v1/audits error:", err); + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json( + { message: "Internal server error", error: errorMessage }, + { status: 500 } + ); + } } diff --git a/app/api/dashboard/transactions/[id]/route.ts b/app/api/dashboard/transactions/[id]/route.ts new file mode 100644 index 0000000..bca9543 --- /dev/null +++ b/app/api/dashboard/transactions/[id]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; + +const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; +const COOKIE_NAME = "auth_token"; + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = await params; + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + + if (!token) { + return NextResponse.json( + { message: "Missing Authorization header" }, + { status: 401 } + ); + } + + const payload = await request.json(); + + const upstream = await fetch(`${BE_BASE_URL}/api/v1/transactions/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(payload), + }); + + const data = await upstream.json(); + console.log("[DEBUG] [TRANSACTIONS] [PUT] Response data:", data); + return NextResponse.json(data, { status: upstream.status }); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json( + { message: "Internal server error", error: errorMessage }, + { status: 500 } + ); + } +} diff --git a/app/api/dashboard/transactions/all/mockData.ts b/app/api/dashboard/transactions/all/mockData.ts deleted file mode 100644 index 8f8b487..0000000 --- a/app/api/dashboard/transactions/all/mockData.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { GridColDef } from "@mui/x-data-grid"; - -export const allTransactionDummyData = [ - { - 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 allTransactionsColumns: 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 allTransactionsExtraColumns: GridColDef[] = [ - { field: "currency", headerName: "Currency", width: 130 }, - { field: "errorInfo", headerName: "Error Info", width: 130 }, - { field: "fraudScore", headerName: "Fraud Score", width: 130 }, -]; - -export const extraColumns = ["currency", "errorInfo", "fraudScore"]; - -export const allTransactionsSearchLabels = [ - { 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" }, -]; diff --git a/app/api/dashboard/transactions/all/route.ts b/app/api/dashboard/transactions/all/route.ts deleted file mode 100644 index e6bf076..0000000 --- a/app/api/dashboard/transactions/all/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - allTransactionDummyData, - allTransactionsColumns, - allTransactionsSearchLabels, - extraColumns, -} 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"); - - const dateTimeStart = searchParams.get("dateTime_start"); - const dateTimeEnd = searchParams.get("dateTime_end"); - - let filteredTransactions = [...allTransactionDummyData]; - - 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 (dateTimeStart && dateTimeEnd) { - const start = new Date(dateTimeStart); - const end = new Date(dateTimeEnd); - - if (isNaN(start.getTime()) || isNaN(end.getTime())) { - return NextResponse.json( - { - error: "Invalid date range", - }, - { status: 400 } - ); - } - - filteredTransactions = filteredTransactions.filter(tx => { - const txDate = new Date(tx.dateTime); - - if (isNaN(txDate.getTime())) { - return false; - } - - return txDate >= start && txDate <= end; - }); - } - - return NextResponse.json({ - tableRows: filteredTransactions, - tableSearchLabels: allTransactionsSearchLabels, - tableColumns: allTransactionsColumns, - extraColumns: extraColumns, - }); -} diff --git a/app/api/dashboard/transactions/deposits/mockData.ts b/app/api/dashboard/transactions/deposits/mockData.ts deleted file mode 100644 index 2ba30c8..0000000 --- a/app/api/dashboard/transactions/deposits/mockData.ts +++ /dev/null @@ -1,264 +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 depositTransactionsExtraColumns: GridColDef[] = [ - { field: "currency", headerName: "Currency", width: 130 }, - { field: "errorInfo", headerName: "Error Info", width: 130 }, - { field: "fraudScore", headerName: "Fraud Score", width: 130 }, -]; - -// export const extraColumns = ["currency", "errorInfo", "fraudScore"] - -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" }, -]; diff --git a/app/api/dashboard/transactions/deposits/route.ts b/app/api/dashboard/transactions/deposits/route.ts index 818f3f8..d294c7c 100644 --- a/app/api/dashboard/transactions/deposits/route.ts +++ b/app/api/dashboard/transactions/deposits/route.ts @@ -1,82 +1,108 @@ import { NextRequest, NextResponse } from "next/server"; -import { - depositTransactionDummyData, - depositTransactionsColumns, - depositTransactionsSearchLabels, - // extraColumns -} from "./mockData"; -// import { formatToDateTimeString } from "@/app/utils/formatDate"; +const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; +const COOKIE_NAME = "auth_token"; -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); +type FilterValue = + | string + | { + operator?: string; + value: string; + }; - 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"); +export async function POST(request: NextRequest) { + try { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; - const dateTimeStart = searchParams.get("dateTime_start"); - const dateTimeEnd = searchParams.get("dateTime_end"); - - 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 (dateTimeStart && dateTimeEnd) { - const start = new Date(dateTimeStart); - const end = new Date(dateTimeEnd); - - if (isNaN(start.getTime()) || isNaN(end.getTime())) { + if (!token) { return NextResponse.json( - { - error: "Invalid date range", - }, - { status: 400 } + { message: "Missing Authorization header" }, + { status: 401 } ); } - filteredTransactions = filteredTransactions.filter(tx => { - const txDate = new Date(tx.dateTime); + const body = await request.json(); + const { filters = {}, pagination = { page: 1, limit: 10 }, sort } = body; - if (isNaN(txDate.getTime())) { - return false; + // Force deposits filter while allowing other filters to stack + const mergedFilters: Record = { + ...filters, + Type: { + operator: "==", + value: "deposit", + }, + }; + + const queryParts: string[] = []; + queryParts.push(`limit=${pagination.limit}`); + queryParts.push(`page=${pagination.page}`); + + if (sort) { + queryParts.push(`sort=${sort.field}:${sort.order}`); + } + + for (const [key, filterValue] of Object.entries(mergedFilters)) { + if (!filterValue) continue; + + let operator: string; + let value: string; + + if (typeof filterValue === "string") { + operator = "=="; + value = filterValue; + } else { + operator = filterValue.operator || "=="; + value = filterValue.value; } - return txDate >= start && txDate <= end; - }); - } + if (!value) continue; - return NextResponse.json({ - tableRows: filteredTransactions, - tableSearchLabels: depositTransactionsSearchLabels, - tableColumns: depositTransactionsColumns, - }); + const encodedValue = encodeURIComponent(value); + const needsEqualsPrefix = /^[A-Za-z]/.test(operator); + const operatorSegment = needsEqualsPrefix ? `=${operator}` : operator; + + queryParts.push(`${key}${operatorSegment}/${encodedValue}`); + } + + const queryString = queryParts.join("&"); + const backendUrl = `${BE_BASE_URL}/api/v1/transactions${ + queryString ? `?${queryString}` : "" + }`; + + const response = await fetch(backendUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); + + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ message: "Failed to fetch deposits" })); + return NextResponse.json( + { + success: false, + message: errorData?.message || "Failed to fetch deposits", + }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (err: unknown) { + console.error( + "Proxy POST /api/dashboard/transactions/deposits error:", + err + ); + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json( + { message: "Internal server error", error: errorMessage }, + { status: 500 } + ); + } } diff --git a/app/api/dashboard/transactions/route.ts b/app/api/dashboard/transactions/route.ts new file mode 100644 index 0000000..2a2348a --- /dev/null +++ b/app/api/dashboard/transactions/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from "next/server"; + +const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; +const COOKIE_NAME = "auth_token"; + +export async function POST(request: NextRequest) { + try { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + + if (!token) { + return NextResponse.json( + { message: "Missing Authorization header" }, + { status: 401 } + ); + } + + // Parse request body + const body = await request.json(); + const { filters = {}, pagination = { page: 1, limit: 10 }, sort } = body; + + // Build query string for backend + const queryParts: string[] = []; + + // Add pagination (standard key=value format) + queryParts.push(`limit=${pagination.limit}`); + queryParts.push(`page=${pagination.page}`); + + // Add sorting if provided (still key=value) + if (sort) { + queryParts.push(`sort=${sort.field}:${sort.order}`); + } + + // Process filters - convert FilterValue objects to operator/value format + for (const [key, filterValue] of Object.entries(filters)) { + if (!filterValue) continue; + + let op: string; + let value: string; + + if (typeof filterValue === "string") { + // Simple string filter - default to == + op = "=="; + value = filterValue; + } else { + // FilterValue object with operator and value + const filterVal = filterValue as { operator?: string; value: string }; + op = filterVal.operator || "=="; + value = filterVal.value; + } + + if (!value) continue; + + // Encode value to prevent breaking URL + const encodedValue = encodeURIComponent(value); + queryParts.push(`${key}=${op}/${encodedValue}`); + } + + const queryString = queryParts.join("&"); + const backendUrl = `${BE_BASE_URL}/api/v1/transactions${queryString ? `?${queryString}` : ""}`; + + console.log("[DEBUG] [TRANSACTIONS] Backend URL:", backendUrl); + + const response = await fetch(backendUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); + + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ message: "Failed to fetch transactions" })); + return NextResponse.json( + { + success: false, + message: errorData?.message || "Failed to fetch transactions", + }, + { status: response.status } + ); + } + + const data = await response.json(); + console.log("[DEBUG] [TRANSACTIONS] Response data:", data); + + return NextResponse.json(data, { status: response.status }); + } catch (err: unknown) { + console.error("Proxy GET /api/v1/transactions error:", err); + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json( + { message: "Internal server error", error: errorMessage }, + { status: 500 } + ); + } +} diff --git a/app/api/metadata/route.ts b/app/api/metadata/route.ts index 26277e4..113b8f4 100644 --- a/app/api/metadata/route.ts +++ b/app/api/metadata/route.ts @@ -28,8 +28,6 @@ export async function GET() { const data = await res.json(); - console.log("metadata", data); - if (!res.ok) { return NextResponse.json( { diff --git a/app/dashboard/audits/page.scss b/app/dashboard/audits/page.scss new file mode 100644 index 0000000..e8f142e --- /dev/null +++ b/app/dashboard/audits/page.scss @@ -0,0 +1,42 @@ +.audits-page { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + + .page-title { + font-size: 1.5rem; + font-weight: 500; + margin: 0; + } + + .error-alert { + margin-bottom: 8px; + padding: 12px 16px; + background-color: #fee; + color: #c62828; + border-radius: 4px; + border: 1px solid #ffcdd2; + } + + .table-container { + width: 100%; + background-color: #fff; + border-radius: 4px; + box-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + overflow: hidden; + + .scroll-wrapper { + width: 100dvw; + overflow-x: auto; + overflow-y: hidden; + + .table-inner { + min-width: 1200px; + } + } + } +} diff --git a/app/dashboard/audits/page.tsx b/app/dashboard/audits/page.tsx index 9bec2b5..10a804f 100644 --- a/app/dashboard/audits/page.tsx +++ b/app/dashboard/audits/page.tsx @@ -1,22 +1,293 @@ -import DataTable from "@/app/features/DataTable/DataTable"; -import { getAudits } from "@/app/services/audits"; +"use client"; -export default async function AuditPage({ - searchParams, -}: { - searchParams: Promise>; -}) { - // Await searchParams before processing - const params = await searchParams; - // Create a safe query string by filtering only string values - const safeParams: Record = {}; - for (const [key, value] of Object.entries(params)) { - if (typeof value === "string") { - safeParams[key] = value; +import { useEffect, useMemo, useState } from "react"; +import { + DataGrid, + GridColDef, + GridPaginationModel, + GridSortModel, +} from "@mui/x-data-grid"; +import { getAudits } from "@/app/services/audits"; +import "./page.scss"; + +type AuditRow = Record & { id: string | number }; + +interface AuditApiResponse { + total?: number; + limit?: number; + page?: number; + data?: unknown; + items?: unknown[]; + audits?: unknown[]; + logs?: unknown[]; + results?: unknown[]; + records?: unknown[]; + meta?: { total?: number }; + pagination?: { total?: number }; +} + +const DEFAULT_PAGE_SIZE = 25; + +const FALLBACK_COLUMNS: GridColDef[] = [ + { + field: "placeholder", + headerName: "Audit Data", + flex: 1, + sortable: false, + filterable: false, + }, +]; + +const CANDIDATE_ARRAY_KEYS: (keyof AuditApiResponse)[] = [ + "items", + "audits", + "logs", + "results", + "records", +]; + +const normalizeValue = (value: unknown): string | number => { + if (value === null || value === undefined) { + return ""; + } + + if (typeof value === "string" || typeof value === "number") { + return value; + } + + if (typeof value === "boolean") { + return value ? "true" : "false"; + } + + return JSON.stringify(value); +}; + +const toTitle = (field: string) => + field + .replace(/_/g, " ") + .replace(/-/g, " ") + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/\s+/g, " ") + .trim() + .replace(/^\w/g, char => char.toUpperCase()); + +const deriveColumns = (rows: AuditRow[]): GridColDef[] => { + if (!rows.length) return []; + + return Object.keys(rows[0]).map(field => ({ + field, + headerName: toTitle(field), + flex: field === "id" ? 0 : 1, + minWidth: field === "id" ? 140 : 200, + sortable: true, + })); +}; + +const extractArray = (payload: AuditApiResponse): unknown[] => { + if (Array.isArray(payload)) { + return payload; + } + + for (const key of CANDIDATE_ARRAY_KEYS) { + const candidate = payload[key]; + if (Array.isArray(candidate)) { + return candidate; } } - const query = new URLSearchParams(safeParams).toString(); - const data = await getAudits({ query }); - return ; + const dataRecord = + payload.data && + typeof payload.data === "object" && + !Array.isArray(payload.data) + ? (payload.data as Record) + : null; + + if (dataRecord) { + for (const key of CANDIDATE_ARRAY_KEYS) { + const candidate = dataRecord[key]; + if (Array.isArray(candidate)) { + return candidate; + } + } + } + + if (Array.isArray(payload.data)) { + return payload.data; + } + + return []; +}; + +const resolveTotal = (payload: AuditApiResponse, fallback: number): number => { + const fromPayload = payload.total; + const fromMeta = payload.meta?.total; + const fromPagination = payload.pagination?.total; + const fromData = + payload.data && + typeof payload.data === "object" && + !Array.isArray(payload.data) + ? (payload.data as { total?: number }).total + : undefined; + + return ( + (typeof fromPayload === "number" && fromPayload) || + (typeof fromMeta === "number" && fromMeta) || + (typeof fromPagination === "number" && fromPagination) || + (typeof fromData === "number" && fromData) || + fallback + ); +}; + +const normalizeRows = (entries: unknown[], page: number): AuditRow[] => + entries.map((entry, index) => { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return { + id: `${page}-${index}`, + value: normalizeValue(entry), + }; + } + + const record = entry as Record; + + const normalized: Record = {}; + Object.entries(record).forEach(([key, value]) => { + normalized[key] = normalizeValue(value); + }); + + const identifier = + record.id ?? + record.audit_id ?? + record.log_id ?? + record._id ?? + `${page}-${index}`; + + return { + id: (identifier as string | number) ?? `${page}-${index}`, + ...normalized, + }; + }); + +export default function AuditPage() { + const [rows, setRows] = useState([]); + const [columns, setColumns] = useState([]); + const [rowCount, setRowCount] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [paginationModel, setPaginationModel] = useState({ + page: 0, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [sortModel, setSortModel] = useState([]); + + useEffect(() => { + const controller = new AbortController(); + + const fetchAudits = async () => { + setLoading(true); + setError(null); + + const sortParam = + sortModel.length && sortModel[0].field && sortModel[0].sort + ? `${sortModel[0].field}:${sortModel[0].sort}` + : undefined; + + try { + const payload = (await getAudits({ + limit: paginationModel.pageSize, + page: paginationModel.page + 1, + sort: sortParam, + signal: controller.signal, + })) as AuditApiResponse; + + const auditEntries = extractArray(payload); + const normalized = normalizeRows(auditEntries, paginationModel.page); + + setColumns(prev => + normalized.length + ? deriveColumns(normalized) + : prev.length + ? prev + : FALLBACK_COLUMNS + ); + + setRows(normalized); + setRowCount(resolveTotal(payload, normalized.length)); + } catch (err) { + if (controller.signal.aborted) return; + const message = + err instanceof Error ? err.message : "Failed to load audits"; + setError(message); + setRows([]); + setColumns(prev => (prev.length ? prev : FALLBACK_COLUMNS)); + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + }; + + fetchAudits(); + + return () => controller.abort(); + }, [paginationModel, sortModel]); + + const handlePaginationChange = (model: GridPaginationModel) => { + setPaginationModel(model); + }; + + const handleSortModelChange = (model: GridSortModel) => { + setSortModel(model); + setPaginationModel(prev => ({ ...prev, page: 0 })); + }; + + const pageTitle = useMemo( + () => + sortModel.length && sortModel[0].field + ? `Audit Logs · sorted by ${toTitle(sortModel[0].field)}` + : "Audit Logs", + [sortModel] + ); + + return ( +
+

{pageTitle}

+ {error && ( +
+ {error} +
+ )} +
+
+
+ +
+
+
+
+ ); } diff --git a/app/dashboard/transactions/all/page.tsx b/app/dashboard/transactions/all/page.tsx index 17e12eb..1e1745a 100644 --- a/app/dashboard/transactions/all/page.tsx +++ b/app/dashboard/transactions/all/page.tsx @@ -1,23 +1,77 @@ +"use client"; + import DataTable from "@/app/features/DataTable/DataTable"; -import { getTransactions } from "@/app/services/transactions"; +import { useEffect, useMemo, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectFilters, + selectPagination, + selectSort, +} from "@/app/redux/advanedSearch/selectors"; +import { AppDispatch } from "@/app/redux/store"; +import { setError as setAdvancedSearchError } from "@/app/redux/advanedSearch/advancedSearchSlice"; +import { TransactionRow, BackendTransaction } from "../interface"; -export default async function AllTransactionPage({ - searchParams, -}: { - searchParams: Promise>; -}) { - // Await searchParams before processing - const params = await searchParams; - // Create a safe query string by filtering only string values - const safeParams: Record = {}; - for (const [key, value] of Object.entries(params)) { - if (typeof value === "string") { - safeParams[key] = value; - } - } - const query = new URLSearchParams(safeParams).toString(); - const transactionType = "all"; - const data = await getTransactions({ transactionType, query }); +export default function AllTransactionPage() { + const dispatch = useDispatch(); + const filters = useSelector(selectFilters); + const pagination = useSelector(selectPagination); + const sort = useSelector(selectSort); - return ; + const [tableRows, setTableRows] = useState([]); + const extraColumns: string[] = []; // static for now + + // Memoize rows to avoid new reference each render + const memoizedRows = useMemo(() => tableRows, [tableRows]); + + // Fetch data when filters, pagination, or sort changes + useEffect(() => { + const fetchData = async () => { + dispatch(setAdvancedSearchError(null)); + try { + const response = await fetch("/api/dashboard/transactions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ filters, pagination, sort }), + }); + + if (!response.ok) { + dispatch(setAdvancedSearchError("Failed to fetch transactions")); + setTableRows([]); + return; + } + + const backendData = await response.json(); + const transactions = backendData.transactions || []; + + const rows = transactions.map((tx: BackendTransaction) => ({ + id: tx.id || 0, + userId: tx.customer, + transactionId: tx.external_id || tx.id, + type: tx.type, + currency: tx.currency, + amount: tx.amount, + status: tx.status, + dateTime: tx.created || tx.modified, + merchantId: tx.merchant_id, + pspId: tx.psp_id, + methodId: tx.method_id, + modified: tx.modified, + })); + + setTableRows(rows); + } catch (error) { + dispatch( + setAdvancedSearchError( + error instanceof Error ? error.message : "Unknown error" + ) + ); + setTableRows([]); + } + }; + + fetchData(); + }, [dispatch, filters, pagination, sort]); + + return ; } diff --git a/app/dashboard/transactions/deposits/page.tsx b/app/dashboard/transactions/deposits/page.tsx index aa589d0..eb59da3 100644 --- a/app/dashboard/transactions/deposits/page.tsx +++ b/app/dashboard/transactions/deposits/page.tsx @@ -1,23 +1,93 @@ +"use client"; + import DataTable from "@/app/features/DataTable/DataTable"; -import { getTransactions } from "@/app/services/transactions"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch } from "@/app/redux/store"; +import { + selectFilters, + selectPagination, + selectSort, +} from "@/app/redux/advanedSearch/selectors"; +import { + setStatus, + setError as setAdvancedSearchError, +} from "@/app/redux/advanedSearch/advancedSearchSlice"; +import { useEffect, useMemo, useState } from "react"; +import { TransactionRow, BackendTransaction } from "../interface"; -export default async function DepositTransactionPage({ - searchParams, -}: { - searchParams: Promise>; -}) { - // Await searchParams before processing - const params = await searchParams; - // Create a safe query string by filtering only string values - const safeParams: Record = {}; - 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 }); +export default function DepositTransactionPage() { + const dispatch = useDispatch(); + const filters = useSelector(selectFilters); + const pagination = useSelector(selectPagination); + const sort = useSelector(selectSort); + const [tableRows, setTableRows] = useState([]); - return ; + const memoizedRows = useMemo(() => tableRows, [tableRows]); + + const depositFilters = useMemo(() => { + return { + ...filters, + Type: { + operator: "==", + value: "deposit", + }, + }; + }, [filters]); + + useEffect(() => { + const fetchDeposits = async () => { + dispatch(setStatus("loading")); + dispatch(setAdvancedSearchError(null)); + try { + const response = await fetch("/api/dashboard/transactions/deposits", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + filters: depositFilters, + pagination, + sort, + }), + }); + + if (!response.ok) { + dispatch(setAdvancedSearchError("Failed to fetch deposits")); + setTableRows([]); + return; + } + + const backendData = await response.json(); + const transactions: BackendTransaction[] = + backendData.transactions || []; + + const rows: TransactionRow[] = transactions.map(tx => ({ + id: tx.id, + userId: tx.customer, + transactionId: String(tx.external_id ?? tx.id), + type: tx.type, + currency: tx.currency, + amount: tx.amount, + status: tx.status, + dateTime: tx.created || tx.modified, + merchantId: tx.merchant_id, + pspId: tx.psp_id, + methodId: tx.method_id, + modified: tx.modified, + })); + + setTableRows(rows); + dispatch(setStatus("succeeded")); + } catch (error) { + dispatch( + setAdvancedSearchError( + error instanceof Error ? error.message : "Unknown error" + ) + ); + setTableRows([]); + } + }; + + fetchDeposits(); + }, [dispatch, depositFilters, pagination, sort]); + + return ; } diff --git a/app/dashboard/transactions/interface.ts b/app/dashboard/transactions/interface.ts new file mode 100644 index 0000000..4392876 --- /dev/null +++ b/app/dashboard/transactions/interface.ts @@ -0,0 +1,29 @@ +export interface TransactionRow { + id: number; + userId?: string; + transactionId: string; + type?: string; + currency?: string; + amount?: number; + status?: string; + dateTime?: string; + merchantId?: string; + pspId?: string; + methodId?: string; + modified?: string; +} + +export interface BackendTransaction { + id: number; + customer?: string; + external_id?: string; + type?: string; + currency?: string; + amount?: number; + status?: string; + created?: string; + modified?: string; + merchant_id?: string; + psp_id?: string; + method_id?: string; +} diff --git a/app/features/AdvancedSearch/AdvancedSearch.tsx b/app/features/AdvancedSearch/AdvancedSearch.tsx index a1350b3..694225b 100644 --- a/app/features/AdvancedSearch/AdvancedSearch.tsx +++ b/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -1,4 +1,5 @@ "use client"; + import { Box, TextField, @@ -16,57 +17,158 @@ 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 { useDispatch, useSelector } from "react-redux"; import { useState, useEffect, useMemo } from "react"; import { ISearchLabel } from "../DataTable/types"; +import { AppDispatch } from "@/app/redux/store"; +import { + updateFilter, + clearFilters, + FilterValue, +} from "@/app/redux/advanedSearch/advancedSearchSlice"; +import { selectFilters } from "@/app/redux/advanedSearch/selectors"; +import { normalizeValue, defaultOperatorForField } from "./utils/utils"; + +// ----------------------------------------------------- +// COMPONENT +// ----------------------------------------------------- export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) { - const searchParams = useSearchParams(); - const router = useRouter(); + const dispatch = useDispatch(); + const filters = useSelector(selectFilters); + const [open, setOpen] = useState(false); + + // Local form state for UI (synced with Redux) const [formValues, setFormValues] = useState>({}); + const [operators, setOperators] = useState>({}); + // ----------------------------------------------------- + // SYNC REDUX FILTERS TO LOCAL STATE ON LOAD + // ----------------------------------------------------- useEffect(() => { - const initialParams = Object.fromEntries(searchParams.entries()); - setFormValues(initialParams); - }, [searchParams]); + const values: Record = {}; + const ops: Record = {}; - const updateURL = useMemo( + labels.forEach(({ field, type }) => { + const filter = filters[field]; + + if (filter) { + if (typeof filter === "string") { + // Simple string filter + values[field] = filter; + ops[field] = defaultOperatorForField(field, type); + } else { + // FilterValue object with operator and value + values[field] = filter.value; + ops[field] = filter.operator; + } + } + + // Handle date ranges + const startKey = `${field}_start`; + const endKey = `${field}_end`; + const startFilter = filters[startKey]; + const endFilter = filters[endKey]; + + if (startFilter && typeof startFilter === "string") { + values[startKey] = startFilter; + } + if (endFilter && typeof endFilter === "string") { + values[endKey] = endFilter; + } + }); + + setFormValues(values); + setOperators(ops); + }, [filters, labels]); + + // ----------------------------------------------------- + // DEBOUNCED FILTER UPDATE + // ----------------------------------------------------- + const debouncedUpdateFilter = useMemo( () => - debounce((newValues: Record) => { - const updatedParams = new URLSearchParams(); - Object.entries(newValues).forEach(([key, value]) => { - if (value) updatedParams.set(key, value); - }); - router.push(`?${updatedParams.toString()}`); - }, 500), - [router] + debounce( + (field: string, value: string | undefined, operator?: string) => { + if (!value || value === "") { + dispatch(updateFilter({ field, value: undefined })); + return; + } + + const safeValue = normalizeValue(value); + if (!safeValue) { + dispatch(updateFilter({ field, value: undefined })); + return; + } + + // For text/select fields, use FilterValue with operator + const filterValue: FilterValue = { + operator: operator ?? defaultOperatorForField(field, "text"), + value: safeValue, + }; + + dispatch(updateFilter({ field, value: filterValue })); + }, + 300 + ), + [dispatch] ); - const handleFieldChange = (field: string, value: string) => { - const updatedValues = { ...formValues, [field]: value }; - console.log(updatedValues); - setFormValues(updatedValues); - updateURL(updatedValues); + // ----------------------------------------------------- + // handlers + // ----------------------------------------------------- + + const updateField = (field: string, value: string) => { + setFormValues(prev => ({ ...prev, [field]: value })); + const operator = operators[field] ?? defaultOperatorForField(field, "text"); + debouncedUpdateFilter(field, value, operator); + }; + + const updateOperator = (field: string, op: string) => { + setOperators(prev => ({ ...prev, [field]: op })); + + // If value exists, update filter immediately with new operator + const currentValue = formValues[field]; + if (currentValue) { + const safeValue = normalizeValue(currentValue); + if (safeValue) { + dispatch( + updateFilter({ + field, + value: { operator: op, value: safeValue }, + }) + ); + } + } + }; + + const updateDateRange = ( + field: string, + start: string | undefined, + end: string | undefined + ) => { + if (start) { + dispatch(updateFilter({ field: `${field}_start`, value: start })); + } else { + dispatch(updateFilter({ field: `${field}_start`, value: undefined })); + } + + if (end) { + dispatch(updateFilter({ field: `${field}_end`, value: end })); + } else { + dispatch(updateFilter({ field: `${field}_end`, value: undefined })); + } }; const resetForm = () => { setFormValues({}); - router.push("?"); + setOperators({}); + dispatch(clearFilters()); }; - 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); - }; - + // ----------------------------------------------------- + // render + // ----------------------------------------------------- return ( - - + setOpen(false)}> + - - - - Search - - - - + + - {extraColumns && extraColumns.length > 0 && ( - - )} - - - - { - if (params.field !== "actions") { - handleClickField(params.field, params.value as string); - } - }} - /> + + + + - - setModalOpen(false)} - handleSave={handleStatusSave} - /> - - setOpen(false)}> - Export Transactions - - - - - setOnlyCurrentTable(e.target.checked)} - /> - } - label="Only export current table" - sx={{ mt: 2 }} - /> - - - - - - - + { + setModalOpen(false); + setReason(""); + setPendingStatus(""); + setStatusUpdateError(null); + setSelectedRowId(null); + }} + handleSave={handleStatusSave} + isSubmitting={isUpdatingStatus} + errorMessage={statusUpdateError} + /> + + ); }; -export default DataTable; +// Memoize to avoid unnecessary re-renders +export default React.memo(DataTable); diff --git a/app/features/DataTable/DataTableHeader.tsx b/app/features/DataTable/DataTableHeader.tsx new file mode 100644 index 0000000..cb14f73 --- /dev/null +++ b/app/features/DataTable/DataTableHeader.tsx @@ -0,0 +1,51 @@ +import { Button, TextField, Stack } from "@mui/material"; +import FileUploadIcon from "@mui/icons-material/FileUpload"; +import AdvancedSearch from "../AdvancedSearch/AdvancedSearch"; +import { TABLE_SEARCH_LABELS } from "./constants"; + +type DataTableHeaderProps = { + extraColumns?: string[]; + showExtraColumns: boolean; + onToggleExtraColumns: () => void; + onOpenExport: () => void; +}; + +export default function DataTableHeader({ + extraColumns, + showExtraColumns, + onToggleExtraColumns, + onOpenExport, +}: DataTableHeaderProps) { + return ( + + console.log(`setSearchQuery(${e.target.value})`)} + sx={{ width: 300, backgroundColor: "#f0f0f0" }} + /> + + + {extraColumns && extraColumns.length > 0 && ( + + )} + + ); +} diff --git a/app/features/DataTable/StatusChangeDialog.tsx b/app/features/DataTable/StatusChangeDialog.tsx index 2ffd269..aa66c8e 100644 --- a/app/features/DataTable/StatusChangeDialog.tsx +++ b/app/features/DataTable/StatusChangeDialog.tsx @@ -5,6 +5,8 @@ import { DialogActions, Button, TextField, + Alert, + CircularProgress, } from "@mui/material"; import { useState, useEffect } from "react"; @@ -15,6 +17,8 @@ interface StatusChangeDialogProps { setReason: React.Dispatch>; handleClose: () => void; handleSave: () => void; + isSubmitting?: boolean; + errorMessage?: string | null; } const StatusChangeDialog = ({ @@ -24,17 +28,19 @@ const StatusChangeDialog = ({ setReason, handleClose, handleSave, + isSubmitting = false, + errorMessage, }: StatusChangeDialogProps) => { const [isValid, setIsValid] = useState(false); useEffect(() => { - const noSpaces = reason.replace(/\s/g, ""); // remove all spaces + const noSpaces = reason.replace(/\s/g, ""); const length = noSpaces.length; setIsValid(length >= 12 && length <= 400); }, [reason]); return ( - + Change Status You want to change the status to {newStatus}. Please provide a @@ -50,10 +56,22 @@ const StatusChangeDialog = ({ helperText="Reason must be between 12 and 400 characters" sx={{ mt: 2 }} /> + {errorMessage && ( + + {errorMessage} + + )} - - + diff --git a/app/features/DataTable/constants.ts b/app/features/DataTable/constants.ts new file mode 100644 index 0000000..4633f04 --- /dev/null +++ b/app/features/DataTable/constants.ts @@ -0,0 +1,37 @@ +import { GridColDef } from "@mui/x-data-grid"; +import { ISearchLabel } from "./types"; + +export const TABLE_COLUMNS: GridColDef[] = [ + { field: "userId", headerName: "User ID", width: 130 }, + { field: "transactionId", headerName: "Transaction ID", width: 180 }, + { field: "type", headerName: "Type", width: 120 }, + { field: "currency", headerName: "Currency", width: 100 }, + { field: "amount", headerName: "Amount", width: 120 }, + { field: "status", headerName: "Status", width: 120 }, + { field: "dateTime", headerName: "Date / Time", width: 180 }, +]; + +export const TABLE_SEARCH_LABELS: ISearchLabel[] = [ + { label: "User", field: "Customer", type: "text" }, + { label: "Transaction ID", field: "ExternalID", type: "text" }, + { + label: "Type", + field: "Type", + type: "select", + options: ["deposit", "withdrawal"], + }, + { + label: "Currency", + field: "Currency", + type: "select", + options: ["USD", "EUR", "GBP", "TRY"], + }, + { + label: "Status", + field: "Status", + type: "select", + options: ["pending", "completed", "failed"], + }, + { label: "Amount", field: "Amount", type: "text" }, + { label: "Date / Time", field: "created", type: "date" }, +]; diff --git a/app/features/DataTable/re-selectors/index.tsx b/app/features/DataTable/re-selectors/index.tsx new file mode 100644 index 0000000..2ac907c --- /dev/null +++ b/app/features/DataTable/re-selectors/index.tsx @@ -0,0 +1,238 @@ +import { createSelector } from "@reduxjs/toolkit"; +import { Box, IconButton, MenuItem, Select } from "@mui/material"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import { RootState } from "@/app/redux/store"; +import { TABLE_COLUMNS } from "../constants"; +import { selectTransactionStatuses } from "@/app/redux/metadata/selectors"; +import { DataRowBase } from "../types"; + +const TRANSACTION_STATUS_FALLBACK: string[] = [ + "pending", + "completed", + "failed", + "inprogress", + "error", +]; + +type StatusChangeHandler = (rowId: number, newStatus: string) => void; + +const selectEnableStatusActions = ( + _state: RootState, + enableStatusActions: boolean +) => enableStatusActions; + +const selectExtraColumns = ( + _state: RootState, + _enableStatusActions: boolean, + extraColumns?: string[] | null +) => extraColumns ?? null; + +const selectShowExtraColumns = ( + _state: RootState, + _enableStatusActions: boolean, + _extraColumns?: string[] | null, + showExtraColumns = false +) => showExtraColumns; + +const selectLocalRows = ( + _state: RootState, + _enableStatusActions: boolean, + _extraColumns?: string[] | null, + _showExtraColumns?: boolean, + localRows?: DataRowBase[] +) => localRows ?? []; + +const noopStatusChangeHandler: StatusChangeHandler = () => {}; + +const selectStatusChangeHandler = ( + _state: RootState, + _enableStatusActions: boolean, + _extraColumns?: string[] | null, + _showExtraColumns?: boolean, + _localRows?: DataRowBase[], + handleStatusChange?: StatusChangeHandler +) => handleStatusChange ?? noopStatusChangeHandler; + +export const selectBaseColumns = createSelector( + [selectEnableStatusActions], + enableStatusActions => { + if (!enableStatusActions) { + return TABLE_COLUMNS; + } + + return [ + ...TABLE_COLUMNS, + { + field: "actions", + headerName: "Actions", + width: 160, + sortable: false, + filterable: false, + } as GridColDef, + ]; + } +); + +export const selectVisibleColumns = createSelector( + [selectBaseColumns, selectExtraColumns, selectShowExtraColumns], + (baseColumns, extraColumns, showExtraColumns) => { + if (!extraColumns || extraColumns.length === 0) { + return baseColumns; + } + + return showExtraColumns + ? baseColumns + : baseColumns.filter(col => !extraColumns.includes(col.field)); + } +); + +export const selectResolvedTransactionStatuses = createSelector( + [selectTransactionStatuses], + statuses => (statuses.length > 0 ? statuses : TRANSACTION_STATUS_FALLBACK) +); + +export const selectEnhancedColumns = createSelector( + [ + selectVisibleColumns, + selectLocalRows, + selectStatusChangeHandler, + selectResolvedTransactionStatuses, + ], + ( + visibleColumns, + localRows, + handleStatusChange, + resolvedStatusOptions + ): GridColDef[] => { + return visibleColumns.map(col => { + if (col.field === "status") { + return { + ...col, + renderCell: (params: GridRenderCellParams) => { + const value = params.value?.toLowerCase(); + let bgColor = "#e0e0e0"; + let textColor = "#000"; + switch (value) { + case "completed": + bgColor = "#d0f0c0"; + textColor = "#1b5e20"; + break; + case "pending": + bgColor = "#fff4cc"; + textColor = "#9e7700"; + break; + case "inprogress": + bgColor = "#cce5ff"; + textColor = "#004085"; + break; + case "error": + bgColor = "#ffcdd2"; + textColor = "#c62828"; + break; + } + return ( + + {params.value} + + ); + }, + }; + } + + if (col.field === "userId") { + return { + ...col, + headerAlign: "center", + align: "center", + renderCell: (params: GridRenderCellParams) => ( + e.stopPropagation()} + > + + {params.value} + + e.stopPropagation()} + > + + + + ), + }; + } + + if (col.field === "actions") { + return { + ...col, + renderCell: (params: GridRenderCellParams) => { + const currentRow = localRows.find(row => row.id === params.id); + const options = + currentRow?.options?.map(option => option.value) ?? + resolvedStatusOptions; + const uniqueOptions: string[] = Array.from(new Set(options)); + + return ( + + value={currentRow?.status ?? ""} + onChange={e => + handleStatusChange( + params.id as number, + e.target.value as string + ) + } + size="small" + fullWidth + displayEmpty + sx={{ + "& .MuiOutlinedInput-notchedOutline": { border: "none" }, + "& .MuiSelect-select": { py: 0.5 }, + }} + onClick={e => e.stopPropagation()} + > + {uniqueOptions.map(option => ( + + {option} + + ))} + + ); + }, + }; + } + + return col; + }) as GridColDef[]; + } +); diff --git a/app/features/DataTable/types.ts b/app/features/DataTable/types.ts index a104fa4..5cc2efe 100644 --- a/app/features/DataTable/types.ts +++ b/app/features/DataTable/types.ts @@ -11,3 +11,9 @@ export interface IDataTable { tableSearchLabels: ISearchLabel[]; extraColumns: string[]; } + +export interface DataRowBase { + id: number; + status?: string; + options?: { value: string; label: string }[]; +} diff --git a/app/features/UserRoles/userRoleCard.tsx b/app/features/UserRoles/userRoleCard.tsx index ce6578c..7f73d34 100644 --- a/app/features/UserRoles/userRoleCard.tsx +++ b/app/features/UserRoles/userRoleCard.tsx @@ -161,7 +161,7 @@ export default function UserRoleCard({ user }: Props) { setShowConfirmModal(false)} - title="Reset Password" + title={`Reset Password - ${user.first_name}`} > {newPassword && (
diff --git a/app/features/dashboard/header/dropDown/DropDown.tsx b/app/features/dashboard/header/dropDown/DropDown.tsx index ae524bf..f81b600 100644 --- a/app/features/dashboard/header/dropDown/DropDown.tsx +++ b/app/features/dashboard/header/dropDown/DropDown.tsx @@ -7,7 +7,6 @@ import { SelectChangeEvent, } from "@mui/material"; import PageLinks from "../../../../components/PageLinks/PageLinks"; -import { SidebarItem } from "@/app/redux/metadata/metadataSlice"; import { useSelector } from "react-redux"; import { selectNavigationSidebar } from "@/app/redux/metadata/selectors"; import "./DropDown.scss"; @@ -24,8 +23,6 @@ export default function SidebarDropdown({ onChange }: Props) { onChange?.(event); }; - console.log("sidebar", sidebar); - return ( Navigate To diff --git a/app/features/dashboard/sidebar/Sidebar.tsx b/app/features/dashboard/sidebar/Sidebar.tsx index 0114135..7d1edbe 100644 --- a/app/features/dashboard/sidebar/Sidebar.tsx +++ b/app/features/dashboard/sidebar/Sidebar.tsx @@ -9,7 +9,7 @@ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { selectNavigationSidebar } from "@/app/redux/metadata/selectors"; -import { SidebarItem } from "@/app/redux/metadata/metadataSlice"; +import { SidebarLink } from "@/app/redux/metadata/metadataSlice"; import { resolveIcon } from "@/app/utils/iconMap"; import "./sideBar.scss"; @@ -20,7 +20,7 @@ interface SidebarProps { const SideBar = ({ isOpen = true, onClose }: SidebarProps) => { const [openMenus, setOpenMenus] = useState>({}); - const sidebar = useSelector(selectNavigationSidebar)?.links; + const sidebar = useSelector(selectNavigationSidebar); const toggleMenu = (title: string) => { setOpenMenus(prev => ({ ...prev, [title]: !prev[title] })); }; @@ -32,12 +32,12 @@ const SideBar = ({ isOpen = true, onClose }: SidebarProps) => {
- Betrise cashier + Cashier
- {sidebar?.map((link: SidebarItem) => { + {sidebar?.map((link: SidebarLink) => { if (link.children) { const Icon = resolveIcon(link.icon as string); return ( diff --git a/app/features/dashboard/sidebar/sideBar.scss b/app/features/dashboard/sidebar/sideBar.scss index b7113a9..b51770b 100644 --- a/app/features/dashboard/sidebar/sideBar.scss +++ b/app/features/dashboard/sidebar/sideBar.scss @@ -10,13 +10,22 @@ flex-direction: column; padding: 16px; z-index: 1100; - border-right: 1px solid #333; + border-right: 1px solid #835454; transition: transform 0.3s ease-in-out; overflow: hidden; + .sidebar__submenu { + overflow: hidden; + max-height: 200px; + transition: max-height 0.3s ease-in-out; + overflow-y: auto; + &:hover { + max-height: 300px; + } + } + &--collapsed { transform: translateX(-210px); // Hide 90% (210px out of 240px) - .sidebar__header, .sidebar__dropdown-button, .sidebar__submenu, @@ -153,3 +162,9 @@ display: none; } } + +/* Track (background behind the thumb) */ +.scrollable-div::-webkit-scrollbar-track { + background: #f0f0f0; + border-radius: 5px; +} diff --git a/app/redux/advanedSearch/advancedSearchSlice.ts b/app/redux/advanedSearch/advancedSearchSlice.ts index b3f5dfc..46f04ce 100644 --- a/app/redux/advanedSearch/advancedSearchSlice.ts +++ b/app/redux/advanedSearch/advancedSearchSlice.ts @@ -1,53 +1,106 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { createSlice, PayloadAction } 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; +// Filter value can be a simple string or an object with operator and value +export interface FilterValue { + operator: string; + value: string; +} + +export type FilterField = string | FilterValue; + +export interface AdvancedSearchFilters { + [field: string]: FilterField | undefined; +} + +export type FetchStatus = "idle" | "loading" | "succeeded" | "failed"; + +export interface AdvancedSearchState { + filters: AdvancedSearchFilters; + pagination: { + page: number; + limit: number; + }; + sort?: { + field: string; + order: "asc" | "desc"; + }; + status: FetchStatus; + error: string | null; } 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: "", + filters: {}, + pagination: { + page: 1, + limit: 10, + }, + status: "idle", + error: null, }; const advancedSearchSlice = createSlice({ name: "advancedSearch", initialState, - reducers: {}, + reducers: { + setFilters: (state, action: PayloadAction) => { + state.filters = action.payload; + // Reset to page 1 when filters change + state.pagination.page = 1; + }, + updateFilter: ( + state, + action: PayloadAction<{ field: string; value: FilterField | undefined }> + ) => { + const { field, value } = action.payload; + if (value === undefined || value === "") { + delete state.filters[field]; + } else { + state.filters[field] = value; + } + // Reset to page 1 when a filter changes + state.pagination.page = 1; + }, + clearFilters: state => { + state.filters = {}; + state.pagination.page = 1; + }, + setPagination: ( + state, + action: PayloadAction<{ page: number; limit: number }> + ) => { + state.pagination = action.payload; + }, + setSort: ( + state, + action: PayloadAction< + { field: string; order: "asc" | "desc" } | undefined + > + ) => { + state.sort = action.payload; + }, + setStatus: (state, action: PayloadAction) => { + state.status = action.payload; + if (action.payload === "loading") { + state.error = null; + } + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload; + if (action.payload) { + state.status = "failed"; + } + }, + }, }); +export const { + setFilters, + updateFilter, + clearFilters, + setPagination, + setSort, + setStatus, + setError, +} = advancedSearchSlice.actions; + export default advancedSearchSlice.reducer; diff --git a/app/redux/advanedSearch/selectors.ts b/app/redux/advanedSearch/selectors.ts new file mode 100644 index 0000000..70f2288 --- /dev/null +++ b/app/redux/advanedSearch/selectors.ts @@ -0,0 +1,24 @@ +import { RootState } from "../store"; +import { + AdvancedSearchFilters, + FilterField, + FetchStatus, +} from "./advancedSearchSlice"; + +export const selectFilters = (state: RootState): AdvancedSearchFilters => + state.advancedSearch.filters; + +export const selectPagination = (state: RootState) => + state.advancedSearch.pagination; + +export const selectSort = (state: RootState) => state.advancedSearch.sort; + +export const selectFilterValue = ( + state: RootState, + field: string +): FilterField | undefined => state.advancedSearch.filters[field]; + +export const selectStatus = (state: RootState): FetchStatus => + state.advancedSearch.status; + +export const selectError = (state: RootState) => state.advancedSearch.error; diff --git a/app/redux/metadata/metadataSlice.ts b/app/redux/metadata/metadataSlice.ts index fbc3518..496c652 100644 --- a/app/redux/metadata/metadataSlice.ts +++ b/app/redux/metadata/metadataSlice.ts @@ -1,21 +1,30 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; -export interface SidebarItem { +export interface SidebarLink { id?: string; title: string; path: string; - icon?: string; // icon name from backend; map client-side if needed - permissions?: string[]; // required permissions for visibility - children?: SidebarItem[]; + icon?: string; + groups?: string[]; + children?: SidebarLink[]; } +export interface SidebarPayload { + links: SidebarLink[]; +} + +export type FieldGroupMap = Record>; + export interface AppMetadata { - groups?: string[]; - job_titles?: string[]; - merchants?: string[]; - message?: string; - sidebar?: SidebarItem[]; success: boolean; + message?: string; + field_names?: FieldGroupMap; + job_titles?: string[]; + groups?: string[]; + merchants?: string[]; + countries?: string[]; + sidebar?: SidebarPayload; + transaction_status?: string[]; } interface MetadataState { diff --git a/app/redux/metadata/selectors.ts b/app/redux/metadata/selectors.ts index 36adaaf..b70e490 100644 --- a/app/redux/metadata/selectors.ts +++ b/app/redux/metadata/selectors.ts @@ -1,7 +1,35 @@ import { RootState } from "../store"; +import { FieldGroupMap, SidebarLink } from "./metadataSlice"; -// Selectors export const selectMetadataState = (state: RootState) => state.metadata; -export const selectAppMetadata = (state: RootState) => state.metadata.data; -export const selectNavigationSidebar = (state: RootState) => - state.metadata.data?.sidebar || []; + +export const selectMetadataStatus = (state: RootState) => + state.metadata?.status; + +export const selectMetadataError = (state: RootState) => state.metadata?.error; + +export const selectAppMetadata = (state: RootState) => state.metadata?.data; + +export const selectFieldNames = (state: RootState): FieldGroupMap | undefined => + state.metadata.data?.field_names; + +export const selectSidebarLinks = (state: RootState): SidebarLink[] => + state.metadata.data?.sidebar?.links ?? []; + +export const selectJobTitles = (state: RootState): string[] => + state.metadata.data?.job_titles ?? []; + +export const selectGroups = (state: RootState): string[] => + state.metadata.data?.groups ?? []; + +export const selectMerchants = (state: RootState): string[] => + state.metadata.data?.merchants ?? []; + +export const selectCountries = (state: RootState): string[] => + state.metadata.data?.countries ?? []; + +export const selectTransactionStatuses = (state: RootState): string[] => + state.metadata.data?.transaction_status ?? []; + +export const selectNavigationSidebar = (state: RootState): SidebarLink[] => + state.metadata.data?.sidebar?.links ?? []; diff --git a/app/redux/types.ts b/app/redux/types.ts index 1194c62..aed2e52 100644 --- a/app/redux/types.ts +++ b/app/redux/types.ts @@ -7,6 +7,25 @@ export type AppDispatch = typeof store.dispatch; export type ThunkSuccess = { message: string } & T; export type ThunkError = string; +//User related types +export type TGroupName = "Super Admin" | "Admin" | "Reader"; + +export type TJobTitle = + | "C-Level" + | "Admin" + | "Operations" + | "Support" + | "KYC" + | "Payments" + | "Risk" + | "Finance" + | "Trading" + | "Compliance" + | "DevOps" + | "Software Engineer"; + +export type TMerchantName = "Data Spin" | "Win Bot"; + export interface IUserResponse { success: boolean; token: string; @@ -17,6 +36,8 @@ export interface IUserResponse { lastName: string | null; merchantId?: number | null; username?: string | null; + groups?: TGroupName[]; + merchants?: TMerchantName[]; phone?: string | null; jobTitle?: string | null; enabled?: boolean; diff --git a/app/services/audits.ts b/app/services/audits.ts index ff8a81b..9723e26 100644 --- a/app/services/audits.ts +++ b/app/services/audits.ts @@ -1,18 +1,41 @@ -export async function getAudits({ query }: { query: string }) { - const res = await fetch( - `http://localhost:4000/api/dashboard/audits?${query}`, +interface GetAuditsParams { + limit?: number; + page?: number; + sort?: string; + filter?: string; + signal?: AbortSignal; +} + +export async function getAudits({ + limit, + page, + sort, + filter, + signal, +}: GetAuditsParams = {}) { + const params = new URLSearchParams(); + + if (limit) params.set("limit", String(limit)); + if (page) params.set("page", String(page)); + if (sort) params.set("sort", sort); + if (filter) params.set("filter", filter); + + const queryString = params.toString(); + const response = await fetch( + `/api/dashboard/audits${queryString ? `?${queryString}` : ""}`, { + method: "GET", cache: "no-store", + signal, } ); - if (!res.ok) { - // Handle error from the API - const errorData = await res + if (!response.ok) { + const errorData = await response .json() .catch(() => ({ message: "Unknown error" })); - throw new Error(errorData.message || `HTTP error! status: ${res.status}`); + throw new Error(errorData.message || "Failed to fetch audits"); } - return res.json(); + return response.json(); } diff --git a/app/utils/iconMap.ts b/app/utils/iconMap.ts index f91ce5a..014b96c 100644 --- a/app/utils/iconMap.ts +++ b/app/utils/iconMap.ts @@ -18,9 +18,16 @@ import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import HistoryIcon from "@mui/icons-material/History"; import FactCheckIcon from "@mui/icons-material/FactCheck"; import WidgetsIcon from "@mui/icons-material/Widgets"; // fallback +import GroupIcon from "@mui/icons-material/Group"; +import SecurityIcon from "@mui/icons-material/Security"; +import TimerIcon from "@mui/icons-material/Timer"; +import StoreIcon from "@mui/icons-material/Store"; +import AccountBalanceIcon from "@mui/icons-material/AccountBalance"; +import PaymentIcon from "@mui/icons-material/Payment"; +import CurrencyExchangeIcon from "@mui/icons-material/CurrencyExchange"; +import ViewSidebarIcon from "@mui/icons-material/ViewSidebar"; -// Map string keys from backend to actual Icon components -const iconRegistry: Record = { +const IconMap = { HomeIcon, AccountBalanceWalletIcon, CheckCircleIcon, @@ -37,6 +44,21 @@ const iconRegistry: Record = { ArrowUpwardIcon, HistoryIcon, FactCheckIcon, + WidgetsIcon, + GroupIcon, + SecurityIcon, + TimerIcon, + StoreIcon, + AccountBalanceIcon, + PaymentIcon, + CurrencyExchangeIcon, + ViewSidebarIcon, +}; + +export default IconMap; +// Map string keys from backend to actual Icon components +const iconRegistry: Record = { + ...IconMap, }; export function resolveIcon(