diff --git a/.env b/.env index a9c8090..1e5287e 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ -REACT_APP_API_BASE_URL=https://trantran.zenstores.com.vn/api/ +# REACT_APP_API_BASE_URL=https://trantran.zenstores.com.vn/api/ +REACT_APP_API_BASE_URL=http://localhost:4000/api/v1 # CORS Proxy for development (uncomment if needed) # REACT_APP_API_BASE_URL=https://cors-anywhere.herokuapp.com/https://trantran.zenstores.com.vn/api/ diff --git a/package-lock.json b/package-lock.json index 29f5a0f..468ebf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "@ant-design/icons": "^5.6.1", "@ckeditor/ckeditor5-build-classic": "^41.2.0", "@ckeditor/ckeditor5-react": "^6.2.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", @@ -142,13 +143,14 @@ } }, "node_modules/@ant-design/icons": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.3.tgz", - "integrity": "sha512-Zfci1s4f4+vfpVD6ksmmPuBv00SB/slpUAQlsBlMeRJdSleVVkgTUdlBM4j/vGzqYfMh2hF8/Poa1VSh542w0Q==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.11.2", + "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, @@ -2042,12 +2044,10 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -21112,11 +21112,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", diff --git a/package.json b/package.json index 2c460eb..fd007e5 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "homepage": "/", "private": true, "dependencies": { + "@ant-design/icons": "^5.6.1", "@ckeditor/ckeditor5-build-classic": "^41.2.0", "@ckeditor/ckeditor5-react": "^6.2.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", diff --git a/public/assets/img/_logo.png b/public/assets/img/_logo.png new file mode 100644 index 0000000..7257955 Binary files /dev/null and b/public/assets/img/_logo.png differ diff --git a/public/assets/img/authentication/_login-img.jpg b/public/assets/img/authentication/_login-img.jpg new file mode 100644 index 0000000..e39c7d2 Binary files /dev/null and b/public/assets/img/authentication/_login-img.jpg differ diff --git a/public/assets/img/authentication/login-img.jpg b/public/assets/img/authentication/login-img.jpg index e39c7d2..d4e6aff 100644 Binary files a/public/assets/img/authentication/login-img.jpg and b/public/assets/img/authentication/login-img.jpg differ diff --git a/public/assets/img/logo.png b/public/assets/img/logo.png index 7257955..36b59bf 100644 Binary files a/public/assets/img/logo.png and b/public/assets/img/logo.png differ diff --git a/public/assets/img/logo.svg b/public/assets/img/logo.svg new file mode 100644 index 0000000..04a0234 --- /dev/null +++ b/public/assets/img/logo.svg @@ -0,0 +1,26 @@ + + + +Created with Fabric.js 3.6.3 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/InitialPage/Sidebar/Header.jsx b/src/InitialPage/Sidebar/Header.jsx index 04b46e9..1a8e9ae 100644 --- a/src/InitialPage/Sidebar/Header.jsx +++ b/src/InitialPage/Sidebar/Header.jsx @@ -1,16 +1,25 @@ /* eslint-disable no-unused-vars */ import React, { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import FeatherIcon from "feather-icons-react"; import ImageWithBasePath from "../../core/img/imagewithbasebath"; import { Search, XCircle } from "react-feather"; import { all_routes } from "../../Router/all_routes"; +import { useSelector } from "react-redux"; +import authApi from "../../services/authApi"; const Header = () => { const route = all_routes; + const navigate = useNavigate(); + const authState = useSelector((state) => state.auth); const [toggle, SetToggle] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); + const handleLogout = () => { + authApi.logout() + navigate(route.signin); + }; + const isElementVisible = (element) => { return element.offsetWidth > 0 || element.offsetHeight > 0; }; @@ -616,8 +625,8 @@ const Header = () => { /> - John Smilga - Super Admin + {authState.user?.name || 'User'} + {authState.user?.role || 'Admin'} @@ -632,12 +641,12 @@ const Header = () => {
-
John Smilga
-
Super Admin
+
{authState.user?.name || 'User'}
+
{authState.user?.role || 'Admin'}

- + My Profile @@ -645,14 +654,18 @@ const Header = () => { Settings
- + @@ -675,9 +688,13 @@ const Header = () => { Settings - + {/* /Mobile Menu */} diff --git a/src/Router/router.jsx b/src/Router/router.jsx index 59f5546..2e3a84d 100644 --- a/src/Router/router.jsx +++ b/src/Router/router.jsx @@ -6,6 +6,7 @@ import { pagesRoute, posRoutes, publicRoutes } from "./router.link"; import { Outlet } from "react-router-dom"; import { useSelector } from "react-redux"; import ThemeSettings from "../InitialPage/themeSettings"; +import ProtectedRoute from "../components/ProtectedRoute"; // import CollapsedSidebar from "../InitialPage/Sidebar/collapsedSidebar"; import Loader from "../feature-module/loader/loader"; // import HorizontalSidebar from "../InitialPage/Sidebar/horizontalSidebar"; @@ -48,8 +49,6 @@ const AllRoutes = () => { ); - console.log(publicRoutes, "dashboard"); - return (
@@ -58,7 +57,14 @@ const AllRoutes = () => { ))} - }> + + + + } + > {publicRoutes.map((route, id) => ( ))} diff --git a/src/Router/router.link.jsx b/src/Router/router.link.jsx index 9d92123..5aeac81 100644 --- a/src/Router/router.link.jsx +++ b/src/Router/router.link.jsx @@ -1404,6 +1404,13 @@ export const publicRoutes = [ element: , route: Route, }, + { + id: 113.1, + path: `${routes.productdetails}/:id`, + name: "productdetails", + element: , + route: Route, + }, { id: 114, path: routes.warehouses, diff --git a/src/components/CustomPagination.jsx b/src/components/CustomPagination.jsx index 2669657..48a5ad2 100644 --- a/src/components/CustomPagination.jsx +++ b/src/components/CustomPagination.jsx @@ -1,5 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { + DoubleLeftOutlined, + DoubleRightOutlined, + LeftOutlined, + RightOutlined, +} from "@ant-design/icons"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; const CustomPagination = ({ currentPage = 1, @@ -13,28 +19,32 @@ const CustomPagination = ({ showInfo = true, showPageSizeSelector = true, compact = false, - className = '' + className = "", }) => { // Theme state for force re-render const [themeKey, setThemeKey] = useState(0); - + // Get theme from Redux and localStorage fallback const reduxTheme = useSelector((state) => state.theme?.isDarkMode); - const localStorageTheme = localStorage.getItem('colorschema') === 'dark_mode'; - const documentTheme = document.documentElement.getAttribute('data-layout-mode') === 'dark_mode'; - + const localStorageTheme = localStorage.getItem("colorschema") === "dark_mode"; + const documentTheme = + document.documentElement.getAttribute("data-layout-mode") === "dark_mode"; + const isDarkMode = reduxTheme || localStorageTheme || documentTheme; // Listen for theme changes useEffect(() => { const handleThemeChange = () => { - setThemeKey(prev => prev + 1); + setThemeKey((prev) => prev + 1); }; // Listen for data-layout-mode attribute changes const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && mutation.attributeName === 'data-layout-mode') { + if ( + mutation.type === "attributes" && + mutation.attributeName === "data-layout-mode" + ) { handleThemeChange(); } }); @@ -42,27 +52,25 @@ const CustomPagination = ({ observer.observe(document.documentElement, { attributes: true, - attributeFilter: ['data-layout-mode'] + attributeFilter: ["data-layout-mode"], }); // Also listen for localStorage changes const handleStorageChange = (e) => { - if (e.key === 'colorschema') { + if (e.key === "colorschema") { handleThemeChange(); } }; - - window.addEventListener('storage', handleStorageChange); + + window.addEventListener("storage", handleStorageChange); return () => { observer.disconnect(); - window.removeEventListener('storage', handleStorageChange); + window.removeEventListener("storage", handleStorageChange); }; }, []); - // Calculate pagination info - const startRecord = totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1; - const endRecord = Math.min(currentPage * pageSize, totalCount); + console.log(totalCount); // Handle page change const handlePageClick = (page) => { @@ -80,190 +88,191 @@ const CustomPagination = ({ // Container styles based on compact mode const containerStyles = { - background: isDarkMode - ? 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)' - : 'linear-gradient(135deg, #ffffff, #f8f9fa)', - border: isDarkMode - ? '1px solid rgba(52, 152, 219, 0.3)' - : '1px solid rgba(0, 0, 0, 0.1)', - borderRadius: compact ? '8px' : '12px', - boxShadow: isDarkMode - ? (compact ? '0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(52, 152, 219, 0.1)' : '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)') - : (compact ? '0 1px 6px rgba(0, 0, 0, 0.06)' : '0 2px 12px rgba(0, 0, 0, 0.08)'), - backdropFilter: isDarkMode ? (compact ? 'blur(8px)' : 'blur(10px)') : 'none', - transition: compact ? 'all 0.2s ease' : 'all 0.3s ease', - position: 'relative', - overflow: 'hidden', - padding: compact ? '8px 16px' : '16px 24px', - margin: compact ? '8px 0' : '16px 0', - fontSize: compact ? '13px' : '14px' + background: isDarkMode + ? "linear-gradient(135deg, #2c3e50 0%, #34495e 100%)" + : "linear-gradient(135deg, #ffffff, #f8f9fa)", + border: isDarkMode + ? "1px solid rgba(52, 152, 219, 0.3)" + : "1px solid rgba(0, 0, 0, 0.1)", + borderRadius: compact ? "8px" : "12px", + boxShadow: isDarkMode + ? compact + ? "0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(52, 152, 219, 0.1)" + : "0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)" + : compact + ? "0 1px 6px rgba(0, 0, 0, 0.06)" + : "0 2px 12px rgba(0, 0, 0, 0.08)", + backdropFilter: isDarkMode + ? compact + ? "blur(8px)" + : "blur(10px)" + : "none", + transition: compact ? "all 0.2s ease" : "all 0.3s ease", + position: "relative", + overflow: "hidden", + padding: compact ? "8px 16px" : "16px 24px", + margin: compact ? "8px 0" : "16px 0", + fontSize: compact ? "13px" : "14px", }; - // Button styles - const getButtonStyles = (isActive) => ({ - background: loading - ? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)') - : isActive - ? (isDarkMode ? 'linear-gradient(45deg, #f39c12, #e67e22)' : 'linear-gradient(135deg, #007bff, #0056b3)') - : (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'), - border: isActive - ? (isDarkMode ? (compact ? '1px solid #f39c12' : '2px solid #f39c12') : (compact ? '1px solid #007bff' : '2px solid #007bff')) - : (isDarkMode ? '1px solid rgba(52, 152, 219, 0.3)' : '1px solid #dee2e6'), - borderRadius: '50%', - width: compact ? '24px' : '32px', - height: compact ? '24px' : '32px', - color: isActive - ? '#ffffff' - : (isDarkMode ? '#ffffff' : '#495057'), - fontSize: compact ? '11px' : '14px', - fontWeight: compact ? '600' : '700', - cursor: loading ? 'not-allowed' : 'pointer', - transition: compact ? 'all 0.2s ease' : 'all 0.3s ease', - boxShadow: loading - ? 'none' - : isActive - ? (isDarkMode ? (compact ? '0 2px 6px rgba(243, 156, 18, 0.3)' : '0 4px 12px rgba(243, 156, 18, 0.4)') : (compact ? '0 2px 4px rgba(0, 123, 255, 0.2)' : '0 3px 8px rgba(0, 123, 255, 0.3)')) - : (isDarkMode ? (compact ? '0 1px 4px rgba(52, 73, 94, 0.2)' : '0 2px 8px rgba(52, 73, 94, 0.3)') : (compact ? '0 1px 2px rgba(0, 0, 0, 0.08)' : '0 1px 3px rgba(0, 0, 0, 0.1)')), - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - opacity: loading ? 0.6 : 1 - }); - return (
{/* Pagination Info */} {showInfo && (
{showPageSizeSelector && ( -
- Số hàng mỗi trang +
+ + Number of rows per page + - bản ghi + + records +
)} -
-
- 📊 -
- - Xem {startRecord} đến {endRecord} của {totalCount} bản - + {/* Pagination Buttons */} +
+
)} - - {/* Pagination Buttons */} -
- {Array.from({ length: totalPages }, (_, i) => { - const pageNum = i + 1; - const isActive = currentPage === pageNum; - - return ( - - ); - })} -
); }; diff --git a/src/components/Loading/EnhancedLoader.scss b/src/components/Loading/EnhancedLoader.scss index 8f356a8..2eae9d5 100644 --- a/src/components/Loading/EnhancedLoader.scss +++ b/src/components/Loading/EnhancedLoader.scss @@ -43,7 +43,7 @@ // Color variants .primary { - --loader-color: #ff9f43; + --loader-color: #36175e; --loader-secondary: rgba(255, 159, 67, 0.3); } diff --git a/src/components/Loading/LoadingButton.scss b/src/components/Loading/LoadingButton.scss index 94b638b..805057a 100644 --- a/src/components/Loading/LoadingButton.scss +++ b/src/components/Loading/LoadingButton.scss @@ -38,7 +38,7 @@ // Color variants &.primary { - background: #ff9f43; + background: #36175e; color: white; &:hover:not(.loading):not(.disabled) { @@ -83,11 +83,11 @@ &.outline-primary { background: transparent; - color: #ff9f43; - border: 2px solid #ff9f43; + color: #36175e; + border: 2px solid #36175e; &:hover:not(.loading):not(.disabled) { - background: #ff9f43; + background: #36175e; color: white; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 159, 67, 0.4); diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..3f2c12b --- /dev/null +++ b/src/components/ProtectedRoute.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +const ProtectedRoute = ({ children }) => { + // Check if user is authenticated using Redux state + const authState = useSelector((state) => state.auth); + const isAuthenticated = authState?.isAuthenticated || authState?.token; + + // Fallback to localStorage check + const localStorageAuth = localStorage.getItem('authToken') || localStorage.getItem('user'); + const isUserAuthenticated = isAuthenticated || localStorageAuth; + + if (!isUserAuthenticated) { + // Redirect to login page if not authenticated + return ; + } + + // If authenticated, render the protected component + return children; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/src/core/json/siderbar_data.jsx b/src/core/json/siderbar_data.jsx index e59ed4d..82673a8 100644 --- a/src/core/json/siderbar_data.jsx +++ b/src/core/json/siderbar_data.jsx @@ -60,22 +60,22 @@ export const SidebarData = [ submenuHdr: "Inventory", submenuItems: [ - { label: "Tiến độ dự án", link: "/project-tracker",icon: ,showSubRoute: false}, - { label: "Sản phẩm", link: "/product-list", icon:,showSubRoute: false,submenu: false }, - { label: "Nhập kho", link: "/product-list-2", icon:,showSubRoute: false,submenu: false }, - { label: "Tồn kho", link: "/product-list-3", icon:,showSubRoute: false,submenu: false }, + { label: "Project Progress", link: "/project-tracker",icon: ,showSubRoute: false}, + { label: "Products", link: "/product-list", icon:,showSubRoute: false,submenu: false }, + { label: "Stock In", link: "/product-list-2", icon:,showSubRoute: false,submenu: false }, + { label: "Stock On Hand", link: "/product-list-3", icon:,showSubRoute: false,submenu: false }, { label: "Create Product", link: "/add-product", icon: ,showSubRoute: false, submenu: false }, - { label: "Sản phẩm hết hạn", link: "/expired-products", icon: ,showSubRoute: false,submenu: false }, - { label: "Hàng tồn kho", link: "/low-stocks", icon: ,showSubRoute: false,submenu: false }, - { label: "Danh mục", link: "/category-list", icon: ,showSubRoute: false,submenu: false }, + { label: "Expired Products", link: "/expired-products", icon: ,showSubRoute: false,submenu: false }, + { label: "Low Stock Items", link: "/low-stocks", icon: ,showSubRoute: false,submenu: false }, + { label: "Category", link: "/category-list", icon: ,showSubRoute: false,submenu: false }, { label: "Sub Category", link: "/sub-categories", icon: ,showSubRoute: false,submenu: false }, - { label: "Thương hiệu", link: "/brand-list", icon: ,showSubRoute: false,submenu: false }, + { label: "Brands", link: "/brand-list", icon: ,showSubRoute: false,submenu: false }, { label: "Units", link: "/units", icon: ,showSubRoute: false,submenu: false }, { label: "Variant Attributes", link: "/variant-attributes", icon: ,showSubRoute: false,submenu: false }, - { label: "Bảo hành", link: "/warranty", icon: ,showSubRoute: false,submenu: false }, + { label: "Warranty", link: "/warranty", icon: ,showSubRoute: false,submenu: false }, { label: "In Barcode", link: "/barcode", icon: , showSubRoute: false,submenu: false }, { label: "In QR Code", link: "/qrcode", icon: ,showSubRoute: false,submenu: false }, - { label: "Khách mời đám cưới", link: "/wedding-guest-list", icon: ,showSubRoute: false,submenu: false } + { label: "Wedding Guests", link: "/wedding-guest-list", icon: ,showSubRoute: false,submenu: false } ] }, { diff --git a/src/core/modals/inventory/addcategorylist.jsx b/src/core/modals/inventory/addcategorylist.jsx index 591cd54..9a026b7 100644 --- a/src/core/modals/inventory/addcategorylist.jsx +++ b/src/core/modals/inventory/addcategorylist.jsx @@ -1,72 +1,148 @@ -import React from 'react' -import { Link } from 'react-router-dom' +import { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import Swal from "sweetalert2"; +import { + createCategory, + fetchCategories, +} from "../../redux/actions/categoryActions"; const AddCategoryList = () => { - return ( -
- {/* Add Category */} -
-
-
-
-
-
-
-

Create Category

-
- -
-
-
-
- - -
-
- - -
-
-
- Status - -
-
-
- - - Create Category - -
-
-
-
-
-
-
-
- {/* /Add Category */} -
- ) -} + const dispatch = useDispatch(); + const { creating } = useSelector((state) => state.categories); -export default AddCategoryList + const [formData, setFormData] = useState({ + name: "", + description: "", + }); + + const handleInputChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + await dispatch(createCategory(formData)); + + await dispatch(fetchCategories()); + + Swal.fire({ + title: "Success!", + text: "Category created successfully!", + icon: "success", + showConfirmButton: false, + timer: 1500, + }).then(() => { + const closeButton = document.querySelector("#add-category .btn-close"); + closeButton.click(); + }); + } catch (error) { + console.error("Error creating category:", error); + + // Show error message + Swal.fire({ + icon: "error", + title: "Error!", + text: error.message || "Failed to create category. Please try again.", + }); + } + }; + + return ( +
+ {/* Add Category */} +
+
+
+
+
+
+
+

Create Category

+
+ +
+
+
+
+ + +
+
+ + +
+
+
+ Status + +
+
+
+ + +
+
+
+
+
+
+
+
+ {/* /Add Category */} +
+ ); +}; + +export default AddCategoryList; diff --git a/src/core/modals/inventory/editcategorylist.jsx b/src/core/modals/inventory/editcategorylist.jsx index dbf0c84..2b7300c 100644 --- a/src/core/modals/inventory/editcategorylist.jsx +++ b/src/core/modals/inventory/editcategorylist.jsx @@ -1,80 +1,160 @@ -import React from 'react' -import { Link } from 'react-router-dom' +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import Swal from "sweetalert2"; +import { + fetchCategories, + updateCategory, +} from "../../redux/actions/categoryActions"; const EditCategoryList = () => { - return ( -
- {/* Edit Category */} -
-
-
-
-
-
-
-

Edit Category

-
- -
-
-
-
- - -
-
- - -
-
-
- Status - -
-
-
- - - Save Changes - -
-
-
-
-
-
-
-
- {/* /Edit Category */} -
- ) -} + const dispatch = useDispatch(); -export default EditCategoryList + const { currentCategory, updating, currentPage, pageSize } = useSelector( + (state) => state.categories + ); + + const [formData, setFormData] = useState({ + name: "", + description: "", + }); + + useEffect(() => { + if (currentCategory) { + setFormData({ + name: currentCategory.name, + description: currentCategory.description, + }); + } + }, [currentCategory]); + + const handleInputChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + await dispatch(updateCategory(currentCategory?.id, formData)); + + await dispatch(fetchCategories({ page: currentPage, limit: pageSize })); + + Swal.fire({ + title: "Success!", + text: "Category updated successfully!", + icon: "success", + showConfirmButton: false, + timer: 1500, + }).then(() => { + const closeButton = document.querySelector("#edit-category .btn-close"); + closeButton.click(); + }); + } catch (error) { + console.error("Error updating category:", error); + + // Show error message + Swal.fire({ + icon: "error", + title: "Error!", + text: error.message || "Failed to update category. Please try again.", + }); + } + }; + + return ( +
+ {/* Edit Category */} +
+
+
+
+
+
+
+

Edit Category

+
+ +
+
+
+
+ + +
+
+ + +
+
+
+ Status + +
+
+
+ + +
+
+
+
+
+
+
+
+ {/* /Edit Category */} +
+ ); +}; + +export default EditCategoryList; diff --git a/src/core/pagination/datatable.jsx b/src/core/pagination/datatable.jsx index 5af663a..dfb373d 100644 --- a/src/core/pagination/datatable.jsx +++ b/src/core/pagination/datatable.jsx @@ -7,7 +7,6 @@ import { onShowSizeChange } from "./pagination"; const Datatable = ({ props, columns, dataSource }) => { const [selectedRowKeys, setSelectedRowKeys] = useState([]); const onSelectChange = (newSelectedRowKeys) => { - console.log("selectedRowKeys changed: ", selectedRowKeys); setSelectedRowKeys(newSelectedRowKeys); }; @@ -15,15 +14,18 @@ const Datatable = ({ props, columns, dataSource }) => { selectedRowKeys, onChange: onSelectChange, }; + return ( record.id} + pagination={{ + pageSize: 100 + }} /> ); }; diff --git a/src/core/redux/actions/categoryActions.js b/src/core/redux/actions/categoryActions.js new file mode 100644 index 0000000..c00a225 --- /dev/null +++ b/src/core/redux/actions/categoryActions.js @@ -0,0 +1,195 @@ +import { categoriesApi } from '../../../services/categoriesApi'; + +// Action Types +export const CATEGORY_ACTIONS = { + // Fetch Categories + FETCH_CATEGORIES_REQUEST: 'FETCH_CATEGORIES_REQUEST', + FETCH_CATEGORIES_SUCCESS: 'FETCH_CATEGORIES_SUCCESS', + FETCH_CATEGORIES_FAILURE: 'FETCH_CATEGORIES_FAILURE', + + // Fetch Single Category + FETCH_CATEGORY_REQUEST: 'FETCH_CATEGORY_REQUEST', + FETCH_CATEGORY_SUCCESS: 'FETCH_CATEGORY_SUCCESS', + FETCH_CATEGORY_FAILURE: 'FETCH_CATEGORY_FAILURE', + + // Create Category + CREATE_CATEGORY_REQUEST: 'CREATE_CATEGORY_REQUEST', + CREATE_CATEGORY_SUCCESS: 'CREATE_CATEGORY_SUCCESS', + CREATE_CATEGORY_FAILURE: 'CREATE_CATEGORY_FAILURE', + + // Update Category + UPDATE_CATEGORY_REQUEST: 'UPDATE_CATEGORY_REQUEST', + UPDATE_CATEGORY_SUCCESS: 'UPDATE_CATEGORY_SUCCESS', + UPDATE_CATEGORY_FAILURE: 'UPDATE_CATEGORY_FAILURE', + + // Delete Category + DELETE_CATEGORY_REQUEST: 'DELETE_CATEGORY_REQUEST', + DELETE_CATEGORY_SUCCESS: 'DELETE_CATEGORY_SUCCESS', + DELETE_CATEGORY_FAILURE: 'DELETE_CATEGORY_FAILURE', + + // Search Categories + SEARCH_CATEGORIES_REQUEST: 'SEARCH_CATEGORIES_REQUEST', + SEARCH_CATEGORIES_SUCCESS: 'SEARCH_CATEGORIES_SUCCESS', + SEARCH_CATEGORIES_FAILURE: 'SEARCH_CATEGORIES_FAILURE', + + // Get Category Products + FETCH_CATEGORY_PRODUCTS_REQUEST: 'FETCH_CATEGORY_PRODUCTS_REQUEST', + FETCH_CATEGORY_PRODUCTS_SUCCESS: 'FETCH_CATEGORY_PRODUCTS_SUCCESS', + FETCH_CATEGORY_PRODUCTS_FAILURE: 'FETCH_CATEGORY_PRODUCTS_FAILURE', + + // Clear States + CLEAR_CATEGORY_ERROR: 'CLEAR_CATEGORY_ERROR', + CLEAR_CURRENT_CATEGORY: 'CLEAR_CURRENT_CATEGORY', +}; + +// Action Creators + +// Fetch all categories +export const fetchCategories = (params = {}) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.FETCH_CATEGORIES_REQUEST }); + + try { + const data = await categoriesApi.getAllCategories(params); + dispatch({ + type: CATEGORY_ACTIONS.FETCH_CATEGORIES_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.FETCH_CATEGORIES_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch categories', + }); + throw error; + } +}; + +// Fetch single category +export const fetchCategory = (id) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.FETCH_CATEGORY_REQUEST }); + + try { + const data = await categoriesApi.getCategoryById(id); + dispatch({ + type: CATEGORY_ACTIONS.FETCH_CATEGORY_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.FETCH_CATEGORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch category', + }); + throw error; + } +}; + +// Create category +export const createCategory = (categoryData) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.CREATE_CATEGORY_REQUEST }); + + try { + const data = await categoriesApi.createCategory(categoryData); + dispatch({ + type: CATEGORY_ACTIONS.CREATE_CATEGORY_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.CREATE_CATEGORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to create category', + }); + throw error; + } +}; + +// Update category +export const updateCategory = (id, categoryData) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.UPDATE_CATEGORY_REQUEST }); + + try { + const data = await categoriesApi.updateCategory(id, categoryData); + dispatch({ + type: CATEGORY_ACTIONS.UPDATE_CATEGORY_SUCCESS, + payload: { id, data }, + }); + return data; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.UPDATE_CATEGORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to update category', + }); + throw error; + } +}; + +// Delete category +export const deleteCategory = (id) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.DELETE_CATEGORY_REQUEST }); + + try { + await categoriesApi.deleteCategory(id); + dispatch({ + type: CATEGORY_ACTIONS.DELETE_CATEGORY_SUCCESS, + payload: id, + }); + return id; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.DELETE_CATEGORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to delete category', + }); + throw error; + } +}; + +// Search categories +export const searchCategories = (query, params = {}) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.SEARCH_CATEGORIES_REQUEST }); + + try { + const data = await categoriesApi.searchCategories(query, params); + dispatch({ + type: CATEGORY_ACTIONS.SEARCH_CATEGORIES_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.SEARCH_CATEGORIES_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to search categories', + }); + throw error; + } +}; + +// Fetch category products +export const fetchCategoryProducts = (id, params = {}) => async (dispatch) => { + dispatch({ type: CATEGORY_ACTIONS.FETCH_CATEGORY_PRODUCTS_REQUEST }); + + try { + const data = await categoriesApi.getCategoryProducts(id, params); + dispatch({ + type: CATEGORY_ACTIONS.FETCH_CATEGORY_PRODUCTS_SUCCESS, + payload: { id, data }, + }); + return data; + } catch (error) { + dispatch({ + type: CATEGORY_ACTIONS.FETCH_CATEGORY_PRODUCTS_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch category products', + }); + throw error; + } +}; + +// Clear error +export const clearCategoryError = () => ({ + type: CATEGORY_ACTIONS.CLEAR_CATEGORY_ERROR, +}); + +// Clear current category +export const clearCurrentCategory = () => ({ + type: CATEGORY_ACTIONS.CLEAR_CURRENT_CATEGORY, +}); \ No newline at end of file diff --git a/src/core/redux/actions/orderActions.js b/src/core/redux/actions/orderActions.js new file mode 100644 index 0000000..b43dbdf --- /dev/null +++ b/src/core/redux/actions/orderActions.js @@ -0,0 +1,138 @@ +import { ordersApi } from '../../../services/ordersApi'; + +// Action Types +export const ORDER_ACTIONS = { + // Fetch Orders + FETCH_ORDERS_REQUEST: 'FETCH_ORDERS_REQUEST', + FETCH_ORDERS_SUCCESS: 'FETCH_ORDERS_SUCCESS', + FETCH_ORDERS_FAILURE: 'FETCH_ORDERS_FAILURE', + + // Fetch Single Order + FETCH_ORDER_REQUEST: 'FETCH_ORDER_REQUEST', + FETCH_ORDER_SUCCESS: 'FETCH_ORDER_SUCCESS', + FETCH_ORDER_FAILURE: 'FETCH_ORDER_FAILURE', + + // Create Order + CREATE_ORDER_REQUEST: 'CREATE_ORDER_REQUEST', + CREATE_ORDER_SUCCESS: 'CREATE_ORDER_SUCCESS', + CREATE_ORDER_FAILURE: 'CREATE_ORDER_FAILURE', + + // Update Order + UPDATE_ORDER_REQUEST: 'UPDATE_ORDER_REQUEST', + UPDATE_ORDER_SUCCESS: 'UPDATE_ORDER_SUCCESS', + UPDATE_ORDER_FAILURE: 'UPDATE_ORDER_FAILURE', + + // Delete Order + DELETE_ORDER_REQUEST: 'DELETE_ORDER_REQUEST', + DELETE_ORDER_SUCCESS: 'DELETE_ORDER_SUCCESS', + DELETE_ORDER_FAILURE: 'DELETE_ORDER_FAILURE', + + // Search Orders + SEARCH_ORDERS_REQUEST: 'SEARCH_ORDERS_REQUEST', + SEARCH_ORDERS_SUCCESS: 'SEARCH_ORDERS_SUCCESS', + SEARCH_ORDERS_FAILURE: 'SEARCH_ORDERS_FAILURE', + + // Clear States + CLEAR_ORDER_ERROR: 'CLEAR_ORDER_ERROR', + CLEAR_CURRENT_ORDER: 'CLEAR_CURRENT_ORDER', +}; + +// Action Creators + +export const fetchOrders = (params = {}) => async (dispatch) => { + dispatch({ type: ORDER_ACTIONS.FETCH_ORDERS_REQUEST }); + try { + const data = await ordersApi.getAllOrders(params); + dispatch({ type: ORDER_ACTIONS.FETCH_ORDERS_SUCCESS, payload: data }); + return data; + } catch (error) { + dispatch({ + type: ORDER_ACTIONS.FETCH_ORDERS_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch orders', + }); + throw error; + } +}; + +export const fetchOrder = (id) => async (dispatch) => { + dispatch({ type: ORDER_ACTIONS.FETCH_ORDER_REQUEST }); + try { + const data = await ordersApi.getOrderById(id); + dispatch({ type: ORDER_ACTIONS.FETCH_ORDER_SUCCESS, payload: data }); + return data; + } catch (error) { + dispatch({ + type: ORDER_ACTIONS.FETCH_ORDER_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch order', + }); + throw error; + } +}; + +export const createOrder = (orderData) => async (dispatch) => { + dispatch({ type: ORDER_ACTIONS.CREATE_ORDER_REQUEST }); + try { + const data = await ordersApi.createOrder(orderData); + dispatch({ type: ORDER_ACTIONS.CREATE_ORDER_SUCCESS, payload: data }); + return data; + } catch (error) { + dispatch({ + type: ORDER_ACTIONS.CREATE_ORDER_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to create order', + }); + throw error; + } +}; + +export const updateOrder = (id, orderData) => async (dispatch) => { + dispatch({ type: ORDER_ACTIONS.UPDATE_ORDER_REQUEST }); + try { + const data = await ordersApi.updateOrder(id, orderData); + dispatch({ type: ORDER_ACTIONS.UPDATE_ORDER_SUCCESS, payload: { id, data } }); + return data; + } catch (error) { + dispatch({ + type: ORDER_ACTIONS.UPDATE_ORDER_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to update order', + }); + throw error; + } +}; + +export const deleteOrder = (id) => async (dispatch) => { + dispatch({ type: ORDER_ACTIONS.DELETE_ORDER_REQUEST }); + try { + await ordersApi.deleteOrder(id); + dispatch({ type: ORDER_ACTIONS.DELETE_ORDER_SUCCESS, payload: id }); + return id; + } catch (error) { + dispatch({ + type: ORDER_ACTIONS.DELETE_ORDER_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to delete order', + }); + throw error; + } +}; + +export const searchOrders = (query, params = {}) => async (dispatch) => { + dispatch({ type: ORDER_ACTIONS.SEARCH_ORDERS_REQUEST }); + try { + const data = await ordersApi.searchOrders(query, params); + dispatch({ type: ORDER_ACTIONS.SEARCH_ORDERS_SUCCESS, payload: data }); + return data; + } catch (error) { + dispatch({ + type: ORDER_ACTIONS.SEARCH_ORDERS_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to search orders', + }); + throw error; + } +}; + +export const clearOrderError = () => ({ + type: ORDER_ACTIONS.CLEAR_ORDER_ERROR, +}); + +export const clearCurrentOrder = () => ({ + type: ORDER_ACTIONS.CLEAR_CURRENT_ORDER, +}); diff --git a/src/core/redux/actions/productActions.js b/src/core/redux/actions/productActions.js index 8e54ef1..e04c553 100644 --- a/src/core/redux/actions/productActions.js +++ b/src/core/redux/actions/productActions.js @@ -89,6 +89,7 @@ export const createProduct = (productData) => async (dispatch) => { try { const data = await productsApi.createProduct(productData); + console.log('data', data) dispatch({ type: PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS, payload: data, @@ -163,21 +164,6 @@ export const searchProducts = (query, params = {}) => async (dispatch) => { } }; -// Fetch categories -export const fetchCategories = () => async (dispatch) => { - try { - const data = await productsApi.getCategories(); - dispatch({ - type: PRODUCT_ACTIONS.FETCH_CATEGORIES_SUCCESS, - payload: data, - }); - return data; - } catch (error) { - console.error('Failed to fetch categories:', error); - throw error; - } -}; - // Fetch brands export const fetchBrands = () => async (dispatch) => { try { diff --git a/src/core/redux/reducer.jsx b/src/core/redux/reducer.jsx index 8e4aa8f..01cf2c0 100644 --- a/src/core/redux/reducer.jsx +++ b/src/core/redux/reducer.jsx @@ -1,6 +1,9 @@ import { combineReducers } from '@reduxjs/toolkit'; import initialState from "./initial.value"; import productReducer from './reducers/productReducer'; +import authReducer from './reducers/authReducer'; +import categoryReducer from './reducers/categoryReducer'; +import orderReducer from './reducers/orderReducer'; // Legacy reducer for existing functionality const legacyReducer = (state = initialState, action) => { @@ -73,6 +76,9 @@ const legacyReducer = (state = initialState, action) => { const rootReducer = combineReducers({ legacy: legacyReducer, products: productReducer, + auth: authReducer, + categories: categoryReducer, + orders: orderReducer, }); export default rootReducer; diff --git a/src/core/redux/reducers/authReducer.js b/src/core/redux/reducers/authReducer.js new file mode 100644 index 0000000..71d8d05 --- /dev/null +++ b/src/core/redux/reducers/authReducer.js @@ -0,0 +1,59 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + isAuthenticated: false, + user: null, + token: localStorage.getItem('authToken') || null, + loading: false, + error: null +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + loginStart: (state) => { + state.loading = true; + state.error = null; + }, + loginSuccess: (state, action) => { + state.isAuthenticated = true; + state.user = action.payload.user; + state.token = action.payload.token; + state.loading = false; + state.error = null; + // Store token in localStorage + localStorage.setItem('authToken', action.payload.token); + localStorage.setItem('user', JSON.stringify(action.payload.user)); + }, + loginFailure: (state, action) => { + state.loading = false; + state.error = action.payload; + state.isAuthenticated = false; + state.user = null; + state.token = null; + }, + logout: (state) => { + state.isAuthenticated = false; + state.user = null; + state.token = null; + state.loading = false; + state.error = null; + // Clear localStorage + localStorage.removeItem('authToken'); + localStorage.removeItem('user'); + }, + checkAuth: (state) => { + const token = localStorage.getItem('authToken'); + const user = localStorage.getItem('user'); + if (token && user) { + state.isAuthenticated = true; + state.token = token; + state.user = JSON.parse(user); + } + } + } +}); + +export const { loginStart, loginSuccess, loginFailure, logout, checkAuth } = authSlice.actions; +export default authSlice.reducer; \ No newline at end of file diff --git a/src/core/redux/reducers/categoryReducer.js b/src/core/redux/reducers/categoryReducer.js new file mode 100644 index 0000000..aa6ab48 --- /dev/null +++ b/src/core/redux/reducers/categoryReducer.js @@ -0,0 +1,245 @@ +import { CATEGORY_ACTIONS } from '../actions/categoryActions'; + +const initialState = { + // Categories list + categories: [], + totalCategories: 0, + currentPage: 1, + totalPages: 1, + pageSize: 10, + hasPrevious: false, + hasNext: false, + + // Current category (for edit/view) + currentCategory: null, + + // Search results + searchResults: [], + searchQuery: '', + + // Category products + categoryProducts: [], + categoryProductsLoading: false, + categoryProductsError: null, + + // Loading states + loading: false, + categoryLoading: false, + searchLoading: false, + + // Error states + error: null, + categoryError: null, + searchError: null, + + // Operation states + creating: false, + updating: false, + deleting: false, +}; + +const categoryReducer = (state = initialState, action) => { + switch (action.type) { + // Fetch Categories + case CATEGORY_ACTIONS.FETCH_CATEGORIES_REQUEST: + return { + ...state, + loading: true, + error: null, + }; + + case CATEGORY_ACTIONS.FETCH_CATEGORIES_SUCCESS: { + // Handle different API response structures + const { categories, total_count, page, total_pages, limit } = + action.payload.data; + + return { + ...state, + loading: false, + categories: categories, + totalCategories: total_count || categories.length, + currentPage: page || 1, + totalPages: total_pages || 1, + pageSize: limit || 10, + hasPrevious: false, + hasNext: false, + error: null, + }; + } + + case CATEGORY_ACTIONS.FETCH_CATEGORIES_FAILURE: + return { + ...state, + loading: false, + error: action.payload, + }; + + // Fetch Single Category + case CATEGORY_ACTIONS.FETCH_CATEGORY_REQUEST: + return { + ...state, + categoryLoading: true, + categoryError: null, + }; + + case CATEGORY_ACTIONS.FETCH_CATEGORY_SUCCESS: + return { + ...state, + categoryLoading: false, + currentCategory: action.payload.data, + categoryError: null, + }; + + case CATEGORY_ACTIONS.FETCH_CATEGORY_FAILURE: + return { + ...state, + categoryLoading: false, + categoryError: action.payload, + }; + + // Create Category + case CATEGORY_ACTIONS.CREATE_CATEGORY_REQUEST: + return { + ...state, + creating: true, + error: null, + }; + + case CATEGORY_ACTIONS.CREATE_CATEGORY_SUCCESS: + return { + ...state, + creating: false, + categories: [action.payload.data, ...state.categories], + totalCategories: state.totalCategories + 1, + error: null, + }; + + case CATEGORY_ACTIONS.CREATE_CATEGORY_FAILURE: + return { + ...state, + creating: false, + error: action.payload, + }; + + // Update Category + case CATEGORY_ACTIONS.UPDATE_CATEGORY_REQUEST: + return { + ...state, + updating: true, + error: null, + }; + + case CATEGORY_ACTIONS.UPDATE_CATEGORY_SUCCESS: + console.log('state', state) + + return { + ...state, + updating: false, + categories: state.categories.map(category => + category.id === action.payload.data.id ? action.payload.data : category + ), + currentCategory: action.payload.data, + error: null, + }; + + case CATEGORY_ACTIONS.UPDATE_CATEGORY_FAILURE: + return { + ...state, + updating: false, + error: action.payload, + }; + + // Delete Category + case CATEGORY_ACTIONS.DELETE_CATEGORY_REQUEST: + return { + ...state, + deleting: true, + error: null, + }; + + case CATEGORY_ACTIONS.DELETE_CATEGORY_SUCCESS: + return { + ...state, + deleting: false, + categories: state.categories.filter(category => category.id !== action.payload), + totalCategories: state.totalCategories - 1, + error: null, + }; + + case CATEGORY_ACTIONS.DELETE_CATEGORY_FAILURE: + return { + ...state, + deleting: false, + error: action.payload, + }; + + // Search Categories + case CATEGORY_ACTIONS.SEARCH_CATEGORIES_REQUEST: + return { + ...state, + searchLoading: true, + searchError: null, + }; + + case CATEGORY_ACTIONS.SEARCH_CATEGORIES_SUCCESS: + return { + ...state, + searchLoading: false, + searchResults: action.payload.data || action.payload, + searchQuery: action.payload.query || '', + searchError: null, + }; + + case CATEGORY_ACTIONS.SEARCH_CATEGORIES_FAILURE: + return { + ...state, + searchLoading: false, + searchError: action.payload, + }; + + // Category Products + case CATEGORY_ACTIONS.FETCH_CATEGORY_PRODUCTS_REQUEST: + return { + ...state, + categoryProductsLoading: true, + categoryProductsError: null, + }; + + case CATEGORY_ACTIONS.FETCH_CATEGORY_PRODUCTS_SUCCESS: + return { + ...state, + categoryProductsLoading: false, + categoryProducts: action.payload.data || action.payload, + categoryProductsError: null, + }; + + case CATEGORY_ACTIONS.FETCH_CATEGORY_PRODUCTS_FAILURE: + return { + ...state, + categoryProductsLoading: false, + categoryProductsError: action.payload, + }; + + // Clear States + case CATEGORY_ACTIONS.CLEAR_CATEGORY_ERROR: + return { + ...state, + error: null, + categoryError: null, + searchError: null, + categoryProductsError: null, + }; + + case CATEGORY_ACTIONS.CLEAR_CURRENT_CATEGORY: + return { + ...state, + currentCategory: null, + categoryError: null, + }; + + default: + return state; + } +}; + +export default categoryReducer; \ No newline at end of file diff --git a/src/core/redux/reducers/orderReducer.js b/src/core/redux/reducers/orderReducer.js new file mode 100644 index 0000000..0abcbb1 --- /dev/null +++ b/src/core/redux/reducers/orderReducer.js @@ -0,0 +1,211 @@ +import { ORDER_ACTIONS } from '../actions/orderActions'; + +const initialState = { + // Orders list + orders: [], + totalOrders: 0, + currentPage: 1, + totalPages: 1, + pageSize: 10, + hasPrevious: false, + hasNext: false, + + // Current order (for detail/edit) + currentOrder: null, + + // Search results + searchResults: [], + searchQuery: '', + + // Loading states + loading: false, + orderLoading: false, + searchLoading: false, + + // Error states + error: null, + orderError: null, + searchError: null, + + // Operation states + creating: false, + updating: false, + deleting: false, +}; + +const orderReducer = (state = initialState, action) => { + switch (action.type) { + // Fetch Orders + case ORDER_ACTIONS.FETCH_ORDERS_REQUEST: + return { + ...state, + loading: true, + error: null, + }; + + case ORDER_ACTIONS.FETCH_ORDERS_SUCCESS: { + const { orders, total_count, page, total_pages, limit } = action.payload.data; + return { + ...state, + loading: false, + orders: orders, + totalOrders: total_count || orders.length, + currentPage: page || 1, + totalPages: total_pages || 1, + pageSize: limit || 10, + hasPrevious: page > 1, + hasNext: page < total_pages, + error: null, + }; + } + + case ORDER_ACTIONS.FETCH_ORDERS_FAILURE: + return { + ...state, + loading: false, + error: action.payload, + }; + + // Fetch Single Order + case ORDER_ACTIONS.FETCH_ORDER_REQUEST: + return { + ...state, + orderLoading: true, + orderError: null, + }; + + case ORDER_ACTIONS.FETCH_ORDER_SUCCESS: + return { + ...state, + orderLoading: false, + currentOrder: action.payload.data, + orderError: null, + }; + + case ORDER_ACTIONS.FETCH_ORDER_FAILURE: + return { + ...state, + orderLoading: false, + orderError: action.payload, + }; + + // Create Order + case ORDER_ACTIONS.CREATE_ORDER_REQUEST: + return { + ...state, + creating: true, + error: null, + }; + + case ORDER_ACTIONS.CREATE_ORDER_SUCCESS: + return { + ...state, + creating: false, + orders: [action.payload.data, ...state.orders], + totalOrders: state.totalOrders + 1, + error: null, + }; + + case ORDER_ACTIONS.CREATE_ORDER_FAILURE: + return { + ...state, + creating: false, + error: action.payload, + }; + + // Update Order + case ORDER_ACTIONS.UPDATE_ORDER_REQUEST: + return { + ...state, + updating: true, + error: null, + }; + + case ORDER_ACTIONS.UPDATE_ORDER_SUCCESS: + return { + ...state, + updating: false, + orders: state.orders.map(order => + order.id === action.payload.data.id ? action.payload.data : order + ), + currentOrder: action.payload.data, + error: null, + }; + + case ORDER_ACTIONS.UPDATE_ORDER_FAILURE: + return { + ...state, + updating: false, + error: action.payload, + }; + + // Delete Order + case ORDER_ACTIONS.DELETE_ORDER_REQUEST: + return { + ...state, + deleting: true, + error: null, + }; + + case ORDER_ACTIONS.DELETE_ORDER_SUCCESS: + return { + ...state, + deleting: false, + orders: state.orders.filter(order => order.id !== action.payload), + totalOrders: state.totalOrders - 1, + error: null, + }; + + case ORDER_ACTIONS.DELETE_ORDER_FAILURE: + return { + ...state, + deleting: false, + error: action.payload, + }; + + // Search Orders + case ORDER_ACTIONS.SEARCH_ORDERS_REQUEST: + return { + ...state, + searchLoading: true, + searchError: null, + }; + + case ORDER_ACTIONS.SEARCH_ORDERS_SUCCESS: + return { + ...state, + searchLoading: false, + searchResults: action.payload.data || action.payload, + searchQuery: action.payload.query || '', + searchError: null, + }; + + case ORDER_ACTIONS.SEARCH_ORDERS_FAILURE: + return { + ...state, + searchLoading: false, + searchError: action.payload, + }; + + // Clear States + case ORDER_ACTIONS.CLEAR_ORDER_ERROR: + return { + ...state, + error: null, + orderError: null, + searchError: null, + }; + + case ORDER_ACTIONS.CLEAR_CURRENT_ORDER: + return { + ...state, + currentOrder: null, + orderError: null, + }; + + default: + return state; + } +}; + +export default orderReducer; diff --git a/src/core/redux/reducers/productReducer.js b/src/core/redux/reducers/productReducer.js index b0bad74..1636c56 100644 --- a/src/core/redux/reducers/productReducer.js +++ b/src/core/redux/reducers/productReducer.js @@ -1,4 +1,4 @@ -import { PRODUCT_ACTIONS } from '../actions/productActions'; +import { PRODUCT_ACTIONS } from "../actions/productActions"; const initialState = { // Products list @@ -6,7 +6,7 @@ const initialState = { totalProducts: 0, currentPage: 1, totalPages: 1, - pageSize: 20, + pageSize: 10, hasPrevious: false, hasNext: false, @@ -15,7 +15,7 @@ const initialState = { // Search results searchResults: [], - searchQuery: '', + searchQuery: "", // Categories and brands categories: [], @@ -49,20 +49,19 @@ const productReducer = (state = initialState, action) => { case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS: { // Handle different API response structures - const isArrayResponse = Array.isArray(action.payload); - const products = isArrayResponse ? action.payload : (action.payload.data || action.payload.items || []); - const pagination = action.payload.pagination || {}; + const { products, total_count, page, total_pages, limit } = + action.payload.data; return { ...state, loading: false, products: products, - totalProducts: pagination.totalCount || action.payload.total || products.length, - currentPage: pagination.currentPage || action.payload.currentPage || 1, - totalPages: pagination.totalPages || action.payload.totalPages || 1, - pageSize: pagination.pageSize || 20, - hasPrevious: pagination.hasPrevious || false, - hasNext: pagination.hasNext || false, + totalProducts: total_count, + currentPage: page, + totalPages: total_pages, + pageSize: limit, + hasPrevious: false, + hasNext: false, error: null, }; } @@ -86,7 +85,7 @@ const productReducer = (state = initialState, action) => { return { ...state, productLoading: false, - currentProduct: action.payload, + currentProduct: action.payload.data, productError: null, }; @@ -106,10 +105,11 @@ const productReducer = (state = initialState, action) => { }; case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS: + console.log("state", state); + return { ...state, creating: false, - products: [action.payload, ...state.products], totalProducts: state.totalProducts + 1, error: null, }; @@ -133,7 +133,7 @@ const productReducer = (state = initialState, action) => { return { ...state, updating: false, - products: state.products.map(product => + products: state.products.map((product) => product.id === action.payload.id ? action.payload.data : product ), currentProduct: action.payload.data, @@ -159,7 +159,9 @@ const productReducer = (state = initialState, action) => { return { ...state, deleting: false, - products: state.products.filter(product => product.id !== action.payload), + products: state.products.filter( + (product) => product.id !== action.payload + ), totalProducts: state.totalProducts - 1, error: null, }; @@ -184,7 +186,7 @@ const productReducer = (state = initialState, action) => { ...state, searchLoading: false, searchResults: action.payload.data || action.payload, - searchQuery: action.payload.query || '', + searchQuery: action.payload.query || "", searchError: null, }; diff --git a/src/core/redux/store.jsx b/src/core/redux/store.jsx index 80c1b12..bcc9e59 100644 --- a/src/core/redux/store.jsx +++ b/src/core/redux/store.jsx @@ -5,4 +5,5 @@ const store = configureStore({ reducer: rootReducer, }); +export { store }; export default store; diff --git a/src/feature-module/inventory/addproduct.jsx b/src/feature-module/inventory/addproduct.jsx index ea7f3b1..b9f3db9 100644 --- a/src/feature-module/inventory/addproduct.jsx +++ b/src/feature-module/inventory/addproduct.jsx @@ -1,47 +1,89 @@ -import React, { useState } from "react"; -import { Link } from "react-router-dom"; -import Select from "react-select"; -import { all_routes } from "../../Router/all_routes"; import { DatePicker } from "antd"; -import Addunits from "../../core/modals/inventory/addunits"; -import AddCategory from "../../core/modals/inventory/addcategory"; -import AddBrand from "../../core/modals/addbrand"; import { ArrowLeft, Calendar, ChevronDown, ChevronUp, + Image, Info, LifeBuoy, List, + Plus, PlusCircle, Trash2, - X, + X } from "feather-icons-react/build/IconComponents"; -import { useDispatch, useSelector } from "react-redux"; -import { setToogleHeader } from "../../core/redux/action"; +import { useEffect, useState } from "react"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import { useDispatch, useSelector } from "react-redux"; +import { Link, useNavigate } from "react-router-dom"; +import Select from "react-select"; +import Swal from "sweetalert2"; import ImageWithBasePath from "../../core/img/imagewithbasebath"; +import AddBrand from "../../core/modals/addbrand"; +import AddCategory from "../../core/modals/inventory/addcategory"; +import Addunits from "../../core/modals/inventory/addunits"; +import { setToogleHeader } from "../../core/redux/action"; +import { createProduct } from "../../core/redux/actions/productActions"; +import { all_routes } from "../../Router/all_routes"; +import { categoriesApi } from "../../services/categoriesApi"; const AddProduct = () => { const route = all_routes; const dispatch = useDispatch(); + const navigate = useNavigate(); const data = useSelector((state) => state.toggle_header); + const { creating } = useSelector((state) => state.products); const [selectedDate, setSelectedDate] = useState(new Date()); - const handleDateChange = (date) => { - setSelectedDate(date); - }; const [selectedDate1, setSelectedDate1] = useState(new Date()); - const handleDateChange1 = (date) => { - setSelectedDate1(date); - }; const renderCollapseTooltip = (props) => ( Collapse ); + + const [category, setCategory] = useState([]); + const [formData, setFormData] = useState({ + name: "", + sku: "", + description: "", + price: 0, + cost: 0, + category_id: "", + }); + + const [variants, setVariants] = useState([ + { name: "", price_modifier: 0, cost: 0 }, + ]); + + const handleInputChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + useEffect(() => { + const loadCategories = async () => { + try { + const response = await categoriesApi.getAllCategories(); + const categories = response?.data?.categories; + const formattedCategory = categories.map((item) => ({ + value: item.id, + label: item.name, + })); + + setCategory(formattedCategory); + } catch (error) { + console.error("Failed to fetch categories", error); + } + }; + + loadCategories(); + }, []); + const store = [ { value: "choose", label: "Choose" }, { value: "thomas", label: "Thomas" }, @@ -54,21 +96,11 @@ const AddProduct = () => { { value: "determined", label: "Determined" }, { value: "sincere", label: "Sincere" }, ]; - const category = [ - { value: "choose", label: "Choose" }, - { value: "lenovo", label: "Lenovo" }, - { value: "electronics", label: "Electronics" }, - ]; const subcategory = [ { value: "choose", label: "Choose" }, { value: "lenovo", label: "Lenovo" }, { value: "electronics", label: "Electronics" }, ]; - const subsubcategories = [ - { value: "Fruits", label: "Fruits" }, - { value: "Computer", label: "Computer" }, - { value: "Shoes", label: "Shoes" }, - ]; const brand = [ { value: "choose", label: "Choose" }, { value: "nike", label: "Nike" }, @@ -114,6 +146,128 @@ const AddProduct = () => { const handleRemoveProduct1 = () => { setIsImageVisible1(false); }; + + // Handle form submission + const handleSubmit = async (e) => { + e.preventDefault(); + + // Basic validation + if (!formData.name.trim()) { + Swal.fire({ + icon: "error", + title: "Validation Error", + text: "Product name is required!", + }); + return; + } + + if (!formData.category_id) { + Swal.fire({ + icon: "error", + title: "Validation Error", + text: "Please select a category!", + }); + return; + } + + if (formData.price <= 0) { + Swal.fire({ + icon: "error", + title: "Validation Error", + text: "Price must be greater than 0!", + }); + return; + } + + try { + // Prepare the data for submission + const productData = { + ...formData, + variants + }; + + // Remove empty values + const cleanData = Object.fromEntries( + Object.entries(productData).filter(([, value]) => { + if (value === null || value === undefined) return false; + if (typeof value === "string" && value.trim() === "") return false; + if (Array.isArray(value) && value.length === 0) return false; + return true; + }) + ); + + cleanData.price = Number(cleanData.price); + cleanData.cost = Number(cleanData.cost); + + // Dispatch the create product action + await dispatch(createProduct(cleanData)); + + // Show success message + Swal.fire({ + icon: "success", + title: "Success!", + text: "Product created successfully!", + showConfirmButton: false, + timer: 1500, + }); + + // Navigate to product list + setTimeout(() => { + navigate(route.productlist); + }, 1500); + } catch (error) { + console.error("Error creating product:", error); + + // Show error message + Swal.fire({ + icon: "error", + title: "Error!", + text: error.message || "Failed to create product. Please try again.", + }); + } + }; + + // Handle select changes + const handleSelectChange = (field, selectedOption) => { + setFormData({ + ...formData, + [field]: selectedOption ? selectedOption.value : "", + }); + }; + + // Handle date changes + const handleDateChange = (date) => { + setSelectedDate(date); + setFormData({ + ...formData, + manufactured_date: date, + }); + }; + + const handleDateChange1 = (date) => { + setSelectedDate1(date); + setFormData({ + ...formData, + expiry_date: date, + }); + }; + + const handleChangeVariant = (index, field, value) => { + const newVariants = [...variants]; + if (['price_modifier', 'cost'].includes(field)) value = Number(value); + newVariants[index][field] = value; + setVariants(newVariants); + }; + + const addVariant = () => { + setVariants([...variants, { name: "", price_modifier: 0, cost: 0 }]); + }; + + const removeVariant = (index) => { + const newVariants = variants.filter((_, i) => i !== index); + setVariants(newVariants); + }; + return (
@@ -127,10 +281,13 @@ const AddProduct = () => {
  • - +
  • @@ -152,9 +309,9 @@ const AddProduct = () => {
{/* /add */} -
+
-
+
{ >
-
+
{
+
-
+
- -
-
-
-
- - -
-
-
-
- +
+
+
+
+ + - - Generate Code -
+ +
+
+ +
+ + +
+
+
+
+ + { />
-
-
- - -
-
+
@@ -352,6 +538,32 @@ const AddProduct = () => {
+ +
+ + +
+
+ {/*
{ Generate Code
-
+
*/}
{/* Editor */}
- +