From 2e5788405ce7863322dbe238ab7723a3052068a0 Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Tue, 22 Jul 2025 19:57:38 +0200 Subject: [PATCH] Added login and manual status change --- app/api/auth/login/route.tsx | 53 ++++ app/api/dashboard/audits/mockData.ts | 88 ++++++ app/api/dashboard/audits/route.ts | 52 ++++ .../transactions/deposits/mockData.ts | 256 ++++++++++++++++++ .../dashboard/transactions/deposits/route.ts | 62 +++++ .../transactions/withdrawal/mockData.ts | 176 ++++++++++++ .../transactions/withdrawal/route.ts | 51 ++++ app/components/PageLinks/PageLinks.tsx | 26 +- app/components/searchFilter/SearchFilters.tsx | 3 - app/dashboard/admin/users/page.tsx | 25 +- app/dashboard/approve/page.tsx | 3 +- app/dashboard/audits/page.tsx | 22 ++ app/dashboard/page.tsx | 2 +- app/dashboard/transactions/deposits/page.tsx | 7 +- app/dashboard/transactions/history/page.tsx | 24 +- .../transactions/withdrawals/page.tsx | 8 +- .../AdvancedSearch/AdvancedSearch.tsx | 53 ++-- app/features/Auth/LoginModal.scss | 197 ++++++++++++++ app/features/Auth/LoginModal.tsx | 112 ++++++++ app/features/DataTable/DataTable.tsx | 199 ++++++++++++++ app/features/DataTable/types.ts | 12 + .../DashboardHomePage/DashboardHomePage.tsx | 1 + .../TransactionsWaitingApproval.tsx | 1 - app/features/dashboard/header/Header.tsx | 11 + .../sidebar/SidebarLink.constants.ts | 3 + app/login/page.scss | 64 +++++ app/login/page.tsx | 103 +++++++ app/page.tsx | 2 +- app/redux/store.ts | 7 +- app/services/audits.ts | 22 ++ app/services/transactions.ts | 22 +- app/utils/exportData.ts | 53 ++-- middleware.ts | 30 ++ 33 files changed, 1618 insertions(+), 132 deletions(-) create mode 100644 app/api/auth/login/route.tsx create mode 100644 app/api/dashboard/audits/mockData.ts create mode 100644 app/api/dashboard/audits/route.ts create mode 100644 app/api/dashboard/transactions/deposits/mockData.ts create mode 100644 app/api/dashboard/transactions/deposits/route.ts create mode 100644 app/api/dashboard/transactions/withdrawal/mockData.ts create mode 100644 app/api/dashboard/transactions/withdrawal/route.ts create mode 100644 app/dashboard/audits/page.tsx create mode 100644 app/features/Auth/LoginModal.scss create mode 100644 app/features/Auth/LoginModal.tsx create mode 100644 app/features/DataTable/DataTable.tsx create mode 100644 app/features/DataTable/types.ts create mode 100644 app/login/page.scss create mode 100644 app/login/page.tsx create mode 100644 app/services/audits.ts create mode 100644 middleware.ts diff --git a/app/api/auth/login/route.tsx b/app/api/auth/login/route.tsx new file mode 100644 index 0000000..644f427 --- /dev/null +++ b/app/api/auth/login/route.tsx @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +// This is your POST handler for the login endpoint +export async function POST(request: Request) { + try { + const { email, password } = await request.json(); + + // --- Replace with your ACTUAL authentication logic --- + // In a real application, you would: + // 1. Query your database for the user by email. + // 2. Hash the provided password and compare it to the stored hashed password. + // 3. If credentials match, generate a secure JWT (JSON Web Token) or session ID. + // 4. Store the token/session ID securely (e.g., in a database or Redis). + + // Mock authentication for demonstration purposes: + if (email === "admin@example.com" && password === "password123") { + const authToken = "mock-jwt-token-12345"; // Replace with a real, securely generated token + + // Set the authentication token as an HTTP-only cookie + // HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript, + // which mitigates XSS attacks. + ( + await // Set the authentication token as an HTTP-only cookie + // HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript, + // which mitigates XSS attacks. + cookies() + ).set("auth_token", authToken, { + httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts + secure: process.env.NODE_ENV === "production", // Use secure in production (HTTPS) + maxAge: 60 * 60 * 24 * 7, // 1 week + path: "/", // Available across the entire site + sameSite: "lax", // Protects against CSRF + }); + + return NextResponse.json( + { success: true, message: "Login successful" }, + { status: 200 } + ); + } else { + return NextResponse.json( + { success: false, message: "Invalid credentials" }, + { status: 401 } + ); + } + } catch (error) { + console.error("Login API error:", error); + return NextResponse.json( + { success: false, message: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/api/dashboard/audits/mockData.ts b/app/api/dashboard/audits/mockData.ts new file mode 100644 index 0000000..596c5cd --- /dev/null +++ b/app/api/dashboard/audits/mockData.ts @@ -0,0 +1,88 @@ +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 new file mode 100644 index 0000000..c73fba3 --- /dev/null +++ b/app/api/dashboard/audits/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData"; +import { formatToDateTimeString } from "@/app/utils/formatDate"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const actionType = searchParams.get("actionType"); + const affectedUserId = searchParams.get("affectedUserId"); + const adminId = searchParams.get("adminId"); + const adminUsername = searchParams.get("adminUsername"); + const timeStampOfTheAction = searchParams.get("dateTime"); + + let filteredRows = [...AuditData]; + + if (actionType) { + filteredRows = filteredRows.filter( + (tx) => tx.actionType.toLocaleLowerCase() === actionType.toLocaleLowerCase(), + ); + } + + if (affectedUserId) { + filteredRows = filteredRows.filter( + (tx) => tx.affectedUserId.toLowerCase() === affectedUserId.toLowerCase(), + ); + } + + if (adminId) { + filteredRows = filteredRows.filter( + (tx) => tx.adminId === adminId, + ); + } + if (adminUsername) { + filteredRows = filteredRows.filter( + (tx) => tx.adminUsername === adminUsername, + ); + } + + if (timeStampOfTheAction) { + filteredRows = filteredRows.filter( + (tx) => + tx.timeStampOfTheAction.split(" ")[0] === + formatToDateTimeString(timeStampOfTheAction).split(" ")[0], + ); + } + + return NextResponse.json({ + tableRows: filteredRows, + tableColumns: AuditColumns, + tableSearchLabels: AuditSearchLabels, + }); +} diff --git a/app/api/dashboard/transactions/deposits/mockData.ts b/app/api/dashboard/transactions/deposits/mockData.ts new file mode 100644 index 0000000..98a37ba --- /dev/null +++ b/app/api/dashboard/transactions/deposits/mockData.ts @@ -0,0 +1,256 @@ +import { GridColDef } from "@mui/x-data-grid"; + +export const depositTransactionDummyData = [ + { + id: 1, + userId: 17, + merchandId: 100987998, + transactionId: 1049131973, + depositMethod: "Card", + status: "Completed", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 2, + userId: 17, + merchandId: 100987998, + transactionId: 1049131973, + depositMethod: "Card", + status: "Completed", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 3, + userId: 17, + merchandId: 100987997, + transactionId: 1049131973, + depositMethod: "Card", + status: "Completed", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 4, + userId: 19, + merchandId: 100987997, + transactionId: 1049136973, + depositMethod: "Card", + status: "Completed", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 5, + userId: 19, + merchandId: 100987998, + transactionId: 1049131973, + depositMethod: "Card", + status: "Completed", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 6, + userId: 27, + merchandId: 100987997, + transactionId: 1049131973, + depositMethod: "Card", + status: "Pending", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 7, + userId: 175, + merchandId: 100987938, + transactionId: 1049136973, + depositMethod: "Card", + status: "Pending", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 8, + userId: 172, + merchandId: 100987938, + transactionId: 1049131973, + depositMethod: "Card", + status: "Pending", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-12 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 9, + userId: 174, + merchandId: 100987938, + transactionId: 1049131973, + depositMethod: "Bank Transfer", + status: "Inprogress", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 10, + userId: 7, + merchandId: 100987998, + transactionId: 1049131973, + depositMethod: "Bank Transfer", + status: "Inprogress", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 11, + userId: 1, + merchandId: 100987998, + transactionId: 1049131973, + depositMethod: "Bank Transfer", + status: "Error", + options: [ + { value: "Pending", label: "Pending" }, + { value: "Completed", label: "Completed" }, + { value: "Inprogress", label: "Inprogress" }, + { value: "Error", label: "Error" }, + ], + amount: 4000, + currency: "EUR", + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, +]; + +export const depositTransactionsColumns: GridColDef[] = [ + { field: "userId", headerName: "User ID", width: 130 }, + { field: "merchandId", headerName: "Merchant ID", width: 130 }, + { field: "transactionId", headerName: "Transaction ID", width: 130 }, + { field: "depositMethod", headerName: "Deposit Method", width: 130 }, + { field: "status", headerName: "Status", width: 130 }, + { field: "actions", headerName: "Actions", width: 150 }, + { field: "amount", headerName: "Amount", width: 130 }, + { field: "currency", headerName: "Currency", width: 130 }, + { field: "dateTime", headerName: "Date / Time", width: 130 }, + { field: "errorInfo", headerName: "Error Info", width: 130 }, + { field: "fraudScore", headerName: "Fraud Score", width: 130 }, +]; + +export const depositTransactionsSearchLabels = [ + { label: "User", field: "userId", type: "text" }, + { label: "Transaction ID", field: "transactionId", type: "text" }, + { + label: "Transaction Reference ID", + field: "transactionReferenceId", + type: "text", + }, + { + label: "Currency", + field: "currency", + type: "select", + options: ["USD", "EUR", "GBP"], + }, + { + label: "Status", + field: "status", + type: "select", + options: ["Pending", "Inprogress", "Completed", "Failed"], + }, + { + label: "Payment Method", + field: "depositMethod", + type: "select", + options: ["Card", "Bank Transfer"], + }, + { label: "Date / Time", field: "dateTime", type: "date" }, +]; diff --git a/app/api/dashboard/transactions/deposits/route.ts b/app/api/dashboard/transactions/deposits/route.ts new file mode 100644 index 0000000..91066cf --- /dev/null +++ b/app/api/dashboard/transactions/deposits/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + depositTransactionDummyData, + depositTransactionsColumns, + depositTransactionsSearchLabels, +} from "./mockData"; +import { formatToDateTimeString } from "@/app/utils/formatDate"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const status = searchParams.get("status"); + const userId = searchParams.get("userId"); + const depositMethod = searchParams.get("depositMethod"); + const merchandId = searchParams.get("merchandId"); + const transactionId = searchParams.get("transactionId"); + const dateTime = searchParams.get("dateTime"); + + let filteredTransactions = [...depositTransactionDummyData]; + + if (userId) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.userId.toString() === userId, + ); + } + + if (status) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.status.toLowerCase() === status.toLowerCase(), + ); + } + + if (depositMethod) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.depositMethod.toLowerCase() === depositMethod.toLowerCase(), + ); + } + if (merchandId) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.merchandId.toString() === merchandId, + ); + } + if (transactionId) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.transactionId.toString() === transactionId, + ); + } + + if (dateTime) { + filteredTransactions = filteredTransactions.filter( + (tx) => + tx.dateTime.split(" ")[0] === + formatToDateTimeString(dateTime).split(" ")[0], + ); + } + + return NextResponse.json({ + tableRows: filteredTransactions, + tableSearchLabels: depositTransactionsSearchLabels, + tableColumns: depositTransactionsColumns, + }); +} diff --git a/app/api/dashboard/transactions/withdrawal/mockData.ts b/app/api/dashboard/transactions/withdrawal/mockData.ts new file mode 100644 index 0000000..3c7918c --- /dev/null +++ b/app/api/dashboard/transactions/withdrawal/mockData.ts @@ -0,0 +1,176 @@ +import { GridColDef } from "@mui/x-data-grid"; + +export const withdrawalTransactionDummyData = [ + { + id: 1, + userId: 17, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, + { + id: 2, + userId: 17, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, + { + id: 3, + userId: 17, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Complete", + amount: 4000, + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 4, + userId: 19, + transactionId: 1049136973, + withdrawalMethod: "Bank Transfer", + status: "Completed", + amount: 4000, + dateTime: "2025-06-18 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 5, + userId: 19, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, + { + id: 6, + userId: 27, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, + { + id: 7, + userId: 1, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, + { + id: 8, + userId: 172, + transactionId: 1049131973, + withdrawalMethod: "Card", + status: "Pending", + amount: 4000, + dateTime: "2025-06-12 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 9, + userId: 174, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Inprogress", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + }, + { + id: 10, + userId: 1, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, + { + id: 11, + userId: 1, + transactionId: 1049131973, + withdrawalMethod: "Bank Transfer", + status: "Error", + amount: 4000, + dateTime: "2025-06-17 10:10:30", + errorInfo: "-", + fraudScore: "frad score 1234", + manualCorrectionFlag: "-", + informationWhoApproved: "-", + }, +]; + +export const withdrawalTransactionsColumns: GridColDef[] = [ + { field: "userId", headerName: "User ID", width: 130 }, + { field: "transactionId", headerName: "Transaction ID", width: 130 }, + { field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 }, + { field: "status", headerName: "Status", width: 130 }, + { field: "amount", headerName: "Amount", width: 130 }, + { field: "dateTime", headerName: "Date / Time", width: 130 }, + { field: "errorInfo", headerName: "Error Info", width: 130 }, + { field: "fraudScore", headerName: "Fraud Score", width: 130 }, + { + field: "manualCorrectionFlag", + headerName: "Manual Correction Flag", + width: 130, + }, + { + field: "informationWhoApproved", + headerName: "Information who approved", + width: 130, + }, +]; + +export const withdrawalTransactionsSearchLabels = [ + { + label: "Status", + field: "status", + type: "select", + options: ["Pending", "Inprogress", "Completed", "Failed"], + }, + { + label: "Payment Method", + field: "depositMethod", + type: "select", + options: ["Card", "Bank Transfer"], + }, + { label: "Date / Time", field: "dateTime", type: "date" }, +]; diff --git a/app/api/dashboard/transactions/withdrawal/route.ts b/app/api/dashboard/transactions/withdrawal/route.ts new file mode 100644 index 0000000..885b0fd --- /dev/null +++ b/app/api/dashboard/transactions/withdrawal/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + withdrawalTransactionDummyData, + withdrawalTransactionsColumns, + withdrawalTransactionsSearchLabels, +} from "./mockData"; +import { formatToDateTimeString } from "@/app/utils/formatDate"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const userId = searchParams.get("userId"); + const status = searchParams.get("status"); + const dateTime = searchParams.get("dateTime"); + const withdrawalMethod = searchParams.get("withdrawalMethod"); + + let filteredTransactions = [...withdrawalTransactionDummyData]; + + if (userId) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.userId.toString() === userId, + ); + } + + if (status) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.status.toLowerCase() === status.toLowerCase(), + ); + } + + if (withdrawalMethod) { + filteredTransactions = filteredTransactions.filter( + (tx) => + tx.withdrawalMethod.toLowerCase() === withdrawalMethod.toLowerCase(), + ); + } + + if (dateTime) { + filteredTransactions = filteredTransactions.filter( + (tx) => + tx.dateTime.split(" ")[0] === + formatToDateTimeString(dateTime).split(" ")[0], + ); + } + + return NextResponse.json({ + tableRows: filteredTransactions, + tableSearchLabels: withdrawalTransactionsSearchLabels, + tableColumns: withdrawalTransactionsColumns, + }); +} diff --git a/app/components/PageLinks/PageLinks.tsx b/app/components/PageLinks/PageLinks.tsx index d90e5f9..f2ef968 100644 --- a/app/components/PageLinks/PageLinks.tsx +++ b/app/components/PageLinks/PageLinks.tsx @@ -1,25 +1,33 @@ +// app/components/PageLinks/PageLinks.tsx "use client"; import Link from "next/link"; -import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; +import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; // Keep this import import clsx from "clsx"; // Utility to merge class names -import "./PageLinks.scss"; +import "./PageLinks.scss"; // Keep this import +// Define the props interface for your PageLinks component +// It now extends ISidebarLink and includes isShowIcon interface IPageLinksProps extends ISidebarLink { isShowIcon?: boolean; } +// PageLinks component export default function PageLinks({ title, path, - icon: Icon, -}: IPageLinksProps) { + icon: Icon, // Destructure icon as Icon +}: // isShowIcon, // If you plan to use this prop, uncomment it and add logic +IPageLinksProps) { return ( - - - {Icon && } - {title} - + // Corrected Link usage for Next.js 13/14 App Router: + // - Removed `passHref` and `legacyBehavior` + // - Applied `className` directly to the Link component + // - Removed the nested `` tag + + {/* Conditionally render Icon if it exists */} + {Icon && } + {title} ); } diff --git a/app/components/searchFilter/SearchFilters.tsx b/app/components/searchFilter/SearchFilters.tsx index c785c9a..bacce04 100644 --- a/app/components/searchFilter/SearchFilters.tsx +++ b/app/components/searchFilter/SearchFilters.tsx @@ -1,12 +1,9 @@ -"use client" import React from 'react'; import { Box, Chip, Typography, Button } from '@mui/material'; import { useRouter, useSearchParams } from 'next/navigation'; interface SearchFiltersProps { filters: Record; - onDeleteFilter?: (key: string) => void; - onClearAll?: () => void; } const SearchFilters = ({ filters }: SearchFiltersProps) => { diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx index b62eea9..24a6ad9 100644 --- a/app/dashboard/admin/users/page.tsx +++ b/app/dashboard/admin/users/page.tsx @@ -1,29 +1,14 @@ -import Users from "@/app/features/pages/admin/users/users"; +import Users from "@/app/features/pages/Admin/Users/users"; export default async function BackOfficeUsersPage() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; - - let users = []; - - try { - const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, { - // cache: "no-store", // optional: disable SSR caching if needed - }); - - if (!res.ok) { - // If the API responds with 500/404/etc., log and bail gracefully - console.error( - `Failed to fetch users: ${res.status} ${res.statusText}` - ); - } else { - users = await res.json(); - } - } catch (error) { - console.error("Error fetching users:", error); - } + const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, { + cache: "no-store", // 👈 disables caching for SSR freshness + }); + const users = await res.json(); return (
diff --git a/app/dashboard/approve/page.tsx b/app/dashboard/approve/page.tsx index 0c1f92a..07d1a0c 100644 --- a/app/dashboard/approve/page.tsx +++ b/app/dashboard/approve/page.tsx @@ -1,7 +1,8 @@ // This ensures this component is rendered only on the client side "use client"; -import { Approve } from "@/app/components/pages/Approve/Approve"; +import { Approve } from "@/app/features/pages/Approve/Approve"; + export default function ApprovePage() { return ( diff --git a/app/dashboard/audits/page.tsx b/app/dashboard/audits/page.tsx new file mode 100644 index 0000000..9bec2b5 --- /dev/null +++ b/app/dashboard/audits/page.tsx @@ -0,0 +1,22 @@ +import DataTable from "@/app/features/DataTable/DataTable"; +import { getAudits } from "@/app/services/audits"; + +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; + } + } + const query = new URLSearchParams(safeParams).toString(); + const data = await getAudits({ query }); + + return ; +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index df3f502..aad0b66 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { DashboardHomePage } from "../features/pages/dashboardhomepage/dashboardhomepage"; +import { DashboardHomePage } from "../features/pages/DashboardHomePage/DashboardHomePage"; const DashboardPage = () => { return ; diff --git a/app/dashboard/transactions/deposits/page.tsx b/app/dashboard/transactions/deposits/page.tsx index d496729..aa589d0 100644 --- a/app/dashboard/transactions/deposits/page.tsx +++ b/app/dashboard/transactions/deposits/page.tsx @@ -1,5 +1,4 @@ - -import TransactionsTable from "@/app/features/pages/transactions/transactionstable"; +import DataTable from "@/app/features/DataTable/DataTable"; import { getTransactions } from "@/app/services/transactions"; export default async function DepositTransactionPage({ @@ -17,8 +16,8 @@ export default async function DepositTransactionPage({ } } const query = new URLSearchParams(safeParams).toString(); - const transactionType = 'deposit'; + const transactionType = "deposits"; const data = await getTransactions({ transactionType, query }); - return ; + return ; } diff --git a/app/dashboard/transactions/history/page.tsx b/app/dashboard/transactions/history/page.tsx index bf93fd7..78e01bc 100644 --- a/app/dashboard/transactions/history/page.tsx +++ b/app/dashboard/transactions/history/page.tsx @@ -1,23 +1,3 @@ -import TransactionsTable from "@/app/features/pages/transactions/transactionstable"; -import { getTransactions } from "@/app/services/transactions"; - -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 = 'deposit'; - const data = await getTransactions({ transactionType, query }); - - return ; +export default async function HistoryTransactionPage() { + return
History Transactions Page
; } diff --git a/app/dashboard/transactions/withdrawals/page.tsx b/app/dashboard/transactions/withdrawals/page.tsx index e48babe..e61e9bd 100644 --- a/app/dashboard/transactions/withdrawals/page.tsx +++ b/app/dashboard/transactions/withdrawals/page.tsx @@ -1,4 +1,4 @@ -import TransactionsTable from "@/app/features/pages/transactions/transactionstable"; +import DataTable from "@/app/features/DataTable/DataTable"; import { getTransactions } from "@/app/services/transactions"; export default async function WithdrawalTransactionPage({ @@ -16,8 +16,8 @@ export default async function WithdrawalTransactionPage({ } } const query = new URLSearchParams(safeParams).toString(); -const transactionType = 'withdrawal'; -const data = await getTransactions({ transactionType, query }); + const transactionType = "withdrawal"; + const data = await getTransactions({ transactionType, query }); - return ; + return ; } diff --git a/app/features/AdvancedSearch/AdvancedSearch.tsx b/app/features/AdvancedSearch/AdvancedSearch.tsx index 79db3d2..78948a7 100644 --- a/app/features/AdvancedSearch/AdvancedSearch.tsx +++ b/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -17,16 +17,9 @@ import SearchIcon from "@mui/icons-material/Search"; import RefreshIcon from "@mui/icons-material/Refresh"; import { useSearchParams, useRouter } from "next/navigation"; import { useState, useEffect, useMemo } from "react"; +import { ISearchLabel } from "../pages/transactions/types"; - -interface ILabel { - label: string; - field: string; - type: string; - options?: string[]; -} - -export default function AdvancedSearch({ labels }: { labels: ILabel[] }) { +export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) { const searchParams = useSearchParams(); const router = useRouter(); const [open, setOpen] = useState(false); @@ -37,7 +30,6 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) { setFormValues(initialParams); }, [searchParams]); - const updateURL = useMemo( () => debounce((newValues: Record) => { @@ -47,7 +39,7 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) { }); router.push(`?${updatedParams.toString()}`); }, 500), - [router] + [router], ); const handleFieldChange = (field: string, value: string) => { @@ -61,19 +53,20 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) { router.push("?"); }; - const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { - if ( - event.type === "keydown" && - ((event as React.KeyboardEvent).key === "Tab" || - (event as React.KeyboardEvent).key === "Shift") - ) { - return; - } - setOpen(open); - }; + const toggleDrawer = + (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { + if ( + event.type === "keydown" && + ((event as React.KeyboardEvent).key === "Tab" || + (event as React.KeyboardEvent).key === "Shift") + ) { + return; + } + setOpen(open); + }; return ( - +
+ + {/* Password input field */} +
+ + setPassword(e.target.value)} + required + disabled={isLoading} + /> +
+ + + + ); +} diff --git a/app/features/DataTable/DataTable.tsx b/app/features/DataTable/DataTable.tsx new file mode 100644 index 0000000..1279c6d --- /dev/null +++ b/app/features/DataTable/DataTable.tsx @@ -0,0 +1,199 @@ +"use client"; +import { useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + FormControl, + Select, + MenuItem, + FormControlLabel, + Checkbox, + Stack, + Paper, + TextField, +} from "@mui/material"; +import FileUploadIcon from "@mui/icons-material/FileUpload"; +import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import AdvancedSearch from "../AdvancedSearch/AdvancedSearch"; +import SearchFilters from "@/app/components/searchFilter/SearchFilters"; +import { exportData } from "@/app/utils/exportData"; +import { IDataTable } from "./types"; + + +interface IDataTableProps { + data: IDataTable; +} + +export type TWithId = { id: number }; +const DataTable = ( + data: IDataTableProps, +) => { + const { tableRows, tableColumns, tableSearchLabels } = data.data; + const router = useRouter(); + const searchParams = useSearchParams(); + + const [open, setOpen] = useState(false); + const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv"); + const [onlyCurrentTable, setOnlyCurrentTable] = useState(false); + const [rows, setRows] = useState(tableRows); + + const filters = Object.fromEntries(searchParams.entries()); + + const handleClickField = (field: string, value: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set(field, value); + router.push(`?${params.toString()}`); + router.refresh(); + }; + + const handleStatusChange = (id: number, newStatus: string) => { + setRows( + rows.map((row) => (row.id === id ? { ...row, status: newStatus } : row)), + ); + }; + + const getColumnsWithDropdown = (columns: TColumn[]): GridColDef[] => { + return columns?.map((col) => { + if (col.field === "actions") { + return { + ...col, + renderCell: (params: GridRenderCellParams) => { + const row = tableRows.find((r) => r.id === params.id) as { + id: number; + status?: string; + options?: { value: string; label: string }[]; + }; + + const options = row?.options; + if (!options) return params.value; + + return ( + + ); + }, + }; + } + + return col; + }); + }; + + return ( + + + console.log(`setSearchQuery(${e.target.value})`)} + sx={{ width: 300 }} + /> + + + + + + { + if (params.field !== "actions") { + handleClickField(params.field, params.value as string); + } + }} + /> + + {/* Export Dialog */} + setOpen(false)}> + Export Transactions + + + + + setOnlyCurrentTable(e.target.checked)} + /> + } + label="Only export the results in the current table" + sx={{ mt: 2 }} + /> + + + + + + + + ); +}; + +export default DataTable; diff --git a/app/features/DataTable/types.ts b/app/features/DataTable/types.ts new file mode 100644 index 0000000..0327000 --- /dev/null +++ b/app/features/DataTable/types.ts @@ -0,0 +1,12 @@ +interface ISearchLabel { + label: string; + field: string; + type: string; + options?: string[]; +} + +export interface IDataTable { + tableRows: TRow[]; + tableColumns: TColumn[]; + tableSearchLabels: ISearchLabel[]; +} \ No newline at end of file diff --git a/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx b/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx index d36e1ad..cea093d 100644 --- a/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx +++ b/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx @@ -11,6 +11,7 @@ import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOve export const DashboardHomePage = () => { return ( <> + {/* Conditional rendering of the Generic Modal, passing LoginModal as children */} diff --git a/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx b/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx index 93d5fbb..34ddc01 100644 --- a/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx +++ b/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { Box, Button, diff --git a/app/features/dashboard/header/Header.tsx b/app/features/dashboard/header/Header.tsx index fa7643d..beccad7 100644 --- a/app/features/dashboard/header/Header.tsx +++ b/app/features/dashboard/header/Header.tsx @@ -6,6 +6,17 @@ import AccountMenu from "./accountMenu/AccountMenu"; import "./Header.scss"; const Header = () => { + // const [anchorEl, setAnchorEl] = useState(null); + + // // Handle menu open + // const handleMenuClick = (event: React.MouseEvent) => { + // setAnchorEl(event.currentTarget); + // }; + + // // Handle menu close + // const handleMenuClose = () => { + // setAnchorEl(null); + // }; const handleChange = () => {}; diff --git a/app/features/dashboard/sidebar/SidebarLink.constants.ts b/app/features/dashboard/sidebar/SidebarLink.constants.ts index f7bb406..c0420c5 100644 --- a/app/features/dashboard/sidebar/SidebarLink.constants.ts +++ b/app/features/dashboard/sidebar/SidebarLink.constants.ts @@ -14,6 +14,8 @@ import SettingsIcon from "@mui/icons-material/Settings"; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import HistoryIcon from '@mui/icons-material/History'; +import FactCheckIcon from "@mui/icons-material/FactCheck"; + import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; @@ -75,6 +77,7 @@ export const PAGE_LINKS: ISidebarLink[] = [ ], }, { title: "Account IQ", path: "/dashboard/account-iq", icon: InsightsIcon }, + { title: "Audits", path: "/dashboard/audits", icon: FactCheckIcon }, // { title: 'Documentation', path: '/documentation', icon: DescriptionIcon }, // { title: 'Support', path: '/support', icon: SupportAgentIcon }, // { title: 'System Status', path: '/system-status', icon: WarningAmberIcon }, diff --git a/app/login/page.scss b/app/login/page.scss new file mode 100644 index 0000000..2e32ae3 --- /dev/null +++ b/app/login/page.scss @@ -0,0 +1,64 @@ +// Variables for consistent styling +$primary-color: #2563eb; // Blue-600 equivalent +$primary-hover-color: #1d4ed8; // Blue-700 equivalent +$success-color: #16a34a; // Green-600 equivalent +$error-color: #dc2626; // Red-600 equivalent +$text-color-dark: #1f2937; // Gray-800 equivalent +$text-color-medium: #4b5563; // Gray-700 equivalent +$text-color-light: #6b7280; // Gray-600 equivalent +$border-color: #d1d5db; // Gray-300 equivalent +$bg-color-light: #f3f4f6; // Gray-100 equivalent +$bg-color-white: #ffffff; + +.page-container { + min-height: 100vh; + background-color: $bg-color-light; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: "Inter", sans-serif; // Assuming Inter font is used + padding: 1rem; +} + +.page-container__content { + width: 100%; + max-width: 56rem; // max-w-4xl + background-color: $bg-color-white; + border-radius: 0.75rem; // rounded-xl + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg + padding: 2rem; // p-8 + text-align: center; +} + +.page-container__title { + font-size: 2.25rem; // text-4xl + font-weight: 700; // font-bold + color: $text-color-dark; + margin-bottom: 1.5rem; // mb-6 +} + +.page-container__message--logged-in { + font-size: 1.25rem; // text-xl + color: $success-color; + margin-bottom: 1rem; // mb-4 +} + +.page-container__text { + color: $text-color-medium; + margin-bottom: 1.5rem; // mb-6 +} + +.page-container__button--logout { + padding: 0.75rem 1.5rem; // px-6 py-3 + background-color: $error-color; + color: $bg-color-white; + font-weight: 600; // font-semibold + border-radius: 0.5rem; // rounded-lg + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md + transition: background-color 0.3s ease-in-out; + &:hover { + background-color: darken($error-color, 5%); // red-700 equivalent + } +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..c185f15 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import LoginModal from "../features/Auth/LoginModal"; // Your LoginModal component + +import "./page.scss"; // Global styles for LoginModal and page +import Modal from "../components/Modal/Modal"; + +export default function LoginPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const redirectPath = searchParams.get("redirect") || "/dashboard"; + + const [authMessage, setAuthMessage] = useState(""); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + // Check if already logged in by trying to fetch a protected resource or checking a client-accessible flag + // For HTTP-only cookies, you can't directly read the token here. + // Instead, you'd rely on a server-side check (e.g., in middleware or a Server Component) + // or a simple client-side flag if your backend also sets one (less secure for token itself). + // For this example, we'll assume if they land here, they need to log in. + // A more robust check might involve a quick API call to /api/auth/status + // if the token is in an HTTP-only cookie. + const checkAuthStatus = async () => { + // In a real app, this might be a call to a /api/auth/status endpoint + // that checks the HTTP-only cookie on the server and returns a boolean. + // For now, we'll rely on the middleware to redirect if unauthenticated. + // If the user somehow lands on /login with a valid cookie, the middleware + // should have redirected them already. + }; + checkAuthStatus(); + }, []); + + const handleLogin = async (email: string, password: string) => { + setAuthMessage("Attempting login..."); + try { + const response = await fetch("/api/auth/login", { + // <--- CALLING YOUR INTERNAL ROUTE HANDLER + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (response.ok) { + // Check if the response status is 2xx + // Backend has successfully set the HTTP-only cookie + setAuthMessage("Login successful!"); + setIsLoggedIn(true); + // Redirect to the intended path after successful login + router.replace(redirectPath); + } else { + // Handle login errors (e.g., invalid credentials) + setAuthMessage(data.message || "Login failed. Please try again."); + setIsLoggedIn(false); + } + } catch (error) { + console.error("Login failed:", error); + setAuthMessage("An error occurred during login. Please try again later."); + setIsLoggedIn(false); + } + return isLoggedIn; // Return the current login status + }; + + const clearAuthMessage = () => setAuthMessage(""); + + // If user is already logged in (e.g., redirected by middleware to dashboard), + // this page shouldn't be visible. The middleware should handle the primary redirect. + // This `isLoggedIn` state here is more for internal page logic if the user somehow + // bypasses middleware or lands on /login with a valid session. + // For a robust setup, the middleware is key. + + return ( +
+
+

Payment Backoffice

+

+ Please log in to access the backoffice. +

+
+ + {/* Always show the modal on the login page */} + { + /* No direct close for login modal, user must log in */ + }} + title="Login to Backoffice" + > + + +
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index a739b02..bb91a4b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { DashboardHomePage } from "./features/pages/dashboardhomepage/dashboardhomepage"; +import { DashboardHomePage } from "./features/Pages/DashboardHomePage/DashboardHomePage"; const DashboardPage = () => { return ; diff --git a/app/redux/store.ts b/app/redux/store.ts index 59a6f77..9609be3 100644 --- a/app/redux/store.ts +++ b/app/redux/store.ts @@ -1,11 +1,8 @@ -import { configureStore } from '@reduxjs/toolkit'; -import advancedSearchReducer from './advanedSearch/advancedSearchSlice'; -import transactionsReducer from './transactions/transactionsSlice'; +import { configureStore } from "@reduxjs/toolkit"; +import advancedSearchReducer from "./advanedSearch/advancedSearchSlice"; export const store = configureStore({ reducer: { advancedSearch: advancedSearchReducer, - transactions: transactionsReducer, }, }); - diff --git a/app/services/audits.ts b/app/services/audits.ts new file mode 100644 index 0000000..ac5e700 --- /dev/null +++ b/app/services/audits.ts @@ -0,0 +1,22 @@ +export async function getAudits({ + query, +}: { + query: string; +}) { + const res = await fetch( + `http://localhost:3000/api/dashboard/audits?${query}`, + { + cache: "no-store", + }, + ); + + if (!res.ok) { + // Handle error from the API + const errorData = await res + .json() + .catch(() => ({ message: "Unknown error" })); + throw new Error(errorData.message || `HTTP error! status: ${res.status}`); + } + + return res.json(); +} diff --git a/app/services/transactions.ts b/app/services/transactions.ts index 08d73a4..0d7f83e 100644 --- a/app/services/transactions.ts +++ b/app/services/transactions.ts @@ -1,11 +1,23 @@ -export async function getTransactions({ transactionType, query }: { transactionType: string, query: string }) { - const res = await fetch(`http://localhost:3000/api/transactions/${transactionType}?${query}`, { - cache: "no-store", - }); +export async function getTransactions({ + transactionType, + query, +}: { + transactionType: string; + query: string; +}) { + + const res = await fetch( + `http://localhost:3000/api/dashboard/transactions/${transactionType}?${query}`, + { + cache: "no-store", + }, + ); if (!res.ok) { // Handle error from the API - const errorData = await res.json().catch(() => ({ message: 'Unknown error' })); + const errorData = await res + .json() + .catch(() => ({ message: "Unknown error" })); throw new Error(errorData.message || `HTTP error! status: ${res.status}`); } diff --git a/app/utils/exportData.ts b/app/utils/exportData.ts index 1f2b3f0..0bf1654 100644 --- a/app/utils/exportData.ts +++ b/app/utils/exportData.ts @@ -1,39 +1,34 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck import * as XLSX from "xlsx"; -import { GridColDef } from "@mui/x-data-grid"; export type FileType = "csv" | "xls" | "xlsx"; import { saveAs } from "file-saver"; +import { GridColDef } from "@mui/x-data-grid"; -import type { ITransaction } from "../features/pages/transactions/types"; - - -export const exportData = ( - transactions: ITransaction[], - columns: GridColDef[], +export const exportData = ( + rows: TRow[], + columns: TColumn[], fileType: FileType = "csv", onlyCurrentTable = false, - setOpen: (open: boolean) => void + setOpen: (open: boolean) => void, ) => { - const exportRows = onlyCurrentTable ? transactions.slice(0, 5) : transactions; - const exportData = [ - columns.map((col) => col.headerName), - ...exportRows.map((row) => columns.map((col) => row[col.field] ?? "")), - ]; + const exportRows = onlyCurrentTable ? rows.slice(0, 5) : rows; + const exportData = [ + columns.map((col) => col.headerName), + ...exportRows.map((row) => columns.map((col) => (row as Record)[col.field] ?? "")), + ]; - const worksheet = XLSX.utils.aoa_to_sheet(exportData); - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, "Transactions"); + const worksheet = XLSX.utils.aoa_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Transactions"); - if (fileType === "csv") { - const csv = XLSX.utils.sheet_to_csv(worksheet); - const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); - saveAs(blob, "transactions.csv"); - } else { - XLSX.writeFile(workbook, `transactions.${fileType}`, { - bookType: fileType, - }); - } + if (fileType === "csv") { + const csv = XLSX.utils.sheet_to_csv(worksheet); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + saveAs(blob, "transactions.csv"); + } else { + XLSX.writeFile(workbook, `transactions.${fileType}`, { + bookType: fileType, + }); + } - setOpen(false); - }; + setOpen(false); +}; diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..99ed90d --- /dev/null +++ b/middleware.ts @@ -0,0 +1,30 @@ +// middleware.ts +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const token = request.cookies.get("auth_token")?.value; // Get token from cookie + + // Define protected paths + const protectedPaths = ["/dashboard", "/settings", "/admin"]; + const isProtected = protectedPaths.some((path) => + request.nextUrl.pathname.startsWith(path) + ); + + // If accessing a protected path and no token + if (isProtected && !token) { + // Redirect to login page + const loginUrl = new URL("/login", request.url); + // Optional: Add a redirect query param to return to original page after login + loginUrl.searchParams.set("redirect", request.nextUrl.pathname); + return NextResponse.redirect(loginUrl); + } + + // Allow the request to proceed if not protected or token exists + return NextResponse.next(); +} + +// Configure matcher to run middleware on specific paths +export const config = { + matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"], // Apply to dashboard and its sub-paths +};