From 277c7e15b90a750758cb62cb0e9d2f6e580702c6 Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Thu, 27 Nov 2025 09:09:43 +0100 Subject: [PATCH] Changed Audits to use SSR and SWR --- app/api/auth/change-password/route.ts | 8 +- app/api/auth/login/route.tsx | 6 +- app/api/auth/logout/route.tsx | 8 +- app/api/auth/register/route.ts | 6 +- app/api/auth/reset-password/[id]/route.ts | 6 +- app/api/auth/validate/route.ts | 6 +- app/api/dashboard/admin/groups/route.ts | 6 +- app/api/dashboard/admin/permissions/route.ts | 6 +- app/api/dashboard/admin/sessions/route.ts | 6 +- app/api/dashboard/admin/users/[id]/route.ts | 8 +- app/api/dashboard/admin/users/route.ts | 7 +- app/api/dashboard/audits/route.ts | 19 +- app/api/dashboard/transactions/[id]/route.ts | 7 +- .../dashboard/transactions/deposits/route.ts | 5 +- app/api/dashboard/transactions/route.ts | 9 +- .../transactions/withdrawal/mockData.ts | 243 ---------- .../transactions/withdrawal/route.ts | 76 +-- .../transactions/withdrawals/route.ts | 5 +- app/api/metadata/route.ts | 2 +- app/dashboard/admin/page.tsx | 10 - app/dashboard/audits/AuditTableClient.tsx | 214 +++++++++ app/dashboard/audits/auditTransforms.ts | 137 ++++++ app/dashboard/audits/page.tsx | 374 +++------------ app/dashboard/transactions/deposits/page.tsx | 2 +- .../AdvancedSearch/AdvancedSearch.tsx | 1 - app/features/DataTable/DataTable.tsx | 84 +--- app/features/DataTable/StatusChangeDialog.tsx | 87 +++- app/services/audits.ts | 42 +- app/services/constants copy.ts | 0 app/services/constants.ts | 5 + app/services/types.ts | 8 + middleware.ts | 9 +- package-lock.json | 153 ++++++ package.json | 1 + yarn.lock | 454 +++--------------- 35 files changed, 804 insertions(+), 1216 deletions(-) delete mode 100644 app/api/dashboard/transactions/withdrawal/mockData.ts delete mode 100644 app/dashboard/admin/page.tsx create mode 100644 app/dashboard/audits/AuditTableClient.tsx create mode 100644 app/dashboard/audits/auditTransforms.ts create mode 100644 app/services/constants copy.ts create mode 100644 app/services/constants.ts create mode 100644 app/services/types.ts diff --git a/app/api/auth/change-password/route.ts b/app/api/auth/change-password/route.ts index 399aaa4..4bf1712 100644 --- a/app/api/auth/change-password/route.ts +++ b/app/api/auth/change-password/route.ts @@ -1,9 +1,7 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; import { decodeJwt } from "jose"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; -const COOKIE_NAME = "auth_token"; - export async function POST(request: Request) { try { const { email, currentPassword, newPassword } = await request.json(); @@ -11,7 +9,7 @@ export async function POST(request: Request) { // Get the auth token from cookies first const { cookies } = await import("next/headers"); const cookieStore = cookies(); - const token = (await cookieStore).get(COOKIE_NAME)?.value; + const token = (await cookieStore).get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -99,7 +97,7 @@ export async function POST(request: Request) { } catch {} response.cookies.set({ - name: COOKIE_NAME, + name: AUTH_COOKIE_NAME, value: newToken, httpOnly: true, secure: process.env.NODE_ENV === "production", diff --git a/app/api/auth/login/route.tsx b/app/api/auth/login/route.tsx index de83096..3615eed 100644 --- a/app/api/auth/login/route.tsx +++ b/app/api/auth/login/route.tsx @@ -1,10 +1,8 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; import { cookies } from "next/headers"; import { decodeJwt } from "jose"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; -const COOKIE_NAME = "auth_token"; - export async function POST(request: Request) { try { const { email, password } = await request.json(); @@ -59,7 +57,7 @@ export async function POST(request: Request) { // Set the cookie const cookieStore = await cookies(); - cookieStore.set(COOKIE_NAME, token, { + cookieStore.set(AUTH_COOKIE_NAME, token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", diff --git a/app/api/auth/logout/route.tsx b/app/api/auth/logout/route.tsx index 0ba3c09..002d1e0 100644 --- a/app/api/auth/logout/route.tsx +++ b/app/api/auth/logout/route.tsx @@ -1,12 +1,10 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:3000"; -const COOKIE_NAME = "auth_token"; - export async function DELETE() { const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (token) { try { @@ -23,6 +21,6 @@ export async function DELETE() { } } - cookieStore.delete(COOKIE_NAME); + cookieStore.delete(AUTH_COOKIE_NAME); return NextResponse.json({ success: true, message: "Logged out" }); } diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 31982d3..6a0c0c8 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,9 +1,7 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; -const COOKIE_NAME = "auth_token"; - // Interface matching the backend RegisterRequest and frontend IEditUserForm interface RegisterRequest { creator: string; @@ -23,7 +21,7 @@ export async function POST(request: Request) { // Get the auth token from cookies const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/auth/reset-password/[id]/route.ts b/app/api/auth/reset-password/[id]/route.ts index 75c63bd..3aecaa4 100644 --- a/app/api/auth/reset-password/[id]/route.ts +++ b/app/api/auth/reset-password/[id]/route.ts @@ -1,11 +1,9 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse, type NextRequest } from "next/server"; import { cookies } from "next/headers"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; -const COOKIE_NAME = "auth_token"; - export async function PUT( request: NextRequest, context: { params: Promise<{ id: string }> } @@ -21,7 +19,7 @@ export async function PUT( } const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/auth/validate/route.ts b/app/api/auth/validate/route.ts index ea6b10b..bf002c1 100644 --- a/app/api/auth/validate/route.ts +++ b/app/api/auth/validate/route.ts @@ -1,12 +1,10 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; -const COOKIE_NAME = "auth_token"; - export async function POST() { const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json({ valid: false }, { status: 401 }); diff --git a/app/api/dashboard/admin/groups/route.ts b/app/api/dashboard/admin/groups/route.ts index a7c6daa..d888cb8 100644 --- a/app/api/dashboard/admin/groups/route.ts +++ b/app/api/dashboard/admin/groups/route.ts @@ -1,14 +1,12 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextRequest, NextResponse } from "next/server"; import { buildFilterParam } from "../utils"; -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; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/dashboard/admin/permissions/route.ts b/app/api/dashboard/admin/permissions/route.ts index 8fb217d..52ef89b 100644 --- a/app/api/dashboard/admin/permissions/route.ts +++ b/app/api/dashboard/admin/permissions/route.ts @@ -1,14 +1,12 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextRequest, NextResponse } from "next/server"; import { buildFilterParam } from "../utils"; -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; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/dashboard/admin/sessions/route.ts b/app/api/dashboard/admin/sessions/route.ts index 17049c6..5b993f1 100644 --- a/app/api/dashboard/admin/sessions/route.ts +++ b/app/api/dashboard/admin/sessions/route.ts @@ -1,14 +1,12 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextRequest, NextResponse } from "next/server"; import { buildFilterParam } from "../utils"; -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; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/dashboard/admin/users/[id]/route.ts b/app/api/dashboard/admin/users/[id]/route.ts index 9a9c5df..6950f2e 100644 --- a/app/api/dashboard/admin/users/[id]/route.ts +++ b/app/api/dashboard/admin/users/[id]/route.ts @@ -1,9 +1,7 @@ // app/api/users/[id]/route.ts +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; -const COOKIE_NAME = "auth_token"; - // Field mapping: snake_case input -> { snake_case for data, PascalCase for fields } // Matches API metadata field_names.users mapping const FIELD_MAPPING: Record = { @@ -75,7 +73,7 @@ export async function PUT( // Get the auth token from cookies const { cookies } = await import("next/headers"); const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -114,7 +112,7 @@ export async function DELETE( const { id } = await context.params; const { cookies } = await import("next/headers"); const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/dashboard/admin/users/route.ts b/app/api/dashboard/admin/users/route.ts index 759225a..ee5764a 100644 --- a/app/api/dashboard/admin/users/route.ts +++ b/app/api/dashboard/admin/users/route.ts @@ -1,13 +1,11 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; -const COOKIE_NAME = "auth_token"; - export async function GET(request: Request) { try { const { cookies } = await import("next/headers"); const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -29,6 +27,7 @@ export async function GET(request: Request) { }); const data = await response.json(); + return NextResponse.json(data, { status: response.status }); } catch (err: unknown) { console.error("Proxy GET /api/v1/users/list error:", err); diff --git a/app/api/dashboard/audits/route.ts b/app/api/dashboard/audits/route.ts index dfe76b8..93f0336 100644 --- a/app/api/dashboard/audits/route.ts +++ b/app/api/dashboard/audits/route.ts @@ -1,11 +1,6 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextRequest, NextResponse } from "next/server"; -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"; @@ -13,7 +8,7 @@ export async function GET(request: NextRequest) { try { const { cookies } = await import("next/headers"); const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -38,7 +33,7 @@ export async function GET(request: NextRequest) { proxiedParams.set("page", DEFAULT_PAGE); } - const backendUrl = `${AUDITS_BASE_URL}/api/v1/audit${ + const backendUrl = `${BE_BASE_URL}/api/v1/audit${ proxiedParams.size ? `?${proxiedParams.toString()}` : "" }`; @@ -48,7 +43,10 @@ export async function GET(request: NextRequest) { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - cache: "no-store", + next: { + revalidate: 60, + tags: ["audits"], + }, }); if (!response.ok) { @@ -65,11 +63,8 @@ export async function GET(request: NextRequest) { } 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( diff --git a/app/api/dashboard/transactions/[id]/route.ts b/app/api/dashboard/transactions/[id]/route.ts index 2702f90..adfd133 100644 --- a/app/api/dashboard/transactions/[id]/route.ts +++ b/app/api/dashboard/transactions/[id]/route.ts @@ -1,8 +1,6 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; 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, context: { params: Promise<{ id: string }> } @@ -11,7 +9,7 @@ export async function PUT( const { id } = await context.params; const { cookies } = await import("next/headers"); const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -32,7 +30,6 @@ export async function PUT( }); 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"; diff --git a/app/api/dashboard/transactions/deposits/route.ts b/app/api/dashboard/transactions/deposits/route.ts index d294c7c..110fb44 100644 --- a/app/api/dashboard/transactions/deposits/route.ts +++ b/app/api/dashboard/transactions/deposits/route.ts @@ -1,6 +1,5 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextRequest, NextResponse } from "next/server"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; -const COOKIE_NAME = "auth_token"; type FilterValue = | string @@ -13,7 +12,7 @@ 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 token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/dashboard/transactions/route.ts b/app/api/dashboard/transactions/route.ts index c297e66..19e55a1 100644 --- a/app/api/dashboard/transactions/route.ts +++ b/app/api/dashboard/transactions/route.ts @@ -1,13 +1,11 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; 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; + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( @@ -100,8 +98,6 @@ export async function POST(request: NextRequest) { 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: { @@ -125,7 +121,6 @@ export async function POST(request: NextRequest) { } const data = await response.json(); - console.log("[DEBUG] [TRANSACTIONS] Response data:", data); return NextResponse.json(data, { status: response.status }); } catch (err: unknown) { diff --git a/app/api/dashboard/transactions/withdrawal/mockData.ts b/app/api/dashboard/transactions/withdrawal/mockData.ts deleted file mode 100644 index 48c8fc8..0000000 --- a/app/api/dashboard/transactions/withdrawal/mockData.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { GridColDef } from "@mui/x-data-grid"; - -export const withdrawalTransactionDummyData = [ - { - id: 1, - userId: 17, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, - { - id: 2, - userId: 17, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, - { - id: 3, - userId: 17, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Completed", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-18 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - }, - { - id: 4, - userId: 19, - transactionId: 1049136973, - withdrawalMethod: "Bank Transfer", - status: "Completed", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-18 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - }, - { - id: 5, - userId: 19, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, - { - id: 6, - userId: 27, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, - { - id: 7, - userId: 1, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, - { - id: 8, - userId: 172, - transactionId: 1049131973, - withdrawalMethod: "Card", - status: "Pending", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-12 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - }, - { - id: 9, - userId: 174, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Inprogress", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - }, - { - id: 10, - userId: 1, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, - { - id: 11, - userId: 1, - transactionId: 1049131973, - withdrawalMethod: "Bank Transfer", - status: "Error", - options: [ - { value: "Pending", label: "Pending" }, - { value: "Completed", label: "Completed" }, - { value: "Inprogress", label: "Inprogress" }, - { value: "Error", label: "Error" }, - ], - amount: 4000, - dateTime: "2025-06-17 10:10:30", - errorInfo: "-", - fraudScore: "frad score 1234", - manualCorrectionFlag: "-", - informationWhoApproved: "-", - }, -]; - -export const withdrawalTransactionsColumns: GridColDef[] = [ - { field: "userId", headerName: "User ID", width: 130 }, - { field: "transactionId", headerName: "Transaction ID", width: 130 }, - { field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 }, - { field: "status", headerName: "Status", width: 130 }, - { field: "actions", headerName: "Actions", width: 150 }, - { field: "amount", headerName: "Amount", width: 130 }, - { field: "dateTime", headerName: "Date / Time", width: 130 }, - { field: "errorInfo", headerName: "Error Info", width: 130 }, - { field: "fraudScore", headerName: "Fraud Score", width: 130 }, - { - field: "manualCorrectionFlag", - headerName: "Manual Correction Flag", - width: 130, - }, - { - field: "informationWhoApproved", - headerName: "Information who approved", - width: 130, - }, -]; - -export const withdrawalTransactionsSearchLabels = [ - { - label: "Status", - field: "status", - type: "select", - options: ["Pending", "Inprogress", "Completed", "Failed"], - }, - { - label: "Payment Method", - field: "depositMethod", - type: "select", - options: ["Card", "Bank Transfer"], - }, - { label: "Date / Time", field: "dateTime", type: "date" }, -]; diff --git a/app/api/dashboard/transactions/withdrawal/route.ts b/app/api/dashboard/transactions/withdrawal/route.ts index 52c8d3c..fa61a38 100644 --- a/app/api/dashboard/transactions/withdrawal/route.ts +++ b/app/api/dashboard/transactions/withdrawal/route.ts @@ -1,67 +1,13 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - withdrawalTransactionDummyData, - withdrawalTransactionsColumns, - withdrawalTransactionsSearchLabels, -} from "./mockData"; +import { NextResponse } from "next/server"; -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - - const userId = searchParams.get("userId"); - const status = searchParams.get("status"); - const withdrawalMethod = searchParams.get("withdrawalMethod"); - - const dateTimeStart = searchParams.get("dateTime_start"); - const dateTimeEnd = searchParams.get("dateTime_end"); - - let filteredTransactions = [...withdrawalTransactionDummyData]; - - if (userId) { - filteredTransactions = filteredTransactions.filter( - tx => tx.userId.toString() === userId - ); - } - - if (status) { - filteredTransactions = filteredTransactions.filter( - tx => tx.status.toLowerCase() === status.toLowerCase() - ); - } - - if (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; - }); - } - - if (withdrawalMethod) { - filteredTransactions = filteredTransactions.filter( - tx => tx.withdrawalMethod.toLowerCase() === withdrawalMethod.toLowerCase() - ); - } - - return NextResponse.json({ - tableRows: filteredTransactions, - tableSearchLabels: withdrawalTransactionsSearchLabels, - tableColumns: withdrawalTransactionsColumns, - }); +/** + * Placeholder Whitdrawal API route. + * Keeps the module valid while the real implementation + * is being built, and makes the intent obvious to clients. + */ +export async function GET() { + return NextResponse.json( + { message: "Settings endpoint not implemented" }, + { status: 501 } + ); } diff --git a/app/api/dashboard/transactions/withdrawals/route.ts b/app/api/dashboard/transactions/withdrawals/route.ts index eafbad8..19bc5ed 100644 --- a/app/api/dashboard/transactions/withdrawals/route.ts +++ b/app/api/dashboard/transactions/withdrawals/route.ts @@ -1,6 +1,5 @@ +import { AUTH_COOKIE_NAME, BE_BASE_URL } from "@/app/services/constants"; import { NextRequest, NextResponse } from "next/server"; -const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; -const COOKIE_NAME = "auth_token"; type FilterValue = | string @@ -13,7 +12,7 @@ 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 token = cookieStore.get(AUTH_COOKIE_NAME)?.value; if (!token) { return NextResponse.json( diff --git a/app/api/metadata/route.ts b/app/api/metadata/route.ts index 113b8f4..911581f 100644 --- a/app/api/metadata/route.ts +++ b/app/api/metadata/route.ts @@ -1,3 +1,4 @@ +import { BE_BASE_URL } from "@/app/services/constants"; import { NextResponse } from "next/server"; // Proxy to backend metadata endpoint. Assumes BACKEND_BASE_URL is set. @@ -5,7 +6,6 @@ export async function GET() { const { cookies } = await import("next/headers"); const cookieStore = await cookies(); const token = cookieStore.get("auth_token")?.value; - const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; if (!token) { return NextResponse.json( diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx deleted file mode 100644 index 4e7a55d..0000000 --- a/app/dashboard/admin/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -export default function BackOfficeUsersPage() { - return ( -
- {/* This page will now be rendered on the client-side */} - hello -
- ); -} diff --git a/app/dashboard/audits/AuditTableClient.tsx b/app/dashboard/audits/AuditTableClient.tsx new file mode 100644 index 0000000..6bc3c73 --- /dev/null +++ b/app/dashboard/audits/AuditTableClient.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + DataGrid, + GridColDef, + GridPaginationModel, + GridSortModel, +} from "@mui/x-data-grid"; +import TextField from "@mui/material/TextField"; +import { Box, debounce } from "@mui/material"; +import useSWR from "swr"; +import { getAudits } from "@/app/services/audits"; +import { + AuditQueryResult, + AuditRow, + DEFAULT_PAGE_SIZE, +} from "./auditTransforms"; + +const FALLBACK_COLUMNS: GridColDef[] = [ + { + field: "placeholder", + headerName: "Audit Data", + flex: 1, + sortable: false, + filterable: false, + }, +]; + +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, + })); +}; + +interface AuditTableClientProps { + initialData: AuditQueryResult; +} + +const ENTITY_PREFIX = "LIKE/"; + +const buildSortParam = (sortModel: GridSortModel) => + sortModel.length && sortModel[0].field && sortModel[0].sort + ? `${sortModel[0].field}:${sortModel[0].sort}` + : undefined; + +const buildEntityParam = (entitySearch: string) => + entitySearch.trim() ? `${ENTITY_PREFIX}${entitySearch.trim()}` : undefined; + +export default function AuditTableClient({ + initialData, +}: AuditTableClientProps) { + const [paginationModel, setPaginationModel] = useState({ + page: initialData.pageIndex ?? 0, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [sortModel, setSortModel] = useState([]); + const [entitySearch, setEntitySearch] = useState(""); + const [entitySearchInput, setEntitySearchInput] = useState(""); + + const [columns, setColumns] = useState( + initialData.rows.length ? deriveColumns(initialData.rows) : FALLBACK_COLUMNS + ); + + const debouncedSetEntitySearch = useMemo( + () => + debounce((value: string) => { + setEntitySearch(value); + setPaginationModel(prev => ({ ...prev, page: 0 })); + }, 500), + [] + ); + + useEffect(() => { + return () => { + debouncedSetEntitySearch.clear(); + }; + }, [debouncedSetEntitySearch]); + + const sortParam = useMemo(() => buildSortParam(sortModel), [sortModel]); + const entityParam = useMemo( + () => buildEntityParam(entitySearch), + [entitySearch] + ); + + const { data, error, isLoading, isValidating } = useSWR( + [ + "audits", + paginationModel.page, + paginationModel.pageSize, + sortParam ?? "", + entityParam ?? "", + ], + () => + getAudits({ + limit: paginationModel.pageSize, + page: paginationModel.page + 1, + sort: sortParam, + entity: entityParam, + }), + { + keepPreviousData: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + fallbackData: initialData, + } + ); + + const rows = data?.rows ?? initialData.rows; + const rowCount = data?.total ?? initialData.total ?? rows.length; + const loading = isLoading || (isValidating && !rows.length); + const pageTitle = useMemo( + () => + sortModel.length && sortModel[0].field + ? `Audit Logs · sorted by ${toTitle(sortModel[0].field)}` + : "Audit Logs", + [sortModel] + ); + + useEffect(() => { + setColumns(prev => + rows.length ? deriveColumns(rows) : prev.length ? prev : FALLBACK_COLUMNS + ); + }, [rows]); + + const handlePaginationChange = (model: GridPaginationModel) => { + setPaginationModel(model); + }; + + const handleSortModelChange = (model: GridSortModel) => { + setSortModel(model); + setPaginationModel(prev => ({ ...prev, page: 0 })); + }; + + const handleEntitySearchChange = (value: string) => { + setEntitySearchInput(value); + debouncedSetEntitySearch(value); + }; + + const errorMessage = + error instanceof Error + ? error.message + : error + ? "Failed to load audits" + : null; + + return ( +
+ + handleEntitySearchChange(e.target.value)} + sx={{ width: 300, backgroundColor: "#f0f0f0" }} + /> + +

{pageTitle}

+ {errorMessage && ( +
+ {errorMessage} +
+ )} +
+
+
+ +
+
+
+
+ ); +} diff --git a/app/dashboard/audits/auditTransforms.ts b/app/dashboard/audits/auditTransforms.ts new file mode 100644 index 0000000..77cbf9f --- /dev/null +++ b/app/dashboard/audits/auditTransforms.ts @@ -0,0 +1,137 @@ +export type AuditRow = Record & { id: string | number }; + +export 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 }; +} + +export interface AuditQueryResult { + rows: AuditRow[]; + total: number; + payload: AuditApiResponse; + pageIndex: number; +} + +export const DEFAULT_PAGE_SIZE = 25; +const CANDIDATE_ARRAY_KEYS: (keyof AuditApiResponse)[] = [ + "items", + "audits", + "logs", + "results", + "records", +]; + +export 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); +}; + +export 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 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 []; +}; + +export 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 + ); +}; + +export const normalizeRows = ( + entries: unknown[], + pageIndex: number +): AuditRow[] => + entries.map((entry, index) => { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return { + id: `${pageIndex}-${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 ?? + `${pageIndex}-${index}`; + + return { + id: (identifier as string | number) ?? `${pageIndex}-${index}`, + ...normalized, + }; + }); diff --git a/app/dashboard/audits/page.tsx b/app/dashboard/audits/page.tsx index 5217065..b672cca 100644 --- a/app/dashboard/audits/page.tsx +++ b/app/dashboard/audits/page.tsx @@ -1,332 +1,70 @@ -"use client"; - -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"; -import TextField from "@mui/material/TextField"; -import { Box, debounce } from "@mui/material"; -type AuditRow = Record & { id: string | number }; +import AuditTableClient from "./AuditTableClient"; +import { + AuditApiResponse, + AuditQueryResult, + DEFAULT_PAGE_SIZE, + extractArray, + normalizeRows, + resolveTotal, +} from "./auditTransforms"; +import { cookies } from "next/headers"; +import { + AUDIT_CACHE_TAG, + AUTH_COOKIE_NAME, + BE_BASE_URL, + REVALIDATE_SECONDS, +} from "@/app/services/constants"; -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 }; -} +async function fetchInitialAudits(): Promise { + const cookieStore = await cookies(); + const token = cookieStore.get(AUTH_COOKIE_NAME)?.value; -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 (!token) { + throw new Error("Missing auth token"); } - if (typeof value === "string" || typeof value === "number") { - return value; - } + const params = new URLSearchParams(); + params.set("limit", DEFAULT_PAGE_SIZE.toString()); + params.set("page", "1"); - if (typeof value === "boolean") { - return value ? "true" : "false"; - } + const backendUrl = `${BE_BASE_URL}/api/v1/audit${ + params.size ? `?${params.toString()}` : "" + }`; - 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 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, - }; + const response = await fetch(backendUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + next: { + revalidate: REVALIDATE_SECONDS, + tags: [AUDIT_CACHE_TAG], + }, }); -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([]); - const [entitySearch, setEntitySearch] = useState(""); - const [entitySearchInput, setEntitySearchInput] = useState(""); + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ message: "Failed to fetch audits" })); + throw new Error(errorData?.message || "Failed to fetch audits"); + } - useEffect(() => { - const controller = new AbortController(); + const payload = (await response.json()) as AuditApiResponse; + const rows = normalizeRows(extractArray(payload), 0); + const total = resolveTotal(payload, rows.length); - const fetchAudits = async () => { - setLoading(true); - setError(null); - - const sortParam = - sortModel.length && sortModel[0].field && sortModel[0].sort - ? `${sortModel[0].field}:${sortModel[0].sort}` - : undefined; - - const entityParam = entitySearch.trim() - ? `LIKE/${entitySearch.trim()}` - : undefined; - - try { - const payload = (await getAudits({ - limit: paginationModel.pageSize, - page: paginationModel.page + 1, - sort: sortParam, - entity: entityParam, - 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, entitySearch]); - - const handlePaginationChange = (model: GridPaginationModel) => { - setPaginationModel(model); + return { + rows, + total, + payload, + pageIndex: 0, }; - - const handleSortModelChange = (model: GridSortModel) => { - setSortModel(model); - setPaginationModel(prev => ({ ...prev, page: 0 })); - }; - - const debouncedSetEntitySearch = useMemo( - () => - debounce((value: string) => { - setEntitySearch(value); - setPaginationModel(prev => ({ ...prev, page: 0 })); - }, 500), - [] - ); - - useEffect(() => { - return () => { - debouncedSetEntitySearch.clear(); - }; - }, [debouncedSetEntitySearch]); - - const handleEntitySearchChange = (value: string) => { - setEntitySearchInput(value); - debouncedSetEntitySearch(value); - }; - - const pageTitle = useMemo( - () => - sortModel.length && sortModel[0].field - ? `Audit Logs · sorted by ${toTitle(sortModel[0].field)}` - : "Audit Logs", - [sortModel] - ); - - return ( -
- - handleEntitySearchChange(e.target.value)} - sx={{ width: 300, backgroundColor: "#f0f0f0" }} - /> - -

{pageTitle}

- {error && ( -
- {error} -
- )} -
-
-
- -
-
-
-
- ); +} + +export default async function AuditPage() { + const initialData = await fetchInitialAudits(); + return ; } diff --git a/app/dashboard/transactions/deposits/page.tsx b/app/dashboard/transactions/deposits/page.tsx index 97d269f..8c660e5 100644 --- a/app/dashboard/transactions/deposits/page.tsx +++ b/app/dashboard/transactions/deposits/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useMemo, useState } from "react"; import DataTable from "@/app/features/DataTable/DataTable"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch } from "@/app/redux/store"; @@ -12,7 +13,6 @@ import { setStatus, setError as setAdvancedSearchError, } from "@/app/redux/advanedSearch/advancedSearchSlice"; -import { useEffect, useMemo, useState } from "react"; import { TransactionRow, BackendTransaction } from "../interface"; export default function DepositTransactionPage() { diff --git a/app/features/AdvancedSearch/AdvancedSearch.tsx b/app/features/AdvancedSearch/AdvancedSearch.tsx index 97088f1..16b2453 100644 --- a/app/features/AdvancedSearch/AdvancedSearch.tsx +++ b/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -45,7 +45,6 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) { const [operators, setOperators] = useState>({}); const conditionOperators = useSelector(selectConditionOperators); - console.log("[conditionOperators]", conditionOperators); // ----------------------------------------------------- // SYNC REDUX FILTERS TO LOCAL STATE ON LOAD // ----------------------------------------------------- diff --git a/app/features/DataTable/DataTable.tsx b/app/features/DataTable/DataTable.tsx index 1816a8b..9a22f26 100644 --- a/app/features/DataTable/DataTable.tsx +++ b/app/features/DataTable/DataTable.tsx @@ -33,14 +33,10 @@ const DataTable = ({ }: DataTableProps) => { const dispatch = useDispatch(); const [showExtraColumns, setShowExtraColumns] = useState(false); - const [modalOpen, setModalOpen] = useState(false); - const [selectedRowId, setSelectedRowId] = useState(null); - const [pendingStatus, setPendingStatus] = useState(""); - const [reason, setReason] = useState(""); - const [statusUpdateError, setStatusUpdateError] = useState( - null - ); - const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + const [statusDialogData, setStatusDialogData] = useState<{ + rowId: number; + newStatus: string; + } | null>(null); const status = useSelector(selectStatus); const errorMessage = useSelector(selectError); @@ -49,7 +45,6 @@ const DataTable = ({ const handlePaginationModelChange = useCallback( (model: GridPaginationModel) => { - console.log("model", model); const nextPage = model.page + 1; const nextLimit = model.pageSize; @@ -61,57 +56,12 @@ const DataTable = ({ ); const handleStatusChange = useCallback((rowId: number, newStatus: string) => { - setSelectedRowId(rowId); - setPendingStatus(newStatus); - setModalOpen(true); + setStatusDialogData({ rowId, newStatus }); }, []); - const handleStatusSave = async () => { - if (!selectedRowId || !pendingStatus) return; - setStatusUpdateError(null); - setIsUpdatingStatus(true); - - try { - const payload = { - data: { - status: pendingStatus, - notes: reason.trim(), - }, - fields: ["Status", "Notes"], - }; - - const response = await fetch( - `/api/dashboard/transactions/${selectedRowId}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - } - ); - - const result = await response.json(); - - if (!response.ok) { - throw new Error( - result?.message || result?.error || "Failed to update transaction" - ); - } - - setModalOpen(false); - setReason(""); - setPendingStatus(""); - setStatusUpdateError(null); - setSelectedRowId(null); - } catch (err) { - setStatusUpdateError( - err instanceof Error ? err.message : "Failed to update transaction" - ); - } finally { - setIsUpdatingStatus(false); - } - }; + const handleDialogClose = useCallback(() => { + setStatusDialogData(null); + }, []); const selectEnhancedColumns = useMemo(makeSelectEnhancedColumns, []); @@ -170,20 +120,10 @@ const DataTable = ({ { - setModalOpen(false); - setReason(""); - setPendingStatus(""); - setStatusUpdateError(null); - setSelectedRowId(null); - }} - handleSave={handleStatusSave} - isSubmitting={isUpdatingStatus} - errorMessage={statusUpdateError} + open={Boolean(statusDialogData)} + transactionId={statusDialogData?.rowId} + newStatus={statusDialogData?.newStatus ?? ""} + onClose={handleDialogClose} /> diff --git a/app/features/DataTable/StatusChangeDialog.tsx b/app/features/DataTable/StatusChangeDialog.tsx index aa66c8e..c75d4bb 100644 --- a/app/features/DataTable/StatusChangeDialog.tsx +++ b/app/features/DataTable/StatusChangeDialog.tsx @@ -8,39 +8,90 @@ import { Alert, CircularProgress, } from "@mui/material"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; interface StatusChangeDialogProps { open: boolean; + transactionId?: number | null; newStatus: string; - reason: string; - setReason: React.Dispatch>; - handleClose: () => void; - handleSave: () => void; - isSubmitting?: boolean; - errorMessage?: string | null; + onClose: () => void; + onStatusUpdated?: () => void; } const StatusChangeDialog = ({ open, + transactionId, newStatus, - reason, - setReason, - handleClose, - handleSave, - isSubmitting = false, - errorMessage, + onClose, + onStatusUpdated, }: StatusChangeDialogProps) => { - const [isValid, setIsValid] = useState(false); + const [reason, setReason] = useState(""); + const [errorMessage, setErrorMessage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { + if (!open) { + setReason(""); + setErrorMessage(null); + setIsSubmitting(false); + } + }, [open]); + + const isValid = useMemo(() => { const noSpaces = reason.replace(/\s/g, ""); const length = noSpaces.length; - setIsValid(length >= 12 && length <= 400); + return length >= 12 && length <= 400; }, [reason]); + const handleSave = async () => { + if (!transactionId || !newStatus || !isValid) return; + + setErrorMessage(null); + setIsSubmitting(true); + + try { + const payload = { + data: { + status: newStatus, + notes: reason.trim(), + }, + fields: ["Status", "Notes"], + }; + + const response = await fetch( + `/api/dashboard/transactions/${transactionId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + } + ); + + const result = await response.json(); + + if (!response.ok) { + throw new Error( + result?.message || result?.error || "Failed to update transaction" + ); + } + + setReason(""); + setErrorMessage(null); + onStatusUpdated?.(); + onClose(); + } catch (err) { + setErrorMessage( + err instanceof Error ? err.message : "Failed to update transaction" + ); + } finally { + setIsSubmitting(false); + } + }; + return ( - + Change Status You want to change the status to {newStatus}. Please provide a @@ -63,13 +114,13 @@ const StatusChangeDialog = ({ )} -