diff --git a/backend/controllers/category.js b/backend/controllers/category.js new file mode 100644 index 0000000..90cbd94 --- /dev/null +++ b/backend/controllers/category.js @@ -0,0 +1,47 @@ +const db = require("../utils/database"); + +exports.getAllCategoriesWithPagination = async (req, res) => { + const limit = +req.query?.limit; + const page = +req.query?.page; + const offset = (page - 1) * limit; + try { + const [data, _] = await db.execute( + "SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?", + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category"); + const { count: total } = result[0]; + return res.json({ data, total }); + } catch (error) { + res.json({ error: "Cannot fetch categories from database!" }); + } +}; + +exports.addCategory = async (req, res) => { + const { name } = req.body; + + try { + const [result] = await db.execute( + "INSERT INTO Category (Name) VALUES (?)", + [name] + ); + res.json({ message: "Adding new category successfully!" }); + } catch (error) { + res.json({ error: "Cannot add new category!" }); + } +}; + +exports.removeCategory = async (req, res) => { + const { id } = req.params; + + try { + const [result] = await db.execute( + `DELETE FROM Category WHERE CategoryID = ?`, + [id] + ); + res.json({ message: "Delete category successfully!" }); + } catch (error) { + res.json({ error: "Cannot remove category from database!" }); + } +}; diff --git a/backend/controllers/product.js b/backend/controllers/product.js index 569e4c6..a606fa2 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -6,7 +6,7 @@ exports.addProduct = async (req, res) => { try { const [result] = await db.execute( `INSERT INTO Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`, - [name, price, qty, userID, description, category], + [name, price, qty, userID, description, category] ); const productID = result.insertId; @@ -15,7 +15,7 @@ exports.addProduct = async (req, res) => { db.execute(`INSERT INTO Image_URL (URL, ProductID) VALUES (?, ?)`, [ imagePath, productID, - ]), + ]) ); await Promise.all(imageInsertPromises); //perallel @@ -39,7 +39,7 @@ exports.addFavorite = async (req, res) => { // Use parameterized query to prevent SQL injection const [result] = await db.execute( `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, - [userID, productID], + [userID, productID] ); res.json({ @@ -59,7 +59,7 @@ exports.removeFavorite = async (req, res) => { // Use parameterized query to prevent SQL injection const [result] = await db.execute( `DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`, - [userID, productID], + [userID, productID] ); res.json({ @@ -103,7 +103,7 @@ exports.getFavorites = async (req, res) => { p.Date, u.Name; `, - [userID], + [userID] ); res.json({ @@ -168,7 +168,7 @@ exports.getProductById = async (req, res) => { JOIN User U ON p.UserID = U.UserID WHERE p.ProductID = ? `, - [id], + [id] ); // Log raw data for debugging @@ -211,6 +211,65 @@ exports.getProductById = async (req, res) => { } }; +exports.getProductWithPagination = async (req, res) => { + const limit = +req.query.limit; + const page = +req.query.page; + + const offset = (page - 1) * limit; + + try { + const [data, fields] = await db.execute( + ` + SELECT + P.ProductID, + P.Name AS ProductName, + P.Price, + P.Date AS DateUploaded, + U.Name AS SellerName, + MIN(I.URL) AS ProductImage, + C.Name AS Category + FROM Product P + LEFT JOIN Image_URL I ON P.ProductID = I.ProductID + LEFT JOIN User U ON P.UserID = U.UserID + LEFT JOIN Category C ON P.CategoryID = C.CategoryID + GROUP BY + P.ProductID, + P.Name, + P.Price, + P.Date, + U.Name, + C.Name + ORDER BY P.ProductID ASC + LIMIT ? OFFSET ? + `, + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute( + `SELECT COUNT(*) AS totalProd FROM Product` + ); + const { totalProd } = result[0]; + + return res.json({ totalProd, products: data }); + } catch (error) { + res.json({ error: "Error fetching products!" }); + } +}; + +exports.removeProduct = async (req, res) => { + const { id } = req.params; + + try { + const [result] = await db.execute( + `DELETE FROM Product WHERE ProductID = ?`, + [id] + ); + res.json({ message: "Delete product successfully!" }); + } catch (error) { + res.json({ error: "Cannot remove product from database!" }); + } +}; + // db_con.query( // "SELECT ProductID FROM product WHERE ProductID = ?", // [productID], diff --git a/backend/controllers/transaction.js b/backend/controllers/transaction.js new file mode 100644 index 0000000..ace31bb --- /dev/null +++ b/backend/controllers/transaction.js @@ -0,0 +1,40 @@ +const db = require("../utils/database"); + +exports.getTransactionWithPagination = async (req, res) => { + const limit = +req.query?.limit; + const page = +req.query?.page; + const offset = (page - 1) * limit; + try { + const [data, _] = await db.execute( + `SELECT T.TransactionID, DATE_FORMAT(T.Date, '%b-%d-%Y %h:%i %p') as Date, T.PaymentStatus, U.Name as UserName, P.Name as ProductName + FROM Transaction T + LEFT JOIN User U ON T.UserID = U.UserID + LEFT JOIN Product P ON T.ProductID = P.ProductID + ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`, + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute( + "SELECT COUNT(*) AS count FROM Transaction" + ); + const { count: total } = result[0]; + return res.json({ data, total }); + } catch (error) { + res.json({ error: "Cannot fetch transactions from database!" }); + } +}; + +exports.removeTransation = async (req, res) => { + const { id } = req.params; + try { + const [result] = await db.execute( + "DELETE FROM Transaction WHERE TransactionID = ?;", + [id.toString()] + ); + return res.json({ message: "Remove transaction successfully!" }); + } catch (error) { + return res + .status(500) + .json({ error: "Cannot remove transactions from database!" }); + } +}; diff --git a/backend/controllers/user.js b/backend/controllers/user.js index 32aef17..37c5b06 100644 --- a/backend/controllers/user.js +++ b/backend/controllers/user.js @@ -13,13 +13,13 @@ exports.sendVerificationCode = async (req, res) => { // Generate a random 6-digit code const verificationCode = crypto.randomInt(100000, 999999).toString(); console.log( - `Generated verification code for ${email}: ${verificationCode}`, + `Generated verification code for ${email}: ${verificationCode}` ); // Check if email already exists in verification table const [results, fields] = await db.execute( "SELECT * FROM AuthVerification WHERE Email = ?", - [email], + [email] ); if (results.length > 0) { @@ -27,7 +27,7 @@ exports.sendVerificationCode = async (req, res) => { const [result] = await db.execute( `UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP WHERE Email = ?`, - [verificationCode, email], + [verificationCode, email] ); // Send email and respond @@ -37,7 +37,7 @@ exports.sendVerificationCode = async (req, res) => { // Insert new record const [result] = await db.execute( "INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)", - [email, verificationCode], + [email, verificationCode] ); // Send email and respond await sendVerificationEmail(email, verificationCode); @@ -62,7 +62,7 @@ exports.verifyCode = async (req, res) => { // Check verification code const [results, fields] = await db.execute( "SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)", - [email, code], + [email, code] ); if (results.length === 0) { console.log(`Invalid or expired verification code for email ${email}`); @@ -76,7 +76,7 @@ exports.verifyCode = async (req, res) => { // Mark as authenticated const [result] = await db.execute( "UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?", - [email], + [email] ); res.json({ success: true, @@ -95,7 +95,7 @@ exports.completeSignUp = async (req, res) => { try { const [results, fields] = await db.execute( `SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`, - [data.email], + [data.email] ); if (results.length === 0) { @@ -105,20 +105,20 @@ exports.completeSignUp = async (req, res) => { // Create the user const [createResult] = await db.execute( `INSERT INTO User (Name, Email, UCID, Password, Phone, Address) - VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`, + VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')` ); // Insert role using the user's ID const [insertResult] = await db.execute( `INSERT INTO UserRole (UserID, Client, Admin) VALUES (LAST_INSERT_ID(), ${data.client || true}, ${ - data.admin || false - })`, + data.admin || false + })` ); // Delete verification record const [deleteResult] = await db.execute( - `DELETE FROM AuthVerification WHERE Email = '${data.email}'`, + `DELETE FROM AuthVerification WHERE Email = '${data.email}'` ); res.json({ @@ -310,7 +310,7 @@ exports.deleteUser = async (req, res) => { // Delete from UserRole first (assuming foreign key constraint) const [result1] = await db.execute( "DELETE FROM UserRole WHERE UserID = ?", - [userId], + [userId] ); // Then delete from User table @@ -328,3 +328,38 @@ exports.deleteUser = async (req, res) => { return res.status(500).json({ error: "Could not delete user!" }); } }; + +exports.getUsersWithPagination = async (req, res) => { + const limit = +req.query.limit; + const page = +req.query.page; + + const offset = (page - 1) * limit; + try { + const [users, fields] = await db.execute( + "SELECT * FROM User LIMIT ? OFFSET ?", + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute("SELECT COUNT(*) AS count FROM User"); + const { count: total } = result[0]; + + res.json({ users, total }); + } catch (error) { + console.error("Errors: ", error); + return res.status(500).json({ error: "\nCould not fetch users!" }); + } +}; + +exports.isAdmin = async (req, res) => { + const { id } = req.params; + try { + const [result] = await db.execute( + "SELECT R.Admin FROM marketplace.userrole R WHERE R.UserID = ?", + [id] + ); + const { Admin } = result[0]; + res.json({ isAdmin: Admin }); + } catch (error) { + res.json({ error: "Cannot verify admin status!" }); + } +}; diff --git a/backend/index.js b/backend/index.js index 7afa3fe..796860f 100644 --- a/backend/index.js +++ b/backend/index.js @@ -9,6 +9,8 @@ const searchRouter = require("./routes/search"); const recommendedRouter = require("./routes/recommendation"); const history = require("./routes/history"); const review = require("./routes/review"); +const categoryRouter = require("./routes/category"); +const transactionRouter = require("./routes/transaction"); const { generateEmailTransporter } = require("./utils/mail"); const { @@ -42,10 +44,11 @@ app.use("/api/search", searchRouter); app.use("/api/engine", recommendedRouter); app.use("/api/history", history); app.use("/api/review", review); - +app.use("/api/category", categoryRouter); +app.use("/api/transaction", transactionRouter); // Set up a scheduler to run cleanup every hour -clean_up_time = 30*60*1000; +clean_up_time = 30 * 60 * 1000; setInterval(cleanupExpiredCodes, clean_up_time); app.listen(3030, () => { diff --git a/backend/routes/category.js b/backend/routes/category.js new file mode 100644 index 0000000..9a04a08 --- /dev/null +++ b/backend/routes/category.js @@ -0,0 +1,14 @@ +const express = require("express"); +const { + getAllCategoriesWithPagination, + addCategory, + removeCategory, +} = require("../controllers/category"); + +const router = express.Router(); + +router.get("/getCategories", getAllCategoriesWithPagination); +router.post("/addCategory", addCategory); +router.delete("/:id", removeCategory); + +module.exports = router; diff --git a/backend/routes/product.js b/backend/routes/product.js index 944e63b..4b59e25 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -7,6 +7,8 @@ const { getAllProducts, getProductById, addProduct, + removeProduct, + getProductWithPagination, } = require("../controllers/product"); const router = express.Router(); @@ -22,6 +24,12 @@ router.post("/delFavorite", removeFavorite); router.post("/addProduct", addProduct); router.get("/getProduct", getAllProducts); + +//Remove product +router.delete("/:id", removeProduct); +//Get products with pagination +router.get("/getProductWithPagination", getProductWithPagination); + router.get("/:id", getProductById); // Simplified route module.exports = router; diff --git a/backend/routes/transaction.js b/backend/routes/transaction.js new file mode 100644 index 0000000..0788b96 --- /dev/null +++ b/backend/routes/transaction.js @@ -0,0 +1,12 @@ +const express = require("express"); +const { + getTransactionWithPagination, + removeTransation, +} = require("../controllers/transaction"); + +const router = express.Router(); + +router.get("/getTransactions", getTransactionWithPagination); +router.delete("/:id", removeTransation); + +module.exports = router; diff --git a/backend/routes/user.js b/backend/routes/user.js index 1ccbc88..657b793 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -8,6 +8,8 @@ const { updateUser, deleteUser, doLogin, + isAdmin, + getUsersWithPagination, } = require("../controllers/user"); const router = express.Router(); @@ -36,4 +38,10 @@ router.post("/update", updateUser); //Delete A uses Data: router.post("/delete", deleteUser); +//Check admin status +router.get("/isAdmin/:id", isAdmin); + +//Fetch user with pagination +router.get("/getUserWithPagination", getUsersWithPagination); + module.exports = router; diff --git a/backend/utils/database.js b/backend/utils/database.js index 6e75c3a..643b1f9 100644 --- a/backend/utils/database.js +++ b/backend/utils/database.js @@ -4,6 +4,7 @@ const pool = mysql.createPool({ host: "localhost", user: "root", database: "Marketplace", + password: "12345678", }); module.exports = pool.promise(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b44684..3add1d8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "lucide-react": "^0.477.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.2.0" }, "devDependencies": { @@ -4369,6 +4370,15 @@ "react": "^19.0.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8a323b6..35ececa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "lucide-react": "^0.477.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.2.0" }, "devDependencies": { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4b88b70..ac28ad5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,13 @@ import Transactions from "./pages/Transactions"; import Favorites from "./pages/Favorites"; import ProductDetail from "./pages/ProductDetail"; import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage +import Dashboard from "./pages/Dashboard"; +import UserDashboard from "./pages/UserDashboard"; +import ProductDashboard from "./pages/ProductDashboard"; +import DashboardNav from "./components/DashboardNav"; +import CategoryDashboard from "./pages/CategoryDashboard"; +import { verifyIsAdmin } from "./api/admin"; +import TransactionDashboard from "./pages/TransactionDashboard"; function App() { // Authentication state - initialize from localStorage if available @@ -56,6 +63,26 @@ function App() { sendSessionDataToServer(); }, []); + const [isAdmin, setIsAdmin] = useState(false); + const [showAdminDashboard, setShowAdminDashboard] = useState(false); + + useEffect(() => { + const userInfo = sessionStorage.getItem("user") + ? JSON.parse(sessionStorage.getItem("user")) + : ""; + const id = userInfo?.ID; + verifyIsAdmin(id).then((data) => { + setIsAdmin(data.isAdmin); + }); + }, [user]); + + const handleShowAdminDashboard = () => { + setShowAdminDashboard(true); + }; + const handleCloseAdminDashboard = () => { + setShowAdminDashboard(false); + }; + // Send verification code const sendVerificationCode = async (userData) => { try { @@ -76,7 +103,7 @@ function App() { email: userData.email, // Add any other required fields }), - }, + } ); if (!response.ok) { @@ -125,7 +152,7 @@ function App() { email: tempUserData.email, code: code, }), - }, + } ); if (!response.ok) { @@ -169,7 +196,7 @@ function App() { "Content-Type": "application/json", }, body: JSON.stringify(userData), - }, + } ); if (!response.ok) { @@ -275,7 +302,7 @@ function App() { email: formValues.email, password: formValues.password, }), - }, + } ); if (!response.ok) { @@ -580,8 +607,8 @@ function App() { {isLoading ? "Please wait..." : isSignUp - ? "Create Account" - : "Sign In"} + ? "Create Account" + : "Sign In"} @@ -672,12 +699,40 @@ function App() { return children; }; + // If user is admin, show admin naviagtion + if (showAdminDashboard) { + return ( + +
+ + + {/* Admin routes */} + } /> + } /> + } /> + } /> + } + /> + } /> + +
+
+ ); + } + return (
{/* Only show navbar when authenticated */} {isAuthenticated && ( - + )} {/* Public routes */} diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js new file mode 100644 index 0000000..bea2a15 --- /dev/null +++ b/frontend/src/api/admin.js @@ -0,0 +1,120 @@ +import client from "./client"; + +export const getUsers = async (page, limit = 10) => { + try { + const { data } = await client.get( + `/user/getUserWithPagination?page=${page}&limit=${limit}` + ); + return { users: data.users, total: data.total }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const getProducts = async (page, limit = 10) => { + try { + const { data } = await client.get( + `/product/getProductWithPagination?limit=${limit}&page=${page}` + ); + + return { products: data.products, total: data.totalProd }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const getCategories = async (page, limit = 10) => { + try { + const { data } = await client.get( + `/category/getCategories?page=${page}&limit=${limit}` + ); + return { data: data.data, total: data.total }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const getTransactions = async (page, limit = 10) => { + try { + const { data } = await client.get( + `/transaction/getTransactions?limit=${limit}&page=${page}` + ); + return { transactions: data.data, total: data.total }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const addCategory = async (name) => { + try { + const { data } = await client.post(`/category/addCategory`, { name: name }); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeCategory = async (id) => { + try { + const { data } = await client.delete(`/category/${id}`); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeUser = async (id) => { + try { + const { data } = await client.post(`/user/delete`, { userId: id }); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeProduct = async (id) => { + try { + const { data } = await client.delete(`/product/${id}`); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const verifyIsAdmin = async (id) => { + try { + const { data } = await client.get(`/user/isAdmin/${id}`); + return { isAdmin: data.isAdmin }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeTransaction = async (id) => { + try { + const { data } = await client.delete(`/transaction/${id}`); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..bcb4831 --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,3 @@ +import axios from "axios"; +const client = axios.create({ baseURL: "http://localhost:3030/api" }); +export default client; diff --git a/frontend/src/components/CategoryForm.jsx b/frontend/src/components/CategoryForm.jsx new file mode 100644 index 0000000..08d7e86 --- /dev/null +++ b/frontend/src/components/CategoryForm.jsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { MdAddBox } from "react-icons/md"; +import { addCategory } from "../api/admin"; + +export default function CategoryForm({ visible, onAddCategory }) { + const [category, setCategory] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + if (!category.trim()) { + document.getElementById("noti").innerHTML = "Category name is missing!"; + document + .getElementById("noti") + .classList.add("bg-red-200", "text-red-500"); + document.getElementById("noti").classList.remove("opacity-0"); + return; + } + addCategory(category) + .then((message) => { + document + .getElementById("noti") + .classList.remove("opacity-0", "bg-red-200", "text-red-500"); + document + .getElementById("noti") + .classList.add("bg-green-200", "text-green-800"); + document.getElementById("noti").innerHTML = `${message.message}`; + setCategory(""); + onAddCategory(); + }) + .catch((err) => { + console.log(err); + }); + }; + + const handleChange = ({ target }) => { + setCategory(target.value); + if (target.value.trim()) + document.getElementById("noti").classList.add("opacity-0"); + }; + + if (!visible) return; + + return ( +
+ + + +

+
+ ); +} diff --git a/frontend/src/components/DashboardNav.jsx b/frontend/src/components/DashboardNav.jsx new file mode 100644 index 0000000..5f19de2 --- /dev/null +++ b/frontend/src/components/DashboardNav.jsx @@ -0,0 +1,98 @@ +import { Link, NavLink } from "react-router-dom"; +import { FaUserTag } from "react-icons/fa"; +import { FaBoxArchive } from "react-icons/fa6"; +import { MdOutlineCategory } from "react-icons/md"; +import { FaArrowLeft } from "react-icons/fa"; +import { FaMoneyBillTransfer } from "react-icons/fa6"; + +export default function DashboardNav({ handleCloseAdminDashboard }) { + const handleClick = () => { + handleCloseAdminDashboard(); + }; + + return ( +
+
+
    +
  • +
    + + Campus Plug + + Campus Plug + + +
    +
  • +
  • + + (isActive + ? " text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5 " + } + > + + Users + +
  • +
  • + + (isActive + ? "text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5" + } + > + + Products + +
  • +
  • + + (isActive + ? "text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5" + } + > + + Categories + +
  • +
  • + + (isActive + ? "text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5" + } + > + + Transaction + +
  • +
+
+ + Go back to user page +
+
+
+ ); +} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index aeceb2c..1c40ad8 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -3,7 +3,7 @@ import { Link, useNavigate } from "react-router-dom"; import UserDropdown from "./UserDropdown"; import { Search, Heart } from "lucide-react"; -const Navbar = ({ onLogout, userName }) => { +const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => { const [searchQuery, setSearchQuery] = useState(""); const navigate = useNavigate(); @@ -76,7 +76,12 @@ const Navbar = ({ onLogout, userName }) => { {/* User Profile */} - +
diff --git a/frontend/src/components/Pagination.jsx b/frontend/src/components/Pagination.jsx new file mode 100644 index 0000000..ca14f76 --- /dev/null +++ b/frontend/src/components/Pagination.jsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import { NavLink } from "react-router-dom"; + +export default function Pagination({ pageNum, onChange }) { + const [currentPage, setCurrentPage] = useState(1); + + const pages = []; + for (let i = 1; i <= pageNum; i++) { + pages.push(i); + } + + const handleClick = (page) => { + setCurrentPage(page); + onChange(page); + }; + + const handleTogglePage = (type) => { + let current = currentPage; + if (type == "next") + current = current + 1 <= pageNum ? current + 1 : current; + else current = current - 1 >= 1 ? current - 1 : current; + setCurrentPage(current); + onChange(current); + }; + + return ( + <> + + + ); +} diff --git a/frontend/src/components/UserDropdown.jsx b/frontend/src/components/UserDropdown.jsx index e8eed00..cf3cb1f 100644 --- a/frontend/src/components/UserDropdown.jsx +++ b/frontend/src/components/UserDropdown.jsx @@ -1,8 +1,14 @@ import { useState, useRef, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react"; +import { RiAdminLine } from "react-icons/ri"; -const UserDropdown = ({ onLogout, userName }) => { +const UserDropdown = ({ + onLogout, + userName, + isAdmin, + handleShowAdminDashboard, +}) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const navigate = useNavigate(); @@ -89,6 +95,20 @@ const UserDropdown = ({ onLogout, userName }) => { Settings + {isAdmin ? ( + { + handleShowAdminDashboard(); + }} + > + + Admin + + ) : ( + <> + )} + + + {categories.length > 0 ? ( + <> + + + + + + + + + + {categories.map((category) => ( + + + + + + ))} + +
CategoryIDNameAction
{category.CategoryID}{category.Name} + { + handleRemove(category.CategoryID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ + + ) : ( +

+ No category exists! +

+ )} + + ); +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..dbcc762 --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,7 @@ +export default function Dashboard() { + return ( +
+ Welcome to admin dashboard +
+ ); +} diff --git a/frontend/src/pages/ProductDashboard.jsx b/frontend/src/pages/ProductDashboard.jsx new file mode 100644 index 0000000..35ce13e --- /dev/null +++ b/frontend/src/pages/ProductDashboard.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { getProducts, removeProduct } from "../api/admin"; +import { MdDelete } from "react-icons/md"; +import Pagination from "../components/Pagination"; + +export default function ProductDashboard() { + const [products, setProducts] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + + let pageLimit = 10; + + const onChangePage = (page, limit = 10) => { + setCurrentPage(page); + fetchProducts(page, limit); + }; + + const fetchProducts = (page = 1, limit = 10) => { + getProducts(page, limit).then(({ products, total }) => { + setTotal(total); + setProducts(products); + }); + }; + + const handleRemoveProduct = (id) => { + removeProduct(id) + .then((res) => { + fetchProducts(currentPage); + }) + .catch((err) => { + console.log(err); + }); + }; + + //Get user when initialize the component + useEffect(fetchProducts, []); + + return ( +
+

+ PRODUCTS +

+ {products.length > 0 ? ( + <> + + + + + + + + + + + + + {products.map((product) => ( + + + + + + + + + ))} + +
ProductIDNamePriceCategorySellerAction
{product.ProductID}{product.ProductName}{product.Price}{product.Category ? product.Category : "N/A"}{product.SellerName ? product.SellerName : "N/A"} + { + handleRemoveProduct(product.ProductID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ + + ) : ( +

+ No product exists! +

+ )} +
+ ); +} diff --git a/frontend/src/pages/TransactionDashboard.jsx b/frontend/src/pages/TransactionDashboard.jsx new file mode 100644 index 0000000..167d5ee --- /dev/null +++ b/frontend/src/pages/TransactionDashboard.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { getTransactions, removeTransaction } from "../api/admin"; +import { MdDelete } from "react-icons/md"; +import Pagination from "../components/Pagination"; + +export default function TransactionDashboard() { + const [transactions, setTransactions] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + + let pageLimit = 10; + + const onChangePage = (page, limit = 10) => { + setCurrentPage(page); + fetchTransactions(page, limit); + }; + + const fetchTransactions = (page = 1, limit = 10) => { + getTransactions(page, limit).then(({ transactions, total }) => { + setTotal(total); + setTransactions(transactions); + }); + }; + + const handleRemoveTransaction = (id) => { + removeTransaction(id) + .then(() => { + fetchTransactions(currentPage); + }) + .catch((err) => { + console.log(err); + }); + }; + + //Get user when initialize the component + useEffect(fetchTransactions, []); + + return ( +
+

+ TRANSACTIONS +

+ {transactions.length > 0 ? ( + <> + + + + + + + + + + + + + {transactions.map((t) => ( + + + + + + + + + ))} + +
TransactionIDUserProductDateStatusAction
{t.TransactionID}{t.UserName ? t.UserName : "N/A"}{t.ProductName ? t.ProductName : "N/A"}{t.Date}{t.PaymentStatus} + { + handleRemoveTransaction(t.TransactionID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ + + ) : ( +

+ No transaction exists! +

+ )} +
+ ); +} diff --git a/frontend/src/pages/UserDashboard.jsx b/frontend/src/pages/UserDashboard.jsx new file mode 100644 index 0000000..9cab96d --- /dev/null +++ b/frontend/src/pages/UserDashboard.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { getUsers, removeUser } from "../api/admin"; +import { MdDelete } from "react-icons/md"; +import Pagination from "../components/Pagination"; + +export default function UserDashboard() { + const [users, setUsers] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + + let pageLimit = 10; + + const onChangePage = (page, limit = 10) => { + setCurrentPage(page); + fetchUsers(page, limit); + }; + + const fetchUsers = (page = 1, limit = 10) => { + getUsers(page, limit).then(({ users, total }) => { + setUsers(users); + setTotal(total); + }); + }; + + const handleRemoveUser = (id) => { + removeUser(id) + .then((res) => { + fetchUsers(currentPage); + }) + .catch((err) => { + console.log(err); + }); + }; + + //Get user when initialize the component + useEffect(fetchUsers, []); + + return ( +
+

+ USERS +

+ {users.length > 0 ? ( + <> + {" "} + + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + + ))} + +
UserIDUCIDNameEmailPhoneAddressAction
{user.UserID}{user.UCID}{user.Name}{user.Email}{user.Phone}{user.Address} + { + handleRemoveUser(user.UserID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ + + ) : ( +

+ No user exists! +

+ )} +
+ ); +} diff --git a/mysql-code/Init-Data.sql b/mysql-code/Init-Data.sql index 638e888..89747ab 100644 --- a/mysql-code/Init-Data.sql +++ b/mysql-code/Init-Data.sql @@ -67,40 +67,42 @@ VALUES (1, TRUE, TRUE), (2, TRUE, FALSE); +-- Insert Categories -- Insert Categories INSERT INTO - Category (CategoryID, Name) + Category (Name) VALUES - (1, 'Textbooks'), - (2, 'Electronics'), - (3, 'Furniture'), - (4, 'Clothing'), - (5, 'Sports Equipment'), - (6, 'Musical Instruments'), - (7, 'Art Supplies'), - (8, 'Kitchen Appliances'), - (9, 'Gaming'), - (10, 'Bicycles'), - (11, 'Computer Accessories'), - (12, 'Stationery'), - (13, 'Fitness Equipment'), - (14, 'Winter Sports'), - (15, 'Lab Equipment'), - (16, 'Camping Gear'), - (17, 'School Supplies'), - (18, 'Office Furniture'), - (19, 'Books (Non-textbook)'), - (20, 'Math & Science Resources'), - (21, 'Engineering Tools'), - (22, 'Backpacks & Bags'), - (23, 'Audio Equipment'), - (24, 'Dorm Essentials'), - (25, 'Smartphones & Tablets'), - (26, 'Winter Clothing'), - (27, 'Photography Equipment'), - (28, 'Event Tickets'), - (29, 'Software Licenses'), - (30, 'Transportation (Car Pool)'); + ('Textbooks'), + ('Electronics'), + ('Furniture'), + ('Clothing'), + ('Sports Equipment'), + ('Musical Instruments'), + ('Art Supplies'), + ('Kitchen Appliances'), + ('Gaming'), + ( 'Bicycles'), + ( 'Computer Accessories'), + ( 'Stationery'), + ( 'Fitness Equipment'), + ( 'Winter Sports'), + ( 'Lab Equipment'), + ( 'Camping Gear'), + ( 'School Supplies'), + ( 'Office Furniture'), + ( 'Books (-textbook)'), + ( 'Math & Science Resources'), + ( 'Engineering Tools'), + ( 'Backpacks & Bags'), + ( 'Audio Equipment'), + ( 'Dorm Essentials'), + ( 'Smartphones & Tablets'), + ( 'Winter Clothing'), + ( 'Photography Equipment'), + ( 'Event Tickets'), + ( 'Software Licenses'), + ( 'Transportation (Car Pool)'); + -- Insert Products INSERT INTO diff --git a/mysql-code/Schema.sql b/mysql-code/Schema.sql index cadb8b5..508dc7d 100644 --- a/mysql-code/Schema.sql +++ b/mysql-code/Schema.sql @@ -24,7 +24,7 @@ CREATE TABLE UserRole ( -- Category Entity (must be created before Product or else error) CREATE TABLE Category ( - CategoryID INT PRIMARY KEY, + CategoryID INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255) NOT NULL ); @@ -38,15 +38,15 @@ CREATE TABLE Product ( Description TEXT, CategoryID INT NOT NULL, Date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, + FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL ); -- Fixed Image_URL table CREATE TABLE Image_URL ( URL VARCHAR(255), ProductID INT, - FOREIGN KEY (ProductID) REFERENCES Product (ProductID) + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE ); -- Fixed Review Entity (Many-to-One with User, Many-to-One with Product) @@ -60,8 +60,8 @@ CREATE TABLE Review ( AND Rating <= 5 ), Date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (ProductID) REFERENCES Product (ProductID) + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE ); -- Transaction Entity (Many-to-One with User, Many-to-One with Product) @@ -71,8 +71,8 @@ CREATE TABLE Transaction ( ProductID INT, Date DATETIME DEFAULT CURRENT_TIMESTAMP, PaymentStatus VARCHAR(50), - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (ProductID) REFERENCES Product (ProductID) + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL ); -- Recommendation Entity (Many-to-One with User, Many-to-One with Product) @@ -81,8 +81,8 @@ CREATE TABLE Recommendation ( UserID INT, RecommendedProductID INT, Date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE ); -- History Entity (Many-to-One with User, Many-to-One with Product) @@ -91,8 +91,8 @@ CREATE TABLE History ( UserID INT, ProductID INT, Date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (ProductID) REFERENCES Product (ProductID) + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE ); -- Favorites Entity (Many-to-One with User, Many-to-One with Product) @@ -100,8 +100,8 @@ CREATE TABLE Favorites ( FavoriteID INT AUTO_INCREMENT PRIMARY KEY, UserID INT, ProductID INT, - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (ProductID) REFERENCES Product (ProductID), + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE, UNIQUE (UserID, ProductID) ); @@ -110,8 +110,8 @@ CREATE TABLE Product_Category ( ProductID INT, CategoryID INT, PRIMARY KEY (ProductID, CategoryID), - FOREIGN KEY (ProductID) REFERENCES Product (ProductID), - FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE, + FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE ); -- Login Authentication table