Added login and manual status change
This commit is contained in:
parent
cd2162584e
commit
2e5788405c
53
app/api/auth/login/route.tsx
Normal file
53
app/api/auth/login/route.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
// This is your POST handler for the login endpoint
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password } = await request.json();
|
||||||
|
|
||||||
|
// --- Replace with your ACTUAL authentication logic ---
|
||||||
|
// In a real application, you would:
|
||||||
|
// 1. Query your database for the user by email.
|
||||||
|
// 2. Hash the provided password and compare it to the stored hashed password.
|
||||||
|
// 3. If credentials match, generate a secure JWT (JSON Web Token) or session ID.
|
||||||
|
// 4. Store the token/session ID securely (e.g., in a database or Redis).
|
||||||
|
|
||||||
|
// Mock authentication for demonstration purposes:
|
||||||
|
if (email === "admin@example.com" && password === "password123") {
|
||||||
|
const authToken = "mock-jwt-token-12345"; // Replace with a real, securely generated token
|
||||||
|
|
||||||
|
// Set the authentication token as an HTTP-only cookie
|
||||||
|
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
|
||||||
|
// which mitigates XSS attacks.
|
||||||
|
(
|
||||||
|
await // Set the authentication token as an HTTP-only cookie
|
||||||
|
// HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript,
|
||||||
|
// which mitigates XSS attacks.
|
||||||
|
cookies()
|
||||||
|
).set("auth_token", authToken, {
|
||||||
|
httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts
|
||||||
|
secure: process.env.NODE_ENV === "production", // Use secure in production (HTTPS)
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||||
|
path: "/", // Available across the entire site
|
||||||
|
sameSite: "lax", // Protects against CSRF
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: true, message: "Login successful" },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Invalid credentials" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login API error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
app/api/dashboard/audits/mockData.ts
Normal file
88
app/api/dashboard/audits/mockData.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
|
|
||||||
|
export const AuditColumns: GridColDef[] = [
|
||||||
|
{ field: "actionType", headerName: "Action Type", width: 130 },
|
||||||
|
{
|
||||||
|
field: "timeStampOfTheAction",
|
||||||
|
headerName: "Timestamp of the action",
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{ field: "adminUsername", headerName: "Admin username", width: 130 },
|
||||||
|
{ field: "adminId", headerName: "Admin ID", width: 130 },
|
||||||
|
{ field: "affectedUserId", headerName: "Affected user ID", width: 130 },
|
||||||
|
{ field: "adminIPAddress", headerName: "Admin IP address", width: 130 },
|
||||||
|
{ field: "reasonNote", headerName: "Reason/Note", width: 130 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AuditData = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
actionType: "Create",
|
||||||
|
timeStampOfTheAction: "2023-03-01T12:00:00",
|
||||||
|
adminUsername: "admin1",
|
||||||
|
adminId: "12345",
|
||||||
|
affectedUserId: "67890",
|
||||||
|
adminIPAddress: "192.168.1.1",
|
||||||
|
reasonNote: "New user created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
actionType: "Update",
|
||||||
|
timeStampOfTheAction: "2023-03-02T12:00:00",
|
||||||
|
adminUsername: "admin2",
|
||||||
|
adminId: "54321",
|
||||||
|
affectedUserId: "09876",
|
||||||
|
adminIPAddress: "192.168.2.2",
|
||||||
|
reasonNote: "User details updated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
actionType: "Delete",
|
||||||
|
timeStampOfTheAction: "2023-03-03T12:00:00",
|
||||||
|
adminUsername: "admin3",
|
||||||
|
adminId: "98765",
|
||||||
|
affectedUserId: "45678",
|
||||||
|
adminIPAddress: "192.168.3.3",
|
||||||
|
reasonNote: "User deleted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
actionType: "Create",
|
||||||
|
timeStampOfTheAction: "2023-03-04T12:00:00",
|
||||||
|
adminUsername: "admin4",
|
||||||
|
adminId: "98765",
|
||||||
|
affectedUserId: "45678",
|
||||||
|
adminIPAddress: "192.168.3.3",
|
||||||
|
reasonNote: "New user created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
actionType: "Update",
|
||||||
|
timeStampOfTheAction: "2023-03-05T12:00:00",
|
||||||
|
adminUsername: "admin2",
|
||||||
|
adminId: "98765",
|
||||||
|
affectedUserId: "45678",
|
||||||
|
adminIPAddress: "192.168.3.3",
|
||||||
|
reasonNote: "User details updated",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AuditSearchLabels = [
|
||||||
|
{ label: "Action Type", field: "actionType", type: "text" },
|
||||||
|
{ label: "Date / Time", field: "dateTime", type: "date" },
|
||||||
|
{
|
||||||
|
label: "affectedUserId",
|
||||||
|
field: "Affected user ID",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Admin ID",
|
||||||
|
field: "adminId",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Admin username",
|
||||||
|
field: "adminUsername",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
];
|
||||||
52
app/api/dashboard/audits/route.ts
Normal file
52
app/api/dashboard/audits/route.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { AuditColumns, AuditData, AuditSearchLabels } from "./mockData";
|
||||||
|
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
const actionType = searchParams.get("actionType");
|
||||||
|
const affectedUserId = searchParams.get("affectedUserId");
|
||||||
|
const adminId = searchParams.get("adminId");
|
||||||
|
const adminUsername = searchParams.get("adminUsername");
|
||||||
|
const timeStampOfTheAction = searchParams.get("dateTime");
|
||||||
|
|
||||||
|
let filteredRows = [...AuditData];
|
||||||
|
|
||||||
|
if (actionType) {
|
||||||
|
filteredRows = filteredRows.filter(
|
||||||
|
(tx) => tx.actionType.toLocaleLowerCase() === actionType.toLocaleLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (affectedUserId) {
|
||||||
|
filteredRows = filteredRows.filter(
|
||||||
|
(tx) => tx.affectedUserId.toLowerCase() === affectedUserId.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminId) {
|
||||||
|
filteredRows = filteredRows.filter(
|
||||||
|
(tx) => tx.adminId === adminId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (adminUsername) {
|
||||||
|
filteredRows = filteredRows.filter(
|
||||||
|
(tx) => tx.adminUsername === adminUsername,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeStampOfTheAction) {
|
||||||
|
filteredRows = filteredRows.filter(
|
||||||
|
(tx) =>
|
||||||
|
tx.timeStampOfTheAction.split(" ")[0] ===
|
||||||
|
formatToDateTimeString(timeStampOfTheAction).split(" ")[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
tableRows: filteredRows,
|
||||||
|
tableColumns: AuditColumns,
|
||||||
|
tableSearchLabels: AuditSearchLabels,
|
||||||
|
});
|
||||||
|
}
|
||||||
256
app/api/dashboard/transactions/deposits/mockData.ts
Normal file
256
app/api/dashboard/transactions/deposits/mockData.ts
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
|
|
||||||
|
export const depositTransactionDummyData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
userId: 17,
|
||||||
|
merchandId: 100987998,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Completed",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
userId: 17,
|
||||||
|
merchandId: 100987998,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Completed",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
userId: 17,
|
||||||
|
merchandId: 100987997,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Completed",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
userId: 19,
|
||||||
|
merchandId: 100987997,
|
||||||
|
transactionId: 1049136973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Completed",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
userId: 19,
|
||||||
|
merchandId: 100987998,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Completed",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
userId: 27,
|
||||||
|
merchandId: 100987997,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Pending",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
userId: 175,
|
||||||
|
merchandId: 100987938,
|
||||||
|
transactionId: 1049136973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Pending",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
userId: 172,
|
||||||
|
merchandId: 100987938,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Card",
|
||||||
|
status: "Pending",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-12 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
userId: 174,
|
||||||
|
merchandId: 100987938,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Bank Transfer",
|
||||||
|
status: "Inprogress",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
userId: 7,
|
||||||
|
merchandId: 100987998,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Bank Transfer",
|
||||||
|
status: "Inprogress",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
userId: 1,
|
||||||
|
merchandId: 100987998,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
depositMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
options: [
|
||||||
|
{ value: "Pending", label: "Pending" },
|
||||||
|
{ value: "Completed", label: "Completed" },
|
||||||
|
{ value: "Inprogress", label: "Inprogress" },
|
||||||
|
{ value: "Error", label: "Error" },
|
||||||
|
],
|
||||||
|
amount: 4000,
|
||||||
|
currency: "EUR",
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const depositTransactionsColumns: GridColDef[] = [
|
||||||
|
{ field: "userId", headerName: "User ID", width: 130 },
|
||||||
|
{ field: "merchandId", headerName: "Merchant ID", width: 130 },
|
||||||
|
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
||||||
|
{ field: "depositMethod", headerName: "Deposit Method", width: 130 },
|
||||||
|
{ field: "status", headerName: "Status", width: 130 },
|
||||||
|
{ field: "actions", headerName: "Actions", width: 150 },
|
||||||
|
{ field: "amount", headerName: "Amount", width: 130 },
|
||||||
|
{ field: "currency", headerName: "Currency", width: 130 },
|
||||||
|
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
||||||
|
{ field: "errorInfo", headerName: "Error Info", width: 130 },
|
||||||
|
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const depositTransactionsSearchLabels = [
|
||||||
|
{ label: "User", field: "userId", type: "text" },
|
||||||
|
{ label: "Transaction ID", field: "transactionId", type: "text" },
|
||||||
|
{
|
||||||
|
label: "Transaction Reference ID",
|
||||||
|
field: "transactionReferenceId",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Currency",
|
||||||
|
field: "currency",
|
||||||
|
type: "select",
|
||||||
|
options: ["USD", "EUR", "GBP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Status",
|
||||||
|
field: "status",
|
||||||
|
type: "select",
|
||||||
|
options: ["Pending", "Inprogress", "Completed", "Failed"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Payment Method",
|
||||||
|
field: "depositMethod",
|
||||||
|
type: "select",
|
||||||
|
options: ["Card", "Bank Transfer"],
|
||||||
|
},
|
||||||
|
{ label: "Date / Time", field: "dateTime", type: "date" },
|
||||||
|
];
|
||||||
62
app/api/dashboard/transactions/deposits/route.ts
Normal file
62
app/api/dashboard/transactions/deposits/route.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import {
|
||||||
|
depositTransactionDummyData,
|
||||||
|
depositTransactionsColumns,
|
||||||
|
depositTransactionsSearchLabels,
|
||||||
|
} from "./mockData";
|
||||||
|
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
const status = searchParams.get("status");
|
||||||
|
const userId = searchParams.get("userId");
|
||||||
|
const depositMethod = searchParams.get("depositMethod");
|
||||||
|
const merchandId = searchParams.get("merchandId");
|
||||||
|
const transactionId = searchParams.get("transactionId");
|
||||||
|
const dateTime = searchParams.get("dateTime");
|
||||||
|
|
||||||
|
let filteredTransactions = [...depositTransactionDummyData];
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.userId.toString() === userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.status.toLowerCase() === status.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depositMethod) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.depositMethod.toLowerCase() === depositMethod.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (merchandId) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.merchandId.toString() === merchandId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (transactionId) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.transactionId.toString() === transactionId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateTime) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) =>
|
||||||
|
tx.dateTime.split(" ")[0] ===
|
||||||
|
formatToDateTimeString(dateTime).split(" ")[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
tableRows: filteredTransactions,
|
||||||
|
tableSearchLabels: depositTransactionsSearchLabels,
|
||||||
|
tableColumns: depositTransactionsColumns,
|
||||||
|
});
|
||||||
|
}
|
||||||
176
app/api/dashboard/transactions/withdrawal/mockData.ts
Normal file
176
app/api/dashboard/transactions/withdrawal/mockData.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
|
|
||||||
|
export const withdrawalTransactionDummyData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
userId: 17,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
userId: 17,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
userId: 17,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Complete",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
userId: 19,
|
||||||
|
transactionId: 1049136973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Completed",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-18 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
userId: 19,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
userId: 27,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
userId: 1,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
userId: 172,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Card",
|
||||||
|
status: "Pending",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-12 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
userId: 174,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Inprogress",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
userId: 1,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
userId: 1,
|
||||||
|
transactionId: 1049131973,
|
||||||
|
withdrawalMethod: "Bank Transfer",
|
||||||
|
status: "Error",
|
||||||
|
amount: 4000,
|
||||||
|
dateTime: "2025-06-17 10:10:30",
|
||||||
|
errorInfo: "-",
|
||||||
|
fraudScore: "frad score 1234",
|
||||||
|
manualCorrectionFlag: "-",
|
||||||
|
informationWhoApproved: "-",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const withdrawalTransactionsColumns: GridColDef[] = [
|
||||||
|
{ field: "userId", headerName: "User ID", width: 130 },
|
||||||
|
{ field: "transactionId", headerName: "Transaction ID", width: 130 },
|
||||||
|
{ field: "withdrawalMethod", headerName: "Withdrawal Method", width: 130 },
|
||||||
|
{ field: "status", headerName: "Status", width: 130 },
|
||||||
|
{ field: "amount", headerName: "Amount", width: 130 },
|
||||||
|
{ field: "dateTime", headerName: "Date / Time", width: 130 },
|
||||||
|
{ field: "errorInfo", headerName: "Error Info", width: 130 },
|
||||||
|
{ field: "fraudScore", headerName: "Fraud Score", width: 130 },
|
||||||
|
{
|
||||||
|
field: "manualCorrectionFlag",
|
||||||
|
headerName: "Manual Correction Flag",
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "informationWhoApproved",
|
||||||
|
headerName: "Information who approved",
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const withdrawalTransactionsSearchLabels = [
|
||||||
|
{
|
||||||
|
label: "Status",
|
||||||
|
field: "status",
|
||||||
|
type: "select",
|
||||||
|
options: ["Pending", "Inprogress", "Completed", "Failed"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Payment Method",
|
||||||
|
field: "depositMethod",
|
||||||
|
type: "select",
|
||||||
|
options: ["Card", "Bank Transfer"],
|
||||||
|
},
|
||||||
|
{ label: "Date / Time", field: "dateTime", type: "date" },
|
||||||
|
];
|
||||||
51
app/api/dashboard/transactions/withdrawal/route.ts
Normal file
51
app/api/dashboard/transactions/withdrawal/route.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import {
|
||||||
|
withdrawalTransactionDummyData,
|
||||||
|
withdrawalTransactionsColumns,
|
||||||
|
withdrawalTransactionsSearchLabels,
|
||||||
|
} from "./mockData";
|
||||||
|
import { formatToDateTimeString } from "@/app/utils/formatDate";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
const userId = searchParams.get("userId");
|
||||||
|
const status = searchParams.get("status");
|
||||||
|
const dateTime = searchParams.get("dateTime");
|
||||||
|
const withdrawalMethod = searchParams.get("withdrawalMethod");
|
||||||
|
|
||||||
|
let filteredTransactions = [...withdrawalTransactionDummyData];
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.userId.toString() === userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) => tx.status.toLowerCase() === status.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withdrawalMethod) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) =>
|
||||||
|
tx.withdrawalMethod.toLowerCase() === withdrawalMethod.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateTime) {
|
||||||
|
filteredTransactions = filteredTransactions.filter(
|
||||||
|
(tx) =>
|
||||||
|
tx.dateTime.split(" ")[0] ===
|
||||||
|
formatToDateTimeString(dateTime).split(" ")[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
tableRows: filteredTransactions,
|
||||||
|
tableSearchLabels: withdrawalTransactionsSearchLabels,
|
||||||
|
tableColumns: withdrawalTransactionsColumns,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,25 +1,33 @@
|
|||||||
|
// app/components/PageLinks/PageLinks.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
|
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; // Keep this import
|
||||||
import clsx from "clsx"; // Utility to merge class names
|
import clsx from "clsx"; // Utility to merge class names
|
||||||
import "./PageLinks.scss";
|
import "./PageLinks.scss"; // Keep this import
|
||||||
|
|
||||||
|
// Define the props interface for your PageLinks component
|
||||||
|
// It now extends ISidebarLink and includes isShowIcon
|
||||||
interface IPageLinksProps extends ISidebarLink {
|
interface IPageLinksProps extends ISidebarLink {
|
||||||
isShowIcon?: boolean;
|
isShowIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageLinks component
|
||||||
export default function PageLinks({
|
export default function PageLinks({
|
||||||
title,
|
title,
|
||||||
path,
|
path,
|
||||||
icon: Icon,
|
icon: Icon, // Destructure icon as Icon
|
||||||
}: IPageLinksProps) {
|
}: // isShowIcon, // If you plan to use this prop, uncomment it and add logic
|
||||||
|
IPageLinksProps) {
|
||||||
return (
|
return (
|
||||||
<Link href={path} passHref legacyBehavior className="page-link">
|
// Corrected Link usage for Next.js 13/14 App Router:
|
||||||
<a className={clsx("page-link__container")}>
|
// - Removed `passHref` and `legacyBehavior`
|
||||||
|
// - Applied `className` directly to the Link component
|
||||||
|
// - Removed the nested `<a>` tag
|
||||||
|
<Link href={path} className={clsx("page-link", "page-link__container")}>
|
||||||
|
{/* Conditionally render Icon if it exists */}
|
||||||
{Icon && <Icon />}
|
{Icon && <Icon />}
|
||||||
<span className="page-link__text">{title}</span>
|
<span className="page-link__text">{title}</span>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
"use client"
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Chip, Typography, Button } from '@mui/material';
|
import { Box, Chip, Typography, Button } from '@mui/material';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
interface SearchFiltersProps {
|
interface SearchFiltersProps {
|
||||||
filters: Record<string, string>;
|
filters: Record<string, string>;
|
||||||
onDeleteFilter?: (key: string) => void;
|
|
||||||
onClearAll?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchFilters = ({ filters }: SearchFiltersProps) => {
|
const SearchFilters = ({ filters }: SearchFiltersProps) => {
|
||||||
|
|||||||
@ -1,29 +1,14 @@
|
|||||||
import Users from "@/app/features/pages/admin/users/users";
|
import Users from "@/app/features/pages/Admin/Users/users";
|
||||||
|
|
||||||
export default async function BackOfficeUsersPage() {
|
export default async function BackOfficeUsersPage() {
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL
|
process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL
|
||||||
? `https://${process.env.VERCEL_URL}`
|
? `https://${process.env.VERCEL_URL}`
|
||||||
: "http://localhost:3000";
|
: "http://localhost:3000";
|
||||||
|
|
||||||
let users = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, {
|
const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, {
|
||||||
// cache: "no-store", // optional: disable SSR caching if needed
|
cache: "no-store", // 👈 disables caching for SSR freshness
|
||||||
});
|
});
|
||||||
|
const users = await res.json();
|
||||||
if (!res.ok) {
|
|
||||||
// If the API responds with 500/404/etc., log and bail gracefully
|
|
||||||
console.error(
|
|
||||||
`Failed to fetch users: ${res.status} ${res.statusText}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
users = await res.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching users:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
// This ensures this component is rendered only on the client side
|
// This ensures this component is rendered only on the client side
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Approve } from "@/app/components/pages/Approve/Approve";
|
import { Approve } from "@/app/features/pages/Approve/Approve";
|
||||||
|
|
||||||
|
|
||||||
export default function ApprovePage() {
|
export default function ApprovePage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
22
app/dashboard/audits/page.tsx
Normal file
22
app/dashboard/audits/page.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import DataTable from "@/app/features/DataTable/DataTable";
|
||||||
|
import { getAudits } from "@/app/services/audits";
|
||||||
|
|
||||||
|
export default async function AuditPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<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 getAudits({ query });
|
||||||
|
|
||||||
|
return <DataTable data={data} />;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DashboardHomePage } from "../features/pages/dashboardhomepage/dashboardhomepage";
|
import { DashboardHomePage } from "../features/pages/DashboardHomePage/DashboardHomePage";
|
||||||
|
|
||||||
const DashboardPage = () => {
|
const DashboardPage = () => {
|
||||||
return <DashboardHomePage />;
|
return <DashboardHomePage />;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
|
import DataTable from "@/app/features/DataTable/DataTable";
|
||||||
import TransactionsTable from "@/app/features/pages/transactions/transactionstable";
|
|
||||||
import { getTransactions } from "@/app/services/transactions";
|
import { getTransactions } from "@/app/services/transactions";
|
||||||
|
|
||||||
export default async function DepositTransactionPage({
|
export default async function DepositTransactionPage({
|
||||||
@ -17,8 +16,8 @@ export default async function DepositTransactionPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const query = new URLSearchParams(safeParams).toString();
|
const query = new URLSearchParams(safeParams).toString();
|
||||||
const transactionType = 'deposit';
|
const transactionType = "deposits";
|
||||||
const data = await getTransactions({ transactionType, query });
|
const data = await getTransactions({ transactionType, query });
|
||||||
|
|
||||||
return <TransactionsTable res={data}/>;
|
return <DataTable data={data} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,3 @@
|
|||||||
import TransactionsTable from "@/app/features/pages/transactions/transactionstable";
|
export default async function HistoryTransactionPage() {
|
||||||
import { getTransactions } from "@/app/services/transactions";
|
return <div>History Transactions Page</div>;
|
||||||
|
|
||||||
export default async function DepositTransactionPage({
|
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
||||||
}) {
|
|
||||||
// Await searchParams before processing
|
|
||||||
const params = await searchParams;
|
|
||||||
// Create a safe query string by filtering only string values
|
|
||||||
const safeParams: Record<string, string> = {};
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
safeParams[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const query = new URLSearchParams(safeParams).toString();
|
|
||||||
const transactionType = 'deposit';
|
|
||||||
const data = await getTransactions({ transactionType, query });
|
|
||||||
|
|
||||||
return <TransactionsTable res={data}/>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import TransactionsTable from "@/app/features/pages/transactions/transactionstable";
|
import DataTable from "@/app/features/DataTable/DataTable";
|
||||||
import { getTransactions } from "@/app/services/transactions";
|
import { getTransactions } from "@/app/services/transactions";
|
||||||
|
|
||||||
export default async function WithdrawalTransactionPage({
|
export default async function WithdrawalTransactionPage({
|
||||||
@ -16,8 +16,8 @@ export default async function WithdrawalTransactionPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const query = new URLSearchParams(safeParams).toString();
|
const query = new URLSearchParams(safeParams).toString();
|
||||||
const transactionType = 'withdrawal';
|
const transactionType = "withdrawal";
|
||||||
const data = await getTransactions({ transactionType, query });
|
const data = await getTransactions({ transactionType, query });
|
||||||
|
|
||||||
return <TransactionsTable res={data}/>;
|
return <DataTable data={data} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,16 +17,9 @@ import SearchIcon from "@mui/icons-material/Search";
|
|||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import { ISearchLabel } from "../pages/transactions/types";
|
||||||
|
|
||||||
|
export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
|
||||||
interface ILabel {
|
|
||||||
label: string;
|
|
||||||
field: string;
|
|
||||||
type: string;
|
|
||||||
options?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -37,7 +30,6 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
setFormValues(initialParams);
|
setFormValues(initialParams);
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
|
|
||||||
const updateURL = useMemo(
|
const updateURL = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((newValues: Record<string, string>) => {
|
debounce((newValues: Record<string, string>) => {
|
||||||
@ -47,7 +39,7 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
});
|
});
|
||||||
router.push(`?${updatedParams.toString()}`);
|
router.push(`?${updatedParams.toString()}`);
|
||||||
}, 500),
|
}, 500),
|
||||||
[router]
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: string) => {
|
const handleFieldChange = (field: string, value: string) => {
|
||||||
@ -61,7 +53,8 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
router.push("?");
|
router.push("?");
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
const toggleDrawer =
|
||||||
|
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
event.type === "keydown" &&
|
event.type === "keydown" &&
|
||||||
((event as React.KeyboardEvent).key === "Tab" ||
|
((event as React.KeyboardEvent).key === "Tab" ||
|
||||||
@ -73,7 +66,7 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '185px' }}>
|
<Box sx={{ width: "185px" }}>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
@ -137,7 +130,9 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={formValues[field] || ""}
|
value={formValues[field] || ""}
|
||||||
onChange={(e) => handleFieldChange(field, e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange(field, e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -145,7 +140,9 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
<FormControl fullWidth size="small">
|
<FormControl fullWidth size="small">
|
||||||
<Select
|
<Select
|
||||||
value={formValues[field] || ""}
|
value={formValues[field] || ""}
|
||||||
onChange={(e) => handleFieldChange(field, e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange(field, e.target.value)
|
||||||
|
}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
>
|
>
|
||||||
<MenuItem value="">
|
<MenuItem value="">
|
||||||
@ -162,15 +159,17 @@ export default function AdvancedSearch({ labels }: { labels: ILabel[] }) {
|
|||||||
|
|
||||||
{type === "date" && (
|
{type === "date" && (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={formValues[field] ? new Date(formValues[field]) : null}
|
value={
|
||||||
|
formValues[field] ? new Date(formValues[field]) : null
|
||||||
|
}
|
||||||
onChange={(newValue) =>
|
onChange={(newValue) =>
|
||||||
handleFieldChange(
|
handleFieldChange(
|
||||||
field,
|
field,
|
||||||
newValue?.toISOString() || ""
|
newValue?.toISOString() || "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: { fullWidth: true, size: "small" }
|
textField: { fullWidth: true, size: "small" },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
197
app/features/Auth/LoginModal.scss
Normal file
197
app/features/Auth/LoginModal.scss
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/* app/styles/LoginModal.scss (BEM Methodology) */
|
||||||
|
|
||||||
|
// Variables for consistent styling
|
||||||
|
$primary-color: #2563eb; // Blue-600 equivalent
|
||||||
|
$primary-hover-color: #1d4ed8; // Blue-700 equivalent
|
||||||
|
$success-color: #16a34a; // Green-600 equivalent
|
||||||
|
$error-color: #dc2626; // Red-600 equivalent
|
||||||
|
$text-color-dark: #1f2937; // Gray-800 equivalent
|
||||||
|
$text-color-medium: #4b5563; // Gray-700 equivalent
|
||||||
|
$text-color-light: #6b7280; // Gray-600 equivalent
|
||||||
|
$border-color: #d1d5db; // Gray-300 equivalent
|
||||||
|
$bg-color-light: #f3f4f6; // Gray-100 equivalent
|
||||||
|
$bg-color-white: #ffffff;
|
||||||
|
|
||||||
|
/* --- Login Modal Block (.login-modal) --- */
|
||||||
|
.login-modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(17, 24, 39, 0.75); // Gray-900 75% opacity
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 50;
|
||||||
|
padding: 1rem; // p-4
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal__content {
|
||||||
|
background-color: $bg-color-white;
|
||||||
|
border-radius: 0.75rem; // rounded-xl
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04); // shadow-2xl
|
||||||
|
padding: 2rem; // p-8
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem; // max-w-md
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.3s ease-in-out; // transition-all duration-300
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal__title {
|
||||||
|
font-size: 1.875rem; // text-3xl
|
||||||
|
font-weight: 700; // font-bold
|
||||||
|
color: $text-color-dark;
|
||||||
|
margin-bottom: 1.5rem; // mb-6
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Login Form Block (.login-form) --- */
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem; // space-y-6
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__group {
|
||||||
|
// No specific styles needed here, just a container for label/input
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem; // text-sm
|
||||||
|
font-weight: 500; // font-medium
|
||||||
|
color: $text-color-medium;
|
||||||
|
margin-bottom: 0.25rem; // mb-1
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 1rem; // px-4 py-2
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.5rem; // rounded-lg
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm
|
||||||
|
font-size: 0.875rem; // sm:text-sm
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-color: $primary-color; // focus:border-blue-500
|
||||||
|
box-shadow: 0 0 0 1px $primary-color, 0 0 0 3px rgba($primary-color, 0.5); // focus:ring-blue-500
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem; // text-sm
|
||||||
|
font-weight: 500; // font-medium
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__message--success {
|
||||||
|
color: $success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__message--error {
|
||||||
|
color: $error-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__button {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem 1rem; // py-3 px-4
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.5rem; // rounded-lg
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm
|
||||||
|
font-size: 1.125rem; // text-lg
|
||||||
|
font-weight: 600; // font-semibold
|
||||||
|
color: $bg-color-white;
|
||||||
|
background-color: $primary-color;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; // transition duration-300 ease-in-out
|
||||||
|
&:hover {
|
||||||
|
background-color: darken($primary-color, 5%); // blue-700 equivalent
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5),
|
||||||
|
0 0 0 4px rgba($primary-color, 0.5); // focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form__spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
height: 1.25rem; // h-5
|
||||||
|
width: 1.25rem; // w-5
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Page Container Block (.page-container) --- */
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: $bg-color-light;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: "Inter", sans-serif; // Assuming Inter font is used
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 56rem; // max-w-4xl
|
||||||
|
background-color: $bg-color-white;
|
||||||
|
border-radius: 0.75rem; // rounded-xl
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg
|
||||||
|
padding: 2rem; // p-8
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__title {
|
||||||
|
font-size: 2.25rem; // text-4xl
|
||||||
|
font-weight: 700; // font-bold
|
||||||
|
color: $text-color-dark;
|
||||||
|
margin-bottom: 1.5rem; // mb-6
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__message--logged-in {
|
||||||
|
font-size: 1.25rem; // text-xl
|
||||||
|
color: $success-color;
|
||||||
|
margin-bottom: 1rem; // mb-4
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__text {
|
||||||
|
color: $text-color-medium;
|
||||||
|
margin-bottom: 1.5rem; // mb-6
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__button--logout {
|
||||||
|
padding: 0.75rem 1.5rem; // px-6 py-3
|
||||||
|
background-color: $error-color;
|
||||||
|
color: $bg-color-white;
|
||||||
|
font-weight: 600; // font-semibold
|
||||||
|
border-radius: 0.5rem; // rounded-lg
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md
|
||||||
|
transition: background-color 0.3s ease-in-out;
|
||||||
|
}
|
||||||
112
app/features/Auth/LoginModal.tsx
Normal file
112
app/features/Auth/LoginModal.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"use client"; // This MUST be the very first line of the file
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import "./LoginModal.scss"; // Adjust path based on your actual structure
|
||||||
|
|
||||||
|
// Define the props interface for LoginModal
|
||||||
|
type LoginModalProps = {
|
||||||
|
onLogin: (email: string, password: string) => Promise<boolean>;
|
||||||
|
authMessage: string;
|
||||||
|
clearAuthMessage: () => void;
|
||||||
|
};
|
||||||
|
// LoginModal component
|
||||||
|
export default function LoginModal({
|
||||||
|
onLogin,
|
||||||
|
authMessage,
|
||||||
|
clearAuthMessage,
|
||||||
|
}: LoginModalProps) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
console.log("LoginModal rendered"); // Debugging log to check if the component renders
|
||||||
|
|
||||||
|
// Effect to clear authentication messages when email or password inputs change
|
||||||
|
useEffect(() => {
|
||||||
|
// clearAuthMessage();
|
||||||
|
}, [
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
// clearAuthMessage
|
||||||
|
]); // Dependency array ensures effect runs when these change
|
||||||
|
|
||||||
|
// Handler for form submission
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault(); // Prevent default form submission behavior
|
||||||
|
setIsLoading(true); // Set loading state to true
|
||||||
|
await onLogin(email, password); // Call the passed onLogin function (now uncommented)
|
||||||
|
setIsLoading(false); // Set loading state back to false after login attempt
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
// The content of the login modal, without the modal overlay/wrapper
|
||||||
|
<form onSubmit={handleSubmit} className="login-form">
|
||||||
|
{/* Email input field */}
|
||||||
|
<div className="login-form__group">
|
||||||
|
<label htmlFor="email" className="login-form__label">
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className="login-form__input"
|
||||||
|
placeholder="admin@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password input field */}
|
||||||
|
<div className="login-form__group">
|
||||||
|
<label htmlFor="password" className="login-form__label">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className="login-form__input"
|
||||||
|
placeholder="password123"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="login-form__button"
|
||||||
|
disabled={isLoading} // Disable button while loading
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
// SVG spinner for loading state
|
||||||
|
<svg
|
||||||
|
className="login-form__spinner"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{/* BEM class name */}
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
"Login" // Button text for normal state
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
app/features/DataTable/DataTable.tsx
Normal file
199
app/features/DataTable/DataTable.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
Stack,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||||
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
|
import AdvancedSearch from "../AdvancedSearch/AdvancedSearch";
|
||||||
|
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
|
||||||
|
import { exportData } from "@/app/utils/exportData";
|
||||||
|
import { IDataTable } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
interface IDataTableProps<TRow, TColumn> {
|
||||||
|
data: IDataTable<TRow, TColumn>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TWithId = { id: number };
|
||||||
|
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>(
|
||||||
|
data: IDataTableProps<TRow, TColumn>,
|
||||||
|
) => {
|
||||||
|
const { tableRows, tableColumns, tableSearchLabels } = data.data;
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
|
||||||
|
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
|
||||||
|
const [rows, setRows] = useState<TRow[]>(tableRows);
|
||||||
|
|
||||||
|
const filters = Object.fromEntries(searchParams.entries());
|
||||||
|
|
||||||
|
const handleClickField = (field: string, value: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
params.set(field, value);
|
||||||
|
router.push(`?${params.toString()}`);
|
||||||
|
router.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (id: number, newStatus: string) => {
|
||||||
|
setRows(
|
||||||
|
rows.map((row) => (row.id === id ? { ...row, status: newStatus } : row)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnsWithDropdown = (columns: TColumn[]): GridColDef[] => {
|
||||||
|
return columns?.map((col) => {
|
||||||
|
if (col.field === "actions") {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
const row = tableRows.find((r) => r.id === params.id) as {
|
||||||
|
id: number;
|
||||||
|
status?: string;
|
||||||
|
options?: { value: string; label: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = row?.options;
|
||||||
|
if (!options) return params.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={params.value ?? row.status}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleStatusChange(params.id as number, e.target.value)
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
||||||
|
"& .MuiSelect-select": { py: 0.5 },
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
p={2}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label="Search"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => console.log(`setSearchQuery(${e.target.value})`)}
|
||||||
|
sx={{ width: 300 }}
|
||||||
|
/>
|
||||||
|
<AdvancedSearch labels={tableSearchLabels} />
|
||||||
|
<SearchFilters filters={filters} />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileUploadIcon />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<DataGrid
|
||||||
|
rows={tableRows}
|
||||||
|
columns={getColumnsWithDropdown(tableColumns)}
|
||||||
|
initialState={{
|
||||||
|
pagination: { paginationModel: { pageSize: 50 } },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[50, 100]}
|
||||||
|
sx={{
|
||||||
|
border: 0,
|
||||||
|
cursor: "pointer",
|
||||||
|
"& .MuiDataGrid-cell": {
|
||||||
|
py: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onCellClick={(params) => {
|
||||||
|
if (params.field !== "actions") {
|
||||||
|
handleClickField(params.field, params.value as string);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Export Dialog */}
|
||||||
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||||
|
<DialogTitle>Export Transactions</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||||
|
<Select
|
||||||
|
value={fileType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFileType(e.target.value as "csv" | "xls" | "xlsx")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="csv">CSV</MenuItem>
|
||||||
|
<MenuItem value="xls">XLS</MenuItem>
|
||||||
|
<MenuItem value="xlsx">XLSX</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={onlyCurrentTable}
|
||||||
|
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Only export the results in the current table"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
exportData(
|
||||||
|
tableRows,
|
||||||
|
tableColumns,
|
||||||
|
fileType,
|
||||||
|
onlyCurrentTable,
|
||||||
|
setOpen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataTable;
|
||||||
12
app/features/DataTable/types.ts
Normal file
12
app/features/DataTable/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
interface ISearchLabel {
|
||||||
|
label: string;
|
||||||
|
field: string;
|
||||||
|
type: string;
|
||||||
|
options?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataTable<TRow, TColumn> {
|
||||||
|
tableRows: TRow[];
|
||||||
|
tableColumns: TColumn[];
|
||||||
|
tableSearchLabels: ISearchLabel[];
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOve
|
|||||||
export const DashboardHomePage = () => {
|
export const DashboardHomePage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Conditional rendering of the Generic Modal, passing LoginModal as children */}
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
<GeneralHealthCard />
|
<GeneralHealthCard />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@ -6,6 +6,17 @@ import AccountMenu from "./accountMenu/AccountMenu";
|
|||||||
import "./Header.scss";
|
import "./Header.scss";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
|
// const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
// // Handle menu open
|
||||||
|
// const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
// setAnchorEl(event.currentTarget);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Handle menu close
|
||||||
|
// const handleMenuClose = () => {
|
||||||
|
// setAnchorEl(null);
|
||||||
|
// };
|
||||||
|
|
||||||
const handleChange = () => {};
|
const handleChange = () => {};
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import SettingsIcon from "@mui/icons-material/Settings";
|
|||||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
|
import FactCheckIcon from "@mui/icons-material/FactCheck";
|
||||||
|
|
||||||
|
|
||||||
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
|
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
|
||||||
|
|
||||||
@ -75,6 +77,7 @@ export const PAGE_LINKS: ISidebarLink[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ title: "Account IQ", path: "/dashboard/account-iq", icon: InsightsIcon },
|
{ title: "Account IQ", path: "/dashboard/account-iq", icon: InsightsIcon },
|
||||||
|
{ title: "Audits", path: "/dashboard/audits", icon: FactCheckIcon },
|
||||||
// { title: 'Documentation', path: '/documentation', icon: DescriptionIcon },
|
// { title: 'Documentation', path: '/documentation', icon: DescriptionIcon },
|
||||||
// { title: 'Support', path: '/support', icon: SupportAgentIcon },
|
// { title: 'Support', path: '/support', icon: SupportAgentIcon },
|
||||||
// { title: 'System Status', path: '/system-status', icon: WarningAmberIcon },
|
// { title: 'System Status', path: '/system-status', icon: WarningAmberIcon },
|
||||||
|
|||||||
64
app/login/page.scss
Normal file
64
app/login/page.scss
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Variables for consistent styling
|
||||||
|
$primary-color: #2563eb; // Blue-600 equivalent
|
||||||
|
$primary-hover-color: #1d4ed8; // Blue-700 equivalent
|
||||||
|
$success-color: #16a34a; // Green-600 equivalent
|
||||||
|
$error-color: #dc2626; // Red-600 equivalent
|
||||||
|
$text-color-dark: #1f2937; // Gray-800 equivalent
|
||||||
|
$text-color-medium: #4b5563; // Gray-700 equivalent
|
||||||
|
$text-color-light: #6b7280; // Gray-600 equivalent
|
||||||
|
$border-color: #d1d5db; // Gray-300 equivalent
|
||||||
|
$bg-color-light: #f3f4f6; // Gray-100 equivalent
|
||||||
|
$bg-color-white: #ffffff;
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: $bg-color-light;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: "Inter", sans-serif; // Assuming Inter font is used
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 56rem; // max-w-4xl
|
||||||
|
background-color: $bg-color-white;
|
||||||
|
border-radius: 0.75rem; // rounded-xl
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg
|
||||||
|
padding: 2rem; // p-8
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__title {
|
||||||
|
font-size: 2.25rem; // text-4xl
|
||||||
|
font-weight: 700; // font-bold
|
||||||
|
color: $text-color-dark;
|
||||||
|
margin-bottom: 1.5rem; // mb-6
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__message--logged-in {
|
||||||
|
font-size: 1.25rem; // text-xl
|
||||||
|
color: $success-color;
|
||||||
|
margin-bottom: 1rem; // mb-4
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__text {
|
||||||
|
color: $text-color-medium;
|
||||||
|
margin-bottom: 1.5rem; // mb-6
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container__button--logout {
|
||||||
|
padding: 0.75rem 1.5rem; // px-6 py-3
|
||||||
|
background-color: $error-color;
|
||||||
|
color: $bg-color-white;
|
||||||
|
font-weight: 600; // font-semibold
|
||||||
|
border-radius: 0.5rem; // rounded-lg
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md
|
||||||
|
transition: background-color 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: darken($error-color, 5%); // red-700 equivalent
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/login/page.tsx
Normal file
103
app/login/page.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import LoginModal from "../features/Auth/LoginModal"; // Your LoginModal component
|
||||||
|
|
||||||
|
import "./page.scss"; // Global styles for LoginModal and page
|
||||||
|
import Modal from "../components/Modal/Modal";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
||||||
|
|
||||||
|
const [authMessage, setAuthMessage] = useState("");
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if already logged in by trying to fetch a protected resource or checking a client-accessible flag
|
||||||
|
// For HTTP-only cookies, you can't directly read the token here.
|
||||||
|
// Instead, you'd rely on a server-side check (e.g., in middleware or a Server Component)
|
||||||
|
// or a simple client-side flag if your backend also sets one (less secure for token itself).
|
||||||
|
// For this example, we'll assume if they land here, they need to log in.
|
||||||
|
// A more robust check might involve a quick API call to /api/auth/status
|
||||||
|
// if the token is in an HTTP-only cookie.
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
// In a real app, this might be a call to a /api/auth/status endpoint
|
||||||
|
// that checks the HTTP-only cookie on the server and returns a boolean.
|
||||||
|
// For now, we'll rely on the middleware to redirect if unauthenticated.
|
||||||
|
// If the user somehow lands on /login with a valid cookie, the middleware
|
||||||
|
// should have redirected them already.
|
||||||
|
};
|
||||||
|
checkAuthStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = async (email: string, password: string) => {
|
||||||
|
setAuthMessage("Attempting login...");
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/login", {
|
||||||
|
// <--- CALLING YOUR INTERNAL ROUTE HANDLER
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Check if the response status is 2xx
|
||||||
|
// Backend has successfully set the HTTP-only cookie
|
||||||
|
setAuthMessage("Login successful!");
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
// Redirect to the intended path after successful login
|
||||||
|
router.replace(redirectPath);
|
||||||
|
} else {
|
||||||
|
// Handle login errors (e.g., invalid credentials)
|
||||||
|
setAuthMessage(data.message || "Login failed. Please try again.");
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login failed:", error);
|
||||||
|
setAuthMessage("An error occurred during login. Please try again later.");
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
}
|
||||||
|
return isLoggedIn; // Return the current login status
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAuthMessage = () => setAuthMessage("");
|
||||||
|
|
||||||
|
// If user is already logged in (e.g., redirected by middleware to dashboard),
|
||||||
|
// this page shouldn't be visible. The middleware should handle the primary redirect.
|
||||||
|
// This `isLoggedIn` state here is more for internal page logic if the user somehow
|
||||||
|
// bypasses middleware or lands on /login with a valid session.
|
||||||
|
// For a robust setup, the middleware is key.
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Always show the modal on the login page */}
|
||||||
|
<Modal
|
||||||
|
open={true} // Always open on the login page
|
||||||
|
onClose={() => {
|
||||||
|
/* No direct close for login modal, user must log in */
|
||||||
|
}}
|
||||||
|
title="Login to Backoffice"
|
||||||
|
>
|
||||||
|
<LoginModal
|
||||||
|
onLogin={handleLogin} // Pass the API call function
|
||||||
|
authMessage={authMessage}
|
||||||
|
clearAuthMessage={clearAuthMessage}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DashboardHomePage } from "./features/pages/dashboardhomepage/dashboardhomepage";
|
import { DashboardHomePage } from "./features/Pages/DashboardHomePage/DashboardHomePage";
|
||||||
|
|
||||||
const DashboardPage = () => {
|
const DashboardPage = () => {
|
||||||
return <DashboardHomePage />;
|
return <DashboardHomePage />;
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import advancedSearchReducer from './advanedSearch/advancedSearchSlice';
|
import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
|
||||||
import transactionsReducer from './transactions/transactionsSlice';
|
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
advancedSearch: advancedSearchReducer,
|
advancedSearch: advancedSearchReducer,
|
||||||
transactions: transactionsReducer,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
22
app/services/audits.ts
Normal file
22
app/services/audits.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export async function getAudits({
|
||||||
|
query,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
}) {
|
||||||
|
const res = await fetch(
|
||||||
|
`http://localhost:3000/api/dashboard/audits?${query}`,
|
||||||
|
{
|
||||||
|
cache: "no-store",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// Handle error from the API
|
||||||
|
const errorData = await res
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ message: "Unknown error" }));
|
||||||
|
throw new Error(errorData.message || `HTTP error! status: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
@ -1,11 +1,23 @@
|
|||||||
export async function getTransactions({ transactionType, query }: { transactionType: string, query: string }) {
|
export async function getTransactions({
|
||||||
const res = await fetch(`http://localhost:3000/api/transactions/${transactionType}?${query}`, {
|
transactionType,
|
||||||
|
query,
|
||||||
|
}: {
|
||||||
|
transactionType: string;
|
||||||
|
query: string;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`http://localhost:3000/api/dashboard/transactions/${transactionType}?${query}`,
|
||||||
|
{
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// Handle error from the API
|
// Handle error from the API
|
||||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
const errorData = await res
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ message: "Unknown error" }));
|
||||||
throw new Error(errorData.message || `HTTP error! status: ${res.status}`);
|
throw new Error(errorData.message || `HTTP error! status: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,19 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
||||||
// @ts-nocheck
|
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx";
|
||||||
import { GridColDef } from "@mui/x-data-grid";
|
|
||||||
export type FileType = "csv" | "xls" | "xlsx";
|
export type FileType = "csv" | "xls" | "xlsx";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
|
|
||||||
import type { ITransaction } from "../features/pages/transactions/types";
|
export const exportData = <TRow, TColumn extends GridColDef>(
|
||||||
|
rows: TRow[],
|
||||||
|
columns: TColumn[],
|
||||||
export const exportData = (
|
|
||||||
transactions: ITransaction[],
|
|
||||||
columns: GridColDef[],
|
|
||||||
fileType: FileType = "csv",
|
fileType: FileType = "csv",
|
||||||
onlyCurrentTable = false,
|
onlyCurrentTable = false,
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void,
|
||||||
) => {
|
) => {
|
||||||
const exportRows = onlyCurrentTable ? transactions.slice(0, 5) : transactions;
|
const exportRows = onlyCurrentTable ? rows.slice(0, 5) : rows;
|
||||||
const exportData = [
|
const exportData = [
|
||||||
columns.map((col) => col.headerName),
|
columns.map((col) => col.headerName),
|
||||||
...exportRows.map((row) => columns.map((col) => row[col.field] ?? "")),
|
...exportRows.map((row) => columns.map((col) => (row as Record<string, unknown>)[col.field] ?? "")),
|
||||||
];
|
];
|
||||||
|
|
||||||
const worksheet = XLSX.utils.aoa_to_sheet(exportData);
|
const worksheet = XLSX.utils.aoa_to_sheet(exportData);
|
||||||
|
|||||||
30
middleware.ts
Normal file
30
middleware.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// middleware.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const token = request.cookies.get("auth_token")?.value; // Get token from cookie
|
||||||
|
|
||||||
|
// Define protected paths
|
||||||
|
const protectedPaths = ["/dashboard", "/settings", "/admin"];
|
||||||
|
const isProtected = protectedPaths.some((path) =>
|
||||||
|
request.nextUrl.pathname.startsWith(path)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If accessing a protected path and no token
|
||||||
|
if (isProtected && !token) {
|
||||||
|
// Redirect to login page
|
||||||
|
const loginUrl = new URL("/login", request.url);
|
||||||
|
// Optional: Add a redirect query param to return to original page after login
|
||||||
|
loginUrl.searchParams.set("redirect", request.nextUrl.pathname);
|
||||||
|
return NextResponse.redirect(loginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the request to proceed if not protected or token exists
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure matcher to run middleware on specific paths
|
||||||
|
export const config = {
|
||||||
|
matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"], // Apply to dashboard and its sub-paths
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user