diff --git a/frontend/src/App.js b/frontend/src/App.js index cadef20..006f899 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -7,7 +7,8 @@ import Home from "./pages/Home"; import Login from "./pages/Login"; import Register from "./pages/Register"; import NewTicket from "./pages/NewTicket"; -import Tickets from "./pages/Ticket"; +import Tickets from "./pages/Tickets"; +import Ticket from "./pages/Ticket"; function App() { return ( @@ -25,6 +26,9 @@ function App() { }> } /> + }> + } /> + diff --git a/frontend/src/components/TicketItem.jsx b/frontend/src/components/TicketItem.jsx new file mode 100644 index 0000000..4bdf932 --- /dev/null +++ b/frontend/src/components/TicketItem.jsx @@ -0,0 +1,15 @@ +import { Link } from "react-router-dom"; + +function TicketItem({ ticket }) { + return ( +
+
{new Date(ticket.createdAt).toLocaleString("en-US")}
+
{ticket.product}
+
{ticket.status}
+ + View + +
+ ); +} +export default TicketItem; diff --git a/frontend/src/features/tickets/ticketService.js b/frontend/src/features/tickets/ticketService.js index 3df23cc..8063f4d 100644 --- a/frontend/src/features/tickets/ticketService.js +++ b/frontend/src/features/tickets/ticketService.js @@ -19,12 +19,39 @@ const getTickets = async (token) => { Authorization: `Bearer ${token}`, }, }; - const response = await axios.get(API_URL, config); return response.data; }; -const ticketService = { createTicket, getTickets }; +const closeTicket = async (ticketId, token) => { + const config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const response = await axios.put( + API_URL + ticketId, + { status: "closed" }, + config + ); + + return response.data; +}; + +const getTicket = async (ticketId, token) => { + const config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const response = await axios.get(API_URL + ticketId, config); + + return response.data; +}; + +const ticketService = { getTicket, createTicket, getTickets, closeTicket }; export default ticketService; diff --git a/frontend/src/features/tickets/ticketSlice.js b/frontend/src/features/tickets/ticketSlice.js index 6fb2b2c..a3affd6 100644 --- a/frontend/src/features/tickets/ticketSlice.js +++ b/frontend/src/features/tickets/ticketSlice.js @@ -42,10 +42,49 @@ export const ticketSlice = createSlice({ state.isLoading = false; state.isError = true; state.message = action.payload; + }) + .addCase(getTicket.pending, (state) => { + state.isLoading = true; + }) + .addCase(getTicket.fulfilled, (state, action) => { + state.isLoading = false; + state.isSuccess = true; + state.ticket = action.payload; + }) + .addCase(getTicket.rejected, (state, action) => { + state.isLoading = false; + state.isError = true; + state.message = action.payload; + }) + .addCase(closeTicket.fulfilled, (state, action) => { + state.isLoading = false; + state.tickets.map((ticket) => + ticket._id === action.payload._id + ? (ticket.status = "closed") + : ticket + ); }); }, }); +export const closeTicket = createAsyncThunk( + "tickets/close", + async (ticketId, thunkAPI) => { + try { + const token = thunkAPI.getState().auth.user.token; + return await ticketService.closeTicket(ticketId, token); + } catch (error) { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + + return thunkAPI.rejectWithValue(message); + } + } +); export const getTickets = createAsyncThunk( "tickets/getAll", async (_, thunkAPI) => { @@ -69,7 +108,25 @@ export const createTicket = createAsyncThunk( async (ticketData, thunkAPI) => { try { const token = thunkAPI.getState().auth.user.token; - return await ticketService(ticketData, token); + return await ticketService.createTicket(ticketData, token); + } catch (error) { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + + return thunkAPI.rejectWithValue(message); + } + } +); +export const getTicket = createAsyncThunk( + "tickets/get", + async (ticketId, thunkAPI) => { + try { + const token = thunkAPI.getState().auth.user.token; + return await ticketService.getTicket(ticketId, token); } catch (error) { const message = (error.response && diff --git a/frontend/src/pages/NewTicket.jsx b/frontend/src/pages/NewTicket.jsx index 1cc5613..3e25d5c 100644 --- a/frontend/src/pages/NewTicket.jsx +++ b/frontend/src/pages/NewTicket.jsx @@ -9,7 +9,7 @@ import { BackButton } from "../components/BackButton"; function NewTicket() { const { user } = useSelector((state) => state.auth); const { isLoading, isError, isSuccess, message } = useSelector( - (state) => state.ticket + (state) => state.tickets ); const [name, setName] = useState(user.name); @@ -20,26 +20,30 @@ function NewTicket() { const dispatch = useDispatch(); const navigate = useNavigate(); - useEffect(()=>{ - if(isError) toast.error(message) + useEffect(() => { + if (isError) toast.error(message); - if(isSuccess){ - dispatch(reset()) - navigate('/tickets') + if (isSuccess) { + dispatch(reset()); + navigate("/tickets"); } - dispatch(reset()) - },[dispatch,isError,isSuccess,navigate,message]) + dispatch(reset()); + }, [dispatch, isError, isSuccess, navigate, message]); const onSubmit = (e) => { e.preventDefault(); - dispatch(createTicket({product,description})) + if (product === "") { + dispatch(createTicket({ product:'iPhone', description })); + } else { + dispatch(createTicket({ product, description })); + } }; - if(isLoading) return + if (isLoading) return ; return ( <> - +

Create New Ticket

Please fill out the form below

diff --git a/frontend/src/pages/Ticket.jsx b/frontend/src/pages/Ticket.jsx index f8d9091..44e16c0 100644 --- a/frontend/src/pages/Ticket.jsx +++ b/frontend/src/pages/Ticket.jsx @@ -1,28 +1,56 @@ import { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { getTickets, reset } from "../features/tickets/ticketSlice"; +import { closeTicket, getTicket, reset } from "../features/tickets/ticketSlice"; +import { useNavigate, useParams } from "react-router-dom"; import Spinner from "../components/Spinner"; import { BackButton } from "../components/BackButton"; +import { toast } from "react-toastify"; -function Tickets() { - const { tickets, isLoading, isSuccess } = useSelector( +function Ticket() { + const { ticket, isLoading, isSuccess, isError, message } = useSelector( (state) => state.tickets ); + const { ticketId } = useParams(); const dispatch = useDispatch(); - useEffect(() => { - return () => { - if (isSuccess) { - dispatch(reset()); - } - }; - }, [dispatch, isSuccess]); - useEffect(() => { - dispatch(getTickets()); - }, [dispatch]); + const navigate = useNavigate(); - if (isLoading) { - return ; + const onTicketClose =()=>{ + dispatch(closeTicket(ticketId)) + toast.success('Ticket closed') + navigate('/tickets') } - return

Head

; + useEffect(() => { + if (isError) toast.error(message); + + dispatch(getTicket(ticketId)); + }, [isError, message, ticketId]); + if (isLoading) return ; + if (isError) return

Something went wrong

; + + return ( +
+
+ +

+ Ticket ID: {ticket._id} + + {ticket.status} + +

+

+ Date Submitted: {new Date(ticket.createdAt).toLocaleString("en-US")} +

+

Product: {ticket.product}

+
+
+

Description of Issue

+

{ticket.description}

+
+
+ {ticket.status !== "closed" && ( + + )} +
+ ); } -export default Tickets; +export default Ticket; diff --git a/frontend/src/pages/Tickets.jsx b/frontend/src/pages/Tickets.jsx new file mode 100644 index 0000000..53fd561 --- /dev/null +++ b/frontend/src/pages/Tickets.jsx @@ -0,0 +1,43 @@ +import { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { getTickets, reset } from "../features/tickets/ticketSlice"; +import Spinner from "../components/Spinner"; +import { BackButton } from "../components/BackButton"; +import TicketItem from "../components/TicketItem"; + +function Tickets() { + const { tickets, isLoading, isSuccess } = useSelector( + (state) => state.tickets + ); + const dispatch = useDispatch(); + useEffect(() => { + return () => { + if (isSuccess) { + dispatch(reset()); + } + }; + }, [dispatch, isSuccess]); + useEffect(() => { + dispatch(getTickets()); + }, [dispatch]); + + if (isLoading) { + return ; + } + return <> + +

Tickets

+
+
+
Date
+
Product
+
Status
+
+
+ {tickets.map((ticket)=>( + + ))} +
+ +} +export default Tickets;