Modified transaction pages

This commit is contained in:
Mitchell Magro 2025-08-06 09:41:20 +02:00
parent c827917fd2
commit 6c68ca79e3
47 changed files with 3424 additions and 916 deletions

View File

@ -1,5 +1,12 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { SignJWT } from "jose";
// Secret key for JWT signing (in production, use environment variable)
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
// Token expiration time (in seconds)
const TOKEN_EXPIRY = 60 * 60 * 12; // 12 hours (in seconds)
// This is your POST handler for the login endpoint
export async function POST(request: Request) {
@ -15,20 +22,25 @@ export async function POST(request: Request) {
// Mock authentication for demonstration purposes:
if (email === "admin@example.com" && password === "password123") {
const authToken = "mock-jwt-token-12345"; // Replace with a real, securely generated token
// Create JWT token with expiration
const token = await new SignJWT({
email,
role: "admin",
iat: Math.floor(Date.now() / 1000), // issued at
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(Math.floor(Date.now() / 1000) + TOKEN_EXPIRY)
.sign(JWT_SECRET);
// Set the authentication token as an HTTP-only cookie
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
// which mitigates XSS attacks.
(
await // Set the authentication token as an HTTP-only cookie
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
// which mitigates XSS attacks.
cookies()
).set("auth_token", authToken, {
const cookieStore = await cookies();
cookieStore.set("auth_token", token, {
httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts
secure: process.env.NODE_ENV === "production", // Use secure in production (HTTPS)
maxAge: 60 * 60 * 24 * 7, // 1 week
maxAge: TOKEN_EXPIRY, // 24 hours
path: "/", // Available across the entire site
sameSite: "lax", // Protects against CSRF
});

View File

@ -0,0 +1,76 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import {
validateToken,
isTokenExpired,
getTimeUntilExpiration,
} from "@/app/utils/auth";
export async function GET() {
try {
const cookieStore = await cookies();
const token = cookieStore.get("auth_token")?.value;
if (!token) {
return NextResponse.json(
{
isAuthenticated: false,
message: "No authentication token found",
},
{ status: 401 }
);
}
// Validate the token
const payload = await validateToken(token);
if (!payload) {
return NextResponse.json(
{
isAuthenticated: false,
message: "Invalid authentication token",
},
{ status: 401 }
);
}
if (isTokenExpired(payload)) {
// Clear the expired cookie
cookieStore.delete("auth_token");
return NextResponse.json(
{
isAuthenticated: false,
message: "Authentication token has expired",
},
{ status: 401 }
);
}
// Token is valid and not expired
const timeUntilExpiration = getTimeUntilExpiration(payload);
return NextResponse.json({
isAuthenticated: true,
user: {
email: payload.email,
role: payload.role,
},
tokenInfo: {
issuedAt: new Date(payload.iat * 1000).toISOString(),
expiresAt: new Date(payload.exp * 1000).toISOString(),
timeUntilExpiration: timeUntilExpiration, // in seconds
expiresInHours: Math.floor(timeUntilExpiration / 3600),
},
});
} catch (error) {
console.error("Auth status check error:", error);
return NextResponse.json(
{
isAuthenticated: false,
message: "Internal server error",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,35 @@
export const users = [
{
merchantId: 100987998,
id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
name: "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: [],
},
];

View File

@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { users } from "../../mockData";
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params;
if (!id) {
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
}
const body = await request.json();
const { firstName, lastName, email, phone, role } = body;
// Find the user by id
const userIndex = users.findIndex((u) => u.id === id);
if (userIndex === -1) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Update the user fields
users[userIndex] = {
...users[userIndex],
firstName: firstName ?? users[userIndex].firstName,
lastName: lastName ?? users[userIndex].lastName,
email: email ?? users[userIndex].email,
phone: phone ?? users[userIndex].phone,
authorities: role ? [role] : users[userIndex].authorities,
};
return NextResponse.json(users[userIndex], { status: 200 });
}

View File

@ -1,41 +1,6 @@
// app/api/dashboard/admin/users/route.ts
import { NextRequest, NextResponse } from "next/server";
const users = [
{
merchantId: 100987998,
id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
name: "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: [],
},
];
import { users } from "../mockData";
export async function GET() {
return NextResponse.json(users);

View File

@ -0,0 +1,119 @@
import { TableColumn } from "@/app/features/Pages/Approve/Approve";
type UserRow = {
id: number;
merchantId: string;
txId: string;
userEmail: string;
kycStatus: string;
};
export const approveRows: UserRow[] = [
{
id: 17,
merchantId: "100987998",
txId: "1049078821",
userEmail: "dhkheni1@yopmail.com",
kycStatus: "N/A",
},
{
id: 12,
merchantId: "100987998",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 11,
merchantId: "100232399",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 10,
merchantId: "100232399",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 1,
merchantId: "100232399",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 2,
merchantId: "101907999",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 3,
merchantId: "101907999",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 4,
merchantId: "10552342",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 5,
merchantId: "10552342",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 6,
merchantId: "10552342",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 7,
merchantId: "10552342",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
{
id: 8,
merchantId: "10552342",
txId: "1049078822",
userEmail: "dhkheni2@yopmail.com",
kycStatus: "Pending",
},
];
export const approveColumns: TableColumn<UserRow>[] = [
{ field: "merchantId", headerName: "Merchant ID" },
{ field: "txId", headerName: "Transaction ID" },
{ field: "userEmail", headerName: "User Email" },
{ field: "kycStatus", headerName: "KYC Status" },
];
export const approveActions = [
{ value: "approve", label: "Approve" },
{ value: "reject", label: "Reject" },
{ value: "forceSuccessful", label: "Force Successful" },
{ value: "forceFiled", label: "Force Field" },
{ value: "forceInconsistent", label: "Force Inconsistent" },
{ value: "forceStatus", label: "Force Status" },
{ value: "partialCapture", label: "Partial Capture" },
{ value: "capture", label: "Capture" },
{ value: "extendedAuth", label: "Extended uth" },
{ value: "partialRefund", label: "Partial Refund" },
{ value: "refund", label: "Refund" },
{ value: "void", label: "Void" },
{ value: "registerCorrection", label: "Register Correction" },
];

View File

@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { approveRows, approveColumns, approveActions } from "./mockData";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const merchantId = searchParams.get("merchantId");
let filteredApproveRows = [...approveRows];
if (merchantId) {
filteredApproveRows = filteredApproveRows.filter((tx) =>
tx.merchantId.toString().includes(merchantId),
);
}
return NextResponse.json({
rows: filteredApproveRows,
columns: approveColumns,
actions: approveActions,
});
}

View File

@ -1,6 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData";
import { formatToDateTimeString } from "@/app/utils/formatDate";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
@ -9,13 +8,16 @@ export async function GET(request: NextRequest) {
const affectedUserId = searchParams.get("affectedUserId");
const adminId = searchParams.get("adminId");
const adminUsername = searchParams.get("adminUsername");
const timeStampOfTheAction = searchParams.get("dateTime");
const dateTimeStart = searchParams.get("dateTime_start");
const dateTimeEnd = searchParams.get("dateTime_end");
let filteredRows = [...AuditData];
if (actionType) {
filteredRows = filteredRows.filter(
(tx) => tx.actionType.toLocaleLowerCase() === actionType.toLocaleLowerCase(),
(tx) =>
tx.actionType.toLocaleLowerCase() === actionType.toLocaleLowerCase(),
);
}
@ -26,9 +28,7 @@ export async function GET(request: NextRequest) {
}
if (adminId) {
filteredRows = filteredRows.filter(
(tx) => tx.adminId === adminId,
);
filteredRows = filteredRows.filter((tx) => tx.adminId === adminId);
}
if (adminUsername) {
filteredRows = filteredRows.filter(
@ -36,14 +36,32 @@ export async function GET(request: NextRequest) {
);
}
if (timeStampOfTheAction) {
filteredRows = filteredRows.filter(
(tx) =>
tx.timeStampOfTheAction.split(" ")[0] ===
formatToDateTimeString(timeStampOfTheAction).split(" ")[0],
if (dateTimeStart && dateTimeEnd) {
const start = new Date(dateTimeStart);
const end = new Date(dateTimeEnd);
// Validate the date range to ensure its correct
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return NextResponse.json(
{
error: "Invalid date range",
},
{ status: 400 },
);
}
filteredRows = filteredRows.filter((tx) => {
const txDate = new Date(tx.timeStampOfTheAction);
// Validate if the timestamp is a valid date
if (isNaN(txDate.getTime())) {
return false; // Skip invalid dates
}
return txDate >= start && txDate <= end;
});
}
return NextResponse.json({
tableRows: filteredRows,
tableColumns: AuditColumns,

View File

@ -0,0 +1,265 @@
import { GridColDef } from "@mui/x-data-grid";
export const allTransactionDummyData = [
{
id: 1,
userId: 17,
merchandId: 100987998,
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 2,
userId: 17,
merchandId: 100987998,
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 3,
userId: 17,
merchandId: 100987997,
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 4,
userId: 19,
merchandId: 100987997,
transactionId: 1049136973,
depositMethod: "Card",
status: "Completed",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 5,
userId: 19,
merchandId: 100987998,
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 6,
userId: 27,
merchandId: 100987997,
transactionId: 1049131973,
depositMethod: "Card",
status: "Pending",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 7,
userId: 175,
merchandId: 100987938,
transactionId: 1049136973,
depositMethod: "Card",
status: "Pending",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 8,
userId: 172,
merchandId: 100987938,
transactionId: 1049131973,
depositMethod: "Card",
status: "Pending",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-12 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 9,
userId: 174,
merchandId: 100987938,
transactionId: 1049131973,
depositMethod: "Bank Transfer",
status: "Inprogress",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 10,
userId: 7,
merchandId: 100987998,
transactionId: 1049131973,
depositMethod: "Bank Transfer",
status: "Inprogress",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
{
id: 11,
userId: 1,
merchandId: 100987998,
transactionId: 1049131973,
depositMethod: "Bank Transfer",
status: "Error",
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
fraudScore: "frad score 1234",
},
];
export const allTransactionsColumns: GridColDef[] = [
{ field: "userId", headerName: "User ID", width: 130 },
{ field: "merchandId", headerName: "Merchant ID", width: 130 },
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
{ field: "depositMethod", headerName: "Deposit Method", width: 130 },
{ field: "status", headerName: "Status", width: 130 },
// { field: "actions", headerName: "Actions", width: 150 },
{ field: "amount", headerName: "Amount", width: 130 },
{ field: "currency", headerName: "Currency", width: 130 },
{ field: "dateTime", headerName: "Date / Time", width: 130 },
{ field: "errorInfo", headerName: "Error Info", width: 130 },
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
];
export const allTransactionsExtraColumns: GridColDef[] = [
{ field: "currency", headerName: "Currency", width: 130 },
{ field: "errorInfo", headerName: "Error Info", width: 130 },
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
];
export const extraColumns = ["currency", "errorInfo", "fraudScore"]
export const allTransactionsSearchLabels = [
{ label: "User", field: "userId", type: "text" },
{ label: "Transaction ID", field: "transactionId", type: "text" },
{
label: "Transaction Reference ID",
field: "transactionReferenceId",
type: "text",
},
{
label: "Currency",
field: "currency",
type: "select",
options: ["USD", "EUR", "GBP"],
},
{
label: "Status",
field: "status",
type: "select",
options: ["Pending", "Inprogress", "Completed", "Failed"],
},
{
label: "Payment Method",
field: "depositMethod",
type: "select",
options: ["Card", "Bank Transfer"],
},
{ label: "Date / Time", field: "dateTime", type: "date" },
];

View File

@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from "next/server";
import { allTransactionDummyData, allTransactionsColumns, allTransactionsSearchLabels, extraColumns } from "./mockData";
// import { formatToDateTimeString } from "@/app/utils/formatDate";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const status = searchParams.get("status");
const userId = searchParams.get("userId");
const depositMethod = searchParams.get("depositMethod");
const merchandId = searchParams.get("merchandId");
const transactionId = searchParams.get("transactionId");
// const dateTime = searchParams.get("dateTime");
const dateTimeStart = searchParams.get("dateTime_start");
const dateTimeEnd = searchParams.get("dateTime_end");
let filteredTransactions = [...allTransactionDummyData];
if (userId) {
filteredTransactions = filteredTransactions.filter(
(tx) => tx.userId.toString() === userId,
);
}
if (status) {
filteredTransactions = filteredTransactions.filter(
(tx) => tx.status.toLowerCase() === status.toLowerCase(),
);
}
if (depositMethod) {
filteredTransactions = filteredTransactions.filter(
(tx) => tx.depositMethod.toLowerCase() === depositMethod.toLowerCase(),
);
}
if (merchandId) {
filteredTransactions = filteredTransactions.filter(
(tx) => tx.merchandId.toString() === merchandId,
);
}
if (transactionId) {
filteredTransactions = filteredTransactions.filter(
(tx) => tx.transactionId.toString() === transactionId,
);
}
if (dateTimeStart && dateTimeEnd) {
const start = new Date(dateTimeStart);
const end = new Date(dateTimeEnd);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return NextResponse.json(
{
error: "Invalid date range",
},
{ status: 400 },
);
}
filteredTransactions = filteredTransactions.filter((tx) => {
const txDate = new Date(tx.dateTime);
if (isNaN(txDate.getTime())) {
return false;
}
return txDate >= start && txDate <= end;
});
}
return NextResponse.json({
tableRows: filteredTransactions,
tableSearchLabels: allTransactionsSearchLabels,
tableColumns: allTransactionsColumns,
extraColumns: extraColumns,
});
}

View File

@ -8,12 +8,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -27,12 +27,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -46,12 +46,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -65,12 +65,12 @@ export const depositTransactionDummyData = [
transactionId: 1049136973,
depositMethod: "Card",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -84,12 +84,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Card",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -103,12 +103,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Card",
status: "Pending",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -122,12 +122,12 @@ export const depositTransactionDummyData = [
transactionId: 1049136973,
depositMethod: "Card",
status: "Pending",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-18 10:10:30",
@ -141,12 +141,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Card",
status: "Pending",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-12 10:10:30",
@ -160,12 +160,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Bank Transfer",
status: "Inprogress",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-17 10:10:30",
@ -179,12 +179,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Bank Transfer",
status: "Inprogress",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-17 10:10:30",
@ -198,12 +198,12 @@ export const depositTransactionDummyData = [
transactionId: 1049131973,
depositMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
// options: [
// { value: "Pending", label: "Pending" },
// { value: "Completed", label: "Completed" },
// { value: "Inprogress", label: "Inprogress" },
// { value: "Error", label: "Error" },
// ],
amount: 4000,
currency: "EUR",
dateTime: "2025-06-17 10:10:30",
@ -218,7 +218,7 @@ export const depositTransactionsColumns: GridColDef[] = [
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
{ field: "depositMethod", headerName: "Deposit Method", width: 130 },
{ field: "status", headerName: "Status", width: 130 },
{ field: "actions", headerName: "Actions", width: 150 },
// { field: "actions", headerName: "Actions", width: 150 },
{ field: "amount", headerName: "Amount", width: 130 },
{ field: "currency", headerName: "Currency", width: 130 },
{ field: "dateTime", headerName: "Date / Time", width: 130 },
@ -226,6 +226,15 @@ export const depositTransactionsColumns: GridColDef[] = [
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
];
export const depositTransactionsExtraColumns: GridColDef[] = [
{ field: "currency", headerName: "Currency", width: 130 },
{ field: "errorInfo", headerName: "Error Info", width: 130 },
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
];
// export const extraColumns = ["currency", "errorInfo", "fraudScore"]
export const depositTransactionsSearchLabels = [
{ label: "User", field: "userId", type: "text" },
{ label: "Transaction ID", field: "transactionId", type: "text" },

View File

@ -3,8 +3,9 @@ import {
depositTransactionDummyData,
depositTransactionsColumns,
depositTransactionsSearchLabels,
// extraColumns
} from "./mockData";
import { formatToDateTimeString } from "@/app/utils/formatDate";
// import { formatToDateTimeString } from "@/app/utils/formatDate";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
@ -14,7 +15,10 @@ export async function GET(request: NextRequest) {
const depositMethod = searchParams.get("depositMethod");
const merchandId = searchParams.get("merchandId");
const transactionId = searchParams.get("transactionId");
const dateTime = searchParams.get("dateTime");
// const dateTime = searchParams.get("dateTime");
const dateTimeStart = searchParams.get("dateTime_start");
const dateTimeEnd = searchParams.get("dateTime_end");
let filteredTransactions = [...depositTransactionDummyData];
@ -46,14 +50,30 @@ export async function GET(request: NextRequest) {
);
}
if (dateTime) {
filteredTransactions = filteredTransactions.filter(
(tx) =>
tx.dateTime.split(" ")[0] ===
formatToDateTimeString(dateTime).split(" ")[0],
if (dateTimeStart && dateTimeEnd) {
const start = new Date(dateTimeStart);
const end = new Date(dateTimeEnd);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return NextResponse.json(
{
error: "Invalid date range",
},
{ status: 400 },
);
}
filteredTransactions = filteredTransactions.filter((tx) => {
const txDate = new Date(tx.dateTime);
if (isNaN(txDate.getTime())) {
return false;
}
return txDate >= start && txDate <= end;
});
}
return NextResponse.json({
tableRows: filteredTransactions,
tableSearchLabels: depositTransactionsSearchLabels,

View File

@ -7,6 +7,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -20,6 +26,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -32,7 +44,13 @@ export const withdrawalTransactionDummyData = [
userId: 17,
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Complete",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
@ -44,6 +62,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049136973,
withdrawalMethod: "Bank Transfer",
status: "Completed",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-18 10:10:30",
errorInfo: "-",
@ -55,6 +79,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -68,6 +98,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -81,6 +117,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -94,6 +136,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Card",
status: "Pending",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-12 10:10:30",
errorInfo: "-",
@ -105,6 +153,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Inprogress",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -116,6 +170,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -129,6 +189,12 @@ export const withdrawalTransactionDummyData = [
transactionId: 1049131973,
withdrawalMethod: "Bank Transfer",
status: "Error",
options: [
{ value: "Pending", label: "Pending" },
{ value: "Completed", label: "Completed" },
{ value: "Inprogress", label: "Inprogress" },
{ value: "Error", label: "Error" },
],
amount: 4000,
dateTime: "2025-06-17 10:10:30",
errorInfo: "-",
@ -143,6 +209,7 @@ export const withdrawalTransactionsColumns: GridColDef[] = [
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
{ field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 },
{ field: "status", headerName: "Status", width: 130 },
{ field: "actions", headerName: "Actions", width: 150 },
{ field: "amount", headerName: "Amount", width: 130 },
{ field: "dateTime", headerName: "Date / Time", width: 130 },
{ field: "errorInfo", headerName: "Error Info", width: 130 },

View File

@ -4,16 +4,17 @@ import {
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");
const dateTimeStart = searchParams.get("dateTime_start");
const dateTimeEnd = searchParams.get("dateTime_end");
let filteredTransactions = [...withdrawalTransactionDummyData];
if (userId) {
@ -28,6 +29,30 @@ export async function GET(request: NextRequest) {
);
}
if (dateTimeStart && dateTimeEnd) {
const start = new Date(dateTimeStart);
const end = new Date(dateTimeEnd);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return NextResponse.json(
{
error: "Invalid date range",
},
{ status: 400 },
);
}
filteredTransactions = filteredTransactions.filter((tx) => {
const txDate = new Date(tx.dateTime);
if (isNaN(txDate.getTime())) {
return false;
}
return txDate >= start && txDate <= end;
});
}
if (withdrawalMethod) {
filteredTransactions = filteredTransactions.filter(
(tx) =>
@ -35,14 +60,6 @@ export async function GET(request: NextRequest) {
);
}
if (dateTime) {
filteredTransactions = filteredTransactions.filter(
(tx) =>
tx.dateTime.split(" ")[0] ===
formatToDateTimeString(dateTime).split(" ")[0],
);
}
return NextResponse.json({
tableRows: filteredTransactions,
tableSearchLabels: withdrawalTransactionsSearchLabels,

View File

@ -0,0 +1,42 @@
"use client";
import React from "react";
import { useDispatch } from "react-redux";
import { Button, Box, Typography, Stack } from "@mui/material";
import { AppDispatch } from "@/app/redux/types";
import { autoLogout, refreshAuthStatus } from "@/app/redux/auth/authSlice";
export default function TestTokenExpiration() {
const dispatch = useDispatch<AppDispatch>();
const handleTestExpiration = () => {
dispatch(autoLogout("Manual test expiration"));
};
const handleRefreshAuth = () => {
dispatch(refreshAuthStatus());
};
return (
<Box sx={{ p: 2, border: "1px solid #ccc", borderRadius: 1, mb: 2 }}>
<Typography variant="h6" gutterBottom>
Test Token Expiration
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Test authentication functionality and token management.
</Typography>
<Stack direction="row" spacing={2}>
<Button
variant="outlined"
color="warning"
onClick={handleTestExpiration}
>
Test Auto-Logout
</Button>
<Button variant="outlined" color="primary" onClick={handleRefreshAuth}>
Refresh Auth Status
</Button>
</Stack>
</Box>
);
}

View File

@ -0,0 +1,54 @@
"use client";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
selectExpiresInHours,
selectTimeUntilExpiration,
} from "@/app/redux/auth/selectors";
import { Alert, AlertTitle, Box, Typography } from "@mui/material";
export default function TokenExpirationInfo() {
const expiresInHours = useSelector(selectExpiresInHours);
const timeUntilExpiration = useSelector(selectTimeUntilExpiration);
const [showWarning, setShowWarning] = useState(false);
useEffect(() => {
// Show warning when token expires in less than 1 hour
if (expiresInHours > 0 && expiresInHours <= 1) {
setShowWarning(true);
} else {
setShowWarning(false);
}
}, [expiresInHours]);
if (expiresInHours <= 0) {
return null; // Don't show anything if not logged in or no token info
}
const formatTime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
};
return (
<Box sx={{ mb: 2 }}>
{showWarning ? (
<Alert severity="warning" sx={{ mb: 2 }}>
<AlertTitle>Session Expiring Soon</AlertTitle>
Your session will expire in {formatTime(timeUntilExpiration)}. Please
save your work and log in again if needed.
</Alert>
) : (
<Typography variant="caption" color="text.secondary">
Session expires in {formatTime(timeUntilExpiration)}
</Typography>
)}
</Box>
);
}

View File

@ -0,0 +1,43 @@
.search-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 16px;
}
.chip {
display: inline-flex;
align-items: center;
background-color: #e0e0e0;
border-radius: 16px;
padding: 4px 8px;
margin: 4px;
font-size: 14px;
}
.chip-label {
margin-right: 8px;
}
.chip-label.bold {
font-weight: bold;
}
.chip-delete {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
color: #333;
}
.clear-all {
margin-left: 8px;
text-decoration: underline;
background: none;
border: none;
color: black;
cursor: pointer;
font-size: 14px;
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Box, Chip, Typography, Button } from '@mui/material';
import { useRouter, useSearchParams } from 'next/navigation';
"use client";
import React from "react";
import { useRouter, useSearchParams } from "next/navigation";
import "./SearchFilters.scss";
interface SearchFiltersProps {
filters: Record<string, string>;
@ -8,57 +9,72 @@ interface SearchFiltersProps {
const SearchFilters = ({ filters }: SearchFiltersProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const filterLabels: Record<string, string> = {
userId: "User",
state: "State",
startDate: "Start Date",
// Add others here
dateRange: "Date Range",
};
const searchParams = useSearchParams()
const handleDeleteFilter = (key: string) => {
const params = new URLSearchParams(searchParams.toString());
if (key === "dateRange") {
params.delete("dateTime_start");
params.delete("dateTime_end");
} else {
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" }}
>
<div className="chip" key={key}>
<span className={`chip-label ${key === "state" ? "bold" : ""}`}>
{label}: {value}
</Typography>
}
onDelete={() => handleDeleteFilter(key)}
sx={{ mr: 1, mb: 1 }}
/>
</span>
<button className="chip-delete" onClick={() => handleDeleteFilter(key)}>
×
</button>
</div>
);
const formatDate = (dateStr: string) =>
new Date(dateStr).toISOString().split("T")[0];
const hasDateRange = filters.dateTime_start && filters.dateTime_end;
const allFilters = [
...Object.entries(filters).filter(
([key]) => key !== "dateTime_start" && key !== "dateTime_end",
),
...(hasDateRange
? [
[
"dateRange",
`${formatDate(filters.dateTime_start)} - ${formatDate(filters.dateTime_end)}`,
] as [string, string],
]
: []),
];
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
<div className="search-filters">
{allFilters.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" }}
>
<button className="clear-all" onClick={onClearAll}>
Clear All
</Button>
</button>
)}
</Box>
</div>
);
};
export default SearchFilters;

View File

@ -1,12 +1,28 @@
"use client";
import {
ApproveTable,
} from "@/app/features/Pages/Approve/Approve";
import { getApproves } from "@/app/services/approve";
import { Approve } from "@/app/features/Pages/Approve/Approve";
export default function ApprovePage() {
export default async function Approve({
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 data = await getApproves({ query });
return (
<div>
{/* This page will now be rendered on the client-side */}
<Approve />
</div>
<ApproveTable data={data} />
);
}
};

View File

@ -1,21 +1,18 @@
"use client";
import React, { useEffect } from "react";
import React from "react";
import { LayoutWrapper } from "../features/dashboard/layout/layoutWrapper";
import { MainContent } from "../features/dashboard/layout/mainContent";
import SideBar from "../features/dashboard/sidebar/Sidebar";
import Header from "../features/dashboard/header/Header";
import { useTokenExpiration } from "../hooks/useTokenExpiration";
import TokenExpirationInfo from "../components/TokenExpirationInfo";
const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
useEffect(() => {
// if (process.env.NODE_ENV === "development") {
import("../../mock/browser").then(({ worker }) => {
worker.start();
});
// }
}, []);
// Monitor token expiration and auto-logout
useTokenExpiration();
return (
<LayoutWrapper>
@ -23,6 +20,7 @@ const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
<MainContent>
<Header />
<TokenExpirationInfo />
{children}
</MainContent>
</div>

View File

@ -0,0 +1,23 @@
import DataTable from "@/app/features/DataTable/DataTable";
import { getTransactions } from "@/app/services/transactions";
export default async function AllTransactionPage({
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 = "all";
const data = await getTransactions({ transactionType, query });
return <DataTable data={data} />;
}

View File

@ -1,3 +1,4 @@
"use client";
import {
Box,
TextField,
@ -39,11 +40,12 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
});
router.push(`?${updatedParams.toString()}`);
}, 500),
[router]
[router],
);
const handleFieldChange = (field: string, value: string) => {
const updatedValues = { ...formValues, [field]: value };
console.log(updatedValues);
setFormValues(updatedValues);
updateURL(updatedValues);
};
@ -158,20 +160,50 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
)}
{type === "date" && (
<Stack spacing={2}>
<DatePicker
label="Start Date"
value={
formValues[field] ? new Date(formValues[field]) : null
formValues[`${field}_start`]
? new Date(formValues[`${field}_start`])
: null
}
onChange={(newValue) =>
onChange={(newValue) => {
if (!newValue)
return handleFieldChange(`${field}_start`, "");
const start = new Date(newValue);
start.setHours(0, 0, 0, 0); // force start of day
handleFieldChange(
field,
newValue?.toISOString() || ""
)
}
`${field}_start`,
start.toISOString(),
);
}}
slotProps={{
textField: { fullWidth: true, size: "small" },
}}
/>
<DatePicker
label="End Date"
value={
formValues[`${field}_end`]
? new Date(formValues[`${field}_end`])
: null
}
onChange={(newValue) => {
if (!newValue)
return handleFieldChange(`${field}_end`, "");
const end = new Date(newValue);
end.setHours(23, 59, 59, 999); // force end of day
handleFieldChange(
`${field}_end`,
end.toISOString(),
);
}}
slotProps={{
textField: { fullWidth: true, size: "small" },
}}
/>
</Stack>
)}
</Box>
))}

View File

@ -15,31 +15,40 @@ import {
Stack,
Paper,
TextField,
Box,
IconButton,
} from "@mui/material";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import AdvancedSearch from "../AdvancedSearch/AdvancedSearch";
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
import { exportData } from "@/app/utils/exportData";
import StatusChangeDialog from "./StatusChangeDialog";
import { IDataTable } from "./types";
interface IDataTableProps<TRow, TColumn> {
data: IDataTable<TRow, TColumn>;
}
export type TWithId = { id: number };
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
data: IDataTableProps<TRow, TColumn>,
) => {
const { tableRows, tableColumns, tableSearchLabels } = data.data;
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>({
data,
}: IDataTableProps<TRow, TColumn>) => {
const { tableRows, tableColumns, tableSearchLabels, extraColumns } = data;
const router = useRouter();
const searchParams = useSearchParams();
const [rows, setRows] = useState<TRow[]>(tableRows);
const [open, setOpen] = useState(false);
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
const [rows, setRows] = useState<TRow[]>(tableRows);
const [modalOpen, setModalOpen] = useState(false);
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
const [newStatus, setNewStatus] = useState<string>("");
const [reason, setReason] = useState<string>("");
const [showExtraColumns, setShowExtraColumns] = useState(false);
const filters = Object.fromEntries(searchParams.entries());
@ -51,13 +60,105 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
};
const handleStatusChange = (id: number, newStatus: string) => {
setRows(
rows.map((row) => (row.id === id ? { ...row, status: newStatus } : row)),
setSelectedRowId(id);
setNewStatus(newStatus);
setModalOpen(true);
};
const handleStatusSave = () => {
console.log(
`Status changed for row with ID ${selectedRowId}. New status: ${newStatus}. Reason: ${reason}`,
);
setRows(
rows.map((row) =>
row.id === selectedRowId ? { ...row, status: newStatus } : row,
),
);
setModalOpen(false);
setReason("");
};
const getColumnsWithDropdown = (columns: TColumn[]): GridColDef[] => {
return columns?.map((col) => {
return columns.map((col) => {
if (col.field === "status") {
return {
...col,
renderCell: (params: GridRenderCellParams) => {
const value = params.value?.toLowerCase();
let bgColor = "#e0e0e0";
let textColor = "#000";
switch (value) {
case "completed":
bgColor = "#d0f0c0";
textColor = "#1b5e20";
break;
case "pending":
bgColor = "#fff4cc";
textColor = "#9e7700";
break;
case "inprogress":
bgColor = "#cce5ff";
textColor = "#004085";
break;
case "error":
bgColor = "#ffcdd2";
textColor = "#c62828";
break;
}
return (
<Box
sx={{
backgroundColor: bgColor,
color: textColor,
px: 1.5,
py: 0.5,
borderRadius: 1,
fontWeight: 500,
textTransform: "capitalize",
display: "inline-block",
width: "100%",
textAlign: "center",
}}
>
{params.value}
</Box>
);
},
};
}
if (col.field === "userId") {
return {
...col,
renderCell: (params: GridRenderCellParams) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: 0.5,
width: "100%",
px: 1,
}}
>
<span>{params.value}</span>
<IconButton
href={`/users/${params.value}`}
target="_blank"
size="small"
onClick={(e) => e.stopPropagation()}
sx={{ p: 0.5 }}
>
<OpenInNewIcon fontSize="small" />
</IconButton>
</Box>
),
};
}
if (col.field === "actions") {
return {
...col,
@ -100,13 +201,22 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
});
};
let filteredColumns = tableColumns;
if (extraColumns && extraColumns.length > 0) {
filteredColumns = showExtraColumns
? tableColumns
: tableColumns.filter((col) => !extraColumns.includes(col.field));
}
return (
<Paper>
<Paper sx={{ width: "calc(100vw - 300px)", overflowX: "hidden" }}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
p={2}
flexWrap="wrap"
gap={2}
>
<TextField
label="Search"
@ -124,11 +234,21 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
>
Export
</Button>
{extraColumns && extraColumns.length > 0 && (
<Button
variant="outlined"
onClick={() => setShowExtraColumns((prev) => !prev)}
>
{showExtraColumns ? "Hide Extra Columns" : "Show Extra Columns"}
</Button>
)}
</Stack>
<Box sx={{ width: "calc(100vw - 300px)", overflowX: "auto" }}>
<Box sx={{ minWidth: 1200 }}>
<DataGrid
rows={tableRows}
columns={getColumnsWithDropdown(tableColumns)}
columns={getColumnsWithDropdown(filteredColumns)}
initialState={{
pagination: { paginationModel: { pageSize: 50 } },
}}
@ -138,6 +258,14 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
cursor: "pointer",
"& .MuiDataGrid-cell": {
py: 1,
textAlign: "center",
justifyContent: "center",
display: "flex",
alignItems: "center",
},
"& .MuiDataGrid-columnHeader": {
textAlign: "center",
justifyContent: "center",
},
}}
onCellClick={(params) => {
@ -146,8 +274,18 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
}
}}
/>
</Box>
</Box>
<StatusChangeDialog
open={modalOpen}
newStatus={newStatus}
reason={reason}
setReason={setReason}
handleClose={() => setModalOpen(false)}
handleSave={handleStatusSave}
/>
{/* Export Dialog */}
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>Export Transactions</DialogTitle>
<DialogContent>
@ -170,7 +308,7 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
/>
}
label="Only export the results in the current table"
label="Only export current table"
sx={{ mt: 2 }}
/>
</DialogContent>

View File

@ -0,0 +1,54 @@
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
} from "@mui/material";
interface StatusChangeDialogProps {
open: boolean;
newStatus: string;
reason: string;
setReason: React.Dispatch<React.SetStateAction<string>>;
handleClose: () => void;
handleSave: () => void;
}
const StatusChangeDialog = ({
open,
newStatus,
reason,
setReason,
handleClose,
handleSave,
}: StatusChangeDialogProps) => {
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Change Status</DialogTitle>
<DialogContent>
You want to change the status to <b>{newStatus}</b>. Please provide a
reason for the change.
<TextField
label="Reason for change"
variant="outlined"
fullWidth
multiline
rows={4}
value={reason}
onChange={(e) => setReason(e.target.value)}
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button variant="contained" onClick={handleSave} disabled={!reason}>
Save
</Button>
</DialogActions>
</Dialog>
);
};
export default StatusChangeDialog;

View File

@ -9,4 +9,5 @@ export interface IDataTable<TRow, TColumn> {
tableRows: TRow[];
tableColumns: TColumn[];
tableSearchLabels: ISearchLabel[];
extraColumns: string[];
}

View File

@ -2,10 +2,10 @@
import React, { useState } from "react";
import { Card, CardContent, Typography, Stack } from "@mui/material";
import { IUser } from "./interfaces";
import UserTopBar from "@/app/features/UserRoles/AddUser/AddUser";
import EditUser from "@/app/features/UserRoles/EditUser/EditUser";
import UserTopBar from "@/app/features/UserRoles/AddUser/AddUserButton";
import Modal from "@/app/components/Modal/Modal";
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
import AddUser from "@/app/features/UserRoles/AddUser/AddUser";
interface UsersProps {
users: IUser[];
@ -25,18 +25,13 @@ const Users: React.FC<UsersProps> = ({ users }) => {
Merchant ID: {user.merchantId}
</Typography>
{/* You can render more UI here for additional properties */}
<Stack direction="row" spacing={1} mt={1}>
<UserRoleCard
username={user.lastName}
name={user.name || ""}
email={user.email}
user={user}
isAdmin={true}
lastLogin="small"
roles={user.authorities}
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
/>
{/* Add more chips or UI elements for other data */}
</Stack>
</CardContent>
</Card>
@ -47,7 +42,7 @@ const Users: React.FC<UsersProps> = ({ users }) => {
onClose={() => setShowAddUser(false)}
title="Add User"
>
<EditUser />
<AddUser />
</Modal>
</div>
);

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
"use client";
import { useState, useEffect, useMemo } from "react";
import {
Box,
TextField,
@ -16,63 +17,104 @@ import {
InputLabel,
Select,
FormControl,
SelectChangeEvent
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
SelectChangeEvent,
debounce,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import { useRouter, useSearchParams } from "next/navigation";
const rows = [
{
merchantId: '100987998',
txId: '1049078821',
userId: 17,
userEmail: 'dhkheni1@yopmail.com',
kycStatus: 'N/A',
},
{
merchantId: '100987998',
txId: '1049078821',
userId: 18,
userEmail: 'dhkheni1@yopmail.com',
kycStatus: 'N/A',
},
{
merchantId: '100987998',
txId: '1049078821',
userId: 19,
userEmail: 'dhkheni1@yopmail.com',
kycStatus: 'N/A',
},
];
export interface TableColumn<T> {
field: keyof T | string;
headerName: string;
render?: (value: unknown, row: T) => React.ReactNode;
}
export const Approve = () => {
const [age, setAge] = useState('');
const [selectedRows, setSelectedRows] = useState<number[]>([]);
interface MenuItemOption {
value: string;
label?: string;
}
interface DynamicTableProps<T extends { id: string | number }> {
data: {
rows: T[];
columns: TableColumn<T>[];
actions: MenuItemOption[];
};
searchParamKey?: string;
}
export function ApproveTable<T extends { id: string | number }>({
data,
searchParamKey = "merchantId",
}: DynamicTableProps<T>) {
const { rows, columns, actions } = data;
const handleCheckboxChange = (userId: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
const isChecked = event.target.checked;
setSelectedRows((prevSelected: number[]) =>
isChecked
? [...prevSelected, userId]
: prevSelected.filter((id) => id !== userId)
const router = useRouter();
const searchParams = useSearchParams();
const [selected, setSelected] = useState<(string | number)[]>([]);
const [search, setSearch] = useState("");
useEffect(() => {
const urlValue = searchParams.get(searchParamKey) ?? "";
setSearch(urlValue);
}, [searchParams, searchParamKey]);
const updateURL = useMemo(
() =>
debounce((value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) params.set(searchParamKey, value);
else params.delete(searchParamKey);
router.replace(`?${params.toString()}`, { scroll: false });
}, 400),
[router, searchParams, searchParamKey],
);
console.log('Selected IDs:', isChecked
? [...selectedRows, userId]
: selectedRows.filter((id) => id !== userId));
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearch(value);
updateURL(value);
};
const handleChangeAge = (event: SelectChangeEvent) => {
setAge(event.target.value as string);
const handleCheckboxChange = (id: string | number, checked: boolean) => {
setSelected((prev) =>
checked ? [...prev, id] : prev.filter((x) => x !== id),
);
};
const handleToggleAll = (checked: boolean) => {
setSelected(checked ? rows.map((r) => r.id) : []);
};
const [action, setAction] = useState("");
const handleActionChange = (e: SelectChangeEvent<string>) => {
const selectedAction = e.target.value;
setAction(selectedAction);
if (selected.length > 0) {
console.log("Selected Ids", selected);
console.log("Selected Action:", selectedAction);
} else {
console.warn("No rows selected for action:", selectedAction);
}
};
return (
<Box p={2}>
<Box mb={2} display="flex" justifyContent="space-between" alignItems="center">
<Box
mb={2}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<TextField
variant="outlined"
placeholder="Filter by tags or search by keyword"
placeholder="Search..."
size="small"
value={search}
onChange={handleSearchChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
@ -83,26 +125,21 @@ export const Approve = () => {
),
}}
/>
<Box sx={{ width: '100px' }}>
{/* <IconButton onClick={handleMenuOpen}> */}
{/* <MoreVertIcon /> */}
{/* </IconButton> */}
{/* <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> */}
{/* <MenuItem onClick={handleMenuClose}>Action 1</MenuItem> */}
{/* <MenuItem onClick={handleMenuClose}>Action 2</MenuItem> */}
{/* </Menu> */}
<Box sx={{ width: 180, display: "flex", justifyContent: "center" }}>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Action</InputLabel>
<InputLabel>Action</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={age}
label="Age"
onChange={handleChangeAge}
value={action}
label="Action"
onChange={handleActionChange}
size="small"
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
{actions.map((item) => (
<MenuItem key={item.value} value={item.value}>
{item.label ?? item.value}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
@ -112,45 +149,38 @@ export const Approve = () => {
<Table size="small">
<TableHead>
<TableRow>
<TableCell padding="checkbox"><Checkbox /></TableCell>
<TableCell>Merchant-id</TableCell>
<TableCell>Tx-id</TableCell>
<TableCell>User</TableCell>
<TableCell>User email</TableCell>
<TableCell>KYC Status</TableCell>
<TableCell>KYC PSP</TableCell>
<TableCell>KYC PSP status</TableCell>
<TableCell>KYC ID status</TableCell>
<TableCell>KYC address status</TableCell>
<TableCell>KYC liveness status</TableCell>
<TableCell>KYC age status</TableCell>
<TableCell>KYC peps and sanctions</TableCell>
<TableCell>Suspected</TableCell>
<TableCell padding="checkbox">
<Checkbox
checked={selected.length === rows.length && rows.length > 0}
indeterminate={
selected.length > 0 && selected.length < rows.length
}
onChange={(e) => handleToggleAll(e.target.checked)}
/>
</TableCell>
{columns.map((col, i) => (
<TableCell key={i}>{col.headerName}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, idx) => (
<TableRow key={idx}>
<TableCell padding="checkbox">
<Checkbox checked={selectedRows.includes(row.userId)}
onChange={handleCheckboxChange(row.userId)} /></TableCell>
<TableCell>{row.merchantId}</TableCell>
<TableCell>{row.txId}</TableCell>
<TableCell>
<a href={`/user/${row.userId}`} target="_blank" rel="noopener noreferrer">
{row.userId}
</a>
<Checkbox
checked={selected.includes(row.id)}
onChange={(e) =>
handleCheckboxChange(row.id, e.target.checked)
}
/>
</TableCell>
<TableCell>{row.userEmail}</TableCell>
<TableCell>{row.kycStatus}</TableCell>
<TableCell />
<TableCell />
<TableCell />
<TableCell />
<TableCell />
<TableCell />
<TableCell />
<TableCell />
{columns.map((col, colIdx) => (
<TableCell key={colIdx}>
{col.render
? col.render(row[col.field as keyof T], row)
: (row[col.field as keyof T] as React.ReactNode)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
@ -159,4 +189,3 @@ export const Approve = () => {
</Box>
);
}

View File

@ -1,4 +1,10 @@
.add-user {
margin-top: 30px;
display: flex;
flex-wrap: wrap;
gap: 16px;
&__sticky-container {
position: sticky;
top: 40px;
width: 100%;
@ -10,6 +16,7 @@
gap: 1rem;
padding: 1rem 0.5rem;
border-bottom: 1px solid #eee;
}
&__button {
padding: 0.5rem 1rem;
@ -20,15 +27,57 @@
display: flex;
align-items: center;
gap: 0.5rem;
}
&__button--primary {
&--primary {
background: #1976d2;
color: #fff;
}
&__button--secondary {
&--secondary {
background: #e0e0e0;
color: #333;
}
}
input,
&__select {
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: 8px;
}
}
}

View File

@ -1,32 +1,110 @@
import { Add } from "@mui/icons-material";
import React from "react";
import "./AddUser.scss";
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import "./AddUser.scss";
import { addUser } from "@/services/roles.services";
import { IEditUserForm } from "../User.interfaces";
interface AddUserFormProps {
onSuccess?: () => void;
}
const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
const router = useRouter();
const [form, setForm] = useState<IEditUserForm>({
firstName: "",
lastName: "",
email: "",
phone: "",
role: "",
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.firstName || !form.lastName || !form.email || !form.role) {
setError("Please fill in all required fields.");
return;
}
try {
setLoading(true);
setError("");
await addUser(form);
if (onSuccess) onSuccess();
router.refresh(); // <- refreshes the page (SSR re-runs)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
setError(err.message || "Something went wrong.");
} finally {
setLoading(false);
}
};
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"
<form className="add-user" onSubmit={handleSubmit}>
<input
name="firstName"
placeholder="First Name"
value={form.firstName}
onChange={handleChange}
required
/>
<input
name="lastName"
placeholder="Last Name"
value={form.lastName}
onChange={handleChange}
required
/>
<input
name="email"
type="email"
placeholder="Email"
value={form.email}
onChange={handleChange}
required
/>
<input
name="phone"
placeholder="Phone (optional)"
value={form.phone}
onChange={handleChange}
/>
<select
name="role"
value={form.role}
onChange={handleChange}
required
className="add-user__select"
>
<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
<option value="">Select Role</option>
<option value="ROLE_IIN">ROLE_IIN</option>
<option value="ROLE_RULES_ADMIN">ROLE_RULES_ADMIN</option>
<option value="ROLE_FIRST_APPROVER">ROLE_FIRST_APPROVER</option>
</select>
{error && <div style={{ color: "red", width: "100%" }}>{error}</div>}
<div className="add-user__button-container">
<button type="submit" disabled={loading}>
{loading ? "Adding..." : "Add User"}
</button>
</div>
</form>
);
};
export default AddUser;
export default AddUserForm;

View File

@ -0,0 +1,37 @@
import { Add } from "@mui/icons-material";
import React from "react";
import "./AddUser.scss";
interface AddUserProps {
onAddUser?: () => void;
onExport?: () => void;
}
const AddUser: React.FC<AddUserProps> = ({ onAddUser, onExport }) => {
return (
<div className="add-user">
<div className="add-user__sticky-container">
<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>
</div>
);
};
export default AddUser;

View File

@ -1,17 +1,19 @@
import React from "react";
import { useRouter } from "next/navigation";
import { IEditUserForm, EditUserField } from "../User.interfaces";
import { createRole } from "@/services/roles.services";
import { addUser, editUser } from "@/services/roles.services";
import { IUser } from "../../Pages/Admin/Users/interfaces";
import "./EditUser.scss";
const EditUser = () => {
const EditUser = ({ user }: { user: IUser }) => {
const router = useRouter();
const { username, lastName, email, authorities: roles, phone } = user;
const [form, setForm] = React.useState<IEditUserForm>({
firstName: "",
lastName: "",
email: "",
role: "",
phone: "",
firstName: username || "",
lastName: lastName || "",
email: email || "",
role: roles[0] || "",
phone: phone || "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -45,7 +47,7 @@ const EditUser = () => {
e.preventDefault();
try {
await createRole(form);
await editUser(user.id, form);
router.refresh(); // <- refreshes the page (SSR re-runs)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {

View File

@ -21,28 +21,23 @@ import {
} from "@mui/icons-material";
import EditUser from "./EditUser/EditUser";
import "./User.scss";
import { IUser } from "../Pages/Admin/Users/interfaces";
interface Props {
username: string;
name: string;
email: string;
user: IUser;
isAdmin: boolean;
lastLogin: string;
merchants: string[];
roles: string[];
extraRolesCount?: number;
}
export default function UserRoleCard({
username,
name,
email,
user,
isAdmin,
roles,
extraRolesCount,
}: Props) {
const [isEditing, setIsEditing] = useState(false);
const { username, name, email, authorities: roles } = user;
const handleEditClick = () => {
setIsEditing(!isEditing);
};
@ -117,7 +112,7 @@ export default function UserRoleCard({
isEditing ? " user-card__edit-transition--open" : ""
}`}
>
{isEditing && <EditUser />}
{isEditing && <EditUser user={user} />}
</div>
</CardContent>
</Card>

View File

@ -37,8 +37,8 @@ export const PAGE_LINKS: ISidebarLink[] = [
icon: ArrowUpwardIcon,
},
{
title: "Transaction History",
path: "/dashboard/transactions/history",
title: "All Transactions",
path: "/dashboard/transactions/all",
icon: HistoryIcon,
},
],

View File

@ -0,0 +1,66 @@
"use client";
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useRouter } from "next/navigation";
import { AppDispatch } from "@/app/redux/types";
import {
selectTimeUntilExpiration,
selectIsLoggedIn,
} from "@/app/redux/auth/selectors";
import { autoLogout } from "@/app/redux/auth/authSlice";
export function useTokenExpiration() {
const dispatch = useDispatch<AppDispatch>();
const router = useRouter();
const timeUntilExpiration = useSelector(selectTimeUntilExpiration);
const isLoggedIn = useSelector(selectIsLoggedIn);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
// Only set up expiration monitoring if user is logged in and we have expiration data
if (isLoggedIn && timeUntilExpiration > 0) {
// Set timeout to logout 1 second after token expires
const logoutDelay = (timeUntilExpiration + 1) * 1000;
timeoutRef.current = setTimeout(() => {
console.log("Token expired, auto-logging out user");
dispatch(autoLogout("Token expired"));
router.push("/login");
}, logoutDelay);
// Also set up periodic checks every 5 minutes
const checkInterval = setInterval(() => {
// Re-dispatch checkAuthStatus to get updated expiration time
// This will be handled by the ReduxProvider
}, 5 * 60 * 1000); // 5 minutes
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
clearInterval(checkInterval);
};
}
}, [isLoggedIn, timeUntilExpiration, dispatch, router]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return {
timeUntilExpiration,
isLoggedIn,
};
}

View File

@ -1,6 +1,6 @@
"use client";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useDispatch, useSelector } from "react-redux";
import LoginModal from "../features/Auth/LoginModal";
@ -18,10 +18,23 @@ export default function LoginPageClient() {
const router = useRouter();
const searchParams = useSearchParams();
const redirectPath = searchParams.get("redirect") || "/dashboard";
const reason = searchParams.get("reason");
const isLoggedIn = useSelector(selectIsLoggedIn);
const status = useSelector(selectStatus);
const authMessage = useSelector(selectAuthMessage);
const dispatch = useDispatch<AppDispatch>();
const [redirectMessage, setRedirectMessage] = useState<string>("");
useEffect(() => {
// Set message based on redirect reason
if (reason === "expired-token") {
setRedirectMessage("Your session has expired. Please log in again.");
} else if (reason === "invalid-token") {
setRedirectMessage("Invalid session detected. Please log in again.");
} else if (reason === "no-token") {
setRedirectMessage("Please log in to access the backoffice.");
}
}, [reason]);
useEffect(() => {
if (isLoggedIn && status === "succeeded") {
@ -44,7 +57,7 @@ export default function LoginPageClient() {
<div className="page-container__content">
<h1 className="page-container__title">Payment Backoffice</h1>
<p className="page-container__message--logged-in">
You are logged in. Redirecting to dashboard...
{redirectMessage || "Please log in to access the backoffice."}
</p>
</div>
</div>
@ -53,13 +66,6 @@ export default function LoginPageClient() {
return (
<div className="page-container">
<div className="page-container__content">
<h1 className="page-container__title">Payment Backoffice</h1>
<p className="page-container__text">
Please log in to access the backoffice.
</p>
</div>
<Modal open={true} title="Login to Payment Cashier">
<LoginModal
onLogin={handleLogin}

View File

@ -1,13 +1,42 @@
"use client";
import React from "react";
import React, { useEffect, useRef } from "react";
import { Provider } from "react-redux";
import { store } from "./store";
import { checkAuthStatus } from "./auth/authSlice";
export default function ReduxProvider({
children,
}: {
children: React.ReactNode;
}) {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const initialCheckRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// Check authentication status when the ReduxProvider component mounts on the client.
// This ensures your Redux auth state is synced with the server-side token.
store.dispatch(checkAuthStatus());
// Do an additional check after 2 seconds to ensure we have the latest token info
initialCheckRef.current = setTimeout(() => {
store.dispatch(checkAuthStatus());
}, 2000);
// Set up periodic token validation every 5 minutes
intervalRef.current = setInterval(() => {
store.dispatch(checkAuthStatus());
}, 5 * 60 * 1000); // 5 minutes
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (initialCheckRef.current) {
clearTimeout(initialCheckRef.current);
}
};
}, []); // Empty dependency array ensures it runs only once on mount
return <Provider store={store}>{children}</Provider>;
}

View File

@ -6,6 +6,15 @@ interface AuthState {
authMessage: string;
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
user: {
email: string;
role: string;
} | null;
tokenInfo: {
expiresAt: string;
timeUntilExpiration: number;
expiresInHours: number;
} | null;
}
const initialState: AuthState = {
@ -13,6 +22,8 @@ const initialState: AuthState = {
authMessage: "",
status: "idle",
error: null,
user: null,
tokenInfo: null,
};
// Async Thunk for Login
@ -21,7 +32,7 @@ export const login = createAsyncThunk(
"auth/login",
async (
{ email, password }: { email: string; password: string },
{ rejectWithValue },
{ rejectWithValue }
) => {
try {
const response = await fetch("/api/auth/login", {
@ -46,14 +57,27 @@ export const login = createAsyncThunk(
// Ensure localStorage access is client-side
localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync
}
// After successful login, check auth status to get token information
// This ensures we have the token expiration details immediately
const authStatusResponse = await fetch("/api/auth/status");
if (authStatusResponse.ok) {
const authData = await authStatusResponse.json();
return {
message: data.message || "Login successful",
user: authData.user,
tokenInfo: authData.tokenInfo,
};
}
return data.message || "Login successful";
} catch (error) {
} catch (error: unknown) {
// Handle network errors or other unexpected issues
if (error instanceof Error) {
return rejectWithValue(error.message || "Network error during login");
const errorMessage =
error instanceof Error ? error.message : "Network error during login";
return rejectWithValue(errorMessage);
}
}
},
);
// Async Thunk for Logout
@ -77,14 +101,95 @@ export const logout = createAsyncThunk(
// Ensure localStorage access is client-side
localStorage.removeItem("userToken"); // Clear client-side flag
}
// After successful logout, check auth status to ensure proper cleanup
const authStatusResponse = await fetch("/api/auth/status");
if (authStatusResponse.ok) {
const authData = await authStatusResponse.json();
return {
message: data.message || "Logged out successfully",
isAuthenticated: authData.isAuthenticated,
};
}
return data.message || "Logged out successfully";
} catch (error) {
} catch (error: unknown) {
// Handle network errors
if (error instanceof Error) {
return rejectWithValue(error.message || "Network error during logout");
const errorMessage =
error instanceof Error ? error.message : "Network error during logout";
return rejectWithValue(errorMessage);
}
}
);
// Async Thunk for automatic logout (when token expires)
export const autoLogout = createAsyncThunk(
"auth/autoLogout",
async (reason: string, { rejectWithValue }) => {
try {
// Clear the cookie by calling logout endpoint
await fetch("/api/auth/logout", {
method: "DELETE",
});
if (typeof window !== "undefined") {
// Clear client-side storage
localStorage.removeItem("userToken");
}
return { message: `Auto-logout: ${reason}` };
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Auto-logout failed";
return rejectWithValue(errorMessage);
}
}
);
// Async Thunk for manual refresh of authentication status
export const refreshAuthStatus = createAsyncThunk(
"auth/refreshStatus",
async (_, { rejectWithValue }) => {
try {
const response = await fetch("/api/auth/status");
const data = await response.json();
if (!response.ok) {
return rejectWithValue(data.message || "Authentication refresh failed");
}
return data;
} catch (error: unknown) {
const errorMessage =
error instanceof Error
? error.message
: "Network error during auth refresh";
return rejectWithValue(errorMessage);
}
}
);
// Async Thunk for checking authentication status
export const checkAuthStatus = createAsyncThunk(
"auth/checkStatus",
async (_, { rejectWithValue }) => {
try {
const response = await fetch("/api/auth/status");
const data = await response.json();
if (!response.ok) {
return rejectWithValue(data.message || "Authentication check failed");
}
return data;
} catch (error: unknown) {
const errorMessage =
error instanceof Error
? error.message
: "Network error during auth check";
return rejectWithValue(errorMessage);
}
}
},
);
// Create the authentication slice
@ -122,7 +227,14 @@ const authSlice = createSlice({
.addCase(login.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = true;
// Handle both old string payload and new object payload
if (typeof action.payload === "string") {
state.authMessage = action.payload;
} else {
state.authMessage = action.payload.message;
state.user = action.payload.user || null;
state.tokenInfo = action.payload.tokenInfo || null;
}
})
.addCase(login.rejected, (state, action) => {
state.status = "failed";
@ -139,13 +251,86 @@ const authSlice = createSlice({
.addCase(logout.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = false;
// Handle both old string payload and new object payload
if (typeof action.payload === "string") {
state.authMessage = action.payload;
} else {
state.authMessage = action.payload.message;
// Ensure we're properly logged out
if (action.payload.isAuthenticated === false) {
state.user = null;
state.tokenInfo = null;
}
}
// Always clear user data on logout
state.user = null;
state.tokenInfo = null;
})
.addCase(logout.rejected, (state, action) => {
state.status = "failed";
state.isLoggedIn = true; // Stay logged in if logout failed
state.error = action.payload as string;
state.authMessage = action.payload as string; // Display error message
})
// Check Auth Status Thunk Reducers
.addCase(checkAuthStatus.pending, (state) => {
state.status = "loading";
state.error = null;
})
.addCase(checkAuthStatus.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = action.payload.isAuthenticated;
state.user = action.payload.user || null;
state.tokenInfo = action.payload.tokenInfo || null;
state.error = null;
})
.addCase(checkAuthStatus.rejected, (state, action) => {
state.status = "failed";
state.isLoggedIn = false;
state.user = null;
state.tokenInfo = null;
state.error = action.payload as string;
})
// Auto Logout Thunk Reducers
.addCase(autoLogout.pending, (state) => {
state.status = "loading";
state.error = null;
state.authMessage = "Session expired, logging out...";
})
.addCase(autoLogout.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = false;
state.authMessage = action.payload.message;
state.user = null;
state.tokenInfo = null;
state.error = null;
})
.addCase(autoLogout.rejected, (state, action) => {
state.status = "failed";
state.isLoggedIn = false;
state.user = null;
state.tokenInfo = null;
state.error = action.payload as string;
state.authMessage = "Auto-logout failed";
})
// Refresh Auth Status Thunk Reducers
.addCase(refreshAuthStatus.pending, (state) => {
state.status = "loading";
state.error = null;
})
.addCase(refreshAuthStatus.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = action.payload.isAuthenticated;
state.user = action.payload.user || null;
state.tokenInfo = action.payload.tokenInfo || null;
state.error = null;
})
.addCase(refreshAuthStatus.rejected, (state, action) => {
state.status = "failed";
state.isLoggedIn = false;
state.user = null;
state.tokenInfo = null;
state.error = action.payload as string;
});
},
});

View File

@ -4,3 +4,9 @@ export const selectIsLoggedIn = (state: RootState) => state.auth.isLoggedIn;
export const selectStatus = (state: RootState) => state.auth?.status;
export const selectError = (state: RootState) => state.auth?.error;
export const selectAuthMessage = (state: RootState) => state.auth?.authMessage;
export const selectUser = (state: RootState) => state.auth?.user;
export const selectTokenInfo = (state: RootState) => state.auth?.tokenInfo;
export const selectTimeUntilExpiration = (state: RootState) =>
state.auth?.tokenInfo?.timeUntilExpiration || 0;
export const selectExpiresInHours = (state: RootState) =>
state.auth?.tokenInfo?.expiresInHours || 0;

View File

@ -8,6 +8,8 @@ export const makeStore = () => {
advancedSearch: advancedSearchReducer,
auth: authReducer,
},
// Enable Redux DevTools
devTools: process.env.NODE_ENV !== "production",
});
};

22
app/services/approve.ts Normal file
View File

@ -0,0 +1,22 @@
export async function getApproves({
query,
}: {
query: string;
}) {
const res = await fetch(
`http://localhost:3000/api/dashboard/approve?${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();
}

47
app/utils/auth.ts Normal file
View File

@ -0,0 +1,47 @@
import { jwtVerify } from "jose";
// Secret key for JWT verification (must match the one used for signing)
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
export interface JWTPayload {
email: string;
role: string;
iat: number;
exp: number;
}
/**
* Validates a JWT token and returns the payload if valid
*/
export async function validateToken(token: string): Promise<JWTPayload | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return payload as unknown as JWTPayload;
} catch (error) {
console.error("Token validation error:", error);
return null;
}
}
/**
* Checks if a token is expired
*/
export function isTokenExpired(payload: JWTPayload): boolean {
const currentTime = Math.floor(Date.now() / 1000);
return payload.exp < currentTime;
}
/**
* Gets token expiration time in a human-readable format
*/
export function getTokenExpirationTime(payload: JWTPayload): Date {
return new Date(payload.exp * 1000);
}
/**
* Gets time until token expires in seconds
*/
export function getTimeUntilExpiration(payload: JWTPayload): number {
const currentTime = Math.floor(Date.now() / 1000);
return Math.max(0, payload.exp - currentTime);
}

View File

@ -8,6 +8,15 @@ const nextConfig: NextConfig = {
}
return config;
},
async redirects() {
return [
{
source: "/",
destination: "/dashboard",
permanent: true,
},
];
},
};
export default nextConfig;

1137
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"file-saver": "^2.0.5",
"jose": "^6.0.12",
"next": "15.3.3",
"react": "^19.0.0",
"react-date-range": "^2.0.1",

View File

@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.10.2'
const PACKAGE_VERSION = '2.10.4'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@ -1,6 +1,6 @@
import { IEditUserForm } from "@/app/features/UserRoles/User.interfaces";
export async function createRole(data: IEditUserForm) {
export async function addUser(data: IEditUserForm) {
const res = await fetch("/api/dashboard/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -13,3 +13,18 @@ export async function createRole(data: IEditUserForm) {
return res.json(); // or return type depending on your backend
}
export async function editUser(id: string, data: IEditUserForm) {
console.log("[editUser] - id", id, data);
const res = await fetch(`/api/dashboard/admin/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) {
console.log("[editUser] - FAILING", id, data);
throw new Error("Failed to update user");
}
return res.json();
}

613
yarn.lock

File diff suppressed because it is too large Load Diff