Modified transaction pages
This commit is contained in:
parent
c827917fd2
commit
6c68ca79e3
@ -1,5 +1,12 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
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
|
// This is your POST handler for the login endpoint
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@ -15,20 +22,25 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
// Mock authentication for demonstration purposes:
|
// Mock authentication for demonstration purposes:
|
||||||
if (email === "admin@example.com" && password === "password123") {
|
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
|
// 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,
|
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
|
||||||
// which mitigates XSS attacks.
|
// which mitigates XSS attacks.
|
||||||
(
|
const cookieStore = await cookies();
|
||||||
await // Set the authentication token as an HTTP-only cookie
|
cookieStore.set("auth_token", token, {
|
||||||
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
|
|
||||||
// which mitigates XSS attacks.
|
|
||||||
cookies()
|
|
||||||
).set("auth_token", authToken, {
|
|
||||||
httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts
|
httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts
|
||||||
secure: process.env.NODE_ENV === "production", // Use secure in production (HTTPS)
|
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
|
path: "/", // Available across the entire site
|
||||||
sameSite: "lax", // Protects against CSRF
|
sameSite: "lax", // Protects against CSRF
|
||||||
});
|
});
|
||||||
|
|||||||
76
app/api/auth/status/route.ts
Normal file
76
app/api/auth/status/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/api/dashboard/admin/mockData.ts
Normal file
35
app/api/dashboard/admin/mockData.ts
Normal 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: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
34
app/api/dashboard/admin/users/[id]/route.ts
Normal file
34
app/api/dashboard/admin/users/[id]/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
@ -1,41 +1,6 @@
|
|||||||
// app/api/dashboard/admin/users/route.ts
|
// app/api/dashboard/admin/users/route.ts
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { users } from "../mockData";
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
return NextResponse.json(users);
|
return NextResponse.json(users);
|
||||||
@ -76,4 +41,4 @@ export async function POST(request: NextRequest) {
|
|||||||
users.push(newUser);
|
users.push(newUser);
|
||||||
|
|
||||||
return NextResponse.json(users, { status: 201 });
|
return NextResponse.json(users, { status: 201 });
|
||||||
}
|
}
|
||||||
|
|||||||
119
app/api/dashboard/approve/mockData.ts
Normal file
119
app/api/dashboard/approve/mockData.ts
Normal 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" },
|
||||||
|
];
|
||||||
20
app/api/dashboard/approve/route.ts
Normal file
20
app/api/dashboard/approve/route.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData";
|
import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData";
|
||||||
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@ -9,13 +8,16 @@ export async function GET(request: NextRequest) {
|
|||||||
const affectedUserId = searchParams.get("affectedUserId");
|
const affectedUserId = searchParams.get("affectedUserId");
|
||||||
const adminId = searchParams.get("adminId");
|
const adminId = searchParams.get("adminId");
|
||||||
const adminUsername = searchParams.get("adminUsername");
|
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];
|
let filteredRows = [...AuditData];
|
||||||
|
|
||||||
if (actionType) {
|
if (actionType) {
|
||||||
filteredRows = filteredRows.filter(
|
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) {
|
if (adminId) {
|
||||||
filteredRows = filteredRows.filter(
|
filteredRows = filteredRows.filter((tx) => tx.adminId === adminId);
|
||||||
(tx) => tx.adminId === adminId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (adminUsername) {
|
if (adminUsername) {
|
||||||
filteredRows = filteredRows.filter(
|
filteredRows = filteredRows.filter(
|
||||||
@ -36,12 +36,30 @@ export async function GET(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeStampOfTheAction) {
|
if (dateTimeStart && dateTimeEnd) {
|
||||||
filteredRows = filteredRows.filter(
|
const start = new Date(dateTimeStart);
|
||||||
(tx) =>
|
const end = new Date(dateTimeEnd);
|
||||||
tx.timeStampOfTheAction.split(" ")[0] ===
|
|
||||||
formatToDateTimeString(timeStampOfTheAction).split(" ")[0],
|
// Validate the date range to ensure it’s 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({
|
return NextResponse.json({
|
||||||
|
|||||||
265
app/api/dashboard/transactions/all/mockData.ts
Normal file
265
app/api/dashboard/transactions/all/mockData.ts
Normal 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" },
|
||||||
|
];
|
||||||
79
app/api/dashboard/transactions/all/route.ts
Normal file
79
app/api/dashboard/transactions/all/route.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -8,12 +8,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Completed",
|
status: "Completed",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -27,12 +27,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Completed",
|
status: "Completed",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -46,12 +46,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Completed",
|
status: "Completed",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -65,12 +65,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049136973,
|
transactionId: 1049136973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Completed",
|
status: "Completed",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -84,12 +84,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Completed",
|
status: "Completed",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -103,12 +103,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Pending",
|
status: "Pending",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -122,12 +122,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049136973,
|
transactionId: 1049136973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Pending",
|
status: "Pending",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
@ -141,12 +141,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Card",
|
depositMethod: "Card",
|
||||||
status: "Pending",
|
status: "Pending",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-12 10:10:30",
|
dateTime: "2025-06-12 10:10:30",
|
||||||
@ -160,12 +160,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Bank Transfer",
|
depositMethod: "Bank Transfer",
|
||||||
status: "Inprogress",
|
status: "Inprogress",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
@ -179,12 +179,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Bank Transfer",
|
depositMethod: "Bank Transfer",
|
||||||
status: "Inprogress",
|
status: "Inprogress",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
@ -198,12 +198,12 @@ export const depositTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
depositMethod: "Bank Transfer",
|
depositMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
options: [
|
// options: [
|
||||||
{ value: "Pending", label: "Pending" },
|
// { value: "Pending", label: "Pending" },
|
||||||
{ value: "Completed", label: "Completed" },
|
// { value: "Completed", label: "Completed" },
|
||||||
{ value: "Inprogress", label: "Inprogress" },
|
// { value: "Inprogress", label: "Inprogress" },
|
||||||
{ value: "Error", label: "Error" },
|
// { value: "Error", label: "Error" },
|
||||||
],
|
// ],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
@ -218,7 +218,7 @@ export const depositTransactionsColumns: GridColDef[] = [
|
|||||||
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
||||||
{ field: "depositMethod", headerName: "Deposit Method", width: 130 },
|
{ field: "depositMethod", headerName: "Deposit Method", width: 130 },
|
||||||
{ field: "status", headerName: "Status", 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: "amount", headerName: "Amount", width: 130 },
|
||||||
{ field: "currency", headerName: "Currency", width: 130 },
|
{ field: "currency", headerName: "Currency", width: 130 },
|
||||||
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
||||||
@ -226,6 +226,15 @@ export const depositTransactionsColumns: GridColDef[] = [
|
|||||||
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
|
{ 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 = [
|
export const depositTransactionsSearchLabels = [
|
||||||
{ label: "User", field: "userId", type: "text" },
|
{ label: "User", field: "userId", type: "text" },
|
||||||
{ label: "Transaction ID", field: "transactionId", type: "text" },
|
{ label: "Transaction ID", field: "transactionId", type: "text" },
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import {
|
|||||||
depositTransactionDummyData,
|
depositTransactionDummyData,
|
||||||
depositTransactionsColumns,
|
depositTransactionsColumns,
|
||||||
depositTransactionsSearchLabels,
|
depositTransactionsSearchLabels,
|
||||||
|
// extraColumns
|
||||||
} from "./mockData";
|
} from "./mockData";
|
||||||
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
// import { formatToDateTimeString } from "@/app/utils/formatDate";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@ -14,7 +15,10 @@ export async function GET(request: NextRequest) {
|
|||||||
const depositMethod = searchParams.get("depositMethod");
|
const depositMethod = searchParams.get("depositMethod");
|
||||||
const merchandId = searchParams.get("merchandId");
|
const merchandId = searchParams.get("merchandId");
|
||||||
const transactionId = searchParams.get("transactionId");
|
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];
|
let filteredTransactions = [...depositTransactionDummyData];
|
||||||
|
|
||||||
@ -46,12 +50,28 @@ export async function GET(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateTime) {
|
if (dateTimeStart && dateTimeEnd) {
|
||||||
filteredTransactions = filteredTransactions.filter(
|
const start = new Date(dateTimeStart);
|
||||||
(tx) =>
|
const end = new Date(dateTimeEnd);
|
||||||
tx.dateTime.split(" ")[0] ===
|
|
||||||
formatToDateTimeString(dateTime).split(" ")[0],
|
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({
|
return NextResponse.json({
|
||||||
|
|||||||
@ -7,6 +7,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -20,6 +26,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -32,7 +44,13 @@ export const withdrawalTransactionDummyData = [
|
|||||||
userId: 17,
|
userId: 17,
|
||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
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,
|
amount: 4000,
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -44,6 +62,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049136973,
|
transactionId: 1049136973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Completed",
|
status: "Completed",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-18 10:10:30",
|
dateTime: "2025-06-18 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -55,6 +79,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -68,6 +98,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -81,6 +117,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -94,6 +136,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Card",
|
withdrawalMethod: "Card",
|
||||||
status: "Pending",
|
status: "Pending",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-12 10:10:30",
|
dateTime: "2025-06-12 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -105,6 +153,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Inprogress",
|
status: "Inprogress",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -116,6 +170,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -129,6 +189,12 @@ export const withdrawalTransactionDummyData = [
|
|||||||
transactionId: 1049131973,
|
transactionId: 1049131973,
|
||||||
withdrawalMethod: "Bank Transfer",
|
withdrawalMethod: "Bank Transfer",
|
||||||
status: "Error",
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
dateTime: "2025-06-17 10:10:30",
|
dateTime: "2025-06-17 10:10:30",
|
||||||
errorInfo: "-",
|
errorInfo: "-",
|
||||||
@ -143,6 +209,7 @@ export const withdrawalTransactionsColumns: GridColDef[] = [
|
|||||||
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
||||||
{ field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 },
|
{ field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 },
|
||||||
{ field: "status", headerName: "Status", width: 130 },
|
{ field: "status", headerName: "Status", width: 130 },
|
||||||
|
{ field: "actions", headerName: "Actions", width: 150 },
|
||||||
{ field: "amount", headerName: "Amount", width: 130 },
|
{ field: "amount", headerName: "Amount", width: 130 },
|
||||||
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
||||||
{ field: "errorInfo", headerName: "Error Info", width: 130 },
|
{ field: "errorInfo", headerName: "Error Info", width: 130 },
|
||||||
|
|||||||
@ -4,16 +4,17 @@ import {
|
|||||||
withdrawalTransactionsColumns,
|
withdrawalTransactionsColumns,
|
||||||
withdrawalTransactionsSearchLabels,
|
withdrawalTransactionsSearchLabels,
|
||||||
} from "./mockData";
|
} from "./mockData";
|
||||||
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
const userId = searchParams.get("userId");
|
const userId = searchParams.get("userId");
|
||||||
const status = searchParams.get("status");
|
const status = searchParams.get("status");
|
||||||
const dateTime = searchParams.get("dateTime");
|
|
||||||
const withdrawalMethod = searchParams.get("withdrawalMethod");
|
const withdrawalMethod = searchParams.get("withdrawalMethod");
|
||||||
|
|
||||||
|
const dateTimeStart = searchParams.get("dateTime_start");
|
||||||
|
const dateTimeEnd = searchParams.get("dateTime_end");
|
||||||
|
|
||||||
let filteredTransactions = [...withdrawalTransactionDummyData];
|
let filteredTransactions = [...withdrawalTransactionDummyData];
|
||||||
|
|
||||||
if (userId) {
|
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) {
|
if (withdrawalMethod) {
|
||||||
filteredTransactions = filteredTransactions.filter(
|
filteredTransactions = filteredTransactions.filter(
|
||||||
(tx) =>
|
(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({
|
return NextResponse.json({
|
||||||
tableRows: filteredTransactions,
|
tableRows: filteredTransactions,
|
||||||
tableSearchLabels: withdrawalTransactionsSearchLabels,
|
tableSearchLabels: withdrawalTransactionsSearchLabels,
|
||||||
|
|||||||
42
app/components/TestTokenExpiration.tsx
Normal file
42
app/components/TestTokenExpiration.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
app/components/TokenExpirationInfo.tsx
Normal file
54
app/components/TokenExpirationInfo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
app/components/searchFilter/SearchFilters.scss
Normal file
43
app/components/searchFilter/SearchFilters.scss
Normal 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;
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
"use client";
|
||||||
import { Box, Chip, Typography, Button } from '@mui/material';
|
import React from "react";
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import "./SearchFilters.scss";
|
||||||
|
|
||||||
interface SearchFiltersProps {
|
interface SearchFiltersProps {
|
||||||
filters: Record<string, string>;
|
filters: Record<string, string>;
|
||||||
@ -8,57 +9,72 @@ interface SearchFiltersProps {
|
|||||||
|
|
||||||
const SearchFilters = ({ filters }: SearchFiltersProps) => {
|
const SearchFilters = ({ filters }: SearchFiltersProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const filterLabels: Record<string, string> = {
|
const filterLabels: Record<string, string> = {
|
||||||
userId: "User",
|
userId: "User",
|
||||||
state: "State",
|
state: "State",
|
||||||
startDate: "Start Date",
|
dateRange: "Date Range",
|
||||||
// Add others here
|
|
||||||
};
|
};
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const handleDeleteFilter = (key: string) => {
|
const handleDeleteFilter = (key: string) => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
params.delete(key);
|
if (key === "dateRange") {
|
||||||
|
params.delete("dateTime_start");
|
||||||
|
params.delete("dateTime_end");
|
||||||
|
} else {
|
||||||
|
params.delete(key);
|
||||||
|
}
|
||||||
router.push(`?${params.toString()}`);
|
router.push(`?${params.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onClearAll = () => {
|
const onClearAll = () => {
|
||||||
router.push("?");
|
router.push("?");
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderChip = (label: string, value: string, key: string) => (
|
const renderChip = (label: string, value: string, key: string) => (
|
||||||
<Chip
|
<div className="chip" key={key}>
|
||||||
key={key}
|
<span className={`chip-label ${key === "state" ? "bold" : ""}`}>
|
||||||
label={
|
{label}: {value}
|
||||||
<Typography
|
</span>
|
||||||
variant="body2"
|
<button className="chip-delete" onClick={() => handleDeleteFilter(key)}>
|
||||||
sx={{ fontWeight: key === "state" ? "bold" : "normal" }}
|
×
|
||||||
>
|
</button>
|
||||||
{label}: {value}
|
</div>
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
onDelete={() => handleDeleteFilter(key)}
|
|
||||||
sx={{ mr: 1, mb: 1 }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Box display="flex" alignItems="center" flexWrap="wrap" sx={{ p: 2 }}>
|
<div className="search-filters">
|
||||||
{Object.entries(filters).map(([key, value]) =>
|
{allFilters.map(([key, value]) =>
|
||||||
value ? renderChip(filterLabels[key] ?? key, value, key) : null
|
value ? renderChip(filterLabels[key] ?? key, value, key) : null,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{Object.values(filters).some(Boolean) && (
|
{Object.values(filters).some(Boolean) && (
|
||||||
<Button
|
<button className="clear-all" onClick={onClearAll}>
|
||||||
onClick={onClearAll}
|
|
||||||
sx={{ ml: 1, textDecoration: "underline", color: "black" }}
|
|
||||||
>
|
|
||||||
Clear All
|
Clear All
|
||||||
</Button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default SearchFilters;
|
export default SearchFilters;
|
||||||
|
|||||||
@ -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 (
|
return (
|
||||||
<div>
|
<ApproveTable data={data} />
|
||||||
{/* This page will now be rendered on the client-side */}
|
|
||||||
<Approve />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { LayoutWrapper } from "../features/dashboard/layout/layoutWrapper";
|
import { LayoutWrapper } from "../features/dashboard/layout/layoutWrapper";
|
||||||
import { MainContent } from "../features/dashboard/layout/mainContent";
|
import { MainContent } from "../features/dashboard/layout/mainContent";
|
||||||
import SideBar from "../features/dashboard/sidebar/Sidebar";
|
import SideBar from "../features/dashboard/sidebar/Sidebar";
|
||||||
import Header from "../features/dashboard/header/Header";
|
import Header from "../features/dashboard/header/Header";
|
||||||
|
import { useTokenExpiration } from "../hooks/useTokenExpiration";
|
||||||
|
import TokenExpirationInfo from "../components/TokenExpirationInfo";
|
||||||
|
|
||||||
const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
|
const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
useEffect(() => {
|
// Monitor token expiration and auto-logout
|
||||||
// if (process.env.NODE_ENV === "development") {
|
useTokenExpiration();
|
||||||
import("../../mock/browser").then(({ worker }) => {
|
|
||||||
worker.start();
|
|
||||||
});
|
|
||||||
// }
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
@ -23,6 +20,7 @@ const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
|
<div style={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
|
||||||
<MainContent>
|
<MainContent>
|
||||||
<Header />
|
<Header />
|
||||||
|
<TokenExpirationInfo />
|
||||||
{children}
|
{children}
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
23
app/dashboard/transactions/all/page.tsx
Normal file
23
app/dashboard/transactions/all/page.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
"use client";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
TextField,
|
TextField,
|
||||||
@ -39,11 +40,12 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
|
|||||||
});
|
});
|
||||||
router.push(`?${updatedParams.toString()}`);
|
router.push(`?${updatedParams.toString()}`);
|
||||||
}, 500),
|
}, 500),
|
||||||
[router]
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: string) => {
|
const handleFieldChange = (field: string, value: string) => {
|
||||||
const updatedValues = { ...formValues, [field]: value };
|
const updatedValues = { ...formValues, [field]: value };
|
||||||
|
console.log(updatedValues);
|
||||||
setFormValues(updatedValues);
|
setFormValues(updatedValues);
|
||||||
updateURL(updatedValues);
|
updateURL(updatedValues);
|
||||||
};
|
};
|
||||||
@ -158,20 +160,50 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "date" && (
|
{type === "date" && (
|
||||||
<DatePicker
|
<Stack spacing={2}>
|
||||||
value={
|
<DatePicker
|
||||||
formValues[field] ? new Date(formValues[field]) : null
|
label="Start Date"
|
||||||
}
|
value={
|
||||||
onChange={(newValue) =>
|
formValues[`${field}_start`]
|
||||||
handleFieldChange(
|
? new Date(formValues[`${field}_start`])
|
||||||
field,
|
: null
|
||||||
newValue?.toISOString() || ""
|
}
|
||||||
)
|
onChange={(newValue) => {
|
||||||
}
|
if (!newValue)
|
||||||
slotProps={{
|
return handleFieldChange(`${field}_start`, "");
|
||||||
textField: { fullWidth: true, size: "small" },
|
const start = new Date(newValue);
|
||||||
}}
|
start.setHours(0, 0, 0, 0); // force start of day
|
||||||
/>
|
handleFieldChange(
|
||||||
|
`${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>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -15,31 +15,40 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
TextField,
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import AdvancedSearch from "../AdvancedSearch/AdvancedSearch";
|
import AdvancedSearch from "../AdvancedSearch/AdvancedSearch";
|
||||||
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
|
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
|
||||||
import { exportData } from "@/app/utils/exportData";
|
import { exportData } from "@/app/utils/exportData";
|
||||||
|
import StatusChangeDialog from "./StatusChangeDialog";
|
||||||
import { IDataTable } from "./types";
|
import { IDataTable } from "./types";
|
||||||
|
|
||||||
|
|
||||||
interface IDataTableProps<TRow, TColumn> {
|
interface IDataTableProps<TRow, TColumn> {
|
||||||
data: IDataTable<TRow, TColumn>;
|
data: IDataTable<TRow, TColumn>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TWithId = { id: number };
|
export type TWithId = { id: number };
|
||||||
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
|
|
||||||
data: IDataTableProps<TRow, TColumn>,
|
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>({
|
||||||
) => {
|
data,
|
||||||
const { tableRows, tableColumns, tableSearchLabels } = data.data;
|
}: IDataTableProps<TRow, TColumn>) => {
|
||||||
|
const { tableRows, tableColumns, tableSearchLabels, extraColumns } = data;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState<TRow[]>(tableRows);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
|
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
|
||||||
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
|
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());
|
const filters = Object.fromEntries(searchParams.entries());
|
||||||
|
|
||||||
@ -51,13 +60,105 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleStatusChange = (id: number, newStatus: string) => {
|
const handleStatusChange = (id: number, newStatus: string) => {
|
||||||
setRows(
|
setSelectedRowId(id);
|
||||||
rows.map((row) => (row.id === id ? { ...row, status: newStatus } : row)),
|
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[] => {
|
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") {
|
if (col.field === "actions") {
|
||||||
return {
|
return {
|
||||||
...col,
|
...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 (
|
return (
|
||||||
<Paper>
|
<Paper sx={{ width: "calc(100vw - 300px)", overflowX: "hidden" }}>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
p={2}
|
p={2}
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap={2}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
label="Search"
|
label="Search"
|
||||||
@ -124,30 +234,58 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
|
|||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
|
{extraColumns && extraColumns.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setShowExtraColumns((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{showExtraColumns ? "Hide Extra Columns" : "Show Extra Columns"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<DataGrid
|
<Box sx={{ width: "calc(100vw - 300px)", overflowX: "auto" }}>
|
||||||
rows={tableRows}
|
<Box sx={{ minWidth: 1200 }}>
|
||||||
columns={getColumnsWithDropdown(tableColumns)}
|
<DataGrid
|
||||||
initialState={{
|
rows={tableRows}
|
||||||
pagination: { paginationModel: { pageSize: 50 } },
|
columns={getColumnsWithDropdown(filteredColumns)}
|
||||||
}}
|
initialState={{
|
||||||
pageSizeOptions={[50, 100]}
|
pagination: { paginationModel: { pageSize: 50 } },
|
||||||
sx={{
|
}}
|
||||||
border: 0,
|
pageSizeOptions={[50, 100]}
|
||||||
cursor: "pointer",
|
sx={{
|
||||||
"& .MuiDataGrid-cell": {
|
border: 0,
|
||||||
py: 1,
|
cursor: "pointer",
|
||||||
},
|
"& .MuiDataGrid-cell": {
|
||||||
}}
|
py: 1,
|
||||||
onCellClick={(params) => {
|
textAlign: "center",
|
||||||
if (params.field !== "actions") {
|
justifyContent: "center",
|
||||||
handleClickField(params.field, params.value as string);
|
display: "flex",
|
||||||
}
|
alignItems: "center",
|
||||||
}}
|
},
|
||||||
|
"& .MuiDataGrid-columnHeader": {
|
||||||
|
textAlign: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onCellClick={(params) => {
|
||||||
|
if (params.field !== "actions") {
|
||||||
|
handleClickField(params.field, params.value as string);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<StatusChangeDialog
|
||||||
|
open={modalOpen}
|
||||||
|
newStatus={newStatus}
|
||||||
|
reason={reason}
|
||||||
|
setReason={setReason}
|
||||||
|
handleClose={() => setModalOpen(false)}
|
||||||
|
handleSave={handleStatusSave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Export Dialog */}
|
|
||||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||||
<DialogTitle>Export Transactions</DialogTitle>
|
<DialogTitle>Export Transactions</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -170,7 +308,7 @@ const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
|
|||||||
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
|
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Only export the results in the current table"
|
label="Only export current table"
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
54
app/features/DataTable/StatusChangeDialog.tsx
Normal file
54
app/features/DataTable/StatusChangeDialog.tsx
Normal 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;
|
||||||
@ -9,4 +9,5 @@ export interface IDataTable<TRow, TColumn> {
|
|||||||
tableRows: TRow[];
|
tableRows: TRow[];
|
||||||
tableColumns: TColumn[];
|
tableColumns: TColumn[];
|
||||||
tableSearchLabels: ISearchLabel[];
|
tableSearchLabels: ISearchLabel[];
|
||||||
|
extraColumns: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Card, CardContent, Typography, 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 UserTopBar from "@/app/features/UserRoles/AddUser/AddUserButton";
|
||||||
import EditUser from "@/app/features/UserRoles/EditUser/EditUser";
|
|
||||||
import Modal from "@/app/components/Modal/Modal";
|
import Modal from "@/app/components/Modal/Modal";
|
||||||
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
||||||
|
import AddUser from "@/app/features/UserRoles/AddUser/AddUser";
|
||||||
|
|
||||||
interface UsersProps {
|
interface UsersProps {
|
||||||
users: IUser[];
|
users: IUser[];
|
||||||
@ -25,18 +25,13 @@ const Users: React.FC<UsersProps> = ({ users }) => {
|
|||||||
Merchant ID: {user.merchantId}
|
Merchant ID: {user.merchantId}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* You can render more UI here for additional properties */}
|
|
||||||
<Stack direction="row" spacing={1} mt={1}>
|
<Stack direction="row" spacing={1} mt={1}>
|
||||||
<UserRoleCard
|
<UserRoleCard
|
||||||
username={user.lastName}
|
user={user}
|
||||||
name={user.name || ""}
|
|
||||||
email={user.email}
|
|
||||||
isAdmin={true}
|
isAdmin={true}
|
||||||
lastLogin="small"
|
lastLogin="small"
|
||||||
roles={user.authorities}
|
|
||||||
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
||||||
/>
|
/>
|
||||||
{/* Add more chips or UI elements for other data */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -47,7 +42,7 @@ const Users: React.FC<UsersProps> = ({ users }) => {
|
|||||||
onClose={() => setShowAddUser(false)}
|
onClose={() => setShowAddUser(false)}
|
||||||
title="Add User"
|
title="Add User"
|
||||||
>
|
>
|
||||||
<EditUser />
|
<AddUser />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
"use client";
|
||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
TextField,
|
TextField,
|
||||||
@ -16,63 +17,104 @@ import {
|
|||||||
InputLabel,
|
InputLabel,
|
||||||
Select,
|
Select,
|
||||||
FormControl,
|
FormControl,
|
||||||
SelectChangeEvent
|
SelectChangeEvent,
|
||||||
} from '@mui/material';
|
debounce,
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
} from "@mui/material";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
const rows = [
|
export interface TableColumn<T> {
|
||||||
{
|
field: keyof T | string;
|
||||||
merchantId: '100987998',
|
headerName: string;
|
||||||
txId: '1049078821',
|
render?: (value: unknown, row: T) => React.ReactNode;
|
||||||
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 const Approve = () => {
|
interface MenuItemOption {
|
||||||
const [age, setAge] = useState('');
|
value: string;
|
||||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
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 router = useRouter();
|
||||||
const isChecked = event.target.checked;
|
const searchParams = useSearchParams();
|
||||||
setSelectedRows((prevSelected: number[]) =>
|
|
||||||
isChecked
|
const [selected, setSelected] = useState<(string | number)[]>([]);
|
||||||
? [...prevSelected, userId]
|
const [search, setSearch] = useState("");
|
||||||
: prevSelected.filter((id) => id !== userId)
|
|
||||||
);
|
useEffect(() => {
|
||||||
console.log('Selected IDs:', isChecked
|
const urlValue = searchParams.get(searchParamKey) ?? "";
|
||||||
? [...selectedRows, userId]
|
setSearch(urlValue);
|
||||||
: selectedRows.filter((id) => id !== userId));
|
}, [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],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearch(value);
|
||||||
|
updateURL(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeAge = (event: SelectChangeEvent) => {
|
const handleCheckboxChange = (id: string | number, checked: boolean) => {
|
||||||
setAge(event.target.value as string);
|
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 (
|
return (
|
||||||
<Box p={2}>
|
<Box p={2}>
|
||||||
<Box mb={2} display="flex" justifyContent="space-between" alignItems="center">
|
<Box
|
||||||
|
mb={2}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="Filter by tags or search by keyword"
|
placeholder="Search..."
|
||||||
size="small"
|
size="small"
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@ -83,26 +125,21 @@ export const Approve = () => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ width: '100px' }}>
|
|
||||||
{/* <IconButton onClick={handleMenuOpen}> */}
|
<Box sx={{ width: 180, display: "flex", justifyContent: "center" }}>
|
||||||
{/* <MoreVertIcon /> */}
|
|
||||||
{/* </IconButton> */}
|
|
||||||
{/* <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> */}
|
|
||||||
{/* <MenuItem onClick={handleMenuClose}>Action 1</MenuItem> */}
|
|
||||||
{/* <MenuItem onClick={handleMenuClose}>Action 2</MenuItem> */}
|
|
||||||
{/* </Menu> */}
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel id="demo-simple-select-label">Action</InputLabel>
|
<InputLabel>Action</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="demo-simple-select-label"
|
value={action}
|
||||||
id="demo-simple-select"
|
label="Action"
|
||||||
value={age}
|
onChange={handleActionChange}
|
||||||
label="Age"
|
size="small"
|
||||||
onChange={handleChangeAge}
|
|
||||||
>
|
>
|
||||||
<MenuItem value={10}>Ten</MenuItem>
|
{actions.map((item) => (
|
||||||
<MenuItem value={20}>Twenty</MenuItem>
|
<MenuItem key={item.value} value={item.value}>
|
||||||
<MenuItem value={30}>Thirty</MenuItem>
|
{item.label ?? item.value}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
@ -112,45 +149,38 @@ export const Approve = () => {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell padding="checkbox"><Checkbox /></TableCell>
|
<TableCell padding="checkbox">
|
||||||
<TableCell>Merchant-id</TableCell>
|
<Checkbox
|
||||||
<TableCell>Tx-id</TableCell>
|
checked={selected.length === rows.length && rows.length > 0}
|
||||||
<TableCell>User</TableCell>
|
indeterminate={
|
||||||
<TableCell>User email</TableCell>
|
selected.length > 0 && selected.length < rows.length
|
||||||
<TableCell>KYC Status</TableCell>
|
}
|
||||||
<TableCell>KYC PSP</TableCell>
|
onChange={(e) => handleToggleAll(e.target.checked)}
|
||||||
<TableCell>KYC PSP status</TableCell>
|
/>
|
||||||
<TableCell>KYC ID status</TableCell>
|
</TableCell>
|
||||||
<TableCell>KYC address status</TableCell>
|
{columns.map((col, i) => (
|
||||||
<TableCell>KYC liveness status</TableCell>
|
<TableCell key={i}>{col.headerName}</TableCell>
|
||||||
<TableCell>KYC age status</TableCell>
|
))}
|
||||||
<TableCell>KYC peps and sanctions</TableCell>
|
|
||||||
<TableCell>Suspected</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.map((row, idx) => (
|
{rows.map((row, idx) => (
|
||||||
<TableRow key={idx}>
|
<TableRow key={idx}>
|
||||||
<TableCell padding="checkbox">
|
<TableCell padding="checkbox">
|
||||||
<Checkbox checked={selectedRows.includes(row.userId)}
|
<Checkbox
|
||||||
onChange={handleCheckboxChange(row.userId)} /></TableCell>
|
checked={selected.includes(row.id)}
|
||||||
<TableCell>{row.merchantId}</TableCell>
|
onChange={(e) =>
|
||||||
<TableCell>{row.txId}</TableCell>
|
handleCheckboxChange(row.id, e.target.checked)
|
||||||
<TableCell>
|
}
|
||||||
<a href={`/user/${row.userId}`} target="_blank" rel="noopener noreferrer">
|
/>
|
||||||
{row.userId}
|
|
||||||
</a>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.userEmail}</TableCell>
|
{columns.map((col, colIdx) => (
|
||||||
<TableCell>{row.kycStatus}</TableCell>
|
<TableCell key={colIdx}>
|
||||||
<TableCell />
|
{col.render
|
||||||
<TableCell />
|
? col.render(row[col.field as keyof T], row)
|
||||||
<TableCell />
|
: (row[col.field as keyof T] as React.ReactNode)}
|
||||||
<TableCell />
|
</TableCell>
|
||||||
<TableCell />
|
))}
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@ -159,4 +189,3 @@ export const Approve = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
.add-user {
|
.add-user {
|
||||||
position: sticky;
|
margin-top: 30px;
|
||||||
top: 40px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
gap: 16px;
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 0.5rem;
|
&__sticky-container {
|
||||||
border-bottom: 1px solid #eee;
|
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 {
|
&__button {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
@ -20,15 +27,57 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: #1976d2;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--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--primary {
|
&__button-container {
|
||||||
background: #1976d2;
|
flex-basis: 100%;
|
||||||
color: #fff;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
&__button--secondary {
|
button {
|
||||||
background: #e0e0e0;
|
flex-basis: 100%;
|
||||||
color: #333;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,110 @@
|
|||||||
import { Add } from "@mui/icons-material";
|
"use client";
|
||||||
import React from "react";
|
|
||||||
import "./AddUser.scss";
|
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 (
|
return (
|
||||||
<div className="add-user">
|
<form className="add-user" onSubmit={handleSubmit}>
|
||||||
<button
|
<input
|
||||||
type="button"
|
name="firstName"
|
||||||
onClick={onAddUser}
|
placeholder="First Name"
|
||||||
className="add-user__button add-user__button--primary"
|
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 />
|
<option value="">Select Role</option>
|
||||||
Add User
|
<option value="ROLE_IIN">ROLE_IIN</option>
|
||||||
</button>
|
<option value="ROLE_RULES_ADMIN">ROLE_RULES_ADMIN</option>
|
||||||
<button
|
<option value="ROLE_FIRST_APPROVER">ROLE_FIRST_APPROVER</option>
|
||||||
type="button"
|
</select>
|
||||||
onClick={onExport}
|
|
||||||
className="add-user__button add-user__button--secondary"
|
{error && <div style={{ color: "red", width: "100%" }}>{error}</div>}
|
||||||
disabled
|
|
||||||
title="Export to Excel (coming soon)"
|
<div className="add-user__button-container">
|
||||||
>
|
<button type="submit" disabled={loading}>
|
||||||
Export to Excel
|
{loading ? "Adding..." : "Add User"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddUser;
|
export default AddUserForm;
|
||||||
|
|||||||
37
app/features/UserRoles/AddUser/AddUserButton.tsx
Normal file
37
app/features/UserRoles/AddUser/AddUserButton.tsx
Normal 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;
|
||||||
@ -1,17 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { IEditUserForm, EditUserField } from "../User.interfaces";
|
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";
|
import "./EditUser.scss";
|
||||||
|
|
||||||
const EditUser = () => {
|
const EditUser = ({ user }: { user: IUser }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { username, lastName, email, authorities: roles, phone } = user;
|
||||||
const [form, setForm] = React.useState<IEditUserForm>({
|
const [form, setForm] = React.useState<IEditUserForm>({
|
||||||
firstName: "",
|
firstName: username || "",
|
||||||
lastName: "",
|
lastName: lastName || "",
|
||||||
email: "",
|
email: email || "",
|
||||||
role: "",
|
role: roles[0] || "",
|
||||||
phone: "",
|
phone: phone || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -45,9 +47,9 @@ const EditUser = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createRole(form);
|
await editUser(user.id, form);
|
||||||
router.refresh(); // <- refreshes the page (SSR re-runs)
|
router.refresh(); // <- refreshes the page (SSR re-runs)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log(err.message || "Error creating role");
|
console.log(err.message || "Error creating role");
|
||||||
// setError(err.message || "Error creating role");
|
// setError(err.message || "Error creating role");
|
||||||
|
|||||||
@ -21,28 +21,23 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import EditUser from "./EditUser/EditUser";
|
import EditUser from "./EditUser/EditUser";
|
||||||
import "./User.scss";
|
import "./User.scss";
|
||||||
|
import { IUser } from "../Pages/Admin/Users/interfaces";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
username: string;
|
user: IUser;
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
lastLogin: string;
|
lastLogin: string;
|
||||||
merchants: string[];
|
merchants: string[];
|
||||||
roles: string[];
|
|
||||||
extraRolesCount?: number;
|
extraRolesCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserRoleCard({
|
export default function UserRoleCard({
|
||||||
username,
|
user,
|
||||||
name,
|
|
||||||
email,
|
|
||||||
isAdmin,
|
isAdmin,
|
||||||
roles,
|
|
||||||
extraRolesCount,
|
extraRolesCount,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const { username, name, email, authorities: roles } = user;
|
||||||
const handleEditClick = () => {
|
const handleEditClick = () => {
|
||||||
setIsEditing(!isEditing);
|
setIsEditing(!isEditing);
|
||||||
};
|
};
|
||||||
@ -117,7 +112,7 @@ export default function UserRoleCard({
|
|||||||
isEditing ? " user-card__edit-transition--open" : ""
|
isEditing ? " user-card__edit-transition--open" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isEditing && <EditUser />}
|
{isEditing && <EditUser user={user} />}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -37,8 +37,8 @@ export const PAGE_LINKS: ISidebarLink[] = [
|
|||||||
icon: ArrowUpwardIcon,
|
icon: ArrowUpwardIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Transaction History",
|
title: "All Transactions",
|
||||||
path: "/dashboard/transactions/history",
|
path: "/dashboard/transactions/all",
|
||||||
icon: HistoryIcon,
|
icon: HistoryIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
66
app/hooks/useTokenExpiration.ts
Normal file
66
app/hooks/useTokenExpiration.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import LoginModal from "../features/Auth/LoginModal";
|
import LoginModal from "../features/Auth/LoginModal";
|
||||||
@ -18,10 +18,23 @@ export default function LoginPageClient() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
||||||
|
const reason = searchParams.get("reason");
|
||||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
const authMessage = useSelector(selectAuthMessage);
|
const authMessage = useSelector(selectAuthMessage);
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
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(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn && status === "succeeded") {
|
if (isLoggedIn && status === "succeeded") {
|
||||||
@ -44,7 +57,7 @@ export default function LoginPageClient() {
|
|||||||
<div className="page-container__content">
|
<div className="page-container__content">
|
||||||
<h1 className="page-container__title">Payment Backoffice</h1>
|
<h1 className="page-container__title">Payment Backoffice</h1>
|
||||||
<p className="page-container__message--logged-in">
|
<p className="page-container__message--logged-in">
|
||||||
You are logged in. Redirecting to dashboard...
|
{redirectMessage || "Please log in to access the backoffice."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,13 +66,6 @@ export default function LoginPageClient() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<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">
|
<Modal open={true} title="Login to Payment Cashier">
|
||||||
<LoginModal
|
<LoginModal
|
||||||
onLogin={handleLogin}
|
onLogin={handleLogin}
|
||||||
|
|||||||
@ -1,13 +1,42 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
import { checkAuthStatus } from "./auth/authSlice";
|
||||||
|
|
||||||
export default function ReduxProvider({
|
export default function ReduxProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
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>;
|
return <Provider store={store}>{children}</Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,15 @@ interface AuthState {
|
|||||||
authMessage: string;
|
authMessage: string;
|
||||||
status: "idle" | "loading" | "succeeded" | "failed";
|
status: "idle" | "loading" | "succeeded" | "failed";
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
} | null;
|
||||||
|
tokenInfo: {
|
||||||
|
expiresAt: string;
|
||||||
|
timeUntilExpiration: number;
|
||||||
|
expiresInHours: number;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
@ -13,6 +22,8 @@ const initialState: AuthState = {
|
|||||||
authMessage: "",
|
authMessage: "",
|
||||||
status: "idle",
|
status: "idle",
|
||||||
error: null,
|
error: null,
|
||||||
|
user: null,
|
||||||
|
tokenInfo: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Async Thunk for Login
|
// Async Thunk for Login
|
||||||
@ -21,7 +32,7 @@ export const login = createAsyncThunk(
|
|||||||
"auth/login",
|
"auth/login",
|
||||||
async (
|
async (
|
||||||
{ email, password }: { email: string; password: string },
|
{ email, password }: { email: string; password: string },
|
||||||
{ rejectWithValue },
|
{ rejectWithValue }
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/auth/login", {
|
const response = await fetch("/api/auth/login", {
|
||||||
@ -46,14 +57,27 @@ export const login = createAsyncThunk(
|
|||||||
// Ensure localStorage access is client-side
|
// Ensure localStorage access is client-side
|
||||||
localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync
|
localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync
|
||||||
}
|
}
|
||||||
return data.message || "Login successful";
|
|
||||||
} catch (error) {
|
// After successful login, check auth status to get token information
|
||||||
// Handle network errors or other unexpected issues
|
// This ensures we have the token expiration details immediately
|
||||||
if (error instanceof Error) {
|
const authStatusResponse = await fetch("/api/auth/status");
|
||||||
return rejectWithValue(error.message || "Network error during login");
|
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: unknown) {
|
||||||
|
// Handle network errors or other unexpected issues
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : "Network error during login";
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Async Thunk for Logout
|
// Async Thunk for Logout
|
||||||
@ -77,14 +101,95 @@ export const logout = createAsyncThunk(
|
|||||||
// Ensure localStorage access is client-side
|
// Ensure localStorage access is client-side
|
||||||
localStorage.removeItem("userToken"); // Clear client-side flag
|
localStorage.removeItem("userToken"); // Clear client-side flag
|
||||||
}
|
}
|
||||||
return data.message || "Logged out successfully";
|
|
||||||
} catch (error) {
|
// After successful logout, check auth status to ensure proper cleanup
|
||||||
// Handle network errors
|
const authStatusResponse = await fetch("/api/auth/status");
|
||||||
if (error instanceof Error) {
|
if (authStatusResponse.ok) {
|
||||||
return rejectWithValue(error.message || "Network error during logout");
|
const authData = await authStatusResponse.json();
|
||||||
|
return {
|
||||||
|
message: data.message || "Logged out successfully",
|
||||||
|
isAuthenticated: authData.isAuthenticated,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data.message || "Logged out successfully";
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Handle network errors
|
||||||
|
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
|
// Create the authentication slice
|
||||||
@ -122,7 +227,14 @@ const authSlice = createSlice({
|
|||||||
.addCase(login.fulfilled, (state, action) => {
|
.addCase(login.fulfilled, (state, action) => {
|
||||||
state.status = "succeeded";
|
state.status = "succeeded";
|
||||||
state.isLoggedIn = true;
|
state.isLoggedIn = true;
|
||||||
state.authMessage = action.payload;
|
// 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) => {
|
.addCase(login.rejected, (state, action) => {
|
||||||
state.status = "failed";
|
state.status = "failed";
|
||||||
@ -139,13 +251,86 @@ const authSlice = createSlice({
|
|||||||
.addCase(logout.fulfilled, (state, action) => {
|
.addCase(logout.fulfilled, (state, action) => {
|
||||||
state.status = "succeeded";
|
state.status = "succeeded";
|
||||||
state.isLoggedIn = false;
|
state.isLoggedIn = false;
|
||||||
state.authMessage = action.payload;
|
// 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) => {
|
.addCase(logout.rejected, (state, action) => {
|
||||||
state.status = "failed";
|
state.status = "failed";
|
||||||
state.isLoggedIn = true; // Stay logged in if logout failed
|
state.isLoggedIn = true; // Stay logged in if logout failed
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
state.authMessage = action.payload as string; // Display error message
|
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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,3 +4,9 @@ export const selectIsLoggedIn = (state: RootState) => state.auth.isLoggedIn;
|
|||||||
export const selectStatus = (state: RootState) => state.auth?.status;
|
export const selectStatus = (state: RootState) => state.auth?.status;
|
||||||
export const selectError = (state: RootState) => state.auth?.error;
|
export const selectError = (state: RootState) => state.auth?.error;
|
||||||
export const selectAuthMessage = (state: RootState) => state.auth?.authMessage;
|
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;
|
||||||
|
|||||||
@ -8,6 +8,8 @@ export const makeStore = () => {
|
|||||||
advancedSearch: advancedSearchReducer,
|
advancedSearch: advancedSearchReducer,
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
},
|
},
|
||||||
|
// Enable Redux DevTools
|
||||||
|
devTools: process.env.NODE_ENV !== "production",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
22
app/services/approve.ts
Normal file
22
app/services/approve.ts
Normal 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
47
app/utils/auth.ts
Normal 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);
|
||||||
|
}
|
||||||
@ -8,6 +8,15 @@ const nextConfig: NextConfig = {
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/",
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
1137
package-lock.json
generated
1137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"jose": "^6.0.12",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-date-range": "^2.0.1",
|
"react-date-range": "^2.0.1",
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.10.2'
|
const PACKAGE_VERSION = '2.10.4'
|
||||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { IEditUserForm } from "@/app/features/UserRoles/User.interfaces";
|
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", {
|
const res = await fetch("/api/dashboard/admin/users", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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
|
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();
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user