diff --git a/app/api/dashboard/admin/users/route.ts b/app/api/dashboard/admin/users/route.ts new file mode 100644 index 0000000..6352b69 --- /dev/null +++ b/app/api/dashboard/admin/users/route.ts @@ -0,0 +1,79 @@ +// app/api/user/route.ts +import { NextRequest, NextResponse } from "next/server"; + +export const users = [ + { + merchantId: 100987998, + id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9", + mame: "Jacob", + username: "lspaddy", + firstName: "Paddy", + lastName: "Man", + email: "patrick@omegasys.eu", + phone: "", + jobTitle: "", + enabled: true, + authorities: [ + "ROLE_IIN", + "ROLE_FIRST_APPROVER", + "ROLE_RULES_ADMIN", + "ROLE_TRANSACTION_VIEWER", + "ROLE_IIN_ADMIN", + "ROLE_USER_PSP_ACCOUNT", + ], + allowedMerchantIds: [100987998], + created: "2025-05-04T15:32:48.432Z", + disabledBy: null, + disabledDate: null, + disabledReason: null, + incidentNotes: false, + lastLogin: "", + lastMandatoryUpdated: "2025-05-04T15:32:48.332Z", + marketingNewsletter: false, + releaseNotes: false, + requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"], + twoFactorCondition: "required", + twoFactorCredentials: [], + }, +]; + +export async function GET() { + return NextResponse.json(users); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { firstName, lastName, email, phone, role } = body; + + // Add the new user to the existing users array (in-memory, not persistent) + const bodytoAdd = { + merchantId: 100987998, + mame: "Jacob", + id: "382eed15-1e21-41fa-b1f3-0c1adb3af714", + username: "lsterence", + firstName, + lastName, + email, + phone, + jobTitle: "", + role, + enabled: true, + authorities: ["ROLE_IIN", "ROLE_FIRST_APPROVER", "ROLE_RULES_ADMIN"], + allowedMerchantIds: [100987998], + created: "2025-05-04T15:32:48.432Z", + disabledBy: null, + disabledDate: null, + disabledReason: null, + incidentNotes: false, + lastLogin: "", + lastMandatoryUpdated: "2025-05-04T15:32:48.332Z", + marketingNewsletter: false, + releaseNotes: false, + requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"], + twoFactorCondition: "required", + twoFactorCredentials: [], + }; + users.push(bodytoAdd); + + return NextResponse.json(users, { status: 201 }); +} diff --git a/app/api/transactions/deposit/mockData.ts b/app/api/transactions/deposit/mockData.ts new file mode 100644 index 0000000..18c0faf --- /dev/null +++ b/app/api/transactions/deposit/mockData.ts @@ -0,0 +1,191 @@ +import { GridColDef } from "@mui/x-data-grid"; + + +export const depositTransactionDummyData = [ + { + id: 1, + userId: 17, + merchandId: 100987998, + transactionId: 1049131973, + depositMethod: "Card", + status: "Completed", + 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", + 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: "Complete", + 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", + 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", + 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", + 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", + 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", + 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", + 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", + 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", + 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: "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" }, +] \ No newline at end of file diff --git a/app/api/transactions/deposit/route.ts b/app/api/transactions/deposit/route.ts new file mode 100644 index 0000000..1f8d62b --- /dev/null +++ b/app/api/transactions/deposit/route.ts @@ -0,0 +1,57 @@ +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({ + filteredTransactions: filteredTransactions, + transactionsSearchLabels: depositTransactionsSearchLabels, + transactionsColumns: depositTransactionsColumns + }); +} diff --git a/app/api/transactions/route.ts b/app/api/transactions/route.ts new file mode 100644 index 0000000..03bdd3d --- /dev/null +++ b/app/api/transactions/route.ts @@ -0,0 +1,25 @@ +import { transactionDummyData } from "@/app/components/test/test2"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const state = searchParams.get("state"); + const user = searchParams.get("user"); + + let filteredTransactions = [...transactionDummyData]; + + if (user) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.user.toString() === user + ); + } + + if (state) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.state.toLowerCase() === state.toLowerCase() + ); + } + + return NextResponse.json(filteredTransactions); +} diff --git a/app/api/transactions/withdrawal/mockData.ts b/app/api/transactions/withdrawal/mockData.ts new file mode 100644 index 0000000..3c7918c --- /dev/null +++ b/app/api/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/transactions/withdrawal/route.ts b/app/api/transactions/withdrawal/route.ts new file mode 100644 index 0000000..d0f22cd --- /dev/null +++ b/app/api/transactions/withdrawal/route.ts @@ -0,0 +1,45 @@ +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({ + filteredTransactions: filteredTransactions, + transactionsColumns: withdrawalTransactionsColumns, + transactionsSearchLabels: withdrawalTransactionsSearchLabels + }); +} diff --git a/app/components/Modal/Modal.scss b/app/components/Modal/Modal.scss new file mode 100644 index 0000000..8dedc7f --- /dev/null +++ b/app/components/Modal/Modal.scss @@ -0,0 +1,53 @@ +.modal__overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2); + position: relative; + min-width: 320px; + max-width: 90vw; + max-height: 90vh; + overflow: auto; + padding: 2rem 1.5rem 1.5rem 1.5rem; + display: flex; + flex-direction: column; +} + +.modal__close { + position: absolute; + top: 1rem; + right: 1rem; + background: transparent; + border: none; + font-size: 2rem; + line-height: 1; + cursor: pointer; + color: #888; + transition: color 0.2s; + + &:hover, + &:focus { + color: #333; + outline: none; + } +} + +.modal__body { + // Example element block for modal content + margin-top: 1rem; + font-size: 1rem; + color: #222; + width: 500px; +} diff --git a/app/components/Modal/Modal.tsx b/app/components/Modal/Modal.tsx new file mode 100644 index 0000000..7a5a2c6 --- /dev/null +++ b/app/components/Modal/Modal.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import "./Modal.scss"; + +interface ModalProps { + open: boolean; + onClose: () => void; + children: React.ReactNode; + className?: string; + overlayClassName?: string; + title?: string; +} + +const Modal: React.FC = ({ + open, + onClose, + children, + title, + className = "", +}) => { + if (!open) return null; + + return ( +
+
e.stopPropagation()} + data-testid="modal-content" + > + + {title &&

{title}

} +
{children}
+
+
+ ); +}; + +export default Modal; diff --git a/app/components/searchFilter/SearchFilters.tsx b/app/components/searchFilter/SearchFilters.tsx new file mode 100644 index 0000000..bacce04 --- /dev/null +++ b/app/components/searchFilter/SearchFilters.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Box, Chip, Typography, Button } from '@mui/material'; +import { useRouter, useSearchParams } from 'next/navigation'; + +interface SearchFiltersProps { + filters: Record; +} + +const SearchFilters = ({ filters }: SearchFiltersProps) => { + const router = useRouter(); + const filterLabels: Record = { + userId: "User", + state: "State", + startDate: "Start Date", + // Add others here + }; + const searchParams = useSearchParams() + const handleDeleteFilter = (key: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.delete(key); + router.push(`?${params.toString()}`); + }; + + + const onClearAll = () => { + router.push("?"); + } + + const renderChip = (label: string, value: string, key: string) => ( + + {label}: {value} + + } + onDelete={() => handleDeleteFilter(key)} + sx={{ mr: 1, mb: 1 }} + /> + ); + + return ( + + {Object.entries(filters).map(([key, value]) => + value ? renderChip(filterLabels[key] ?? key, value, key) : null + )} + + {Object.values(filters).some(Boolean) && ( + + )} + + ); +}; + + +export default SearchFilters; diff --git a/app/components/test/test1.tsx b/app/components/test/test1.tsx new file mode 100644 index 0000000..5a9accf --- /dev/null +++ b/app/components/test/test1.tsx @@ -0,0 +1,103 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import Button from "@mui/material/Button"; +import List from "@mui/material/List"; +import Divider from "@mui/material/Divider"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import InboxIcon from "@mui/icons-material/MoveToInbox"; +import MailIcon from "@mui/icons-material/Mail"; +import SearchIcon from "@mui/icons-material/Search"; + +export default function RightTemporaryDrawer() { + const [open, setOpen] = React.useState(false); + + 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 list = () => ( + + + {["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => ( + + + + {index % 2 === 0 ? : } + + + + + ))} + + + + {["All mail", "Trash", "Spam"].map((text, index) => ( + + + + {index % 2 === 0 ? : } + + + + + ))} + + + ); + + return ( +
+ + {/* */} + + {list()} + +
+ ); +} diff --git a/app/components/test/test2.tsx b/app/components/test/test2.tsx new file mode 100644 index 0000000..3db3076 --- /dev/null +++ b/app/components/test/test2.tsx @@ -0,0 +1,186 @@ +// app/transactions/page.tsx +"use client"; + +import { useState } from "react"; + +// mocks/transactionData.ts +export const transactionDummyData = [ + { + id: 1, + merchandId: 100987998, + transactionID: 1049131973, + user: 1, + created: "2025-06-18 10:10:30", + state: "FAILED", + statusDescription: "ERR_ABOVE_LIMIT", + pspStatusCode: 100501, + }, + { + id: 2, + merchandId: 100987998, + transactionID: 1049131973, + user: 2, + created: "2025-06-18 10:10:30", + state: "FAILED", + statusDescription: "ERR_ABOVE_LIMIT", + pspStatusCode: 100501, + }, + { + id: 3, + merchandId: 100987998, + transactionID: 1049131973, + user: 3, + created: "2025-06-18 10:10:30", + state: "FAILED", + statusDescription: "ERR_ABOVE_LIMIT", + pspStatusCode: 100501, + }, +]; + +export default function TransactionsPage() { + const [userId, setUserId] = useState(""); + const [state, setState] = useState(""); + const [statusCode, setStatusCode] = useState(""); + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchTransactions = async () => { + setLoading(true); + try { + const url = new URL("https://api.example.com/transactions"); + if (userId) url.searchParams.append("userId", userId); + if (state) url.searchParams.append("state", state); + if (statusCode) url.searchParams.append("statusCode", statusCode); + + const response = await fetch(url.toString()); + const data = await response.json(); + setTransactions(data.transactions); + } catch (error) { + console.error("Error fetching transactions:", error); + } finally { + setLoading(false); + } + }; + + return ( +
+

Transaction Search

+ +
+
+ + setUserId(e.target.value)} + className="border p-2 rounded text-sm" + placeholder="Filter by user ID" + /> +
+ +
+ + setState(e.target.value)} + className="border p-2 rounded text-sm" + placeholder="Filter by state" + /> +
+ +
+ + setStatusCode(e.target.value)} + className="border p-2 rounded text-sm" + placeholder="Filter by status code" + /> +
+ +
+ +
+
+ + {transactions.length > 0 ? ( +
+ + + + + + + + + + + + {transactions.map((tx) => ( + + + + + + + + ))} + +
IDUserStateStatus CodeCreated
{tx.id}{tx.user}{tx.state}{tx.pspStatusCode}{tx.created}
+
+ ) : ( +
+ {loading ? "Loading transactions..." : "No transactions found"} +
+ )} +
+ ); +} + +// mocks/handlers.ts +import { http, HttpResponse } from "msw"; + +export const handlers = [ + http.get("https://api.example.com/transactions", ({ request }) => { + const url = new URL(request.url); + + // Get query parameters + const userId = url.searchParams.get("userId"); + const state = url.searchParams.get("state"); + const statusCode = url.searchParams.get("statusCode"); + + // Filter transactions based on query parameters + let filteredTransactions = [...transactionDummyData]; + + if (userId) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.user.toString() === userId + ); + } + + if (state) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.state.toLowerCase() === state.toLowerCase() + ); + } + + if (statusCode) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.pspStatusCode.toString() === statusCode + ); + } + + return HttpResponse.json({ + transactions: filteredTransactions, + count: filteredTransactions.length, + }); + }), +]; diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx index 9a8be75..a3a3654 100644 --- a/app/dashboard/admin/users/page.tsx +++ b/app/dashboard/admin/users/page.tsx @@ -1,12 +1,18 @@ -// This ensures this component is rendered only on the client side -"use client"; - import Users from "@/app/features/Pages/Admin/Users/users"; -export default function BackOfficeUsersPage() { +export default async function BackOfficeUsersPage() { + const baseUrl = + process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"; + const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, { + cache: "no-store", // 👈 disables caching for SSR freshness + }); + const users = await res.json(); + return (
- +
); } diff --git a/app/dashboard/transactions/deposits/page.tsx b/app/dashboard/transactions/deposits/page.tsx new file mode 100644 index 0000000..922a508 --- /dev/null +++ b/app/dashboard/transactions/deposits/page.tsx @@ -0,0 +1,23 @@ +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 ; +} diff --git a/app/dashboard/transactions/history/page.tsx b/app/dashboard/transactions/history/page.tsx new file mode 100644 index 0000000..922a508 --- /dev/null +++ b/app/dashboard/transactions/history/page.tsx @@ -0,0 +1,23 @@ +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 ; +} diff --git a/app/dashboard/transactions/withdrawals/page.tsx b/app/dashboard/transactions/withdrawals/page.tsx new file mode 100644 index 0000000..2800942 --- /dev/null +++ b/app/dashboard/transactions/withdrawals/page.tsx @@ -0,0 +1,23 @@ +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 = 'withdrawal'; +const data = await getTransactions({ transactionType, query }); + + return ; +} diff --git a/app/features/AdvancedSearch/AdvancedSearch.tsx b/app/features/AdvancedSearch/AdvancedSearch.tsx new file mode 100644 index 0000000..79db3d2 --- /dev/null +++ b/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -0,0 +1,186 @@ +import { + Box, + TextField, + MenuItem, + Button, + Drawer, + FormControl, + Select, + Typography, + Stack, + debounce, +} from "@mui/material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import SearchIcon from "@mui/icons-material/Search"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { useSearchParams, useRouter } from "next/navigation"; +import { useState, useEffect, useMemo } from "react"; + + +interface ILabel { + label: string; + field: string; + type: string; + options?: string[]; +} + +export default function AdvancedSearch({ labels }: { labels: ILabel[] }) { + const searchParams = useSearchParams(); + const router = useRouter(); + const [open, setOpen] = useState(false); + const [formValues, setFormValues] = useState>({}); + + useEffect(() => { + const initialParams = Object.fromEntries(searchParams.entries()); + setFormValues(initialParams); + }, [searchParams]); + + + const updateURL = useMemo( + () => + debounce((newValues: Record) => { + const updatedParams = new URLSearchParams(); + Object.entries(newValues).forEach(([key, value]) => { + if (value) updatedParams.set(key, value); + }); + router.push(`?${updatedParams.toString()}`); + }, 500), + [router] + ); + + const handleFieldChange = (field: string, value: string) => { + const updatedValues = { ...formValues, [field]: value }; + setFormValues(updatedValues); + updateURL(updatedValues); + }; + + const resetForm = () => { + setFormValues({}); + router.push("?"); + }; + + const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { + if ( + event.type === "keydown" && + ((event as React.KeyboardEvent).key === "Tab" || + (event as React.KeyboardEvent).key === "Shift") + ) { + return; + } + setOpen(open); + }; + + return ( + + + + + + + + + + Search + + + + + + + { + 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 TransactionsTable diff --git a/app/features/Pages/transactions/constants.ts b/app/features/Pages/transactions/constants.ts index 3a64c16..e34fa3b 100644 --- a/app/features/Pages/transactions/constants.ts +++ b/app/features/Pages/transactions/constants.ts @@ -1,80 +1,168 @@ import { GridColDef } from "@mui/x-data-grid"; export const columns: GridColDef[] = [ - { field: 'merchandId', headerName: 'Merchant ID', width: 130 }, - { field: 'transactionID', headerName: 'Transaction ID', width: 130 }, - { field: 'user', headerName: 'User', width: 75 }, - { field: 'created', headerName: 'Created', type: 'number', width: 130 }, - { field: 'state', headerName: 'State', type: 'number', width: 130 }, - { field: 'statusDescription', headerName: 'Status Description', type: 'number', width: 130 }, - { field: 'pspStatusCode', headerName: 'PSP Status Code', type: 'number', width: 130 }, - { field: 'pspStatusMessage', headerName: 'PSP Status Message', type: 'number', width: 90 }, - { field: 'psp', headerName: 'PSP', type: 'number', width: 90 }, - { field: 'pspAccount', headerName: 'PSP Account', type: 'number', width: 90 }, - { field: 'initPSP', headerName: 'Init PSP', type: 'number', width: 90 }, - { field: 'initPSPAccout', headerName: 'Init PSP Account', type: 'number', width: 90 }, - { field: 'pspService', headerName: 'PSP Service', type: 'number', width: 90 }, - { field: 'transactionType', headerName: 'Transaction Type', type: 'number', width: 90 }, - { field: 'paymentMethod', headerName: 'Payment Method', type: 'number', width: 90 }, - { field: 'rules', headerName: 'Rules', type: 'number', width: 90 }, - { field: 'amount', headerName: 'Amount', type: 'number', width: 90 }, - { field: 'fee', headerName: 'Fee', type: 'number', width: 90 }, - { field: 'transactionAmount', headerName: 'Transaction Amount', type: 'number', width: 90 }, - { field: 'baseAmount', headerName: 'Base Amount', type: 'number', width: 90 }, - { field: 'baseFee', headerName: 'Base Fee', type: 'number', width: 90 }, - { field: 'baseTransaction', headerName: 'Base Transaction', type: 'number', width: 90 }, - { field: 'pspFee', headerName: 'PSP Fee', type: 'number', width: 90 }, - { field: 'basePspFee', headerName: 'Base PSP Fee', type: 'number', width: 90 }, - { field: 'authAmount', headerName: 'Auth Amount', type: 'number', width: 90 }, - { field: 'baseAuthAmount', headerName: 'Base Auth Amount', type: 'number', width: 90 }, - { field: 'userBalance', headerName: 'User Balance', type: 'number', width: 90 }, - { field: 'updated', headerName: 'Updated', type: 'number', width: 90 }, - { field: 'userIp', headerName: 'User IP', type: 'number', width: 90 }, - { field: 'channel', headerName: 'Channel', type: 'number', width: 90 }, - { field: 'depositType', headerName: 'Deposit Type', type: 'number', width: 90 }, - { field: 'userEmal', headerName: 'User Emal', type: 'number', width: 90 }, - { field: 'userCategory', headerName: 'User Category', type: 'number', width: 90 }, - { field: 'userCountry', headerName: 'User Country', type: 'number', width: 90 }, - { field: 'userAccount', headerName: 'User Account', type: 'number', width: 90 }, - { field: 'bankName', headerName: 'Bank Name', type: 'number', width: 90 }, - { field: 'pspUserReference', headerName: 'PSP User Reference', type: 'number', width: 90 }, - { field: 'pspFraudScore', headerName: 'PSP Fraud Score', type: 'number', width: 90 }, - { field: 'fraudStatus', headerName: 'FraudStatus', type: 'number', width: 90 }, - { field: 'blocked', headerName: 'Blocked', type: 'number', width: 90 }, - { field: 'abuse', headerName: 'Abuse', type: 'number', width: 90 }, - { field: 'kycStatus', headerName: 'KYC Status', type: 'number', width: 90 }, - { field: 'kycPSPName', headerName: 'KYC PSP Name', type: 'number', width: 90 }, - { field: 'kycPSPStatus', headerName: 'KYC PSP Status', type: 'number', width: 90 }, - { field: 'kycIdStatus', headerName: 'KYC ID Status', type: 'number', width: 90 }, - { field: 'kycAddressStatus', headerName: 'KYC Address Status', type: 'number', width: 90 }, - { field: 'kycAgeStatus', headerName: 'KYC Age Status', type: 'number', width: 90 }, - { field: 'kycPEPAndSanction', headerName: 'KYC PEP And Sanction', type: 'number', width: 90 }, - { field: 'pspReferenceId', headerName: 'PSPReferenceID', type: 'number', width: 90 }, - { field: 'siteReferenceId', headerName: 'Site Reference ID', type: 'number', width: 90 }, - { field: 'info', headerName: 'Info', type: 'number', width: 90 }, - { field: 'accountHolder', headerName: 'Account Holder', type: 'number', width: 90 }, - { field: 'firstName', headerName: 'First Name', type: 'number', width: 90 }, - { field: 'lastName', headerName: 'Last Name', type: 'number', width: 90 }, - { field: 'street', headerName: 'Street', type: 'number', width: 90 }, - { field: 'city', headerName: 'City', type: 'number', width: 90 }, - { field: 'zip', headerName: 'ZIP', type: 'number', width: 90 }, - { field: 'dob', headerName: 'DOB', type: 'number', width: 90 }, - { field: 'mobile', headerName: 'Mobile', type: 'number', width: 90 }, - { field: 'lastUpdatedBy', headerName: 'Last Updated By', type: 'number', width: 90 }, - { field: 'ipCity', headerName: 'IP City', type: 'number', width: 90 }, - { field: 'ipRegion', headerName: 'IP Region', type: 'number', width: 90 }, - { field: 'ipCountry', headerName: 'IP Country', type: 'number', width: 90 }, - { field: 'cardIssuerCountry', headerName: 'Card Issuer Country', type: 'number', width: 90 }, - { field: 'cardBand', headerName: 'Card Band', type: 'number', width: 90 }, - { field: 'cardCategory', headerName: 'Card Category', type: 'number', width: 90 }, - { field: 'cardIssuerName', headerName: 'Card Issuer Name', type: 'number', width: 90 }, - { field: 'inn', headerName: 'INN', type: 'number', width: 90 }, - { field: 'cardType', headerName: 'Card Type', type: 'number', width: 90 }, - { field: 'firstAttempt', headerName: 'First Attempt', type: 'number', width: 90 }, - { field: 'firstSuccessful', headerName: 'First Successful', type: 'number', width: 90 }, - { field: 'firstTransaction', headerName: 'First Transaction', type: 'number', width: 90 }, - { field: 'firstPspAcountAttempt', headerName: 'First PSP Acount Attempt', type: 'number', width: 90 }, - { field: 'firstPspAcountSuccessful', headerName: 'First PSP Acount Successful', type: 'number', width: 90 }, - { field: 'originTransactionId', headerName: 'Origin Transaction ID', type: 'number', width: 90 }, - { field: 'transactionReferenceId', headerName: 'Transaction Reference ID', type: 'number', width: 90 }, -]; \ No newline at end of file + { field: "merchandId", headerName: "Merchant ID", width: 130 }, + { field: "transactionID", headerName: "Transaction ID", width: 130 }, + { field: "user", headerName: "User", width: 75 }, + { field: "created", headerName: "Created", type: "number", width: 130 }, + { field: "state", headerName: "State", type: "number", width: 130 }, + { + field: "statusDescription", + headerName: "Status Description", + type: "number", + width: 130, + }, + { + field: "statusCode", + headerName: "Status Code", + type: "number", + width: 130, + }, + { + field: "pspStatusCode", + headerName: "PSP Status Code", + type: "number", + width: 130, + }, + { + field: "pspStatusMessage", + headerName: "PSP Status Message", + type: "number", + width: 90, + }, + { field: "psp", headerName: "PSP", type: "number", width: 90 }, + { field: "pspAccount", headerName: "PSP Account", type: "number", width: 90 }, + { field: "initPSP", headerName: "Init PSP", type: "number", width: 90 }, + { + field: "initPSPAccout", + headerName: "Init PSP Account", + type: "number", + width: 90, + }, + { field: "pspService", headerName: "PSP Service", type: "number", width: 90 }, + { + field: "transactionType", + headerName: "Transaction Type", + type: "number", + width: 90, + }, + { + field: "paymentMethod", + headerName: "Payment Method", + type: "number", + width: 90, + }, + { field: "rules", headerName: "Rules", type: "number", width: 90 }, + { field: "amount", headerName: "Amount", type: "number", width: 90 }, + { field: "fee", headerName: "Fee", type: "number", width: 90 }, + { + field: "transactionAmount", + headerName: "Transaction Amount", + type: "number", + width: 90, + }, + // { field: 'baseAmount', headerName: 'Base Amount', type: 'number', width: 90 }, + // { field: 'baseFee', headerName: 'Base Fee', type: 'number', width: 90 }, + // { field: 'baseTransaction', headerName: 'Base Transaction', type: 'number', width: 90 }, + // { field: 'pspFee', headerName: 'PSP Fee', type: 'number', width: 90 }, + // { field: 'basePspFee', headerName: 'Base PSP Fee', type: 'number', width: 90 }, + // { field: 'authAmount', headerName: 'Auth Amount', type: 'number', width: 90 }, + // { field: 'baseAuthAmount', headerName: 'Base Auth Amount', type: 'number', width: 90 }, + // { field: 'userBalance', headerName: 'User Balance', type: 'number', width: 90 }, + // { field: 'updated', headerName: 'Updated', type: 'number', width: 90 }, + // { field: 'userIp', headerName: 'User IP', type: 'number', width: 90 }, + // { field: 'channel', headerName: 'Channel', type: 'number', width: 90 }, + // { field: 'depositType', headerName: 'Deposit Type', type: 'number', width: 90 }, + // { field: 'userEmal', headerName: 'User Emal', type: 'number', width: 90 }, + // { field: 'userCategory', headerName: 'User Category', type: 'number', width: 90 }, + // { field: 'userCountry', headerName: 'User Country', type: 'number', width: 90 }, + // { field: 'userAccount', headerName: 'User Account', type: 'number', width: 90 }, + // { field: 'bankName', headerName: 'Bank Name', type: 'number', width: 90 }, + // { field: 'pspUserReference', headerName: 'PSP User Reference', type: 'number', width: 90 }, + // { field: 'pspFraudScore', headerName: 'PSP Fraud Score', type: 'number', width: 90 }, + // { field: 'fraudStatus', headerName: 'FraudStatus', type: 'number', width: 90 }, + // { field: 'blocked', headerName: 'Blocked', type: 'number', width: 90 }, + // { field: 'abuse', headerName: 'Abuse', type: 'number', width: 90 }, + // { field: 'kycStatus', headerName: 'KYC Status', type: 'number', width: 90 }, + // { field: 'kycPSPName', headerName: 'KYC PSP Name', type: 'number', width: 90 }, + // { field: 'kycPSPStatus', headerName: 'KYC PSP Status', type: 'number', width: 90 }, + // { field: 'kycIdStatus', headerName: 'KYC ID Status', type: 'number', width: 90 }, + // { field: 'kycAddressStatus', headerName: 'KYC Address Status', type: 'number', width: 90 }, + // { field: 'kycAgeStatus', headerName: 'KYC Age Status', type: 'number', width: 90 }, + // { field: 'kycPEPAndSanction', headerName: 'KYC PEP And Sanction', type: 'number', width: 90 }, + // { field: 'pspReferenceId', headerName: 'PSPReferenceID', type: 'number', width: 90 }, + // { field: 'siteReferenceId', headerName: 'Site Reference ID', type: 'number', width: 90 }, + // { field: 'info', headerName: 'Info', type: 'number', width: 90 }, + // { field: 'accountHolder', headerName: 'Account Holder', type: 'number', width: 90 }, + // { field: 'firstName', headerName: 'First Name', type: 'number', width: 90 }, + // { field: 'lastName', headerName: 'Last Name', type: 'number', width: 90 }, + // { field: 'street', headerName: 'Street', type: 'number', width: 90 }, + // { field: 'city', headerName: 'City', type: 'number', width: 90 }, + // { field: 'zip', headerName: 'ZIP', type: 'number', width: 90 }, + // { field: 'dob', headerName: 'DOB', type: 'number', width: 90 }, + // { field: 'mobile', headerName: 'Mobile', type: 'number', width: 90 }, + // { field: 'lastUpdatedBy', headerName: 'Last Updated By', type: 'number', width: 90 }, + // { field: 'ipCity', headerName: 'IP City', type: 'number', width: 90 }, + // { field: 'ipRegion', headerName: 'IP Region', type: 'number', width: 90 }, + // { field: 'ipCountry', headerName: 'IP Country', type: 'number', width: 90 }, + // { field: 'cardIssuerCountry', headerName: 'Card Issuer Country', type: 'number', width: 90 }, + // { field: 'cardBand', headerName: 'Card Band', type: 'number', width: 90 }, + // { field: 'cardCategory', headerName: 'Card Category', type: 'number', width: 90 }, + // { field: 'cardIssuerName', headerName: 'Card Issuer Name', type: 'number', width: 90 }, + // { field: 'inn', headerName: 'INN', type: 'number', width: 90 }, + // { field: 'cardType', headerName: 'Card Type', type: 'number', width: 90 }, + // { field: 'firstAttempt', headerName: 'First Attempt', type: 'number', width: 90 }, + // { field: 'firstSuccessful', headerName: 'First Successful', type: 'number', width: 90 }, + // { field: 'firstTransaction', headerName: 'First Transaction', type: 'number', width: 90 }, + // { field: 'firstPspAcountAttempt', headerName: 'First PSP Acount Attempt', type: 'number', width: 90 }, + // { field: 'firstPspAcountSuccessful', headerName: 'First PSP Acount Successful', type: 'number', width: 90 }, + // { field: 'originTransactionId', headerName: 'Origin Transaction ID', type: 'number', width: 90 }, + // { field: 'transactionReferenceId', headerName: 'Transaction Reference ID', type: 'number', width: 90 }, +]; + +export const depositTransactionsColumns = [ + { 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: "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 currencies = ["USD", "EUR", "GBP"]; +export const states = ["Pending","Inprogress", "Completed", "Failed"]; +export const depositMethod = ["Card", "Bank Transfer"]; + +export const Labels = [ + { 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: currencies, + }, + { + label: "Status", + field: "status", + type: "select", + options: states, + }, + { + label: "Payment Method", + field: "depositMethod", + type: "select", + options: depositMethod, + }, + { label: "Date / Time", field: "dateTime", type: "date" }, +] diff --git a/app/features/Pages/transactions/types.ts b/app/features/Pages/transactions/types.ts new file mode 100644 index 0000000..886eff4 --- /dev/null +++ b/app/features/Pages/transactions/types.ts @@ -0,0 +1,32 @@ +interface IDepositTransactionsColumns { + field: string; + headerName: string; + width: number; +} + +interface IFilteredTransactions { + id: number; + userId: number; + merchandId: number; + transactionId: number; + depositMethod: string; + status: string; + amount: number; + currency: string; + dateTime: string; + errorInfo: string; + fraudScore: string; +} + +interface IDepositTransactionsSearchLabels { + label: string; + field: string; + type: string; + options?: string[]; +} + +export interface ITransaction { + filteredTransactions: IFilteredTransactions[]; + transactionsColumns: IDepositTransactionsColumns[]; + transactionsSearchLabels: IDepositTransactionsSearchLabels[]; +} diff --git a/app/features/UserRoles/AddUser/AddUser.scss b/app/features/UserRoles/AddUser/AddUser.scss new file mode 100644 index 0000000..d6fc563 --- /dev/null +++ b/app/features/UserRoles/AddUser/AddUser.scss @@ -0,0 +1,34 @@ +.add-user { + position: sticky; + top: 40px; + width: 100%; + background: #fff; + z-index: 10; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 1rem; + padding: 1rem 0.5rem; + border-bottom: 1px solid #eee; + + &__button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + &__button--primary { + background: #1976d2; + color: #fff; + } + + &__button--secondary { + background: #e0e0e0; + color: #333; + } +} diff --git a/app/features/UserRoles/AddUser/AddUser.tsx b/app/features/UserRoles/AddUser/AddUser.tsx new file mode 100644 index 0000000..9f79eb7 --- /dev/null +++ b/app/features/UserRoles/AddUser/AddUser.tsx @@ -0,0 +1,32 @@ +import { Add } from "@mui/icons-material"; +import React from "react"; +import "./AddUser.scss"; + +const AddUser: React.FC<{ + onAddUser?: () => void; + onExport?: () => void; +}> = ({ onAddUser, onExport }) => { + return ( +
+ + +
+ ); +}; + +export default AddUser; diff --git a/app/features/UserRoles/EditUser/EditUser.scss b/app/features/UserRoles/EditUser/EditUser.scss new file mode 100644 index 0000000..7e2e617 --- /dev/null +++ b/app/features/UserRoles/EditUser/EditUser.scss @@ -0,0 +1,46 @@ +.edit-user { + margin-top: 30px; + display: flex; + flex-wrap: wrap; + gap: 16px; + + input { + flex: 1 1 20%; + min-width: 150px; + box-sizing: border-box; + padding: 8px; + font-size: 1rem; + border-radius: 4px; + border: 1px solid #ccc; + outline: none; + transition: border-color 0.3s ease; + + &:focus { + border-color: #0070f3; + } + } + &__button-container { + flex-basis: 100%; + width: 100%; + + button { + flex-basis: 100%; + margin-top: 16px; + padding: 10px 0; + font-size: 1rem; + border-radius: 4px; + width: 100px; + cursor: pointer; + } + + button:first-child { + color: var(--button-primary); + border-color: var(--button-primary); + } + button:last-child { + color: var(--button-secondary); + border-color: var(--button-secondary); + margin-left: 8; + } + } +} diff --git a/app/features/UserRoles/EditUser/EditUser.tsx b/app/features/UserRoles/EditUser/EditUser.tsx new file mode 100644 index 0000000..9f384ff --- /dev/null +++ b/app/features/UserRoles/EditUser/EditUser.tsx @@ -0,0 +1,107 @@ +import React from "react"; +import { useRouter } from "next/navigation"; +import { IEditUserForm, EditUserField } from "../User.interfaces"; +import { createRole } from "@/services/roles.services"; +import "./EditUser.scss"; + +const EditUser = () => { + const router = useRouter(); + const [form, setForm] = React.useState({ + firstName: "", + lastName: "", + email: "", + role: "", + phone: "", + }); + + const handleChange = (e: React.ChangeEvent) => { + const name = e.target.name as EditUserField; + const value = e.target.value; + if (name === "phone") { + const filtered = value.replace(/[^0-9+\-\s()]/g, ""); + setForm((prev) => ({ + ...prev, + phone: filtered, + })); + } else { + setForm((prev) => ({ + ...prev, + [name]: value, + })); + } + }; + + const handleResetForm = () => { + setForm({ + firstName: "", + lastName: "", + email: "", + role: "", + phone: "", + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await createRole(form); + router.refresh(); // <- refreshes the page (SSR re-runs) + } catch (err: any) { + console.log(err.message || "Error creating role"); + // setError(err.message || "Error creating role"); + } + }; + + return ( +
+ + + + + +
+ + +
+
+ ); +}; + +export default EditUser; diff --git a/app/features/UserRoles/User.interfaces.ts b/app/features/UserRoles/User.interfaces.ts new file mode 100644 index 0000000..1170cee --- /dev/null +++ b/app/features/UserRoles/User.interfaces.ts @@ -0,0 +1,14 @@ +export interface IEditUserForm { + firstName: string; + lastName: string; + email: string; + role: string; + phone: string; +} + +export type EditUserField = + | "firstName" + | "lastName" + | "email" + | "role" + | "phone"; diff --git a/app/features/UserRoles/User.scss b/app/features/UserRoles/User.scss new file mode 100644 index 0000000..970b03f --- /dev/null +++ b/app/features/UserRoles/User.scss @@ -0,0 +1,17 @@ +.user-card { + &__edit { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + } + + &__edit-transition { + opacity: 0; + transition: opacity 0.5s ease-in-out; + + &--open { + opacity: 1; + } + } +} diff --git a/app/features/UserRoles/User.types.ts b/app/features/UserRoles/User.types.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/features/UserRoles/userRoleCard.tsx b/app/features/UserRoles/userRoleCard.tsx index 43828bb..32b3e93 100644 --- a/app/features/UserRoles/userRoleCard.tsx +++ b/app/features/UserRoles/userRoleCard.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Card, CardContent, @@ -18,6 +19,8 @@ import { AdminPanelSettings, History, } from "@mui/icons-material"; +import EditUser from "./EditUser/EditUser"; +import "./User.scss"; interface Props { username: string; @@ -40,12 +43,18 @@ export default function UserRoleCard({ roles, extraRolesCount, }: Props) { + const [isEditing, setIsEditing] = useState(false); + + const handleEditClick = () => { + setIsEditing(!isEditing); + }; + return ( {/* Header */} - {username.slice(0, 2).toUpperCase()} + {username?.slice(0, 2).toUpperCase()} {username} {name} @@ -58,7 +67,7 @@ export default function UserRoleCard({ - + @@ -91,7 +100,7 @@ export default function UserRoleCard({ - Roles{" "} + Roles @@ -105,13 +114,13 @@ export default function UserRoleCard({ {extraRolesCount && }
- - {/* Footer */} - - - {lastLogin} - - +
+ {isEditing && } +
); diff --git a/app/features/dashboard/sidebar/SidebarLink.constants.ts b/app/features/dashboard/sidebar/SidebarLink.constants.ts index c732888..f7bb406 100644 --- a/app/features/dashboard/sidebar/SidebarLink.constants.ts +++ b/app/features/dashboard/sidebar/SidebarLink.constants.ts @@ -10,14 +10,36 @@ import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; import InsightsIcon from "@mui/icons-material/Insights"; import ListAltIcon from "@mui/icons-material/ListAlt"; import SettingsIcon from "@mui/icons-material/Settings"; + +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import HistoryIcon from '@mui/icons-material/History'; + import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; export const PAGE_LINKS: ISidebarLink[] = [ { title: "Home", path: "/dashboard", icon: HomeIcon }, + { title: "Transaction", path: "/dashboard/transactions", - icon: AccountBalanceWalletIcon, + icon: AccountBalanceWalletIcon, children: [ + { + title: "Deposits", + path: "/dashboard/transactions/deposits", + icon: ArrowDownwardIcon, + }, + { + title: "Withdrawals", + path: "/dashboard/transactions/withdrawals", + icon: ArrowUpwardIcon, + }, + { + title: "Transaction History", + path: "/dashboard/transactions/history", + icon: HistoryIcon, + }, + ], }, { title: "Approve", path: "/dashboard/approve", icon: CheckCircleIcon }, { title: "Investigate", path: "/dashboard/investigate", icon: SearchIcon }, diff --git a/app/layout.tsx b/app/layout.tsx index 2a6d7a1..142102a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,8 @@ import ThemeRegistry from "@/config/ThemeRegistry"; import type { Metadata } from "next"; import "../styles/globals.scss"; +import { Providers } from "./providers/providers"; +import { MSWProvider } from './providers/msw-provider'; export const metadata: Metadata = { title: "Your App", @@ -15,7 +17,11 @@ export default function RootLayout({ return ( - {children} + + + {children} + + ); diff --git a/app/providers/msw-provider.tsx b/app/providers/msw-provider.tsx new file mode 100644 index 0000000..d186b21 --- /dev/null +++ b/app/providers/msw-provider.tsx @@ -0,0 +1,14 @@ +"use client"; +import { useEffect } from "react"; + +export function MSWProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") { + import("../../mock/browser").then(({ worker }) => { + // worker.start(); + }); + } + }, []); + + return <>{children}; +} diff --git a/app/providers/providers.tsx b/app/providers/providers.tsx new file mode 100644 index 0000000..3d205c8 --- /dev/null +++ b/app/providers/providers.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { store } from '../redux/store'; + +export function Providers({ children }: { children: ReactNode }) { + return {children}; +} + diff --git a/app/redux/advanedSearch/advancedSearchSlice.ts b/app/redux/advanedSearch/advancedSearchSlice.ts new file mode 100644 index 0000000..43b055c --- /dev/null +++ b/app/redux/advanedSearch/advancedSearchSlice.ts @@ -0,0 +1,56 @@ +import { createSlice } from '@reduxjs/toolkit'; + +interface AdvancedSearchState { + keyword: string, + transactionID: string, + transactionReferenceId: string, + user: string, + currency: string, + state: string, + statusDescription: string, + transactionType: string, + paymentMethod: string, + psps: string, + initialPsps: string, + merchants: string, + startDate: null | string, + endDate: null | string, + lastUpdatedFrom: null | string, + lastUpdatedTo: null | string, + minAmount: string, + maxAmount: string, + channel: string, +} + +const initialState: AdvancedSearchState = { + keyword: "", + transactionID: "", + transactionReferenceId: "", + user: "", + currency: "", + state: "", + statusDescription: "", + transactionType: "", + paymentMethod: "", + psps: "", + initialPsps: "", + merchants: "", + startDate: null, + endDate: null, + lastUpdatedFrom: null, + lastUpdatedTo: null, + minAmount: "", + maxAmount: "", + channel: "", +}; + +const advancedSearchSlice = createSlice({ + name: 'advancedSearch', + initialState, + reducers: { + }, +}, +); + +export default advancedSearchSlice.reducer; + diff --git a/app/redux/store.ts b/app/redux/store.ts new file mode 100644 index 0000000..59a6f77 --- /dev/null +++ b/app/redux/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from '@reduxjs/toolkit'; +import advancedSearchReducer from './advanedSearch/advancedSearchSlice'; +import transactionsReducer from './transactions/transactionsSlice'; + +export const store = configureStore({ + reducer: { + advancedSearch: advancedSearchReducer, + transactions: transactionsReducer, + }, +}); + diff --git a/app/redux/transactions/selectors.ts b/app/redux/transactions/selectors.ts new file mode 100644 index 0000000..1ee7436 --- /dev/null +++ b/app/redux/transactions/selectors.ts @@ -0,0 +1,3 @@ +import { RootState } from "../types"; + +export const selectTransactions = (state: RootState) => state.transactions.data; diff --git a/app/redux/transactions/transactionsSlice.ts b/app/redux/transactions/transactionsSlice.ts new file mode 100644 index 0000000..6fcd7a9 --- /dev/null +++ b/app/redux/transactions/transactionsSlice.ts @@ -0,0 +1,83 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; + +interface ITransactionsData { + id: number; + merchandId: number; + transactionID: number; + user: number; + created: string; + state: string; + statusDescription: string; + pspStatusCode: number; + +} +interface ITransactionsState { + data: ITransactionsData[]; + loading: boolean; + error: null | string; + totalTransactions: number +} + + + +const initialState: ITransactionsState = { + data: [], + loading: false, + error: null, + totalTransactions: 0, +} + +const transactionsSlice = createSlice({ + name: 'transactions', + initialState, + reducers: { + }, + extraReducers: (builder) => { + builder + .addCase(getTransactions.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(getTransactions.fulfilled, (state, action) => { + state.data = action.payload.transactions; + state.totalTransactions = action.payload.count; + state.loading = false; + }) + .addCase(getTransactions.rejected, (state, action) => { + state.error = action.error.message || "Failed to fetch categories"; + state.loading = false; + state.data = []; + }) + } +}, +); + +export default transactionsSlice.reducer; + + +export const getTransactions = createAsyncThunk( + 'transactions/getTransactions', + async ( + { + userId = '', + state = '', + statusCode = '', + }: { userId?: string; state?: string; statusCode?: string } = {} + ) => { + const url = new URL('https://api.example.com/transactions'); + + if (userId) url.searchParams.append('userId', userId); + if (state) url.searchParams.append('state', state); + if (statusCode) url.searchParams.append('statusCode', statusCode); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error('Failed to fetch transactions'); + } + + const data = await response.json(); + return data; // Let the reducer store this + } +); + diff --git a/app/redux/types.ts b/app/redux/types.ts new file mode 100644 index 0000000..574261a --- /dev/null +++ b/app/redux/types.ts @@ -0,0 +1,5 @@ +import { store } from "./store"; + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + diff --git a/app/services/transactions.ts b/app/services/transactions.ts new file mode 100644 index 0000000..08d73a4 --- /dev/null +++ b/app/services/transactions.ts @@ -0,0 +1,13 @@ +export async function getTransactions({ transactionType, query }: { transactionType: string, query: string }) { + const res = await fetch(`http://localhost:3000/api/transactions/${transactionType}?${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/test/page.tsx b/app/test/page.tsx new file mode 100644 index 0000000..966a371 --- /dev/null +++ b/app/test/page.tsx @@ -0,0 +1,50 @@ +// app/test/page.tsx +'use client'; + +import { useEffect, useState } from 'react'; + +export default function TestPage() { + const [user, setUser] = useState(null); + const [loginStatus, setLoginStatus] = useState(''); + + useEffect(() => { + // Test GET request + fetch('https://api.example.com/user') + .then(res => res.json()) + .then(data => setUser(data)); + }, []); + + const handleLogin = async () => { + // Test POST request + const response = await fetch('https://api.example.com/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: 'admin', + password: 'password123' + }) + }); + + const result = await response.json(); + setLoginStatus(response.ok ? 'Login successful' : `Error: ${result.error}`); + }; + + return ( +
+

MSW Test Page

+ +
+

User Data (GET)

+
{JSON.stringify(user, null, 2)}
+
+ +
+

Login Test (POST)

+ +

{loginStatus}

+
+
+ ); +} diff --git a/app/test1/page.tsx b/app/test1/page.tsx new file mode 100644 index 0000000..67a44e9 --- /dev/null +++ b/app/test1/page.tsx @@ -0,0 +1,29 @@ +'use client' +import React, { useState } from 'react'; +import SearchFilters from '../components/searchFilter/SearchFilters'; + +export default function Home() { + const [filters, setFilters] = useState({ + user: '42', + state: 'FAILED', + startDate: '2025-06-28 23:25', + }); + + const handleDeleteFilter = (key) => { + setFilters((prev) => ({ ...prev, [key]: null })); + }; + + const handleClearAll = () => { + setFilters({ user: null, state: null, startDate: null }); + }; + + return ( +
+ +
+ ); +} diff --git a/app/testWithParams/page.tsx b/app/testWithParams/page.tsx new file mode 100644 index 0000000..71f1def --- /dev/null +++ b/app/testWithParams/page.tsx @@ -0,0 +1,97 @@ +// app/products/page.tsx +'use client'; + +import { useState } from 'react'; + +export default function ProductsPage() { + const [category, setCategory] = useState(''); + const [sort, setSort] = useState('price'); + const [limit, setLimit] = useState('10'); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchProducts = async () => { + setLoading(true); + try { + // Construct URL with query parameters + const url = new URL('https://api.example.com/products'); + if (category) url.searchParams.append('category', category); + if (sort) url.searchParams.append('sort', sort); + if (limit) url.searchParams.append('limit', limit); + + const response = await fetch(url.toString()); + const data = await response.json(); + setProducts(data.products); + } catch (error) { + console.error('Error fetching products:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+

Product Search

+ +
+
+ + setCategory(e.target.value)} + className="border p-2 rounded" + placeholder="electronics, clothing, etc." + /> +
+ +
+ + +
+ +
+ + setLimit(e.target.value)} + className="border p-2 rounded" + min="1" + max="100" + /> +
+ + +
+ + {products.length > 0 && ( +
+

Results

+
+ {products.map((product) => ( +
+

{product.name}

+

Category: {product.category}

+

Price: ${product.price}

+
+ ))} +
+
+ )} +
+ ); +} diff --git a/app/utils/exportData.ts b/app/utils/exportData.ts new file mode 100644 index 0000000..17121fa --- /dev/null +++ b/app/utils/exportData.ts @@ -0,0 +1,37 @@ +import * as XLSX from "xlsx"; +import { GridColDef } from "@mui/x-data-grid"; +export type FileType = "csv" | "xls" | "xlsx"; +import { saveAs } from "file-saver"; + +import type { ITransaction } from "../features/Pages/Transactions/types"; + + +export const exportData = ( + transactions: ITransaction[], + columns: GridColDef[], + fileType: FileType = "csv", + onlyCurrentTable = false, + 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 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, + }); + } + + setOpen(false); + }; diff --git a/app/utils/formatDate.ts b/app/utils/formatDate.ts new file mode 100644 index 0000000..93c2071 --- /dev/null +++ b/app/utils/formatDate.ts @@ -0,0 +1,13 @@ +export const formatToDateTimeString = (dateString: string): string => { + const date = new Date(dateString); + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); // months are 0-indexed + const day = String(date.getDate()).padStart(2, '0'); + + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/config/theme.ts b/config/theme.ts index c1b46e5..2d17df3 100644 --- a/config/theme.ts +++ b/config/theme.ts @@ -1,39 +1,43 @@ -import { createTheme } from '@mui/material/styles'; +import { createTheme } from "@mui/material/styles"; const palette = { primary: { - main: '#1976d2', + main: "#1976d2", }, secondary: { - main: '#d32f2f', + main: "#d32f2f", }, background: { - default: '#fafafa', - paper: '#ffffff', - primary: 'rgb(69, 190, 171)', + default: "#fafafa", + paper: "#ffffff", + primary: "rgb(69, 190, 171)", }, text: { - primary: '#000000', - secondary: '#555555', - tertiary: '#fff', + primary: "#000000", + secondary: "#555555", + tertiary: "#fff", + }, + button: { + primary: "#0070f3", + secondary: "##FF00FF", }, action: { - hover: 'rgba(0, 0, 0, 0.08)', + hover: "rgba(0, 0, 0, 0.08)", }, }; const typography = { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', h1: { - fontSize: '3rem', + fontSize: "3rem", fontWeight: 700, }, h2: { - fontSize: '2.5rem', + fontSize: "2.5rem", fontWeight: 700, }, body1: { - fontSize: '1rem', + fontSize: "1rem", fontWeight: 400, }, }; @@ -41,8 +45,8 @@ const typography = { // Create the theme based on the light or dark mode preference const theme = createTheme({ palette: { - mode: 'light', // Change this to 'dark' for dark mode - ...palette + mode: "light", // Change this to 'dark' for dark mode + ...palette, }, // typography, breakpoints: { diff --git a/mock/browser.ts b/mock/browser.ts index 975f2cd..bcd82e4 100644 --- a/mock/browser.ts +++ b/mock/browser.ts @@ -1,4 +1,3 @@ -// mocks/browser.ts import { setupWorker } from "msw/browser"; import { handlers } from "./handlers"; diff --git a/mock/constants.ts b/mock/constants.ts new file mode 100644 index 0000000..5e64ec5 --- /dev/null +++ b/mock/constants.ts @@ -0,0 +1,61 @@ +export const users = [ + { + merchantId: 100987998, + id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9", + mame: "Jacob", + username: "lspaddy", + firstName: "Paddy", + lastName: "Man", + email: "patrick@omegasys.eu", + phone: "", + jobTitle: "", + enabled: true, + authorities: [ + "ROLE_IIN", + "ROLE_FIRST_APPROVER", + "ROLE_RULES_ADMIN", + "ROLE_TRANSACTION_VIEWER", + "ROLE_IIN_ADMIN", + "ROLE_USER_PSP_ACCOUNT", + ], + allowedMerchantIds: [100987998], + created: "2025-05-04T15:32:48.432Z", + disabledBy: null, + disabledDate: null, + disabledReason: null, + incidentNotes: false, + lastLogin: "", + lastMandatoryUpdated: "2025-05-04T15:32:48.332Z", + marketingNewsletter: false, + releaseNotes: false, + requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"], + twoFactorCondition: "required", + twoFactorCredentials: [], + }, + { + merchantId: 100987998, + mame: "Jacob", + id: "382eed15-1e21-41fa-b1f3-0c1adb3af714", + username: "lsterence", + firstName: "Terence", + lastName: "User", + email: "terence@omegasys.eu", + phone: "", + jobTitle: "", + enabled: true, + authorities: ["ROLE_IIN", "ROLE_FIRST_APPROVER", "ROLE_RULES_ADMIN"], + allowedMerchantIds: [100987998], + created: "2025-05-04T15:32:48.432Z", + disabledBy: null, + disabledDate: null, + disabledReason: null, + incidentNotes: false, + lastLogin: "", + lastMandatoryUpdated: "2025-05-04T15:32:48.332Z", + marketingNewsletter: false, + releaseNotes: false, + requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"], + twoFactorCondition: "required", + twoFactorCredentials: [], + }, +]; diff --git a/mock/handlers.ts b/mock/handlers.ts index 6db727a..b67318f 100644 --- a/mock/handlers.ts +++ b/mock/handlers.ts @@ -1,96 +1,108 @@ import { http, HttpResponse } from "msw"; export const handlers = [ - http.get( - "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary", - (req, _res, _ctx) => { - const merchantId = req.url.searchParams.get("merchantId"); - const fromDate = req.url.searchParams.get("fromDate"); - const toDate = req.url.searchParams.get("toDate"); + // Simple GET endpoint + http.get("https://api.example.com/user", () => { + return HttpResponse.json([ + { + id: "usr_123", + name: "John Doe", + email: "john@example.com", + }, + ]); + }), - console.log(merchantId, fromDate, toDate); + // POST endpoint with request validation + http.post("https://api.example.com/login", async ({ request }) => { + const { username, password } = (await request.json()) as { + username: string; + password: string; + }; + if (username === "admin" && password === "password123") { return HttpResponse.json({ - result: { - txCount: { total: 0, successful: 0 }, - amount: { value: "0", currency: "EUR" }, - }, + token: "mock-jwt-token", + user: { id: "usr_123", name: "Admin User" }, }); } - ), - http.get( - "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/users/", - (req, _res, _ctx) => { - // Mock data for merchantId = 100987998 - if (true) { - return HttpResponse.json({ - result: [ - { - merchantId: 100987998, - id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9", - username: "lspaddy", - firstName: "Paddy", - lastName: "Man", - email: "patrick@omegasys.eu", - phone: "", - jobTitle: "", - enabled: true, - authorities: [ - "ROLE_IIN", - "ROLE_FIRST_APPROVER", - "ROLE_RULES_ADMIN", - "ROLE_TRANSACTION_VIEWER", - "ROLE_IIN_ADMIN", - "ROLE_USER_PSP_ACCOUNT", - ], - allowedMerchantIds: [100987998], - created: "2025-05-04T15:32:48.432Z", - disabledBy: null, - disabledDate: null, - disabledReason: null, - incidentNotes: false, - lastLogin: "", - lastMandatoryUpdated: "2025-05-04T15:32:48.332Z", - marketingNewsletter: false, - releaseNotes: false, - requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"], - twoFactorCondition: "required", - twoFactorCredentials: [], - }, - { - merchantId: 100987998, - id: "382eed15-1e21-41fa-b1f3-0c1adb3af714", - username: "lsterence", - firstName: "Terence", - lastName: "User", - email: "terence@omegasys.eu", - phone: "", - jobTitle: "", - enabled: true, - authorities: [ - "ROLE_IIN", - "ROLE_FIRST_APPROVER", - "ROLE_RULES_ADMIN", - ], - allowedMerchantIds: [100987998], - created: "2025-05-04T15:32:48.432Z", - disabledBy: null, - disabledDate: null, - disabledReason: null, - incidentNotes: false, - lastLogin: "", - lastMandatoryUpdated: "2025-05-04T15:32:48.332Z", - marketingNewsletter: false, - releaseNotes: false, - requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"], - twoFactorCondition: "required", - twoFactorCredentials: [], - }, - // Add more users if needed - ], - total: 4, - }); - } + + return HttpResponse.json({ error: "Invalid credentials" }, { status: 401 }); + }), + + // Example with query parameters + http.get("https://api.example.com/products", ({ request }) => { + // Parse the URL to access query parameters + const url = new URL(request.url); + + // Get query parameters + const category = url.searchParams.get("category"); + const sort = url.searchParams.get("sort") || "price"; + const page = url.searchParams.get("page") || "1"; + const limit = url.searchParams.get("limit") || "10"; + + // Validate parameters + if (limit && parseInt(limit) > 100) { + return HttpResponse.json( + { error: "Limit cannot exceed 100" }, + { status: 400 } + ); } - ), + + // Generate mock response based on parameters + const mockProducts = Array.from({ length: parseInt(limit) }, (_, i) => ({ + id: i + 1, + name: `Product ${i + 1}${category ? ` in ${category}` : ""}`, + price: Math.floor(Math.random() * 100), + category: category || "general", + })); + + // Sort products if sort parameter provided + if (sort === "price") { + mockProducts.sort((a, b) => a.price - b.price); + } else if (sort === "name") { + mockProducts.sort((a, b) => a.name.localeCompare(b.name)); + } + + return HttpResponse.json({ + products: mockProducts, + page: parseInt(page), + totalPages: 5, + itemsPerPage: parseInt(limit), + sortBy: sort, + }); + }), + http.get("https://api.example.com/transactions", ({ request }) => { + const url = new URL(request.url); + + // Get query parameters + const userId = url.searchParams.get("userId"); + const state = url.searchParams.get("state"); + const statusCode = url.searchParams.get("statusCode"); + + // Filter transactions based on query parameters + let filteredTransactions = [...transactionDummyData]; + + if (userId) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.user.toString() === userId + ); + } + + if (state) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.state.toLowerCase() === state.toLowerCase() + ); + } + + if (statusCode) { + filteredTransactions = filteredTransactions.filter( + (tx) => tx.pspStatusCode.toString() === statusCode + ); + } + + return HttpResponse.json({ + transactions: filteredTransactions, + count: filteredTransactions.length, + }); + }), ]; diff --git a/mock/server.ts b/mock/server.ts new file mode 100644 index 0000000..719bd8c --- /dev/null +++ b/mock/server.ts @@ -0,0 +1,5 @@ +// mocks/server.ts +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/next.config.ts b/next.config.ts index e9ffa30..0030107 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,12 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + webpack: (config) => { + if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") { + config.resolve.alias["@mswjs/interceptors"] = false; + } + return config; + }, }; export default nextConfig; diff --git a/package.json b/package.json index d3f094a..c37bf54 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "msw-init": "msw init public/ --save", "dev": "next dev --turbopack", "build": "next build", "start": "next start", @@ -15,6 +16,7 @@ "@mui/material": "^7.1.2", "@mui/x-data-grid": "^8.5.2", "@mui/x-date-pickers": "^8.5.3", + "@reduxjs/toolkit": "^2.8.2", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.13", @@ -23,6 +25,7 @@ "react": "^19.0.0", "react-date-range": "^2.0.1", "react-dom": "^19.0.0", + "react-redux": "^9.2.0", "recharts": "^2.15.3", "sass": "^1.89.2", "xlsx": "^0.18.5" @@ -34,6 +37,7 @@ "@types/react": "^19", "@types/react-date-range": "^1.4.10", "@types/react-dom": "^19", + "@types/react-redux": "^7.1.34", "eslint": "^9", "eslint-config-next": "15.3.3", "msw": "^2.10.2", @@ -44,4 +48,4 @@ "public" ] } -} \ No newline at end of file +} diff --git a/services/roles.services.ts b/services/roles.services.ts new file mode 100644 index 0000000..4f14b61 --- /dev/null +++ b/services/roles.services.ts @@ -0,0 +1,15 @@ +import { IEditUserForm } from "@/app/features/UserRoles/User.interfaces"; + +export async function createRole(data: IEditUserForm) { + const res = await fetch("/api/dashboard/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + + if (!res.ok) { + throw new Error("Failed to create role"); + } + + return res.json(); // or return type depending on your backend +} diff --git a/styles/globals.scss b/styles/globals.scss index 366294f..940adbe 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -11,6 +11,8 @@ --text-tertiary: #{$text-tertiary}; --hover-color: #{$hover-color}; --font-family-base: #{$font-family-base}; + --button-primary: #{$button-primary}; + --button-secondary: #{$button-secondary}; } @media (prefers-color-scheme: dark) { diff --git a/styles/variables.scss b/styles/variables.scss index d8311d9..5b4eca0 100644 --- a/styles/variables.scss +++ b/styles/variables.scss @@ -5,6 +5,8 @@ $background-primary: rgb(69, 190, 171); $text-primary: #000000; $text-secondary: #555555; $text-tertiary: #ffffff; +$button-primary: #0070f3; +$button-secondary: #ff00ff; $hover-color: rgba(0, 0, 0, 0.08); $font-family-base: "Roboto", "Helvetica", "Arial", sans-serif; diff --git a/yarn.lock b/yarn.lock index cc739b9..2400af5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,7 +47,7 @@ dependencies: "@babel/types" "^7.27.3" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.27.1", "@babel/runtime@^7.27.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.27.1", "@babel/runtime@^7.27.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.27.6" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz" integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== @@ -850,6 +850,18 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@reduxjs/toolkit@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.8.2.tgz#f4e9f973c6fc930c1e0f3bf462cc95210c28f5f9" + integrity sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A== + dependencies: + "@standard-schema/spec" "^1.0.0" + "@standard-schema/utils" "^0.3.0" + immer "^10.0.3" + redux "^5.0.1" + redux-thunk "^3.1.0" + reselect "^5.1.0" + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" @@ -860,6 +872,16 @@ resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz" integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ== +"@standard-schema/spec@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" + integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== + +"@standard-schema/utils@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" + integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== + "@swc/counter@0.1.3": version "0.1.3" resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" @@ -945,6 +967,14 @@ resolved "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz" integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A== +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.6" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz#6bba74383cdab98e8db4e20ce5b4a6b98caed010" + integrity sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" @@ -985,6 +1015,16 @@ resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz" integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw== +"@types/react-redux@^7.1.34": + version "7.1.34" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" + integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.12": version "4.4.12" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" @@ -1007,6 +1047,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.34.1" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz" @@ -2420,7 +2465,7 @@ headers-polyfill@^4.0.2: resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== -hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -2437,6 +2482,11 @@ ignore@^7.0.0: resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== +immer@^10.0.3: + version "10.1.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" + integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== + immutable@^5.0.2: version "5.1.3" resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4" @@ -3174,6 +3224,14 @@ react-list@^0.8.13: resolved "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz" integrity sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw== +react-redux@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5" + integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== + dependencies: + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" + react-smooth@^4.0.4: version "4.0.4" resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz" @@ -3224,6 +3282,23 @@ recharts@^2.15.3: tiny-invariant "^1.3.1" victory-vendor "^36.6.8" +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + +redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" @@ -3260,7 +3335,7 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reselect@^5.1.1: +reselect@^5.1.0, reselect@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== @@ -3832,7 +3907,7 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -use-sync-external-store@^1.5.0: +use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==