diff --git a/src/backend/app.js b/src/backend/app.js index 2a9f631..dcc554f 100644 --- a/src/backend/app.js +++ b/src/backend/app.js @@ -8,7 +8,7 @@ const mongoose = require('mongoose'); require('dotenv').config(); const routes = require('./routes'); const passport = require('./config/passport'); - +const cache = require('express-cache-headers'); const cors = require('cors'); const app = express(); @@ -22,7 +22,7 @@ const app = express(); // } // Initializing middlewares - +app.use(cache(30)); app.use(session({ secret:process.env.SESSION_SECRET, resave: false, diff --git a/src/backend/controllers/issueController.js b/src/backend/controllers/issueController.js index 85aef5d..48ff798 100644 --- a/src/backend/controllers/issueController.js +++ b/src/backend/controllers/issueController.js @@ -1,127 +1,177 @@ -const Issue = require( '../models/issueModel'); +const Issue = require("../models/issueModel"); const { authenticate } = require("../config/passport"); exports.issue_list = [ - async (req,res)=>{ - try{ - const issues = await Issue.find({}).populate({ - path: 'replies', - populate: { - path: 'sender', - model: 'User' - } - }).populate('sender').exec(); - res.status(200).json(issues||[]); - }catch(error){ - res.status(500).json({ error: 'Internal Server Error' }); - } + authenticate, + async (req, res) => { + try { + const issues = await Issue.find({}) + .populate({ + path: "replies", + populate: { + path: "sender", + model: "User", + }, + }) + .populate("sender") + .exec(); + res.status(200).json(issues || []); + } catch (error) { + res.status(500).json({ error: "Internal Server Error" }); } + }, ]; -exports.issue_detail = async (req,res)=>{ - try{ - const id = req.params.issueId; - const issue = await Issue.findById(id).populate({ - path: 'replies', - populate: { - path: 'sender', - model: 'User' - } - }).populate('sender').exec(); - if(issue !== null) - return res.status(200).json(issue); - else - return res.status(404).json({error:"Not found"}); - - }catch(error){ - res.status(500).json({ error: 'Internal Server Error' }); +exports.issue_detail = [ + authenticate, + async (req, res) => { + try { + const id = req.params.issueId; + const issue = await Issue.findById(id) + .populate({ + path: "replies", + populate: { + path: "sender", + model: "User", + }, + }) + .populate("sender") + .exec(); + if (issue !== null) return res.status(200).json(issue); + else return res.status(404).json({ error: "Not found" }); + } catch (error) { + res.status(500).json({ error: "Internal Server Error" }); } -} - -exports.issue_list_user = async (req,res)=>{ - try{ - const id = req.params.userId; - const userIssues = await Issue.find({sender:id}).populate({ - path: 'replies', - populate: { - path: 'sender', - model: 'User' - } - }).populate('sender').exec(); - return res.status(200).json(userIssues||[]); - + }, +]; - }catch(error){ - res.status(500).json({ error: 'Internal Server Error' }); +exports.issue_list_user = [ + authenticate, + async (req, res) => { + try { + const id = req.params.userId; + const userIssues = await Issue.find({ sender: id }) + .populate({ + path: "replies", + populate: { + path: "sender", + model: "User", + }, + }) + .populate("sender") + .exec(); + return res.status(200).json(userIssues || []); + } catch (error) { + res.status(500).json({ error: "Internal Server Error" }); } -} + }, +]; exports.issue_create = [ - async (req,res)=>{ - try{ - const { - sender,subject,description - } = req.body; - if(!sender||!subject||!description){ - return res.status(400).json({error:"Some of the required fields are empty"}); - } - const issue= new Issue({ - seen:false, - sender:sender, - description:description, - status:"open", - subject:subject, - replies:[] - }); - issue.save(); - res.status(200).json(issue); - - }catch(error){ - res.status(500).json({ error: 'Internal Server Error' }); - } + authenticate, + async (req, res) => { + try { + const { sender, subject, description } = req.body; + if (!sender || !subject || !description) { + return res + .status(400) + .json({ error: "Some of the required fields are empty" }); + } + const issue = new Issue({ + seen: false, + sender: sender, + description: description, + status: "open", + subject: subject, + replies: [], + }); + issue.save(); + res.status(200).json(issue); + } catch (error) { + res.status(500).json({ error: "Internal Server Error" }); } -] + }, +]; exports.issue_delete = [ - authenticate, - async (req, res) => { - try{ - const issueId = req.params.issueId; - const deletedIssue = await Issue.findByIdAndDelete(issueId); - - if(!deletedIssue) - res.status(404).json({error:"Issue doesn't exist"}); - else - return res.status(200).json({message:("successfully deleted issue " +issueId)}) - } catch(error){ - res.status(500).json({ error: 'Internal Server Error' }); + authenticate, + async (req, res) => { + try { + const issueId = req.params.issueId; + const deletedIssue = await Issue.findByIdAndDelete(issueId); - } + if (!deletedIssue) res.status(404).json({ error: "Issue doesn't exist" }); + else + return res + .status(200) + .json({ message: "successfully deleted issue " + issueId }); + } catch (error) { + res.status(500).json({ error: "Internal Server Error" }); } - ]; + }, +]; +exports.issue_reply = [ + authenticate, + async (req, res) => { + try { + const issueId = req.params.issueId; + const { sender, body } = req.body; + if (!sender || !body) + return res.status(400).json({ error: "Some fields are not specified" }); + let issue = await Issue.findById(issueId); + - exports.issue_reply = async (req, res) => { - try { - const issueId = req.params.issueId; - const { sender, body } = req.body; - - if (!sender || !body) - return res.status(400).json({ error: "Some fields are not specified" }); - - const issue = await Issue.findById(issueId); - - if (!issue) - return res.status(404).json({ error: "Issue not found" }); - - issue.replies.push({ sender, body }); - await issue.save(); // Save the updated issue - - return res.status(200).json(issue); - } catch (error) { - console.error('Error adding reply:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } - }; - \ No newline at end of file + if (!issue) return res.status(404).json({ error: "Issue not found" }); + issue.seen = false; + issue.replies.push({ sender, body }); + await issue.save(); // Save the updated issue + issue = await Issue.findById(issueId) + .populate({ + path: "replies", + populate: { + path: "sender", + model: "User", + }, + }) + .populate("sender") + .exec(); + + return res.status(200).json(issue); + } catch (error) { + console.error("Error adding reply:", error); + return res.status(500).json({ error: "Internal Server Error" }); + } + }, +]; + +exports.issue_seen = [ + authenticate, + async (req, res) => { + try { + const {userId}=req.body; + const issueId = req.params.issueId; + const issue = await Issue.findById(issueId) + .populate({ + path: "replies", + populate: { + path: "sender", + model: "User", + }, + }) + .populate("sender") + .exec(); + if (!issue) return res.status(404).json({ error: "Issue not found" }); + issue.replies.forEach((reply) => { + if(reply.sender._id!==userId) reply.seen = true; + }); + issue.seen=true; + await issue.save(); + res.status(200).json(issue); + } catch (error) { + console.error("Error marking replies as seen:", error); + return res.status(500).json({ error: "Internal Server Error" }); + } + }, +]; diff --git a/src/backend/package-lock.json b/src/backend/package-lock.json index ad7dc53..65c30b9 100644 --- a/src/backend/package-lock.json +++ b/src/backend/package-lock.json @@ -16,6 +16,7 @@ "dotenv": "^16.4.5", "express": "~4.16.1", "express-async-handler": "^1.2.0", + "express-cache-headers": "^0.1.4", "express-validator": "^7.0.1", "jest": "^29.7.0", "jsonwebtoken": "^9.0.2", @@ -2063,6 +2064,11 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" }, + "node_modules/express-cache-headers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/express-cache-headers/-/express-cache-headers-0.1.4.tgz", + "integrity": "sha512-dBYkl2mtb9slDlBIdCL2r8ORSUlhLs/o47498eCwx+s/z0fSfTdHWWo1Y1AyMDSzaw5LlYoGgggRYbT4GLdVPw==" + }, "node_modules/express-validator": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", diff --git a/src/backend/package.json b/src/backend/package.json index f114c9e..157fdb4 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -17,6 +17,7 @@ "dotenv": "^16.4.5", "express": "~4.16.1", "express-async-handler": "^1.2.0", + "express-cache-headers": "^0.1.4", "express-validator": "^7.0.1", "jest": "^29.7.0", "jsonwebtoken": "^9.0.2", diff --git a/src/backend/routes/issueRoute.js b/src/backend/routes/issueRoute.js index 25af68e..f126e33 100644 --- a/src/backend/routes/issueRoute.js +++ b/src/backend/routes/issueRoute.js @@ -9,4 +9,5 @@ router.get('/user/:userId',issue_controller.issue_list_user); router.post('/',issue_controller.issue_create); router.delete('/:issueId',issue_controller.issue_delete); router.put('/reply/:issueId',issue_controller.issue_reply); +router.put('/seen/:issueId',issue_controller.issue_seen); module.exports = router; diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 7129763..20680a9 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -33,6 +33,7 @@ "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.18", + "daisyui": "^4.10.1", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", @@ -2444,6 +2445,16 @@ "node": ">= 8" } }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2460,6 +2471,34 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.10.1.tgz", + "integrity": "sha512-Ds0Z0Fv+Xf6ZEqV4Q5JIOeKfg83xxnww0Lzid0V94vPtlQ0yYmucEa33zSctsX2VEgBALtmk5zVEqd59pnUbuQ==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/date-fns": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", @@ -3131,6 +3170,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index bbdd92f..22d7eda 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -35,6 +35,7 @@ "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.18", + "daisyui": "^4.10.1", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", diff --git a/src/frontend/src/Pages/AdminIssue.jsx b/src/frontend/src/Pages/AdminIssue.jsx new file mode 100644 index 0000000..cb839d5 --- /dev/null +++ b/src/frontend/src/Pages/AdminIssue.jsx @@ -0,0 +1,5 @@ +import CustomerIssue from "./Issue"; + +export default function AdminIssue() { + return ; +} diff --git a/src/frontend/src/Pages/CustomerIssues.jsx b/src/frontend/src/Pages/CustomerIssues.jsx new file mode 100644 index 0000000..8594cf1 --- /dev/null +++ b/src/frontend/src/Pages/CustomerIssues.jsx @@ -0,0 +1,39 @@ +import { useState, useEffect, useContext } from 'react'; +import fetchData from '../utilities/fetchData'; +import {Link} from 'react-router-dom'; +import { UserContext } from './Root'; +import Issues from './Issues'; + +export default function CustomerIssues() { + const {user} = useContext(UserContext); + const [error, setError] = useState(false); + const [issues, setIssues] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + async function fetchCustomerIssues() { + setLoading(true); + const result = await fetchData(`http://localhost:3000/api/issues/user/${user.id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${localStorage.getItem("token")}`, + } + }); + if (result.data) { + setLoading(false); + setIssues(result.data); + } else if (result.error) { + setError(true); + setLoading(false); + } + } + fetchCustomerIssues(); + }, [user.id]); + + return ( +
+ +
+ ); +} diff --git a/src/frontend/src/Pages/Dashboard.jsx b/src/frontend/src/Pages/Dashboard.jsx index f799b7e..5e69720 100644 --- a/src/frontend/src/Pages/Dashboard.jsx +++ b/src/frontend/src/Pages/Dashboard.jsx @@ -1,15 +1,22 @@ -import { useContext } from "react"; +import { useContext,useEffect,useState } from "react"; import { Navigate, Outlet, Link } from "react-router-dom"; import { UserContext } from "./Root"; - +import notifications from "../utilities/notifications"; export default function AdminVerification() { const { user } = useContext(UserContext); - + const [notification,setNotification]=useState(false); // Redirect to homepage if user is not an admin + + useEffect(()=>{ + async function notificationFetch(){ + const notify = await notifications(user.id); + setNotification(notify); + } + notificationFetch(); + }) if (!user || user.role !== "admin") { return ; } - return (
@@ -27,7 +34,7 @@ export default function AdminVerification() { Account */} -
  • +
  • - Issues + Issues{" "} + {(notification) && ( +
    + )}
  • diff --git a/src/frontend/src/Pages/Issue.jsx b/src/frontend/src/Pages/Issue.jsx index b2ab61e..45378d1 100644 --- a/src/frontend/src/Pages/Issue.jsx +++ b/src/frontend/src/Pages/Issue.jsx @@ -1,11 +1,11 @@ -import { useParams } from "react-router-dom"; +import { useParams, Link } from "react-router-dom"; import { useState, useEffect, useContext, useRef } from "react"; import fetchData from "../utilities/fetchData"; import { UserContext } from "./Root"; import Button from "../components/generalPurpose/Button"; -export default function Issue() { +export default function Issue({ admin = false }) { const scrollRef = useRef(); - const {user} = useContext(UserContext); + const { user } = useContext(UserContext); const { issueId } = useParams(); const [error, setError] = useState(false); const [loading, setLoading] = useState(false); @@ -14,9 +14,12 @@ export default function Issue() { const [deleteBtn, setDeleteBtn] = useState(false); const [deleting, setDeleting] = useState(null); const [textAreaValue, setTextAreaValue] = useState(""); - const [replySucceeded,setReplySucceeded]=useState(true); - const [isTextAreaValid,setIsTextAreaValid]=useState(true); + const [replySucceeded, setReplySucceeded] = useState(true); + const [isTextAreaValid, setIsTextAreaValid] = useState(true); + useEffect(() => { + let fetchTimeout; + async function fetchIssue() { setLoading(true); const issueData = await fetchData( @@ -33,18 +36,38 @@ export default function Issue() { setIssue(issueData.data); setLoading(false); setSuccess(true); + fetchTimeout = setTimeout(async () => { + const seenData = await fetchData( + `http://localhost:3000/api/issues/seen/${issueId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify({ userId: user.id }), // Corrected JSON.stringify + } + ); + if (seenData.data) setIssue(seenData.data); + }, 2000); } else if (issueData.error) { setLoading(false); setError(true); } } + fetchIssue(); - }, [issueId]); - useEffect(()=>{ - if(scrollRef.current){ + + return () => { + clearTimeout(fetchTimeout); // Cleanup function to clear the timeout + }; + }, [issueId, user.id]); + + useEffect(() => { + if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - },[issue]); + }, [issue]); const handleClickDeleteIssue = async () => { const response = await fetchData( `http://localhost:3000/api/issues/${issueId}`, @@ -58,18 +81,17 @@ export default function Issue() { ); setDeleting(response); }; - - const handleReply =async (e) =>{ + + const handleReply = async (e) => { e.preventDefault(); - if(textAreaValue==="")setIsTextAreaValid(false); + if (textAreaValue === "") setIsTextAreaValid(false); else setIsTextAreaValid(true); const reply = { - sender:user.id, - body:textAreaValue - } - if(isTextAreaValid){ - - const issueData = await fetchData( + sender: user.id, + body: textAreaValue, + }; + if (isTextAreaValid) { + const issueData = await fetchData( `http://localhost:3000/api/issues/reply/${issueId}`, { method: "PUT", @@ -77,23 +99,24 @@ export default function Issue() { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("token")}`, }, - - body: JSON.stringify(reply) + + body: JSON.stringify(reply), } ); if (issueData.data) { - setReplySucceeded(true); - setIssue(issueData.data); + setReplySucceeded(true); + setIssue(issueData.data); + setTextAreaValue(""); + console.log(issue.sender); } else if (issueData.error) { setReplySucceeded(false); } } - - } + }; return (

    Issue Details

    - {success ? ( + {success && user ? ( !deleteBtn ? (
    @@ -102,16 +125,31 @@ export default function Issue() {
    - {issue.replies.map((reply) => ( -
    -

    {reply.body}

    -
    -

    {new Date(reply.createdAt).toLocaleString()}

    -

    Posted by: {reply.sender.username}

    -
    -
    - ))} -
    + {issue.replies.map((reply) => ( +
    +

    {reply.body}

    +
    +

    + {new Date(reply.createdAt).toLocaleString()} +

    +

    + {reply.sender.username} +

    +
    +
    + ))} +
    @@ -119,62 +157,78 @@ export default function Issue() { className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:border-blue-500" rows="3" placeholder="Type your reply here..." - onChange={(e)=>setTextAreaValue(e.target.value)} + onChange={(e) => setTextAreaValue(e.target.value)} + value={textAreaValue} required > - {!isTextAreaValid && ( -

    This field is required

    + {!isTextAreaValid && ( +

    This field is required

    )} - - {/*
    - ) : ( - deleting ? ( - deleting.data ? ( -
    - Successfully deleted issue {issueId} -
    - ) : deleting.loading ? ( -
    - Attempting to delete issue {issueId}... -
    - ) : ( -
    - Failed to delete issue {issueId} -
    - ) + ) : deleting ? ( + deleting.data ? ( +
    + Successfully deleted issue {issueId} +
    + ) : deleting.loading ? ( +
    + Attempting to delete issue {issueId}... +
    ) : ( -
    -

    - Are you sure you want to delete{" "} - issue {issueId}? -

    -
    - -
    +
    + Failed to delete issue {issueId}
    ) + ) : ( +
    +

    + Are you sure you want to delete{" "} + issue {issueId}? +

    +
    + +
    +
    ) ) : loading ? (

    Loading...

    ) : error ? ( -

    Error

    +

    + Error +

    ) : ( "" )} diff --git a/src/frontend/src/Pages/Issues.jsx b/src/frontend/src/Pages/Issues.jsx index bc052c5..3cba513 100644 --- a/src/frontend/src/Pages/Issues.jsx +++ b/src/frontend/src/Pages/Issues.jsx @@ -1,15 +1,25 @@ import { Link } from "react-router-dom"; import fetchData from "../utilities/fetchData"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useContext } from "react"; +import FmdBadIcon from "@mui/icons-material/FmdBad"; +import Button from "../components/generalPurpose/Button"; +import { UserContext } from "./Root"; -export default function Issues() { +export default function Issues({ admin = true }) { + const { user } = useContext(UserContext); const [error, setError] = useState(false); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); - const [Issues, setIssues] = useState([]); + const [issues, setIssues] = useState([]); + const [issueDialog, setIssueDialog] = useState(false); + const [formInputValid, setFormInputValid] = useState(true); + const [input, setInput] = useState(""); + const [textArea, setTextArea] = useState(""); + const [createdIssue, setCreatedIssue] = useState(false); + const [created, setCreated] = useState(false); // Convert created into a state variable useEffect(() => { - async function fetchUsers() { + async function fetchIssues() { setLoading(true); const response = await fetchData("http://localhost:3000/api/issues", { method: "GET", @@ -21,46 +31,166 @@ export default function Issues() { if (response.data) { setIssues(response.data); setLoading(false); + setFormInputValid(true); setSuccess(true); } else if (response.error) { setLoading(false); setError(true); + setFormInputValid(false); } - setLoading(false); } - fetchUsers(); - }, []); + fetchIssues(); + }, [created]); // Listen for changes in the 'created' state + + const handleOpenAnIssue = async (e) => { + e.preventDefault(); + if (input.trim() === "" || textArea.trim() === "" || user === null) { + setFormInputValid(false); + return; + } + + const issue = { + sender: user.id, + subject: input, + description: textArea, + }; + const response = await fetchData("http://localhost:3000/api/issues", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify(issue), + }); + if (response.data) { + setCreatedIssue(true); + setFormInputValid(true); + setTextArea(""); + setInput(""); + setCreated(!created); // Toggle the 'created' state to trigger re-render + setTimeout(() => setCreatedIssue(false), 1000); + } else if (response.error) { + setCreatedIssue(false); + } + }; return (
    -

    Options

    -
    -
    -
    Username
    -
    Subject
    -
    Date posted
    -
    +

    Issues

    +
    + {admin && ( +
    +
    Username
    +
    Subject
    +
    Date posted
    +
    + )} {success ? ( - Issues.map((issue, index) => ( - -
    {issue.sender.username}
    -
    {issue.subject}
    -
    {issue.createdAt.substring(0, 10)}
    - + issues.map((issue, index) => ( +
    + {admin ? ( + +
    +
    {issue.sender?.username}
    +
    {issue.subject}
    +
    + {issue.createdAt.substring(0, 10)} +
    +
    + {((!issue.seen && issue.replies[issue.replies.length-1].sender._id!=user.id) || issue.replies.length === 0) && ( + + )} + + ) : ( + <> + +
    +

    + {issue.subject} +

    +

    {issue.description}

    +
    + {!issue.seen && issue.replies[issue.replies.length-1].sender._id!=user.id && ( + + )} + + + )} +
    )) ) : (
    - {loading && !error ? "Loading..." : "Failed to load customers"} + {loading && !error ? "Loading..." : "Failed to load issues"}
    )}
    + {issueDialog && ( +
    + setInput(e.target.value)} + placeholder="Issue Subject" + className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:border-blue-500" + /> + + +
    + )} +{ + !admin &&
    ); } diff --git a/src/frontend/src/Router.jsx b/src/frontend/src/Router.jsx index ceb18f4..d362172 100644 --- a/src/frontend/src/Router.jsx +++ b/src/frontend/src/Router.jsx @@ -28,6 +28,8 @@ import CheckInPage from "./Pages/CheckInPage.jsx"; import AddBranch from "./Pages/AddBranch.jsx"; import CheckOutPage from "./Pages/CheckOutPage.jsx"; import Issues from "./Pages/Issues.jsx"; +import AdminIssue from "./Pages/AdminIssue.jsx"; +import CustomerIssues from "./Pages/CustomerIssues.jsx"; import Issue from "./Pages/Issue.jsx"; const Router = () => { @@ -52,6 +54,14 @@ const Router = () => { path: "signup", element: , }, + { + path: "issues", + element: , + }, + { + path: "issues/:issueId", + element: , + }, { path: "reservation/book/:vehicleId", element: , @@ -78,11 +88,11 @@ const Router = () => { }, { path: "reservation/checkin/:reservationId", - element: , + element: , }, { path: "reservation/checkout/:reservationId", - element: , + element: , }, { @@ -90,8 +100,8 @@ const Router = () => { element: , children: [ { - index:true, - element: + index: true, + element: , }, { path: "customers", @@ -131,20 +141,20 @@ const Router = () => { }, { path: "branches/add-branch", - element: + element: , }, { path: "reservations", - element: , + element: , }, { - path:"issues", - element: + path: "issues", + element: , }, { - path:"issues/:issueId", - element: - } + path: "issues/:issueId", + element: , + }, ], }, ], diff --git a/src/frontend/src/components/header/AccountDropdown.jsx b/src/frontend/src/components/header/AccountDropdown.jsx index fde1928..4e553f6 100644 --- a/src/frontend/src/components/header/AccountDropdown.jsx +++ b/src/frontend/src/components/header/AccountDropdown.jsx @@ -1,59 +1,115 @@ -import React from 'react'; +import React from "react"; import LoginButton from "./LoginButton"; import logout from "../../utilities/logout"; -import {useContext} from 'react'; -import { Link } from 'react-router-dom'; -import {UserContext} from "./../../Pages/Root"; +import { useContext, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { UserContext } from "./../../Pages/Root"; +import notifications from "../../utilities/notifications"; export default function AccountDropdown() { - - const {user,setToken,token} = useContext(UserContext); - - // const token = localStorage.getItem("token"); - const isLoggedIn = (token !== null); - if(isLoggedIn){ - return showAccountButton(setToken,user); - }else { - return showLoginButton(); + const [notification, setNotification] = useState(false); + useEffect(() => { + async function notificationFetch() { + const notify = await notifications(user.id); + setNotification(notify); } + notificationFetch(); + }); + const { user, setToken, token } = useContext(UserContext); + // const token = localStorage.getItem("token"); + const isLoggedIn = token !== null; + if (isLoggedIn) { + return showAccountButton(setToken, user, notification); + } else { + return showLoginButton(); + } } -function showAccountButton(setToken,user){ - const [isDropdownVisible, setDropdownVisible] = React.useState(false); - window.onclick = function(event) { - if (!event.target.matches('.relative')) { - setDropdownVisible(false); - } +function showAccountButton(setToken, user, notification) { + const [isDropdownVisible, setDropdownVisible] = React.useState(false); + window.onclick = function (event) { + if (!event.target.matches(".relative")) { + setDropdownVisible(false); } - return ( -
    - - {isDropdownVisible && ( -
    -

    My Account

    -

    window.open("/user/reservation", "_self")}*/>My Reservations

    - {(user && user.role === "admin") &&

    Admin Dashboard

    } - {(user && user.role === "representative") &&

    CSR Dashboard

    } - {(user && user.role === "customer") &&

    Report an Issue

    } -
    -

    logoutAccount(setToken)}>Logout

    - -
    - )} + }; + return ( +
    + + {isDropdownVisible && ( +
    +

    + My Account +

    + +

    window.open("/user/reservation", "_self")}*/ + > + My Reservations +

    + + {user && user.role === "admin" && ( + +

    + Admin Dashboard +

    + + )} + {user && user.role === "representative" && ( + +

    + CSR Dashboard +

    + + )} + {user && user.role === "customer" && ( + +

    Issues

    + {notification && ( + +
    +
    + )} + + )} +
    + +

    logoutAccount(setToken)} + > + Logout +

    +
    - ); + )} +
    + ); } function showLoginButton() { - return ( - - ); + return ; } // async function calling logout function -async function logoutAccount(setToken){ - await logout(setToken); +async function logoutAccount(setToken) { + await logout(setToken); } diff --git a/src/frontend/src/utilities/notifications.js b/src/frontend/src/utilities/notifications.js new file mode 100644 index 0000000..e2123d8 --- /dev/null +++ b/src/frontend/src/utilities/notifications.js @@ -0,0 +1,22 @@ +import fetchData from "./fetchData"; +export default async function notifications (id){ + const response = await fetchData("http://localhost:3000/api/issues", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + let counter = 0; + if(response.data){ + response.data.map(issue=>{ + if(issue.replies[issue.replies.length-1].sender._id!=id && !issue.replies[issue.replies.length-1].seen)counter++; + } + ) + + const result = counter>0?true:false; + console.log(result); + return result; + } + else {return false;} +} \ No newline at end of file