From 12b7caa101a878eb8b8582d8b178845d05799b7a Mon Sep 17 00:00:00 2001 From: yvanddniyo Date: Fri, 12 Jul 2024 17:53:30 +0200 Subject: [PATCH] ft(order): A buyer and seller should be manage orders - A buyer should see their orders and status - A seller should see their orders made on their products [Delivers #187900465] --- .github/workflows/deploy.yml | 2 +- package-lock.json | 26 +- package.json | 5 +- src/__test__/cartSlice.test.tsx | 82 ++++- src/__test__/deleteNotify.test.tsx | 64 ++++ src/__test__/orders.test.ts | 99 ++++++ src/components/cards/ProductCard.tsx | 2 +- src/components/common/auth/Loader.tsx | 2 +- src/components/common/header/Navbar.tsx | 3 + src/components/dashboard/SideBar.tsx | 9 +- .../dashboard/orders/SellerOrder.tsx | 303 ++++++++++++++++++ src/index.css | 4 + src/pages/BuyerOrders.tsx | 266 +++++++++++++++ src/pages/CartManagement.tsx | 1 + src/pages/ProductPage.tsx | 5 +- src/pages/SignupVerification.tsx | 23 ++ src/redux/api/api.ts | 1 - src/redux/reducers/ordersSlice.ts | 101 ++++++ src/redux/store.ts | 2 + src/routes/AppRoutes.tsx | 8 + type.d.ts | 46 +++ 21 files changed, 1029 insertions(+), 25 deletions(-) create mode 100644 src/__test__/deleteNotify.test.tsx create mode 100644 src/__test__/orders.test.ts create mode 100644 src/components/dashboard/orders/SellerOrder.tsx create mode 100644 src/pages/BuyerOrders.tsx create mode 100644 src/pages/SignupVerification.tsx create mode 100644 src/redux/reducers/ordersSlice.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c75d731..050f99a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,7 +26,7 @@ jobs: node-version: "20" - name: Install dependencies - run: npm install + run: npm install --force - name: Running test run: npm run test:coverage diff --git a/package-lock.json b/package-lock.json index 19fce6d..1bca9ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,6 @@ "@radix-ui/react-navigation-menu": "^1.1.4", "@reduxjs/toolkit": "^2.2.5", "@tanstack/react-query": "^5.45.1", - "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "axios": "^1.7.2", "axios-mock-adapter": "^1.22.0", @@ -32,6 +30,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^3.0.7", + "jwt-decode": "^4.0.0", "moment": "^2.30.1", "node-fetch": "^3.3.2", "npm": "^10.8.1", @@ -43,6 +42,7 @@ "react-icons": "^5.2.1", "react-jwt": "^1.2.1", "react-loading-skeleton": "^3.4.0", + "react-number-format": "^5.4.0", "react-redux": "^9.1.2", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", @@ -58,7 +58,6 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@testing-library/dom": "^10.2.0", - "@testing-library/jest-dom": "^6.4.6", "@types/jest": "^29.5.12", "@types/node": "^20.14.8", "@types/react": "^18.2.66", @@ -3856,6 +3855,7 @@ "version": "16.0.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -10171,6 +10171,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -14144,6 +14152,18 @@ "react": ">=16.8.0" } }, + "node_modules/react-number-format": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.0.tgz", + "integrity": "sha512-NWdICrqLhI7rAS8yUeLVd6Wr4cN7UjJ9IBTS0f/a9i7UB4x4Ti70kGnksBtZ7o4Z7YRbvCMMR/jQmkoOBa/4fg==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-redux": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", diff --git a/package.json b/package.json index cab36cc..33ff949 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,6 @@ "@radix-ui/react-navigation-menu": "^1.1.4", "@reduxjs/toolkit": "^2.2.5", "@tanstack/react-query": "^5.45.1", - "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "axios": "^1.7.2", "axios-mock-adapter": "^1.22.0", @@ -40,6 +38,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^3.0.7", + "jwt-decode": "^4.0.0", "moment": "^2.30.1", "node-fetch": "^3.3.2", "npm": "^10.8.1", @@ -51,6 +50,7 @@ "react-icons": "^5.2.1", "react-jwt": "^1.2.1", "react-loading-skeleton": "^3.4.0", + "react-number-format": "^5.4.0", "react-redux": "^9.1.2", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", @@ -66,7 +66,6 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@testing-library/dom": "^10.2.0", - "@testing-library/jest-dom": "^6.4.6", "@types/jest": "^29.5.12", "@types/node": "^20.14.8", "@types/react": "^18.2.66", diff --git a/src/__test__/cartSlice.test.tsx b/src/__test__/cartSlice.test.tsx index a275cc4..6d4a437 100644 --- a/src/__test__/cartSlice.test.tsx +++ b/src/__test__/cartSlice.test.tsx @@ -1,4 +1,3 @@ -// @ts-nochec import { configureStore } from "@reduxjs/toolkit"; import cartReducer, { @@ -49,6 +48,16 @@ describe("cartManageSlice", () => { expect(store.getState().cart.isLoading).toBeFalsy(); }); + it("should handle cartManage.rejected", async () => { + const errorMessage = "Network Error"; + (axios.get as jest.Mock).mockRejectedValue(new Error(errorMessage)); + + await store.dispatch(cartManage()); + + expect(store.getState().cart.error).toBeTruthy(); + expect(store.getState().cart.isLoading).toBeFalsy(); + }); + it("should handle addToCart.fulfilled", async () => { const mockItem = { id: "2", quantity: 1 }; (axios.post as jest.Mock).mockResolvedValue({ data: mockItem }); @@ -58,31 +67,86 @@ describe("cartManageSlice", () => { expect(store.getState().cart.data).toContainEqual(mockItem); }); + it("should handle addToCart.rejected", async () => { + const errorMessage = "Failed to add item to cart"; + (axios.post as jest.Mock).mockRejectedValue(new Error(errorMessage)); + + await store.dispatch(addToCart({ productId: 2, quantity: 1 })); + + expect(store.getState().cart.add.error).toEqual(errorMessage); + }); + it("should handle removeFromCart.fulfilled", async () => { (axios.put as jest.Mock).mockResolvedValue({ data: {} }); - // @ts-ignore - await store.dispatch(removeFromCart("1")); + await store.dispatch(removeFromCart()); expect(store.getState().cart.remove.isLoading).toBeFalsy(); }); - it("should handle clear FromCart.fulfilled", async () => { + + it("should handle removeFromCart.rejected", async () => { + const errorMessage = "Failed to remove item from cart"; + (axios.put as jest.Mock).mockRejectedValue(new Error(errorMessage)); + + await store.dispatch(removeFromCart()); + + expect(store.getState().cart.remove.error).toBeTruthy(); + }); + + it("should handle cartDelete.fulfilled", async () => { (axios.delete as jest.Mock).mockResolvedValue({ data: {} }); - // @ts-ignore - await store.dispatch(cartDelete("1")); + await store.dispatch(cartDelete()); expect(store.getState().cart.delete.isLoading).toBeFalsy(); }); - it("should handle updateCarts.fulfilled", async () => { - const mockItem = { productId: "1", quantity: 3 }; + it("should handle cartDelete.rejected", async () => { + const errorMessage = "Failed to delete cart"; + (axios.delete as jest.Mock).mockRejectedValue(new Error(errorMessage)); + + await store.dispatch(cartDelete()); + + expect(store.getState().cart.delete.error).toBeTruthy(); + }); + + it("should handle updateCarts return undefined.", async () => { + const mockItem = { id: "1", quantity: 3 }; (axios.patch as jest.Mock).mockResolvedValue({ data: mockItem }); store.dispatch({ type: "carts/cartManage/fulfilled", - payload: [{ productId: "1", quantity: 2 }], + payload: [{ id: "1", quantity: 2 }], }); await store.dispatch(updateCarts({ productId: "1", quantity: 3 })); expect(store.getState().cart.data[0]).toEqual(undefined); }); + + it("should handle updateCarts.rejected", async () => { + const errorMessage = "Failed to update cart"; + (axios.patch as jest.Mock).mockRejectedValue(new Error(errorMessage)); + + await store.dispatch(updateCarts({ productId: "1", quantity: 3 })); + + expect(store.getState().cart.update.error).toBeTruthy(); + }); + + it("should return undefined increaseQuantity", () => { + store.dispatch({ + type: "carts/cartManage/fulfilled", + payload: [{ id: "1", quantity: 2 }], + }); + store.dispatch(increaseQuantity("1")); + + expect(store.getState().cart.data.quantity).toEqual(undefined); + }); + + it("should return undefined decreaseQuantity", () => { + store.dispatch({ + type: "carts/cartManage/fulfilled", + payload: [{ id: "1", quantity: 2 }], + }); + store.dispatch(decreaseQuantity("1")); + + expect(store.getState().cart.data.quantity).toEqual(undefined); + }); }); diff --git a/src/__test__/deleteNotify.test.tsx b/src/__test__/deleteNotify.test.tsx new file mode 100644 index 0000000..e09682a --- /dev/null +++ b/src/__test__/deleteNotify.test.tsx @@ -0,0 +1,64 @@ +import "@testing-library/jest-dom/jest-globals"; +import "@testing-library/jest-dom"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Provider } from "react-redux"; +import { BrowserRouter as Router } from "react-router-dom"; + +import store from "../redux/store"; +import DeleteNotify from "../components/common/notify/DeleteNotify"; + +describe("DeleteNotify component", () => { + const mockOnConfirm = jest.fn(); + const mockOnCancel = jest.fn(); + + beforeEach(() => { + render( + + + + + , + ); + }); + + test('renders the "Clear Cart" button', () => { + const clearCartButton = screen.getByText("Clear Cart"); + expect(clearCartButton).toBeInTheDocument(); + }); + + test("opens the modal when Clear Cart button is clicked", () => { + const clearCartButton = screen.getByText("Clear Cart"); + fireEvent.click(clearCartButton); + + const modalHeader = screen.getByText( + "Are you sure you want to delete this product?", + ); + expect(modalHeader).toBeInTheDocument(); + }); + + test("calls onConfirm and closes modal when is clicked", () => { + const clearCartButton = screen.getByText("Clear Cart"); + fireEvent.click(clearCartButton); + + const confirmButton = screen.getByText("Yes, I'm sure"); + fireEvent.click(confirmButton); + + expect(mockOnConfirm).toHaveBeenCalledTimes(1); + expect( + screen.queryByText("Are you sure you want to delete this product?"), + ).toBeInTheDocument(); + }); + + test('calls onCancel and closes modal when "No, cancel" is clicked', () => { + const clearCartButton = screen.getByText("Clear Cart"); + fireEvent.click(clearCartButton); + + const cancelButton = screen.getByText("No, cancel"); + fireEvent.click(cancelButton); + + expect(mockOnCancel).toHaveBeenCalledTimes(1); + expect( + screen.queryByText("Are you sure you want to delete this product?"), + ).toBeInTheDocument(); + }); +}); diff --git a/src/__test__/orders.test.ts b/src/__test__/orders.test.ts new file mode 100644 index 0000000..150c84a --- /dev/null +++ b/src/__test__/orders.test.ts @@ -0,0 +1,99 @@ +import { orders } from "../../type"; +import ordersReducer, { + fetchOrders, + updateOrderStatus, +} from "../redux/reducers/ordersSlice"; + +describe("orders reducer", () => { + const initialState = { + isLoading: false, + data: [] as unknown as orders, + error: false, + }; + + it("should handle initial state", () => { + expect(ordersReducer(undefined, { type: "unknown" })).toEqual(initialState); + }); + + it("should handle fetchOrders.pending", () => { + const action = { type: fetchOrders.pending.type }; + const state = ordersReducer(initialState, action); + expect(state).toEqual({ + ...initialState, + isLoading: true, + }); + }); + + it("should handle fetchOrders.fulfilled", () => { + const mockOrders = [ + { id: 1, status: "pending" }, + { id: 2, status: "completed" }, + ]; + const action = { type: fetchOrders.fulfilled.type, payload: mockOrders }; + const state = ordersReducer(initialState, action); + expect(state).toEqual({ + ...initialState, + isLoading: false, + data: mockOrders, + }); + }); + + it("should handle fetchOrders.rejected", () => { + const action = { type: fetchOrders.rejected.type }; + const state = ordersReducer(initialState, action); + expect(state).toEqual({ + ...initialState, + isLoading: false, + error: true, + }); + }); + + it("should handle updateOrderStatus.pending", () => { + const action = { type: updateOrderStatus.pending.type }; + const state = ordersReducer(initialState, action); + expect(state).toEqual({ + ...initialState, + isLoading: true, + }); + }); + + it("should handle updateOrderStatus.rejected", () => { + const action = { type: updateOrderStatus.rejected.type }; + const state = ordersReducer(initialState, action); + expect(state).toEqual({ + ...initialState, + isLoading: false, + error: true, + }); + }); + + it("should handle updateOrderStatus.fulfilled", () => { + const initialStateWithData: any = { + ...initialState, + data: [ + { + order: { id: 1 }, + products: [ + { id: 101, status: "pending" }, + { id: 102, status: "pending" }, + ], + }, + { + order: { id: 2 }, + products: [{ id: 201, status: "pending" }], + }, + ], + }; + + const action = { + type: updateOrderStatus.fulfilled.type, + payload: { orderId: 1, productId: 102, status: "completed" }, + }; + + const state = ordersReducer(initialStateWithData, action); + + expect(state.data[0].products[1].status).toEqual("completed"); + expect(state.data[0].products[0].status).toEqual("pending"); + expect(state.data[1].products[0].status).toEqual("pending"); + }); +}); diff --git a/src/components/cards/ProductCard.tsx b/src/components/cards/ProductCard.tsx index 12ee843..deb096b 100644 --- a/src/components/cards/ProductCard.tsx +++ b/src/components/cards/ProductCard.tsx @@ -84,7 +84,7 @@ const ProductCard: React.FC = ({ product }) => { const handleAddToCart = async () => { if (!localStorage.getItem("accessToken")) { - toast.info("Please Log in to add to cart."); + toast.error("Please Log in to add to cart."); setTimeout(() => { navigate("/login"); }, 4000); diff --git a/src/components/common/auth/Loader.tsx b/src/components/common/auth/Loader.tsx index 360f8ba..3197528 100644 --- a/src/components/common/auth/Loader.tsx +++ b/src/components/common/auth/Loader.tsx @@ -1,5 +1,5 @@ const Spinner = () => ( -
+
= ({ searchQuery, setSearchQuery }) => { New Arrival + + Orders + diff --git a/src/components/dashboard/SideBar.tsx b/src/components/dashboard/SideBar.tsx index a6b1980..5bdd87d 100644 --- a/src/components/dashboard/SideBar.tsx +++ b/src/components/dashboard/SideBar.tsx @@ -4,7 +4,7 @@ import { IoBriefcaseOutline, IoSettingsOutline } from "react-icons/io5"; import { AiFillProduct } from "react-icons/ai"; import { MdInsertChartOutlined } from "react-icons/md"; import { FiLogOut } from "react-icons/fi"; -import { NavLink, useLocation } from "react-router-dom"; +import { Link, NavLink, useLocation } from "react-router-dom"; import { FaCircle } from "react-icons/fa"; interface SidebarProps { @@ -14,7 +14,8 @@ interface SidebarProps { const SideBar: React.FC = ({ isOpen }) => { const location = useLocation(); - const getLinkClass = (path: string) => (location.pathname === path ? "text-primary" : "text-dark-gray"); + const getLinkClass = (path: string) => + (location.pathname === path ? "text-primary" : "text-dark-gray"); return (
= ({ isOpen }) => { Add product diff --git a/src/components/dashboard/orders/SellerOrder.tsx b/src/components/dashboard/orders/SellerOrder.tsx new file mode 100644 index 0000000..0dcbfef --- /dev/null +++ b/src/components/dashboard/orders/SellerOrder.tsx @@ -0,0 +1,303 @@ +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import moment from "moment"; +import { jwtDecode } from "jwt-decode"; +import { ToastContainer, toast } from "react-toastify"; +import { NumericFormat } from "react-number-format"; +import { Link } from "react-router-dom"; + +import Layout from "../../layouts/SellerLayout"; +import { useAppDispatch } from "../../../redux/hooks"; +import { RootState } from "../../../redux/store"; +import Spinner from "../../common/auth/Loader"; +import { + fetchOrders, + updateOrderStatus, +} from "../../../redux/reducers/ordersSlice"; +import { ProductOrders } from "../../../../type"; +import Warning from "../../common/notify/Warning"; + +const SellerOrder = () => { + const token: any = localStorage.getItem("accessToken"); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState( + null, + ); + const [isLoading, setIsLoading] = useState(true); + + const openModal = (product: ProductOrders) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setSelectedProduct(null); + setIsModalOpen(false); + }; + + const dispatch = useAppDispatch(); + const { data, error } = useSelector((state: RootState) => state.order); + const loading = useSelector((state: RootState) => state.order.isLoading); + + useEffect(() => { + setTimeout(() => { + setIsLoading(false); + }, 2000); + dispatch(fetchOrders()); + }, [dispatch]); + + if (error) { + return ( +
+

+ Cannot get orders for you, unknow error occured. + + {" "} + check your network or server error 😓 + +

+
+ ); + } + + let decoded; + try { + decoded = jwtDecode(token); + } catch (e) { + return ; + } + // @ts-ignore + if (decoded.roleId !== 2) { + return ( +
+

+ You're not allowed to perform this action 🚫 +

+
+ ); + } + + const handleStatusChange = (orderId: number, status: string) => { + setTimeout(() => { + dispatch(fetchOrders()); + dispatch(fetchOrders()); + }, 1000); + dispatch(updateOrderStatus({ orderId, status })); + toast.success("status updated successfully"); + }; + + return ( + + + {isLoading ? ( + + ) : ( +
+
+

Orders

+
+
+ {!Array.isArray(data) && data.length !== 0 ? ( +
+

Oops, you have no order yet 😢

+

Add products to attract more clients

+ + + +
+ ) : ( + + + + + + + + + + + + + {Array.isArray(data) + && data.map((order, index) => ( + + + + + + + + + + ))} + +
+ Customer Name + + Email + + Last Order + + Spent + + Status + + Action +
+
+ users +

{order.order?.buyer.name}

+
+
+

{order.order?.buyer.email}

+
+

+ {moment(order.order?.deliveryDate).format( + "DD MMM, YYYY", + )} +

+
+

+ + total + product.product.price, + 0, + )} + thousandSeparator="," + displayType="text" + prefix="RWF" + /> + {} +

+
+ {order.products && order.products.length > 0 ? ( +

+ {order.products[0].status} +

+ ) : ( +

No Products

+ )} +
+ + + openModal(order.products)} + > + View + +
+ )} + {isModalOpen && selectedProduct && ( +
+
e.stopPropagation()} + > +

Product Details

+ + + + + + + + + + + {selectedProduct.map((productItem: any) => ( + + + + + + + ))} + +
+ Product Name + + Price + + Quantity + + Total +
+ {productItem.product.name} + + + + {productItem.quantity} + + +
+ +
+
+ )} +
+
+ )} +
+ ); +}; + +export default SellerOrder; diff --git a/src/index.css b/src/index.css index 4c36632..19f6dfa 100644 --- a/src/index.css +++ b/src/index.css @@ -56,3 +56,7 @@ height: 100%; object-fit: cover; } + +.hide::-webkit-scrollbar { + display: none; +} diff --git a/src/pages/BuyerOrders.tsx b/src/pages/BuyerOrders.tsx new file mode 100644 index 0000000..132f163 --- /dev/null +++ b/src/pages/BuyerOrders.tsx @@ -0,0 +1,266 @@ +import { useEffect, useState } from "react"; +import moment from "moment"; +import { useSelector } from "react-redux"; +import { jwtDecode } from "jwt-decode"; +import { NumericFormat } from "react-number-format"; +import { Link } from "react-router-dom"; + +import { ProductOrders } from "../../type"; +import { useAppDispatch } from "../redux/hooks"; +import { RootState } from "../redux/store"; +import Spinner from "../components/common/auth/Loader"; +import Warning from "../components/common/notify/Warning"; +import { fetchOrders } from "../redux/reducers/ordersSlice"; + +const SellerOrder = () => { + const token: any = localStorage.getItem("accessToken"); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState( + null, + ); + + const openModal = (product: ProductOrders) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setSelectedProduct(null); + setIsModalOpen(false); + }; + + const dispatch = useAppDispatch(); + const { + data = [], + isLoading, + error, + } = useSelector((state: RootState) => state.order); + + let decoded; + try { + decoded = jwtDecode(token); + } catch (e) { + return ; + } + + if (decoded.roleId !== 1) { + return ( +
+

+ You're not allowed to perform this action 🚫 +

+
+ ); + } + + useEffect(() => { + dispatch(fetchOrders()); + }, [dispatch]); + + const getOrderStatus = (products: any[]) => { + if (products.some((product) => product.status === "Pending")) { + return "Pending"; + } + if (products.some((product) => product.status === "Cancelled")) { + return "Cancelled"; + } + if (products.every((product) => product.status === "Delivered")) { + return "Complete"; + } + return "Pending"; + }; + + if (isLoading) { + return ; + } + + if (error) { + return ( +
+

+ Cannot get orders for you, an unknown error occurred. + + {" "} + Check your network or server error 😓 + +

+
+ ); + } + + return ( +
+
+

Orders

+
+
+ {!Array.isArray(data) && data.length !== 0 ? ( +
+

Oops, you have no order yet 😢

+

Place your order now click below button

+ + + +
+ ) : ( + + + + + + + + + + + + {Array.isArray(data) + && data.map((order: any) => ( + + + + + + + + ))} + +
NameEmail + Delivery Date + + Status + + Details +
+
+ normal +

{order.order.buyer.name}

+
+
+

{order.order.buyer.email}

+
+

+ {moment(order.order.deliveryDate).format( + "DD MMM, YYYY", + )} +

+
+ {getOrderStatus(order.products)} + + openModal(order.products)} + > + View + +
+ )} + {isModalOpen && selectedProduct && ( +
+
e.stopPropagation()} + > +

Product Details

+ + + + + + + + + + + + {selectedProduct?.map((productItem) => ( + + + + + + + + ))} + +
+ Product Name + + Price + + Quantity + + Total + + Status +
+
+ +

+ {productItem.product.name} +

+
+
+ + + {productItem.quantity} + + + +

+ {productItem.status} +

+
+ +
+
+ )} +
+
+ ); +}; + +export default SellerOrder; diff --git a/src/pages/CartManagement.tsx b/src/pages/CartManagement.tsx index ea182d8..ed1af86 100644 --- a/src/pages/CartManagement.tsx +++ b/src/pages/CartManagement.tsx @@ -50,6 +50,7 @@ const CartManagement: React.FC = () => {
); } + console.log(userCart); const handleDelete = async () => { await dispatch(cartDelete()); diff --git a/src/pages/ProductPage.tsx b/src/pages/ProductPage.tsx index 4144080..1413246 100644 --- a/src/pages/ProductPage.tsx +++ b/src/pages/ProductPage.tsx @@ -61,7 +61,7 @@ const ProductPage = () => { }), ); } catch (error: any) { - alert("error"); + alert("error"); // eslint-disable-line no-alert } }; fetchFilteredProducts(); @@ -78,7 +78,8 @@ const ProductPage = () => { const category = searchParams.get("category") || ""; if (name) { - filtered = filtered.filter((product) => product.name.toLowerCase().includes(name.toLowerCase())); + filtered = filtered.filter((product) => + product.name.toLowerCase().includes(name.toLowerCase())); } // if (category) { diff --git a/src/pages/SignupVerification.tsx b/src/pages/SignupVerification.tsx new file mode 100644 index 0000000..ca0883b --- /dev/null +++ b/src/pages/SignupVerification.tsx @@ -0,0 +1,23 @@ +import { Link } from "react-router-dom"; + +const SignupVerification = () => ( +
+
+

Email Verified!

+

+ Thank you for verifying your email address. Your registration is now + complete. +

+

+ You can now log in to your account and start using our services. +

+ + + +
+
+); + +export default SignupVerification; diff --git a/src/redux/api/api.ts b/src/redux/api/api.ts index e21cbcc..bc0c55d 100644 --- a/src/redux/api/api.ts +++ b/src/redux/api/api.ts @@ -34,7 +34,6 @@ api.interceptors.response.use( return Promise.reject(error); }, ); - export default api; export const setNavigate = (navigate) => { diff --git a/src/redux/reducers/ordersSlice.ts b/src/redux/reducers/ordersSlice.ts new file mode 100644 index 0000000..bc41571 --- /dev/null +++ b/src/redux/reducers/ordersSlice.ts @@ -0,0 +1,101 @@ +import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; + +import axios from "../api/api"; +import { orders } from "../../../type"; + +export const fetchOrders = createAsyncThunk< +orders, +void, +{ rejectValue: string } +>("orders/fetchOrders", async (_, { rejectWithValue }) => { + try { + const response = await axios.get("/orders", { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }); + + return response.data.orders as orders; + } catch (error: any) { + return rejectWithValue(error.data.message); + } +}); + +export const updateOrderStatus = createAsyncThunk< +any, +{ orderId: number; status: string }, +{ rejectValue: string } +>( + "orders/updateOrderStatus", + async ({ orderId, status }, { rejectWithValue }) => { + try { + const response = await axios.patch( + `/orders/${orderId}/status`, + { status }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }, + ); + console.log(response.data.updatedItems); + return response.data.updatedItems; + } catch (error: any) { + if ( + error.response + && error.response.data + && error.response.data.message + ) { + return rejectWithValue(error.response.data.message); + } + return rejectWithValue("An unexpected error occurred."); + } + }, +); + +const ordersSlice = createSlice({ + name: "orders", + initialState: { + isLoading: false, + data: [] as unknown as orders, + error: false, + }, + reducers: {}, + extraReducers(builder) { + builder + .addCase(fetchOrders.pending, (state) => { + state.isLoading = true; + }) + .addCase(fetchOrders.fulfilled, (state, action) => { + state.isLoading = false; + state.data = action.payload; + }) + .addCase(fetchOrders.rejected, (state, action) => { + state.isLoading = false; + state.error = true; + }) + .addCase(updateOrderStatus.pending, (state) => { + state.isLoading = true; + }) + .addCase(updateOrderStatus.fulfilled, (state, action) => { + const { orderId, productId, status } = action.payload; + const orderIndex = state.data.findIndex( + (order) => order.order.id === orderId, + ); + if (orderIndex !== -1) { + const productIndex = state.data[orderIndex].products.findIndex( + (product) => product.id === productId, + ); + if (productIndex !== -1) { + state.data[orderIndex].products[productIndex].status = status; + } + } + }) + .addCase(updateOrderStatus.rejected, (state, action) => { + state.isLoading = false; + state.error = true; + }); + }, +}); + +export default ordersSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 94c266b..b85a3de 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -14,6 +14,7 @@ import cartsReducer from "./reducers/cartSlice"; import reviewSlice from "./reducers/reviewSlice"; import chatSlice from "./reducers/chatSlice"; import authReducer from "./reducers/authSlice"; +import ordersReducer from "./reducers/ordersSlice"; const store = configureStore({ reducer: { @@ -31,6 +32,7 @@ const store = configureStore({ review: reviewSlice, chats: chatSlice, auth: authReducer, + order: ordersReducer, }, }); export type RootState = ReturnType; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index fa07270..998fff3 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -23,6 +23,10 @@ import Dashboard from "../dashboard/admin/Dashboard"; import CartManagement from "../pages/CartManagement"; import { setNavigate } from "../redux/api/api"; import ChatPage from "../pages/ChatPage"; +// import { setNavigateFunction } from "../redux/api/api"; +import SellerOrder from "../components/dashboard/orders/SellerOrder"; +import BuyerOrders from "../pages/BuyerOrders"; +import SignupVerification from "../pages/SignupVerification"; const AppRoutes = () => { const navigate = useNavigate(); @@ -53,11 +57,14 @@ const AppRoutes = () => { } /> } /> } /> + } /> } /> } /> } /> } /> + } /> + { } /> } /> } /> + } /> } /> } /> } /> diff --git a/type.d.ts b/type.d.ts index 8f43a42..78a5f49 100644 --- a/type.d.ts +++ b/type.d.ts @@ -79,6 +79,52 @@ export interface UserCart { updatedAt: string; } +export interface Buyer { + id: number; + name: string; + username: string; + email: string; + password: string; +} + +export interface Product { + productId: number; + productName: string; + name: string; + price: number; + quantity: number; +} + +export interface Item { + itemId: number; + quantity: number; +} + +export interface orders { + [x: string]: any; + id: number; + buyerId: number; + buyer: Buyer; + createdAt: string; + deliveryDate: string; + items: Item[]; + products: Product[]; + status: string; + updatedAt: string; +} + +export interface ProductOrders { + map( + arg0: (product: any) => import("react/jsx-runtime").JSX.Element, + ): import("react").ReactNode; + order: any; + name: any; + product: any; + products: Product[]; + price: any; + quantity: number; +} + export interface CartState { isLoading: boolean; data: UserCart | null;