Added user roles and transaction filtering
This commit is contained in:
parent
60af9a6e28
commit
6303feaf97
79
app/api/dashboard/admin/users/route.ts
Normal file
79
app/api/dashboard/admin/users/route.ts
Normal file
@ -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 });
|
||||||
|
}
|
||||||
191
app/api/transactions/deposit/mockData.ts
Normal file
191
app/api/transactions/deposit/mockData.ts
Normal file
@ -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" },
|
||||||
|
]
|
||||||
57
app/api/transactions/deposit/route.ts
Normal file
57
app/api/transactions/deposit/route.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
25
app/api/transactions/route.ts
Normal file
25
app/api/transactions/route.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
176
app/api/transactions/withdrawal/mockData.ts
Normal file
176
app/api/transactions/withdrawal/mockData.ts
Normal file
@ -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" },
|
||||||
|
];
|
||||||
45
app/api/transactions/withdrawal/route.ts
Normal file
45
app/api/transactions/withdrawal/route.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
53
app/components/Modal/Modal.scss
Normal file
53
app/components/Modal/Modal.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
48
app/components/Modal/Modal.tsx
Normal file
48
app/components/Modal/Modal.tsx
Normal file
@ -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<ModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={"modal__overlay"}
|
||||||
|
onClick={onClose}
|
||||||
|
data-testid="modal-overlay"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`modal${className ? " " + className : ""}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
data-testid="modal-content"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="modal__close"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
{title && <h2 className="modal__title">{title}</h2>}
|
||||||
|
<div className={"modal__body"}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
64
app/components/searchFilter/SearchFilters.tsx
Normal file
64
app/components/searchFilter/SearchFilters.tsx
Normal file
@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchFilters = ({ filters }: SearchFiltersProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const filterLabels: Record<string, string> = {
|
||||||
|
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) => (
|
||||||
|
<Chip
|
||||||
|
key={key}
|
||||||
|
label={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontWeight: key === "state" ? "bold" : "normal" }}
|
||||||
|
>
|
||||||
|
{label}: {value}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
onDelete={() => handleDeleteFilter(key)}
|
||||||
|
sx={{ mr: 1, mb: 1 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" alignItems="center" flexWrap="wrap" sx={{ p: 2 }}>
|
||||||
|
{Object.entries(filters).map(([key, value]) =>
|
||||||
|
value ? renderChip(filterLabels[key] ?? key, value, key) : null
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Object.values(filters).some(Boolean) && (
|
||||||
|
<Button
|
||||||
|
onClick={onClearAll}
|
||||||
|
sx={{ ml: 1, textDecoration: "underline", color: "black" }}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default SearchFilters;
|
||||||
103
app/components/test/test1.tsx
Normal file
103
app/components/test/test1.tsx
Normal file
@ -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 = () => (
|
||||||
|
<Box
|
||||||
|
sx={{ width: 400 }}
|
||||||
|
role="presentation"
|
||||||
|
onClick={toggleDrawer(false)}
|
||||||
|
onKeyDown={toggleDrawer(false)}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{["All mail", "Trash", "Spam"].map((text, index) => (
|
||||||
|
<ListItem key={text} disablePadding>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
borderRadius: "8px",
|
||||||
|
textTransform: "none",
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
|
color: "#555",
|
||||||
|
padding: "6px 12px",
|
||||||
|
boxShadow: "inset 0 0 0 1px #ddd",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
"& .MuiButton-startIcon": {
|
||||||
|
marginRight: "12px",
|
||||||
|
backgroundColor: "#eee",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#e0e0e0",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
startIcon={<SearchIcon />}
|
||||||
|
onClick={toggleDrawer(true)}
|
||||||
|
>
|
||||||
|
Advanced Search
|
||||||
|
</Button>
|
||||||
|
{/* <Button onClick={toggleDrawer(true)}>Open Right Drawer</Button> */}
|
||||||
|
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
|
||||||
|
{list()}
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
app/components/test/test2.tsx
Normal file
186
app/components/test/test2.tsx
Normal file
@ -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<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-xl font-bold mb-4">Transaction Search</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1">User ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={userId}
|
||||||
|
onChange={(e) => setUserId(e.target.value)}
|
||||||
|
className="border p-2 rounded text-sm"
|
||||||
|
placeholder="Filter by user ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1">State</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={state}
|
||||||
|
onChange={(e) => setState(e.target.value)}
|
||||||
|
className="border p-2 rounded text-sm"
|
||||||
|
placeholder="Filter by state"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1">Status Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={statusCode}
|
||||||
|
onChange={(e) => setStatusCode(e.target.value)}
|
||||||
|
className="border p-2 rounded text-sm"
|
||||||
|
placeholder="Filter by status code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end">
|
||||||
|
<button
|
||||||
|
onClick={fetchTransactions}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded text-sm"
|
||||||
|
>
|
||||||
|
{loading ? "Loading..." : "Search"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{transactions.length > 0 ? (
|
||||||
|
<div className="border rounded overflow-hidden">
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead className="bg-gray-100">
|
||||||
|
<tr>
|
||||||
|
<th className="py-2 px-4 text-left text-sm">ID</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm">User</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm">State</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm">Status Code</th>
|
||||||
|
<th className="py-2 px-4 text-left text-sm">Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.map((tx) => (
|
||||||
|
<tr key={tx.id} className="border-t">
|
||||||
|
<td className="py-2 px-4 text-sm">{tx.id}</td>
|
||||||
|
<td className="py-2 px-4 text-sm">{tx.user}</td>
|
||||||
|
<td className="py-2 px-4 text-sm">{tx.state}</td>
|
||||||
|
<td className="py-2 px-4 text-sm">{tx.pspStatusCode}</td>
|
||||||
|
<td className="py-2 px-4 text-sm">{tx.created}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 text-sm">
|
||||||
|
{loading ? "Loading transactions..." : "No transactions found"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
];
|
||||||
@ -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";
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Users />
|
<Users users={users} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/dashboard/transactions/deposits/page.tsx
Normal file
23
app/dashboard/transactions/deposits/page.tsx
Normal file
@ -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<Record<string, string | string[] | undefined>>;
|
||||||
|
}) {
|
||||||
|
// Await searchParams before processing
|
||||||
|
const params = await searchParams;
|
||||||
|
// Create a safe query string by filtering only string values
|
||||||
|
const safeParams: Record<string, string> = {};
|
||||||
|
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 <TransactionsTable res={data}/>;
|
||||||
|
}
|
||||||
23
app/dashboard/transactions/history/page.tsx
Normal file
23
app/dashboard/transactions/history/page.tsx
Normal file
@ -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<Record<string, string | string[] | undefined>>;
|
||||||
|
}) {
|
||||||
|
// Await searchParams before processing
|
||||||
|
const params = await searchParams;
|
||||||
|
// Create a safe query string by filtering only string values
|
||||||
|
const safeParams: Record<string, string> = {};
|
||||||
|
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 <TransactionsTable res={data}/>;
|
||||||
|
}
|
||||||
23
app/dashboard/transactions/withdrawals/page.tsx
Normal file
23
app/dashboard/transactions/withdrawals/page.tsx
Normal file
@ -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<Record<string, string | string[] | undefined>>;
|
||||||
|
}) {
|
||||||
|
// Await searchParams before processing
|
||||||
|
const params = await searchParams;
|
||||||
|
// Create a safe query string by filtering only string values
|
||||||
|
const safeParams: Record<string, string> = {};
|
||||||
|
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 <TransactionsTable res={data}/>;
|
||||||
|
}
|
||||||
186
app/features/AdvancedSearch/AdvancedSearch.tsx
Normal file
186
app/features/AdvancedSearch/AdvancedSearch.tsx
Normal file
@ -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<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialParams = Object.fromEntries(searchParams.entries());
|
||||||
|
setFormValues(initialParams);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
|
||||||
|
const updateURL = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((newValues: Record<string, string>) => {
|
||||||
|
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 (
|
||||||
|
<Box sx={{ width: '185px' }}>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
borderRadius: "8px",
|
||||||
|
textTransform: "none",
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
|
color: "#555",
|
||||||
|
padding: "6px 12px",
|
||||||
|
boxShadow: "inset 0 0 0 1px #ddd",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
"& .MuiButton-startIcon": {
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#e0e0e0",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
startIcon={<SearchIcon />}
|
||||||
|
onClick={toggleDrawer(true)}
|
||||||
|
>
|
||||||
|
Advanced Search
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
|
||||||
|
<Box sx={{ width: 400 }} role="presentation">
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
|
<Box p={2}>
|
||||||
|
<Box sx={{ display: "flex", gap: "60px" }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Search
|
||||||
|
</Typography>
|
||||||
|
<Box display="flex" justifyContent="flex-end" gap={2}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<SearchIcon />}
|
||||||
|
onClick={() => console.log("Params:", formValues)}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={resetForm}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{labels?.map(({ label, field, type, options }) => (
|
||||||
|
<Box key={field}>
|
||||||
|
<Typography variant="body2" fontWeight={600} mb={0.5}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{type === "text" && (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
value={formValues[field] || ""}
|
||||||
|
onChange={(e) => handleFieldChange(field, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "select" && (
|
||||||
|
<FormControl fullWidth size="small">
|
||||||
|
<Select
|
||||||
|
value={formValues[field] || ""}
|
||||||
|
onChange={(e) => handleFieldChange(field, e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>{label}</em>
|
||||||
|
</MenuItem>
|
||||||
|
{options?.map((option) => (
|
||||||
|
<MenuItem value={option} key={option}>
|
||||||
|
{option}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "date" && (
|
||||||
|
<DatePicker
|
||||||
|
value={formValues[field] ? new Date(formValues[field]) : null}
|
||||||
|
onChange={(newValue) =>
|
||||||
|
handleFieldChange(
|
||||||
|
field,
|
||||||
|
newValue?.toISOString() || ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
slotProps={{
|
||||||
|
textField: { fullWidth: true, size: "small" }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,92 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Card, CardContent, Typography, Chip, Stack } from "@mui/material";
|
import { Card, CardContent, Typography, Stack } from "@mui/material";
|
||||||
import { IUser } from "./interfaces";
|
import { IUser } from "./interfaces";
|
||||||
|
import UserTopBar from "@/app/features/UserRoles/AddUser/AddUser";
|
||||||
|
import EditUser from "@/app/features/UserRoles/EditUser/EditUser";
|
||||||
|
import Modal from "@/app/components/Modal/Modal";
|
||||||
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
||||||
|
|
||||||
const Users = () => {
|
interface UsersProps {
|
||||||
const [data, setData] = useState([
|
users: IUser[];
|
||||||
{
|
}
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
// Add more users if needed
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const Users: React.FC<UsersProps> = ({ users }) => {
|
||||||
// Only run MSW in the browser environment
|
const [showAddUser, setShowAddUser] = useState(false);
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const fetchData = async () => {
|
|
||||||
const response = await fetch(
|
|
||||||
"https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/users/?includeSubMids=false&size=20&page=0&merchantId=100987998"
|
|
||||||
); // This would be intercepted by MSW in the browser
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("[DATA]", data);
|
|
||||||
// setData(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{data.map((user: IUser) => (
|
<UserTopBar onAddUser={() => setShowAddUser(true)} />
|
||||||
|
{users.map((user: IUser) => (
|
||||||
<Card key={user.id} sx={{ mb: 2 }}>
|
<Card key={user.id} sx={{ mb: 2 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">{user.username}</Typography>
|
<Typography variant="h6">{user.username}</Typography>
|
||||||
@ -103,29 +34,22 @@ const Users = () => {
|
|||||||
isAdmin={true}
|
isAdmin={true}
|
||||||
lastLogin="small"
|
lastLogin="small"
|
||||||
roles={user.authorities}
|
roles={user.authorities}
|
||||||
// merchants={Numberuser.allowedMerchantIds}
|
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
||||||
/>
|
/>
|
||||||
{/* Add more chips or UI elements for other data */}
|
{/* Add more chips or UI elements for other data */}
|
||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={showAddUser}
|
||||||
|
onClose={() => setShowAddUser(false)}
|
||||||
|
title="Add User"
|
||||||
|
>
|
||||||
|
<EditUser />
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch data server-side using getServerSideProps
|
|
||||||
// export async function getServerSideProps() {
|
|
||||||
// // Replace this with your actual API call
|
|
||||||
// const res = await fetch("https://api.example.com/users");
|
|
||||||
// const data = await res.json();
|
|
||||||
|
|
||||||
// // Return the fetched data as props
|
|
||||||
// return {
|
|
||||||
// props: {
|
|
||||||
// result: data, // Assuming data is an array of users
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
export default Users;
|
export default Users;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
"use client";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { GeneralHealthCard } from "../../GeneralHealthCard/GeneralHealthCard";
|
import { GeneralHealthCard } from "../../GeneralHealthCard/GeneralHealthCard";
|
||||||
import { TransactionsWaitingApproval } from "../../TransactionsWaitingApproval/TransactionsWaitingApproval";
|
import { TransactionsWaitingApproval } from "../../TransactionsWaitingApproval/TransactionsWaitingApproval";
|
||||||
@ -6,53 +7,8 @@ import { Documentation } from "../../Documentation/Documentation";
|
|||||||
import { AccountIQ } from "../../AccountIQ/AccountIQ";
|
import { AccountIQ } from "../../AccountIQ/AccountIQ";
|
||||||
import { WhatsNew } from "../../WhatsNew/WhatsNew";
|
import { WhatsNew } from "../../WhatsNew/WhatsNew";
|
||||||
import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOverview";
|
import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOverview";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export const DashboardHomePage = () => {
|
export const DashboardHomePage = () => {
|
||||||
// useEffect to fetch data when the component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
// Construct the URL with query parameters
|
|
||||||
const url =
|
|
||||||
"https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary?merchantId=100987998&fromDate=2025-06-29+19%3A10&toDate=2025-07-01+19%3A09";
|
|
||||||
|
|
||||||
// Perform the fetch request inside useEffect
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
// Start loading
|
|
||||||
|
|
||||||
// Make the API request
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET", // You can change this to 'POST' if necessary
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
// Add any other headers you need here
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if the response is OK (status code 200-299)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the JSON response
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("[DATA]", data);
|
|
||||||
|
|
||||||
// Update the state with the fetched data
|
|
||||||
// setTransactions(data.result || []);
|
|
||||||
} catch (err) {
|
|
||||||
// Handle any errors that occurred during the fetch
|
|
||||||
// setError("Failed to load data, please try again.");
|
|
||||||
console.error("Fetch error:", err);
|
|
||||||
} finally {
|
|
||||||
// Set loading to false when the request is complete
|
|
||||||
// setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the fetch function
|
|
||||||
fetchData();
|
|
||||||
}, []); // Empty dependency
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
|
|||||||
130
app/features/Pages/transactions/TransactionsTable.tsx
Normal file
130
app/features/Pages/transactions/TransactionsTable.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
"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 } from "@mui/x-data-grid";
|
||||||
|
import AdvancedSearch from "../../AdvancedSearch/AdvancedSearch";
|
||||||
|
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
|
||||||
|
import { exportData } from "@/app/utils/exportData";
|
||||||
|
import { ITransaction } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
const paginationModel = { page: 0, pageSize: 50 };
|
||||||
|
|
||||||
|
interface IDepositProps {
|
||||||
|
res: ITransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransactionsTable = ({ res }: IDepositProps) => {
|
||||||
|
const {filteredTransactions, transactionsColumns, transactionsSearchLabels} = res;
|
||||||
|
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 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()
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
p={2}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label="Search"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => console.log(`setSearchQuery(${e.target.value})`)}
|
||||||
|
sx={{ width: 300 }}
|
||||||
|
/>
|
||||||
|
<AdvancedSearch labels={transactionsSearchLabels} />
|
||||||
|
<SearchFilters filters={filters} />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileUploadIcon />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<DataGrid
|
||||||
|
rows={filteredTransactions}
|
||||||
|
columns={transactionsColumns}
|
||||||
|
initialState={{ pagination: { paginationModel } }}
|
||||||
|
pageSizeOptions={[50, 100]}
|
||||||
|
sx={{ border: 0, cursor: 'pointer' }}
|
||||||
|
|
||||||
|
onCellClick={(params) => {
|
||||||
|
handleClickField(params.field, params.value as string)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Export Dialog */}
|
||||||
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||||
|
<DialogTitle>Export Transactions</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||||
|
<Select
|
||||||
|
value={fileType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFileType(e.target.value as "csv" | "xls" | "xlsx")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="csv">CSV</MenuItem>
|
||||||
|
<MenuItem value="xls">XLS</MenuItem>
|
||||||
|
<MenuItem value="xlsx">XLSX</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={onlyCurrentTable}
|
||||||
|
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Only export the results in the current table"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||||
|
<Button variant="contained" onClick={() => exportData(filteredTransactions as unknown as ITransaction[], transactionsColumns, fileType, onlyCurrentTable, setOpen)}>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionsTable
|
||||||
@ -1,80 +1,168 @@
|
|||||||
import { GridColDef } from "@mui/x-data-grid";
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
|
|
||||||
export const columns: GridColDef[] = [
|
export const columns: GridColDef[] = [
|
||||||
{ field: 'merchandId', headerName: 'Merchant ID', width: 130 },
|
{ field: "merchandId", headerName: "Merchant ID", width: 130 },
|
||||||
{ field: 'transactionID', headerName: 'Transaction ID', width: 130 },
|
{ field: "transactionID", headerName: "Transaction ID", width: 130 },
|
||||||
{ field: 'user', headerName: 'User', width: 75 },
|
{ field: "user", headerName: "User", width: 75 },
|
||||||
{ field: 'created', headerName: 'Created', type: 'number', width: 130 },
|
{ field: "created", headerName: "Created", type: "number", width: 130 },
|
||||||
{ field: 'state', headerName: 'State', 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: "statusDescription",
|
||||||
{ field: 'pspStatusMessage', headerName: 'PSP Status Message', type: 'number', width: 90 },
|
headerName: "Status Description",
|
||||||
{ field: 'psp', headerName: 'PSP', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'pspAccount', headerName: 'PSP Account', type: 'number', width: 90 },
|
width: 130,
|
||||||
{ 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: "statusCode",
|
||||||
{ field: 'transactionType', headerName: 'Transaction Type', type: 'number', width: 90 },
|
headerName: "Status Code",
|
||||||
{ field: 'paymentMethod', headerName: 'Payment Method', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'rules', headerName: 'Rules', type: 'number', width: 90 },
|
width: 130,
|
||||||
{ 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: "pspStatusCode",
|
||||||
{ field: 'baseAmount', headerName: 'Base Amount', type: 'number', width: 90 },
|
headerName: "PSP Status Code",
|
||||||
{ field: 'baseFee', headerName: 'Base Fee', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'baseTransaction', headerName: 'Base Transaction', type: 'number', width: 90 },
|
width: 130,
|
||||||
{ 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: "pspStatusMessage",
|
||||||
{ field: 'baseAuthAmount', headerName: 'Base Auth Amount', type: 'number', width: 90 },
|
headerName: "PSP Status Message",
|
||||||
{ field: 'userBalance', headerName: 'User Balance', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'updated', headerName: 'Updated', type: 'number', width: 90 },
|
width: 90,
|
||||||
{ field: 'userIp', headerName: 'User IP', type: 'number', width: 90 },
|
},
|
||||||
{ field: 'channel', headerName: 'Channel', type: 'number', width: 90 },
|
{ field: "psp", headerName: "PSP", type: "number", width: 90 },
|
||||||
{ field: 'depositType', headerName: 'Deposit Type', type: 'number', width: 90 },
|
{ field: "pspAccount", headerName: "PSP Account", type: "number", width: 90 },
|
||||||
{ field: 'userEmal', headerName: 'User Emal', type: 'number', width: 90 },
|
{ field: "initPSP", headerName: "Init PSP", type: "number", width: 90 },
|
||||||
{ field: 'userCategory', headerName: 'User Category', type: 'number', width: 90 },
|
{
|
||||||
{ field: 'userCountry', headerName: 'User Country', type: 'number', width: 90 },
|
field: "initPSPAccout",
|
||||||
{ field: 'userAccount', headerName: 'User Account', type: 'number', width: 90 },
|
headerName: "Init PSP Account",
|
||||||
{ field: 'bankName', headerName: 'Bank Name', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'pspUserReference', headerName: 'PSP User Reference', type: 'number', width: 90 },
|
width: 90,
|
||||||
{ field: 'pspFraudScore', headerName: 'PSP Fraud Score', type: 'number', width: 90 },
|
},
|
||||||
{ field: 'fraudStatus', headerName: 'FraudStatus', type: 'number', width: 90 },
|
{ field: "pspService", headerName: "PSP Service", type: "number", width: 90 },
|
||||||
{ field: 'blocked', headerName: 'Blocked', type: 'number', width: 90 },
|
{
|
||||||
{ field: 'abuse', headerName: 'Abuse', type: 'number', width: 90 },
|
field: "transactionType",
|
||||||
{ field: 'kycStatus', headerName: 'KYC Status', type: 'number', width: 90 },
|
headerName: "Transaction Type",
|
||||||
{ field: 'kycPSPName', headerName: 'KYC PSP Name', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'kycPSPStatus', headerName: 'KYC PSP Status', type: 'number', width: 90 },
|
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: "paymentMethod",
|
||||||
{ field: 'kycPEPAndSanction', headerName: 'KYC PEP And Sanction', type: 'number', width: 90 },
|
headerName: "Payment Method",
|
||||||
{ field: 'pspReferenceId', headerName: 'PSPReferenceID', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'siteReferenceId', headerName: 'Site Reference ID', type: 'number', width: 90 },
|
width: 90,
|
||||||
{ field: 'info', headerName: 'Info', type: 'number', width: 90 },
|
},
|
||||||
{ field: 'accountHolder', headerName: 'Account Holder', type: 'number', width: 90 },
|
{ field: "rules", headerName: "Rules", type: "number", width: 90 },
|
||||||
{ field: 'firstName', headerName: 'First Name', type: 'number', width: 90 },
|
{ field: "amount", headerName: "Amount", type: "number", width: 90 },
|
||||||
{ field: 'lastName', headerName: 'Last Name', type: 'number', width: 90 },
|
{ field: "fee", headerName: "Fee", type: "number", width: 90 },
|
||||||
{ field: 'street', headerName: 'Street', type: 'number', width: 90 },
|
{
|
||||||
{ field: 'city', headerName: 'City', type: 'number', width: 90 },
|
field: "transactionAmount",
|
||||||
{ field: 'zip', headerName: 'ZIP', type: 'number', width: 90 },
|
headerName: "Transaction Amount",
|
||||||
{ field: 'dob', headerName: 'DOB', type: 'number', width: 90 },
|
type: "number",
|
||||||
{ field: 'mobile', headerName: 'Mobile', type: 'number', width: 90 },
|
width: 90,
|
||||||
{ field: 'lastUpdatedBy', headerName: 'Last Updated By', type: 'number', width: 90 },
|
},
|
||||||
{ field: 'ipCity', headerName: 'IP City', type: 'number', width: 90 },
|
// { field: 'baseAmount', headerName: 'Base Amount', type: 'number', width: 90 },
|
||||||
{ field: 'ipRegion', headerName: 'IP Region', type: 'number', width: 90 },
|
// { field: 'baseFee', headerName: 'Base Fee', type: 'number', width: 90 },
|
||||||
{ field: 'ipCountry', headerName: 'IP Country', type: 'number', width: 90 },
|
// { field: 'baseTransaction', headerName: 'Base Transaction', type: 'number', width: 90 },
|
||||||
{ field: 'cardIssuerCountry', headerName: 'Card Issuer Country', type: 'number', width: 90 },
|
// { field: 'pspFee', headerName: 'PSP Fee', type: 'number', width: 90 },
|
||||||
{ field: 'cardBand', headerName: 'Card Band', type: 'number', width: 90 },
|
// { field: 'basePspFee', headerName: 'Base PSP Fee', type: 'number', width: 90 },
|
||||||
{ field: 'cardCategory', headerName: 'Card Category', type: 'number', width: 90 },
|
// { field: 'authAmount', headerName: 'Auth Amount', type: 'number', width: 90 },
|
||||||
{ field: 'cardIssuerName', headerName: 'Card Issuer Name', type: 'number', width: 90 },
|
// { field: 'baseAuthAmount', headerName: 'Base Auth Amount', type: 'number', width: 90 },
|
||||||
{ field: 'inn', headerName: 'INN', type: 'number', width: 90 },
|
// { field: 'userBalance', headerName: 'User Balance', type: 'number', width: 90 },
|
||||||
{ field: 'cardType', headerName: 'Card Type', type: 'number', width: 90 },
|
// { field: 'updated', headerName: 'Updated', type: 'number', width: 90 },
|
||||||
{ field: 'firstAttempt', headerName: 'First Attempt', type: 'number', width: 90 },
|
// { field: 'userIp', headerName: 'User IP', type: 'number', width: 90 },
|
||||||
{ field: 'firstSuccessful', headerName: 'First Successful', type: 'number', width: 90 },
|
// { field: 'channel', headerName: 'Channel', type: 'number', width: 90 },
|
||||||
{ field: 'firstTransaction', headerName: 'First Transaction', type: 'number', width: 90 },
|
// { field: 'depositType', headerName: 'Deposit Type', type: 'number', width: 90 },
|
||||||
{ field: 'firstPspAcountAttempt', headerName: 'First PSP Acount Attempt', type: 'number', width: 90 },
|
// { field: 'userEmal', headerName: 'User Emal', type: 'number', width: 90 },
|
||||||
{ field: 'firstPspAcountSuccessful', headerName: 'First PSP Acount Successful', type: 'number', width: 90 },
|
// { field: 'userCategory', headerName: 'User Category', type: 'number', width: 90 },
|
||||||
{ field: 'originTransactionId', headerName: 'Origin Transaction ID', type: 'number', width: 90 },
|
// { field: 'userCountry', headerName: 'User Country', type: 'number', width: 90 },
|
||||||
{ field: 'transactionReferenceId', headerName: 'Transaction Reference ID', 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" },
|
||||||
|
]
|
||||||
|
|||||||
32
app/features/Pages/transactions/types.ts
Normal file
32
app/features/Pages/transactions/types.ts
Normal file
@ -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[];
|
||||||
|
}
|
||||||
34
app/features/UserRoles/AddUser/AddUser.scss
Normal file
34
app/features/UserRoles/AddUser/AddUser.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/features/UserRoles/AddUser/AddUser.tsx
Normal file
32
app/features/UserRoles/AddUser/AddUser.tsx
Normal file
@ -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 (
|
||||||
|
<div className="add-user">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAddUser}
|
||||||
|
className="add-user__button add-user__button--primary"
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onExport}
|
||||||
|
className="add-user__button add-user__button--secondary"
|
||||||
|
disabled
|
||||||
|
title="Export to Excel (coming soon)"
|
||||||
|
>
|
||||||
|
Export to Excel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUser;
|
||||||
46
app/features/UserRoles/EditUser/EditUser.scss
Normal file
46
app/features/UserRoles/EditUser/EditUser.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/features/UserRoles/EditUser/EditUser.tsx
Normal file
107
app/features/UserRoles/EditUser/EditUser.tsx
Normal file
@ -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<IEditUserForm>({
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
role: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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<HTMLFormElement>) => {
|
||||||
|
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 (
|
||||||
|
<form className="edit-user" onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="First Name"
|
||||||
|
name="firstName"
|
||||||
|
value={form.firstName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
name="lastName"
|
||||||
|
value={form.lastName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
name="email"
|
||||||
|
value={form.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Role"
|
||||||
|
name="role"
|
||||||
|
value={form.role}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder="Phone"
|
||||||
|
name="phone"
|
||||||
|
value={form.phone}
|
||||||
|
maxLength={15}
|
||||||
|
pattern="[0-9+\-\s()]*"
|
||||||
|
onChange={handleChange}
|
||||||
|
inputMode="tel"
|
||||||
|
autoComplete="tel"
|
||||||
|
/>
|
||||||
|
<div className="edit-user__button-container">
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
<button type="button" onClick={handleResetForm}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUser;
|
||||||
14
app/features/UserRoles/User.interfaces.ts
Normal file
14
app/features/UserRoles/User.interfaces.ts
Normal file
@ -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";
|
||||||
17
app/features/UserRoles/User.scss
Normal file
17
app/features/UserRoles/User.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
app/features/UserRoles/User.types.ts
Normal file
0
app/features/UserRoles/User.types.ts
Normal file
@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -18,6 +19,8 @@ import {
|
|||||||
AdminPanelSettings,
|
AdminPanelSettings,
|
||||||
History,
|
History,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import EditUser from "./EditUser/EditUser";
|
||||||
|
import "./User.scss";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
username: string;
|
username: string;
|
||||||
@ -40,12 +43,18 @@ export default function UserRoleCard({
|
|||||||
roles,
|
roles,
|
||||||
extraRolesCount,
|
extraRolesCount,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setIsEditing(!isEditing);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ mb: 2, minWidth: "100%" }}>
|
<Card sx={{ mb: 2, minWidth: "100%" }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Stack direction="row" alignItems="center" spacing={2}>
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
<Avatar>{username.slice(0, 2).toUpperCase()}</Avatar>
|
<Avatar>{username?.slice(0, 2).toUpperCase()}</Avatar>
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Typography fontWeight="bold">{username}</Typography>
|
<Typography fontWeight="bold">{username}</Typography>
|
||||||
<Typography variant="body2">{name}</Typography>
|
<Typography variant="body2">{name}</Typography>
|
||||||
@ -58,7 +67,7 @@ export default function UserRoleCard({
|
|||||||
<History />
|
<History />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Tooltip title="Edit">
|
<Tooltip title="Edit">
|
||||||
<IconButton>
|
<IconButton onClick={handleEditClick}>
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -91,7 +100,7 @@ export default function UserRoleCard({
|
|||||||
|
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Typography fontWeight="bold">
|
<Typography fontWeight="bold">
|
||||||
Roles{" "}
|
Roles
|
||||||
<Tooltip title="Roles assigned to this user">
|
<Tooltip title="Roles assigned to this user">
|
||||||
<InfoOutlined fontSize="small" />
|
<InfoOutlined fontSize="small" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -105,13 +114,13 @@ export default function UserRoleCard({
|
|||||||
{extraRolesCount && <Chip label={`+${extraRolesCount}`} />}
|
{extraRolesCount && <Chip label={`+${extraRolesCount}`} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
<div
|
||||||
{/* Footer */}
|
className={`user-card__edit-transition${
|
||||||
<Box mt={2}>
|
isEditing ? " user-card__edit-transition--open" : ""
|
||||||
<Typography variant="caption" color="text.secondary">
|
}`}
|
||||||
{lastLogin}
|
>
|
||||||
</Typography>
|
{isEditing && <EditUser />}
|
||||||
</Box>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,14 +10,36 @@ import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
|
|||||||
import InsightsIcon from "@mui/icons-material/Insights";
|
import InsightsIcon from "@mui/icons-material/Insights";
|
||||||
import ListAltIcon from "@mui/icons-material/ListAlt";
|
import ListAltIcon from "@mui/icons-material/ListAlt";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
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";
|
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
|
||||||
|
|
||||||
export const PAGE_LINKS: ISidebarLink[] = [
|
export const PAGE_LINKS: ISidebarLink[] = [
|
||||||
{ title: "Home", path: "/dashboard", icon: HomeIcon },
|
{ title: "Home", path: "/dashboard", icon: HomeIcon },
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Transaction",
|
title: "Transaction",
|
||||||
path: "/dashboard/transactions",
|
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: "Approve", path: "/dashboard/approve", icon: CheckCircleIcon },
|
||||||
{ title: "Investigate", path: "/dashboard/investigate", icon: SearchIcon },
|
{ title: "Investigate", path: "/dashboard/investigate", icon: SearchIcon },
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import ThemeRegistry from "@/config/ThemeRegistry";
|
import ThemeRegistry from "@/config/ThemeRegistry";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "../styles/globals.scss";
|
import "../styles/globals.scss";
|
||||||
|
import { Providers } from "./providers/providers";
|
||||||
|
import { MSWProvider } from './providers/msw-provider';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Your App",
|
title: "Your App",
|
||||||
@ -15,7 +17,11 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<ThemeRegistry>{children}</ThemeRegistry>
|
<Providers>
|
||||||
|
<MSWProvider>
|
||||||
|
<ThemeRegistry>{children}</ThemeRegistry>
|
||||||
|
</MSWProvider>
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
14
app/providers/msw-provider.tsx
Normal file
14
app/providers/msw-provider.tsx
Normal file
@ -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}</>;
|
||||||
|
}
|
||||||
10
app/providers/providers.tsx
Normal file
10
app/providers/providers.tsx
Normal file
@ -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 <Provider store={store}>{children}</Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
56
app/redux/advanedSearch/advancedSearchSlice.ts
Normal file
56
app/redux/advanedSearch/advancedSearchSlice.ts
Normal file
@ -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;
|
||||||
|
|
||||||
11
app/redux/store.ts
Normal file
11
app/redux/store.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
3
app/redux/transactions/selectors.ts
Normal file
3
app/redux/transactions/selectors.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { RootState } from "../types";
|
||||||
|
|
||||||
|
export const selectTransactions = (state: RootState) => state.transactions.data;
|
||||||
83
app/redux/transactions/transactionsSlice.ts
Normal file
83
app/redux/transactions/transactionsSlice.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
5
app/redux/types.ts
Normal file
5
app/redux/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { store } from "./store";
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
13
app/services/transactions.ts
Normal file
13
app/services/transactions.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
50
app/test/page.tsx
Normal file
50
app/test/page.tsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<h1>MSW Test Page</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>User Data (GET)</h2>
|
||||||
|
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Login Test (POST)</h2>
|
||||||
|
<button onClick={handleLogin}>Login as Admin</button>
|
||||||
|
<p>{loginStatus}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
app/test1/page.tsx
Normal file
29
app/test1/page.tsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<SearchFilters
|
||||||
|
filters={filters}
|
||||||
|
onDeleteFilter={handleDeleteFilter}
|
||||||
|
onClearAll={handleClearAll}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
app/testWithParams/page.tsx
Normal file
97
app/testWithParams/page.tsx
Normal file
@ -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<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Product Search</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1">Category</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={category}
|
||||||
|
onChange={(e) => setCategory(e.target.value)}
|
||||||
|
className="border p-2 rounded"
|
||||||
|
placeholder="electronics, clothing, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1">Sort By</label>
|
||||||
|
<select
|
||||||
|
value={sort}
|
||||||
|
onChange={(e) => setSort(e.target.value)}
|
||||||
|
className="border p-2 rounded"
|
||||||
|
>
|
||||||
|
<option value="price">Price</option>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1">Items Per Page</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={limit}
|
||||||
|
onChange={(e) => setLimit(e.target.value)}
|
||||||
|
className="border p-2 rounded"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={fetchProducts}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded self-end"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Search'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{products.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Results</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{products.map((product) => (
|
||||||
|
<div key={product.id} className="border p-4 rounded">
|
||||||
|
<h3 className="font-medium">{product.name}</h3>
|
||||||
|
<p>Category: {product.category}</p>
|
||||||
|
<p>Price: ${product.price}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
app/utils/exportData.ts
Normal file
37
app/utils/exportData.ts
Normal file
@ -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);
|
||||||
|
};
|
||||||
13
app/utils/formatDate.ts
Normal file
13
app/utils/formatDate.ts
Normal file
@ -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}`;
|
||||||
|
}
|
||||||
@ -1,39 +1,43 @@
|
|||||||
import { createTheme } from '@mui/material/styles';
|
import { createTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
const palette = {
|
const palette = {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#1976d2',
|
main: "#1976d2",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#d32f2f',
|
main: "#d32f2f",
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
default: '#fafafa',
|
default: "#fafafa",
|
||||||
paper: '#ffffff',
|
paper: "#ffffff",
|
||||||
primary: 'rgb(69, 190, 171)',
|
primary: "rgb(69, 190, 171)",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#000000',
|
primary: "#000000",
|
||||||
secondary: '#555555',
|
secondary: "#555555",
|
||||||
tertiary: '#fff',
|
tertiary: "#fff",
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
primary: "#0070f3",
|
||||||
|
secondary: "##FF00FF",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
hover: 'rgba(0, 0, 0, 0.08)',
|
hover: "rgba(0, 0, 0, 0.08)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const typography = {
|
const typography = {
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
h1: {
|
h1: {
|
||||||
fontSize: '3rem',
|
fontSize: "3rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
},
|
},
|
||||||
h2: {
|
h2: {
|
||||||
fontSize: '2.5rem',
|
fontSize: "2.5rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
},
|
},
|
||||||
body1: {
|
body1: {
|
||||||
fontSize: '1rem',
|
fontSize: "1rem",
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -41,8 +45,8 @@ const typography = {
|
|||||||
// Create the theme based on the light or dark mode preference
|
// Create the theme based on the light or dark mode preference
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'light', // Change this to 'dark' for dark mode
|
mode: "light", // Change this to 'dark' for dark mode
|
||||||
...palette
|
...palette,
|
||||||
},
|
},
|
||||||
// typography,
|
// typography,
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// mocks/browser.ts
|
|
||||||
import { setupWorker } from "msw/browser";
|
import { setupWorker } from "msw/browser";
|
||||||
import { handlers } from "./handlers";
|
import { handlers } from "./handlers";
|
||||||
|
|
||||||
|
|||||||
61
mock/constants.ts
Normal file
61
mock/constants.ts
Normal file
@ -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: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
184
mock/handlers.ts
184
mock/handlers.ts
@ -1,96 +1,108 @@
|
|||||||
import { http, HttpResponse } from "msw";
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
http.get(
|
// Simple GET endpoint
|
||||||
"https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary",
|
http.get("https://api.example.com/user", () => {
|
||||||
(req, _res, _ctx) => {
|
return HttpResponse.json([
|
||||||
const merchantId = req.url.searchParams.get("merchantId");
|
{
|
||||||
const fromDate = req.url.searchParams.get("fromDate");
|
id: "usr_123",
|
||||||
const toDate = req.url.searchParams.get("toDate");
|
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({
|
return HttpResponse.json({
|
||||||
result: {
|
token: "mock-jwt-token",
|
||||||
txCount: { total: 0, successful: 0 },
|
user: { id: "usr_123", name: "Admin User" },
|
||||||
amount: { value: "0", currency: "EUR" },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
),
|
|
||||||
http.get(
|
return HttpResponse.json({ error: "Invalid credentials" }, { status: 401 });
|
||||||
"https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/users/",
|
}),
|
||||||
(req, _res, _ctx) => {
|
|
||||||
// Mock data for merchantId = 100987998
|
// Example with query parameters
|
||||||
if (true) {
|
http.get("https://api.example.com/products", ({ request }) => {
|
||||||
return HttpResponse.json({
|
// Parse the URL to access query parameters
|
||||||
result: [
|
const url = new URL(request.url);
|
||||||
{
|
|
||||||
merchantId: 100987998,
|
// Get query parameters
|
||||||
id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
|
const category = url.searchParams.get("category");
|
||||||
username: "lspaddy",
|
const sort = url.searchParams.get("sort") || "price";
|
||||||
firstName: "Paddy",
|
const page = url.searchParams.get("page") || "1";
|
||||||
lastName: "Man",
|
const limit = url.searchParams.get("limit") || "10";
|
||||||
email: "patrick@omegasys.eu",
|
|
||||||
phone: "",
|
// Validate parameters
|
||||||
jobTitle: "",
|
if (limit && parseInt(limit) > 100) {
|
||||||
enabled: true,
|
return HttpResponse.json(
|
||||||
authorities: [
|
{ error: "Limit cannot exceed 100" },
|
||||||
"ROLE_IIN",
|
{ status: 400 }
|
||||||
"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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
),
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
5
mock/server.ts
Normal file
5
mock/server.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// mocks/server.ts
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
import { handlers } from './handlers';
|
||||||
|
|
||||||
|
export const server = setupServer(...handlers);
|
||||||
@ -2,6 +2,12 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
webpack: (config) => {
|
||||||
|
if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
|
||||||
|
config.resolve.alias["@mswjs/interceptors"] = false;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"msw-init": "msw init public/ --save",
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"@mui/material": "^7.1.2",
|
"@mui/material": "^7.1.2",
|
||||||
"@mui/x-data-grid": "^8.5.2",
|
"@mui/x-data-grid": "^8.5.2",
|
||||||
"@mui/x-date-pickers": "^8.5.3",
|
"@mui/x-date-pickers": "^8.5.3",
|
||||||
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@ -23,6 +25,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-date-range": "^2.0.1",
|
"react-date-range": "^2.0.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
@ -34,6 +37,7 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-date-range": "^1.4.10",
|
"@types/react-date-range": "^1.4.10",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-redux": "^7.1.34",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.3",
|
"eslint-config-next": "15.3.3",
|
||||||
"msw": "^2.10.2",
|
"msw": "^2.10.2",
|
||||||
@ -44,4 +48,4 @@
|
|||||||
"public"
|
"public"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
services/roles.services.ts
Normal file
15
services/roles.services.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@
|
|||||||
--text-tertiary: #{$text-tertiary};
|
--text-tertiary: #{$text-tertiary};
|
||||||
--hover-color: #{$hover-color};
|
--hover-color: #{$hover-color};
|
||||||
--font-family-base: #{$font-family-base};
|
--font-family-base: #{$font-family-base};
|
||||||
|
--button-primary: #{$button-primary};
|
||||||
|
--button-secondary: #{$button-secondary};
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|||||||
@ -5,6 +5,8 @@ $background-primary: rgb(69, 190, 171);
|
|||||||
$text-primary: #000000;
|
$text-primary: #000000;
|
||||||
$text-secondary: #555555;
|
$text-secondary: #555555;
|
||||||
$text-tertiary: #ffffff;
|
$text-tertiary: #ffffff;
|
||||||
|
$button-primary: #0070f3;
|
||||||
|
$button-secondary: #ff00ff;
|
||||||
$hover-color: rgba(0, 0, 0, 0.08);
|
$hover-color: rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
$font-family-base: "Roboto", "Helvetica", "Arial", sans-serif;
|
$font-family-base: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||||
|
|||||||
83
yarn.lock
83
yarn.lock
@ -47,7 +47,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.27.3"
|
"@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"
|
version "7.27.6"
|
||||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz"
|
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz"
|
||||||
integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==
|
integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==
|
||||||
@ -850,6 +850,18 @@
|
|||||||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
||||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
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":
|
"@rtsao/scc@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz"
|
||||||
integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==
|
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":
|
"@swc/counter@0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz"
|
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"
|
resolved "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz"
|
||||||
integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==
|
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":
|
"@types/json-schema@^7.0.15":
|
||||||
version "7.0.15"
|
version "7.0.15"
|
||||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
|
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"
|
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz"
|
||||||
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
|
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":
|
"@types/react-transition-group@^4.4.12":
|
||||||
version "4.4.12"
|
version "4.4.12"
|
||||||
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
|
||||||
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
|
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":
|
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||||
version "8.34.1"
|
version "8.34.1"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz"
|
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"
|
resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07"
|
||||||
integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==
|
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"
|
version "3.3.2"
|
||||||
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
|
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
@ -2437,6 +2482,11 @@ ignore@^7.0.0:
|
|||||||
resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz"
|
resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz"
|
||||||
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
|
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:
|
immutable@^5.0.2:
|
||||||
version "5.1.3"
|
version "5.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4"
|
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"
|
resolved "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz"
|
||||||
integrity sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw==
|
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:
|
react-smooth@^4.0.4:
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz"
|
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"
|
tiny-invariant "^1.3.1"
|
||||||
victory-vendor "^36.6.8"
|
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:
|
reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
|
||||||
version "1.0.10"
|
version "1.0.10"
|
||||||
resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz"
|
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"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
|
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
|
||||||
|
|
||||||
reselect@^5.1.1:
|
reselect@^5.1.0, reselect@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
|
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
|
||||||
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
|
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
|
||||||
@ -3832,7 +3907,7 @@ url-parse@^1.5.3:
|
|||||||
querystringify "^2.1.1"
|
querystringify "^2.1.1"
|
||||||
requires-port "^1.0.0"
|
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"
|
version "1.5.0"
|
||||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
|
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
|
||||||
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user