From 5638d02793fb2c68648186174a3aa52d2b4f76cc Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Mon, 17 Nov 2025 12:15:20 +0100 Subject: [PATCH] Hooked up transaction with filtering --- app/AuthBootstrap.tsx | 1 - app/api/dashboard/admin/users/[id]/route.ts | 1 - .../dashboard/transactions/all/mockData.ts | 264 ---------- app/api/dashboard/transactions/all/route.ts | 84 --- app/api/dashboard/transactions/route.ts | 99 ++++ app/api/metadata/route.ts | 2 - app/dashboard/transactions/all/page.tsx | 120 ++++- .../AdvancedSearch/AdvancedSearch.tsx | 483 ++++++++++++------ app/features/AdvancedSearch/utils/utils.ts | 54 ++ app/features/DataTable/DataTable.tsx | 317 +++--------- app/features/DataTable/DataTableHeader.tsx | 51 ++ app/features/DataTable/constants.ts | 37 ++ app/features/UserRoles/userRoleCard.tsx | 2 +- .../dashboard/header/dropDown/DropDown.tsx | 2 - .../advanedSearch/advancedSearchSlice.ts | 135 +++-- app/redux/advanedSearch/selectors.ts | 24 + app/redux/types.ts | 21 + 17 files changed, 898 insertions(+), 799 deletions(-) delete mode 100644 app/api/dashboard/transactions/all/mockData.ts delete mode 100644 app/api/dashboard/transactions/all/route.ts create mode 100644 app/api/dashboard/transactions/route.ts create mode 100644 app/features/AdvancedSearch/utils/utils.ts create mode 100644 app/features/DataTable/DataTableHeader.tsx create mode 100644 app/features/DataTable/constants.ts create mode 100644 app/redux/advanedSearch/selectors.ts 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/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/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/transactions/all/page.tsx b/app/dashboard/transactions/all/page.tsx index 17e12eb..7e6165e 100644 --- a/app/dashboard/transactions/all/page.tsx +++ b/app/dashboard/transactions/all/page.tsx @@ -1,23 +1,105 @@ +"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, + selectStatus, + selectError, +} from "@/app/redux/advanedSearch/selectors"; +import { GridColDef } from "@mui/x-data-grid"; +import Spinner from "@/app/components/Spinner/Spinner"; +import { AppDispatch } from "@/app/redux/store"; +import { + setStatus, + setError as setAdvancedSearchError, +} from "@/app/redux/advanedSearch/advancedSearchSlice"; -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 }); +import { + TABLE_COLUMNS, + TABLE_SEARCH_LABELS, +} from "@/app/features/DataTable/constants"; - return ; +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 default function AllTransactionPage() { + const dispatch = useDispatch(); + const filters = useSelector(selectFilters); + const pagination = useSelector(selectPagination); + const sort = useSelector(selectSort); + + 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(setStatus("loading")); + 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: any) => ({ + id: tx.id, + 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); + dispatch(setStatus("succeeded")); + } catch (error) { + dispatch( + setAdvancedSearchError( + error instanceof Error ? error.message : "Unknown error" + ) + ); + setTableRows([]); + } + }; + + fetchData(); + }, [dispatch, filters, pagination, sort]); + + return ; } 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)} + handleSave={handleStatusSave} + /> + + ); }; -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/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/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..773f0a9 100644 --- a/app/features/dashboard/header/dropDown/DropDown.tsx +++ b/app/features/dashboard/header/dropDown/DropDown.tsx @@ -24,8 +24,6 @@ export default function SidebarDropdown({ onChange }: Props) { onChange?.(event); }; - console.log("sidebar", sidebar); - return ( Navigate To 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/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;